From eef61aa4dae59322d509c96c29a12c262fe2b8be Mon Sep 17 00:00:00 2001 From: josdejong Date: Mon, 3 Jun 2013 12:11:58 +0200 Subject: [PATCH] Initial import of Graph (formerly Network visualization) --- Jakefile.js | 1 + examples/graph/01_basic_usage.html | 45 + examples/graph/02_random_nodes.html | 105 + examples/graph/03_images.html | 78 + examples/graph/04_shapes.html | 73 + examples/graph/05_social_network.html | 96 + examples/graph/06_groups.html | 140 + examples/graph/08_selections.html | 67 + examples/graph/16_scalable_images.html | 82 + examples/graph/19_dot_language.html | 37 + .../graph/20_dot_language_playground.html | 212 + .../img/refresh-cl/Hardware-Fax-icon.png | Bin 0 -> 3645 bytes .../img/refresh-cl/Hardware-Laptop-1-icon.png | Bin 0 -> 3781 bytes .../refresh-cl/Hardware-Mobile-Phone-icon.png | Bin 0 -> 3463 bytes .../Hardware-My-Computer-3-icon.png | Bin 0 -> 5402 bytes .../refresh-cl/Hardware-My-PDA-02-icon.png | Bin 0 -> 3531 bytes .../refresh-cl/Hardware-My-PDA-04-icon.png | Bin 0 -> 4153 bytes .../refresh-cl/Hardware-My-PDA-05-icon.png | Bin 0 -> 4131 bytes .../Hardware-My-Phone-Picture-icon.png | Bin 0 -> 3234 bytes .../refresh-cl/Hardware-Printer-Blue-icon.png | Bin 0 -> 3478 bytes .../refresh-cl/Misc-Scanner-default-icon.png | Bin 0 -> 3168 bytes .../img/refresh-cl/Network-Drive-icon.png | Bin 0 -> 3320 bytes .../Network-Internet-Connection-icon.png | Bin 0 -> 4320 bytes .../img/refresh-cl/Network-Pipe-icon.png | Bin 0 -> 1000 bytes .../img/refresh-cl/System-Firewall-2-icon.png | Bin 0 -> 4668 bytes .../img/refresh-cl/System-Globe-icon.png | Bin 0 -> 4595 bytes examples/graph/img/refresh-cl/license.txt | 14 + .../img/soft-scraps-icons/Document-icon24.png | Bin 0 -> 1104 bytes .../img/soft-scraps-icons/Document-icon32.png | Bin 0 -> 2771 bytes .../img/soft-scraps-icons/Document-icon48.png | Bin 0 -> 2771 bytes .../img/soft-scraps-icons/Email-icon24.png | Bin 0 -> 668 bytes .../img/soft-scraps-icons/Email-icon32.png | Bin 0 -> 873 bytes .../img/soft-scraps-icons/Email-icon48.png | Bin 0 -> 1487 bytes .../img/soft-scraps-icons/Folder-icon24.png | Bin 0 -> 691 bytes .../img/soft-scraps-icons/Folder-icon32.png | Bin 0 -> 874 bytes .../img/soft-scraps-icons/Folder-icon48.png | Bin 0 -> 1506 bytes .../img/soft-scraps-icons/Folder-icon64.png | Bin 0 -> 1771 bytes .../soft-scraps-icons/Smiley-Angry-icon.png | Bin 0 -> 3210 bytes .../soft-scraps-icons/Smiley-Grin-icon.png | Bin 0 -> 3917 bytes .../User-Administrator-Blue-icon.png | Bin 0 -> 3901 bytes .../User-Administrator-Green-icon.png | Bin 0 -> 3906 bytes .../soft-scraps-icons/User-Coat-Blue-icon.png | Bin 0 -> 3559 bytes .../User-Coat-Green-icon.png | Bin 0 -> 3571 bytes .../soft-scraps-icons/User-Coat-Red-icon.png | Bin 0 -> 3544 bytes .../User-Executive-Green-icon.png | Bin 0 -> 3805 bytes .../User-Preppy-Blue-icon.png | Bin 0 -> 3802 bytes .../User-Preppy-Red-icon.png | Bin 0 -> 3800 bytes .../graph/img/soft-scraps-icons/license.txt | 12 + src/graph/graph.js | 5876 ++++++++++++++++ src/module/exports.js | 3 +- vis.js | 5973 ++++++++++++++++- vis.min.js | 8 +- 52 files changed, 12771 insertions(+), 51 deletions(-) create mode 100644 examples/graph/01_basic_usage.html create mode 100755 examples/graph/02_random_nodes.html create mode 100755 examples/graph/03_images.html create mode 100755 examples/graph/04_shapes.html create mode 100644 examples/graph/05_social_network.html create mode 100644 examples/graph/06_groups.html create mode 100644 examples/graph/08_selections.html create mode 100644 examples/graph/16_scalable_images.html create mode 100644 examples/graph/19_dot_language.html create mode 100644 examples/graph/20_dot_language_playground.html create mode 100644 examples/graph/img/refresh-cl/Hardware-Fax-icon.png create mode 100644 examples/graph/img/refresh-cl/Hardware-Laptop-1-icon.png create mode 100644 examples/graph/img/refresh-cl/Hardware-Mobile-Phone-icon.png create mode 100644 examples/graph/img/refresh-cl/Hardware-My-Computer-3-icon.png create mode 100644 examples/graph/img/refresh-cl/Hardware-My-PDA-02-icon.png create mode 100644 examples/graph/img/refresh-cl/Hardware-My-PDA-04-icon.png create mode 100644 examples/graph/img/refresh-cl/Hardware-My-PDA-05-icon.png create mode 100644 examples/graph/img/refresh-cl/Hardware-My-Phone-Picture-icon.png create mode 100644 examples/graph/img/refresh-cl/Hardware-Printer-Blue-icon.png create mode 100644 examples/graph/img/refresh-cl/Misc-Scanner-default-icon.png create mode 100644 examples/graph/img/refresh-cl/Network-Drive-icon.png create mode 100644 examples/graph/img/refresh-cl/Network-Internet-Connection-icon.png create mode 100644 examples/graph/img/refresh-cl/Network-Pipe-icon.png create mode 100644 examples/graph/img/refresh-cl/System-Firewall-2-icon.png create mode 100644 examples/graph/img/refresh-cl/System-Globe-icon.png create mode 100644 examples/graph/img/refresh-cl/license.txt create mode 100644 examples/graph/img/soft-scraps-icons/Document-icon24.png create mode 100644 examples/graph/img/soft-scraps-icons/Document-icon32.png create mode 100644 examples/graph/img/soft-scraps-icons/Document-icon48.png create mode 100644 examples/graph/img/soft-scraps-icons/Email-icon24.png create mode 100644 examples/graph/img/soft-scraps-icons/Email-icon32.png create mode 100644 examples/graph/img/soft-scraps-icons/Email-icon48.png create mode 100644 examples/graph/img/soft-scraps-icons/Folder-icon24.png create mode 100644 examples/graph/img/soft-scraps-icons/Folder-icon32.png create mode 100644 examples/graph/img/soft-scraps-icons/Folder-icon48.png create mode 100644 examples/graph/img/soft-scraps-icons/Folder-icon64.png create mode 100644 examples/graph/img/soft-scraps-icons/Smiley-Angry-icon.png create mode 100644 examples/graph/img/soft-scraps-icons/Smiley-Grin-icon.png create mode 100644 examples/graph/img/soft-scraps-icons/User-Administrator-Blue-icon.png create mode 100644 examples/graph/img/soft-scraps-icons/User-Administrator-Green-icon.png create mode 100644 examples/graph/img/soft-scraps-icons/User-Coat-Blue-icon.png create mode 100644 examples/graph/img/soft-scraps-icons/User-Coat-Green-icon.png create mode 100644 examples/graph/img/soft-scraps-icons/User-Coat-Red-icon.png create mode 100644 examples/graph/img/soft-scraps-icons/User-Executive-Green-icon.png create mode 100644 examples/graph/img/soft-scraps-icons/User-Preppy-Blue-icon.png create mode 100644 examples/graph/img/soft-scraps-icons/User-Preppy-Red-icon.png create mode 100644 examples/graph/img/soft-scraps-icons/license.txt create mode 100644 src/graph/graph.js diff --git a/Jakefile.js b/Jakefile.js index 5d696bee..d0b3052d 100644 --- a/Jakefile.js +++ b/Jakefile.js @@ -66,6 +66,7 @@ task('build', {async: true}, function () { './src/component/groupset.js', './src/timeline.js', + './src/graph/graph.js', './src/module/exports.js' ], diff --git a/examples/graph/01_basic_usage.html b/examples/graph/01_basic_usage.html new file mode 100644 index 00000000..a858738d --- /dev/null +++ b/examples/graph/01_basic_usage.html @@ -0,0 +1,45 @@ + + + + Graph | Basic usage + + + + + + +
+ + + + + diff --git a/examples/graph/02_random_nodes.html b/examples/graph/02_random_nodes.html new file mode 100755 index 00000000..4471d474 --- /dev/null +++ b/examples/graph/02_random_nodes.html @@ -0,0 +1,105 @@ + + + + + Graph | Random nodes + + + + + + + + + + +
+ + + +
+
+ +
+ +

+ + diff --git a/examples/graph/03_images.html b/examples/graph/03_images.html new file mode 100755 index 00000000..f9765a42 --- /dev/null +++ b/examples/graph/03_images.html @@ -0,0 +1,78 @@ + + + + Graph | Images + + + + + + + + + + +
+ + + diff --git a/examples/graph/04_shapes.html b/examples/graph/04_shapes.html new file mode 100755 index 00000000..c8833b9b --- /dev/null +++ b/examples/graph/04_shapes.html @@ -0,0 +1,73 @@ + + + + Graph | Shapes + + + + + + + + + +
+ +
+ + diff --git a/examples/graph/05_social_network.html b/examples/graph/05_social_network.html new file mode 100644 index 00000000..41c00711 --- /dev/null +++ b/examples/graph/05_social_network.html @@ -0,0 +1,96 @@ + + + + Graph | Social Network + + + + + + + + + + +
+

+ Icons: Scrap Icons by Deleket +

+ +
+ + diff --git a/examples/graph/06_groups.html b/examples/graph/06_groups.html new file mode 100644 index 00000000..8fdefd92 --- /dev/null +++ b/examples/graph/06_groups.html @@ -0,0 +1,140 @@ + + + + Graph | Groups + + + + + + + + + + +
+ Number of groups: + + Number of nodes per group: + + +
+
+ +
+ +
+ + diff --git a/examples/graph/08_selections.html b/examples/graph/08_selections.html new file mode 100644 index 00000000..8e8985f3 --- /dev/null +++ b/examples/graph/08_selections.html @@ -0,0 +1,67 @@ + + + + Graph | Selections + + + + + + +
+
+ + + + + diff --git a/examples/graph/16_scalable_images.html b/examples/graph/16_scalable_images.html new file mode 100644 index 00000000..498aba88 --- /dev/null +++ b/examples/graph/16_scalable_images.html @@ -0,0 +1,82 @@ + + + + Graph | Scalable images + + + + + + + + + +
+ +
+ + diff --git a/examples/graph/19_dot_language.html b/examples/graph/19_dot_language.html new file mode 100644 index 00000000..06db5d67 --- /dev/null +++ b/examples/graph/19_dot_language.html @@ -0,0 +1,37 @@ + + + + Graph | DOT Language + + + + + + +
+ + + + diff --git a/examples/graph/20_dot_language_playground.html b/examples/graph/20_dot_language_playground.html new file mode 100644 index 00000000..85985865 --- /dev/null +++ b/examples/graph/20_dot_language_playground.html @@ -0,0 +1,212 @@ + + + + Graph | DOT language playground + + + + + + + + + + + + + + + + + +
+

Directed graph from data in DOT language

+ +
+
+ + +
+
+ + +
+
+ + + + + + + + + + + + + diff --git a/examples/graph/img/refresh-cl/Hardware-Fax-icon.png b/examples/graph/img/refresh-cl/Hardware-Fax-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..eab07c7e2b492d183e796d5bbc4c14f398f8eac0 GIT binary patch literal 3645 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 literal 0 HcmV?d00001 diff --git a/examples/graph/img/refresh-cl/Hardware-My-Computer-3-icon.png b/examples/graph/img/refresh-cl/Hardware-My-Computer-3-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..26ac6260791799717be9893fcffaa4d4edf3cbdb GIT binary patch literal 5402 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 Ef_{oenE(I) literal 0 HcmV?d00001 diff --git a/examples/graph/img/refresh-cl/Hardware-My-PDA-02-icon.png b/examples/graph/img/refresh-cl/Hardware-My-PDA-02-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..a761307d25ed268636a98d310b74555559f436a9 GIT binary patch literal 3531 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 literal 0 HcmV?d00001 diff --git a/examples/graph/img/refresh-cl/Hardware-My-Phone-Picture-icon.png b/examples/graph/img/refresh-cl/Hardware-My-Phone-Picture-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..5e9ea6c56f7ee92297ac2953e22d0f9988c7f0b1 GIT binary patch literal 3234 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 O0000>P)i`S}gIuRu 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 literal 0 HcmV?d00001 diff --git a/examples/graph/img/refresh-cl/license.txt b/examples/graph/img/refresh-cl/license.txt new file mode 100644 index 00000000..7b8b9d43 --- /dev/null +++ b/examples/graph/img/refresh-cl/license.txt @@ -0,0 +1,14 @@ +Refresh Cl icon set + +http://www.iconarchive.com/show/refresh-cl-icons-by-tpdkdesign.net.html +http://www.iconarchive.com/artist/tpdkdesign.net.html + +Artist: TpdkDesign.net +License: Free for non-commercial use. + +Name: TpdkDesign.net +URL: http://www.tpdkdesign.net +Available for custom work: No +Default License: Free for non-commercial use. +Commercial usage: Not allowed + diff --git a/examples/graph/img/soft-scraps-icons/Document-icon24.png b/examples/graph/img/soft-scraps-icons/Document-icon24.png new file mode 100644 index 0000000000000000000000000000000000000000..c420b202ed4b113b4ed87575181c8034b95b29dc GIT binary patch literal 1104 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 Z{tXy76{%(M^g;jt002ovPDHLkV1kG0M@j$y literal 0 HcmV?d00001 diff --git a/examples/graph/img/soft-scraps-icons/Document-icon48.png b/examples/graph/img/soft-scraps-icons/Document-icon48.png new file mode 100644 index 0000000000000000000000000000000000000000..5938c512fbe77d44642dda1cab861ba8c3d851a2 GIT binary patch literal 2771 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 Z{tXy76{%(M^g;jt002ovPDHLkV1kG0M@j$y literal 0 HcmV?d00001 diff --git a/examples/graph/img/soft-scraps-icons/Email-icon24.png b/examples/graph/img/soft-scraps-icons/Email-icon24.png new file mode 100644 index 0000000000000000000000000000000000000000..c01c9040ecb5f5567275dd9da81d3c18db7bb40f GIT binary patch literal 668 zcmV;N0%QG&P)z34j@%(E-J`BtZH8 z4QP=NL=U4NyLWx8f2Rwqe85i|p!61+-hxnEC-x9@gx$Li@{u-Mw3i0X&A0%KZ^&+c z1;!&T{(p*Y#euF4Brc+21DHjYC=5|5f^s8Hq9b8B`W;yF-jA5f=U24(?TEDB*Y zLFG+gLO&Tm-Qb`aL?{AkT5L~y0Bgv`+&Ze)fa(r|@nX&EAa4MgOFw)ys2yAX-E%ixIzu!t<*VuZK?6y>naoi?zx$`LX) zI3mA+>$li9eAu_kAe{z3*;b#rXHZ{&rZP2!f?I`@%c$9#A~o#vqM%m6tjT1lQ*#c3 z@5Tbgld<3r`t3#b8MU|<&EbdKTzdH&51-BUcd`C9=2_Ag^H`PulPt~a**x;Y>glxW znh9Fnz?ZJTJ#SO?(P?=ur$oTyS>U;UHvY*29e)6g0VL7EWag3p0000jt+nzW0hPEBe_rsJs7KhD|NG~O>`kQ?^EoW0imzrEJl>l`mk@$i}k zmezkk%~#B78g}ZqopljKKCh1)!u5QdS!9 zy#~+)POf+t6}_W@i#LJn-v9-Wwq*dV7NE0%5FwKzZtVe75r8@vkgpM7p8z;XfOMx( z(?=S$19L(rR=l%|KS6*d-1%R=PJjaf;5Y#`^c%l@4TLfk;S&@IE5&V`LHhn@@(ltU z5&%aDu-?_Fnnf}{h9Cjs7jHKMVbQM7kNx-_eP8+JG<5fO+w(DV6)WTuy)!0$@J@ zQr(Rh1eo3cjm<#vMpmzCI9Uapj&%=tYW9`b*n#`Ke3t;H1VBCkR=FEBGy&Qu=J5w$ ztR_SjPh%fiyq7!HRsfOAxym1~d;sR^1^~E60D}O?Bfv^m0}D?b!aM7ybVmyd$@B?W zwwWgy1j5eUOw6rb3k1({KC`k8+CK9jVSfSKCqSV9*h_#F{fpn)0Dn68sa>7w>i{!z zo4CmxqEEbx)y_0&BVE_5^yz2;X8dujjeqD@&jRy-e>$vz_hb`xMZE7!_C%?70Q9y-rPR(I1^ zhB|=|+@YCJI(E3Yr9=DKc&mEYWi`Ck@*@JA6#zRSZAoTWdDVP|5f{VUNQinTfSw`O z>PF_*O$a-i)8)qmIHv%d&xN#=DexYl_;`n-zg%Dnb`&`QYKw9Zlat~ZaWRSskXwv8 z2f)?Ui;-ED@3{{|#|2tr04l0b>d;)oS(%xLplsHG|0rATXh#S0U&8#CJ_p+ylEk4| z&ma?s0QH!6AK74601?2LJ53r500000NkvXXu0mjf-=}-n literal 0 HcmV?d00001 diff --git a/examples/graph/img/soft-scraps-icons/Email-icon48.png b/examples/graph/img/soft-scraps-icons/Email-icon48.png new file mode 100644 index 0000000000000000000000000000000000000000..799ebb70bfe43285841801bb4b70ea7899e4fce7 GIT binary patch literal 1487 zcmV;=1u*)FP)leV_qYTI?LcFJhGx>3u7DXSfbf`}BIhz#Z!RQ#uZL{a=D{t>4l zg2-^x3o3R9Zrwz-sk^AV)Xudqtz(^?y(CSVq_^bwd|#U8%a^1{)=OH*3x~cvIp;g? z^F7b^zV8uO<0euaKpH@*14si%bpUAq9Q!%SQXR-`+K>X^0!O_DkX*pm&Ly%w*=Hu} zUkU-^qvOlihwMw(pP~=1K=fq zTn0Yw0<4>L60-LIg#{2Z(~y@>0*%!W!nZmYfB|A+5;Ct2$jF3nj6+(t=p4GrdMfn>8^A0BgH0UMt8v*2pD6KU(g2-P7xv7G4a+w(r zkWL3MI|5JqU`{izl_^sfazIY>4d0cl`UKk zOiV-Ex(d0mHUS16*GuTD*Z`D&um8f+2oJU*cRI$ z%4$Quk9!~;|L{ZJpvgkf06d|W(3i0#^zTiUbG{*qF;Y^$vU_#%@6NsKxe{zS{A8?` zvdQE64O=56<775+^UU({1tj#OUP7O%03zPjD0`x;;=r4a)=K!9pHCfS@Z}IKPlq>u zQ5ndfY$#XUg1Bb~*NfAfM@^CwcS=)^&656Ae(4i0mlMF`Y z@_uYg?n0x~h!d>VnJkDk5Avh0o}0f8d6SnU>4s=-<9jJc?n2|8WC!78XPT>F;90$d zJ_!Jro`RgsWPUeA_C>n>D56JEeAhT|?j#SPelz%V4q>3~ZPapo3q+nJ_A?jqJ)i?S(JJ;RJzFJRrP zlhDYA)H@l-GRE&)Vo7MZ?!;PuhH`R|f#Ur-2@QY5a&Afduv7t1{1M)XOZJ5ToaNYd z;~=o!wl)9^jzLy*s3DwJ0KCs};9Y*z9j%Mg=t|kaZs3JOs{4TgfW;hVtR`f3zXY+h zdUZ4K`v64iTab1Sn3K>PY(w7Qc*T?r@eB*mc3$8O@p{@7HXp-3%MSQ3Z)~1v70jC) zuLi?Z{=UfZI!C@{PU?K^T&y+8`pNENkAT-W-a&xDFe~OzE^9T2f3H|+Qvi?y@bZfh p?HgRvmIjb&O9M!?r2(Yc{s$l2wJRpk{*eFx002ovPDHLkV1gzKz-Ryf literal 0 HcmV?d00001 diff --git a/examples/graph/img/soft-scraps-icons/Folder-icon24.png b/examples/graph/img/soft-scraps-icons/Folder-icon24.png new file mode 100644 index 0000000000000000000000000000000000000000..16d050846ffe218089d2306d6064ce8bdf2e6718 GIT binary patch literal 691 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 literal 0 HcmV?d00001 diff --git a/examples/graph/img/soft-scraps-icons/Folder-icon64.png b/examples/graph/img/soft-scraps-icons/Folder-icon64.png new file mode 100644 index 0000000000000000000000000000000000000000..6c5d457aa6d13f68f28253ec4180b051899f6e67 GIT binary patch literal 1771 zcmV;P)>{9t5Old5y2RZR3ro>h!=`cV>}26m|!$2cp%D6 zjDXRYpaBgPu}U~pVuC0djP26xwQV`vw7c^)}Vk6#z`4 zwo-j8uJ<@KgZfP-fSahjR1c9UDWOMq0=@FhsmxmgZ2uTIawJ|1?nZCGO?0MeHccqQA?;5B2!?tA`*w^qw-i~$fqAENq-%xF#zGdU>@DbRXNTFifz$^at51HTOe zavUfrk^q1DOU?jSBp6|XZ@ zyqQOmLUTQ+-^Zb_9BaxHRQ_N?|9m?N^K8g*D#+&MD2rQzOuK>%a&ZcACT12ko1$-B zm8k4CC^m5+A;sQ4ugSr`6F0HxQ~IbCH8^q72*8_6jbRVSEHD><7zT%*L&#Z)FMd++ z%T61z6$QDRZZrrE9YTg93c;qupvMyfpamN()agWjD;gN>dhA-XToE_nwTY|hP<`46 zz)Ge@avWs1%mpAU*u>)qiK|}tx!W#LakoCXP8wLNA;f# z**H>`gN0ig;cGSmz!eI^xcv3#WetFiruxmfZ@>rCmI0yT*zd{3^cPyu*wPU|-zb2g zk8#lD$pK)haow-nh8EQW_X9vfb7CKm0I-@V_dEjVDcf6ftgQ<0eu_blbI+sibUOfe zkEwy&4mxvXyPAFz^Gow_&N=09Kf=!7GXn5FQ~mGanj>$Yp5CC@&HZfZ)=Itg!y&EM zy)YGkwM-RG=29W&l$M^LBV1w`T^m;wzc(Iqvbh%+0Vs$9aE-qN0YvnDMS!jCuQ3ez z902u30C*TeZ=Qv1nX)6gFbna78Du1Z`hN|&9f0|%0Ekr25$pk76oBa7Y72vI)D;PK z>NaAbGX}7UshsQB1Jd*iAYG=m0cTIL#ZR%NI~R020CQ6T5Kl|9uO+y13e! z_LfcwV0J11A2H>0nfEv>9yE7lVZSbMBDC1tvr+-r#*`!98~|F-L4D?y5Og~LrA7ey z5x{l8+?xu( zXNc|rI`O_vw6HjQbkU&O0k}IAfE}XD5r3w3{(vLUT3ZV^B0Hvw12Dq~09U^X@oH3b z|C9hC(O6@_Uz+BgP5?DV0CtKpqu?*yU=0CGO9kLd*{)dxV2Tld{si!qY}YITFqr_V zjR2I%cFiIH6H@`$Bil8L08B6fP(%QGWxHk(fD!`m8UZMm?V3dZZc72+ukXY=L2KXD zTW=}`-`ZpVU;qK^(ym|L#`YaNl|7)!06@h-+1^=W?y+$ITLgd)s0A+ee&12qu32M! zuLl~MwB%X=U=gC9t{&h{aO_Cg?wM-`4+G<-#IsKb0Neqff^rtKYkbWkaYh$=DxNM2 z1c5tv6yB!~b@2ew9Xt7mk7J$`iAg1#6jlR8{bakFUK5|j&U+Nty*FO8idq~4AYKK1 zLyZ#|@frL0VnRsgx8gE`=fUUOeQff?L+1n3?UX?_`hiwSX-Y==!Y@$CB{szqds z4*z4Q2ch?d%jH`c@lR8t)x}4lhmtSI{--JF)(t?m1G23ffNTe3+dmSwJ_Hi#dGG)L N002ovPDHLkV1fXtCQ1MR literal 0 HcmV?d00001 diff --git a/examples/graph/img/soft-scraps-icons/Smiley-Angry-icon.png b/examples/graph/img/soft-scraps-icons/Smiley-Angry-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..f40bc381b951af8fc418a9816b33dce90fbba030 GIT binary patch literal 3210 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 literal 0 HcmV?d00001 diff --git a/examples/graph/img/soft-scraps-icons/User-Administrator-Blue-icon.png b/examples/graph/img/soft-scraps-icons/User-Administrator-Blue-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..ba2ffac73718abfc5a1d4e0d3dfcaa4201fea7b6 GIT binary patch literal 3901 zcmV-D55n+?P)1f`GteSdQ1 z%sG4Zf7ZYLz4kr`=dR^4W-(?nrZ6fQb_O!SjAq7RMjd0Xaqbg0JO_PT$oLxL8ip&a zUqaNnN8*-#o)KWY%=jr||G$dBM8-iO(w-}CmF9YRxpnIvj~_O-(aj|*sM8r@RpATcVQ`@$qZJj1w1Yn zIeW1Z<3F8B5S)g2M7D z^c0tqS(3n;@4=GdLh`s>A{;5Hq<%v?R@Ef>{Q+w2=!C9rKPDB0{OzZ(^VD%7RTY=K z$hgTMbj}Hggxt(nV3Bj6bnGOMZMm3hw^KpB8zjlXeWkRHXFOIE1(^he`yv>NgsJtT z4Mn|5;h8O2~t!j*}m8qN@o$e=+00Cm|rd z-@%xhmtO=nrwa^k|5#17nBfP@s^RTJ?eM$3C$nB7ri@510KMHE;^R)ng0u&4)&wqN zY&Bb~CU;ROq5jxu(ETHdKt-Vwp16BH&YW04y}>Xny8l%?-WH^6*K#ewP47C%f?VI*`UW6*e_ z9j;%y1^#xbCu<);e;0>C6&XE>(Q&o}zRdWQWRcnL?MR}w5p_*k1kyjF@$^QO7QnT& zpTQfhnoXY?Rcu@%RFM=l1xFyEMF5jAq-2x=JpugAhMn~M_9KwwBu?)y7U^Sm(j4h4 zjOWjmz!Qu+ELIszl9cHf{Wl|)7rCfa&tZYVebj@hvHiID$mpj-BO~zf7!UJtkUs`s{B2vX*%u+1XZmO?F2>o(xR$Z&bOa_a_A%TmZn_+9`oese zam{z&w%Rf9#9cK!XeWS$VRi6-E{!B6BH7X+UKfOs!_?eRKzYHJf5c=`MA>`Oaf{j;x$oiv*GZ#Og9qR=gCjcKS14*oK z4kXhf5aN8`xq%66dh-Y_{o%XRymjH)y9nDlvoqku|ObD>NL zr3eJ!(9x6lndRH5b;BI+R8GSf$uxP{&*6A$ubA8_7;S?Ie2($T9al_*pWeyK+E_RL zP83RS-2n|xEQb67_Ht{UE}wo0F)S=0bDD&Ne1z zxV)!{fSeWq^B@Vqz;S;BgpCRb#q3i z579{Gmj)BKL}d82vr1^w@@l}`VkDcF);{+R+DWh*>E9^fPH{~Ipso}1E`nT z{S#6;0g(gRJdntAAtl{~1c4s#cXi<7+XJ*}*-XHQ4=LPEOV%HNm)>bZ!zte$Oh6X8 za^#rZ4L_O*6@@&=vf1zfJ1?Hv1=Ge?0_5bgWE_wppvlNATu9a>WJ=8gS!5(i32iP= z2F(LmoG9822ao=pE5LbhUMb((QU@nHLon%^`yd=m_89pZ%jQDgWvsqrvJ1A{I}M#S zGl9(up-`A5R6uAi)?LV=4<02OM<8b)0-6hn2$-|X1FAj`Xr%;4Kutv;s=1LMfC(c4 z;dYi#7do6Q*N6{-ktANZ?jXJ0Aa-M5jWN!wHYyhFTNyRuOLFj@t1IZzNiOiZ9hhHG zNa!d-lJh8)z$oRgYy<&$KtkyRG!GhvKr(K)kU@ZxStAhZWdbdn#6I-(glX4@0eEEd zNj%yX6*AnzsD-pee>S@in^U6p_)_Y`#p7MD_u;vm7lpjEFV&q#N+Bg5t05s{A{$al zsJa8CD$;%##dIf1xly8V3-}ek*p6jd##z|rkxMYL9?TXz&%cT`uglE2w2hxn6nU|p#;=~ zK|qw%FT)R zHCAVeCLQ!0MDjOMLvs)otlER^Ju%@nB70YZZYJ?hLf`_%&luHavl(hH{~Xj@dKrWR zKDe{G1g(5;L=pDh7#{w~Hk5rLXHm*Dby?xWG8$7phRZQD-x^z0LQFvOc_A0yLBL=$@*0?q=hR@M`9eDIegiCCI9Ds9nwn0) zqmTRm_w0U~j0oJzcnq@a4`m}DRwH8H{~LC@o$g<`8kQ{iqSmo-J^iKr5EObnysRul zOWa3A-P<9r=RF>-s^^2P04b+HFS7*!o^c`C#WhWr<~%Gl=yhL!9r+6>VfA8rM<;dq z{BZt^sr_qi=8P}C_#67(gKMC#uTPv8TkqmMLh^J3#OI$d?kp)O!DpWPB~70`BXz!? zK$pJ<{&L_0_>U`VF`iIJ;dQG?31Lq60g`(TL9DA0rG%esCPgFAr%I$q9+15gFy@|0 zQBO6f76(d_L{^K0zu&Tz&aXZX3Ow#1dqnTQUk^7gy8+wU+Qj+C8FxeG&PYaEE}ni_ zSs85p<0~+3+_+&BMzH_S@57i;;aRImP!BVroW$p6O4;4m_Dqcj~zP(S6_7nw6wH<5rs#y5fJaXb~~L;%hosE zfEhDpWNyl)Nl2XN_6Oj26R(;+J&h!@S-Yma!$%Kn+y*uMXKb81cOJgJ^>s37 zHaPG!%x04of%FBsXo-Y^Ja7`^!r=%O7Zy+~9*3uQ9>Rx@+i746p6I@+2;4Gv3d&Y1 zwRd(wZZ4PNtT`rv7K=1w13*L|$u^Z9MV0M$acwQW_10VB{8GlR(fCq4{R_8k+lDnY zbI7;_!B|Q04&~9-emId8WF$x~z$m zlOuzn^E0-Vwhp-dr6xESHiOgaU;-8hb%$WGoPdp=uYj_Wq7n6@C`pLMVp+Z+m_2(o z9Q@z|Q6o)BO;|s<_uKcuiWT={O+JSsHx=eHZm;{;&*wgO6g$aE1_2Cpg{VM{!uF+A zSm@0k@`@?jZwh(xN-*oz{rX4N(YkdHi{rb+7tbFteJhoq60Ylx6FP2=sC? zPk5|>%WiuHfkbZz4&N~WS@RI z0i76Hk&=`J6%pZ%yT-<7y)5?hufoY!~uEfdec?5{H?@ z(~G@0xhRJW0;i2YZ!m->TiUX|YmP+v=&Lt=Nfdnb;)cI6W=Q%r`Y^AIgg?NDBqWSa zQMK1;B;G2RhVAq81hnM?P}ciIajqz0J_C(_SS(LGV@&@&+mZhV7RJ=rE$aIu00000 LNkvXXu0mjf!el#T literal 0 HcmV?d00001 diff --git a/examples/graph/img/soft-scraps-icons/User-Administrator-Green-icon.png b/examples/graph/img/soft-scraps-icons/User-Administrator-Green-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..a3d3167785d95927c0699b5e9f3c58ac43c77aaf GIT binary patch literal 3906 zcmV-I554e-P)1BugT_~a4q*hQt=W9%0-A)#baBw7ptiCptX4HdB968&^}&HI#hUefO7clN*T*Gjs+?Np5 z+avLodYs{Byu$beWADF;zy!ux#+MidgVBf%PafIbxnQ+B(VS%^gHcj{LrKUKi-aK@ z3Sg+Gi-NumP!b98+X=>B7|R%k|5*f#jPEg4Gpyz;8+c1bf;+DW&|n0!*#sV!3!DzS zx-Yg^EDnKS5PbfCx=%_1dOBL5^JD|WB7NeNFk=Pd=Q^o>7y&Ee8OC*JFhE|(X!I16 zkWrGro8!U4f;{rLT_PMwsU&|x-B*+Z`h0$BY43opZXYIOnS5<0vE$@%B1I9WyvVp& zCv?sUh=kn8m}jzNLD86rV6kRnw#`PlIc|_76W=STbv)y~EX&9wsJAbS(Qq%deDV>* zqY*Klt&FP~edm&Zkj)0hS1h({C>%8&jFJg#R%^dZKIM!%>oBQStq9#zW_V zKqX@jYu=P!ItG~p8N|4Ci#*#wqrOim1l0emEJY#Y!BczFC!FX?oR43`*!Ot|i0`*C zW;=88!Rl~4(9Pq^5bFpedE(LvN8*SX5>%0aWmI{3$iMSvX@Z~W4Ew0zOUkYksS6qz6qgCh+cc+JX9)b0ysye<|E zW60kv_FrZE$7dl>z-Zt^wKzPvaJHKy8R7DoQ{aJHYhcQld>$4*OhsnIH6)^NVR`Wf}7H=-yPuB|yA zZ(KN&zA&OdKS!t{A$kgqKwOOgCZb4*2>E;b`2F?U>G`dPA;C$UdcSD6kKIXiq;D~v zKU)G%Fzzs!EntwObjRqw7_lVZMU@lEU`FK^a9&k8m5(3E7HdXA195Mm5n!7X5kN)O z2q;WIjxm8S+fEoM7N)u*jqv2gI{g3LC&(9yskc|SQpYt*eG;^Wao7ZkK}(!T!t+Jy zuDW3gj4#Urvo#wW4kr}k=RjFWA=vHQfJ#|h>MRT{Y6b>!6UT#xq+^NV%MsuXPM1$i zl@KH%T#~tKh;#9ZiT@!!9DohmkHAeoe_wSZt_|W2^`IF_%Ww!>!uTU2FUw-4C|Ald zuN99zwUEp{V;CvCT#2slZ7jNBpMUr>|k8W*l{`n;~9Gx?qxS$4mW>wE>v9e zeOO#G3ZA&Dng{IwFfq(_{?DzE#6%=(N(9Ux2rwI6#VHXG%1cIo34ol80Jl8xK0OK{ zH3|?5z@6)A;i=6Hu+Wub(%O(CkDqa|BcZ$q3j8j2r=pBVaI*kPsI;RLxxcfV!_WaGU_VFApTK%sG%q zjX;R=folU3*zne2yy1V|qsEQPpr{}ZS3mV8t^B_?#cei$*6a--P{?Q+UF0;>KYJ@! zFp9p$15~tlC%pf&`8Z`HDPB0geL75!Hp#C6pu( zgo8($@cgA)sb&2v@RXHflq@u9(XZinOF$I2Qby}w0#`6zyW`3U@QWopt&Mi`??j;J zwrx=V#AT3^%U*7FYVzrq5W~b0GNwpK$mdk;hMLUsKoZNzBu+|5AgD^HyQ>2xeRngo z{BkY_mG5uKf$y-ao_tN6xLwKk^I!sB70LU9>nGCc8z!@fnE1&d&0oF^UU{Gj?JnVaxgrZsm*BQHxkK>d4IiPN z%r6ZlaIwhnYiAVFhNYE&*#$^eFRgy|U08Cd2R+_=LW}M~DgoO70b|mII0sN`v-=BD zDgluLY8{BDyO5mfLV`dK_`2G0-Bv%XSX2cV?nLTsqwCi0gO}cIMcpai8BD+;bY;&n zy6b;d1*LgB$l{r>pPd&^?tt=fWdK<@EEzi_38*qM3Kz0y5;7#~Kn59!Rzj@>@}N49 z!HFW>aNy{t+yTyksYQHkb1gJ=gka)-?}6S(qDRlyXeJl>5o6`WlU%Ur-g0zUjRaON zghIV6p-^{2USD1zOTwO|+Omuu8TajgJx2qn<9dN{2WX38a5@6X?Lx-a7}xVjrQ23rM6)YL zAi2g6Se$D0)goZgB4FSK&L9nqfZB~JDG?AZBpO1n#lw)7Pr6AYe)%V9Y>(h7wTXIswsE zM>PrgK1MmzPIH#m!mh(TX?A(4Z8!v23$T+hKlVFZ^bTb>xl}t5!t&EG&6~P9s<)DzhqPzjYg=s{0gYPL(1i0ctq>Fo+F4cKhY7lTw$M&506gPXZt-XUfI(=|n z#gzUzH*>}pU;G0-^x!J!>+2K8#nQWYkB~SW0rB}U#*)IqLVWt!-_o>c70KiM1iE}Z z@Yj9&;mcRmU@R_^%=1=*9Kx*bePro52(hk1Xp+0hqDLUYqlrs7V9uS4QTG&zcq&0L z*-?@tGMgm))27XIUgcEC^|*(u5q_u701^x?uPW0k+f~8c>2Y~#jx?! z*I?|}v8PZN!QKx(gi#|(U?f95qKI-uaI&KbR&Kiwx~*Mm_Jgj0oNz9zn)e8lcuT<` zh#9n>Zus;hoM2oyecCA%eeBpVxO(B0(A?Y%dK7+|iGX<5wbS8nm^Q!pCR9{Zq%X>* zNJt#$_W9v>1J9biSWc4BsGif--bw3TUJKC)do!-KSq@)_OTMy<3i7?`aXFEo_y2YP zM~^I}9FHsQTp^*+2I z6a_lFe5}tfj2Tr1{y-3pooIxn<`&p*dI~~@*0l5ON+C>*|0k3d7r|IQ-{o|`Cw#um zW(9Aq2Mh*7dW?nJQhLrl{q(Ql_S}+nuS#wMTO(to`0)U7>f^8}_iVEBBWi>VU_S0hvA%NDGzo<;g4 z2>MKle*pOj4Iv=T;z*Eq%VIX0)N`7e+hE1s$MNInZYp;x;4~4|`wdhq&4A_8@50jJ zLK4yAYml#D3m$k$l6rqW2l)XF8OyF+J8}N}`J_vzN?a^nmmVsgI@M5HTbnVFva&3o z>->ycb89;+-nkn5i9=A~QNYF1ZR<#m}3-gC{A6ZU7BpS`|4Z+NrGvUDg z{h~*joGe(6-Fx5tux!~q8I#X$&rXKI7v3+V)Kzt2E{coDoX=P}Il z<_vkol<7BxJb5OVap`{Tqibl*nn%R`-QtU97&ray`4^a3b;g@{+%B|O%)=rO;AB33 z<4riy9!VqcaUct}U-4UXIvv9z5RJyr*W=Gfptg1wee0X*eCs#j3o*N&_1YU-AwRz$ zV>k=*bJaJJXWE2JTLWD%=k>)f!9{5j+TUYe{pvR_fYoZt_)eD-51kJC86zN!uip15-1)&nINnJ#)kB=b$VrTJ zhzUGWaX(IUk071EX(JE_hOnu*HRHSHaJY}Yebd)P!&fIR_#2}_(w5Or@ybZ}0}M+- z3V)Q9gnGWjJTrM@>>+837k#DdTdqSfHSE5at$@l2uW_ zpO=fpMFo`W^GY~UQc2xI-`BJxwzdYTzOf0KTUs%xs?@r_4x8#eBhoba$;*scCZS_a zKoW8#W3tWRgp$F-z~Rcq?5r%x&+~zz*!Ws0t>a<$RaHeMK^>hDj72)Ae$OY6h(%>Q zYZwcNcE?1XKj&>7Cbw(nPR1?rpQw6b3-JD)!OoKki zsjmd?WGqzd4lEv6K{9xoP2uNh$taa0+((TxQWFU#p<=_{YEt7d`F!ZAbhsXf#IXW#J#e%278OuyxDKwD!yCKGY7rgIr@9W4QI zoeSOGJhVGpJ#W@00_>=G_4(hSc{5LkJhy_RsRYS5RPAlTsS8$7V{0VidGS~T!@(BW z{~P0bM?;{9v6mCo;mOU1Bi$6m3g?X*33pt34vZXJ$iw1?si};Ffn*G-YWBnQg{$Dx zy0(nh2(>nII5f%V0gT2YC2%F<1;yrIzt2LF-bVVGv1sn`_t>_#Wj_XP8gQ@6c`O-p9%B>BXNd{=S%9|J!2#c zDJuZGD;qqX94IQxgR;_MaJ#tyRh(Sv91JdM76x(?$AgEIV@dJl2yh2y$|tT%2$E4Q z$=o$0xOm0oGt7tEVa2*CxaQIKbw}dbAXnH8hN0B=g}@n%zc31%4m-uTQkMB$SiR&T zvS;~_Y;FP@xBm8Q9w$pn;pAolXmuc2?Zn~YaVus7(2Rg7Az}$7%?OBu)Tj}GL?;Oe zjCb&Ui2n^j#r3POHV`2PSNdoyF5_%sT*g>`C;~$mTNu7uXPpPLE}aA?PQ4wjKc^fP z&l%5y_5j!zb~k@=Yoss{#g!HTI|u=0W2!hU0;0TB1egG*sR(e(ljt;~5Z0pr@picB ziH)#i^o>srk4(Xl75z;D9uoP;(??qm30X`D@vzD4I=kMo%afe`Gh-iIg8 zT|@OxPk`LAQ5Yi!4WIcOd{*Bs#jQW1p(lawGG4#of}!x_%{;A*weWqSP%?Wh>|T5> zsMZnra zLU3TeD*|GpBB7WO0W~cGi7-Jd2;pD=r{1@f-nwU!d0v9%FWUvbS@8*)$^1%B0;fxc zUpBUwR?Hm@m|cYA^3%fSH^a?mtg*XS$XtTQtQaS<20lf|+ zy1S5??m|MK4O*KU@rgA-nm=<4U?hOlv7at~a2vd`xdBb5e5WS?hv>@fwEA{GG6wn= z@F4q*gOAyHv2Hz#swe|+=CNelkRqVV$SN-6FeGG2)qxB$l2$^m1!|8vkim(fEwH2d zbM64ogyT#2+%Go5zNRn?``H%gh$h?2e2rytp}QChP9N@tRlgjC9+#EC<%e*%gC&$t zm{nxBkj)r8DlU$I^FRc27m^5AGt>dir~`T{!4c3>5s2z;Bm^*FMxbLqOQ;#$9+qn) z0HH_{FMMDJy|Y_ZW8iLcoEdFYChe;k<131t_=}7C(;36O;P<&PFTa4$-4Dr;L#YHt z#mTY}0*(U`N++OuP%i|ManprN0-Ve`fmk~e_=1x-fPuCSTK`E9ez$TTRyRaNhVL@Y zfwW0~CcBWuDd|1FpE___g%>v6KaumIfT#8)hVv*Xq~v2aC1e(|gIWpAaG+F2+9jiy z;Y2An>SzG!Y=p75t%ohu?djwCGvfv@7RBID1XA0nj4K$^_(%QM-hCQP96bQZJD9+c zqgP)e0uCbr7H;4y%E1xPyHPDI0^&ld8G$$#GNT?yE?}%3sqH_setRuUTCi1&K&EZ7 znD7$xvVzx_`JryFMkVkeM^7(M)ef}wlo6P3NbkWlNV zsD>Nq%y}E(-O9FZcG=t37XqvW*uWV3_z#D}oQsCC$jSk7%8kOM_djM2)FmI&g$|NX zniHjDBz=*525WRIT2%=@`TZwymMj8`wQFCNfNUcfTO0~zzs*t_JCgGtcNm~=(1HHP zA|0sJjDSTig56z+{{^W7Ns?Sh%?)_sixk{L&{7GTcGlzMUvH(DO0uXWP20}H4XiH& z`0?t=BlhJj^g6gYo(`kRJb<|sNd8e~4p zJ3bG?l=&O5F&GoKk?dUnhMB~_41tpv&w{KCSrzb%p9=0%FMx4cEj&J^2Kw0o>`L4t z6`um&E;XIUL~4?$OUV4wB_l?-j>{(JWk(}?q59z`n+L&0OBsZ>z7C;xUt?M66B$_( zn$7s&ND0XCIgAG+0v*SWbZdvmxF8D8o2-53K*Jv4+ljz>I+@yDKnzs9mFk0 z_Q4Wj0=mzOTmm})L$%0rVEW8C9xJV<(63fkL+6QS>1|Z}i#-rnwh$v9ttT@Azho?e z4C_Og2*_+i*8P8qjx5TXeg}9aPSd;M)FeK(XBRv?-VetQC`4O4K!q)9AgAqp9Eqh&FRi)2urWOAXq6Ct+jW6iV3r`0?kB^z(NE;2b>Sz@h>FD)g@v z)Uy0uzG0_~QNwSxZN*(W-)NA1&e5a!t{&e%j7*D9A z^1RiehSAxwjT~(|Ar`1aCDB4ItELm^Oj<~_<$=RL6l1=T6wMtCn$3-hqLAID;MGs8 zqc=fPe!{GeG?`s|82LU0_l z;6LX~AzAa3pHyd z^9Zv&hnUGB#%p&|^CL5Xq8+m6JAv_DIsti^wV82J&h)#$HTDuX>?W(p9c}RQ6TNWT z`6s}xx)!)%>1Jqj@Dp}sWjsZTc0yq+058ot4Mvpa!~Ji31P^Zyf+gG6`z$VPi)?xo z+LzxYyW&b?Bsi(FrZbk=jyoB>H$Fp#4Q5`;PuCw?9a`g^bkCWkaR2(f*lKkXD)Pbu zGo33RoX`o%O@`sNGX~HHsTp+z%5Yp9kUX+)}EJ!YFU8ypj_J?!fsfTMdq(8p``X+O5^ zIZCXP1_5r7FP1W{v0wElSjtcOMi9_yKA`vu@~RsCC|}6zeu8q-%iIq5znYjIvs9#L&k~v()f|x?CT>S`$?&1pc9bE h^3bm)*kAcp`CkvQKiH);tiu2R002ovPDHLkV1fWgs-XY? literal 0 HcmV?d00001 diff --git a/examples/graph/img/soft-scraps-icons/User-Coat-Green-icon.png b/examples/graph/img/soft-scraps-icons/User-Coat-Green-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..4a4326b749ed16552a3db1fff820d53a29594c84 GIT binary patch literal 3571 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 Sw%*AA0000)oZU}yJ~@ZOWNC8 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 TfngXZ00000NkvXXu0mjf7*j=+ literal 0 HcmV?d00001 diff --git a/examples/graph/img/soft-scraps-icons/User-Preppy-Blue-icon.png b/examples/graph/img/soft-scraps-icons/User-Preppy-Blue-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..b3c774083ec033c622e17a995397c318d94eb750 GIT binary patch literal 3802 zcmV<04khu4P)u)XOFk)nuO{)aJN zCv?RL2#4Ivs5DuukUP2q{B`ekBN$ zF%GfjOsPf`Yy(l z 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 Q&j0`b07*qoM6N<$f?a(FSpWb4 literal 0 HcmV?d00001 diff --git a/examples/graph/img/soft-scraps-icons/User-Preppy-Red-icon.png b/examples/graph/img/soft-scraps-icons/User-Preppy-Red-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..f569776ea011a9bbd1488f51c350e6aec7dc2071 GIT binary patch literal 3800 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 Visit artist homepage for details). + diff --git a/src/graph/graph.js b/src/graph/graph.js new file mode 100644 index 00000000..2ea86a29 --- /dev/null +++ b/src/graph/graph.js @@ -0,0 +1,5876 @@ +/** + * @constructor Graph + * Create a graph visualization connecting nodes via edges. + * @param {Element} container The DOM element in which the Graph will + * be created. Normally a div element. + */ +function Graph (container) { + // create variables and set default values + this.containerElement = container; + this.width = "100%"; + this.height = "100%"; + this.refreshRate = 50; // milliseconds + this.stabilize = true; // stabilize before displaying the network + this.selectable = true; + + // set constant values + this.constants = { + "nodes": { + "radiusMin": 5, + "radiusMax": 20, + "radius": 5, + "distance": 100, // px + "style": "rect", + "image": undefined, + "widthMin": 16, // px + "widthMax": 64, // px + "fontColor": "black", + "fontSize": 14, // px + //"fontFace": "verdana", + "fontFace": "arial", + "borderColor": "#2B7CE9", + "backgroundColor": "#97C2FC", + "highlightColor": "#D2E5FF", + "group": undefined + }, + "edges": { + "widthMin": 1, + "widthMax": 15, + "width": 1, + "style": "line", + "color": "#343434", + "fontColor": "#343434", + "fontSize": 14, // px + "fontFace": "arial", + //"distance": 100, //px + "length": 100, // px + "dashlength": 10, + "dashgap": 5 + }, + "packages": { + "radius": 5, + "radiusMin": 5, + "radiusMax": 10, + "style": "dot", + "color": "#2B7CE9", + "image": undefined, + "widthMin": 16, // px + "widthMax": 64, // px + "duration": 1.0 // seconds + }, + "minForce": 0.05, + "minVelocity": 0.02, // px/s + "maxIterations": 1000 // maximum number of iteration to stabilize + }; + + this.nodes = []; // array with Node objects + this.edges = []; // array with Edge objects + this.packages = []; // array with all Package packages + this.images = new Graph.Images(); // object with images + this.groups = new Graph.Groups(); // object with groups + + // properties of the data + this.hasMovingEdges = false; // True if one or more of the edges or nodes have an animation + this.hasMovingNodes = false; // True if any of the nodes have an undefined position + this.hasMovingPackages = false; // True if there are one or more packages + + this.selection = []; + this.timer = undefined; + + // create a frame and canvas + this._create(); +}; + +/** + * Main drawing logic. This is the function that needs to be called + * in the html page, to draw the Network. + * Note that Object DataTable is defined in google.visualization.DataTable + * + * A data table with the events must be provided, and an options table. + * @param {google.visualization.DataTable | Array} [nodes] The data containing the nodes. + * @param {google.visualization.DataTable | Array} [edges] The data containing the edges. + * @param {google.visualization.DataTable | Array} [packages] The data containing the packages + * @param {Object} options A name/value map containing settings + */ +Graph.prototype.draw = function(nodes, edges, packages, options) { + var nodesTable, edgesTable, packagesTable; + // correctly read the parameters. edges and packages are optional. + if (options != undefined) { + nodesTable = nodes; + edgesTable = edges; + packagesTable = packages; + } + else if (packages != undefined) { + nodesTable = nodes; + edgesTable = edges; + packagesTable = undefined; + options = packages; + } + else if (edges != undefined) { + nodesTable = nodes; + edgesTable = undefined; + packagesTable = undefined; + options = edges; + } + else if (nodes != undefined) { + nodesTable = undefined; + edgesTable = undefined; + packagesTable = undefined; + options = nodes; + } + + if (options != undefined) { + // retrieve parameter values + if (options.width != undefined) {this.width = options.width;} + if (options.height != undefined) {this.height = options.height;} + if (options.stabilize != undefined) {this.stabilize = options.stabilize;} + if (options.selectable != undefined) {this.selectable = options.selectable;} + + // TODO: work out these options and document them + if (options.edges) { + for (var prop in options.edges) { + if (options.edges.hasOwnProperty(prop)) { + this.constants.edges[prop] = options.edges[prop]; + } + } + + if (options.edges.length != undefined && + options.nodes && options.nodes.distance == undefined) { + this.constants.edges.length = options.edges.length; + this.constants.nodes.distance = options.edges.length * 1.25; + } + + if (!options.edges.fontColor) { + this.constants.edges.fontColor = options.edges.color; + } + + // Added to support dashed lines + // David Jordan + // 2012-08-08 + if (options.edges.dashlength != undefined) { + this.constants.edges.dashlength = options.edges.dashlength; + } + if (options.edges.dashgap != undefined) { + this.constants.edges.dashgap = options.edges.dashgap; + } + if (options.edges.altdashlength != undefined) { + this.constants.edges.altdashlength = options.edges.altdashlength; + } + } + if (options.nodes) { + for (prop in options.nodes) { + if (options.nodes.hasOwnProperty(prop)) { + this.constants.nodes[prop] = options.nodes[prop]; + } + } + + /* + if (options.nodes.widthMin) this.constants.nodes.radiusMin = options.nodes.widthMin; + if (options.nodes.widthMax) this.constants.nodes.radiusMax = options.nodes.widthMax; + */ + } + if (options.packages) { + for (prop in options.packages) { + if (options.packages.hasOwnProperty(prop)) { + this.constants.packages[prop] = options.packages[prop]; + } + } + + /* + if (options.packages.widthMin) this.constants.packages.radiusMin = options.packages.widthMin; + if (options.packages.widthMax) this.constants.packages.radiusMax = options.packages.widthMax; + */ + } + + if (options.groups) { + for (var groupname in options.groups) { + if (options.groups.hasOwnProperty(groupname)) { + var group = options.groups[groupname]; + this.groups.add(groupname, group); + } + } + } + } + + this._setBackgroundColor(options.backgroundColor); + + this._setSize(this.width, this.height); + this._setTranslation(0, 0); + this._setScale(1.0); + + // set all data + this.hasTimestamps = false; + this.setNodes(nodesTable); + this.setEdges(edgesTable); + this.setPackages(packagesTable); + + this._reposition(); // TODO: bad solution + if (this.stabilize) { + this._doStabilize(); + } + this.start(); + + // create an onload callback method for the images + var network = this; + var callback = function () { + network._redraw(); + }; + this.images.setOnloadCallback(callback); + + // fire the ready event + this.trigger('ready'); +}; + +/** + * fire an event + * @param {String} event The name of an event, for example "select" or "ready" + * @param {Object} params Optional object with event parameters + */ +Graph.prototype.trigger = function (event, params) { + // trigger the edges event bus + events.trigger(this, event, params); + + // trigger the google event bus + if (typeof google !== 'undefined' && google.visualization && google.visualization.events) { + google.visualization.events.trigger(this, event, params); + } +}; + + +/** + * Create the main frame for the Network. + * This function is executed once when a Network object is created. The frame + * contains a canvas, and this canvas contains all objects like the axis and + * nodes. + */ +Graph.prototype._create = function () { + // remove all elements from the container element. + while (this.containerElement.hasChildNodes()) { + this.containerElement.removeChild(this.containerElement.firstChild); + } + + this.frame = document.createElement("div"); + this.frame.className = "network-frame"; + this.frame.style.position = "relative"; + this.frame.style.overflow = "hidden"; + + // create the graph canvas (HTML canvas element) + this.frame.canvas = document.createElement( "canvas" ); + this.frame.canvas.style.position = "relative"; + this.frame.appendChild(this.frame.canvas); + if (!this.frame.canvas.getContext) { + var noCanvas = document.createElement( "DIV" ); + noCanvas.style.color = "red"; + noCanvas.style.fontWeight = "bold" ; + noCanvas.style.padding = "10px"; + noCanvas.innerHTML = "Error: your browser does not support HTML canvas"; + this.frame.canvas.appendChild(noCanvas); + } + + // create event listeners + var me = this; + var onmousedown = function (event) {me._onMouseDown(event);}; + var onmousemove = function (event) {me._onMouseMoveTitle(event);}; + var onmousewheel = function (event) {me._onMouseWheel(event);}; + var ontouchstart = function (event) {me._onTouchStart(event);}; + Graph.addEventListener(this.frame.canvas, "mousedown", onmousedown); + Graph.addEventListener(this.frame.canvas, "mousemove", onmousemove); + Graph.addEventListener(this.frame.canvas, "mousewheel", onmousewheel); + Graph.addEventListener(this.frame.canvas, "touchstart", ontouchstart); + + // add the frame to the container element + this.containerElement.appendChild(this.frame); +}; + +/** + * Set the background and border styling for the graph + * @param {String | Object} backgroundColor + */ +Graph.prototype._setBackgroundColor = function(backgroundColor) { + var fill = "white"; + var stroke = "lightgray"; + var strokeWidth = 1; + + if (typeof(backgroundColor) == "string") { + fill = backgroundColor; + stroke = "none"; + strokeWidth = 0; + } + else if (typeof(backgroundColor) == "object") { + if (backgroundColor.fill != undefined) fill = backgroundColor.fill; + if (backgroundColor.stroke != undefined) stroke = backgroundColor.stroke; + if (backgroundColor.strokeWidth != undefined) strokeWidth = backgroundColor.strokeWidth; + } + else if (backgroundColor == undefined) { + // use use defaults + } + else { + throw "Unsupported type of backgroundColor"; + } + + this.frame.style.boxSizing = 'border-box'; + this.frame.style.backgroundColor = fill; + this.frame.style.borderColor = stroke; + this.frame.style.borderWidth = strokeWidth + "px"; + this.frame.style.borderStyle = "solid"; +}; + + +/** + * handle on mouse down event + */ +Graph.prototype._onMouseDown = function (event) { + event = event || window.event; + + if (!this.selectable) { + return; + } + + // check if mouse is still down (may be up when focus is lost for example + // in an iframe) + if (this.leftButtonDown) { + this._onMouseUp(event); + } + + // only react on left mouse button down + this.leftButtonDown = event.which ? (event.which == 1) : (event.button == 1); + if (!this.leftButtonDown && !this.touchDown) { + return; + } + + // add event listeners to handle moving the contents + // we store the function onmousemove and onmouseup in the timeline, so we can + // remove the eventlisteners lateron in the function mouseUp() + var me = this; + if (!this.onmousemove) { + this.onmousemove = function (event) {me._onMouseMove(event);}; + Graph.addEventListener(document, "mousemove", me.onmousemove); + } + if (!this.onmouseup) { + this.onmouseup = function (event) {me._onMouseUp(event);}; + Graph.addEventListener(document, "mouseup", me.onmouseup); + } + Graph.preventDefault(event); + + // store the start x and y position of the mouse + this.startMouseX = event.clientX || event.targetTouches[0].clientX; + this.startMouseY = event.clientY || event.targetTouches[0].clientY; + this.startFrameLeft = Graph._getAbsoluteLeft(this.frame.canvas); + this.startFrameTop = Graph._getAbsoluteTop(this.frame.canvas); + this.startTranslation = this._getTranslation(); + + this.ctrlKeyDown = event.ctrlKey; + this.shiftKeyDown = event.shiftKey; + + var obj = { + "left" : this._xToCanvas(this.startMouseX - this.startFrameLeft), + "top" : this._yToCanvas(this.startMouseY - this.startFrameTop), + "right" : this._xToCanvas(this.startMouseX - this.startFrameLeft), + "bottom" : this._yToCanvas(this.startMouseY - this.startFrameTop) + }; + var overlappingNodes = this._getNodesOverlappingWith(obj); + // if there are overlapping nodes, select the last one, this is the + // one which is drawn on top of the others + this.startClickedObj = (overlappingNodes.length > 0) ? + overlappingNodes[overlappingNodes.length - 1] : undefined; + + if (this.startClickedObj) { + // move clicked node with the mouse + + // make the clicked node temporarily fixed, and store their original state + var node = this.nodes[this.startClickedObj.row]; + this.startClickedObj.xFixed = node.xFixed; + this.startClickedObj.yFixed = node.yFixed; + node.xFixed = true; + node.yFixed = true; + + if (!this.ctrlKeyDown || !node.isSelected()) { + // select this node + this._selectNodes([this.startClickedObj], this.ctrlKeyDown); + } + else { + // unselect this node + this._unselectNodes([this.startClickedObj]); + } + + if (!this.hasMovingNodes) { + this._redraw(); + } + } + else if (this.shiftKeyDown) { + // start selection of multiple nodes + } + else { + // start moving the graph + this.moved = false; + } +}; + +/** + * handle on mouse move event + */ +Graph.prototype._onMouseMove = function (event) { + event = event || window.event; + + if (!this.selectable) { + return; + } + + var mouseX = event.clientX || (event.targetTouches && event.targetTouches[0].clientX) || 0; + var mouseY = event.clientY || (event.targetTouches && event.targetTouches[0].clientY) || 0; + this.mouseX = mouseX; + this.mouseY = mouseY; + + if (this.startClickedObj) { + var node = this.nodes[this.startClickedObj.row]; + + if (!this.startClickedObj.xFixed) + node.x = this._xToCanvas(mouseX - this.startFrameLeft); + + if (!this.startClickedObj.yFixed) + node.y = this._yToCanvas(mouseY - this.startFrameTop); + + // start animation if not yet running + if (!this.hasMovingNodes) { + this.hasMovingNodes = true; + this.start(); + } + } + else if (this.shiftKeyDown) { + // draw a rect from start mouse location to current mouse location + if (this.frame.selRect == undefined) { + this.frame.selRect = document.createElement("DIV"); + this.frame.appendChild(this.frame.selRect); + + this.frame.selRect.style.position = "absolute"; + this.frame.selRect.style.border = "1px dashed red"; + } + + var left = Math.min(this.startMouseX, mouseX) - this.startFrameLeft; + var top = Math.min(this.startMouseY, mouseY) - this.startFrameTop; + var right = Math.max(this.startMouseX, mouseX) - this.startFrameLeft; + var bottom = Math.max(this.startMouseY, mouseY) - this.startFrameTop; + + this.frame.selRect.style.left = left + "px"; + this.frame.selRect.style.top = top + "px"; + this.frame.selRect.style.width = (right - left) + "px"; + this.frame.selRect.style.height = (bottom - top) + "px"; + } + else { + // move the network + var diffX = mouseX - this.startMouseX; + var diffY = mouseY - this.startMouseY; + + this._setTranslation( + this.startTranslation.x + diffX, + this.startTranslation.y + diffY); + this._redraw(); + + this.moved = true; + } + + Graph.preventDefault(event); +}; + +/** + * handle on mouse up event + */ +Graph.prototype._onMouseUp = function (event) { + event = event || window.event; + + if (!this.selectable) { + return; + } + + // remove event listeners here, important for Safari + if (this.onmousemove) { + Graph.removeEventListener(document, "mousemove", this.onmousemove); + this.onmousemove = undefined; + } + if (this.onmouseup) { + Graph.removeEventListener(document, "mouseup", this.onmouseup); + this.onmouseup = undefined; + } + Graph.preventDefault(event); + + // check selected nodes + var endMouseX = event.clientX || this.mouseX || 0; + var endMouseY = event.clientY || this.mouseY || 0; + + var ctrlKey = event ? event.ctrlKey : window.event.ctrlKey; + + if (this.startClickedObj) { + // restore the original fixed state + var node = this.nodes[this.startClickedObj.row]; + node.xFixed = this.startClickedObj.xFixed; + node.yFixed = this.startClickedObj.yFixed; + } + else if (this.shiftKeyDown) { + // select nodes inside selection area + var obj = { + "left": this._xToCanvas(Math.min(this.startMouseX, endMouseX) - this.startFrameLeft), + "top": this._yToCanvas(Math.min(this.startMouseY, endMouseY) - this.startFrameTop), + "right": this._xToCanvas(Math.max(this.startMouseX, endMouseX) - this.startFrameLeft), + "bottom": this._yToCanvas(Math.max(this.startMouseY, endMouseY) - this.startFrameTop) + }; + var overlappingNodes = this._getNodesOverlappingWith(obj); + this._selectNodes(overlappingNodes, ctrlKey); + this.redraw(); + + // remove the selection rectangle + if (this.frame.selRect) { + this.frame.removeChild(this.frame.selRect); + this.frame.selRect = undefined; + } + } + else { + if (!this.ctrlKeyDown && !this.moved) { + // remove selection + this._unselectNodes(); + this._redraw(); + } + } + + this.leftButtonDown = false; + this.ctrlKeyDown = false; +}; + + +/** + * Event handler for mouse wheel event, used to zoom the timeline + * Code from http://adomas.org/javascript-mouse-wheel/ + * @param {event} event The event + */ +Graph.prototype._onMouseWheel = function(event) { + event = event || window.event; + var mouseX = event.clientX; + var mouseY = event.clientY; + + // retrieve delta + var delta = 0; + if (event.wheelDelta) { /* IE/Opera. */ + delta = event.wheelDelta/120; + } else if (event.detail) { /* Mozilla case. */ + // In Mozilla, sign of delta is different than in IE. + // Also, delta is multiple of 3. + delta = -event.detail/3; + } + + // If delta is nonzero, handle it. + // Basically, delta is now positive if wheel was scrolled up, + // and negative, if wheel was scrolled down. + if (delta) { + // determine zoom factor, and adjust the zoom factor such that zooming in + // and zooming out correspond wich each other + var zoom = delta / 10; + if (delta < 0) { + zoom = zoom / (1 - zoom); + } + + var scaleOld = this._getScale(); + var scaleNew = scaleOld * (1 + zoom); + if (scaleNew < 0.01) { + scaleNew = 0.01; + } + if (scaleNew > 10) { + scaleNew = 10; + } + + var frameLeft = Graph._getAbsoluteLeft(this.frame.canvas); + var frameTop = Graph._getAbsoluteTop(this.frame.canvas); + var x = mouseX - frameLeft; + var y = mouseY - frameTop; + + var translation = this._getTranslation(); + var scaleFrac = scaleNew / scaleOld; + var tx = (1 - scaleFrac) * x + translation.x * scaleFrac; + var ty = (1 - scaleFrac) * y + translation.y * scaleFrac; + + this._setScale(scaleNew); + this._setTranslation(tx, ty); + this._redraw(); + } + + // Prevent default actions caused by mouse wheel. + // That might be ugly, but we handle scrolls somehow + // anyway, so don't bother here... + Graph.preventDefault(event); +}; + + +/** + * Mouse move handler for checking whether the title moves over a node or + * package with a title. + */ +Graph.prototype._onMouseMoveTitle = function (event) { + event = event || window.event; + + var startMouseX = event.clientX; + var startMouseY = event.clientY; + this.startFrameLeft = this.startFrameLeft || Graph._getAbsoluteLeft(this.frame.canvas); + this.startFrameTop = this.startFrameTop || Graph._getAbsoluteTop(this.frame.canvas); + + var x = startMouseX - this.startFrameLeft; + var y = startMouseY - this.startFrameTop; + + // check if the previously selected node is still selected + if (this.popupNode) { + this._checkHidePopup(x, y); + } + + // start a timeout that will check if the mouse is positioned above + // an element + var me = this; + var checkShow = function() { + me._checkShowPopup(x, y); + }; + if (this.popupTimer) { + clearInterval(this.popupTimer); // stop any running timer + } + if (!this.leftButtonDown) { + this.popupTimer = setTimeout(checkShow, 300); + } +}; + +/** + * Check if there is an element on the given position in the network ( + * (a node, package, or edge). If so, and if this element has a title, + * show a popup window with its title. + * + * @param {number} x + * @param {number} y + */ +Graph.prototype._checkShowPopup = function (x, y) { + var obj = { + "left" : this._xToCanvas(x), + "top" : this._yToCanvas(y), + "right" : this._xToCanvas(x), + "bottom" : this._yToCanvas(y) + }; + + var i, len; + var lastPopupNode = this.popupNode; + + if (this.popupNode == undefined) { + // search the packages for overlap + + for (i = 0, len = this.packages.length; i < len; i++) { + var p = this.packages[i]; + if (p.getTitle() != undefined && p.isOverlappingWith(obj)) { + this.popupNode = p; + break; + } + } + } + + if (this.popupNode == undefined) { + // search the nodes for overlap, select the top one in case of multiple nodes + var nodes = this.nodes; + for (i = nodes.length - 1; i >= 0; i--) { + var node = nodes[i]; + if (node.getTitle() != undefined && node.isOverlappingWith(obj)) { + this.popupNode = node; + break; + } + } + } + + if (this.popupNode == undefined) { + // search the edges for overlap + var allEdges = this.edges; + for (i = 0, len = allEdges.length; i < len; i++) { + var edge = allEdges[i]; + if (edge.getTitle() != undefined && edge.isOverlappingWith(obj)) { + this.popupNode = edge; + break; + } + } + } + + if (this.popupNode) { + // show popup message window + if (this.popupNode != lastPopupNode) { + var me = this; + if (!me.popup) { + me.popup = new Graph.Popup(me.frame); + } + + // adjust a small offset such that the mouse cursor is located in the + // bottom left location of the popup, and you can easily move over the + // popup area + me.popup.setPosition(x - 3, y - 3); + me.popup.setText(me.popupNode.getTitle()); + me.popup.show(); + } + } + else { + if (this.popup) { + this.popup.hide(); + } + } +}; + +/** + * Check if the popup must be hided, which is the case when the mouse is no + * longer hovering on the object + * @param {number} x + * @param {number} y + */ +Graph.prototype._checkHidePopup = function (x, y) { + var obj = { + "left" : x, + "top" : y, + "right" : x, + "bottom" : y + }; + + if (!this.popupNode || !this.popupNode.isOverlappingWith(obj) ) { + this.popupNode = undefined; + if (this.popup) { + this.popup.hide(); + } + } +}; + +/** + * Event handler for touchstart event on mobile devices + */ +Graph.prototype._onTouchStart = function(event) { + Graph.preventDefault(event); + + if (this.touchDown) { + // if already moving, return + return; + } + this.touchDown = true; + + var me = this; + if (!this.ontouchmove) { + this.ontouchmove = function (event) {me._onTouchMove(event);}; + Graph.addEventListener(document, "touchmove", this.ontouchmove); + } + if (!this.ontouchend) { + this.ontouchend = function (event) {me._onTouchEnd(event);}; + Graph.addEventListener(document, "touchend", this.ontouchend); + } + + this._onMouseDown(event); +}; + +/** + * Event handler for touchmove event on mobile devices + */ +Graph.prototype._onTouchMove = function(event) { + Graph.preventDefault(event); + this._onMouseMove(event); +}; + +/** + * Event handler for touchend event on mobile devices + */ +Graph.prototype._onTouchEnd = function(event) { + Graph.preventDefault(event); + + this.touchDown = false; + + if (this.ontouchmove) { + Graph.removeEventListener(document, "touchmove", this.ontouchmove); + this.ontouchmove = undefined; + } + if (this.ontouchend) { + Graph.removeEventListener(document, "touchend", this.ontouchend); + this.ontouchend = undefined; + } + + this._onMouseUp(event); +}; + + +/** + * Unselect selected nodes. If no selection array is provided, all nodes + * are unselected + * @param {Object[]} selection Array with selection objects, each selection + * object has a parameter row. Optional + * @param {Boolean} triggerSelect If true (default), the select event + * is triggered when nodes are unselected + * @return {Boolean} changed True if the selection is changed + */ +Graph.prototype._unselectNodes = function(selection, triggerSelect) { + var changed = false; + var i, iMax, row; + + if (selection) { + // remove provided selections + for (i = 0, iMax = selection.length; i < iMax; i++) { + row = selection[i].row; + this.nodes[row].unselect(); + + var j = 0; + while (j < this.selection.length) { + if (this.selection[j].row == row) { + this.selection.splice(j, 1); + changed = true; + } + else { + j++; + } + } + } + } + else if (this.selection && this.selection.length) { + // remove all selections + for (i = 0, iMax = this.selection.length; i < iMax; i++) { + row = this.selection[i].row; + this.nodes[row].unselect(); + changed = true; + } + this.selection = []; + } + + if (changed && (triggerSelect == true || triggerSelect == undefined)) { + // fire the select event + this.trigger('select'); + } + + return changed; +}; + +/** + * select all nodes on given location x, y + * @param {Array} selection an array with selection objects. Each selection + * object has a parameter row + * @param {boolean} append If true, the new selection will be appended to the + * current selection (except for duplicate entries) + * @return {Boolean} changed True if the selection is changed + */ +Graph.prototype._selectNodes = function(selection, append) { + var changed = false; + var i, iMax; + + // TODO: the selectNodes method is a little messy, rework this + + // check if the current selection equals the desired selection + var selectionAlreadyDone = true; + if (selection.length != this.selection.length) { + selectionAlreadyDone = false; + } + else { + for (i = 0, iMax = Math.min(selection.length, this.selection.length); i < iMax; i++) { + if (selection[i].row != this.selection[i].row) { + selectionAlreadyDone = false; + break; + } + } + } + if (selectionAlreadyDone) { + return changed; + } + + if (append == undefined || append == false) { + // first deselect any selected node + var triggerSelect = false; + changed = this._unselectNodes(undefined, triggerSelect); + } + + for (i = 0, iMax = selection.length; i < iMax; i++) { + // add each of the new selections, but only when they are not duplicate + var row = selection[i].row; + var isDuplicate = false; + for (var j = 0, jMax = this.selection.length; j < jMax; j++) { + if (this.selection[j].row == row) { + isDuplicate = true; + break; + } + } + + if (!isDuplicate) { + this.nodes[row].select(); + this.selection.push(selection[i]); + changed = true; + } + } + + if (changed) { + // fire the select event + this.trigger('select'); + } + + return changed; +}; + +/** + * retrieve all nodes overlapping with given object + * @param {Object} obj An object with parameters left, top, right, bottom + * @return {Object[]} An array with selection objects containing + * the parameter row. + */ +Graph.prototype._getNodesOverlappingWith = function (obj) { + var overlappingNodes = []; + + for (var i = 0; i < this.nodes.length; i++) { + if (this.nodes[i].isOverlappingWith(obj)) { + var sel = {"row": i}; + overlappingNodes.push(sel); + } + } + + return overlappingNodes; +}; + +/** + * retrieve the currently selected nodes + * @return {Object[]} an array with zero or more objects. Each object + * contains the parameter row + */ +Graph.prototype.getSelection = function() { + var selection = []; + + for (var i = 0; i < this.selection.length; i++) { + var row = this.selection[i].row; + selection.push({"row": row}); + } + + return selection; +}; + +/** + * select zero or more nodes + * @param {object[]} selection an array with zero or more objects. Each object + * contains the parameter row + */ +Graph.prototype.setSelection = function(selection) { + var i, iMax, row; + + if (selection.length == undefined) + throw "Selection must be an array with objects"; + + // first unselect any selected node + for (i = 0, iMax = this.selection.length; i < iMax; i++) { + row = this.selection[i].row; + this.nodes[row].unselect(); + } + + this.selection = []; + + for (i = 0, iMax = selection.length; i < iMax; i++) { + row = selection[i].row; + + if (row == undefined) + throw "Parameter row missing in selection object"; + if (row > this.nodes.length-1) + throw "Parameter row out of range"; + + var sel = {"row": row}; + this.selection.push(sel); + this.nodes[row].select(); + } + + this.redraw(); +}; + + +/** + * Temporary method to test calculating a hub value for the nodes + * @param {number} level Maximum number edges between two nodes in order + * to call them connected. Optional, 1 by default + * @return {Number[]} connectioncount array with the connection count + * for each node + */ +Graph.prototype._getConnectionCount = function(level) { + var conn = this.edges; + if (level == undefined) { + level = 1; + } + + // get the nodes connected to given nodes + function getConnectedNodes(nodes) { + var connectedNodes = []; + + for (var j = 0, jMax = nodes.length; j < jMax; j++) { + var node = nodes[j]; + + // find all nodes connected to this node + for (var i = 0, iMax = conn.length; i < iMax; i++) { + var other = null; + + // check if connected + if (conn[i].from == node) + other = conn[i].to; + else if (conn[i].to == node) + other = conn[i].from; + + // check if the other node is not already in the list with nodes + var k, kMax; + if (other) { + for (k = 0, kMax = nodes.length; k < kMax; k++) { + if (nodes[k] == other) { + other = null; + break; + } + } + } + if (other) { + for (k = 0, kMax = connectedNodes.length; k < kMax; k++) { + if (connectedNodes[k] == other) { + other = null; + break; + } + } + } + + if (other) + connectedNodes.push(other); + } + } + + return connectedNodes; + } + + var connections = []; + var level0 = []; + var nodes = this.nodes; + var i, iMax; + for (i = 0, iMax = nodes.length; i < iMax; i++) { + var c = [nodes[i]]; + for (var l = 0; l < level; l++) { + c = c.concat(getConnectedNodes(c)); + } + connections.push(c); + } + + var hubs = []; + for (i = 0, len = connections.length; i < len; i++) { + hubs.push(connections[i].length); + } + + return hubs; +}; + + +/** + * Set a new size for the network + * @param {string} width Width in pixels or percentage (for example "800px" + * or "50%") + * @param {string} height Height in pixels or percentage (for example "400px" + * or "30%") + */ +Graph.prototype._setSize = function(width, height) { + this.frame.style.width = width; + this.frame.style.height = height; + + this.frame.canvas.style.width = "100%"; + this.frame.canvas.style.height = "100%"; + + this.frame.canvas.width = this.frame.canvas.clientWidth; + this.frame.canvas.height = this.frame.canvas.clientHeight; + + if (this.slider) { + this.slider.redraw(); + } +}; + +/** + * Convert a Google DataTable to a Javascript Array + * @param {google.visualization.DataTable} table + * @return {Array} array + */ +Graph.tableToArray = function(table) { + var array = []; + var col; + + // read the column names + var colCount = table.getNumberOfColumns(); + var cols = {}; + for (col = 0; col < colCount; col++) { + var label = table.getColumnLabel(col); + cols[label] = col; + } + + var rowCount = table.getNumberOfRows(); + for (var i = 0; i < rowCount; i++) { + // copy all properties from the table columns to an object + var properties = {}; + for (col in cols) { + if (cols.hasOwnProperty(col)) { + properties[col] = table.getValue(i, cols[col]); + } + } + + array.push(properties); + } + + return array; +}; + +/** + * Append nodes + * Nodes with a duplicate id will be replaced + * @param {google.visualization.DataTable | Array} nodesTable The data containing the nodes. + */ +Graph.prototype.addNodes = function(nodesTable) { + var table; + if (typeof google !== 'undefined' && google.visualization && google.visualization.DataTable && + nodesTable instanceof google.visualization.DataTable) { + // Google DataTable. + // Convert to a Javascript Array + table = Graph.tableToArray(nodesTable); + } + else if (Graph.isArray(nodesTable)){ + // Javascript Array + table = nodesTable; + } + else { + return; + } + + var hasValues = false; + var rowCount = table.length; + for (var i = 0; i < rowCount; i++) { + var properties = table[i]; + + if (properties.value != undefined) { + hasValues = true; + } + if (properties.id == undefined) { + throw "Column 'id' missing in table with nodes (row " + i + ")"; + } + + this._createNode(properties); + } + + // calculate scaling function when value is provided + if (hasValues) { + this._updateValueRange(this.nodes); + } + + this.start(); +}; + +/** + * Load all nodes by reading the data table nodesTable + * Note that Object DataTable is defined in google.visualization.DataTable + * @param {google.visualization.DataTable | Array} nodesTable The data containing the nodes. + */ +Graph.prototype.setNodes = function(nodesTable) { + var table; + if (typeof google !== 'undefined' && google.visualization && google.visualization.DataTable && + nodesTable instanceof google.visualization.DataTable) { + // Google DataTable. + // Convert to a Javascript Array + table = Graph.tableToArray(nodesTable); + } + else if (Graph.isArray(nodesTable)){ + // Javascript Array + table = nodesTable; + } + else { + return; + } + + this.hasMovingNodes = false; + this.nodesTable = table; + this.nodes = []; + this.selection = []; + + var hasValues = false; + var rowCount = table.length; + for (var i = 0; i < rowCount; i++) { + var properties = table[i]; + + if (properties.value != undefined) { + hasValues = true; + } + if (properties.timestamp) { + this.hasTimestamps = this.hasTimestamps || properties.timestamp; + } + if (properties.id == undefined) { + throw "Column 'id' missing in table with nodes (row " + i + ")"; + } + this._createNode(properties); + } + + // calculate scaling function when value is provided + if (hasValues) { + this._updateValueRange(this.nodes); + } +}; + +/** + * Filter the current nodes table for nodes with a timestamp older than given + * timestamp. Can only be used for nodes added via setNodes(), not via + * addNodes(). + * @param {*} [timestamp] If timestamp is undefined, all nodes are shown + */ +Graph.prototype._filterNodes = function(timestamp) { + if (this.nodesTable == undefined) { + return; + } + + // remove existing nodes with a too new timestamp + if (timestamp !== undefined) { + var ns = this.nodes; + var n = 0; + while (n < ns.length) { + var t = ns[n].timestamp; + if (t !== undefined && t > timestamp) { + // remove this node + ns.splice(n, 1); + } + else { + n++; + } + } + } + + // add all nodes with an old enough timestamp + var table = this.nodesTable; + var rowCount = table.length; + for (var i = 0; i < rowCount; i++) { + // copy all properties + var properties = table[i]; + + if (properties.id === undefined) { + throw "Column 'id' missing in table with nodes (row " + i + ")"; + } + + // check what the timestamp is + var ts = properties.timestamp ? properties.timestamp : undefined; + + var visible = true; + if (ts !== undefined && timestamp !== undefined && ts > timestamp) { + visible = false; + } + + if (visible) { + // create or update the node + this._createNode(properties); + } + } + + this.start(); +}; + +/** + * Create a node with the given properties + * If the new node has an id identical to an existing package, the existing + * node will be overwritten. + * The properties can contain a property "action", which can have values + * "create", "update", or "delete" + * @param {Object} properties An object with properties + */ +Graph.prototype._createNode = function(properties) { + var action = properties.action ? properties.action : "update"; + var id, index, newNode, oldNode; + + if (action === "create") { + // create the node + newNode = new Graph.Node(properties, this.images, this.groups, this.constants); + id = properties.id; + index = (id !== undefined) ? this._findNode(id) : undefined; + + if (index !== undefined) { + // replace node + oldNode = this.nodes[index]; + this.nodes[index] = newNode; + + // remove selection of old node + if (oldNode.selected) { + this._unselectNodes([{'row': index}], false); + } + + /* TODO: implement this? -> will give performance issues, searching all edges and node... + // update edges linking to this node + var edgesTable = this.edges; + for (var i = 0, iMax = edgesTable.length; i < iMax; i++) { + var edge = edgesTable[i]; + if (edge.from == oldNode) { + edge.from = newNode; + } + if (edge.to == oldNode) { + edge.to = newNode; + } + } + + // update packages linking to this node + var packagesTable = this.packages; + for (var i = 0, iMax = packagesTable.length; i < iMax; i++) { + var package = packagesTable[i]; + if (package.from == oldNode) { + package.from = newNode; + } + if (package.to == oldNode) { + package.to = newNode; + } + } + */ + } + else { + // add new node + this.nodes.push(newNode); + } + + if (!newNode.isFixed()) { + // note: no not use node.isMoving() here, as that gives the current + // velocity of the node, which is zero after creation of the node. + this.hasMovingNodes = true; + } + } + else if (action === "update") { + // update existing node, or create it when not yet existing + id = properties.id; + if (id === undefined) { + throw "Cannot update a node without id"; + } + + index = this._findNode(id); + if (index !== undefined) { + // update node + this.nodes[index].setProperties(properties, this.constants); + } + else { + // create node + newNode = new Graph.Node(properties, this.images, this.groups, this.constants); + this.nodes.push(newNode); + + if (!newNode.isFixed()) { + // note: no not use node.isMoving() here, as that gives the current + // velocity of the node, which is zero after creation of the node. + this.hasMovingNodes = true; + } + } + } + else if (action === "delete") { + // delete existing node + id = properties.id; + if (id === undefined) { + throw "Cannot delete node without its id"; + } + + index = this._findNode(id); + if (index !== undefined) { + oldNode = this.nodes[index]; + // remove selection of old node + if (oldNode.selected) { + this._unselectNodes([{'row': index}], false); + } + this.nodes.splice(index, 1); + } + else { + throw "Node with id " + id + " not found"; + } + } + else { + throw "Unknown action " + action + ". Choose 'create', 'update', or 'delete'."; + } +}; + +/** + * Find a node by its id + * @param {Number} id Id of the node + * @return {Number} index Index of the node in the array this.nodes, or + * undefined when not found. * + */ +Graph.prototype._findNode = function (id) { + var nodes = this.nodes; + for (var n = 0, len = nodes.length; n < len; n++) { + if (nodes[n].id === id) { + return n; + } + } + + return undefined; +}; + +/** + * Find a node by its rowNumber + * @param {Number} row Row number of the node + * @return {Graph.Node} node     The node with the given row number, or + *                            undefined when not found. + */ +Graph.prototype._findNodeByRow = function (row) { + return this.nodes[row]; +}; + +/** + * Load edges by reading the data table + * Note that Object DataTable is defined in google.visualization.DataTable + * @param {google.visualization.DataTable | Array} edgesTable The data containing the edges. + */ +Graph.prototype.setEdges = function(edgesTable) { + var table; + if (typeof google !== 'undefined' && google.visualization && google.visualization.DataTable && + edgesTable instanceof google.visualization.DataTable) { + // Google DataTable. + // Convert to a Javascript Array + table = Graph.tableToArray(edgesTable); + } + else if (Graph.isArray(edgesTable)){ + // Javascript Array + table = edgesTable; + } + else { + return; + } + + this.edgesTable = table; + this.edges = []; + this.hasMovingEdges = false; + + var hasValues = false; + var rowCount = table.length; + for (var i = 0; i < rowCount; i++) { + var properties = table[i]; + + if (properties.from === undefined) { + throw "Column 'from' missing in table with edges (row " + i + ")"; + } + if (properties.to === undefined) { + throw "Column 'to' missing in table with edges (row " + i + ")"; + } + if (properties.timestamp != undefined) { + this.hasTimestamps = this.hasTimestamps || properties.timestamp; + } + if (properties.value != undefined) { + hasValues = true; + } + + this._createEdge(properties); + } + + // calculate scaling function when value is provided + if (hasValues) { + this._updateValueRange(this.edges); + } +}; + + +/** + * Load edges by reading the data table + * Note that Object DataTable is defined in google.visualization.DataTable + * @param {google.visualization.DataTable | Array} edgesTable The data containing the edges. + */ +Graph.prototype.addEdges = function(edgesTable) { + var table; + if (typeof google !== 'undefined' && google.visualization && google.visualization.DataTable && + edgesTable instanceof google.visualization.DataTable) { + // Google DataTable. + // Convert to a Javascript Array + table = Graph.tableToArray(edgesTable); + } + else if (Graph.isArray(edgesTable)){ + // Javascript Array + table = edgesTable; + } + else { + return; + } + + var hasValues = false; + var rowCount = table.length; + for (var i = 0; i < rowCount; i++) { + // copy all properties + var properties = table[i]; + + if (properties.from === undefined) { + throw "Column 'from' missing in table with edges (row " + i + ")"; + } + if (properties.to === undefined) { + throw "Column 'to' missing in table with edges (row " + i + ")"; + } + if (properties.value != undefined) { + hasValues = true; + } + + this._createEdge(properties); + } + + // calculate scaling function when value is provided + if (hasValues) { + this._updateValueRange(this.edges); + } + + this.start(); +}; + + +/** + * Filter the current edges table for edges with a timestamp below given + * timestamp. Can only be used for edges added via setEdges(), not via + * addEdges(). + * @param {*} [timestamp] If timestamp is undefined, all edges are shown + */ +Graph.prototype._filterEdges = function(timestamp) { + if (this.edgesTable == undefined) { + return; + } + + // remove existing packages with a too new timestamp + if (timestamp !== undefined) { + var ls = this.edges; + var l = 0; + while (l < ls.length) { + var t = ls[l].timestamp; + if (t !== undefined && t > timestamp) { + // remove this edge + ls.splice(l, 1); + } + else { + l++; + } + } + } + + // add all edges with an old enough timestamp + var table = this.edgesTable; + var rowCount = table.length; + for (var i = 0; i < rowCount; i++) { + var properties = table[i]; + + if (properties.from === undefined) { + throw "Column 'from' missing in table with edges (row " + i + ")"; + } + if (properties.to === undefined) { + throw "Column 'to' missing in table with edges (row " + i + ")"; + } + + // check what the timestamp is + var ts = properties.timestamp ? properties.timestamp : undefined; + + var visible = true; + if (ts !== undefined && timestamp !== undefined && ts > timestamp) { + visible = false; + } + + if (visible) { + // create or update the edge + this._createEdge(properties); + } + } + + this.start(); +}; + +/** + * Create a edge with the given properties + * If the new edge has an id identical to an existing edge, the existing + * edge will be overwritten or updated. + * The properties can contain a property "action", which can have values + * "create", "update", or "delete" + * @param {Object} properties An object with properties + */ +Graph.prototype._createEdge = function(properties) { + var action = properties.action ? properties.action : "create"; + var id, index, edge, oldEdge, newEdge; + + if (action === "create") { + // create the edge, or replace it if already existing + id = properties.id; + index = (id !== undefined) ? this._findEdge(id) : undefined; + edge = new Graph.Edge(properties, this, this.constants); + + if (index !== undefined) { + // replace existing edge + oldEdge = this.edges[index]; + oldEdge.from.detachEdge(oldEdge); + oldEdge.to.detachEdge(oldEdge); + this.edges[index] = edge; + } + else { + // add new edge + this.edges.push(edge); + } + edge.from.attachEdge(edge); + edge.to.attachEdge(edge); + + if (edge.isMoving()) { + this.hasMovingEdges = true; + } + } + else if (action === "update") { + // update existing edge, or create the edge if not existing + id = properties.id; + if (id === undefined) { + throw "Cannot update a edge without id"; + } + + index = this._findEdge(id); + if (index !== undefined) { + // update edge + edge = this.edges[index]; + edge.from.detachEdge(edge); + edge.to.detachEdge(edge); + + edge.setProperties(properties, this.constants); + edge.from.attachEdge(edge); + edge.to.attachEdge(edge); + } + else { + // add new edge + edge = new Graph.Edge(properties, this, this.constants); + edge.from.attachEdge(edge); + edge.to.attachEdge(edge); + this.edges.push(edge); + if (edge.isMoving()) { + this.hasMovingEdges = true; + } + } + } + else if (action === "delete") { + // delete existing edge + id = properties.id; + if (id === undefined) { + throw "Cannot delete edge without its id"; + } + + index = this._findEdge(id); + if (index !== undefined) { + oldEdge = this.edges[id]; + edge.from.detachEdge(oldEdge); + edge.to.detachEdge(oldEdge); + this.edges.splice(index, 1); + } + else { + throw "Edge with id " + id + " not found"; + } + } + else { + throw "Unknown action " + action + ". Choose 'create', 'update', or 'delete'."; + } +}; + +/** + * Update the edge to oldNode in all edges and packages. + * @param {Node} oldNode + * @param {Node} newNode + */ +// TODO: start utilizing this method _updateNodeReferences +Graph.prototype._updateNodeReferences = function(oldNode, newNode) { + var arrays = [this.edges, this.packages]; + for (var a = 0, aMax = arrays.length; a < aMax; a++) { + var array = arrays[a]; + for (var i = 0, iMax = array.length; i < iMax; i++) { + if (array.from === oldNode) { + array.from = newNode; + } + if (array.to === oldNode) { + array.to = newNode; + } + } + } +}; + +/** + * Find a edge by its id + * @param {Number} id Id of the edge + * @return {Number} index Index of the edge in the array this.edges, or + * undefined when not found. * + */ +Graph.prototype._findEdge = function (id) { + var edges = this.edges; + for (var n = 0, len = edges.length; n < len; n++) { + if (edges[n].id === id) { + return n; + } + } + + return undefined; +}; + +/** + * Find a edge by its row + * @param {Number} row Row of the edge + * @return {Graph.Edge} the found edge, or undefined when not found + */ +Graph.prototype._findEdgeByRow = function (row) { + return this.edges[row]; +}; + +/** + * Append packages + * Packages with a duplicate id will be replaced + * Note that Object DataTable is defined in google.visualization.DataTable + * @param {google.visualization.DataTable | Array} packagesTable The data containing the packages. + */ +Graph.prototype.addPackages = function(packagesTable) { + var table; + if (typeof google !== 'undefined' && google.visualization && google.visualization.DataTable && + packagesTable instanceof google.visualization.DataTable) { + // Google DataTable. + // Convert to a Javascript Array + table = Graph.tableToArray(packagesTable); + } + else if (Graph.isArray(packagesTable)){ + // Javascript Array + table = packagesTable; + } + else { + return; + } + + var rowCount = table.length; + for (var i = 0; i < rowCount; i++) { + var properties = table[i]; + + if (properties.from === undefined) { + throw "Column 'from' missing in table with packages (row " + i + ")"; + } + if (properties.to === undefined) { + throw "Column 'to' missing in table with packages (row " + i + ")"; + } + + this._createPackage(properties); + } + + // calculate scaling function when value is provided + this._updateValueRange(this.packages); + + this.start(); +}; + +/** + * Set a new packages table + * Packages with a duplicate id will be replaced + * Note that Object DataTable is defined in google.visualization.DataTable + * @param {google.visualization.DataTable | Array} packagesTable The data containing the packages. + */ +Graph.prototype.setPackages = function(packagesTable) { + var table; + if (typeof google !== 'undefined' && google.visualization && google.visualization.DataTable && + packagesTable instanceof google.visualization.DataTable) { + // Google DataTable. + // Convert to a Javascript Array + table = Graph.tableToArray(packagesTable); + } + else if (Graph.isArray(packagesTable)){ + // Javascript Array + table = packagesTable; + } + else { + return; + } + + this.packagesTable = table; + this.packages = []; + + var rowCount = table.length; + for (var i = 0; i < rowCount; i++) { + var properties = table[i]; + + if (properties.from === undefined) { + throw "Column 'from' missing in table with packages (row " + i + ")"; + } + if (properties.to === undefined) { + throw "Column 'to' missing in table with packages (row " + i + ")"; + } + if (properties.timestamp) { + this.hasTimestamps = this.hasTimestamps || properties.timestamp; + } + + this._createPackage(properties); + } + + // calculate scaling function when value is provided + this._updateValueRange(this.packages); + + /* TODO: adjust examples and documentation for this? + this.start(); + */ +}; + +/** + * Filter the current package table for packages with a timestamp below given + * timestamp. Can only be used for packages added via setPackages(), not via + * addPackages(). + * @param {*} [timestamp] If timestamp is undefined, all packages are shown + */ +Graph.prototype._filterPackages = function(timestamp) { + if (this.packagesTable == undefined) { + return; + } + + // remove all current packages + this.packages = []; + + /* TODO: cleanup + // remove existing packages with a too new timestamp + if (timestamp !== undefined) { + var packages = this.packages; + var p = 0; + while (p < packages.length) { + var package = packages[p]; + var t = package.timestamp; + + if (t !== undefined && t > timestamp ) { + // remove this package + packages.splice(p, 1); + } + else { + p++; + } + } + } + */ + + // add all packages with an old enough timestamp + var table = this.packagesTable; + var rowCount = table.length; + for (var i = 0; i < rowCount; i++) { + var properties = table[i]; + + if (properties.from === undefined) { + throw "Column 'from' missing in table with packages (row " + i + ")"; + } + if (properties.to === undefined) { + throw "Column 'to' missing in table with packages (row " + i + ")"; + } + // check what the timestamp is + var pTimestamp = properties.timestamp ? properties.timestamp : undefined; + + var visible = true; + if (pTimestamp !== undefined && timestamp !== undefined && pTimestamp > timestamp) { + visible = false; + } + + if (visible === true) { + if (properties.progress == undefined) { + // when no progress is provided, we need to add our own progress + var duration = properties.duration || this.constants.packages.duration; // seconds + + var diff = (timestamp.getTime() - pTimestamp.getTime()) / 1000; // seconds + if (diff < duration) { + // copy the properties, and fill in the current progress based on the + // timestamp and the duration + var original = properties; + properties = {}; + for (var j in original) { + if (original.hasOwnProperty(j)) { + properties[j] = original[j]; + } + } + + properties.progress = diff / duration; // scale 0-1 + } + else { + visible = false; + } + } + } + + if (visible === true) { + // create or update the package + this._createPackage(properties); + } + } + + this.start(); +}; + +/** + * Create a package with the given properties + * If the new package has an id identical to an existing package, the existing + * package will be overwritten. + * The properties can contain a property "action", which can have values + * "create", "update", or "delete" + * @param {Object} properties An object with properties + */ +Graph.prototype._createPackage = function(properties) { + var action = properties.action ? properties.action : "create"; + var id, index, newPackage; + + if (action === "create") { + // create the package + id = properties.id; + index = (id !== undefined) ? this._findPackage(id) : undefined; + newPackage = new Graph.Package(properties, this, this.images, this.constants); + + if (index !== undefined) { + // replace existing package + this.packages[index] = newPackage; + } + else { + // add new package + this.packages.push(newPackage); + } + + if (newPackage.isMoving()) { + this.hasMovingPackages = true; + } + } + else if (action === "update") { + // update a package, or create it when not existing + id = properties.id; + if (id === undefined) { + throw "Cannot update a edge without id"; + } + + index = this._findPackage(id); + if (index !== undefined) { + // update existing package + this.packages[index].setProperties(properties, this.constants); + } + else { + // add new package + newPackage = new Graph.Package(properties, this, this.images, this.constants); + this.packages.push(newPackage); + if (newPackage.isMoving()) { + this.hasMovingPackages = true; + } + } + } + else if (action === "delete") { + // delete existing package + id = properties.id; + if (id === undefined) { + throw "Cannot delete package without its id"; + } + + index = this._findPackage(id); + if (index !== undefined) { + this.packages.splice(index, 1); + } + else { + throw "Package with id " + id + " not found"; + } + } + else { + throw "Unknown action " + action + ". Choose 'create', 'update', or 'delete'."; + } +}; + +/** + * Find a package by its id. + * @param {Number} id + * @return {Number} index Index of the package in the array this.packages, + * or undefined when not found + */ +Graph.prototype._findPackage = function (id) { + var packages = this.packages; + for (var n = 0, len = packages.length; n < len; n++) { + if (packages[n].id === id) { + return n; + } + } + + return undefined; +}; + +/** + * Find a package by its row + * @param {Number} row Row of the package + * @return {Graph.Package} the found package, or undefined when not found + */ +Graph.prototype._findPackageByRow = function (row) { + return this.packages[row]; +}; + +/** + * Retrieve an object which maps the column ids by their names + * For example a table with columns [id, name, value] will return an + * object {"id": 0, "name": 1, "value": 2} + * @param {google.visualization.DataTable} table A google datatable + * @return {Object} columnIds An object + */ +// TODO: cleanup this unused method +Graph.prototype._getColumnNames = function (table) { + var colCount = table.getNumberOfColumns(); + var cols = {}; + for (var col = 0; col < colCount; col++) { + var label = table.getColumnLabel(col); + cols[label] = col; + } + return cols; +}; + + +/** + * Update the values of all object in the given array according to the current + * value range of the objects in the array. + * @param {Array} array. An array with objects like Edges, Nodes, or Packages + * The objects must have a method getValue() and + * setValueRange(min, max). + */ +Graph.prototype._updateValueRange = function(array) { + var count = array.length; + var i; + + // determine the range of the node values + var valueMin = undefined; + var valueMax = undefined; + for (i = 0; i < count; i++) { + var value = array[i].getValue(); + if (value !== undefined) { + valueMin = (valueMin === undefined) ? value : Math.min(value, valueMin); + valueMax = (valueMax === undefined) ? value : Math.max(value, valueMax); + } + } + + // adjust the range of all nodes + if (valueMin !== undefined && valueMax !== undefined) { + for (i = 0; i < count; i++) { + array[i].setValueRange(valueMin, valueMax); + } + } +}; + + +/** + * Set the current timestamp. All packages with a timestamp smaller or equal + * than the given timestamp will be drawn. + * @param {Date | Number} timestamp + */ +Graph.prototype.setTimestamp = function(timestamp) { + this._filterNodes(timestamp); + this._filterEdges(timestamp); + this._filterPackages(timestamp); +}; + + +/** + * Get the range of all timestamps defined in the nodes, edges and packages + * @return {Object} A range object, containing parameters start and end. + */ +Graph.prototype._getRange = function() { + // range is stored as number. at the end of the method, it is converted to + // Date when needed. + var range = { + "start": undefined, + "end": undefined + }; + + var tables = [this.nodesTable, this.edgesTable]; + for (var t = 0, tMax = tables.length; t < tMax; t++) { + var table = tables[t]; + + if (table !== undefined) { + for (var i = 0, iMax = table.length; i < iMax; i++) { + var timestamp = table[i].timestamp; + if (timestamp) { + // to long + if (timestamp instanceof Date) { + timestamp = timestamp.getTime(); + } + + // calculate new range + range.start = range.start ? Math.min(timestamp, range.start) : timestamp; + range.end = range.end ? Math.max(timestamp, range.end) : timestamp; + } + } + } + } + + // calculate the range for the packagesTable by hand. In case of packages + // without a progress provided, we need to calculate the end time by hand. + if (this.packagesTable) { + var packagesTable = this.packagesTable; + for (var row = 0, len = packagesTable.length; row < len; row ++) { + var pkg = packagesTable[row], + timestamp = pkg.timestamp, + progress = pkg.progress, + duration = pkg.duration || this.constants.packages.duration; + + // convert to number + if (timestamp instanceof Date) { + timestamp = timestamp.getTime(); + } + + if (timestamp != undefined) { + var start = timestamp, + end = progress ? timestamp : (timestamp + duration * 1000); + + range.start = range.start ? Math.min(start, range.start) : start; + range.end = range.end ? Math.max(end, range.end) : end; + } + } + } + + // convert to the right type: number or date + var rangeFormat = { + "start": new Date(range.start), + "end": new Date(range.end) + }; + + return rangeFormat; +}; + +/** + * Start animation. + * Only applicable when packages with a timestamp are available + */ +Graph.prototype.animationStart = function() { + if (this.slider) { + this.slider.play(); + } +}; + +/** + * Start animation. + * Only applicable when packages with a timestamp are available + */ +Graph.prototype.animationStop = function() { + if (this.slider) { + this.slider.stop(); + } +}; + +/** + * Set framerate for the animation. + * Only applicable when packages with a timestamp are available + * @param {number} framerate The framerate in frames per second + */ +Graph.prototype.setAnimationFramerate = function(framerate) { + if (this.slider) { + this.slider.setFramerate(framerate); + } +} + +/** + * Set the duration of playing the whole package history + * Only applicable when packages with a timestamp are available + * @param {number} duration The duration in seconds + */ +Graph.prototype.setAnimationDuration = function(duration) { + if (this.slider) { + this.slider.setDuration(duration); + } +}; + +/** + * Set the time acceleration for playing the history. + * Only applicable when packages with a timestamp are available + * @param {number} acceleration Acceleration, for example 10 means play + * ten times as fast as real time. A value + * of 1 will play the history in real time. + */ +Graph.prototype.setAnimationAcceleration = function(acceleration) { + if (this.slider) { + this.slider.setAcceleration(acceleration); + } +}; + +/** + * Redraw the graph with the current data + * chart will be resized too. + */ +Graph.prototype.redraw = function() { + this._setSize(this.width, this.height); + + this._redraw(); +}; + +/** + * Redraw the graph with the current data + */ +Graph.prototype._redraw = function() { + var ctx = this.frame.canvas.getContext("2d"); + + // clear the canvas + var w = this.frame.canvas.width; + var h = this.frame.canvas.height; + ctx.clearRect(0, 0, w, h); + + // set scaling and translation + ctx.save(); + ctx.translate(this.translation.x, this.translation.y); + ctx.scale(this.scale, this.scale); + + this._drawEdges(ctx); + this._drawNodes(ctx); + this._drawPackages(ctx); + this._drawSlider(); + + // restore original scaling and translation + ctx.restore(); +}; + +/** + * Set the translation of the graph + * @param {Number} offsetX Horizontal offset + * @param {Number} offsetY Vertical offset + */ +Graph.prototype._setTranslation = function(offsetX, offsetY) { + if (this.translation === undefined) { + this.translation = { + "x": 0, + "y": 0 + }; + } + + if (offsetX !== undefined) { + this.translation.x = offsetX; + } + if (offsetY !== undefined) { + this.translation.y = offsetY; + } +}; + +/** + * Get the translation of the graph + * @return {Object} translation An object with parameters x and y, both a number + */ +Graph.prototype._getTranslation = function() { + return { + "x": this.translation.x, + "y": this.translation.y + }; +}; + +/** + * Scale the graph + * @param {Number} scale Scaling factor 1.0 is unscaled + */ +Graph.prototype._setScale = function(scale) { + this.scale = scale; +}; +/** + * Get the current scale of the graph + * @return {Number} scale Scaling factor 1.0 is unscaled + */ +Graph.prototype._getScale = function() { + return this.scale; +}; + +Graph.prototype._xToCanvas = function(x) { + return (x - this.translation.x) / this.scale; +}; + +Graph.prototype._canvasToX = function(x) { + return x * this.scale + this.translation.x; +}; + +Graph.prototype._yToCanvas = function(y) { + return (y - this.translation.y) / this.scale; +}; + +Graph.prototype._canvasToY = function(y) { + return y * this.scale + this.translation.y ; +}; + + + +/** + * Get a node by its id + * @param {number} id + * @return {Node} node, or null if not found + */ +Graph.prototype._getNode = function(id) { + for (var i = 0; i < this.nodes.length; i++) { + if (this.nodes[i].id == id) + return this.nodes[i]; + } + + return null; +}; + +/** + * Redraw all nodes + * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d"); + * @param {CanvasRenderingContext2D} ctx + */ +Graph.prototype._drawNodes = function(ctx) { + // first draw the unselected nodes + var nodes = this.nodes; + var selected = []; + for (var i = 0, iMax = nodes.length; i < iMax; i++) { + if (nodes[i].isSelected()) { + selected.push(i); + } + else { + nodes[i].draw(ctx); + } + } + + // draw the selected nodes on top + for (var s = 0, sMax = selected.length; s < sMax; s++) { + nodes[selected[s]].draw(ctx); + } +}; + +/** + * Redraw all edges + * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d"); + * @param {CanvasRenderingContext2D} ctx + */ +Graph.prototype._drawEdges = function(ctx) { + var edges = this.edges; + for (var i = 0, iMax = edges.length; i < iMax; i++) { + edges[i].draw(ctx); + } +}; + +/** + * Redraw all packages + * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d"); + * @param {CanvasRenderingContext2D} ctx + */ +Graph.prototype._drawPackages = function(ctx) { + var packages = this.packages; + for (var i = 0, iMax = packages.length; i < iMax; i++) { + packages[i].draw(ctx); + } +}; + + +/** + * Redraw the filter + */ +Graph.prototype._drawSlider = function() { + var sliderNode; + if (this.hasTimestamps) { + sliderNode = this.frame.slider; + if (sliderNode === undefined) { + sliderNode = document.createElement( "div" ); + sliderNode.style.position = "absolute"; + sliderNode.style.bottom = "0px"; + sliderNode.style.left = "0px"; + sliderNode.style.right = "0px"; + sliderNode.style.backgroundColor = "rgba(255, 255, 255, 0.7)"; + + this.frame.slider = sliderNode; + this.frame.slider.style.padding = "10px"; + //this.frame.filter.style.backgroundColor = "#EFEFEF"; + this.frame.appendChild(sliderNode); + + + var range = this._getRange(); + this.slider = new Graph.Slider(sliderNode); + this.slider.setLoop(false); + this.slider.setRange(range.start, range.end); + + // create an event handler + var me = this; + var onchange = function () { + var timestamp = me.slider.getValue(); + me.setTimestamp(timestamp); + // TODO: do only a redraw when the graph is not still moving + me.redraw(); + }; + this.slider.setOnChangeCallback(onchange); + onchange(); // perform the first update by hand. + } + } + else { + sliderNode = this.frame.slider; + if (sliderNode !== undefined) { + this.frame.removeChild(sliderNode); + this.frame.slider = undefined; + this.slider = undefined; + } + } +}; + +/** + * Recalculate the best positions for all nodes + */ +Graph.prototype._reposition = function() { + // TODO: implement function reposition + + + /* + var w = this.frame.canvas.clientWidth; + var h = this.frame.canvas.clientHeight; + for (var i = 0; i < this.nodes.length; i++) { + if (!this.nodes[i].xFixed) this.nodes[i].x = w * Math.random(); + if (!this.nodes[i].yFixed) this.nodes[i].y = h * Math.random(); + } + //*/ + + //* + // TODO + var radius = this.constants.edges.length * 2; + var cx = this.frame.canvas.clientWidth / 2; + var cy = this.frame.canvas.clientHeight / 2; + for (var i = 0; i < this.nodes.length; i++) { + var angle = 2*Math.PI * (i / this.nodes.length); + + if (!this.nodes[i].xFixed) this.nodes[i].x = cx + radius * Math.cos(angle); + if (!this.nodes[i].yFixed) this.nodes[i].y = cy + radius * Math.sin(angle); + + } + //*/ + + /* + // TODO + var radius = this.constants.edges.length * 2; + var w = this.frame.canvas.clientWidth, + h = this.frame.canvas.clientHeight; + var cx = this.frame.canvas.clientWidth / 2; + var cy = this.frame.canvas.clientHeight / 2; + var s = Math.sqrt(this.nodes.length); + for (var i = 0; i < this.nodes.length; i++) { + //var angle = 2*Math.PI * (i / this.nodes.length); + + if (!this.nodes[i].xFixed) this.nodes[i].x = w/s * (i % s); + if (!this.nodes[i].yFixed) this.nodes[i].y = h/s * (i / s); + } + //*/ + + + /* + var cx = this.frame.canvas.clientWidth / 2; + var cy = this.frame.canvas.clientHeight / 2; + for (var i = 0; i < this.nodes.length; i++) { + this.nodes[i].x = cx; + this.nodes[i].y = cy; + } + + //*/ + +}; + + +/** + * Find a stable position for all nodes + */ +Graph.prototype._doStabilize = function() { + var start = new Date(); + + // find stable position + var count = 0; + var vmin = this.constants.minVelocity; + var stable = false; + while (!stable && count < this.constants.maxIterations) { + this._calculateForces(); + this._discreteStepNodes(); + stable = !this.isMoving(vmin); + count++; + } + + var end = new Date(); + + //console.log("Stabilized in " + (end-start) + " ms, " + count + " iterations" ); // TODO: cleanup +}; + +/** + * Calculate the external forces acting on the nodes + * Forces are caused by: edges, repulsing forces between nodes, gravity + */ +Graph.prototype._calculateForces = function(nodeId) { + // create a local edge to the nodes and edges, that is faster + var nodes = this.nodes, + edges = this.edges; + + // gravity, add a small constant force to pull the nodes towards the center of + // the graph + // Also, the forces are reset to zero in this loop by using _setForce instead + // of _addForce + var gravity = 0.01, + gx = this.frame.canvas.clientWidth / 2, + gy = this.frame.canvas.clientHeight / 2; + for (var n = 0; n < nodes.length; n++) { + var dx = gx - nodes[n].x, + dy = gy - nodes[n].y, + angle = Math.atan2(dy, dx), + fx = Math.cos(angle) * gravity, + fy = Math.sin(angle) * gravity; + + this.nodes[n]._setForce(fx, fy); + } + + // repulsing forces between nodes + var minimumDistance = this.constants.nodes.distance, + steepness = 10; // higher value gives steeper slope of the force around the given minimumDistance + for (var n = 0; n < nodes.length; n++) { + for (var n2 = n + 1; n2 < this.nodes.length; n2++) { + //var dmin = (nodes[n].width + nodes[n].height + nodes[n2].width + nodes[n2].height) / 1 || minimumDistance, // TODO: dmin + //var dmin = (nodes[n].width + nodes[n2].width)/2 || minimumDistance, // TODO: dmin + //dmin = 40 + ((nodes[n].width/2 + nodes[n2].width/2) || 0), + + // calculate normally distributed force + var dx = nodes[n2].x - nodes[n].x, + dy = nodes[n2].y - nodes[n].y, + distance = Math.sqrt(dx * dx + dy * dy), + angle = Math.atan2(dy, dx), + + // TODO: correct factor for repulsing force + //var repulsingforce = 2 * Math.exp(-5 * (distance * distance) / (dmin * dmin) ); // TODO: customize the repulsing force + //repulsingforce = Math.exp(-1 * (distance * distance) / (dmin * dmin) ), // TODO: customize the repulsing force + repulsingforce = 1 / (1 + Math.exp((distance / minimumDistance - 1) * steepness)), // TODO: customize the repulsing force + fx = Math.cos(angle) * repulsingforce, + fy = Math.sin(angle) * repulsingforce; + + this.nodes[n]._addForce(-fx, -fy); + this.nodes[n2]._addForce(fx, fy); + } + /* TODO: re-implement repulsion of edges + for (var l = 0; l < edges.length; l++) { + var lx = edges[l].from.x+(edges[l].to.x - edges[l].from.x)/2, + ly = edges[l].from.y+(edges[l].to.y - edges[l].from.y)/2, + + // calculate normally distributed force + dx = nodes[n].x - lx, + dy = nodes[n].y - ly, + distance = Math.sqrt(dx * dx + dy * dy), + angle = Math.atan2(dy, dx), + + + // TODO: correct factor for repulsing force + //var repulsingforce = 2 * Math.exp(-5 * (distance * distance) / (dmin * dmin) ); // TODO: customize the repulsing force + //repulsingforce = Math.exp(-1 * (distance * distance) / (dmin * dmin) ), // TODO: customize the repulsing force + repulsingforce = 1 / (1 + Math.exp((distance / (minimumDistance / 2) - 1) * steepness)), // TODO: customize the repulsing force + fx = Math.cos(angle) * repulsingforce, + fy = Math.sin(angle) * repulsingforce; + nodes[n]._addForce(fx, fy); + edges[l].from._addForce(-fx/2,-fy/2); + edges[l].to._addForce(-fx/2,-fy/2); + } + */ + } + + // forces caused by the edges, modelled as springs + for (var l = 0, lMax = edges.length; l < lMax; l++) { + var edge = edges[l], + + dx = (edge.to.x - edge.from.x), + dy = (edge.to.y - edge.from.y), + //edgeLength = (edge.from.width + edge.from.height + edge.to.width + edge.to.height)/2 || edge.length, // TODO: dmin + //edgeLength = (edge.from.width + edge.to.width)/2 || edge.length, // TODO: dmin + //edgeLength = 20 + ((edge.from.width + edge.to.width) || 0) / 2, + edgeLength = edge.length, + length = Math.sqrt(dx * dx + dy * dy), + angle = Math.atan2(dy, dx), + + springforce = edge.stiffness * (edgeLength - length), + + fx = Math.cos(angle) * springforce, + fy = Math.sin(angle) * springforce; + + edge.from._addForce(-fx, -fy); + edge.to._addForce(fx, fy); + } + + /* TODO: re-implement repulsion of edges + // repulsing forces between edges + var minimumDistance = this.constants.edges.distance, + steepness = 10; // higher value gives steeper slope of the force around the given minimumDistance + for (var l = 0; l < edges.length; l++) { + //Keep distance from other edge centers + for (var l2 = l + 1; l2 < this.edges.length; l2++) { + //var dmin = (nodes[n].width + nodes[n].height + nodes[n2].width + nodes[n2].height) / 1 || minimumDistance, // TODO: dmin + //var dmin = (nodes[n].width + nodes[n2].width)/2 || minimumDistance, // TODO: dmin + //dmin = 40 + ((nodes[n].width/2 + nodes[n2].width/2) || 0), + var lx = edges[l].from.x+(edges[l].to.x - edges[l].from.x)/2, + ly = edges[l].from.y+(edges[l].to.y - edges[l].from.y)/2, + l2x = edges[l2].from.x+(edges[l2].to.x - edges[l2].from.x)/2, + l2y = edges[l2].from.y+(edges[l2].to.y - edges[l2].from.y)/2, + + // calculate normally distributed force + dx = l2x - lx, + dy = l2y - ly, + distance = Math.sqrt(dx * dx + dy * dy), + angle = Math.atan2(dy, dx), + + + // TODO: correct factor for repulsing force + //var repulsingforce = 2 * Math.exp(-5 * (distance * distance) / (dmin * dmin) ); // TODO: customize the repulsing force + //repulsingforce = Math.exp(-1 * (distance * distance) / (dmin * dmin) ), // TODO: customize the repulsing force + repulsingforce = 1 / (1 + Math.exp((distance / minimumDistance - 1) * steepness)), // TODO: customize the repulsing force + fx = Math.cos(angle) * repulsingforce, + fy = Math.sin(angle) * repulsingforce; + + edges[l].from._addForce(-fx, -fy); + edges[l].to._addForce(-fx, -fy); + edges[l2].from._addForce(fx, fy); + edges[l2].to._addForce(fx, fy); + } + } + */ +}; + + +/** + * Check if any of the nodes is still moving + * @param {number} vmin the minimum velocity considered as "moving" + * @return {boolean} true if moving, false if non of the nodes is moving + */ +Graph.prototype.isMoving = function(vmin) { + // TODO: ismoving does not work well: should check the kinetic energy, not its velocity + var nodes = this.nodes; + for (var n = 0, nMax = nodes.length; n < nMax; n++) { + if (nodes[n].isMoving(vmin)) { + return true; + } + } + return false; +}; + + +/** + * Perform one discrete step for all nodes + */ +Graph.prototype._discreteStepNodes = function() { + var interval = this.refreshRate / 1000.0; // in seconds + var nodes = this.nodes; + for (var n = 0, nMax = nodes.length; n < nMax; n++) { + nodes[n].discreteStep(interval); + } +}; + + +/** + * Perform one discrete step for all packages + */ +Graph.prototype._discreteStepPackages = function() { + var interval = this.refreshRate / 1000.0; // in seconds + var packages = this.packages; + for (var n = 0, nMax = packages.length; n < nMax; n++) { + packages[n].discreteStep(interval); + } +}; + + +/** + * Cleanup finished packages. + * also checks if there are moving packages + */ +Graph.prototype._deleteFinishedPackages = function() { + var n = 0; + var hasMovingPackages = false; + while (n < this.packages.length) { + if (this.packages[n].isFinished()) { + this.packages.splice(n, 1); + n--; + } + else if (this.packages[n].isMoving()) { + hasMovingPackages = true; + } + n++; + } + + this.hasMovingPackages = hasMovingPackages; +}; + +/** + * Start animating nodes, edges, and packages. + */ +Graph.prototype.start = function() { + if (this.hasMovingNodes) { + this._calculateForces(); + this._discreteStepNodes(); + + var vmin = this.constants.minVelocity; + this.hasMovingNodes = this.isMoving(vmin); + } + + if (this.hasMovingPackages) { + this._discreteStepPackages(); + this._deleteFinishedPackages(); + } + + if (this.hasMovingNodes || this.hasMovingEdges || this.hasMovingPackages) { + // start animation. only start timer if it is not already running + if (!this.timer) { + var graph = this; + this.timer = window.setTimeout(function () { + graph.timer = undefined; + graph.start(); + graph._redraw(); + }, this.refreshRate); + } + } + else { + this._redraw(); + } +}; + +/** + * Stop animating nodes, edges, and packages. + */ +Graph.prototype.stop = function () { + if (this.timer) { + window.clearInterval(this.timer); + this.timer = undefined; + } +}; + + + +/**--------------------------------------------------------------------------**/ + + +/** + * Add and event listener. Works for all browsers + * @param {Element} element An html element + * @param {String} action The action, for example "click", + * without the prefix "on" + * @param {function} listener The callback function to be executed + * @param {boolean} useCapture + */ +Graph.addEventListener = function (element, action, listener, useCapture) { + if (element.addEventListener) { + if (useCapture === undefined) + useCapture = false; + + if (action === "mousewheel" && navigator.userAgent.indexOf("Firefox") >= 0) { + action = "DOMMouseScroll"; // For Firefox + } + + element.addEventListener(action, listener, useCapture); + } else { + element.attachEvent("on" + action, listener); // IE browsers + } +}; + +/** + * Remove an event listener from an element + * @param {Element} element An html dom element + * @param {string} action The name of the event, for example "mousedown" + * @param {function} listener The listener function + * @param {boolean} useCapture + */ +Graph.removeEventListener = function(element, action, listener, useCapture) { + if (element.removeEventListener) { + // non-IE browsers + if (useCapture === undefined) + useCapture = false; + + if (action === "mousewheel" && navigator.userAgent.indexOf("Firefox") >= 0) { + action = "DOMMouseScroll"; // For Firefox + } + + element.removeEventListener(action, listener, useCapture); + } else { + // IE browsers + element.detachEvent("on" + action, listener); + } +}; + + +/** + * Stop event propagation + */ +Graph.stopPropagation = function (event) { + if (!event) + event = window.event; + + if (event.stopPropagation) { + event.stopPropagation(); // non-IE browsers + } + else { + event.cancelBubble = true; // IE browsers + } +}; + + +/** + * Cancels the event if it is cancelable, without stopping further propagation of the event. + */ +Graph.preventDefault = function (event) { + if (!event) + event = window.event; + + if (event.preventDefault) { + event.preventDefault(); // non-IE browsers + } + else { + event.returnValue = false; // IE browsers + } +}; + +/** + * Retrieve the absolute left value of a DOM element + * @param {Element} elem A dom element, for example a div + * @return {number} left The absolute left position of this element + * in the browser page. + */ +Graph._getAbsoluteLeft = function(elem) { + var left = 0; + while( elem != null ) { + left += elem.offsetLeft; + left -= elem.scrollLeft; + elem = elem.offsetParent; + } + if (!document.body.scrollLeft && window.pageXOffset) { + // FF + left -= window.pageXOffset; + } + return left; +}; + +/** + * Retrieve the absolute top value of a DOM element + * @param {Element} elem A dom element, for example a div + * @return {number} top The absolute top position of this element + * in the browser page. + */ +Graph._getAbsoluteTop = function(elem) { + var top = 0; + while( elem != null ) { + top += elem.offsetTop; + top -= elem.scrollTop; + elem = elem.offsetParent; + } + if (!document.body.scrollTop && window.pageYOffset) { + // FF + top -= window.pageYOffset; + } + return top; +}; + + + +/**--------------------------------------------------------------------------**/ + + +/** + * @class Node + * A node. A node can be connected to other nodes via one or multiple edges. + * @param {object} properties An object containing properties for the node. All + * properties are optional, except for the id. + * {number} id Id of the node. Required + * {string} text Title for the node + * {number} x Horizontal position of the node + * {number} y Vertical position of the node + * {string} style Drawing style, available: + * "database", "circle", "rect", + * "image", "text", "dot", "star", + * "triangle", "triangleDown", + * "square" + * {string} image An image url + * {string} title An title text, can be HTML + * {anytype} group A group name or number + * @param {Graph.Images} imagelist A list with images. Only needed + * when the node has an image + * @param {Graph.Groups} grouplist A list with groups. Needed for + * retrieving group properties + * @param {Object} constants An object with default values for + * example for the color + */ +Graph.Node = function (properties, imagelist, grouplist, constants) { + this.selected = false; + + this.edges = []; // all edges connected to this node + this.group = constants.nodes.group; + + this.fontSize = constants.nodes.fontSize; + this.fontFace = constants.nodes.fontFace; + this.fontColor = constants.nodes.fontColor; + + this.borderColor = constants.nodes.borderColor; + this.backgroundColor = constants.nodes.backgroundColor; + this.highlightColor = constants.nodes.highlightColor; + + // set defaults for the properties + this.id = undefined; + this.style = constants.nodes.style; + this.image = constants.nodes.image; + this.x = 0; + this.y = 0; + this.xFixed = false; + this.yFixed = false; + this.radius = constants.nodes.radius; + this.radiusFixed = false; + this.radiusMin = constants.nodes.radiusMin; + this.radiusMax = constants.nodes.radiusMax; + + this.imagelist = imagelist; + this.grouplist = grouplist; + + this.setProperties(properties, constants); + + // mass, force, velocity + this.mass = 50; // kg (mass is adjusted for the number of connected edges) + this.fx = 0.0; // external force x + this.fy = 0.0; // external force y + this.vx = 0.0; // velocity x + this.vy = 0.0; // velocity y + this.minForce = constants.minForce; + this.damping = 0.9; // damping factor +}; + +/** + * Attach a edge to the node + * @param {Graph.Edge} edge + */ +Graph.Node.prototype.attachEdge = function(edge) { + this.edges.push(edge); + this._updateMass(); +}; + +/** + * Detach a edge from the node + * @param {Graph.Edge} edge + */ +Graph.Node.prototype.detachEdge = function(edge) { + var index = this.edges.indexOf(edge); + if (index != -1) { + this.edges.splice(index, 1); + } + this._updateMass(); +}; + +/** + * Update the nodes mass, which is determined by the number of edges connecting + * to it (more edges -> heavier node). + * @private + */ +Graph.Node.prototype._updateMass = function() { + this.mass = 50 + 20 * this.edges.length; // kg +}; + +/** + * Set or overwrite properties for the node + * @param {Object} properties an object with properties + * @param {Object} constants and object with default, global properties + */ +Graph.Node.prototype.setProperties = function(properties, constants) { + if (!properties) { + return; + } + + // basic properties + if (properties.id != undefined) {this.id = properties.id;} + if (properties.text != undefined) {this.text = properties.text;} + if (properties.title != undefined) {this.title = properties.title;} + if (properties.group != undefined) {this.group = properties.group;} + if (properties.x != undefined) {this.x = properties.x;} + if (properties.y != undefined) {this.y = properties.y;} + if (properties.value != undefined) {this.value = properties.value;} + if (properties.timestamp != undefined) {this.timestamp = properties.timestamp;} + + if (this.id === undefined) { + throw "Node must have an id"; + } + + // copy group properties + if (this.group) { + var groupObj = this.grouplist.get(this.group); + for (var prop in groupObj) { + if (groupObj.hasOwnProperty(prop)) { + this[prop] = groupObj[prop]; + } + } + } + + // individual style properties + if (properties.style != undefined) {this.style = properties.style;} + if (properties.image != undefined) {this.image = properties.image;} + if (properties.radius != undefined) {this.radius = properties.radius;} + if (properties.borderColor != undefined) {this.borderColor = properties.borderColor;} + if (properties.backgroundColor != undefined){this.backgroundColor = properties.backgroundColor;} + if (properties.highlightColor != undefined) {this.highlightColor = properties.highlightColor;} + if (properties.fontColor != undefined) {this.fontColor = properties.fontColor;} + if (properties.fontSize != undefined) {this.fontSize = properties.fontSize;} + if (properties.fontFace != undefined) {this.fontFace = properties.fontFace;} + + + if (this.image != undefined) { + if (this.imagelist) { + this.imageObj = this.imagelist.load(this.image); + } + else { + throw "No imagelist provided"; + } + } + + this.xFixed = this.xFixed || (properties.x != undefined); + this.yFixed = this.yFixed || (properties.y != undefined); + this.radiusFixed = this.radiusFixed || (properties.radius != undefined); + + if (this.style == 'image') { + this.radiusMin = constants.nodes.widthMin; + this.radiusMax = constants.nodes.widthMax; + } + + // choose draw method depending on the style + var style = this.style; + switch (style) { + case 'database': this.draw = this._drawDatabase; this.resize = this._resizeDatabase; break; + case 'rect': this.draw = this._drawRect; this.resize = this._resizeRect; break; + case 'circle': this.draw = this._drawCircle; this.resize = this._resizeCircle; break; + // TODO: add ellipse shape + // TODO: add diamond shape + case 'image': this.draw = this._drawImage; this.resize = this._resizeImage; break; + case 'text': this.draw = this._drawText; this.resize = this._resizeText; break; + case 'dot': this.draw = this._drawDot; this.resize = this._resizeShape; break; + case 'square': this.draw = this._drawSquare; this.resize = this._resizeShape; break; + case 'triangle': this.draw = this._drawTriangle; this.resize = this._resizeShape; break; + case 'triangleDown': this.draw = this._drawTriangleDown; this.resize = this._resizeShape; break; + case 'star': this.draw = this._drawStar; this.resize = this._resizeShape; break; + default: this.draw = this._drawRect; this.resize = this._resizeRect; break; + } + + // reset the size of the node, this can be changed + this._reset(); +}; + +/** + * select this node + */ +Graph.Node.prototype.select = function() { + this.selected = true; + this._reset(); +}; + +/** + * unselect this node + */ +Graph.Node.prototype.unselect = function() { + this.selected = false; + this._reset(); +}; + +/** + * Reset the calculated size of the node, forces it to recalculate its size + */ +Graph.Node.prototype._reset = function() { + this.width = undefined; + this.height = undefined; +}; + +/** + * get the title of this node. + * @return {string} title The title of the node, or undefined when no title + * has been set. + */ +Graph.Node.prototype.getTitle = function() { + return this.title; +}; + +/** + * Calculate the distance to the border of the Node + * @param {CanvasRenderingContext2D} ctx + * @param {Number} angle Angle in radians + * @returns {number} distance Distance to the border in pixels + */ +Graph.Node.prototype.distanceToBorder = function (ctx, angle) { + var borderWidth = 1; + + if (!this.width) { + this.resize(ctx); + } + + //noinspection FallthroughInSwitchStatementJS + switch (this.style) { + case 'circle': + case 'dot': + return this.radius + borderWidth; + + // TODO: implement distanceToBorder for database + // TODO: implement distanceToBorder for triangle + // TODO: implement distanceToBorder for triangleDown + + case 'rect': + case 'image': + case 'text': + default: + if (this.width) { + return Math.min( + Math.abs(this.width / 2 / Math.cos(angle)), + Math.abs(this.height / 2 / Math.sin(angle))) + borderWidth; + // TODO: reckon with border radius too in case of rect + } + else { + return 0; + } + + } + + // TODO: implement calculation of distance to border for all shapes +}; + +/** + * Set forces acting on the node + * @param {number} fx Force in horizontal direction + * @param {number} fy Force in vertical direction + */ +Graph.Node.prototype._setForce = function(fx, fy) { + this.fx = fx; + this.fy = fy; +}; + +/** + * Add forces acting on the node + * @param {number} fx Force in horizontal direction + * @param {number} fy Force in vertical direction + */ +Graph.Node.prototype._addForce = function(fx, fy) { + this.fx += fx; + this.fy += fy; +}; + +/** + * Perform one discrete step for the node + * @param {number} interval Time interval in seconds + */ +Graph.Node.prototype.discreteStep = function(interval) { + if (!this.xFixed) { + var dx = -this.damping * this.vx; // damping force + var ax = (this.fx + dx) / this.mass; // acceleration + this.vx += ax / interval; // velocity + this.x += this.vx / interval; // position + } + + if (!this.yFixed) { + var dy = -this.damping * this.vy; // damping force + var ay = (this.fy + dy) / this.mass; // acceleration + this.vy += ay / interval; // velocity + this.y += this.vy / interval; // position + } +}; + + +/** + * Check if this node has a fixed x and y position + * @return {boolean} true if fixed, false if not + */ +Graph.Node.prototype.isFixed = function() { + return (this.xFixed && this.yFixed); +}; + +/** + * Check if this node is moving + * @param {number} vmin the minimum velocity considered as "moving" + * @return {boolean} true if moving, false if it has no velocity + */ +// TODO: replace this method with calculating the kinetic energy +Graph.Node.prototype.isMoving = function(vmin) { + return (Math.abs(this.vx) > vmin || Math.abs(this.vy) > vmin || + (!this.xFixed && Math.abs(this.fx) > this.minForce) || + (!this.yFixed && Math.abs(this.fy) > this.minForce)); +}; + +/** + * check if this node is selecte + * @return {boolean} selected True if node is selected, else false + */ +Graph.Node.prototype.isSelected = function() { + return this.selected; +}; + +/** + * Retrieve the value of the node. Can be undefined + * @return {Number} value + */ +Graph.Node.prototype.getValue = function() { + return this.value; +}; + +/** + * Calculate the distance from the nodes location to the given location (x,y) + * @param {Number} x + * @param {Number} y + * @return {Number} value + */ +Graph.Node.prototype.getDistance = function(x, y) { + var dx = this.x - x, + dy = this.y - y; + return Math.sqrt(dx * dx + dy * dy); +}; + + +/** + * Adjust the value range of the node. The node will adjust it's radius + * based on its value. + * @param {Number} min + * @param {Number} max + */ +Graph.Node.prototype.setValueRange = function(min, max) { + if (!this.radiusFixed && this.value !== undefined) { + var scale = (this.radiusMax - this.radiusMin) / (max - min); + this.radius = (this.value - min) * scale + this.radiusMin; + } +}; + +/** + * Draw this node in the given canvas + * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d"); + * @param {CanvasRenderingContext2D} ctx + */ +Graph.Node.prototype.draw = function(ctx) { + throw "Draw method not initialized for node"; +}; + +/** + * Recalculate the size of this node in the given canvas + * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d"); + * @param {CanvasRenderingContext2D} ctx + */ +Graph.Node.prototype.resize = function(ctx) { + throw "Resize method not initialized for node"; +}; + +/** + * Check if this object is overlapping with the provided object + * @param {Object} obj an object with parameters left, top, right, bottom + * @return {boolean} True if location is located on node + */ +Graph.Node.prototype.isOverlappingWith = function(obj) { + return (this.left < obj.right && + this.left + this.width > obj.left && + this.top < obj.bottom && + this.top + this.height > obj.top); +}; + +Graph.Node.prototype._resizeImage = function (ctx) { + // TODO: pre calculate the image size + if (!this.width) { // undefined or 0 + var width, height; + if (this.value) { + var scale = this.imageObj.height / this.imageObj.width; + width = this.radius || this.imageObj.width; + height = this.radius * scale || this.imageObj.height; + } + else { + width = this.imageObj.width; + height = this.imageObj.height; + } + this.width = width; + this.height = height; + } +}; + +Graph.Node.prototype._drawImage = function (ctx) { + this._resizeImage(ctx); + + this.left = this.x - this.width / 2; + this.top = this.y - this.height / 2; + + var yText; + if (this.imageObj) { + ctx.drawImage(this.imageObj, this.left, this.top, this.width, this.height); + yText = this.y + this.height / 2; + } + else { + // image still loading... just draw the text for now + yText = this.y; + } + + this._text(ctx, this.text, this.x, yText, undefined, "top"); +}; + + +Graph.Node.prototype._resizeRect = function (ctx) { + if (!this.width) { + var margin = 5; + var textSize = this.getTextSize(ctx); + this.width = textSize.width + 2 * margin; + this.height = textSize.height + 2 * margin; + } +}; + +Graph.Node.prototype._drawRect = function (ctx) { + this._resizeRect(ctx); + + this.left = this.x - this.width / 2; + this.top = this.y - this.height / 2; + + ctx.strokeStyle = this.borderColor; + ctx.fillStyle = this.selected ? this.highlightColor : this.backgroundColor; + ctx.lineWidth = this.selected ? 2.0 : 1.0; + ctx.roundRect(this.left, this.top, this.width, this.height, this.radius); + ctx.fill(); + ctx.stroke(); + + this._text(ctx, this.text, this.x, this.y); +}; + + +Graph.Node.prototype._resizeDatabase = function (ctx) { + if (!this.width) { + var margin = 5; + var textSize = this.getTextSize(ctx); + var size = textSize.width + 2 * margin; + this.width = size; + this.height = size; + } +}; + +Graph.Node.prototype._drawDatabase = function (ctx) { + this._resizeDatabase(ctx); + this.left = this.x - this.width / 2; + this.top = this.y - this.height / 2; + + ctx.strokeStyle = this.borderColor; + ctx.fillStyle = this.selected ? this.highlightColor : this.backgroundColor; + ctx.lineWidth = this.selected ? 2.0 : 1.0; + ctx.database(this.x - this.width/2, this.y - this.height*0.5, this.width, this.height); + ctx.fill(); + ctx.stroke(); + + this._text(ctx, this.text, this.x, this.y); +}; + + +Graph.Node.prototype._resizeCircle = function (ctx) { + if (!this.width) { + var margin = 5; + var textSize = this.getTextSize(ctx); + var diameter = Math.max(textSize.width, textSize.height) + 2 * margin; + this.radius = diameter / 2; + + this.width = diameter; + this.height = diameter; + } +}; + +Graph.Node.prototype._drawCircle = function (ctx) { + this._resizeCircle(ctx); + this.left = this.x - this.width / 2; + this.top = this.y - this.height / 2; + + ctx.strokeStyle = this.borderColor; + ctx.fillStyle = this.selected ? this.highlightColor : this.backgroundColor; + ctx.lineWidth = this.selected ? 2.0 : 1.0; + ctx.circle(this.x, this.y, this.radius); + ctx.fill(); + ctx.stroke(); + + this._text(ctx, this.text, this.x, this.y); +}; + +Graph.Node.prototype._drawDot = function (ctx) { + this._drawShape(ctx, 'circle'); +}; + +Graph.Node.prototype._drawTriangle = function (ctx) { + this._drawShape(ctx, 'triangle'); +}; + +Graph.Node.prototype._drawTriangleDown = function (ctx) { + this._drawShape(ctx, 'triangleDown'); +}; + +Graph.Node.prototype._drawSquare = function (ctx) { + this._drawShape(ctx, 'square'); +}; + +Graph.Node.prototype._drawStar = function (ctx) { + this._drawShape(ctx, 'star'); +}; + +Graph.Node.prototype._resizeShape = function (ctx) { + if (!this.width) { + var size = 2 * this.radius; + this.width = size; + this.height = size; + } +}; + +Graph.Node.prototype._drawShape = function (ctx, shape) { + this._resizeShape(ctx); + + this.left = this.x - this.width / 2; + this.top = this.y - this.height / 2; + + ctx.strokeStyle = this.borderColor; + ctx.fillStyle = this.selected ? this.highlightColor : this.backgroundColor; + ctx.lineWidth = this.selected ? 2.0 : 1.0; + + ctx[shape](this.x, this.y, this.radius); + ctx.fill(); + ctx.stroke(); + + if (this.text) { + this._text(ctx, this.text, this.x, this.y + this.height / 2, undefined, 'top'); + } +}; + +Graph.Node.prototype._resizeText = function (ctx) { + if (!this.width) { + var margin = 5; + var textSize = this.getTextSize(ctx); + this.width = textSize.width + 2 * margin; + this.height = textSize.height + 2 * margin; + } +}; + +Graph.Node.prototype._drawText = function (ctx) { + this._resizeText(ctx); + this.left = this.x - this.width / 2; + this.top = this.y - this.height / 2; + + this._text(ctx, this.text, this.x, this.y); +}; + + +Graph.Node.prototype._text = function (ctx, text, x, y, align, baseline) { + if (text) { + ctx.font = (this.selected ? "bold " : "") + this.fontSize + "px " + this.fontFace; + ctx.fillStyle = this.fontColor || "black"; + ctx.textAlign = align || "center"; + ctx.textBaseline = baseline || "middle"; + + var lines = text.split('\n'), + lineCount = lines.length, + fontSize = (this.fontSize + 4), + yLine = y + (1 - lineCount) / 2 * fontSize; + + for (var i = 0; i < lineCount; i++) { + ctx.fillText(lines[i], x, yLine); + yLine += fontSize; + } + } +}; + + +Graph.Node.prototype.getTextSize = function(ctx) { + if (this.text != undefined) { + ctx.font = (this.selected ? "bold " : "") + this.fontSize + "px " + this.fontFace; + + var lines = this.text.split('\n'), + height = (this.fontSize + 4) * lines.length, + width = 0; + + for (var i = 0, iMax = lines.length; i < iMax; i++) { + width = Math.max(width, ctx.measureText(lines[i]).width); + } + + return {"width": width, "height": height}; + } + else { + return {"width": 0, "height": 0}; + } +}; + + + +/**--------------------------------------------------------------------------**/ + + +/** + * @class Edge + * + * A edge connects two nodes + * @param {Object} properties Object with properties. Must contain + * At least properties from and to. + * Available properties: from (number), + * to (number), color (string), + * width (number), style (string), + * length (number), title (string) + * @param {Graph} graph A graph object, used to find and edge to + * nodes. + * @param {Object} constants An object with default values for + * example for the color + */ +Graph.Edge = function (properties, graph, constants) { + if (!graph) { + throw "No graph provided"; + } + this.graph = graph; + + // initialize constants + this.widthMin = constants.edges.widthMin; + this.widthMax = constants.edges.widthMax; + + // initialize variables + this.id = undefined; + this.style = constants.edges.style; + this.title = undefined; + this.width = constants.edges.width; + this.value = undefined; + this.length = constants.edges.length; + + // Added to support dashed lines + // David Jordan + // 2012-08-08 + this.dashlength = constants.edges.dashlength; + this.dashgap = constants.edges.dashgap; + this.altdashlength = constants.edges.altdashlength; + + this.stiffness = undefined; // depends on the length of the edge + this.color = constants.edges.color; + this.timestamp = undefined; + this.widthFixed = false; + this.lengthFixed = false; + + this.setProperties(properties, constants); +}; + +/** + * Set or overwrite properties for the edge + * @param {Object} properties an object with properties + * @param {Object} constants and object with default, global properties + */ +Graph.Edge.prototype.setProperties = function(properties, constants) { + if (!properties) { + return; + } + + if (properties.from != undefined) {this.from = this.graph._getNode(properties.from);} + if (properties.to != undefined) {this.to = this.graph._getNode(properties.to);} + + if (properties.id != undefined) {this.id = properties.id;} + if (properties.style != undefined) {this.style = properties.style;} + if (properties.text != undefined) {this.text = properties.text;} + if (this.text) { + this.fontSize = constants.edges.fontSize; + this.fontFace = constants.edges.fontFace; + this.fontColor = constants.edges.fontColor; + if (properties.fontColor != undefined) {this.fontColor = properties.fontColor;} + if (properties.fontSize != undefined) {this.fontSize = properties.fontSize;} + if (properties.fontFace != undefined) {this.fontFace = properties.fontFace;} + } + if (properties.title != undefined) {this.title = properties.title;} + if (properties.width != undefined) {this.width = properties.width;} + if (properties.value != undefined) {this.value = properties.value;} + if (properties.length != undefined) {this.length = properties.length;} + + // Added to support dashed lines + // David Jordan + // 2012-08-08 + if (properties.dashlength != undefined) {this.dashlength = properties.dashlength;} + if (properties.dashgap != undefined) {this.dashgap = properties.dashgap;} + if (properties.altdashlength != undefined) {this.altdashlength = properties.altdashlength;} + + if (properties.color != undefined) {this.color = properties.color;} + if (properties.timestamp != undefined) {this.timestamp = properties.timestamp;} + + if (!this.from) { + throw "Node with id " + properties.from + " not found"; + } + if (!this.to) { + throw "Node with id " + properties.to + " not found"; + } + + this.widthFixed = this.widthFixed || (properties.width != undefined); + this.lengthFixed = this.lengthFixed || (properties.length != undefined); + + this.stiffness = 1 / this.length; + + // initialize animation + if (this.style === 'arrow') { + this.arrows = [0.5]; + this.animation = false; + } + else if (this.style === 'arrow-end') { + this.animation = false; + } + else if (this.style === 'moving-arrows') { + this.arrows = []; + var arrowCount = 3; // TODO: make customizable + for (var a = 0; a < arrowCount; a++) { + this.arrows.push(a / arrowCount); + } + this.animation = true; + } + else if (this.style === 'moving-dot') { + this.dot = 0.0; + this.animation = true; + } + else { + this.animation = false; + } + + // set draw method based on style + switch (this.style) { + case 'line': this.draw = this._drawLine; break; + case 'arrow': this.draw = this._drawArrow; break; + case 'arrow-end': this.draw = this._drawArrowEnd; break; + case 'moving-arrows': this.draw = this._drawMovingArrows; break; + case 'moving-dot': this.draw = this._drawMovingDot; break; + case 'dash-line': this.draw = this._drawDashLine; break; + default: this.draw = this._drawLine; break; + } +}; + + + +/** + * Check if a node has an animating contents. If so, the graph needs to be + * redrawn regularly + * @return {boolean} true if this edge needs animation, else false + */ +Graph.Edge.prototype.isMoving = function() { + // TODO: be able to set the interval somehow + + return this.animation; +}; + +/** + * get the title of this edge. + * @return {string} title The title of the edge, or undefined when no title + * has been set. + */ +Graph.Edge.prototype.getTitle = function() { + return this.title; +}; + + +/** + * Retrieve the value of the edge. Can be undefined + * @return {Number} value + */ +Graph.Edge.prototype.getValue = function() { + return this.value; +} + +/** + * Adjust the value range of the edge. The edge will adjust it's width + * based on its value. + * @param {Number} min + * @param {Number} max + */ +Graph.Edge.prototype.setValueRange = function(min, max) { + if (!this.widthFixed && this.value !== undefined) { + var factor = (this.widthMax - this.widthMin) / (max - min); + this.width = (this.value - min) * factor + this.widthMin; + } +}; + + +/** + * Check if the length is fixed. + * @return {boolean} lengthFixed True if the length is fixed, else false + */ +Graph.Edge.prototype.isLengthFixed = function() { + return this.lengthFixed; +}; + +/** + * Retrieve the length of the edge. Can be undefined + * @return {Number} length + */ +Graph.Edge.prototype.getLength = function() { + return this.length; +}; + +/** + * Adjust the length of the edge. This can only be done when the length + * is not fixed (which is the case when the edge is created with a length property) + * @param {Number} length + */ +Graph.Edge.prototype.setLength = function(length) { + if (!this.lengthFixed) { + this.length = length; + } +}; + +/** + * Retrieve the length of the edges dashes. Can be undefined + * @author David Jordan + * @date 2012-08-08 + * @return {Number} dashlength + */ +Graph.Edge.prototype.getDashLength = function() { + return this.dashlength; +}; + +/** + * Adjust the length of the edges dashes. + * @author David Jordan + * @date 2012-08-08 + * @param {Number} dashlength + */ +Graph.Edge.prototype.setDashLength = function(dashlength) { + this.dashlength = dashlength; +}; + +/** + * Retrieve the length of the edges dashes gaps. Can be undefined + * @author David Jordan + * @date 2012-08-08 + * @return {Number} dashgap + */ +Graph.Edge.prototype.getDashGap = function() { + return this.dashgap; +}; + +/** + * Adjust the length of the edges dashes gaps. + * @author David Jordan + * @date 2012-08-08 + * @param {Number} dashgap + */ +Graph.Edge.prototype.setDashGap = function(dashgap) { + this.dashgap = dashgap; +}; + +/** + * Retrieve the length of the edges alternate dashes. Can be undefined + * @author David Jordan + * @date 2012-08-08 + * @return {Number} altdashlength + */ +Graph.Edge.prototype.getAltDashLength = function() { + return this.altdashlength; +}; + +/** + * Adjust the length of the edges alternate dashes. + * @author David Jordan + * @date 2012-08-08 + * @param {Number} altdashlength + */ +Graph.Edge.prototype.setAltDashLength = function(altdashlength) { + this.altdashlength = altdashlength; +}; + + + +/** + * Redraw a edge + * Draw this edge in the given canvas + * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d"); + * @param {CanvasRenderingContext2D} ctx + */ +Graph.Edge.prototype.draw = function(ctx) { + throw "Method draw not initialized in edge"; +}; + + +/** + * Check if this object is overlapping with the provided object + * @param {Object} obj an object with parameters left, top + * @return {boolean} True if location is located on the edge + */ +Graph.Edge.prototype.isOverlappingWith = function(obj) { + var distMax = 10; + + var xFrom = this.from.x; + var yFrom = this.from.y; + var xTo = this.to.x; + var yTo = this.to.y; + var xObj = obj.left; + var yObj = obj.top; + + + var dist = Graph._dist(xFrom, yFrom, xTo, yTo, xObj, yObj); + + return (dist < distMax); +}; + +/** + * Calculate the distance between a point (x3,y3) and a line segment from + * (x1,y1) to (x2,y2). + * http://stackoverflow.com/questions/849211/shortest-distancae-between-a-point-and-a-line-segment + * @param {number} x1 + * @param {number} y1 + * @param {number} x2 + * @param {number} y2 + * @param {number} x3 + * @param {number} y3 + */ +Graph._dist = function (x1,y1, x2,y2, x3,y3) { // x3,y3 is the point + var px = x2-x1, + py = y2-y1, + something = px*px + py*py, + u = ((x3 - x1) * px + (y3 - y1) * py) / something; + + if (u > 1) { + u = 1; + } + else if (u < 0) { + u = 0; + } + + var x = x1 + u * px, + y = y1 + u * py, + dx = x - x3, + dy = y - y3; + + //# Note: If the actual distance does not matter, + //# if you only want to compare what this function + //# returns to other results of this function, you + //# can just return the squared distance instead + //# (i.e. remove the sqrt) to gain a little performance + + return Math.sqrt(dx*dx + dy*dy); +}; + +/** + * Redraw a edge as a line + * Draw this edge in the given canvas + * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d"); + * @param {CanvasRenderingContext2D} ctx + */ +Graph.Edge.prototype._drawLine = function(ctx) { + // set style + ctx.strokeStyle = this.color; + ctx.lineWidth = this._getLineWidth(); + + var point; + if (this.from != this.to) { + // draw line + this._line(ctx); + + // draw text + if (this.text) { + point = this._pointOnLine(0.5); + this._text(ctx, this.text, point.x, point.y); + } + } + else { + var radius = this.length / 2 / Math.PI; + var x, y; + var node = this.from; + if (!node.width) { + node.resize(ctx); + } + if (node.width > node.height) { + x = node.x + node.width / 2; + y = node.y - radius; + } + else { + x = node.x + radius; + y = node.y - node.height / 2; + } + this._circle(ctx, x, y, radius); + point = this._pointOnCircle(x, y, radius, 0.5); + this._text(ctx, this.text, point.x, point.y); + } +}; + +/** + * Get the line width of the edge. Depends on width and whether one of the + * connected nodes is selected. + * @return {Number} width + * @private + */ +Graph.Edge.prototype._getLineWidth = function() { + if (this.from.selected || this.to.selected) { + return Math.min(this.width * 2, this.widthMax); + } + else { + return this.width; + } +}; + +/** + * Draw a line between two nodes + * @param {CanvasRenderingContext2D} ctx + * @private + */ +Graph.Edge.prototype._line = function (ctx) { + // draw a straight line + ctx.beginPath(); + ctx.moveTo(this.from.x, this.from.y); + ctx.lineTo(this.to.x, this.to.y); + ctx.stroke(); +}; + +/** + * Draw a line from a node to itself, a circle + * @param {CanvasRenderingContext2D} ctx + * @param {Number} x + * @param {Number} y + * @param {Number} radius + * @private + */ +Graph.Edge.prototype._circle = function (ctx, x, y, radius) { + // draw a circle + ctx.beginPath(); + ctx.arc(x, y, radius, 0, 2 * Math.PI, false); + ctx.stroke(); +}; + +/** + * Draw text with white background and with the middle at (x, y) + * @param {CanvasRenderingContext2D} ctx + * @param {String} text + * @param {Number} x + * @param {Number} y + */ +Graph.Edge.prototype._text = function (ctx, text, x, y) { + if (text) { + // TODO: cache the calculated size + ctx.font = ((this.from.selected || this.to.selected) ? "bold " : "") + + this.fontSize + "px " + this.fontFace; + ctx.fillStyle = 'white'; + var width = ctx.measureText(this.text).width; + var height = this.fontSize; + var left = x - width / 2; + var top = y - height / 2; + + ctx.fillRect(left, top, width, height); + + // draw text + ctx.fillStyle = this.fontColor || "black"; + ctx.textAlign = "left"; + ctx.textBaseline = "top"; + ctx.fillText(this.text, left, top); + } +}; + +/** + * Sets up the dashedLine functionality for drawing + * Original code came from http://stackoverflow.com/questions/4576724/dotted-stroke-in-canvas + * @author David Jordan + * @date 2012-08-08 + */ +var CP = (typeof window !== 'undefined') && + window.CanvasRenderingContext2D && + CanvasRenderingContext2D.prototype; +if (CP && CP.lineTo){ + CP.dashedLine = function(x,y,x2,y2,dashArray){ + if (!dashArray) dashArray=[10,5]; + if (dashLength==0) dashLength = 0.001; // Hack for Safari + var dashCount = dashArray.length; + this.moveTo(x, y); + var dx = (x2-x), dy = (y2-y); + var slope = dy/dx; + var distRemaining = Math.sqrt( dx*dx + dy*dy ); + var dashIndex=0, draw=true; + while (distRemaining>=0.1){ + var dashLength = dashArray[dashIndex++%dashCount]; + if (dashLength > distRemaining) dashLength = distRemaining; + var xStep = Math.sqrt( dashLength*dashLength / (1 + slope*slope) ); + if (dx<0) xStep = -xStep; + x += xStep + y += slope*xStep; + this[draw ? 'lineTo' : 'moveTo'](x,y); + distRemaining -= dashLength; + draw = !draw; + } + } +} + +/** + * Redraw a edge as a dashed line + * Draw this edge in the given canvas + * @author David Jordan + * @date 2012-08-08 + * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d"); + * @param {CanvasRenderingContext2D} ctx + */ +Graph.Edge.prototype._drawDashLine = function(ctx) { + // set style + ctx.strokeStyle = this.color; + ctx.lineWidth = this._getLineWidth(); + + // draw dashed line + ctx.beginPath(); + ctx.lineCap = 'round'; + if (this.altdashlength != undefined) //If an alt dash value has been set add to the array this value + { + ctx.dashedLine(this.from.x,this.from.y,this.to.x,this.to.y,[this.dashlength,this.dashgap,this.altdashlength,this.dashgap]); + } + else if (this.dashlength != undefined && this.dashgap != undefined) //If a dash and gap value has been set add to the array this value + { + ctx.dashedLine(this.from.x,this.from.y,this.to.x,this.to.y,[this.dashlength,this.dashgap]); + } + else //If all else fails draw a line + { + ctx.moveTo(this.from.x, this.from.y); + ctx.lineTo(this.to.x, this.to.y); + } + ctx.stroke(); + + // draw text + if (this.text) { + var point = this._pointOnLine(0.5); + this._text(ctx, this.text, point.x, point.y); + } +}; + +/** + * Get a point on a line + * @param {Number} percentage. Value between 0 (line start) and 1 (line end) + * @return {Object} point + * @private + */ +Graph.Edge.prototype._pointOnLine = function (percentage) { + return { + x: (1 - percentage) * this.from.x + percentage * this.to.x, + y: (1 - percentage) * this.from.y + percentage * this.to.y + } +}; + +/** + * Get a point on a circle + * @param {Number} x + * @param {Number} y + * @param {Number} radius + * @param {Number} percentage. Value between 0 (line start) and 1 (line end) + * @return {Object} point + * @private + */ +Graph.Edge.prototype._pointOnCircle = function (x, y, radius, percentage) { + var angle = (percentage - 3/8) * 2 * Math.PI; + return { + x: x + radius * Math.cos(angle), + y: y - radius * Math.sin(angle) + } +}; + +/** + * Redraw a edge as a line with a moving arrow + * Draw this edge in the given canvas + * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d"); + * @param {CanvasRenderingContext2D} ctx + */ +Graph.Edge.prototype._drawMovingArrows = function(ctx) { + this._drawArrow(ctx); + + for (var a in this.arrows) { + if (this.arrows.hasOwnProperty(a)) { + this.arrows[a] += 0.02; // TODO determine speed from interval + if (this.arrows[a] > 1.0) this.arrows[a] = 0.0; + } + } +}; + +/** + * Redraw a edge as a line with a moving dot + * Draw this edge in the given canvas + * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d"); + * @param {CanvasRenderingContext2D} ctx + */ +Graph.Edge.prototype._drawMovingDot = function(ctx) { + // set style + ctx.strokeStyle = this.color; + ctx.fillStyle = this.color; + ctx.lineWidth = this._getLineWidth(); + + // draw line + var point; + if (this.from != this.to) { + this._line(ctx); + + // draw dot + var radius = 4 + this.width * 2; + point = this._pointOnLine(this.dot); + ctx.circle(point.x, point.y, radius); + ctx.fill(); + + // move the dot to the next position + this.dot += 0.05; // TODO determine speed from interval + if (this.dot > 1.0) this.dot = 0.0; + + // draw text + if (this.text) { + point = this._pointOnLine(0.5); + this._text(ctx, this.text, point.x, point.y); + } + } + else { + // TODO: moving dot for a circular edge + } +}; + + +/** + * Redraw a edge as a line with an arrow + * Draw this edge in the given canvas + * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d"); + * @param {CanvasRenderingContext2D} ctx + */ +Graph.Edge.prototype._drawArrow = function(ctx) { + var point; + // set style + ctx.strokeStyle = this.color; + ctx.fillStyle = this.color; + ctx.lineWidth = this._getLineWidth(); + + if (this.from != this.to) { + // draw line + this._line(ctx); + + // draw all arrows + var angle = Math.atan2((this.to.y - this.from.y), (this.to.x - this.from.x)); + var length = 10 + 5 * this.width; // TODO: make customizable? + for (var a in this.arrows) { + if (this.arrows.hasOwnProperty(a)) { + point = this._pointOnLine(this.arrows[a]); + ctx.arrow(point.x, point.y, angle, length); + ctx.fill(); + ctx.stroke(); + } + } + + // draw text + if (this.text) { + point = this._pointOnLine(0.5); + this._text(ctx, this.text, point.x, point.y); + } + } + else { + // draw circle + var radius = this.length / 2 / Math.PI; + var x, y; + var node = this.from; + if (!node.width) { + node.resize(ctx); + } + if (node.width > node.height) { + x = node.x + node.width / 2; + y = node.y - radius; + } + else { + x = node.x + radius; + y = node.y - node.height / 2; + } + this._circle(ctx, x, y, radius); + + // draw all arrows + var angle = 0.2 * Math.PI; + var length = 10 + 5 * this.width; // TODO: make customizable? + for (var a in this.arrows) { + if (this.arrows.hasOwnProperty(a)) { + point = this._pointOnCircle(x, y, radius, this.arrows[a]); + ctx.arrow(point.x, point.y, angle, length); + ctx.fill(); + ctx.stroke(); + } + } + + // draw text + if (this.text) { + point = this._pointOnCircle(x, y, radius, 0.5); + this._text(ctx, this.text, point.x, point.y); + } + } +}; + + + +/** + * Redraw a edge as a line with an arrow + * Draw this edge in the given canvas + * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d"); + * @param {CanvasRenderingContext2D} ctx + */ +Graph.Edge.prototype._drawArrowEnd = function(ctx) { + // set style + ctx.strokeStyle = this.color; + ctx.fillStyle = this.color; + ctx.lineWidth = this._getLineWidth(); + + // draw line + var angle, length; + if (this.from != this.to) { + // calculate length and angle of the line + angle = Math.atan2((this.to.y - this.from.y), (this.to.x - this.from.x)); + var dx = (this.to.x - this.from.x); + var dy = (this.to.y - this.from.y); + var lEdge = Math.sqrt(dx * dx + dy * dy); + + var lFrom = this.to.distanceToBorder(ctx, angle + Math.PI); + var pFrom = (lEdge - lFrom) / lEdge; + var xFrom = (pFrom) * this.from.x + (1 - pFrom) * this.to.x; + var yFrom = (pFrom) * this.from.y + (1 - pFrom) * this.to.y; + + var lTo = this.to.distanceToBorder(ctx, angle); + var pTo = (lEdge - lTo) / lEdge; + var xTo = (1 - pTo) * this.from.x + pTo * this.to.x; + var yTo = (1 - pTo) * this.from.y + pTo * this.to.y; + + ctx.beginPath(); + ctx.moveTo(xFrom, yFrom); + ctx.lineTo(xTo, yTo); + ctx.stroke(); + + // draw arrow at the end of the line + length = 10 + 5 * this.width; // TODO: make customizable? + ctx.arrow(xTo, yTo, angle, length); + ctx.fill(); + ctx.stroke(); + + // draw text + if (this.text) { + var point = this._pointOnLine(0.5); + this._text(ctx, this.text, point.x, point.y); + } + } + else { + // draw circle + var radius = this.length / 2 / Math.PI; + var x, y, arrow; + var node = this.from; + if (!node.width) { + node.resize(ctx); + } + if (node.width > node.height) { + x = node.x + node.width / 2; + y = node.y - radius; + arrow = { + x: x, + y: node.y, + angle: 0.9 * Math.PI + }; + } + else { + x = node.x + radius; + y = node.y - node.height / 2; + arrow = { + x: node.x, + y: y, + angle: 0.6 * Math.PI + }; + } + ctx.beginPath(); + // TODO: do not draw a circle, but an arc + // TODO: similarly, for a line without arrows, draw to the border of the nodes instead of the center + ctx.arc(x, y, radius, 0, 2 * Math.PI, false); + ctx.stroke(); + + // draw all arrows + length = 10 + 5 * this.width; // TODO: make customizable? + ctx.arrow(arrow.x, arrow.y, arrow.angle, length); + ctx.fill(); + ctx.stroke(); + + // draw text + if (this.text) { + point = this._pointOnCircle(x, y, radius, 0.5); + this._text(ctx, this.text, point.x, point.y); + } + } + +}; + +/**--------------------------------------------------------------------------**/ + + +/** + * @class Images + * This class loades images and keeps them stored. + */ +Graph.Images = function () { + this.images = {}; + + this.callback = undefined; +}; + +/** + * Set an onload callback function. This will be called each time an image + * is loaded + * @param {function} callback + */ +Graph.Images.prototype.setOnloadCallback = function(callback) { + this.callback = callback; +}; + + +/** + * + * @param {string} url Url of the image + * @return {Image} img The image object + */ +Graph.Images.prototype.load = function(url) { + var img = this.images[url]; + if (img == undefined) { + // create the image + var images = this; + img = new Image(); + this.images[url] = img; + img.onload = function() { + if (images.callback) { + images.callback(this); + } + }; + img.src = url; + } + + return img; +}; + + +/**--------------------------------------------------------------------------**/ + + +/** + * @class Package + * This class contains one package + * + * @param {number} properties Properties for the package. Optional. Available + * properties are: id {number}, title {string}, + * style {string} with available values "dot" and + * "image", radius {number}, image {string}, + * color {string}, progress {number} with a value + * between 0-1, duration {number}, timestamp {number + * or Date}. + * @param {Graph} graph The graph object, used to find + * and edge to nodes. + * @param {Graph.Images} imagelist An Images object. Only needed + * when the package has style 'image' + * @param {Object} constants An object with default values for + * example for the color + */ +Graph.Package = function (properties, graph, imagelist, constants) { + if (graph == undefined) { + throw "No graph provided"; + } + + // constants + this.radiusMin = constants.packages.radiusMin; + this.radiusMax = constants.packages.radiusMax; + this.imagelist = imagelist; + this.graph = graph; + + // initialize variables + this.id = undefined; + this.from = undefined; + this.to = undefined; + this.title = undefined; + this.style = constants.packages.style; + this.radius = constants.packages.radius; + this.color = constants.packages.color; + this.image = constants.packages.image; + this.value = undefined; + this.progress = 0.0; + this.timestamp = undefined; + this.duration = constants.packages.duration; + this.autoProgress = true; + this.radiusFixed = false; + + // set properties + this.setProperties(properties, constants); +}; + +Graph.Package.DEFAULT_DURATION = 1.0; // seconds + +/** + * Set or overwrite properties for the package + * @param {Object} properties an object with properties + * @param {Object} constants and object with default, global properties + */ +Graph.Package.prototype.setProperties = function(properties, constants) { + if (!properties) { + return; + } + + // note that the provided properties can also be null, when they come from the Google DataTable + if (properties.from != undefined) {this.from = this.graph._getNode(properties.from);} + if (properties.to != undefined) {this.to = this.graph._getNode(properties.to);} + + if (!this.from) { + throw "Node with id " + properties.from + " not found"; + } + if (!this.to) { + throw "Node with id " + properties.to + " not found"; + } + + if (properties.id != undefined) {this.id = properties.id;} + if (properties.title != undefined) {this.title = properties.title;} + if (properties.style != undefined) {this.style = properties.style;} + if (properties.radius != undefined) {this.radius = properties.radius;} + if (properties.value != undefined) {this.value = properties.value;} + if (properties.image != undefined) {this.image = properties.image;} + if (properties.color != undefined) {this.color = properties.color;} + if (properties.dashlength != undefined) {this.dashlength = properties.dashlength;} + if (properties.dashgap != undefined) {this.dashgap = properties.dashgap;} + if (properties.altdashlength != undefined) {this.altdashlength = properties.altdashlength;} + if (properties.progress != undefined) {this.progress = properties.progress;} + if (properties.timestamp != undefined) {this.timestamp = properties.timestamp;} + if (properties.duration != undefined) {this.duration = properties.duration;} + + this.radiusFixed = this.radiusFixed || (properties.radius != undefined); + this.autoProgress = (this.autoProgress == true) ? (properties.progress == undefined) : false; + + if (this.style == 'image') { + this.radiusMin = constants.packages.widthMin; + this.radiusMax = constants.packages.widthMax; + } + + // handle progress + if (this.progress < 0.0) {this.progress = 0.0;} + if (this.progress > 1.0) {this.progress = 1.0;} + + // handle image + if (this.image != undefined) { + if (this.imagelist) { + this.imageObj = this.imagelist.load(this.image); + } + else { + throw "No imagelist provided"; + } + } + + // choose draw method depending on the style + switch (this.style) { + // TODO: add more styles + case 'dot': this.draw = this._drawDot; break; + case 'square': this.draw = this._drawSquare; break; + case 'triangle': this.draw = this._drawTriangle; break; + case 'triangleDown':this.draw = this._drawTriangleDown; break; + case 'star': this.draw = this._drawStar; break; + case 'image': this.draw = this._drawImage; break; + default: this.draw = this._drawDot; break; + } +}; + +/** + * Set a new value for the progress of the package + * @param {number} progress A value between 0 and 1 + */ +Graph.Package.prototype.setProgress = function (progress) { + this.progress = progress; + this.autoProgress = false; +}; + +/** + * Check if a package is finished, if it has reached its destination. + * If so, the package can be removed. + * Only packages with automatically animated progress can be finished + * @return {boolean} true if finished, else false. + */ +Graph.Package.prototype.isFinished = function () { + return (this.autoProgress == true && this.progress >= 1.0); +}; + +/** + * Check if this package is moving. + * A packages moves when it has automatic progress and not yet reached its + * destination. + * @return {boolean} true if moving, else false. + */ +Graph.Package.prototype.isMoving = function () { + return (this.autoProgress || this.isFinished()); +}; + + +/** + * Perform one discrete step for the package. Only applicable when the + * package has no manually set, fixed progress. + * @param {number} interval Time interval in seconds + */ +Graph.Package.prototype.discreteStep = function(interval) { + if (this.autoProgress == true) { + this.progress += (parseFloat(interval) / this.duration); + + if (this.progress > 1.0) + this.progress = 1.0; + } +}; + + +/** + * Draw this package in the given canvas + * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d"); + * @param {CanvasRenderingContext2D} ctx + */ +Graph.Package.prototype.draw = function(ctx) { + throw "Draw method not initialized for package"; +}; + + +/** + * Check if this object is overlapping with the provided object + * @param {Object} obj an object with parameters left, top, right, bottom + * @return {boolean} True if location is located on node + */ +Graph.Package.prototype.isOverlappingWith = function(obj) { + // radius minimum 10px else it is too hard to get your mouse at the exact right position + var radius = Math.max(this.radius, 10); + var pos = this._getPosition(); + + return (pos.x - radius < obj.right && + pos.x + radius > obj.left && + pos.y - radius < obj.bottom && + pos.y + radius > obj.top); +}; + +/** + * Calculate the current position of the package + * @return {Object} position The object has parameters x and y. + */ +Graph.Package.prototype._getPosition = function() { + return { + "x" : (1 - this.progress) * this.from.x + this.progress * this.to.x, + "y" : (1 - this.progress) * this.from.y + this.progress * this.to.y + }; +}; + + +/** + * get the title of this package. + * @return {string} title The title of the package, or undefined when no + * title has been set. + */ +Graph.Package.prototype.getTitle = function() { + return this.title; +}; + +/** + * Retrieve the value of the package. Can be undefined + * @return {Number} value + */ +Graph.Package.prototype.getValue = function() { + return this.value; +}; + +/** + * Calculate the distance from the packages location to the given location (x,y) + * @param {Number} x + * @param {Number} y + * @return {Number} value + */ +Graph.Package.prototype.getDistance = function(x, y) { + var pos = this._getPosition(), + dx = pos.x - x, + dy = pos.y - y; + return Math.sqrt(dx * dx + dy * dy); +}; + +/** + * Adjust the value range of the package. The package will adjust it's radius + * based on its value. + * @param {Number} min + * @param {Number} max + */ +Graph.Package.prototype.setValueRange = function(min, max) { + if (!this.radiusFixed && this.value !== undefined) { + var factor = (this.radiusMax - this.radiusMin) / (max - min); + this.radius = (this.value - min) * factor + this.radiusMin; + } +}; + + + +/** + * Redraw a package as a dot + * Draw this edge in the given canvas + * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d"); + * @param {CanvasRenderingContext2D} ctx + */ +/* TODO: cleanup + Graph.Package.prototype._drawDot = function(ctx) { + // set style + ctx.fillStyle = this.color; + // draw dot + var pos = this._getPosition(); + ctx.circle(pos.x, pos.y, this.radius); + ctx.fill(); + } + */ + +Graph.Package.prototype._drawDot = function (ctx) { + this._drawShape(ctx, 'circle'); +}; + +Graph.Package.prototype._drawTriangle = function (ctx) { + this._drawShape(ctx, 'triangle'); +}; + +Graph.Package.prototype._drawTriangleDown = function (ctx) { + this._drawShape(ctx, 'triangleDown'); +}; + +Graph.Package.prototype._drawSquare = function (ctx) { + this._drawShape(ctx, 'square'); +}; + +Graph.Package.prototype._drawStar = function (ctx) { + this._drawShape(ctx, 'star'); +}; + +Graph.Package.prototype._drawShape = function (ctx, shape) { + // set style + ctx.fillStyle = this.color; + + // draw shape + var pos = this._getPosition(); + ctx[shape](pos.x, pos.y, this.radius); + ctx.fill(); +}; + +/** + * Redraw a package as an image + * Draw this edge in the given canvas + * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d"); + * @param {CanvasRenderingContext2D} ctx + */ +Graph.Package.prototype._drawImage = function (ctx) { + if (this.imageObj) { + var width, height; + if (this.value) { + var scale = this.imageObj.height / this.imageObj.width; + width = this.radius || this.imageObj.width; + height = this.radius * scale || this.imageObj.height; + } + else { + width = this.imageObj.width; + height = this.imageObj.height; + } + var pos = this._getPosition(); + + ctx.drawImage(this.imageObj, pos.x - width / 2, pos.y - height / 2, width, height); + } + else { + console.log("image still loading..."); + } +}; + + + +/**--------------------------------------------------------------------------**/ + + +/** + * @class Groups + * This class can store groups and properties specific for groups. + */ +Graph.Groups = function () { + this.clear(); + this.defaultIndex = 0; +}; + + +/** + * default constants for group colors + */ +Graph.Groups.DEFAULT = [ + {"borderColor": "#2B7CE9", "backgroundColor": "#97C2FC", "highlightColor": "#D2E5FF"}, // blue + {"borderColor": "#FFA500", "backgroundColor": "#FFFF00", "highlightColor": "#FFFFA3"}, // yellow + {"borderColor": "#FA0A10", "backgroundColor": "#FB7E81", "highlightColor": "#FFAFB1"}, // red + {"borderColor": "#41A906", "backgroundColor": "#7BE141", "highlightColor": "#A1EC76"}, // green + {"borderColor": "#E129F0", "backgroundColor": "#EB7DF4", "highlightColor": "#F0B3F5"}, // magenta + {"borderColor": "#7C29F0", "backgroundColor": "#AD85E4", "highlightColor": "#D3BDF0"}, // purple + {"borderColor": "#C37F00", "backgroundColor": "#FFA807", "highlightColor": "#FFCA66"}, // orange + {"borderColor": "#4220FB", "backgroundColor": "#6E6EFD", "highlightColor": "#9B9BFD"}, // darkblue + {"borderColor": "#FD5A77", "backgroundColor": "#FFC0CB", "highlightColor": "#FFD1D9"}, // pink + {"borderColor": "#4AD63A", "backgroundColor": "#C2FABC", "highlightColor": "#E6FFE3"} // mint +]; + + +/** + * Clear all groups + */ +Graph.Groups.prototype.clear = function () { + this.groups = {}; + this.groups.length = function() + { + var i = 0; + for ( var p in this ) { + if (this.hasOwnProperty(p)) { + i++; + } + } + return i; + } +}; + + +/** + * get group properties of a groupname. If groupname is not found, a new group + * is added. + * @param {*} groupname Can be a number, string, Date, etc. + * @return {Object} group The created group, containing all group properties + */ +Graph.Groups.prototype.get = function (groupname) { + var group = this.groups[groupname]; + + if (group == undefined) { + // create new group + var index = this.defaultIndex % Graph.Groups.DEFAULT.length; + this.defaultIndex++; + group = {}; + group.borderColor = Graph.Groups.DEFAULT[index].borderColor; + group.backgroundColor = Graph.Groups.DEFAULT[index].backgroundColor; + group.highlightColor = Graph.Groups.DEFAULT[index].highlightColor; + this.groups[groupname] = group; + } + + return group; +}; + +/** + * Add a custom group style + * @param {String} groupname + * @param {Object} style An object containing borderColor, + * backgroundColor, etc. + * @return {Object} group The created group object + */ +Graph.Groups.prototype.add = function (groupname, style) { + this.groups[groupname] = style; + return style; +}; + +/** + * Check if given object is a Javascript Array + * @param {*} obj + * @return {Boolean} isArray true if the given object is an array + */ +// See http://stackoverflow.com/questions/2943805/javascript-instanceof-typeof-in-gwt-jsni +Graph.isArray = function (obj) { + if (obj instanceof Array) { + return true; + } + return (Object.prototype.toString.call(obj) === '[object Array]'); +}; + + + +/**--------------------------------------------------------------------------**/ + + +/** + * @class Slider + * + * An html slider control with start/stop/prev/next buttons + * @param {Element} container The element where the slider will be created + */ +Graph.Slider = function(container) { + if (container === undefined) throw "Error: No container element defined"; + + this.container = container; + + this.frame = document.createElement("DIV"); + //this.frame.style.backgroundColor = "#E5E5E5"; + this.frame.style.width = "100%"; + this.frame.style.position = "relative"; + + this.title = document.createElement("DIV"); + this.title.style.margin = "2px"; + this.title.style.marginBottom = "5px"; + this.title.innerHTML = ""; + this.container.appendChild(this.title); + + this.frame.prev = document.createElement("INPUT"); + this.frame.prev.type = "BUTTON"; + this.frame.prev.value = "Prev"; + this.frame.appendChild(this.frame.prev); + + this.frame.play = document.createElement("INPUT"); + this.frame.play.type = "BUTTON"; + this.frame.play.value = "Play"; + this.frame.appendChild(this.frame.play); + + this.frame.next = document.createElement("INPUT"); + this.frame.next.type = "BUTTON"; + this.frame.next.value = "Next"; + this.frame.appendChild(this.frame.next); + + this.frame.bar = document.createElement("INPUT"); + this.frame.bar.type = "BUTTON"; + this.frame.bar.style.position = "absolute"; + this.frame.bar.style.border = "1px solid red"; + this.frame.bar.style.width = "100px"; + this.frame.bar.style.height = "6px"; + this.frame.bar.style.borderRadius = "2px"; + this.frame.bar.style.MozBorderRadius = "2px"; + this.frame.bar.style.border = "1px solid #7F7F7F"; + this.frame.bar.style.backgroundColor = "#E5E5E5"; + this.frame.appendChild(this.frame.bar); + + this.frame.slide = document.createElement("INPUT"); + this.frame.slide.type = "BUTTON"; + this.frame.slide.style.margin = "0px"; + this.frame.slide.value = " "; + this.frame.slide.style.position = "relative"; + this.frame.slide.style.left = "-100px"; + this.frame.appendChild(this.frame.slide); + + // create events + var me = this; + this.frame.slide.onmousedown = function (event) {me._onMouseDown(event);}; + this.frame.prev.onclick = function (event) {me.prev(event);}; + this.frame.play.onclick = function (event) {me.togglePlay(event);}; + this.frame.next.onclick = function (event) {me.next(event);}; + + this.container.appendChild(this.frame); + + this.onChangeCallback = undefined; + + this.playTimeout = undefined; + this.framerate = 20; // frames per second + this.duration = 10; // seconds + this.doLoop = true; + + this.start = 0; + this.end = 0; + this.value = 0; + this.step = 0; + this.rangeIsDate = false; + + this.redraw(); +}; + +/** + * Retrieve the step size, depending on the range, framerate, and duration + */ +Graph.Slider.prototype._updateStep = function() { + var range = (this.end - this.start); + var frameCount = this.duration * this.framerate; + + this.step = range / frameCount; +}; + +/** + * Select the previous index + */ +Graph.Slider.prototype.prev = function() { + this._setValue(this.value - this.step); +}; + +/** + * Select the next index + */ +Graph.Slider.prototype.next = function() { + this._setValue(this.value + this.step); +}; + +/** + * Select the next index + */ +Graph.Slider.prototype.playNext = function() { + var start = new Date(); + + if (!this.leftButtonDown) { + if (this.value + this.step < this.end) { + this._setValue(this.value + this.step); + } + else { + if (this.doLoop) { + this._setValue(this.start); + } + else { + this._setValue(this.end); + this.stop(); + return; + } + } + } + + var end = new Date(); + var diff = (end - start); + + // calculate how much time it to to set the index and to execute the callback + // function. + var interval = Math.max(1000 / this.framerate - diff, 0); + + var me = this; + this.playTimeout = setTimeout(function() {me.playNext();}, interval); +}; + +/** + * Toggle start or stop playing + */ +Graph.Slider.prototype.togglePlay = function() { + if (this.playTimeout === undefined) { + this.play(); + } else { + this.stop(); + } +}; + +/** + * Start playing + */ +Graph.Slider.prototype.play = function() { + this.frame.play.value = "Stop"; + + this.playNext(); +}; + +/** + * Stop playing + */ +Graph.Slider.prototype.stop = function() { + this.frame.play.value = "Play"; + + clearInterval(this.playTimeout); + this.playTimeout = undefined; +}; + +/** + * Set a callback function which will be triggered when the value of the + * slider bar has changed. + */ +Graph.Slider.prototype.setOnChangeCallback = function(callback) { + this.onChangeCallback = callback; +}; + +/** + * Set the interval for playing the list + * @param {number} framerate Framerate in frames per second + */ +Graph.Slider.prototype.setFramerate = function(framerate) { + this.framerate = framerate; + this._updateStep(); +}; + +/** + * Retrieve the current framerate + * @return {number} framerate in frames per second + */ +Graph.Slider.prototype.getFramerate = function() { + return this.framerate; +}; + +/** + * Set the duration for playing + * @param {number} duration Duration in seconds + */ +Graph.Slider.prototype.setDuration = function(duration) { + this.duration = duration; + this._updateStep(); +}; + +/** + * Set the time acceleration for playing the history. Only applicable when + * the values are of type Date. + * @param {number} acceleration Acceleration, for example 10 means play + * ten times as fast as real time. A value + * of 1 will play the history in real time. + */ +Graph.Slider.prototype.setAcceleration = function(acceleration) { + var durationRealtime = (this.end - this.start) / 1000; // in seconds + + this.duration = durationRealtime / acceleration; + this._updateStep(); +}; + + +/** + * Set looping on or off + * @param {boolean} doLoop If true, the slider will jump to the start when + * the end is passed, and will jump to the end + * when the start is passed. + */ +Graph.Slider.prototype.setLoop = function(doLoop) { + this.doLoop = doLoop; +}; + +/** + * Retrieve the current value of loop + * @return {boolean} doLoop If true, the slider will jump to the start when + * the end is passed, and will jump to the end + * when the start is passed. + */ +Graph.Slider.prototype.getLoop = function() { + return this.doLoop; +}; + + +/** + * Execute the onchange callback function + */ +Graph.Slider.prototype.onChange = function() { + if (this.onChangeCallback !== undefined) { + this.onChangeCallback(); + } +}; + +/** + * redraw the slider on the correct place + */ +Graph.Slider.prototype.redraw = function() { + // resize the bar + var barTop = (this.frame.clientHeight/2 - + this.frame.bar.offsetHeight/2); + var barWidth = (this.frame.clientWidth - + this.frame.prev.clientWidth - + this.frame.play.clientWidth - + this.frame.next.clientWidth - 30); + this.frame.bar.style.top = barTop + "px"; + this.frame.bar.style.width = barWidth + "px"; + + // position the slider button + this.frame.slide.title = this.getValue(); + this.frame.slide.style.left = this._valueToLeft(this.value) + "px"; + + // set the title + this.title.innerHTML = this.getValue(); +}; + + +/** + * Set the range for the slider + * @param {Date | Number} start Start of the range + * @param {Date | Number} end End of the range + */ +Graph.Slider.prototype.setRange = function(start, end) { + if (start === undefined || start === null || start === NaN) { + this.start = 0; + this.rangeIsDate = false; + } + else if (start instanceof Date) { + this.start = start.getTime(); + this.rangeIsDate = true; + } + else { + this.start = start; + this.rangeIsDate = false; + } + + if (end === undefined || end === null || end === NaN) { + if (this.start instanceof Date) { + this.end = new Date(this.start); + } + else { + this.end = this.start; + } + } + else if (end instanceof Date) { + this.end = end.getTime(); + } + else { + this.end = end; + } + + this.value = this.start; + + this._updateStep(); + this.redraw(); +}; + + + +/** + * Set a value for the slider. The value must be between start and end + * When the range are Dates, the value will be translated to a date + * @param {Number} value + */ +Graph.Slider.prototype._setValue = function(value) { + this.value = this._limitValue(value); + this.redraw(); + + this.onChange(); +}; + +/** + * retrieve the current value in the correct type, Number or Date + * @return {Date | Number} value + */ +Graph.Slider.prototype.getValue = function() { + if (this.rangeIsDate) { + return new Date(this.value); + } + else { + return this.value; + } +}; + + +Graph.Slider.prototype.offset = 3; + +Graph.Slider.prototype._leftToValue = function (left) { + var width = parseFloat(this.frame.bar.style.width) - + this.frame.slide.clientWidth - 10; + var x = left - this.offset; + + var range = this.end - this.start; + var value = this._limitValue(x / width * range + this.start); + + return value; +}; + +Graph.Slider.prototype._valueToLeft = function (value) { + var width = parseFloat(this.frame.bar.style.width) - + this.frame.slide.clientWidth - 10; + + var x; + if (this.end > this.start) { + x = (value - this.start) / (this.end - this.start) * width; + } + else { + x = 0; + } + var left = x + this.offset; + + return left; +}; + +Graph.Slider.prototype._limitValue = function(value) { + if (value < this.start) { + value = this.start + } + if (value > this.end) { + value = this.end; + } + + return value; +}; + +Graph.Slider.prototype._onMouseDown = function(event) { + // only react on left mouse button down + this.leftButtonDown = event.which ? (event.which === 1) : (event.button === 1); + if (!this.leftButtonDown) return; + + this.startClientX = event.clientX; + this.startSlideX = parseFloat(this.frame.slide.style.left); + + this.frame.style.cursor = 'move'; + + // add event listeners to handle moving the contents + // we store the function onmousemove and onmouseup in the graph, so we can + // remove the eventlisteners lateron in the function mouseUp() + var me = this; + this.onmousemove = function (event) {me._onMouseMove(event);}; + this.onmouseup = function (event) {me._onMouseUp(event);}; + Graph.addEventListener(document, "mousemove", this.onmousemove); + Graph.addEventListener(document, "mouseup", this.onmouseup); + Graph.preventDefault(event); +}; + + +Graph.Slider.prototype._onMouseMove = function (event) { + var diff = event.clientX - this.startClientX; + var x = this.startSlideX + diff; + + var value = this._leftToValue(x); + this._setValue(value); + + Graph.preventDefault(event); +}; + + +Graph.Slider.prototype._onMouseUp = function (event) { + this.frame.style.cursor = 'auto'; + + this.leftButtonDown = false; + + // remove event listeners + Graph.removeEventListener(document, "mousemove", this.onmousemove); + Graph.removeEventListener(document, "mouseup", this.onmouseup); + + Graph.preventDefault(event); +}; + + + +/**--------------------------------------------------------------------------**/ + + +/** + * Popup is a class to create a popup window with some text + * @param {Element} container The container object. + * @param {Number} x + * @param {Number} y + * @param {String} text + */ +Graph.Popup = function (container, x, y, text) { + if (container) { + this.container = container; + } + else { + this.container = document.body; + } + this.x = 0; + this.y = 0; + this.padding = 5; + + if (x !== undefined && y !== undefined ) { + this.setPosition(x, y); + } + if (text !== undefined) { + this.setText(text); + } + + // create the frame + this.frame = document.createElement("div"); + var style = this.frame.style; + style.position = "absolute"; + style.visibility = "hidden"; + style.border = "1px solid #666"; + style.color = "black"; + style.padding = this.padding + "px"; + style.backgroundColor = "#FFFFC6"; + style.borderRadius = "3px"; + style.MozBorderRadius = "3px"; + style.WebkitBorderRadius = "3px"; + style.boxShadow = "3px 3px 10px rgba(128, 128, 128, 0.5)"; + style.whiteSpace = "nowrap"; + this.container.appendChild(this.frame); +}; + +/** + * @param {number} x Horizontal position of the popup window + * @param {number} y Vertical position of the popup window + */ +Graph.Popup.prototype.setPosition = function(x, y) { + this.x = parseInt(x); + this.y = parseInt(y); +}; + +/** + * Set the text for the popup window. This can be HTML code + * @param {string} text + */ +Graph.Popup.prototype.setText = function(text) { + this.frame.innerHTML = text; +}; + +/** + * Show the popup window + * @param {boolean} show Optional. Show or hide the window + */ +Graph.Popup.prototype.show = function (show) { + if (show === undefined) { + show = true; + } + + if (show) { + var height = this.frame.clientHeight; + var width = this.frame.clientWidth; + var maxHeight = this.frame.parentNode.clientHeight; + var maxWidth = this.frame.parentNode.clientWidth; + + var top = (this.y - height); + if (top + height + this.padding > maxHeight) { + top = maxHeight - height - this.padding; + } + if (top < this.padding) { + top = this.padding; + } + + var left = this.x; + if (left + width + this.padding > maxWidth) { + left = maxWidth - width - this.padding; + } + if (left < this.padding) { + left = this.padding; + } + + this.frame.style.left = left + "px"; + this.frame.style.top = top + "px"; + this.frame.style.visibility = "visible"; + } + else { + this.hide(); + } +}; + +/** + * Hide the popup window + */ +Graph.Popup.prototype.hide = function () { + this.frame.style.visibility = "hidden"; +}; + + +/**--------------------------------------------------------------------------**/ + +if (typeof CanvasRenderingContext2D !== 'undefined') { + /** + * Draw a circle shape + */ + CanvasRenderingContext2D.prototype.circle = function(x, y, r) { + this.beginPath(); + this.arc(x, y, r, 0, 2*Math.PI, false); + }; + + /** + * Draw a square shape + * @param {Number} x horizontal center + * @param {Number} y vertical center + * @param {Number} r size, width and height of the square + */ + CanvasRenderingContext2D.prototype.square = function(x, y, r) { + this.beginPath(); + this.rect(x - r, y - r, r * 2, r * 2); + }; + + /** + * Draw a triangle shape + * @param {Number} x horizontal center + * @param {Number} y vertical center + * @param {Number} r radius, half the length of the sides of the triangle + */ + CanvasRenderingContext2D.prototype.triangle = function(x, y, r) { + // http://en.wikipedia.org/wiki/Equilateral_triangle + this.beginPath(); + + var s = r * 2; + var s2 = s / 2; + var ir = Math.sqrt(3) / 6 * s; // radius of inner circle + var h = Math.sqrt(s * s - s2 * s2); // height + + this.moveTo(x, y - (h - ir)); + this.lineTo(x + s2, y + ir); + this.lineTo(x - s2, y + ir); + this.lineTo(x, y - (h - ir)); + this.closePath(); + }; + + /** + * Draw a triangle shape in downward orientation + * @param {Number} x horizontal center + * @param {Number} y vertical center + * @param {Number} r radius + */ + CanvasRenderingContext2D.prototype.triangleDown = function(x, y, r) { + // http://en.wikipedia.org/wiki/Equilateral_triangle + this.beginPath(); + + var s = r * 2; + var s2 = s / 2; + var ir = Math.sqrt(3) / 6 * s; // radius of inner circle + var h = Math.sqrt(s * s - s2 * s2); // height + + this.moveTo(x, y + (h - ir)); + this.lineTo(x + s2, y - ir); + this.lineTo(x - s2, y - ir); + this.lineTo(x, y + (h - ir)); + this.closePath(); + }; + + /** + * Draw a star shape, a star with 5 points + * @param {Number} x horizontal center + * @param {Number} y vertical center + * @param {Number} r radius, half the length of the sides of the triangle + */ + CanvasRenderingContext2D.prototype.star = function(x, y, r) { + // http://www.html5canvastutorials.com/labs/html5-canvas-star-spinner/ + this.beginPath(); + + for (var n = 0; n < 10; n++) { + var radius = (n % 2 === 0) ? r * 1.3 : r * 0.5; + this.lineTo( + x + radius * Math.sin(n * 2 * Math.PI / 10), + y - radius * Math.cos(n * 2 * Math.PI / 10) + ); + } + + this.closePath(); + }; + + /** + * http://stackoverflow.com/questions/1255512/how-to-draw-a-rounded-rectangle-on-html-canvas + */ + CanvasRenderingContext2D.prototype.roundRect = function(x, y, w, h, r) { + var r2d = Math.PI/180; + if( w - ( 2 * r ) < 0 ) { r = ( w / 2 ); } //ensure that the radius isn't too large for x + if( h - ( 2 * r ) < 0 ) { r = ( h / 2 ); } //ensure that the radius isn't too large for y + this.beginPath(); + this.moveTo(x+r,y); + this.lineTo(x+w-r,y); + this.arc(x+w-r,y+r,r,r2d*270,r2d*360,false); + this.lineTo(x+w,y+h-r); + this.arc(x+w-r,y+h-r,r,0,r2d*90,false); + this.lineTo(x+r,y+h); + this.arc(x+r,y+h-r,r,r2d*90,r2d*180,false); + this.lineTo(x,y+r); + this.arc(x+r,y+r,r,r2d*180,r2d*270,false); + }; + + /** + * http://stackoverflow.com/questions/2172798/how-to-draw-an-oval-in-html5-canvas + */ + CanvasRenderingContext2D.prototype.ellipse = function(x, y, w, h) { + var kappa = .5522848, + ox = (w / 2) * kappa, // control point offset horizontal + oy = (h / 2) * kappa, // control point offset vertical + xe = x + w, // x-end + ye = y + h, // y-end + xm = x + w / 2, // x-middle + ym = y + h / 2; // y-middle + + this.beginPath(); + this.moveTo(x, ym); + this.bezierCurveTo(x, ym - oy, xm - ox, y, xm, y); + this.bezierCurveTo(xm + ox, y, xe, ym - oy, xe, ym); + this.bezierCurveTo(xe, ym + oy, xm + ox, ye, xm, ye); + this.bezierCurveTo(xm - ox, ye, x, ym + oy, x, ym); + }; + + + + /** + * http://stackoverflow.com/questions/2172798/how-to-draw-an-oval-in-html5-canvas + */ + CanvasRenderingContext2D.prototype.database = function(x, y, w, h) { + var f = 1/3; + var wEllipse = w; + var hEllipse = h * f; + + var kappa = .5522848, + ox = (wEllipse / 2) * kappa, // control point offset horizontal + oy = (hEllipse / 2) * kappa, // control point offset vertical + xe = x + wEllipse, // x-end + ye = y + hEllipse, // y-end + xm = x + wEllipse / 2, // x-middle + ym = y + hEllipse / 2, // y-middle + ymb = y + (h - hEllipse/2), // y-midlle, bottom ellipse + yeb = y + h; // y-end, bottom ellipse + + this.beginPath(); + this.moveTo(xe, ym); + + this.bezierCurveTo(xe, ym + oy, xm + ox, ye, xm, ye); + this.bezierCurveTo(xm - ox, ye, x, ym + oy, x, ym); + + this.bezierCurveTo(x, ym - oy, xm - ox, y, xm, y); + this.bezierCurveTo(xm + ox, y, xe, ym - oy, xe, ym); + + this.lineTo(xe, ymb); + + this.bezierCurveTo(xe, ymb + oy, xm + ox, yeb, xm, yeb); + this.bezierCurveTo(xm - ox, yeb, x, ymb + oy, x, ymb); + + this.lineTo(x, ym); + }; + + + /** + * Draw an arrow point (no line) + */ + CanvasRenderingContext2D.prototype.arrow = function(x, y, angle, length) { + // tail + var xt = x - length * Math.cos(angle); + var yt = y - length * Math.sin(angle); + + // inner tail + // TODO: allow to customize different shapes + var xi = x - length * 0.9 * Math.cos(angle); + var yi = y - length * 0.9 * Math.sin(angle); + + // left + var xl = xt + length / 3 * Math.cos(angle + 0.5 * Math.PI); + var yl = yt + length / 3 * Math.sin(angle + 0.5 * Math.PI); + + // right + var xr = xt + length / 3 * Math.cos(angle - 0.5 * Math.PI); + var yr = yt + length / 3 * Math.sin(angle - 0.5 * Math.PI); + + this.beginPath(); + this.moveTo(x, y); + this.lineTo(xl, yl); + this.lineTo(xi, yi); + this.lineTo(xr, yr); + this.closePath(); + }; + + + // TODO: add diamond shape +} + + +/*----------------------------------------------------------------------------*/ + +// utility methods +Graph.util = {}; + +/** + * Parse a text source containing data in DOT language into a JSON object. + * The object contains two lists: one with nodes and one with edges. + * @param {String} data Text containing a graph in DOT-notation + * @return {Object} json An object containing two parameters: + * {Object[]} nodes + * {Object[]} edges + */ +Graph.util.parseDOT = function (data) { + /** + * Test whether given character is a whitespace character + * @param {String} c + * @return {Boolean} isWhitespace + */ + function isWhitespace(c) { + return c == ' ' || c == '\t' || c == '\n' || c == '\r'; + } + + /** + * Test whether given character is a delimeter + * @param {String} c + * @return {Boolean} isDelimeter + */ + function isDelimeter(c) { + return '[]{}();,=->'.indexOf(c) != -1; + } + + var i = -1; // current index in the data + var c = ''; // current character in the data + + /** + * Read the next character from the data + */ + function next() { + i++; + c = data[i]; + } + + /** + * Preview the next character in the data + * @returns {String} nextChar + */ + function previewNext () { + return data[i + 1]; + } + + /** + * Preview the next character in the data + * @returns {String} nextChar + */ + function previewPrevious () { + return data[i + 1]; + } + + /** + * Get a text description of the the current index in the data + * @return {String} desc + */ + function pos() { + return '(char ' + i + ')'; + } + + /** + * Skip whitespace and comments + */ + function parseWhitespace() { + // skip whitespace + while (c && isWhitespace(c)) { + next(); + } + + // test for comment + var cNext = data[i + 1]; + var cPrev = data[i - 1]; + var c2 = c + cNext; + if (c2 == '/*') { + // block comment. skip until the block is closed + while (c && !(c == '*' && data[i + 1] == '/')) { + next(); + } + next(); + next(); + + parseWhitespace(); + } + else if (c2 == '//' || (c == '#' && cPrev == '\n')) { + // line comment. skip until the next return + while (c && c != '\n') { + next(); + } + next(); + parseWhitespace(); + } + } + + /** + * Parse a string + * The string may be enclosed by double quotes + * @return {String | undefined} value + */ + function parseString() { + parseWhitespace(); + + var name = ''; + if (c == '"') { + next(); + while (c && c != '"') { + name += c; + next(); + } + next(); // skip the closing quote + } + else { + while (c && !isWhitespace(c) && !isDelimeter(c)) { + name += c; + next(); + } + + // cast string to number or boolean + var number = Number(name); + if (!isNaN(number)) { + name = number; + } + else if (name == 'true') { + name = true; + } + else if (name == 'false') { + name = false; + } + else if (name == 'null') { + name = null; + } + } + + return name; + } + + /** + * Parse a value, can be a string, number, or boolean. + * The value may be enclosed by double quotes + * @return {String | Number | Boolean | undefined} value + */ + function parseValue() { + parseWhitespace(); + + if (c == '"') { + return parseString(); + } + else { + var value = parseString(); + if (value != undefined) { + // cast string to number or boolean + var number = Number(value); + if (!isNaN(number)) { + value = number; + } + else if (value == 'true') { + value = true; + } + else if (value == 'false') { + value = false; + } + else if (value == 'null') { + value = null; + } + } + return value; + } + } + + /** + * Parse a set with attributes, + * for example [label="1.000", style=solid] + * @return {Object | undefined} attr + */ + function parseAttributes() { + parseWhitespace(); + + if (c == '[') { + next(); + var attr = {}; + while (c && c != ']') { + parseWhitespace(); + + var name = parseString(); + if (!name) { + throw new SyntaxError('Attribute name expected ' + pos()); + } + + parseWhitespace(); + if (c != '=') { + throw new SyntaxError('Equal sign = expected ' + pos()); + } + next(); + + var value = parseValue(); + if (!value) { + throw new SyntaxError('Attribute value expected ' + pos()); + } + attr[name] = value; + + parseWhitespace(); + + if (c ==',') { + next(); + } + } + next(); + + return attr; + } + else { + return undefined; + } + } + + /** + * Parse a directed or undirected arrow '->' or '--' + * @return {String | undefined} arrow + */ + function parseArrow() { + parseWhitespace(); + + if (c == '-') { + next(); + if (c == '>' || c == '-') { + var arrow = '-' + c; + next(); + return arrow; + } + else { + throw new SyntaxError('Arrow "->" or "--" expected ' + pos()); + } + } + + return undefined; + } + + /** + * Parse a line separator ';' + * @return {String | undefined} separator + */ + function parseSeparator() { + parseWhitespace(); + + if (c == ';') { + next(); + return ';'; + } + + return undefined; + } + + /** + * Merge all properties of object b into object b + * @param {Object} a + * @param {Object} b + */ + function merge (a, b) { + if (a && b) { + for (var name in b) { + if (b.hasOwnProperty(name)) { + a[name] = b[name]; + } + } + } + } + + var nodeMap = {}; + var edgeList = []; + + /** + * Register a node with attributes + * @param {String} id + * @param {Object} [attr] + */ + function addNode(id, attr) { + var node = { + id: String(id), + attr: attr || {} + }; + if (!nodeMap[id]) { + nodeMap[id] = node; + } + else { + merge(nodeMap[id].attr, node.attr); + } + } + + /** + * Register an edge + * @param {String} from + * @param {String} to + * @param {String} type A string "->" or "--" + * @param {Object} [attr] + */ + function addEdge(from, to, type, attr) { + edgeList.push({ + from: String(from), + to: String(to), + type: type, + attr: attr || {} + }); + } + + // find the opening curly bracket + next(); + while (c && c != '{') { + next(); + } + if (c != '{') { + throw new SyntaxError('Invalid data. Curly bracket { expected ' + pos()) + } + next(); + + // parse all data until a closing curly bracket is encountered + while (c && c != '}') { + // parse node id and optional node attributes + var id = parseString(); + if (id == undefined) { + throw new SyntaxError('String with id expected ' + pos()); + } + var attr = parseAttributes(); + addNode(id, attr); + + // TODO: parse global attributes "graph", "node", "edge" + + // parse arrow + var type = parseArrow(); + while (type) { + // parse node id + var prevId = id; + id = parseString(); + if (id == undefined) { + throw new SyntaxError('String with id expected ' + pos()); + } + addNode(id); + + // parse edge attributes and register edge + attr = parseAttributes(); + addEdge(prevId, id, type, attr); + + // parse next arrow (optional) + type = parseArrow(); + } + + // parse separator (optional) + parseSeparator(); + + parseWhitespace(); + } + if (c != '}') { + throw new SyntaxError('Invalid data. Curly bracket } expected'); + } + + // crop data between the curly brackets + var start = data.indexOf('{'); + var end = data.indexOf('}', start); + var text = (start != -1 && end != -1) ? data.substring(start + 1, end) : undefined; + + if (!text) { + throw new Error('Invalid data. no curly brackets containing data found'); + } + + // return the results + var nodeList = []; + for (id in nodeMap) { + if (nodeMap.hasOwnProperty(id)) { + nodeList.push(nodeMap[id]); + } + } + return { + nodes: nodeList, + edges: edgeList + } +}; + +/** + * Convert a string containing a graph in DOT language into a map containing + * with nodes and edges in the format of graph. + * @param {String} data Text containing a graph in DOT-notation + * @return {Object} graphData + */ +Graph.util.DOTToGraph = function (data) { + // parse the DOT file + var dotData = Graph.util.parseDOT(data); + var graphData = { + nodes: [], + edges: [], + options: { + nodes: {}, + edges: {} + } + }; + + /** + * Merge the properties of object b into object a, and adjust properties + * not supported by Graph (for example replace "shape" with "style" + * @param {Object} a + * @param {Object} b + * @param {Array} [ignore] Optional array with property names to be ignored + */ + function merge (a, b, ignore) { + for (var prop in b) { + if (b.hasOwnProperty(prop) && (!ignore || ignore.indexOf(prop) == -1)) { + a[prop] = b[prop]; + } + } + + // Convert aliases to configuration settings supported by Graph + if (a.label) { + a.text = a.label; + delete a.label; + } + if (a.shape) { + a.style = a.shape; + delete a.shape; + } + } + + dotData.nodes.forEach(function (node) { + if (node.id.toLowerCase() == 'graph') { + merge(graphData.options, node.attr); + } + else if (node.id.toLowerCase() == 'node') { + merge(graphData.options.nodes, node.attr); + } + else if (node.id.toLowerCase() == 'edge') { + merge(graphData.options.edges, node.attr); + } + else { + var graphNode = {}; + graphNode.id = node.id; + graphNode.text = node.id; + merge(graphNode, node.attr); + graphData.nodes.push(graphNode); + } + }); + + dotData.edges.forEach(function (edge) { + var graphEdge = {}; + graphEdge.from = edge.from; + graphEdge.to = edge.to; + graphEdge.text = edge.id; + graphEdge.style = (edge.type == '->') ? 'arrow-end' : 'line'; + merge(graphEdge, edge.attr); + graphData.edges.push(graphEdge); + }); + + return graphData; +}; diff --git a/src/module/exports.js b/src/module/exports.js index 9d84c54f..91c9484a 100644 --- a/src/module/exports.js +++ b/src/module/exports.js @@ -28,7 +28,8 @@ var vis = { TimeAxis: TimeAxis }, - Timeline: Timeline + Timeline: Timeline, + Graph: Graph }; /** diff --git a/vis.js b/vis.js index 11a40b8d..e044cb3a 100644 --- a/vis.js +++ b/vis.js @@ -5,7 +5,7 @@ * A dynamic, browser-based visualization library. * * @version 0.0.8 - * @date 2013-05-31 + * @date 2013-06-03 * * @license * Copyright (C) 2011-2013 Almende B.V, http://almende.com @@ -24,7 +24,7 @@ */ (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; return (function(e,t,n){function i(n,s){if(!t[n]){if(!e[n]){var o=typeof require=="function"&&require;if(!s&&o)return o(n,!0);if(r)return r(n,!0);throw new Error("Cannot find module '"+n+"'")}var u=t[n]={exports:{}};e[n][0].call(u.exports,function(t){var r=e[n][1][t];return i(r?r:t)},u,u.exports)}return t[n].exports}var r=typeof require=="function"&&require;for(var s=0;s 0) ? + overlappingNodes[overlappingNodes.length - 1] : undefined; + + if (this.startClickedObj) { + // move clicked node with the mouse + + // make the clicked node temporarily fixed, and store their original state + var node = this.nodes[this.startClickedObj.row]; + this.startClickedObj.xFixed = node.xFixed; + this.startClickedObj.yFixed = node.yFixed; + node.xFixed = true; + node.yFixed = true; + + if (!this.ctrlKeyDown || !node.isSelected()) { + // select this node + this._selectNodes([this.startClickedObj], this.ctrlKeyDown); + } + else { + // unselect this node + this._unselectNodes([this.startClickedObj]); + } + + if (!this.hasMovingNodes) { + this._redraw(); + } + } + else if (this.shiftKeyDown) { + // start selection of multiple nodes + } + else { + // start moving the graph + this.moved = false; + } +}; /** - * Window exports + * handle on mouse move event */ -if (typeof window !== 'undefined') { - // attach the module to the window, load as a regular javascript file - window['vis'] = vis; -} +Graph.prototype._onMouseMove = function (event) { + event = event || window.event; -// inject css -util.loadCss("/* vis.js stylesheet */\n\n.graph {\n position: relative;\n border: 1px solid #bfbfbf;\n}\n\n.graph .panel {\n position: absolute;\n}\n\n.graph .groupset {\n position: absolute;\n padding: 0;\n margin: 0;\n}\n\n\n.graph .itemset {\n position: absolute;\n padding: 0;\n margin: 0;\n overflow: hidden;\n}\n\n.graph .background {\n}\n\n.graph .foreground {\n}\n\n.graph .itemset-axis {\n position: absolute;\n}\n\n.graph .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.graph .groupset .itemset-axis:last-child {\n border-top: none;\n}\n*/\n\n\n.graph .item {\n position: absolute;\n color: #1A1A1A;\n border-color: #97B0F8;\n background-color: #D5DDF6;\n display: inline-block;\n}\n\n.graph .item.selected {\n border-color: #FFC200;\n background-color: #FFF785;\n z-index: 999;\n}\n\n.graph .item.cluster {\n /* TODO: use another color or pattern? */\n background: #97B0F8 url('img/cluster_bg.png');\n color: white;\n}\n.graph .item.cluster.point {\n border-color: #D5DDF6;\n}\n\n.graph .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.graph .item.point {\n background: none;\n}\n\n.graph .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.graph .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.graph .item.range .drag-left {\n cursor: w-resize;\n z-index: 1000;\n}\n\n.graph .item.range .drag-right {\n cursor: e-resize;\n z-index: 1000;\n}\n\n.graph .item.range .content {\n position: relative;\n display: inline-block;\n}\n\n.graph .item.line {\n position: absolute;\n width: 0;\n border-left-width: 1px;\n border-left-style: solid;\n}\n\n.graph .item .content {\n margin: 5px;\n white-space: nowrap;\n overflow: hidden;\n}\n\n/* TODO: better css name, 'graph' is way to generic */\n\n.graph {\n overflow: hidden;\n}\n\n.graph .axis {\n position: relative;\n}\n\n.graph .axis .text {\n position: absolute;\n color: #4d4d4d;\n padding: 3px;\n white-space: nowrap;\n}\n\n.graph .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.graph .axis .grid.vertical {\n position: absolute;\n width: 0;\n border-right: 1px solid;\n}\n\n.graph .axis .grid.horizontal {\n position: absolute;\n left: 0;\n width: 100%;\n height: 0;\n border-bottom: 1px solid;\n}\n\n.graph .axis .grid.minor {\n border-color: #e5e5e5;\n}\n\n.graph .axis .grid.major {\n border-color: #bfbfbf;\n}\n\n"); + if (!this.selectable) { + return; + } + + var mouseX = event.clientX || (event.targetTouches && event.targetTouches[0].clientX) || 0; + var mouseY = event.clientY || (event.targetTouches && event.targetTouches[0].clientY) || 0; + this.mouseX = mouseX; + this.mouseY = mouseY; + + if (this.startClickedObj) { + var node = this.nodes[this.startClickedObj.row]; + + if (!this.startClickedObj.xFixed) + node.x = this._xToCanvas(mouseX - this.startFrameLeft); + + if (!this.startClickedObj.yFixed) + node.y = this._yToCanvas(mouseY - this.startFrameTop); + + // start animation if not yet running + if (!this.hasMovingNodes) { + this.hasMovingNodes = true; + this.start(); + } + } + else if (this.shiftKeyDown) { + // draw a rect from start mouse location to current mouse location + if (this.frame.selRect == undefined) { + this.frame.selRect = document.createElement("DIV"); + this.frame.appendChild(this.frame.selRect); + + this.frame.selRect.style.position = "absolute"; + this.frame.selRect.style.border = "1px dashed red"; + } + + var left = Math.min(this.startMouseX, mouseX) - this.startFrameLeft; + var top = Math.min(this.startMouseY, mouseY) - this.startFrameTop; + var right = Math.max(this.startMouseX, mouseX) - this.startFrameLeft; + var bottom = Math.max(this.startMouseY, mouseY) - this.startFrameTop; + + this.frame.selRect.style.left = left + "px"; + this.frame.selRect.style.top = top + "px"; + this.frame.selRect.style.width = (right - left) + "px"; + this.frame.selRect.style.height = (bottom - top) + "px"; + } + else { + // move the network + var diffX = mouseX - this.startMouseX; + var diffY = mouseY - this.startMouseY; + + this._setTranslation( + this.startTranslation.x + diffX, + this.startTranslation.y + diffY); + this._redraw(); + + this.moved = true; + } + + Graph.preventDefault(event); +}; + +/** + * handle on mouse up event + */ +Graph.prototype._onMouseUp = function (event) { + event = event || window.event; + + if (!this.selectable) { + return; + } + + // remove event listeners here, important for Safari + if (this.onmousemove) { + Graph.removeEventListener(document, "mousemove", this.onmousemove); + this.onmousemove = undefined; + } + if (this.onmouseup) { + Graph.removeEventListener(document, "mouseup", this.onmouseup); + this.onmouseup = undefined; + } + Graph.preventDefault(event); + + // check selected nodes + var endMouseX = event.clientX || this.mouseX || 0; + var endMouseY = event.clientY || this.mouseY || 0; + + var ctrlKey = event ? event.ctrlKey : window.event.ctrlKey; + + if (this.startClickedObj) { + // restore the original fixed state + var node = this.nodes[this.startClickedObj.row]; + node.xFixed = this.startClickedObj.xFixed; + node.yFixed = this.startClickedObj.yFixed; + } + else if (this.shiftKeyDown) { + // select nodes inside selection area + var obj = { + "left": this._xToCanvas(Math.min(this.startMouseX, endMouseX) - this.startFrameLeft), + "top": this._yToCanvas(Math.min(this.startMouseY, endMouseY) - this.startFrameTop), + "right": this._xToCanvas(Math.max(this.startMouseX, endMouseX) - this.startFrameLeft), + "bottom": this._yToCanvas(Math.max(this.startMouseY, endMouseY) - this.startFrameTop) + }; + var overlappingNodes = this._getNodesOverlappingWith(obj); + this._selectNodes(overlappingNodes, ctrlKey); + this.redraw(); + + // remove the selection rectangle + if (this.frame.selRect) { + this.frame.removeChild(this.frame.selRect); + this.frame.selRect = undefined; + } + } + else { + if (!this.ctrlKeyDown && !this.moved) { + // remove selection + this._unselectNodes(); + this._redraw(); + } + } + + this.leftButtonDown = false; + this.ctrlKeyDown = false; +}; + + +/** + * Event handler for mouse wheel event, used to zoom the timeline + * Code from http://adomas.org/javascript-mouse-wheel/ + * @param {event} event The event + */ +Graph.prototype._onMouseWheel = function(event) { + event = event || window.event; + var mouseX = event.clientX; + var mouseY = event.clientY; + + // retrieve delta + var delta = 0; + if (event.wheelDelta) { /* IE/Opera. */ + delta = event.wheelDelta/120; + } else if (event.detail) { /* Mozilla case. */ + // In Mozilla, sign of delta is different than in IE. + // Also, delta is multiple of 3. + delta = -event.detail/3; + } + + // If delta is nonzero, handle it. + // Basically, delta is now positive if wheel was scrolled up, + // and negative, if wheel was scrolled down. + if (delta) { + // determine zoom factor, and adjust the zoom factor such that zooming in + // and zooming out correspond wich each other + var zoom = delta / 10; + if (delta < 0) { + zoom = zoom / (1 - zoom); + } + + var scaleOld = this._getScale(); + var scaleNew = scaleOld * (1 + zoom); + if (scaleNew < 0.01) { + scaleNew = 0.01; + } + if (scaleNew > 10) { + scaleNew = 10; + } + + var frameLeft = Graph._getAbsoluteLeft(this.frame.canvas); + var frameTop = Graph._getAbsoluteTop(this.frame.canvas); + var x = mouseX - frameLeft; + var y = mouseY - frameTop; + + var translation = this._getTranslation(); + var scaleFrac = scaleNew / scaleOld; + var tx = (1 - scaleFrac) * x + translation.x * scaleFrac; + var ty = (1 - scaleFrac) * y + translation.y * scaleFrac; + + this._setScale(scaleNew); + this._setTranslation(tx, ty); + this._redraw(); + } + + // Prevent default actions caused by mouse wheel. + // That might be ugly, but we handle scrolls somehow + // anyway, so don't bother here... + Graph.preventDefault(event); +}; + + +/** + * Mouse move handler for checking whether the title moves over a node or + * package with a title. + */ +Graph.prototype._onMouseMoveTitle = function (event) { + event = event || window.event; + + var startMouseX = event.clientX; + var startMouseY = event.clientY; + this.startFrameLeft = this.startFrameLeft || Graph._getAbsoluteLeft(this.frame.canvas); + this.startFrameTop = this.startFrameTop || Graph._getAbsoluteTop(this.frame.canvas); + + var x = startMouseX - this.startFrameLeft; + var y = startMouseY - this.startFrameTop; + + // check if the previously selected node is still selected + if (this.popupNode) { + this._checkHidePopup(x, y); + } + + // start a timeout that will check if the mouse is positioned above + // an element + var me = this; + var checkShow = function() { + me._checkShowPopup(x, y); + }; + if (this.popupTimer) { + clearInterval(this.popupTimer); // stop any running timer + } + if (!this.leftButtonDown) { + this.popupTimer = setTimeout(checkShow, 300); + } +}; + +/** + * Check if there is an element on the given position in the network ( + * (a node, package, or edge). If so, and if this element has a title, + * show a popup window with its title. + * + * @param {number} x + * @param {number} y + */ +Graph.prototype._checkShowPopup = function (x, y) { + var obj = { + "left" : this._xToCanvas(x), + "top" : this._yToCanvas(y), + "right" : this._xToCanvas(x), + "bottom" : this._yToCanvas(y) + }; + + var i, len; + var lastPopupNode = this.popupNode; + + if (this.popupNode == undefined) { + // search the packages for overlap + + for (i = 0, len = this.packages.length; i < len; i++) { + var p = this.packages[i]; + if (p.getTitle() != undefined && p.isOverlappingWith(obj)) { + this.popupNode = p; + break; + } + } + } + + if (this.popupNode == undefined) { + // search the nodes for overlap, select the top one in case of multiple nodes + var nodes = this.nodes; + for (i = nodes.length - 1; i >= 0; i--) { + var node = nodes[i]; + if (node.getTitle() != undefined && node.isOverlappingWith(obj)) { + this.popupNode = node; + break; + } + } + } + + if (this.popupNode == undefined) { + // search the edges for overlap + var allEdges = this.edges; + for (i = 0, len = allEdges.length; i < len; i++) { + var edge = allEdges[i]; + if (edge.getTitle() != undefined && edge.isOverlappingWith(obj)) { + this.popupNode = edge; + break; + } + } + } + + if (this.popupNode) { + // show popup message window + if (this.popupNode != lastPopupNode) { + var me = this; + if (!me.popup) { + me.popup = new Graph.Popup(me.frame); + } + + // adjust a small offset such that the mouse cursor is located in the + // bottom left location of the popup, and you can easily move over the + // popup area + me.popup.setPosition(x - 3, y - 3); + me.popup.setText(me.popupNode.getTitle()); + me.popup.show(); + } + } + else { + if (this.popup) { + this.popup.hide(); + } + } +}; + +/** + * Check if the popup must be hided, which is the case when the mouse is no + * longer hovering on the object + * @param {number} x + * @param {number} y + */ +Graph.prototype._checkHidePopup = function (x, y) { + var obj = { + "left" : x, + "top" : y, + "right" : x, + "bottom" : y + }; + if (!this.popupNode || !this.popupNode.isOverlappingWith(obj) ) { + this.popupNode = undefined; + if (this.popup) { + this.popup.hide(); + } + } +}; + +/** + * Event handler for touchstart event on mobile devices + */ +Graph.prototype._onTouchStart = function(event) { + Graph.preventDefault(event); + + if (this.touchDown) { + // if already moving, return + return; + } + this.touchDown = true; + + var me = this; + if (!this.ontouchmove) { + this.ontouchmove = function (event) {me._onTouchMove(event);}; + Graph.addEventListener(document, "touchmove", this.ontouchmove); + } + if (!this.ontouchend) { + this.ontouchend = function (event) {me._onTouchEnd(event);}; + Graph.addEventListener(document, "touchend", this.ontouchend); + } + + this._onMouseDown(event); +}; + +/** + * Event handler for touchmove event on mobile devices + */ +Graph.prototype._onTouchMove = function(event) { + Graph.preventDefault(event); + this._onMouseMove(event); +}; + +/** + * Event handler for touchend event on mobile devices + */ +Graph.prototype._onTouchEnd = function(event) { + Graph.preventDefault(event); + + this.touchDown = false; + + if (this.ontouchmove) { + Graph.removeEventListener(document, "touchmove", this.ontouchmove); + this.ontouchmove = undefined; + } + if (this.ontouchend) { + Graph.removeEventListener(document, "touchend", this.ontouchend); + this.ontouchend = undefined; + } + + this._onMouseUp(event); +}; + + +/** + * Unselect selected nodes. If no selection array is provided, all nodes + * are unselected + * @param {Object[]} selection Array with selection objects, each selection + * object has a parameter row. Optional + * @param {Boolean} triggerSelect If true (default), the select event + * is triggered when nodes are unselected + * @return {Boolean} changed True if the selection is changed + */ +Graph.prototype._unselectNodes = function(selection, triggerSelect) { + var changed = false; + var i, iMax, row; + + if (selection) { + // remove provided selections + for (i = 0, iMax = selection.length; i < iMax; i++) { + row = selection[i].row; + this.nodes[row].unselect(); + + var j = 0; + while (j < this.selection.length) { + if (this.selection[j].row == row) { + this.selection.splice(j, 1); + changed = true; + } + else { + j++; + } + } + } + } + else if (this.selection && this.selection.length) { + // remove all selections + for (i = 0, iMax = this.selection.length; i < iMax; i++) { + row = this.selection[i].row; + this.nodes[row].unselect(); + changed = true; + } + this.selection = []; + } + + if (changed && (triggerSelect == true || triggerSelect == undefined)) { + // fire the select event + this.trigger('select'); + } + + return changed; +}; + +/** + * select all nodes on given location x, y + * @param {Array} selection an array with selection objects. Each selection + * object has a parameter row + * @param {boolean} append If true, the new selection will be appended to the + * current selection (except for duplicate entries) + * @return {Boolean} changed True if the selection is changed + */ +Graph.prototype._selectNodes = function(selection, append) { + var changed = false; + var i, iMax; + + // TODO: the selectNodes method is a little messy, rework this + + // check if the current selection equals the desired selection + var selectionAlreadyDone = true; + if (selection.length != this.selection.length) { + selectionAlreadyDone = false; + } + else { + for (i = 0, iMax = Math.min(selection.length, this.selection.length); i < iMax; i++) { + if (selection[i].row != this.selection[i].row) { + selectionAlreadyDone = false; + break; + } + } + } + if (selectionAlreadyDone) { + return changed; + } + + if (append == undefined || append == false) { + // first deselect any selected node + var triggerSelect = false; + changed = this._unselectNodes(undefined, triggerSelect); + } + + for (i = 0, iMax = selection.length; i < iMax; i++) { + // add each of the new selections, but only when they are not duplicate + var row = selection[i].row; + var isDuplicate = false; + for (var j = 0, jMax = this.selection.length; j < jMax; j++) { + if (this.selection[j].row == row) { + isDuplicate = true; + break; + } + } + + if (!isDuplicate) { + this.nodes[row].select(); + this.selection.push(selection[i]); + changed = true; + } + } + + if (changed) { + // fire the select event + this.trigger('select'); + } + + return changed; +}; + +/** + * retrieve all nodes overlapping with given object + * @param {Object} obj An object with parameters left, top, right, bottom + * @return {Object[]} An array with selection objects containing + * the parameter row. + */ +Graph.prototype._getNodesOverlappingWith = function (obj) { + var overlappingNodes = []; + + for (var i = 0; i < this.nodes.length; i++) { + if (this.nodes[i].isOverlappingWith(obj)) { + var sel = {"row": i}; + overlappingNodes.push(sel); + } + } + + return overlappingNodes; +}; + +/** + * retrieve the currently selected nodes + * @return {Object[]} an array with zero or more objects. Each object + * contains the parameter row + */ +Graph.prototype.getSelection = function() { + var selection = []; + + for (var i = 0; i < this.selection.length; i++) { + var row = this.selection[i].row; + selection.push({"row": row}); + } + + return selection; +}; + +/** + * select zero or more nodes + * @param {object[]} selection an array with zero or more objects. Each object + * contains the parameter row + */ +Graph.prototype.setSelection = function(selection) { + var i, iMax, row; + + if (selection.length == undefined) + throw "Selection must be an array with objects"; + + // first unselect any selected node + for (i = 0, iMax = this.selection.length; i < iMax; i++) { + row = this.selection[i].row; + this.nodes[row].unselect(); + } + + this.selection = []; + + for (i = 0, iMax = selection.length; i < iMax; i++) { + row = selection[i].row; + + if (row == undefined) + throw "Parameter row missing in selection object"; + if (row > this.nodes.length-1) + throw "Parameter row out of range"; + + var sel = {"row": row}; + this.selection.push(sel); + this.nodes[row].select(); + } + + this.redraw(); +}; + + +/** + * Temporary method to test calculating a hub value for the nodes + * @param {number} level Maximum number edges between two nodes in order + * to call them connected. Optional, 1 by default + * @return {Number[]} connectioncount array with the connection count + * for each node + */ +Graph.prototype._getConnectionCount = function(level) { + var conn = this.edges; + if (level == undefined) { + level = 1; + } + + // get the nodes connected to given nodes + function getConnectedNodes(nodes) { + var connectedNodes = []; + + for (var j = 0, jMax = nodes.length; j < jMax; j++) { + var node = nodes[j]; + + // find all nodes connected to this node + for (var i = 0, iMax = conn.length; i < iMax; i++) { + var other = null; + + // check if connected + if (conn[i].from == node) + other = conn[i].to; + else if (conn[i].to == node) + other = conn[i].from; + + // check if the other node is not already in the list with nodes + var k, kMax; + if (other) { + for (k = 0, kMax = nodes.length; k < kMax; k++) { + if (nodes[k] == other) { + other = null; + break; + } + } + } + if (other) { + for (k = 0, kMax = connectedNodes.length; k < kMax; k++) { + if (connectedNodes[k] == other) { + other = null; + break; + } + } + } + + if (other) + connectedNodes.push(other); + } + } + + return connectedNodes; + } + + var connections = []; + var level0 = []; + var nodes = this.nodes; + var i, iMax; + for (i = 0, iMax = nodes.length; i < iMax; i++) { + var c = [nodes[i]]; + for (var l = 0; l < level; l++) { + c = c.concat(getConnectedNodes(c)); + } + connections.push(c); + } + + var hubs = []; + for (i = 0, len = connections.length; i < len; i++) { + hubs.push(connections[i].length); + } + + return hubs; +}; + + +/** + * Set a new size for the network + * @param {string} width Width in pixels or percentage (for example "800px" + * or "50%") + * @param {string} height Height in pixels or percentage (for example "400px" + * or "30%") + */ +Graph.prototype._setSize = function(width, height) { + this.frame.style.width = width; + this.frame.style.height = height; + + this.frame.canvas.style.width = "100%"; + this.frame.canvas.style.height = "100%"; + + this.frame.canvas.width = this.frame.canvas.clientWidth; + this.frame.canvas.height = this.frame.canvas.clientHeight; + + if (this.slider) { + this.slider.redraw(); + } +}; + +/** + * Convert a Google DataTable to a Javascript Array + * @param {google.visualization.DataTable} table + * @return {Array} array + */ +Graph.tableToArray = function(table) { + var array = []; + var col; + + // read the column names + var colCount = table.getNumberOfColumns(); + var cols = {}; + for (col = 0; col < colCount; col++) { + var label = table.getColumnLabel(col); + cols[label] = col; + } + + var rowCount = table.getNumberOfRows(); + for (var i = 0; i < rowCount; i++) { + // copy all properties from the table columns to an object + var properties = {}; + for (col in cols) { + if (cols.hasOwnProperty(col)) { + properties[col] = table.getValue(i, cols[col]); + } + } + + array.push(properties); + } + + return array; +}; + +/** + * Append nodes + * Nodes with a duplicate id will be replaced + * @param {google.visualization.DataTable | Array} nodesTable The data containing the nodes. + */ +Graph.prototype.addNodes = function(nodesTable) { + var table; + if (typeof google !== 'undefined' && google.visualization && google.visualization.DataTable && + nodesTable instanceof google.visualization.DataTable) { + // Google DataTable. + // Convert to a Javascript Array + table = Graph.tableToArray(nodesTable); + } + else if (Graph.isArray(nodesTable)){ + // Javascript Array + table = nodesTable; + } + else { + return; + } + + var hasValues = false; + var rowCount = table.length; + for (var i = 0; i < rowCount; i++) { + var properties = table[i]; + + if (properties.value != undefined) { + hasValues = true; + } + if (properties.id == undefined) { + throw "Column 'id' missing in table with nodes (row " + i + ")"; + } + + this._createNode(properties); + } + + // calculate scaling function when value is provided + if (hasValues) { + this._updateValueRange(this.nodes); + } + + this.start(); +}; + +/** + * Load all nodes by reading the data table nodesTable + * Note that Object DataTable is defined in google.visualization.DataTable + * @param {google.visualization.DataTable | Array} nodesTable The data containing the nodes. + */ +Graph.prototype.setNodes = function(nodesTable) { + var table; + if (typeof google !== 'undefined' && google.visualization && google.visualization.DataTable && + nodesTable instanceof google.visualization.DataTable) { + // Google DataTable. + // Convert to a Javascript Array + table = Graph.tableToArray(nodesTable); + } + else if (Graph.isArray(nodesTable)){ + // Javascript Array + table = nodesTable; + } + else { + return; + } + + this.hasMovingNodes = false; + this.nodesTable = table; + this.nodes = []; + this.selection = []; + + var hasValues = false; + var rowCount = table.length; + for (var i = 0; i < rowCount; i++) { + var properties = table[i]; + + if (properties.value != undefined) { + hasValues = true; + } + if (properties.timestamp) { + this.hasTimestamps = this.hasTimestamps || properties.timestamp; + } + if (properties.id == undefined) { + throw "Column 'id' missing in table with nodes (row " + i + ")"; + } + this._createNode(properties); + } + + // calculate scaling function when value is provided + if (hasValues) { + this._updateValueRange(this.nodes); + } +}; + +/** + * Filter the current nodes table for nodes with a timestamp older than given + * timestamp. Can only be used for nodes added via setNodes(), not via + * addNodes(). + * @param {*} [timestamp] If timestamp is undefined, all nodes are shown + */ +Graph.prototype._filterNodes = function(timestamp) { + if (this.nodesTable == undefined) { + return; + } + + // remove existing nodes with a too new timestamp + if (timestamp !== undefined) { + var ns = this.nodes; + var n = 0; + while (n < ns.length) { + var t = ns[n].timestamp; + if (t !== undefined && t > timestamp) { + // remove this node + ns.splice(n, 1); + } + else { + n++; + } + } + } + + // add all nodes with an old enough timestamp + var table = this.nodesTable; + var rowCount = table.length; + for (var i = 0; i < rowCount; i++) { + // copy all properties + var properties = table[i]; + + if (properties.id === undefined) { + throw "Column 'id' missing in table with nodes (row " + i + ")"; + } + + // check what the timestamp is + var ts = properties.timestamp ? properties.timestamp : undefined; + + var visible = true; + if (ts !== undefined && timestamp !== undefined && ts > timestamp) { + visible = false; + } + + if (visible) { + // create or update the node + this._createNode(properties); + } + } + + this.start(); +}; + +/** + * Create a node with the given properties + * If the new node has an id identical to an existing package, the existing + * node will be overwritten. + * The properties can contain a property "action", which can have values + * "create", "update", or "delete" + * @param {Object} properties An object with properties + */ +Graph.prototype._createNode = function(properties) { + var action = properties.action ? properties.action : "update"; + var id, index, newNode, oldNode; + + if (action === "create") { + // create the node + newNode = new Graph.Node(properties, this.images, this.groups, this.constants); + id = properties.id; + index = (id !== undefined) ? this._findNode(id) : undefined; + + if (index !== undefined) { + // replace node + oldNode = this.nodes[index]; + this.nodes[index] = newNode; + + // remove selection of old node + if (oldNode.selected) { + this._unselectNodes([{'row': index}], false); + } + + /* TODO: implement this? -> will give performance issues, searching all edges and node... + // update edges linking to this node + var edgesTable = this.edges; + for (var i = 0, iMax = edgesTable.length; i < iMax; i++) { + var edge = edgesTable[i]; + if (edge.from == oldNode) { + edge.from = newNode; + } + if (edge.to == oldNode) { + edge.to = newNode; + } + } + + // update packages linking to this node + var packagesTable = this.packages; + for (var i = 0, iMax = packagesTable.length; i < iMax; i++) { + var package = packagesTable[i]; + if (package.from == oldNode) { + package.from = newNode; + } + if (package.to == oldNode) { + package.to = newNode; + } + } + */ + } + else { + // add new node + this.nodes.push(newNode); + } + + if (!newNode.isFixed()) { + // note: no not use node.isMoving() here, as that gives the current + // velocity of the node, which is zero after creation of the node. + this.hasMovingNodes = true; + } + } + else if (action === "update") { + // update existing node, or create it when not yet existing + id = properties.id; + if (id === undefined) { + throw "Cannot update a node without id"; + } + + index = this._findNode(id); + if (index !== undefined) { + // update node + this.nodes[index].setProperties(properties, this.constants); + } + else { + // create node + newNode = new Graph.Node(properties, this.images, this.groups, this.constants); + this.nodes.push(newNode); + + if (!newNode.isFixed()) { + // note: no not use node.isMoving() here, as that gives the current + // velocity of the node, which is zero after creation of the node. + this.hasMovingNodes = true; + } + } + } + else if (action === "delete") { + // delete existing node + id = properties.id; + if (id === undefined) { + throw "Cannot delete node without its id"; + } + + index = this._findNode(id); + if (index !== undefined) { + oldNode = this.nodes[index]; + // remove selection of old node + if (oldNode.selected) { + this._unselectNodes([{'row': index}], false); + } + this.nodes.splice(index, 1); + } + else { + throw "Node with id " + id + " not found"; + } + } + else { + throw "Unknown action " + action + ". Choose 'create', 'update', or 'delete'."; + } +}; + +/** + * Find a node by its id + * @param {Number} id Id of the node + * @return {Number} index Index of the node in the array this.nodes, or + * undefined when not found. * + */ +Graph.prototype._findNode = function (id) { + var nodes = this.nodes; + for (var n = 0, len = nodes.length; n < len; n++) { + if (nodes[n].id === id) { + return n; + } + } + + return undefined; +}; + +/** + * Find a node by its rowNumber + * @param {Number} row Row number of the node + * @return {Graph.Node} node     The node with the given row number, or + *                            undefined when not found. + */ +Graph.prototype._findNodeByRow = function (row) { + return this.nodes[row]; +}; + +/** + * Load edges by reading the data table + * Note that Object DataTable is defined in google.visualization.DataTable + * @param {google.visualization.DataTable | Array} edgesTable The data containing the edges. + */ +Graph.prototype.setEdges = function(edgesTable) { + var table; + if (typeof google !== 'undefined' && google.visualization && google.visualization.DataTable && + edgesTable instanceof google.visualization.DataTable) { + // Google DataTable. + // Convert to a Javascript Array + table = Graph.tableToArray(edgesTable); + } + else if (Graph.isArray(edgesTable)){ + // Javascript Array + table = edgesTable; + } + else { + return; + } + + this.edgesTable = table; + this.edges = []; + this.hasMovingEdges = false; + + var hasValues = false; + var rowCount = table.length; + for (var i = 0; i < rowCount; i++) { + var properties = table[i]; + + if (properties.from === undefined) { + throw "Column 'from' missing in table with edges (row " + i + ")"; + } + if (properties.to === undefined) { + throw "Column 'to' missing in table with edges (row " + i + ")"; + } + if (properties.timestamp != undefined) { + this.hasTimestamps = this.hasTimestamps || properties.timestamp; + } + if (properties.value != undefined) { + hasValues = true; + } + + this._createEdge(properties); + } + + // calculate scaling function when value is provided + if (hasValues) { + this._updateValueRange(this.edges); + } +}; + + +/** + * Load edges by reading the data table + * Note that Object DataTable is defined in google.visualization.DataTable + * @param {google.visualization.DataTable | Array} edgesTable The data containing the edges. + */ +Graph.prototype.addEdges = function(edgesTable) { + var table; + if (typeof google !== 'undefined' && google.visualization && google.visualization.DataTable && + edgesTable instanceof google.visualization.DataTable) { + // Google DataTable. + // Convert to a Javascript Array + table = Graph.tableToArray(edgesTable); + } + else if (Graph.isArray(edgesTable)){ + // Javascript Array + table = edgesTable; + } + else { + return; + } + + var hasValues = false; + var rowCount = table.length; + for (var i = 0; i < rowCount; i++) { + // copy all properties + var properties = table[i]; + + if (properties.from === undefined) { + throw "Column 'from' missing in table with edges (row " + i + ")"; + } + if (properties.to === undefined) { + throw "Column 'to' missing in table with edges (row " + i + ")"; + } + if (properties.value != undefined) { + hasValues = true; + } + + this._createEdge(properties); + } + + // calculate scaling function when value is provided + if (hasValues) { + this._updateValueRange(this.edges); + } + + this.start(); +}; + + +/** + * Filter the current edges table for edges with a timestamp below given + * timestamp. Can only be used for edges added via setEdges(), not via + * addEdges(). + * @param {*} [timestamp] If timestamp is undefined, all edges are shown + */ +Graph.prototype._filterEdges = function(timestamp) { + if (this.edgesTable == undefined) { + return; + } + + // remove existing packages with a too new timestamp + if (timestamp !== undefined) { + var ls = this.edges; + var l = 0; + while (l < ls.length) { + var t = ls[l].timestamp; + if (t !== undefined && t > timestamp) { + // remove this edge + ls.splice(l, 1); + } + else { + l++; + } + } + } + + // add all edges with an old enough timestamp + var table = this.edgesTable; + var rowCount = table.length; + for (var i = 0; i < rowCount; i++) { + var properties = table[i]; + + if (properties.from === undefined) { + throw "Column 'from' missing in table with edges (row " + i + ")"; + } + if (properties.to === undefined) { + throw "Column 'to' missing in table with edges (row " + i + ")"; + } + + // check what the timestamp is + var ts = properties.timestamp ? properties.timestamp : undefined; + + var visible = true; + if (ts !== undefined && timestamp !== undefined && ts > timestamp) { + visible = false; + } + + if (visible) { + // create or update the edge + this._createEdge(properties); + } + } + + this.start(); +}; + +/** + * Create a edge with the given properties + * If the new edge has an id identical to an existing edge, the existing + * edge will be overwritten or updated. + * The properties can contain a property "action", which can have values + * "create", "update", or "delete" + * @param {Object} properties An object with properties + */ +Graph.prototype._createEdge = function(properties) { + var action = properties.action ? properties.action : "create"; + var id, index, edge, oldEdge, newEdge; + + if (action === "create") { + // create the edge, or replace it if already existing + id = properties.id; + index = (id !== undefined) ? this._findEdge(id) : undefined; + edge = new Graph.Edge(properties, this, this.constants); + + if (index !== undefined) { + // replace existing edge + oldEdge = this.edges[index]; + oldEdge.from.detachEdge(oldEdge); + oldEdge.to.detachEdge(oldEdge); + this.edges[index] = edge; + } + else { + // add new edge + this.edges.push(edge); + } + edge.from.attachEdge(edge); + edge.to.attachEdge(edge); + + if (edge.isMoving()) { + this.hasMovingEdges = true; + } + } + else if (action === "update") { + // update existing edge, or create the edge if not existing + id = properties.id; + if (id === undefined) { + throw "Cannot update a edge without id"; + } + + index = this._findEdge(id); + if (index !== undefined) { + // update edge + edge = this.edges[index]; + edge.from.detachEdge(edge); + edge.to.detachEdge(edge); + + edge.setProperties(properties, this.constants); + edge.from.attachEdge(edge); + edge.to.attachEdge(edge); + } + else { + // add new edge + edge = new Graph.Edge(properties, this, this.constants); + edge.from.attachEdge(edge); + edge.to.attachEdge(edge); + this.edges.push(edge); + if (edge.isMoving()) { + this.hasMovingEdges = true; + } + } + } + else if (action === "delete") { + // delete existing edge + id = properties.id; + if (id === undefined) { + throw "Cannot delete edge without its id"; + } + + index = this._findEdge(id); + if (index !== undefined) { + oldEdge = this.edges[id]; + edge.from.detachEdge(oldEdge); + edge.to.detachEdge(oldEdge); + this.edges.splice(index, 1); + } + else { + throw "Edge with id " + id + " not found"; + } + } + else { + throw "Unknown action " + action + ". Choose 'create', 'update', or 'delete'."; + } +}; + +/** + * Update the edge to oldNode in all edges and packages. + * @param {Node} oldNode + * @param {Node} newNode + */ +// TODO: start utilizing this method _updateNodeReferences +Graph.prototype._updateNodeReferences = function(oldNode, newNode) { + var arrays = [this.edges, this.packages]; + for (var a = 0, aMax = arrays.length; a < aMax; a++) { + var array = arrays[a]; + for (var i = 0, iMax = array.length; i < iMax; i++) { + if (array.from === oldNode) { + array.from = newNode; + } + if (array.to === oldNode) { + array.to = newNode; + } + } + } +}; + +/** + * Find a edge by its id + * @param {Number} id Id of the edge + * @return {Number} index Index of the edge in the array this.edges, or + * undefined when not found. * + */ +Graph.prototype._findEdge = function (id) { + var edges = this.edges; + for (var n = 0, len = edges.length; n < len; n++) { + if (edges[n].id === id) { + return n; + } + } + + return undefined; +}; + +/** + * Find a edge by its row + * @param {Number} row Row of the edge + * @return {Graph.Edge} the found edge, or undefined when not found + */ +Graph.prototype._findEdgeByRow = function (row) { + return this.edges[row]; +}; + +/** + * Append packages + * Packages with a duplicate id will be replaced + * Note that Object DataTable is defined in google.visualization.DataTable + * @param {google.visualization.DataTable | Array} packagesTable The data containing the packages. + */ +Graph.prototype.addPackages = function(packagesTable) { + var table; + if (typeof google !== 'undefined' && google.visualization && google.visualization.DataTable && + packagesTable instanceof google.visualization.DataTable) { + // Google DataTable. + // Convert to a Javascript Array + table = Graph.tableToArray(packagesTable); + } + else if (Graph.isArray(packagesTable)){ + // Javascript Array + table = packagesTable; + } + else { + return; + } + + var rowCount = table.length; + for (var i = 0; i < rowCount; i++) { + var properties = table[i]; + + if (properties.from === undefined) { + throw "Column 'from' missing in table with packages (row " + i + ")"; + } + if (properties.to === undefined) { + throw "Column 'to' missing in table with packages (row " + i + ")"; + } + + this._createPackage(properties); + } + + // calculate scaling function when value is provided + this._updateValueRange(this.packages); + + this.start(); +}; + +/** + * Set a new packages table + * Packages with a duplicate id will be replaced + * Note that Object DataTable is defined in google.visualization.DataTable + * @param {google.visualization.DataTable | Array} packagesTable The data containing the packages. + */ +Graph.prototype.setPackages = function(packagesTable) { + var table; + if (typeof google !== 'undefined' && google.visualization && google.visualization.DataTable && + packagesTable instanceof google.visualization.DataTable) { + // Google DataTable. + // Convert to a Javascript Array + table = Graph.tableToArray(packagesTable); + } + else if (Graph.isArray(packagesTable)){ + // Javascript Array + table = packagesTable; + } + else { + return; + } + + this.packagesTable = table; + this.packages = []; + + var rowCount = table.length; + for (var i = 0; i < rowCount; i++) { + var properties = table[i]; + + if (properties.from === undefined) { + throw "Column 'from' missing in table with packages (row " + i + ")"; + } + if (properties.to === undefined) { + throw "Column 'to' missing in table with packages (row " + i + ")"; + } + if (properties.timestamp) { + this.hasTimestamps = this.hasTimestamps || properties.timestamp; + } + + this._createPackage(properties); + } + + // calculate scaling function when value is provided + this._updateValueRange(this.packages); + + /* TODO: adjust examples and documentation for this? + this.start(); + */ +}; + +/** + * Filter the current package table for packages with a timestamp below given + * timestamp. Can only be used for packages added via setPackages(), not via + * addPackages(). + * @param {*} [timestamp] If timestamp is undefined, all packages are shown + */ +Graph.prototype._filterPackages = function(timestamp) { + if (this.packagesTable == undefined) { + return; + } + + // remove all current packages + this.packages = []; + + /* TODO: cleanup + // remove existing packages with a too new timestamp + if (timestamp !== undefined) { + var packages = this.packages; + var p = 0; + while (p < packages.length) { + var package = packages[p]; + var t = package.timestamp; + + if (t !== undefined && t > timestamp ) { + // remove this package + packages.splice(p, 1); + } + else { + p++; + } + } + } + */ + + // add all packages with an old enough timestamp + var table = this.packagesTable; + var rowCount = table.length; + for (var i = 0; i < rowCount; i++) { + var properties = table[i]; + + if (properties.from === undefined) { + throw "Column 'from' missing in table with packages (row " + i + ")"; + } + if (properties.to === undefined) { + throw "Column 'to' missing in table with packages (row " + i + ")"; + } + // check what the timestamp is + var pTimestamp = properties.timestamp ? properties.timestamp : undefined; + + var visible = true; + if (pTimestamp !== undefined && timestamp !== undefined && pTimestamp > timestamp) { + visible = false; + } + + if (visible === true) { + if (properties.progress == undefined) { + // when no progress is provided, we need to add our own progress + var duration = properties.duration || this.constants.packages.duration; // seconds + + var diff = (timestamp.getTime() - pTimestamp.getTime()) / 1000; // seconds + if (diff < duration) { + // copy the properties, and fill in the current progress based on the + // timestamp and the duration + var original = properties; + properties = {}; + for (var j in original) { + if (original.hasOwnProperty(j)) { + properties[j] = original[j]; + } + } + + properties.progress = diff / duration; // scale 0-1 + } + else { + visible = false; + } + } + } + + if (visible === true) { + // create or update the package + this._createPackage(properties); + } + } + + this.start(); +}; + +/** + * Create a package with the given properties + * If the new package has an id identical to an existing package, the existing + * package will be overwritten. + * The properties can contain a property "action", which can have values + * "create", "update", or "delete" + * @param {Object} properties An object with properties + */ +Graph.prototype._createPackage = function(properties) { + var action = properties.action ? properties.action : "create"; + var id, index, newPackage; + + if (action === "create") { + // create the package + id = properties.id; + index = (id !== undefined) ? this._findPackage(id) : undefined; + newPackage = new Graph.Package(properties, this, this.images, this.constants); + + if (index !== undefined) { + // replace existing package + this.packages[index] = newPackage; + } + else { + // add new package + this.packages.push(newPackage); + } + + if (newPackage.isMoving()) { + this.hasMovingPackages = true; + } + } + else if (action === "update") { + // update a package, or create it when not existing + id = properties.id; + if (id === undefined) { + throw "Cannot update a edge without id"; + } + + index = this._findPackage(id); + if (index !== undefined) { + // update existing package + this.packages[index].setProperties(properties, this.constants); + } + else { + // add new package + newPackage = new Graph.Package(properties, this, this.images, this.constants); + this.packages.push(newPackage); + if (newPackage.isMoving()) { + this.hasMovingPackages = true; + } + } + } + else if (action === "delete") { + // delete existing package + id = properties.id; + if (id === undefined) { + throw "Cannot delete package without its id"; + } + + index = this._findPackage(id); + if (index !== undefined) { + this.packages.splice(index, 1); + } + else { + throw "Package with id " + id + " not found"; + } + } + else { + throw "Unknown action " + action + ". Choose 'create', 'update', or 'delete'."; + } +}; + +/** + * Find a package by its id. + * @param {Number} id + * @return {Number} index Index of the package in the array this.packages, + * or undefined when not found + */ +Graph.prototype._findPackage = function (id) { + var packages = this.packages; + for (var n = 0, len = packages.length; n < len; n++) { + if (packages[n].id === id) { + return n; + } + } + + return undefined; +}; + +/** + * Find a package by its row + * @param {Number} row Row of the package + * @return {Graph.Package} the found package, or undefined when not found + */ +Graph.prototype._findPackageByRow = function (row) { + return this.packages[row]; +}; + +/** + * Retrieve an object which maps the column ids by their names + * For example a table with columns [id, name, value] will return an + * object {"id": 0, "name": 1, "value": 2} + * @param {google.visualization.DataTable} table A google datatable + * @return {Object} columnIds An object + */ +// TODO: cleanup this unused method +Graph.prototype._getColumnNames = function (table) { + var colCount = table.getNumberOfColumns(); + var cols = {}; + for (var col = 0; col < colCount; col++) { + var label = table.getColumnLabel(col); + cols[label] = col; + } + return cols; +}; + + +/** + * Update the values of all object in the given array according to the current + * value range of the objects in the array. + * @param {Array} array. An array with objects like Edges, Nodes, or Packages + * The objects must have a method getValue() and + * setValueRange(min, max). + */ +Graph.prototype._updateValueRange = function(array) { + var count = array.length; + var i; + + // determine the range of the node values + var valueMin = undefined; + var valueMax = undefined; + for (i = 0; i < count; i++) { + var value = array[i].getValue(); + if (value !== undefined) { + valueMin = (valueMin === undefined) ? value : Math.min(value, valueMin); + valueMax = (valueMax === undefined) ? value : Math.max(value, valueMax); + } + } + + // adjust the range of all nodes + if (valueMin !== undefined && valueMax !== undefined) { + for (i = 0; i < count; i++) { + array[i].setValueRange(valueMin, valueMax); + } + } +}; + + +/** + * Set the current timestamp. All packages with a timestamp smaller or equal + * than the given timestamp will be drawn. + * @param {Date | Number} timestamp + */ +Graph.prototype.setTimestamp = function(timestamp) { + this._filterNodes(timestamp); + this._filterEdges(timestamp); + this._filterPackages(timestamp); +}; + + +/** + * Get the range of all timestamps defined in the nodes, edges and packages + * @return {Object} A range object, containing parameters start and end. + */ +Graph.prototype._getRange = function() { + // range is stored as number. at the end of the method, it is converted to + // Date when needed. + var range = { + "start": undefined, + "end": undefined + }; + + var tables = [this.nodesTable, this.edgesTable]; + for (var t = 0, tMax = tables.length; t < tMax; t++) { + var table = tables[t]; + + if (table !== undefined) { + for (var i = 0, iMax = table.length; i < iMax; i++) { + var timestamp = table[i].timestamp; + if (timestamp) { + // to long + if (timestamp instanceof Date) { + timestamp = timestamp.getTime(); + } + + // calculate new range + range.start = range.start ? Math.min(timestamp, range.start) : timestamp; + range.end = range.end ? Math.max(timestamp, range.end) : timestamp; + } + } + } + } + + // calculate the range for the packagesTable by hand. In case of packages + // without a progress provided, we need to calculate the end time by hand. + if (this.packagesTable) { + var packagesTable = this.packagesTable; + for (var row = 0, len = packagesTable.length; row < len; row ++) { + var pkg = packagesTable[row], + timestamp = pkg.timestamp, + progress = pkg.progress, + duration = pkg.duration || this.constants.packages.duration; + + // convert to number + if (timestamp instanceof Date) { + timestamp = timestamp.getTime(); + } + + if (timestamp != undefined) { + var start = timestamp, + end = progress ? timestamp : (timestamp + duration * 1000); + + range.start = range.start ? Math.min(start, range.start) : start; + range.end = range.end ? Math.max(end, range.end) : end; + } + } + } + + // convert to the right type: number or date + var rangeFormat = { + "start": new Date(range.start), + "end": new Date(range.end) + }; + + return rangeFormat; +}; + +/** + * Start animation. + * Only applicable when packages with a timestamp are available + */ +Graph.prototype.animationStart = function() { + if (this.slider) { + this.slider.play(); + } +}; + +/** + * Start animation. + * Only applicable when packages with a timestamp are available + */ +Graph.prototype.animationStop = function() { + if (this.slider) { + this.slider.stop(); + } +}; + +/** + * Set framerate for the animation. + * Only applicable when packages with a timestamp are available + * @param {number} framerate The framerate in frames per second + */ +Graph.prototype.setAnimationFramerate = function(framerate) { + if (this.slider) { + this.slider.setFramerate(framerate); + } +} + +/** + * Set the duration of playing the whole package history + * Only applicable when packages with a timestamp are available + * @param {number} duration The duration in seconds + */ +Graph.prototype.setAnimationDuration = function(duration) { + if (this.slider) { + this.slider.setDuration(duration); + } +}; + +/** + * Set the time acceleration for playing the history. + * Only applicable when packages with a timestamp are available + * @param {number} acceleration Acceleration, for example 10 means play + * ten times as fast as real time. A value + * of 1 will play the history in real time. + */ +Graph.prototype.setAnimationAcceleration = function(acceleration) { + if (this.slider) { + this.slider.setAcceleration(acceleration); + } +}; + +/** + * Redraw the graph with the current data + * chart will be resized too. + */ +Graph.prototype.redraw = function() { + this._setSize(this.width, this.height); + + this._redraw(); +}; + +/** + * Redraw the graph with the current data + */ +Graph.prototype._redraw = function() { + var ctx = this.frame.canvas.getContext("2d"); + + // clear the canvas + var w = this.frame.canvas.width; + var h = this.frame.canvas.height; + ctx.clearRect(0, 0, w, h); + + // set scaling and translation + ctx.save(); + ctx.translate(this.translation.x, this.translation.y); + ctx.scale(this.scale, this.scale); + + this._drawEdges(ctx); + this._drawNodes(ctx); + this._drawPackages(ctx); + this._drawSlider(); + + // restore original scaling and translation + ctx.restore(); +}; + +/** + * Set the translation of the graph + * @param {Number} offsetX Horizontal offset + * @param {Number} offsetY Vertical offset + */ +Graph.prototype._setTranslation = function(offsetX, offsetY) { + if (this.translation === undefined) { + this.translation = { + "x": 0, + "y": 0 + }; + } + + if (offsetX !== undefined) { + this.translation.x = offsetX; + } + if (offsetY !== undefined) { + this.translation.y = offsetY; + } +}; + +/** + * Get the translation of the graph + * @return {Object} translation An object with parameters x and y, both a number + */ +Graph.prototype._getTranslation = function() { + return { + "x": this.translation.x, + "y": this.translation.y + }; +}; + +/** + * Scale the graph + * @param {Number} scale Scaling factor 1.0 is unscaled + */ +Graph.prototype._setScale = function(scale) { + this.scale = scale; +}; +/** + * Get the current scale of the graph + * @return {Number} scale Scaling factor 1.0 is unscaled + */ +Graph.prototype._getScale = function() { + return this.scale; +}; + +Graph.prototype._xToCanvas = function(x) { + return (x - this.translation.x) / this.scale; +}; + +Graph.prototype._canvasToX = function(x) { + return x * this.scale + this.translation.x; +}; + +Graph.prototype._yToCanvas = function(y) { + return (y - this.translation.y) / this.scale; +}; + +Graph.prototype._canvasToY = function(y) { + return y * this.scale + this.translation.y ; +}; + + + +/** + * Get a node by its id + * @param {number} id + * @return {Node} node, or null if not found + */ +Graph.prototype._getNode = function(id) { + for (var i = 0; i < this.nodes.length; i++) { + if (this.nodes[i].id == id) + return this.nodes[i]; + } + + return null; +}; + +/** + * Redraw all nodes + * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d"); + * @param {CanvasRenderingContext2D} ctx + */ +Graph.prototype._drawNodes = function(ctx) { + // first draw the unselected nodes + var nodes = this.nodes; + var selected = []; + for (var i = 0, iMax = nodes.length; i < iMax; i++) { + if (nodes[i].isSelected()) { + selected.push(i); + } + else { + nodes[i].draw(ctx); + } + } + + // draw the selected nodes on top + for (var s = 0, sMax = selected.length; s < sMax; s++) { + nodes[selected[s]].draw(ctx); + } +}; + +/** + * Redraw all edges + * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d"); + * @param {CanvasRenderingContext2D} ctx + */ +Graph.prototype._drawEdges = function(ctx) { + var edges = this.edges; + for (var i = 0, iMax = edges.length; i < iMax; i++) { + edges[i].draw(ctx); + } +}; + +/** + * Redraw all packages + * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d"); + * @param {CanvasRenderingContext2D} ctx + */ +Graph.prototype._drawPackages = function(ctx) { + var packages = this.packages; + for (var i = 0, iMax = packages.length; i < iMax; i++) { + packages[i].draw(ctx); + } +}; + + +/** + * Redraw the filter + */ +Graph.prototype._drawSlider = function() { + var sliderNode; + if (this.hasTimestamps) { + sliderNode = this.frame.slider; + if (sliderNode === undefined) { + sliderNode = document.createElement( "div" ); + sliderNode.style.position = "absolute"; + sliderNode.style.bottom = "0px"; + sliderNode.style.left = "0px"; + sliderNode.style.right = "0px"; + sliderNode.style.backgroundColor = "rgba(255, 255, 255, 0.7)"; + + this.frame.slider = sliderNode; + this.frame.slider.style.padding = "10px"; + //this.frame.filter.style.backgroundColor = "#EFEFEF"; + this.frame.appendChild(sliderNode); + + + var range = this._getRange(); + this.slider = new Graph.Slider(sliderNode); + this.slider.setLoop(false); + this.slider.setRange(range.start, range.end); + + // create an event handler + var me = this; + var onchange = function () { + var timestamp = me.slider.getValue(); + me.setTimestamp(timestamp); + // TODO: do only a redraw when the graph is not still moving + me.redraw(); + }; + this.slider.setOnChangeCallback(onchange); + onchange(); // perform the first update by hand. + } + } + else { + sliderNode = this.frame.slider; + if (sliderNode !== undefined) { + this.frame.removeChild(sliderNode); + this.frame.slider = undefined; + this.slider = undefined; + } + } +}; + +/** + * Recalculate the best positions for all nodes + */ +Graph.prototype._reposition = function() { + // TODO: implement function reposition + + + /* + var w = this.frame.canvas.clientWidth; + var h = this.frame.canvas.clientHeight; + for (var i = 0; i < this.nodes.length; i++) { + if (!this.nodes[i].xFixed) this.nodes[i].x = w * Math.random(); + if (!this.nodes[i].yFixed) this.nodes[i].y = h * Math.random(); + } + //*/ + + //* + // TODO + var radius = this.constants.edges.length * 2; + var cx = this.frame.canvas.clientWidth / 2; + var cy = this.frame.canvas.clientHeight / 2; + for (var i = 0; i < this.nodes.length; i++) { + var angle = 2*Math.PI * (i / this.nodes.length); + + if (!this.nodes[i].xFixed) this.nodes[i].x = cx + radius * Math.cos(angle); + if (!this.nodes[i].yFixed) this.nodes[i].y = cy + radius * Math.sin(angle); + + } + //*/ + + /* + // TODO + var radius = this.constants.edges.length * 2; + var w = this.frame.canvas.clientWidth, + h = this.frame.canvas.clientHeight; + var cx = this.frame.canvas.clientWidth / 2; + var cy = this.frame.canvas.clientHeight / 2; + var s = Math.sqrt(this.nodes.length); + for (var i = 0; i < this.nodes.length; i++) { + //var angle = 2*Math.PI * (i / this.nodes.length); + + if (!this.nodes[i].xFixed) this.nodes[i].x = w/s * (i % s); + if (!this.nodes[i].yFixed) this.nodes[i].y = h/s * (i / s); + } + //*/ + + + /* + var cx = this.frame.canvas.clientWidth / 2; + var cy = this.frame.canvas.clientHeight / 2; + for (var i = 0; i < this.nodes.length; i++) { + this.nodes[i].x = cx; + this.nodes[i].y = cy; + } + + //*/ + +}; + + +/** + * Find a stable position for all nodes + */ +Graph.prototype._doStabilize = function() { + var start = new Date(); + + // find stable position + var count = 0; + var vmin = this.constants.minVelocity; + var stable = false; + while (!stable && count < this.constants.maxIterations) { + this._calculateForces(); + this._discreteStepNodes(); + stable = !this.isMoving(vmin); + count++; + } + + var end = new Date(); + + //console.log("Stabilized in " + (end-start) + " ms, " + count + " iterations" ); // TODO: cleanup +}; + +/** + * Calculate the external forces acting on the nodes + * Forces are caused by: edges, repulsing forces between nodes, gravity + */ +Graph.prototype._calculateForces = function(nodeId) { + // create a local edge to the nodes and edges, that is faster + var nodes = this.nodes, + edges = this.edges; + + // gravity, add a small constant force to pull the nodes towards the center of + // the graph + // Also, the forces are reset to zero in this loop by using _setForce instead + // of _addForce + var gravity = 0.01, + gx = this.frame.canvas.clientWidth / 2, + gy = this.frame.canvas.clientHeight / 2; + for (var n = 0; n < nodes.length; n++) { + var dx = gx - nodes[n].x, + dy = gy - nodes[n].y, + angle = Math.atan2(dy, dx), + fx = Math.cos(angle) * gravity, + fy = Math.sin(angle) * gravity; + + this.nodes[n]._setForce(fx, fy); + } + + // repulsing forces between nodes + var minimumDistance = this.constants.nodes.distance, + steepness = 10; // higher value gives steeper slope of the force around the given minimumDistance + for (var n = 0; n < nodes.length; n++) { + for (var n2 = n + 1; n2 < this.nodes.length; n2++) { + //var dmin = (nodes[n].width + nodes[n].height + nodes[n2].width + nodes[n2].height) / 1 || minimumDistance, // TODO: dmin + //var dmin = (nodes[n].width + nodes[n2].width)/2 || minimumDistance, // TODO: dmin + //dmin = 40 + ((nodes[n].width/2 + nodes[n2].width/2) || 0), + + // calculate normally distributed force + var dx = nodes[n2].x - nodes[n].x, + dy = nodes[n2].y - nodes[n].y, + distance = Math.sqrt(dx * dx + dy * dy), + angle = Math.atan2(dy, dx), + + // TODO: correct factor for repulsing force + //var repulsingforce = 2 * Math.exp(-5 * (distance * distance) / (dmin * dmin) ); // TODO: customize the repulsing force + //repulsingforce = Math.exp(-1 * (distance * distance) / (dmin * dmin) ), // TODO: customize the repulsing force + repulsingforce = 1 / (1 + Math.exp((distance / minimumDistance - 1) * steepness)), // TODO: customize the repulsing force + fx = Math.cos(angle) * repulsingforce, + fy = Math.sin(angle) * repulsingforce; + + this.nodes[n]._addForce(-fx, -fy); + this.nodes[n2]._addForce(fx, fy); + } + /* TODO: re-implement repulsion of edges + for (var l = 0; l < edges.length; l++) { + var lx = edges[l].from.x+(edges[l].to.x - edges[l].from.x)/2, + ly = edges[l].from.y+(edges[l].to.y - edges[l].from.y)/2, + + // calculate normally distributed force + dx = nodes[n].x - lx, + dy = nodes[n].y - ly, + distance = Math.sqrt(dx * dx + dy * dy), + angle = Math.atan2(dy, dx), + + + // TODO: correct factor for repulsing force + //var repulsingforce = 2 * Math.exp(-5 * (distance * distance) / (dmin * dmin) ); // TODO: customize the repulsing force + //repulsingforce = Math.exp(-1 * (distance * distance) / (dmin * dmin) ), // TODO: customize the repulsing force + repulsingforce = 1 / (1 + Math.exp((distance / (minimumDistance / 2) - 1) * steepness)), // TODO: customize the repulsing force + fx = Math.cos(angle) * repulsingforce, + fy = Math.sin(angle) * repulsingforce; + nodes[n]._addForce(fx, fy); + edges[l].from._addForce(-fx/2,-fy/2); + edges[l].to._addForce(-fx/2,-fy/2); + } + */ + } + + // forces caused by the edges, modelled as springs + for (var l = 0, lMax = edges.length; l < lMax; l++) { + var edge = edges[l], + + dx = (edge.to.x - edge.from.x), + dy = (edge.to.y - edge.from.y), + //edgeLength = (edge.from.width + edge.from.height + edge.to.width + edge.to.height)/2 || edge.length, // TODO: dmin + //edgeLength = (edge.from.width + edge.to.width)/2 || edge.length, // TODO: dmin + //edgeLength = 20 + ((edge.from.width + edge.to.width) || 0) / 2, + edgeLength = edge.length, + length = Math.sqrt(dx * dx + dy * dy), + angle = Math.atan2(dy, dx), + + springforce = edge.stiffness * (edgeLength - length), + + fx = Math.cos(angle) * springforce, + fy = Math.sin(angle) * springforce; + + edge.from._addForce(-fx, -fy); + edge.to._addForce(fx, fy); + } + + /* TODO: re-implement repulsion of edges + // repulsing forces between edges + var minimumDistance = this.constants.edges.distance, + steepness = 10; // higher value gives steeper slope of the force around the given minimumDistance + for (var l = 0; l < edges.length; l++) { + //Keep distance from other edge centers + for (var l2 = l + 1; l2 < this.edges.length; l2++) { + //var dmin = (nodes[n].width + nodes[n].height + nodes[n2].width + nodes[n2].height) / 1 || minimumDistance, // TODO: dmin + //var dmin = (nodes[n].width + nodes[n2].width)/2 || minimumDistance, // TODO: dmin + //dmin = 40 + ((nodes[n].width/2 + nodes[n2].width/2) || 0), + var lx = edges[l].from.x+(edges[l].to.x - edges[l].from.x)/2, + ly = edges[l].from.y+(edges[l].to.y - edges[l].from.y)/2, + l2x = edges[l2].from.x+(edges[l2].to.x - edges[l2].from.x)/2, + l2y = edges[l2].from.y+(edges[l2].to.y - edges[l2].from.y)/2, + + // calculate normally distributed force + dx = l2x - lx, + dy = l2y - ly, + distance = Math.sqrt(dx * dx + dy * dy), + angle = Math.atan2(dy, dx), + + + // TODO: correct factor for repulsing force + //var repulsingforce = 2 * Math.exp(-5 * (distance * distance) / (dmin * dmin) ); // TODO: customize the repulsing force + //repulsingforce = Math.exp(-1 * (distance * distance) / (dmin * dmin) ), // TODO: customize the repulsing force + repulsingforce = 1 / (1 + Math.exp((distance / minimumDistance - 1) * steepness)), // TODO: customize the repulsing force + fx = Math.cos(angle) * repulsingforce, + fy = Math.sin(angle) * repulsingforce; + + edges[l].from._addForce(-fx, -fy); + edges[l].to._addForce(-fx, -fy); + edges[l2].from._addForce(fx, fy); + edges[l2].to._addForce(fx, fy); + } + } + */ +}; + + +/** + * Check if any of the nodes is still moving + * @param {number} vmin the minimum velocity considered as "moving" + * @return {boolean} true if moving, false if non of the nodes is moving + */ +Graph.prototype.isMoving = function(vmin) { + // TODO: ismoving does not work well: should check the kinetic energy, not its velocity + var nodes = this.nodes; + for (var n = 0, nMax = nodes.length; n < nMax; n++) { + if (nodes[n].isMoving(vmin)) { + return true; + } + } + return false; +}; + + +/** + * Perform one discrete step for all nodes + */ +Graph.prototype._discreteStepNodes = function() { + var interval = this.refreshRate / 1000.0; // in seconds + var nodes = this.nodes; + for (var n = 0, nMax = nodes.length; n < nMax; n++) { + nodes[n].discreteStep(interval); + } +}; + + +/** + * Perform one discrete step for all packages + */ +Graph.prototype._discreteStepPackages = function() { + var interval = this.refreshRate / 1000.0; // in seconds + var packages = this.packages; + for (var n = 0, nMax = packages.length; n < nMax; n++) { + packages[n].discreteStep(interval); + } +}; + + +/** + * Cleanup finished packages. + * also checks if there are moving packages + */ +Graph.prototype._deleteFinishedPackages = function() { + var n = 0; + var hasMovingPackages = false; + while (n < this.packages.length) { + if (this.packages[n].isFinished()) { + this.packages.splice(n, 1); + n--; + } + else if (this.packages[n].isMoving()) { + hasMovingPackages = true; + } + n++; + } + + this.hasMovingPackages = hasMovingPackages; +}; + +/** + * Start animating nodes, edges, and packages. + */ +Graph.prototype.start = function() { + if (this.hasMovingNodes) { + this._calculateForces(); + this._discreteStepNodes(); + + var vmin = this.constants.minVelocity; + this.hasMovingNodes = this.isMoving(vmin); + } + + if (this.hasMovingPackages) { + this._discreteStepPackages(); + this._deleteFinishedPackages(); + } + + if (this.hasMovingNodes || this.hasMovingEdges || this.hasMovingPackages) { + // start animation. only start timer if it is not already running + if (!this.timer) { + var graph = this; + this.timer = window.setTimeout(function () { + graph.timer = undefined; + graph.start(); + graph._redraw(); + }, this.refreshRate); + } + } + else { + this._redraw(); + } +}; + +/** + * Stop animating nodes, edges, and packages. + */ +Graph.prototype.stop = function () { + if (this.timer) { + window.clearInterval(this.timer); + this.timer = undefined; + } +}; + + + +/**--------------------------------------------------------------------------**/ + + +/** + * Add and event listener. Works for all browsers + * @param {Element} element An html element + * @param {String} action The action, for example "click", + * without the prefix "on" + * @param {function} listener The callback function to be executed + * @param {boolean} useCapture + */ +Graph.addEventListener = function (element, action, listener, useCapture) { + if (element.addEventListener) { + if (useCapture === undefined) + useCapture = false; + + if (action === "mousewheel" && navigator.userAgent.indexOf("Firefox") >= 0) { + action = "DOMMouseScroll"; // For Firefox + } + + element.addEventListener(action, listener, useCapture); + } else { + element.attachEvent("on" + action, listener); // IE browsers + } +}; + +/** + * Remove an event listener from an element + * @param {Element} element An html dom element + * @param {string} action The name of the event, for example "mousedown" + * @param {function} listener The listener function + * @param {boolean} useCapture + */ +Graph.removeEventListener = function(element, action, listener, useCapture) { + if (element.removeEventListener) { + // non-IE browsers + if (useCapture === undefined) + useCapture = false; + + if (action === "mousewheel" && navigator.userAgent.indexOf("Firefox") >= 0) { + action = "DOMMouseScroll"; // For Firefox + } + + element.removeEventListener(action, listener, useCapture); + } else { + // IE browsers + element.detachEvent("on" + action, listener); + } +}; + + +/** + * Stop event propagation + */ +Graph.stopPropagation = function (event) { + if (!event) + event = window.event; + + if (event.stopPropagation) { + event.stopPropagation(); // non-IE browsers + } + else { + event.cancelBubble = true; // IE browsers + } +}; + + +/** + * Cancels the event if it is cancelable, without stopping further propagation of the event. + */ +Graph.preventDefault = function (event) { + if (!event) + event = window.event; + + if (event.preventDefault) { + event.preventDefault(); // non-IE browsers + } + else { + event.returnValue = false; // IE browsers + } +}; + +/** + * Retrieve the absolute left value of a DOM element + * @param {Element} elem A dom element, for example a div + * @return {number} left The absolute left position of this element + * in the browser page. + */ +Graph._getAbsoluteLeft = function(elem) { + var left = 0; + while( elem != null ) { + left += elem.offsetLeft; + left -= elem.scrollLeft; + elem = elem.offsetParent; + } + if (!document.body.scrollLeft && window.pageXOffset) { + // FF + left -= window.pageXOffset; + } + return left; +}; + +/** + * Retrieve the absolute top value of a DOM element + * @param {Element} elem A dom element, for example a div + * @return {number} top The absolute top position of this element + * in the browser page. + */ +Graph._getAbsoluteTop = function(elem) { + var top = 0; + while( elem != null ) { + top += elem.offsetTop; + top -= elem.scrollTop; + elem = elem.offsetParent; + } + if (!document.body.scrollTop && window.pageYOffset) { + // FF + top -= window.pageYOffset; + } + return top; +}; + + + +/**--------------------------------------------------------------------------**/ + + +/** + * @class Node + * A node. A node can be connected to other nodes via one or multiple edges. + * @param {object} properties An object containing properties for the node. All + * properties are optional, except for the id. + * {number} id Id of the node. Required + * {string} text Title for the node + * {number} x Horizontal position of the node + * {number} y Vertical position of the node + * {string} style Drawing style, available: + * "database", "circle", "rect", + * "image", "text", "dot", "star", + * "triangle", "triangleDown", + * "square" + * {string} image An image url + * {string} title An title text, can be HTML + * {anytype} group A group name or number + * @param {Graph.Images} imagelist A list with images. Only needed + * when the node has an image + * @param {Graph.Groups} grouplist A list with groups. Needed for + * retrieving group properties + * @param {Object} constants An object with default values for + * example for the color + */ +Graph.Node = function (properties, imagelist, grouplist, constants) { + this.selected = false; + + this.edges = []; // all edges connected to this node + this.group = constants.nodes.group; + + this.fontSize = constants.nodes.fontSize; + this.fontFace = constants.nodes.fontFace; + this.fontColor = constants.nodes.fontColor; + + this.borderColor = constants.nodes.borderColor; + this.backgroundColor = constants.nodes.backgroundColor; + this.highlightColor = constants.nodes.highlightColor; + + // set defaults for the properties + this.id = undefined; + this.style = constants.nodes.style; + this.image = constants.nodes.image; + this.x = 0; + this.y = 0; + this.xFixed = false; + this.yFixed = false; + this.radius = constants.nodes.radius; + this.radiusFixed = false; + this.radiusMin = constants.nodes.radiusMin; + this.radiusMax = constants.nodes.radiusMax; + + this.imagelist = imagelist; + this.grouplist = grouplist; + + this.setProperties(properties, constants); + + // mass, force, velocity + this.mass = 50; // kg (mass is adjusted for the number of connected edges) + this.fx = 0.0; // external force x + this.fy = 0.0; // external force y + this.vx = 0.0; // velocity x + this.vy = 0.0; // velocity y + this.minForce = constants.minForce; + this.damping = 0.9; // damping factor +}; + +/** + * Attach a edge to the node + * @param {Graph.Edge} edge + */ +Graph.Node.prototype.attachEdge = function(edge) { + this.edges.push(edge); + this._updateMass(); +}; + +/** + * Detach a edge from the node + * @param {Graph.Edge} edge + */ +Graph.Node.prototype.detachEdge = function(edge) { + var index = this.edges.indexOf(edge); + if (index != -1) { + this.edges.splice(index, 1); + } + this._updateMass(); +}; + +/** + * Update the nodes mass, which is determined by the number of edges connecting + * to it (more edges -> heavier node). + * @private + */ +Graph.Node.prototype._updateMass = function() { + this.mass = 50 + 20 * this.edges.length; // kg +}; + +/** + * Set or overwrite properties for the node + * @param {Object} properties an object with properties + * @param {Object} constants and object with default, global properties + */ +Graph.Node.prototype.setProperties = function(properties, constants) { + if (!properties) { + return; + } + + // basic properties + if (properties.id != undefined) {this.id = properties.id;} + if (properties.text != undefined) {this.text = properties.text;} + if (properties.title != undefined) {this.title = properties.title;} + if (properties.group != undefined) {this.group = properties.group;} + if (properties.x != undefined) {this.x = properties.x;} + if (properties.y != undefined) {this.y = properties.y;} + if (properties.value != undefined) {this.value = properties.value;} + if (properties.timestamp != undefined) {this.timestamp = properties.timestamp;} + + if (this.id === undefined) { + throw "Node must have an id"; + } + + // copy group properties + if (this.group) { + var groupObj = this.grouplist.get(this.group); + for (var prop in groupObj) { + if (groupObj.hasOwnProperty(prop)) { + this[prop] = groupObj[prop]; + } + } + } + + // individual style properties + if (properties.style != undefined) {this.style = properties.style;} + if (properties.image != undefined) {this.image = properties.image;} + if (properties.radius != undefined) {this.radius = properties.radius;} + if (properties.borderColor != undefined) {this.borderColor = properties.borderColor;} + if (properties.backgroundColor != undefined){this.backgroundColor = properties.backgroundColor;} + if (properties.highlightColor != undefined) {this.highlightColor = properties.highlightColor;} + if (properties.fontColor != undefined) {this.fontColor = properties.fontColor;} + if (properties.fontSize != undefined) {this.fontSize = properties.fontSize;} + if (properties.fontFace != undefined) {this.fontFace = properties.fontFace;} + + + if (this.image != undefined) { + if (this.imagelist) { + this.imageObj = this.imagelist.load(this.image); + } + else { + throw "No imagelist provided"; + } + } + + this.xFixed = this.xFixed || (properties.x != undefined); + this.yFixed = this.yFixed || (properties.y != undefined); + this.radiusFixed = this.radiusFixed || (properties.radius != undefined); + + if (this.style == 'image') { + this.radiusMin = constants.nodes.widthMin; + this.radiusMax = constants.nodes.widthMax; + } + + // choose draw method depending on the style + var style = this.style; + switch (style) { + case 'database': this.draw = this._drawDatabase; this.resize = this._resizeDatabase; break; + case 'rect': this.draw = this._drawRect; this.resize = this._resizeRect; break; + case 'circle': this.draw = this._drawCircle; this.resize = this._resizeCircle; break; + // TODO: add ellipse shape + // TODO: add diamond shape + case 'image': this.draw = this._drawImage; this.resize = this._resizeImage; break; + case 'text': this.draw = this._drawText; this.resize = this._resizeText; break; + case 'dot': this.draw = this._drawDot; this.resize = this._resizeShape; break; + case 'square': this.draw = this._drawSquare; this.resize = this._resizeShape; break; + case 'triangle': this.draw = this._drawTriangle; this.resize = this._resizeShape; break; + case 'triangleDown': this.draw = this._drawTriangleDown; this.resize = this._resizeShape; break; + case 'star': this.draw = this._drawStar; this.resize = this._resizeShape; break; + default: this.draw = this._drawRect; this.resize = this._resizeRect; break; + } + + // reset the size of the node, this can be changed + this._reset(); +}; + +/** + * select this node + */ +Graph.Node.prototype.select = function() { + this.selected = true; + this._reset(); +}; + +/** + * unselect this node + */ +Graph.Node.prototype.unselect = function() { + this.selected = false; + this._reset(); +}; + +/** + * Reset the calculated size of the node, forces it to recalculate its size + */ +Graph.Node.prototype._reset = function() { + this.width = undefined; + this.height = undefined; +}; + +/** + * get the title of this node. + * @return {string} title The title of the node, or undefined when no title + * has been set. + */ +Graph.Node.prototype.getTitle = function() { + return this.title; +}; + +/** + * Calculate the distance to the border of the Node + * @param {CanvasRenderingContext2D} ctx + * @param {Number} angle Angle in radians + * @returns {number} distance Distance to the border in pixels + */ +Graph.Node.prototype.distanceToBorder = function (ctx, angle) { + var borderWidth = 1; + + if (!this.width) { + this.resize(ctx); + } + + //noinspection FallthroughInSwitchStatementJS + switch (this.style) { + case 'circle': + case 'dot': + return this.radius + borderWidth; + + // TODO: implement distanceToBorder for database + // TODO: implement distanceToBorder for triangle + // TODO: implement distanceToBorder for triangleDown + + case 'rect': + case 'image': + case 'text': + default: + if (this.width) { + return Math.min( + Math.abs(this.width / 2 / Math.cos(angle)), + Math.abs(this.height / 2 / Math.sin(angle))) + borderWidth; + // TODO: reckon with border radius too in case of rect + } + else { + return 0; + } + + } + + // TODO: implement calculation of distance to border for all shapes +}; + +/** + * Set forces acting on the node + * @param {number} fx Force in horizontal direction + * @param {number} fy Force in vertical direction + */ +Graph.Node.prototype._setForce = function(fx, fy) { + this.fx = fx; + this.fy = fy; +}; + +/** + * Add forces acting on the node + * @param {number} fx Force in horizontal direction + * @param {number} fy Force in vertical direction + */ +Graph.Node.prototype._addForce = function(fx, fy) { + this.fx += fx; + this.fy += fy; +}; + +/** + * Perform one discrete step for the node + * @param {number} interval Time interval in seconds + */ +Graph.Node.prototype.discreteStep = function(interval) { + if (!this.xFixed) { + var dx = -this.damping * this.vx; // damping force + var ax = (this.fx + dx) / this.mass; // acceleration + this.vx += ax / interval; // velocity + this.x += this.vx / interval; // position + } + + if (!this.yFixed) { + var dy = -this.damping * this.vy; // damping force + var ay = (this.fy + dy) / this.mass; // acceleration + this.vy += ay / interval; // velocity + this.y += this.vy / interval; // position + } +}; + + +/** + * Check if this node has a fixed x and y position + * @return {boolean} true if fixed, false if not + */ +Graph.Node.prototype.isFixed = function() { + return (this.xFixed && this.yFixed); +}; + +/** + * Check if this node is moving + * @param {number} vmin the minimum velocity considered as "moving" + * @return {boolean} true if moving, false if it has no velocity + */ +// TODO: replace this method with calculating the kinetic energy +Graph.Node.prototype.isMoving = function(vmin) { + return (Math.abs(this.vx) > vmin || Math.abs(this.vy) > vmin || + (!this.xFixed && Math.abs(this.fx) > this.minForce) || + (!this.yFixed && Math.abs(this.fy) > this.minForce)); +}; + +/** + * check if this node is selecte + * @return {boolean} selected True if node is selected, else false + */ +Graph.Node.prototype.isSelected = function() { + return this.selected; +}; + +/** + * Retrieve the value of the node. Can be undefined + * @return {Number} value + */ +Graph.Node.prototype.getValue = function() { + return this.value; +}; + +/** + * Calculate the distance from the nodes location to the given location (x,y) + * @param {Number} x + * @param {Number} y + * @return {Number} value + */ +Graph.Node.prototype.getDistance = function(x, y) { + var dx = this.x - x, + dy = this.y - y; + return Math.sqrt(dx * dx + dy * dy); +}; + + +/** + * Adjust the value range of the node. The node will adjust it's radius + * based on its value. + * @param {Number} min + * @param {Number} max + */ +Graph.Node.prototype.setValueRange = function(min, max) { + if (!this.radiusFixed && this.value !== undefined) { + var scale = (this.radiusMax - this.radiusMin) / (max - min); + this.radius = (this.value - min) * scale + this.radiusMin; + } +}; + +/** + * Draw this node in the given canvas + * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d"); + * @param {CanvasRenderingContext2D} ctx + */ +Graph.Node.prototype.draw = function(ctx) { + throw "Draw method not initialized for node"; +}; + +/** + * Recalculate the size of this node in the given canvas + * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d"); + * @param {CanvasRenderingContext2D} ctx + */ +Graph.Node.prototype.resize = function(ctx) { + throw "Resize method not initialized for node"; +}; + +/** + * Check if this object is overlapping with the provided object + * @param {Object} obj an object with parameters left, top, right, bottom + * @return {boolean} True if location is located on node + */ +Graph.Node.prototype.isOverlappingWith = function(obj) { + return (this.left < obj.right && + this.left + this.width > obj.left && + this.top < obj.bottom && + this.top + this.height > obj.top); +}; + +Graph.Node.prototype._resizeImage = function (ctx) { + // TODO: pre calculate the image size + if (!this.width) { // undefined or 0 + var width, height; + if (this.value) { + var scale = this.imageObj.height / this.imageObj.width; + width = this.radius || this.imageObj.width; + height = this.radius * scale || this.imageObj.height; + } + else { + width = this.imageObj.width; + height = this.imageObj.height; + } + this.width = width; + this.height = height; + } +}; + +Graph.Node.prototype._drawImage = function (ctx) { + this._resizeImage(ctx); + + this.left = this.x - this.width / 2; + this.top = this.y - this.height / 2; + + var yText; + if (this.imageObj) { + ctx.drawImage(this.imageObj, this.left, this.top, this.width, this.height); + yText = this.y + this.height / 2; + } + else { + // image still loading... just draw the text for now + yText = this.y; + } + + this._text(ctx, this.text, this.x, yText, undefined, "top"); +}; + + +Graph.Node.prototype._resizeRect = function (ctx) { + if (!this.width) { + var margin = 5; + var textSize = this.getTextSize(ctx); + this.width = textSize.width + 2 * margin; + this.height = textSize.height + 2 * margin; + } +}; + +Graph.Node.prototype._drawRect = function (ctx) { + this._resizeRect(ctx); + + this.left = this.x - this.width / 2; + this.top = this.y - this.height / 2; + + ctx.strokeStyle = this.borderColor; + ctx.fillStyle = this.selected ? this.highlightColor : this.backgroundColor; + ctx.lineWidth = this.selected ? 2.0 : 1.0; + ctx.roundRect(this.left, this.top, this.width, this.height, this.radius); + ctx.fill(); + ctx.stroke(); + + this._text(ctx, this.text, this.x, this.y); +}; + + +Graph.Node.prototype._resizeDatabase = function (ctx) { + if (!this.width) { + var margin = 5; + var textSize = this.getTextSize(ctx); + var size = textSize.width + 2 * margin; + this.width = size; + this.height = size; + } +}; + +Graph.Node.prototype._drawDatabase = function (ctx) { + this._resizeDatabase(ctx); + this.left = this.x - this.width / 2; + this.top = this.y - this.height / 2; + + ctx.strokeStyle = this.borderColor; + ctx.fillStyle = this.selected ? this.highlightColor : this.backgroundColor; + ctx.lineWidth = this.selected ? 2.0 : 1.0; + ctx.database(this.x - this.width/2, this.y - this.height*0.5, this.width, this.height); + ctx.fill(); + ctx.stroke(); + + this._text(ctx, this.text, this.x, this.y); +}; + + +Graph.Node.prototype._resizeCircle = function (ctx) { + if (!this.width) { + var margin = 5; + var textSize = this.getTextSize(ctx); + var diameter = Math.max(textSize.width, textSize.height) + 2 * margin; + this.radius = diameter / 2; + + this.width = diameter; + this.height = diameter; + } +}; + +Graph.Node.prototype._drawCircle = function (ctx) { + this._resizeCircle(ctx); + this.left = this.x - this.width / 2; + this.top = this.y - this.height / 2; + + ctx.strokeStyle = this.borderColor; + ctx.fillStyle = this.selected ? this.highlightColor : this.backgroundColor; + ctx.lineWidth = this.selected ? 2.0 : 1.0; + ctx.circle(this.x, this.y, this.radius); + ctx.fill(); + ctx.stroke(); + + this._text(ctx, this.text, this.x, this.y); +}; + +Graph.Node.prototype._drawDot = function (ctx) { + this._drawShape(ctx, 'circle'); +}; + +Graph.Node.prototype._drawTriangle = function (ctx) { + this._drawShape(ctx, 'triangle'); +}; + +Graph.Node.prototype._drawTriangleDown = function (ctx) { + this._drawShape(ctx, 'triangleDown'); +}; + +Graph.Node.prototype._drawSquare = function (ctx) { + this._drawShape(ctx, 'square'); +}; + +Graph.Node.prototype._drawStar = function (ctx) { + this._drawShape(ctx, 'star'); +}; + +Graph.Node.prototype._resizeShape = function (ctx) { + if (!this.width) { + var size = 2 * this.radius; + this.width = size; + this.height = size; + } +}; + +Graph.Node.prototype._drawShape = function (ctx, shape) { + this._resizeShape(ctx); + + this.left = this.x - this.width / 2; + this.top = this.y - this.height / 2; + + ctx.strokeStyle = this.borderColor; + ctx.fillStyle = this.selected ? this.highlightColor : this.backgroundColor; + ctx.lineWidth = this.selected ? 2.0 : 1.0; + + ctx[shape](this.x, this.y, this.radius); + ctx.fill(); + ctx.stroke(); + + if (this.text) { + this._text(ctx, this.text, this.x, this.y + this.height / 2, undefined, 'top'); + } +}; + +Graph.Node.prototype._resizeText = function (ctx) { + if (!this.width) { + var margin = 5; + var textSize = this.getTextSize(ctx); + this.width = textSize.width + 2 * margin; + this.height = textSize.height + 2 * margin; + } +}; + +Graph.Node.prototype._drawText = function (ctx) { + this._resizeText(ctx); + this.left = this.x - this.width / 2; + this.top = this.y - this.height / 2; + + this._text(ctx, this.text, this.x, this.y); +}; + + +Graph.Node.prototype._text = function (ctx, text, x, y, align, baseline) { + if (text) { + ctx.font = (this.selected ? "bold " : "") + this.fontSize + "px " + this.fontFace; + ctx.fillStyle = this.fontColor || "black"; + ctx.textAlign = align || "center"; + ctx.textBaseline = baseline || "middle"; + + var lines = text.split('\n'), + lineCount = lines.length, + fontSize = (this.fontSize + 4), + yLine = y + (1 - lineCount) / 2 * fontSize; + + for (var i = 0; i < lineCount; i++) { + ctx.fillText(lines[i], x, yLine); + yLine += fontSize; + } + } +}; + + +Graph.Node.prototype.getTextSize = function(ctx) { + if (this.text != undefined) { + ctx.font = (this.selected ? "bold " : "") + this.fontSize + "px " + this.fontFace; + + var lines = this.text.split('\n'), + height = (this.fontSize + 4) * lines.length, + width = 0; + + for (var i = 0, iMax = lines.length; i < iMax; i++) { + width = Math.max(width, ctx.measureText(lines[i]).width); + } + + return {"width": width, "height": height}; + } + else { + return {"width": 0, "height": 0}; + } +}; + + + +/**--------------------------------------------------------------------------**/ + + +/** + * @class Edge + * + * A edge connects two nodes + * @param {Object} properties Object with properties. Must contain + * At least properties from and to. + * Available properties: from (number), + * to (number), color (string), + * width (number), style (string), + * length (number), title (string) + * @param {Graph} graph A graph object, used to find and edge to + * nodes. + * @param {Object} constants An object with default values for + * example for the color + */ +Graph.Edge = function (properties, graph, constants) { + if (!graph) { + throw "No graph provided"; + } + this.graph = graph; + + // initialize constants + this.widthMin = constants.edges.widthMin; + this.widthMax = constants.edges.widthMax; + + // initialize variables + this.id = undefined; + this.style = constants.edges.style; + this.title = undefined; + this.width = constants.edges.width; + this.value = undefined; + this.length = constants.edges.length; + + // Added to support dashed lines + // David Jordan + // 2012-08-08 + this.dashlength = constants.edges.dashlength; + this.dashgap = constants.edges.dashgap; + this.altdashlength = constants.edges.altdashlength; + + this.stiffness = undefined; // depends on the length of the edge + this.color = constants.edges.color; + this.timestamp = undefined; + this.widthFixed = false; + this.lengthFixed = false; + + this.setProperties(properties, constants); +}; + +/** + * Set or overwrite properties for the edge + * @param {Object} properties an object with properties + * @param {Object} constants and object with default, global properties + */ +Graph.Edge.prototype.setProperties = function(properties, constants) { + if (!properties) { + return; + } + + if (properties.from != undefined) {this.from = this.graph._getNode(properties.from);} + if (properties.to != undefined) {this.to = this.graph._getNode(properties.to);} + + if (properties.id != undefined) {this.id = properties.id;} + if (properties.style != undefined) {this.style = properties.style;} + if (properties.text != undefined) {this.text = properties.text;} + if (this.text) { + this.fontSize = constants.edges.fontSize; + this.fontFace = constants.edges.fontFace; + this.fontColor = constants.edges.fontColor; + if (properties.fontColor != undefined) {this.fontColor = properties.fontColor;} + if (properties.fontSize != undefined) {this.fontSize = properties.fontSize;} + if (properties.fontFace != undefined) {this.fontFace = properties.fontFace;} + } + if (properties.title != undefined) {this.title = properties.title;} + if (properties.width != undefined) {this.width = properties.width;} + if (properties.value != undefined) {this.value = properties.value;} + if (properties.length != undefined) {this.length = properties.length;} + + // Added to support dashed lines + // David Jordan + // 2012-08-08 + if (properties.dashlength != undefined) {this.dashlength = properties.dashlength;} + if (properties.dashgap != undefined) {this.dashgap = properties.dashgap;} + if (properties.altdashlength != undefined) {this.altdashlength = properties.altdashlength;} + + if (properties.color != undefined) {this.color = properties.color;} + if (properties.timestamp != undefined) {this.timestamp = properties.timestamp;} + + if (!this.from) { + throw "Node with id " + properties.from + " not found"; + } + if (!this.to) { + throw "Node with id " + properties.to + " not found"; + } + + this.widthFixed = this.widthFixed || (properties.width != undefined); + this.lengthFixed = this.lengthFixed || (properties.length != undefined); + + this.stiffness = 1 / this.length; + + // initialize animation + if (this.style === 'arrow') { + this.arrows = [0.5]; + this.animation = false; + } + else if (this.style === 'arrow-end') { + this.animation = false; + } + else if (this.style === 'moving-arrows') { + this.arrows = []; + var arrowCount = 3; // TODO: make customizable + for (var a = 0; a < arrowCount; a++) { + this.arrows.push(a / arrowCount); + } + this.animation = true; + } + else if (this.style === 'moving-dot') { + this.dot = 0.0; + this.animation = true; + } + else { + this.animation = false; + } + + // set draw method based on style + switch (this.style) { + case 'line': this.draw = this._drawLine; break; + case 'arrow': this.draw = this._drawArrow; break; + case 'arrow-end': this.draw = this._drawArrowEnd; break; + case 'moving-arrows': this.draw = this._drawMovingArrows; break; + case 'moving-dot': this.draw = this._drawMovingDot; break; + case 'dash-line': this.draw = this._drawDashLine; break; + default: this.draw = this._drawLine; break; + } +}; + + + +/** + * Check if a node has an animating contents. If so, the graph needs to be + * redrawn regularly + * @return {boolean} true if this edge needs animation, else false + */ +Graph.Edge.prototype.isMoving = function() { + // TODO: be able to set the interval somehow + + return this.animation; +}; + +/** + * get the title of this edge. + * @return {string} title The title of the edge, or undefined when no title + * has been set. + */ +Graph.Edge.prototype.getTitle = function() { + return this.title; +}; + + +/** + * Retrieve the value of the edge. Can be undefined + * @return {Number} value + */ +Graph.Edge.prototype.getValue = function() { + return this.value; +} + +/** + * Adjust the value range of the edge. The edge will adjust it's width + * based on its value. + * @param {Number} min + * @param {Number} max + */ +Graph.Edge.prototype.setValueRange = function(min, max) { + if (!this.widthFixed && this.value !== undefined) { + var factor = (this.widthMax - this.widthMin) / (max - min); + this.width = (this.value - min) * factor + this.widthMin; + } +}; + + +/** + * Check if the length is fixed. + * @return {boolean} lengthFixed True if the length is fixed, else false + */ +Graph.Edge.prototype.isLengthFixed = function() { + return this.lengthFixed; +}; + +/** + * Retrieve the length of the edge. Can be undefined + * @return {Number} length + */ +Graph.Edge.prototype.getLength = function() { + return this.length; +}; + +/** + * Adjust the length of the edge. This can only be done when the length + * is not fixed (which is the case when the edge is created with a length property) + * @param {Number} length + */ +Graph.Edge.prototype.setLength = function(length) { + if (!this.lengthFixed) { + this.length = length; + } +}; + +/** + * Retrieve the length of the edges dashes. Can be undefined + * @author David Jordan + * @date 2012-08-08 + * @return {Number} dashlength + */ +Graph.Edge.prototype.getDashLength = function() { + return this.dashlength; +}; + +/** + * Adjust the length of the edges dashes. + * @author David Jordan + * @date 2012-08-08 + * @param {Number} dashlength + */ +Graph.Edge.prototype.setDashLength = function(dashlength) { + this.dashlength = dashlength; +}; + +/** + * Retrieve the length of the edges dashes gaps. Can be undefined + * @author David Jordan + * @date 2012-08-08 + * @return {Number} dashgap + */ +Graph.Edge.prototype.getDashGap = function() { + return this.dashgap; +}; + +/** + * Adjust the length of the edges dashes gaps. + * @author David Jordan + * @date 2012-08-08 + * @param {Number} dashgap + */ +Graph.Edge.prototype.setDashGap = function(dashgap) { + this.dashgap = dashgap; +}; + +/** + * Retrieve the length of the edges alternate dashes. Can be undefined + * @author David Jordan + * @date 2012-08-08 + * @return {Number} altdashlength + */ +Graph.Edge.prototype.getAltDashLength = function() { + return this.altdashlength; +}; + +/** + * Adjust the length of the edges alternate dashes. + * @author David Jordan + * @date 2012-08-08 + * @param {Number} altdashlength + */ +Graph.Edge.prototype.setAltDashLength = function(altdashlength) { + this.altdashlength = altdashlength; +}; + + + +/** + * Redraw a edge + * Draw this edge in the given canvas + * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d"); + * @param {CanvasRenderingContext2D} ctx + */ +Graph.Edge.prototype.draw = function(ctx) { + throw "Method draw not initialized in edge"; +}; + + +/** + * Check if this object is overlapping with the provided object + * @param {Object} obj an object with parameters left, top + * @return {boolean} True if location is located on the edge + */ +Graph.Edge.prototype.isOverlappingWith = function(obj) { + var distMax = 10; + + var xFrom = this.from.x; + var yFrom = this.from.y; + var xTo = this.to.x; + var yTo = this.to.y; + var xObj = obj.left; + var yObj = obj.top; + + + var dist = Graph._dist(xFrom, yFrom, xTo, yTo, xObj, yObj); + + return (dist < distMax); +}; + +/** + * Calculate the distance between a point (x3,y3) and a line segment from + * (x1,y1) to (x2,y2). + * http://stackoverflow.com/questions/849211/shortest-distancae-between-a-point-and-a-line-segment + * @param {number} x1 + * @param {number} y1 + * @param {number} x2 + * @param {number} y2 + * @param {number} x3 + * @param {number} y3 + */ +Graph._dist = function (x1,y1, x2,y2, x3,y3) { // x3,y3 is the point + var px = x2-x1, + py = y2-y1, + something = px*px + py*py, + u = ((x3 - x1) * px + (y3 - y1) * py) / something; + + if (u > 1) { + u = 1; + } + else if (u < 0) { + u = 0; + } + + var x = x1 + u * px, + y = y1 + u * py, + dx = x - x3, + dy = y - y3; + + //# Note: If the actual distance does not matter, + //# if you only want to compare what this function + //# returns to other results of this function, you + //# can just return the squared distance instead + //# (i.e. remove the sqrt) to gain a little performance + + return Math.sqrt(dx*dx + dy*dy); +}; + +/** + * Redraw a edge as a line + * Draw this edge in the given canvas + * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d"); + * @param {CanvasRenderingContext2D} ctx + */ +Graph.Edge.prototype._drawLine = function(ctx) { + // set style + ctx.strokeStyle = this.color; + ctx.lineWidth = this._getLineWidth(); + + var point; + if (this.from != this.to) { + // draw line + this._line(ctx); + + // draw text + if (this.text) { + point = this._pointOnLine(0.5); + this._text(ctx, this.text, point.x, point.y); + } + } + else { + var radius = this.length / 2 / Math.PI; + var x, y; + var node = this.from; + if (!node.width) { + node.resize(ctx); + } + if (node.width > node.height) { + x = node.x + node.width / 2; + y = node.y - radius; + } + else { + x = node.x + radius; + y = node.y - node.height / 2; + } + this._circle(ctx, x, y, radius); + point = this._pointOnCircle(x, y, radius, 0.5); + this._text(ctx, this.text, point.x, point.y); + } +}; + +/** + * Get the line width of the edge. Depends on width and whether one of the + * connected nodes is selected. + * @return {Number} width + * @private + */ +Graph.Edge.prototype._getLineWidth = function() { + if (this.from.selected || this.to.selected) { + return Math.min(this.width * 2, this.widthMax); + } + else { + return this.width; + } +}; + +/** + * Draw a line between two nodes + * @param {CanvasRenderingContext2D} ctx + * @private + */ +Graph.Edge.prototype._line = function (ctx) { + // draw a straight line + ctx.beginPath(); + ctx.moveTo(this.from.x, this.from.y); + ctx.lineTo(this.to.x, this.to.y); + ctx.stroke(); +}; + +/** + * Draw a line from a node to itself, a circle + * @param {CanvasRenderingContext2D} ctx + * @param {Number} x + * @param {Number} y + * @param {Number} radius + * @private + */ +Graph.Edge.prototype._circle = function (ctx, x, y, radius) { + // draw a circle + ctx.beginPath(); + ctx.arc(x, y, radius, 0, 2 * Math.PI, false); + ctx.stroke(); +}; + +/** + * Draw text with white background and with the middle at (x, y) + * @param {CanvasRenderingContext2D} ctx + * @param {String} text + * @param {Number} x + * @param {Number} y + */ +Graph.Edge.prototype._text = function (ctx, text, x, y) { + if (text) { + // TODO: cache the calculated size + ctx.font = ((this.from.selected || this.to.selected) ? "bold " : "") + + this.fontSize + "px " + this.fontFace; + ctx.fillStyle = 'white'; + var width = ctx.measureText(this.text).width; + var height = this.fontSize; + var left = x - width / 2; + var top = y - height / 2; + + ctx.fillRect(left, top, width, height); + + // draw text + ctx.fillStyle = this.fontColor || "black"; + ctx.textAlign = "left"; + ctx.textBaseline = "top"; + ctx.fillText(this.text, left, top); + } +}; + +/** + * Sets up the dashedLine functionality for drawing + * Original code came from http://stackoverflow.com/questions/4576724/dotted-stroke-in-canvas + * @author David Jordan + * @date 2012-08-08 + */ +var CP = (typeof window !== 'undefined') && + window.CanvasRenderingContext2D && + CanvasRenderingContext2D.prototype; +if (CP && CP.lineTo){ + CP.dashedLine = function(x,y,x2,y2,dashArray){ + if (!dashArray) dashArray=[10,5]; + if (dashLength==0) dashLength = 0.001; // Hack for Safari + var dashCount = dashArray.length; + this.moveTo(x, y); + var dx = (x2-x), dy = (y2-y); + var slope = dy/dx; + var distRemaining = Math.sqrt( dx*dx + dy*dy ); + var dashIndex=0, draw=true; + while (distRemaining>=0.1){ + var dashLength = dashArray[dashIndex++%dashCount]; + if (dashLength > distRemaining) dashLength = distRemaining; + var xStep = Math.sqrt( dashLength*dashLength / (1 + slope*slope) ); + if (dx<0) xStep = -xStep; + x += xStep + y += slope*xStep; + this[draw ? 'lineTo' : 'moveTo'](x,y); + distRemaining -= dashLength; + draw = !draw; + } + } +} + +/** + * Redraw a edge as a dashed line + * Draw this edge in the given canvas + * @author David Jordan + * @date 2012-08-08 + * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d"); + * @param {CanvasRenderingContext2D} ctx + */ +Graph.Edge.prototype._drawDashLine = function(ctx) { + // set style + ctx.strokeStyle = this.color; + ctx.lineWidth = this._getLineWidth(); + + // draw dashed line + ctx.beginPath(); + ctx.lineCap = 'round'; + if (this.altdashlength != undefined) //If an alt dash value has been set add to the array this value + { + ctx.dashedLine(this.from.x,this.from.y,this.to.x,this.to.y,[this.dashlength,this.dashgap,this.altdashlength,this.dashgap]); + } + else if (this.dashlength != undefined && this.dashgap != undefined) //If a dash and gap value has been set add to the array this value + { + ctx.dashedLine(this.from.x,this.from.y,this.to.x,this.to.y,[this.dashlength,this.dashgap]); + } + else //If all else fails draw a line + { + ctx.moveTo(this.from.x, this.from.y); + ctx.lineTo(this.to.x, this.to.y); + } + ctx.stroke(); + + // draw text + if (this.text) { + var point = this._pointOnLine(0.5); + this._text(ctx, this.text, point.x, point.y); + } +}; + +/** + * Get a point on a line + * @param {Number} percentage. Value between 0 (line start) and 1 (line end) + * @return {Object} point + * @private + */ +Graph.Edge.prototype._pointOnLine = function (percentage) { + return { + x: (1 - percentage) * this.from.x + percentage * this.to.x, + y: (1 - percentage) * this.from.y + percentage * this.to.y + } +}; + +/** + * Get a point on a circle + * @param {Number} x + * @param {Number} y + * @param {Number} radius + * @param {Number} percentage. Value between 0 (line start) and 1 (line end) + * @return {Object} point + * @private + */ +Graph.Edge.prototype._pointOnCircle = function (x, y, radius, percentage) { + var angle = (percentage - 3/8) * 2 * Math.PI; + return { + x: x + radius * Math.cos(angle), + y: y - radius * Math.sin(angle) + } +}; + +/** + * Redraw a edge as a line with a moving arrow + * Draw this edge in the given canvas + * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d"); + * @param {CanvasRenderingContext2D} ctx + */ +Graph.Edge.prototype._drawMovingArrows = function(ctx) { + this._drawArrow(ctx); + + for (var a in this.arrows) { + if (this.arrows.hasOwnProperty(a)) { + this.arrows[a] += 0.02; // TODO determine speed from interval + if (this.arrows[a] > 1.0) this.arrows[a] = 0.0; + } + } +}; + +/** + * Redraw a edge as a line with a moving dot + * Draw this edge in the given canvas + * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d"); + * @param {CanvasRenderingContext2D} ctx + */ +Graph.Edge.prototype._drawMovingDot = function(ctx) { + // set style + ctx.strokeStyle = this.color; + ctx.fillStyle = this.color; + ctx.lineWidth = this._getLineWidth(); + + // draw line + var point; + if (this.from != this.to) { + this._line(ctx); + + // draw dot + var radius = 4 + this.width * 2; + point = this._pointOnLine(this.dot); + ctx.circle(point.x, point.y, radius); + ctx.fill(); + + // move the dot to the next position + this.dot += 0.05; // TODO determine speed from interval + if (this.dot > 1.0) this.dot = 0.0; + + // draw text + if (this.text) { + point = this._pointOnLine(0.5); + this._text(ctx, this.text, point.x, point.y); + } + } + else { + // TODO: moving dot for a circular edge + } +}; + + +/** + * Redraw a edge as a line with an arrow + * Draw this edge in the given canvas + * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d"); + * @param {CanvasRenderingContext2D} ctx + */ +Graph.Edge.prototype._drawArrow = function(ctx) { + var point; + // set style + ctx.strokeStyle = this.color; + ctx.fillStyle = this.color; + ctx.lineWidth = this._getLineWidth(); + + if (this.from != this.to) { + // draw line + this._line(ctx); + + // draw all arrows + var angle = Math.atan2((this.to.y - this.from.y), (this.to.x - this.from.x)); + var length = 10 + 5 * this.width; // TODO: make customizable? + for (var a in this.arrows) { + if (this.arrows.hasOwnProperty(a)) { + point = this._pointOnLine(this.arrows[a]); + ctx.arrow(point.x, point.y, angle, length); + ctx.fill(); + ctx.stroke(); + } + } + + // draw text + if (this.text) { + point = this._pointOnLine(0.5); + this._text(ctx, this.text, point.x, point.y); + } + } + else { + // draw circle + var radius = this.length / 2 / Math.PI; + var x, y; + var node = this.from; + if (!node.width) { + node.resize(ctx); + } + if (node.width > node.height) { + x = node.x + node.width / 2; + y = node.y - radius; + } + else { + x = node.x + radius; + y = node.y - node.height / 2; + } + this._circle(ctx, x, y, radius); + + // draw all arrows + var angle = 0.2 * Math.PI; + var length = 10 + 5 * this.width; // TODO: make customizable? + for (var a in this.arrows) { + if (this.arrows.hasOwnProperty(a)) { + point = this._pointOnCircle(x, y, radius, this.arrows[a]); + ctx.arrow(point.x, point.y, angle, length); + ctx.fill(); + ctx.stroke(); + } + } + + // draw text + if (this.text) { + point = this._pointOnCircle(x, y, radius, 0.5); + this._text(ctx, this.text, point.x, point.y); + } + } +}; + + + +/** + * Redraw a edge as a line with an arrow + * Draw this edge in the given canvas + * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d"); + * @param {CanvasRenderingContext2D} ctx + */ +Graph.Edge.prototype._drawArrowEnd = function(ctx) { + // set style + ctx.strokeStyle = this.color; + ctx.fillStyle = this.color; + ctx.lineWidth = this._getLineWidth(); + + // draw line + var angle, length; + if (this.from != this.to) { + // calculate length and angle of the line + angle = Math.atan2((this.to.y - this.from.y), (this.to.x - this.from.x)); + var dx = (this.to.x - this.from.x); + var dy = (this.to.y - this.from.y); + var lEdge = Math.sqrt(dx * dx + dy * dy); + + var lFrom = this.to.distanceToBorder(ctx, angle + Math.PI); + var pFrom = (lEdge - lFrom) / lEdge; + var xFrom = (pFrom) * this.from.x + (1 - pFrom) * this.to.x; + var yFrom = (pFrom) * this.from.y + (1 - pFrom) * this.to.y; + + var lTo = this.to.distanceToBorder(ctx, angle); + var pTo = (lEdge - lTo) / lEdge; + var xTo = (1 - pTo) * this.from.x + pTo * this.to.x; + var yTo = (1 - pTo) * this.from.y + pTo * this.to.y; + + ctx.beginPath(); + ctx.moveTo(xFrom, yFrom); + ctx.lineTo(xTo, yTo); + ctx.stroke(); + + // draw arrow at the end of the line + length = 10 + 5 * this.width; // TODO: make customizable? + ctx.arrow(xTo, yTo, angle, length); + ctx.fill(); + ctx.stroke(); + + // draw text + if (this.text) { + var point = this._pointOnLine(0.5); + this._text(ctx, this.text, point.x, point.y); + } + } + else { + // draw circle + var radius = this.length / 2 / Math.PI; + var x, y, arrow; + var node = this.from; + if (!node.width) { + node.resize(ctx); + } + if (node.width > node.height) { + x = node.x + node.width / 2; + y = node.y - radius; + arrow = { + x: x, + y: node.y, + angle: 0.9 * Math.PI + }; + } + else { + x = node.x + radius; + y = node.y - node.height / 2; + arrow = { + x: node.x, + y: y, + angle: 0.6 * Math.PI + }; + } + ctx.beginPath(); + // TODO: do not draw a circle, but an arc + // TODO: similarly, for a line without arrows, draw to the border of the nodes instead of the center + ctx.arc(x, y, radius, 0, 2 * Math.PI, false); + ctx.stroke(); + + // draw all arrows + length = 10 + 5 * this.width; // TODO: make customizable? + ctx.arrow(arrow.x, arrow.y, arrow.angle, length); + ctx.fill(); + ctx.stroke(); + + // draw text + if (this.text) { + point = this._pointOnCircle(x, y, radius, 0.5); + this._text(ctx, this.text, point.x, point.y); + } + } + +}; + +/**--------------------------------------------------------------------------**/ + + +/** + * @class Images + * This class loades images and keeps them stored. + */ +Graph.Images = function () { + this.images = {}; + + this.callback = undefined; +}; + +/** + * Set an onload callback function. This will be called each time an image + * is loaded + * @param {function} callback + */ +Graph.Images.prototype.setOnloadCallback = function(callback) { + this.callback = callback; +}; + + +/** + * + * @param {string} url Url of the image + * @return {Image} img The image object + */ +Graph.Images.prototype.load = function(url) { + var img = this.images[url]; + if (img == undefined) { + // create the image + var images = this; + img = new Image(); + this.images[url] = img; + img.onload = function() { + if (images.callback) { + images.callback(this); + } + }; + img.src = url; + } + + return img; +}; + + +/**--------------------------------------------------------------------------**/ + + +/** + * @class Package + * This class contains one package + * + * @param {number} properties Properties for the package. Optional. Available + * properties are: id {number}, title {string}, + * style {string} with available values "dot" and + * "image", radius {number}, image {string}, + * color {string}, progress {number} with a value + * between 0-1, duration {number}, timestamp {number + * or Date}. + * @param {Graph} graph The graph object, used to find + * and edge to nodes. + * @param {Graph.Images} imagelist An Images object. Only needed + * when the package has style 'image' + * @param {Object} constants An object with default values for + * example for the color + */ +Graph.Package = function (properties, graph, imagelist, constants) { + if (graph == undefined) { + throw "No graph provided"; + } + + // constants + this.radiusMin = constants.packages.radiusMin; + this.radiusMax = constants.packages.radiusMax; + this.imagelist = imagelist; + this.graph = graph; + + // initialize variables + this.id = undefined; + this.from = undefined; + this.to = undefined; + this.title = undefined; + this.style = constants.packages.style; + this.radius = constants.packages.radius; + this.color = constants.packages.color; + this.image = constants.packages.image; + this.value = undefined; + this.progress = 0.0; + this.timestamp = undefined; + this.duration = constants.packages.duration; + this.autoProgress = true; + this.radiusFixed = false; + + // set properties + this.setProperties(properties, constants); +}; + +Graph.Package.DEFAULT_DURATION = 1.0; // seconds + +/** + * Set or overwrite properties for the package + * @param {Object} properties an object with properties + * @param {Object} constants and object with default, global properties + */ +Graph.Package.prototype.setProperties = function(properties, constants) { + if (!properties) { + return; + } + + // note that the provided properties can also be null, when they come from the Google DataTable + if (properties.from != undefined) {this.from = this.graph._getNode(properties.from);} + if (properties.to != undefined) {this.to = this.graph._getNode(properties.to);} + + if (!this.from) { + throw "Node with id " + properties.from + " not found"; + } + if (!this.to) { + throw "Node with id " + properties.to + " not found"; + } + + if (properties.id != undefined) {this.id = properties.id;} + if (properties.title != undefined) {this.title = properties.title;} + if (properties.style != undefined) {this.style = properties.style;} + if (properties.radius != undefined) {this.radius = properties.radius;} + if (properties.value != undefined) {this.value = properties.value;} + if (properties.image != undefined) {this.image = properties.image;} + if (properties.color != undefined) {this.color = properties.color;} + if (properties.dashlength != undefined) {this.dashlength = properties.dashlength;} + if (properties.dashgap != undefined) {this.dashgap = properties.dashgap;} + if (properties.altdashlength != undefined) {this.altdashlength = properties.altdashlength;} + if (properties.progress != undefined) {this.progress = properties.progress;} + if (properties.timestamp != undefined) {this.timestamp = properties.timestamp;} + if (properties.duration != undefined) {this.duration = properties.duration;} + + this.radiusFixed = this.radiusFixed || (properties.radius != undefined); + this.autoProgress = (this.autoProgress == true) ? (properties.progress == undefined) : false; + + if (this.style == 'image') { + this.radiusMin = constants.packages.widthMin; + this.radiusMax = constants.packages.widthMax; + } + + // handle progress + if (this.progress < 0.0) {this.progress = 0.0;} + if (this.progress > 1.0) {this.progress = 1.0;} + + // handle image + if (this.image != undefined) { + if (this.imagelist) { + this.imageObj = this.imagelist.load(this.image); + } + else { + throw "No imagelist provided"; + } + } + + // choose draw method depending on the style + switch (this.style) { + // TODO: add more styles + case 'dot': this.draw = this._drawDot; break; + case 'square': this.draw = this._drawSquare; break; + case 'triangle': this.draw = this._drawTriangle; break; + case 'triangleDown':this.draw = this._drawTriangleDown; break; + case 'star': this.draw = this._drawStar; break; + case 'image': this.draw = this._drawImage; break; + default: this.draw = this._drawDot; break; + } +}; + +/** + * Set a new value for the progress of the package + * @param {number} progress A value between 0 and 1 + */ +Graph.Package.prototype.setProgress = function (progress) { + this.progress = progress; + this.autoProgress = false; +}; + +/** + * Check if a package is finished, if it has reached its destination. + * If so, the package can be removed. + * Only packages with automatically animated progress can be finished + * @return {boolean} true if finished, else false. + */ +Graph.Package.prototype.isFinished = function () { + return (this.autoProgress == true && this.progress >= 1.0); +}; + +/** + * Check if this package is moving. + * A packages moves when it has automatic progress and not yet reached its + * destination. + * @return {boolean} true if moving, else false. + */ +Graph.Package.prototype.isMoving = function () { + return (this.autoProgress || this.isFinished()); +}; + + +/** + * Perform one discrete step for the package. Only applicable when the + * package has no manually set, fixed progress. + * @param {number} interval Time interval in seconds + */ +Graph.Package.prototype.discreteStep = function(interval) { + if (this.autoProgress == true) { + this.progress += (parseFloat(interval) / this.duration); + + if (this.progress > 1.0) + this.progress = 1.0; + } +}; + + +/** + * Draw this package in the given canvas + * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d"); + * @param {CanvasRenderingContext2D} ctx + */ +Graph.Package.prototype.draw = function(ctx) { + throw "Draw method not initialized for package"; +}; + + +/** + * Check if this object is overlapping with the provided object + * @param {Object} obj an object with parameters left, top, right, bottom + * @return {boolean} True if location is located on node + */ +Graph.Package.prototype.isOverlappingWith = function(obj) { + // radius minimum 10px else it is too hard to get your mouse at the exact right position + var radius = Math.max(this.radius, 10); + var pos = this._getPosition(); + + return (pos.x - radius < obj.right && + pos.x + radius > obj.left && + pos.y - radius < obj.bottom && + pos.y + radius > obj.top); +}; + +/** + * Calculate the current position of the package + * @return {Object} position The object has parameters x and y. + */ +Graph.Package.prototype._getPosition = function() { + return { + "x" : (1 - this.progress) * this.from.x + this.progress * this.to.x, + "y" : (1 - this.progress) * this.from.y + this.progress * this.to.y + }; +}; + + +/** + * get the title of this package. + * @return {string} title The title of the package, or undefined when no + * title has been set. + */ +Graph.Package.prototype.getTitle = function() { + return this.title; +}; + +/** + * Retrieve the value of the package. Can be undefined + * @return {Number} value + */ +Graph.Package.prototype.getValue = function() { + return this.value; +}; + +/** + * Calculate the distance from the packages location to the given location (x,y) + * @param {Number} x + * @param {Number} y + * @return {Number} value + */ +Graph.Package.prototype.getDistance = function(x, y) { + var pos = this._getPosition(), + dx = pos.x - x, + dy = pos.y - y; + return Math.sqrt(dx * dx + dy * dy); +}; + +/** + * Adjust the value range of the package. The package will adjust it's radius + * based on its value. + * @param {Number} min + * @param {Number} max + */ +Graph.Package.prototype.setValueRange = function(min, max) { + if (!this.radiusFixed && this.value !== undefined) { + var factor = (this.radiusMax - this.radiusMin) / (max - min); + this.radius = (this.value - min) * factor + this.radiusMin; + } +}; + + + +/** + * Redraw a package as a dot + * Draw this edge in the given canvas + * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d"); + * @param {CanvasRenderingContext2D} ctx + */ +/* TODO: cleanup + Graph.Package.prototype._drawDot = function(ctx) { + // set style + ctx.fillStyle = this.color; + // draw dot + var pos = this._getPosition(); + ctx.circle(pos.x, pos.y, this.radius); + ctx.fill(); + } + */ + +Graph.Package.prototype._drawDot = function (ctx) { + this._drawShape(ctx, 'circle'); +}; + +Graph.Package.prototype._drawTriangle = function (ctx) { + this._drawShape(ctx, 'triangle'); +}; + +Graph.Package.prototype._drawTriangleDown = function (ctx) { + this._drawShape(ctx, 'triangleDown'); +}; + +Graph.Package.prototype._drawSquare = function (ctx) { + this._drawShape(ctx, 'square'); +}; + +Graph.Package.prototype._drawStar = function (ctx) { + this._drawShape(ctx, 'star'); +}; + +Graph.Package.prototype._drawShape = function (ctx, shape) { + // set style + ctx.fillStyle = this.color; + + // draw shape + var pos = this._getPosition(); + ctx[shape](pos.x, pos.y, this.radius); + ctx.fill(); +}; + +/** + * Redraw a package as an image + * Draw this edge in the given canvas + * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d"); + * @param {CanvasRenderingContext2D} ctx + */ +Graph.Package.prototype._drawImage = function (ctx) { + if (this.imageObj) { + var width, height; + if (this.value) { + var scale = this.imageObj.height / this.imageObj.width; + width = this.radius || this.imageObj.width; + height = this.radius * scale || this.imageObj.height; + } + else { + width = this.imageObj.width; + height = this.imageObj.height; + } + var pos = this._getPosition(); + + ctx.drawImage(this.imageObj, pos.x - width / 2, pos.y - height / 2, width, height); + } + else { + console.log("image still loading..."); + } +}; + + + +/**--------------------------------------------------------------------------**/ + + +/** + * @class Groups + * This class can store groups and properties specific for groups. + */ +Graph.Groups = function () { + this.clear(); + this.defaultIndex = 0; +}; + + +/** + * default constants for group colors + */ +Graph.Groups.DEFAULT = [ + {"borderColor": "#2B7CE9", "backgroundColor": "#97C2FC", "highlightColor": "#D2E5FF"}, // blue + {"borderColor": "#FFA500", "backgroundColor": "#FFFF00", "highlightColor": "#FFFFA3"}, // yellow + {"borderColor": "#FA0A10", "backgroundColor": "#FB7E81", "highlightColor": "#FFAFB1"}, // red + {"borderColor": "#41A906", "backgroundColor": "#7BE141", "highlightColor": "#A1EC76"}, // green + {"borderColor": "#E129F0", "backgroundColor": "#EB7DF4", "highlightColor": "#F0B3F5"}, // magenta + {"borderColor": "#7C29F0", "backgroundColor": "#AD85E4", "highlightColor": "#D3BDF0"}, // purple + {"borderColor": "#C37F00", "backgroundColor": "#FFA807", "highlightColor": "#FFCA66"}, // orange + {"borderColor": "#4220FB", "backgroundColor": "#6E6EFD", "highlightColor": "#9B9BFD"}, // darkblue + {"borderColor": "#FD5A77", "backgroundColor": "#FFC0CB", "highlightColor": "#FFD1D9"}, // pink + {"borderColor": "#4AD63A", "backgroundColor": "#C2FABC", "highlightColor": "#E6FFE3"} // mint +]; + + +/** + * Clear all groups + */ +Graph.Groups.prototype.clear = function () { + this.groups = {}; + this.groups.length = function() + { + var i = 0; + for ( var p in this ) { + if (this.hasOwnProperty(p)) { + i++; + } + } + return i; + } +}; + + +/** + * get group properties of a groupname. If groupname is not found, a new group + * is added. + * @param {*} groupname Can be a number, string, Date, etc. + * @return {Object} group The created group, containing all group properties + */ +Graph.Groups.prototype.get = function (groupname) { + var group = this.groups[groupname]; + + if (group == undefined) { + // create new group + var index = this.defaultIndex % Graph.Groups.DEFAULT.length; + this.defaultIndex++; + group = {}; + group.borderColor = Graph.Groups.DEFAULT[index].borderColor; + group.backgroundColor = Graph.Groups.DEFAULT[index].backgroundColor; + group.highlightColor = Graph.Groups.DEFAULT[index].highlightColor; + this.groups[groupname] = group; + } + + return group; +}; + +/** + * Add a custom group style + * @param {String} groupname + * @param {Object} style An object containing borderColor, + * backgroundColor, etc. + * @return {Object} group The created group object + */ +Graph.Groups.prototype.add = function (groupname, style) { + this.groups[groupname] = style; + return style; +}; + +/** + * Check if given object is a Javascript Array + * @param {*} obj + * @return {Boolean} isArray true if the given object is an array + */ +// See http://stackoverflow.com/questions/2943805/javascript-instanceof-typeof-in-gwt-jsni +Graph.isArray = function (obj) { + if (obj instanceof Array) { + return true; + } + return (Object.prototype.toString.call(obj) === '[object Array]'); +}; + + + +/**--------------------------------------------------------------------------**/ + + +/** + * @class Slider + * + * An html slider control with start/stop/prev/next buttons + * @param {Element} container The element where the slider will be created + */ +Graph.Slider = function(container) { + if (container === undefined) throw "Error: No container element defined"; + + this.container = container; + + this.frame = document.createElement("DIV"); + //this.frame.style.backgroundColor = "#E5E5E5"; + this.frame.style.width = "100%"; + this.frame.style.position = "relative"; + + this.title = document.createElement("DIV"); + this.title.style.margin = "2px"; + this.title.style.marginBottom = "5px"; + this.title.innerHTML = ""; + this.container.appendChild(this.title); + + this.frame.prev = document.createElement("INPUT"); + this.frame.prev.type = "BUTTON"; + this.frame.prev.value = "Prev"; + this.frame.appendChild(this.frame.prev); + + this.frame.play = document.createElement("INPUT"); + this.frame.play.type = "BUTTON"; + this.frame.play.value = "Play"; + this.frame.appendChild(this.frame.play); + + this.frame.next = document.createElement("INPUT"); + this.frame.next.type = "BUTTON"; + this.frame.next.value = "Next"; + this.frame.appendChild(this.frame.next); + + this.frame.bar = document.createElement("INPUT"); + this.frame.bar.type = "BUTTON"; + this.frame.bar.style.position = "absolute"; + this.frame.bar.style.border = "1px solid red"; + this.frame.bar.style.width = "100px"; + this.frame.bar.style.height = "6px"; + this.frame.bar.style.borderRadius = "2px"; + this.frame.bar.style.MozBorderRadius = "2px"; + this.frame.bar.style.border = "1px solid #7F7F7F"; + this.frame.bar.style.backgroundColor = "#E5E5E5"; + this.frame.appendChild(this.frame.bar); + + this.frame.slide = document.createElement("INPUT"); + this.frame.slide.type = "BUTTON"; + this.frame.slide.style.margin = "0px"; + this.frame.slide.value = " "; + this.frame.slide.style.position = "relative"; + this.frame.slide.style.left = "-100px"; + this.frame.appendChild(this.frame.slide); + + // create events + var me = this; + this.frame.slide.onmousedown = function (event) {me._onMouseDown(event);}; + this.frame.prev.onclick = function (event) {me.prev(event);}; + this.frame.play.onclick = function (event) {me.togglePlay(event);}; + this.frame.next.onclick = function (event) {me.next(event);}; + + this.container.appendChild(this.frame); + + this.onChangeCallback = undefined; + + this.playTimeout = undefined; + this.framerate = 20; // frames per second + this.duration = 10; // seconds + this.doLoop = true; + + this.start = 0; + this.end = 0; + this.value = 0; + this.step = 0; + this.rangeIsDate = false; + + this.redraw(); +}; + +/** + * Retrieve the step size, depending on the range, framerate, and duration + */ +Graph.Slider.prototype._updateStep = function() { + var range = (this.end - this.start); + var frameCount = this.duration * this.framerate; + + this.step = range / frameCount; +}; + +/** + * Select the previous index + */ +Graph.Slider.prototype.prev = function() { + this._setValue(this.value - this.step); +}; + +/** + * Select the next index + */ +Graph.Slider.prototype.next = function() { + this._setValue(this.value + this.step); +}; + +/** + * Select the next index + */ +Graph.Slider.prototype.playNext = function() { + var start = new Date(); + + if (!this.leftButtonDown) { + if (this.value + this.step < this.end) { + this._setValue(this.value + this.step); + } + else { + if (this.doLoop) { + this._setValue(this.start); + } + else { + this._setValue(this.end); + this.stop(); + return; + } + } + } + + var end = new Date(); + var diff = (end - start); + + // calculate how much time it to to set the index and to execute the callback + // function. + var interval = Math.max(1000 / this.framerate - diff, 0); + + var me = this; + this.playTimeout = setTimeout(function() {me.playNext();}, interval); +}; + +/** + * Toggle start or stop playing + */ +Graph.Slider.prototype.togglePlay = function() { + if (this.playTimeout === undefined) { + this.play(); + } else { + this.stop(); + } +}; + +/** + * Start playing + */ +Graph.Slider.prototype.play = function() { + this.frame.play.value = "Stop"; + + this.playNext(); +}; + +/** + * Stop playing + */ +Graph.Slider.prototype.stop = function() { + this.frame.play.value = "Play"; + + clearInterval(this.playTimeout); + this.playTimeout = undefined; +}; + +/** + * Set a callback function which will be triggered when the value of the + * slider bar has changed. + */ +Graph.Slider.prototype.setOnChangeCallback = function(callback) { + this.onChangeCallback = callback; +}; + +/** + * Set the interval for playing the list + * @param {number} framerate Framerate in frames per second + */ +Graph.Slider.prototype.setFramerate = function(framerate) { + this.framerate = framerate; + this._updateStep(); +}; + +/** + * Retrieve the current framerate + * @return {number} framerate in frames per second + */ +Graph.Slider.prototype.getFramerate = function() { + return this.framerate; +}; + +/** + * Set the duration for playing + * @param {number} duration Duration in seconds + */ +Graph.Slider.prototype.setDuration = function(duration) { + this.duration = duration; + this._updateStep(); +}; + +/** + * Set the time acceleration for playing the history. Only applicable when + * the values are of type Date. + * @param {number} acceleration Acceleration, for example 10 means play + * ten times as fast as real time. A value + * of 1 will play the history in real time. + */ +Graph.Slider.prototype.setAcceleration = function(acceleration) { + var durationRealtime = (this.end - this.start) / 1000; // in seconds + + this.duration = durationRealtime / acceleration; + this._updateStep(); +}; + + +/** + * Set looping on or off + * @param {boolean} doLoop If true, the slider will jump to the start when + * the end is passed, and will jump to the end + * when the start is passed. + */ +Graph.Slider.prototype.setLoop = function(doLoop) { + this.doLoop = doLoop; +}; + +/** + * Retrieve the current value of loop + * @return {boolean} doLoop If true, the slider will jump to the start when + * the end is passed, and will jump to the end + * when the start is passed. + */ +Graph.Slider.prototype.getLoop = function() { + return this.doLoop; +}; + + +/** + * Execute the onchange callback function + */ +Graph.Slider.prototype.onChange = function() { + if (this.onChangeCallback !== undefined) { + this.onChangeCallback(); + } +}; + +/** + * redraw the slider on the correct place + */ +Graph.Slider.prototype.redraw = function() { + // resize the bar + var barTop = (this.frame.clientHeight/2 - + this.frame.bar.offsetHeight/2); + var barWidth = (this.frame.clientWidth - + this.frame.prev.clientWidth - + this.frame.play.clientWidth - + this.frame.next.clientWidth - 30); + this.frame.bar.style.top = barTop + "px"; + this.frame.bar.style.width = barWidth + "px"; + + // position the slider button + this.frame.slide.title = this.getValue(); + this.frame.slide.style.left = this._valueToLeft(this.value) + "px"; + + // set the title + this.title.innerHTML = this.getValue(); +}; + + +/** + * Set the range for the slider + * @param {Date | Number} start Start of the range + * @param {Date | Number} end End of the range + */ +Graph.Slider.prototype.setRange = function(start, end) { + if (start === undefined || start === null || start === NaN) { + this.start = 0; + this.rangeIsDate = false; + } + else if (start instanceof Date) { + this.start = start.getTime(); + this.rangeIsDate = true; + } + else { + this.start = start; + this.rangeIsDate = false; + } + + if (end === undefined || end === null || end === NaN) { + if (this.start instanceof Date) { + this.end = new Date(this.start); + } + else { + this.end = this.start; + } + } + else if (end instanceof Date) { + this.end = end.getTime(); + } + else { + this.end = end; + } + + this.value = this.start; + + this._updateStep(); + this.redraw(); +}; + + + +/** + * Set a value for the slider. The value must be between start and end + * When the range are Dates, the value will be translated to a date + * @param {Number} value + */ +Graph.Slider.prototype._setValue = function(value) { + this.value = this._limitValue(value); + this.redraw(); + + this.onChange(); +}; + +/** + * retrieve the current value in the correct type, Number or Date + * @return {Date | Number} value + */ +Graph.Slider.prototype.getValue = function() { + if (this.rangeIsDate) { + return new Date(this.value); + } + else { + return this.value; + } +}; + + +Graph.Slider.prototype.offset = 3; + +Graph.Slider.prototype._leftToValue = function (left) { + var width = parseFloat(this.frame.bar.style.width) - + this.frame.slide.clientWidth - 10; + var x = left - this.offset; + + var range = this.end - this.start; + var value = this._limitValue(x / width * range + this.start); + + return value; +}; + +Graph.Slider.prototype._valueToLeft = function (value) { + var width = parseFloat(this.frame.bar.style.width) - + this.frame.slide.clientWidth - 10; + + var x; + if (this.end > this.start) { + x = (value - this.start) / (this.end - this.start) * width; + } + else { + x = 0; + } + var left = x + this.offset; + + return left; +}; + +Graph.Slider.prototype._limitValue = function(value) { + if (value < this.start) { + value = this.start + } + if (value > this.end) { + value = this.end; + } + + return value; +}; + +Graph.Slider.prototype._onMouseDown = function(event) { + // only react on left mouse button down + this.leftButtonDown = event.which ? (event.which === 1) : (event.button === 1); + if (!this.leftButtonDown) return; + + this.startClientX = event.clientX; + this.startSlideX = parseFloat(this.frame.slide.style.left); + + this.frame.style.cursor = 'move'; + + // add event listeners to handle moving the contents + // we store the function onmousemove and onmouseup in the graph, so we can + // remove the eventlisteners lateron in the function mouseUp() + var me = this; + this.onmousemove = function (event) {me._onMouseMove(event);}; + this.onmouseup = function (event) {me._onMouseUp(event);}; + Graph.addEventListener(document, "mousemove", this.onmousemove); + Graph.addEventListener(document, "mouseup", this.onmouseup); + Graph.preventDefault(event); +}; + + +Graph.Slider.prototype._onMouseMove = function (event) { + var diff = event.clientX - this.startClientX; + var x = this.startSlideX + diff; + + var value = this._leftToValue(x); + this._setValue(value); + + Graph.preventDefault(event); +}; + + +Graph.Slider.prototype._onMouseUp = function (event) { + this.frame.style.cursor = 'auto'; + + this.leftButtonDown = false; + + // remove event listeners + Graph.removeEventListener(document, "mousemove", this.onmousemove); + Graph.removeEventListener(document, "mouseup", this.onmouseup); + + Graph.preventDefault(event); +}; + + + +/**--------------------------------------------------------------------------**/ + + +/** + * Popup is a class to create a popup window with some text + * @param {Element} container The container object. + * @param {Number} x + * @param {Number} y + * @param {String} text + */ +Graph.Popup = function (container, x, y, text) { + if (container) { + this.container = container; + } + else { + this.container = document.body; + } + this.x = 0; + this.y = 0; + this.padding = 5; + + if (x !== undefined && y !== undefined ) { + this.setPosition(x, y); + } + if (text !== undefined) { + this.setText(text); + } + + // create the frame + this.frame = document.createElement("div"); + var style = this.frame.style; + style.position = "absolute"; + style.visibility = "hidden"; + style.border = "1px solid #666"; + style.color = "black"; + style.padding = this.padding + "px"; + style.backgroundColor = "#FFFFC6"; + style.borderRadius = "3px"; + style.MozBorderRadius = "3px"; + style.WebkitBorderRadius = "3px"; + style.boxShadow = "3px 3px 10px rgba(128, 128, 128, 0.5)"; + style.whiteSpace = "nowrap"; + this.container.appendChild(this.frame); +}; + +/** + * @param {number} x Horizontal position of the popup window + * @param {number} y Vertical position of the popup window + */ +Graph.Popup.prototype.setPosition = function(x, y) { + this.x = parseInt(x); + this.y = parseInt(y); +}; + +/** + * Set the text for the popup window. This can be HTML code + * @param {string} text + */ +Graph.Popup.prototype.setText = function(text) { + this.frame.innerHTML = text; +}; + +/** + * Show the popup window + * @param {boolean} show Optional. Show or hide the window + */ +Graph.Popup.prototype.show = function (show) { + if (show === undefined) { + show = true; + } + + if (show) { + var height = this.frame.clientHeight; + var width = this.frame.clientWidth; + var maxHeight = this.frame.parentNode.clientHeight; + var maxWidth = this.frame.parentNode.clientWidth; + + var top = (this.y - height); + if (top + height + this.padding > maxHeight) { + top = maxHeight - height - this.padding; + } + if (top < this.padding) { + top = this.padding; + } + + var left = this.x; + if (left + width + this.padding > maxWidth) { + left = maxWidth - width - this.padding; + } + if (left < this.padding) { + left = this.padding; + } + + this.frame.style.left = left + "px"; + this.frame.style.top = top + "px"; + this.frame.style.visibility = "visible"; + } + else { + this.hide(); + } +}; + +/** + * Hide the popup window + */ +Graph.Popup.prototype.hide = function () { + this.frame.style.visibility = "hidden"; +}; + + +/**--------------------------------------------------------------------------**/ + +if (typeof CanvasRenderingContext2D !== 'undefined') { + /** + * Draw a circle shape + */ + CanvasRenderingContext2D.prototype.circle = function(x, y, r) { + this.beginPath(); + this.arc(x, y, r, 0, 2*Math.PI, false); + }; + + /** + * Draw a square shape + * @param {Number} x horizontal center + * @param {Number} y vertical center + * @param {Number} r size, width and height of the square + */ + CanvasRenderingContext2D.prototype.square = function(x, y, r) { + this.beginPath(); + this.rect(x - r, y - r, r * 2, r * 2); + }; + + /** + * Draw a triangle shape + * @param {Number} x horizontal center + * @param {Number} y vertical center + * @param {Number} r radius, half the length of the sides of the triangle + */ + CanvasRenderingContext2D.prototype.triangle = function(x, y, r) { + // http://en.wikipedia.org/wiki/Equilateral_triangle + this.beginPath(); + + var s = r * 2; + var s2 = s / 2; + var ir = Math.sqrt(3) / 6 * s; // radius of inner circle + var h = Math.sqrt(s * s - s2 * s2); // height + + this.moveTo(x, y - (h - ir)); + this.lineTo(x + s2, y + ir); + this.lineTo(x - s2, y + ir); + this.lineTo(x, y - (h - ir)); + this.closePath(); + }; + + /** + * Draw a triangle shape in downward orientation + * @param {Number} x horizontal center + * @param {Number} y vertical center + * @param {Number} r radius + */ + CanvasRenderingContext2D.prototype.triangleDown = function(x, y, r) { + // http://en.wikipedia.org/wiki/Equilateral_triangle + this.beginPath(); + + var s = r * 2; + var s2 = s / 2; + var ir = Math.sqrt(3) / 6 * s; // radius of inner circle + var h = Math.sqrt(s * s - s2 * s2); // height + + this.moveTo(x, y + (h - ir)); + this.lineTo(x + s2, y - ir); + this.lineTo(x - s2, y - ir); + this.lineTo(x, y + (h - ir)); + this.closePath(); + }; + + /** + * Draw a star shape, a star with 5 points + * @param {Number} x horizontal center + * @param {Number} y vertical center + * @param {Number} r radius, half the length of the sides of the triangle + */ + CanvasRenderingContext2D.prototype.star = function(x, y, r) { + // http://www.html5canvastutorials.com/labs/html5-canvas-star-spinner/ + this.beginPath(); + + for (var n = 0; n < 10; n++) { + var radius = (n % 2 === 0) ? r * 1.3 : r * 0.5; + this.lineTo( + x + radius * Math.sin(n * 2 * Math.PI / 10), + y - radius * Math.cos(n * 2 * Math.PI / 10) + ); + } + + this.closePath(); + }; + + /** + * http://stackoverflow.com/questions/1255512/how-to-draw-a-rounded-rectangle-on-html-canvas + */ + CanvasRenderingContext2D.prototype.roundRect = function(x, y, w, h, r) { + var r2d = Math.PI/180; + if( w - ( 2 * r ) < 0 ) { r = ( w / 2 ); } //ensure that the radius isn't too large for x + if( h - ( 2 * r ) < 0 ) { r = ( h / 2 ); } //ensure that the radius isn't too large for y + this.beginPath(); + this.moveTo(x+r,y); + this.lineTo(x+w-r,y); + this.arc(x+w-r,y+r,r,r2d*270,r2d*360,false); + this.lineTo(x+w,y+h-r); + this.arc(x+w-r,y+h-r,r,0,r2d*90,false); + this.lineTo(x+r,y+h); + this.arc(x+r,y+h-r,r,r2d*90,r2d*180,false); + this.lineTo(x,y+r); + this.arc(x+r,y+r,r,r2d*180,r2d*270,false); + }; + + /** + * http://stackoverflow.com/questions/2172798/how-to-draw-an-oval-in-html5-canvas + */ + CanvasRenderingContext2D.prototype.ellipse = function(x, y, w, h) { + var kappa = .5522848, + ox = (w / 2) * kappa, // control point offset horizontal + oy = (h / 2) * kappa, // control point offset vertical + xe = x + w, // x-end + ye = y + h, // y-end + xm = x + w / 2, // x-middle + ym = y + h / 2; // y-middle + + this.beginPath(); + this.moveTo(x, ym); + this.bezierCurveTo(x, ym - oy, xm - ox, y, xm, y); + this.bezierCurveTo(xm + ox, y, xe, ym - oy, xe, ym); + this.bezierCurveTo(xe, ym + oy, xm + ox, ye, xm, ye); + this.bezierCurveTo(xm - ox, ye, x, ym + oy, x, ym); + }; + + + + /** + * http://stackoverflow.com/questions/2172798/how-to-draw-an-oval-in-html5-canvas + */ + CanvasRenderingContext2D.prototype.database = function(x, y, w, h) { + var f = 1/3; + var wEllipse = w; + var hEllipse = h * f; + + var kappa = .5522848, + ox = (wEllipse / 2) * kappa, // control point offset horizontal + oy = (hEllipse / 2) * kappa, // control point offset vertical + xe = x + wEllipse, // x-end + ye = y + hEllipse, // y-end + xm = x + wEllipse / 2, // x-middle + ym = y + hEllipse / 2, // y-middle + ymb = y + (h - hEllipse/2), // y-midlle, bottom ellipse + yeb = y + h; // y-end, bottom ellipse + + this.beginPath(); + this.moveTo(xe, ym); + + this.bezierCurveTo(xe, ym + oy, xm + ox, ye, xm, ye); + this.bezierCurveTo(xm - ox, ye, x, ym + oy, x, ym); + + this.bezierCurveTo(x, ym - oy, xm - ox, y, xm, y); + this.bezierCurveTo(xm + ox, y, xe, ym - oy, xe, ym); + + this.lineTo(xe, ymb); + + this.bezierCurveTo(xe, ymb + oy, xm + ox, yeb, xm, yeb); + this.bezierCurveTo(xm - ox, yeb, x, ymb + oy, x, ymb); + + this.lineTo(x, ym); + }; + + + /** + * Draw an arrow point (no line) + */ + CanvasRenderingContext2D.prototype.arrow = function(x, y, angle, length) { + // tail + var xt = x - length * Math.cos(angle); + var yt = y - length * Math.sin(angle); + + // inner tail + // TODO: allow to customize different shapes + var xi = x - length * 0.9 * Math.cos(angle); + var yi = y - length * 0.9 * Math.sin(angle); + + // left + var xl = xt + length / 3 * Math.cos(angle + 0.5 * Math.PI); + var yl = yt + length / 3 * Math.sin(angle + 0.5 * Math.PI); + + // right + var xr = xt + length / 3 * Math.cos(angle - 0.5 * Math.PI); + var yr = yt + length / 3 * Math.sin(angle - 0.5 * Math.PI); + + this.beginPath(); + this.moveTo(x, y); + this.lineTo(xl, yl); + this.lineTo(xi, yi); + this.lineTo(xr, yr); + this.closePath(); + }; + + + // TODO: add diamond shape +} + + +/*----------------------------------------------------------------------------*/ + +// utility methods +Graph.util = {}; + +/** + * Parse a text source containing data in DOT language into a JSON object. + * The object contains two lists: one with nodes and one with edges. + * @param {String} data Text containing a graph in DOT-notation + * @return {Object} json An object containing two parameters: + * {Object[]} nodes + * {Object[]} edges + */ +Graph.util.parseDOT = function (data) { + /** + * Test whether given character is a whitespace character + * @param {String} c + * @return {Boolean} isWhitespace + */ + function isWhitespace(c) { + return c == ' ' || c == '\t' || c == '\n' || c == '\r'; + } + + /** + * Test whether given character is a delimeter + * @param {String} c + * @return {Boolean} isDelimeter + */ + function isDelimeter(c) { + return '[]{}();,=->'.indexOf(c) != -1; + } + + var i = -1; // current index in the data + var c = ''; // current character in the data + + /** + * Read the next character from the data + */ + function next() { + i++; + c = data[i]; + } + + /** + * Preview the next character in the data + * @returns {String} nextChar + */ + function previewNext () { + return data[i + 1]; + } + + /** + * Preview the next character in the data + * @returns {String} nextChar + */ + function previewPrevious () { + return data[i + 1]; + } + + /** + * Get a text description of the the current index in the data + * @return {String} desc + */ + function pos() { + return '(char ' + i + ')'; + } + + /** + * Skip whitespace and comments + */ + function parseWhitespace() { + // skip whitespace + while (c && isWhitespace(c)) { + next(); + } + + // test for comment + var cNext = data[i + 1]; + var cPrev = data[i - 1]; + var c2 = c + cNext; + if (c2 == '/*') { + // block comment. skip until the block is closed + while (c && !(c == '*' && data[i + 1] == '/')) { + next(); + } + next(); + next(); + + parseWhitespace(); + } + else if (c2 == '//' || (c == '#' && cPrev == '\n')) { + // line comment. skip until the next return + while (c && c != '\n') { + next(); + } + next(); + parseWhitespace(); + } + } + + /** + * Parse a string + * The string may be enclosed by double quotes + * @return {String | undefined} value + */ + function parseString() { + parseWhitespace(); + + var name = ''; + if (c == '"') { + next(); + while (c && c != '"') { + name += c; + next(); + } + next(); // skip the closing quote + } + else { + while (c && !isWhitespace(c) && !isDelimeter(c)) { + name += c; + next(); + } + + // cast string to number or boolean + var number = Number(name); + if (!isNaN(number)) { + name = number; + } + else if (name == 'true') { + name = true; + } + else if (name == 'false') { + name = false; + } + else if (name == 'null') { + name = null; + } + } + + return name; + } + + /** + * Parse a value, can be a string, number, or boolean. + * The value may be enclosed by double quotes + * @return {String | Number | Boolean | undefined} value + */ + function parseValue() { + parseWhitespace(); + + if (c == '"') { + return parseString(); + } + else { + var value = parseString(); + if (value != undefined) { + // cast string to number or boolean + var number = Number(value); + if (!isNaN(number)) { + value = number; + } + else if (value == 'true') { + value = true; + } + else if (value == 'false') { + value = false; + } + else if (value == 'null') { + value = null; + } + } + return value; + } + } + + /** + * Parse a set with attributes, + * for example [label="1.000", style=solid] + * @return {Object | undefined} attr + */ + function parseAttributes() { + parseWhitespace(); + + if (c == '[') { + next(); + var attr = {}; + while (c && c != ']') { + parseWhitespace(); + + var name = parseString(); + if (!name) { + throw new SyntaxError('Attribute name expected ' + pos()); + } + + parseWhitespace(); + if (c != '=') { + throw new SyntaxError('Equal sign = expected ' + pos()); + } + next(); + + var value = parseValue(); + if (!value) { + throw new SyntaxError('Attribute value expected ' + pos()); + } + attr[name] = value; + + parseWhitespace(); + + if (c ==',') { + next(); + } + } + next(); + + return attr; + } + else { + return undefined; + } + } + + /** + * Parse a directed or undirected arrow '->' or '--' + * @return {String | undefined} arrow + */ + function parseArrow() { + parseWhitespace(); + + if (c == '-') { + next(); + if (c == '>' || c == '-') { + var arrow = '-' + c; + next(); + return arrow; + } + else { + throw new SyntaxError('Arrow "->" or "--" expected ' + pos()); + } + } + + return undefined; + } + + /** + * Parse a line separator ';' + * @return {String | undefined} separator + */ + function parseSeparator() { + parseWhitespace(); + + if (c == ';') { + next(); + return ';'; + } + + return undefined; + } + + /** + * Merge all properties of object b into object b + * @param {Object} a + * @param {Object} b + */ + function merge (a, b) { + if (a && b) { + for (var name in b) { + if (b.hasOwnProperty(name)) { + a[name] = b[name]; + } + } + } + } + + var nodeMap = {}; + var edgeList = []; + + /** + * Register a node with attributes + * @param {String} id + * @param {Object} [attr] + */ + function addNode(id, attr) { + var node = { + id: String(id), + attr: attr || {} + }; + if (!nodeMap[id]) { + nodeMap[id] = node; + } + else { + merge(nodeMap[id].attr, node.attr); + } + } + + /** + * Register an edge + * @param {String} from + * @param {String} to + * @param {String} type A string "->" or "--" + * @param {Object} [attr] + */ + function addEdge(from, to, type, attr) { + edgeList.push({ + from: String(from), + to: String(to), + type: type, + attr: attr || {} + }); + } + + // find the opening curly bracket + next(); + while (c && c != '{') { + next(); + } + if (c != '{') { + throw new SyntaxError('Invalid data. Curly bracket { expected ' + pos()) + } + next(); + + // parse all data until a closing curly bracket is encountered + while (c && c != '}') { + // parse node id and optional node attributes + var id = parseString(); + if (id == undefined) { + throw new SyntaxError('String with id expected ' + pos()); + } + var attr = parseAttributes(); + addNode(id, attr); + + // TODO: parse global attributes "graph", "node", "edge" + + // parse arrow + var type = parseArrow(); + while (type) { + // parse node id + var prevId = id; + id = parseString(); + if (id == undefined) { + throw new SyntaxError('String with id expected ' + pos()); + } + addNode(id); + + // parse edge attributes and register edge + attr = parseAttributes(); + addEdge(prevId, id, type, attr); + + // parse next arrow (optional) + type = parseArrow(); + } + + // parse separator (optional) + parseSeparator(); + + parseWhitespace(); + } + if (c != '}') { + throw new SyntaxError('Invalid data. Curly bracket } expected'); + } + + // crop data between the curly brackets + var start = data.indexOf('{'); + var end = data.indexOf('}', start); + var text = (start != -1 && end != -1) ? data.substring(start + 1, end) : undefined; + + if (!text) { + throw new Error('Invalid data. no curly brackets containing data found'); + } + + // return the results + var nodeList = []; + for (id in nodeMap) { + if (nodeMap.hasOwnProperty(id)) { + nodeList.push(nodeMap[id]); + } + } + return { + nodes: nodeList, + edges: edgeList + } +}; + +/** + * Convert a string containing a graph in DOT language into a map containing + * with nodes and edges in the format of graph. + * @param {String} data Text containing a graph in DOT-notation + * @return {Object} graphData + */ +Graph.util.DOTToGraph = function (data) { + // parse the DOT file + var dotData = Graph.util.parseDOT(data); + var graphData = { + nodes: [], + edges: [], + options: { + nodes: {}, + edges: {} + } + }; + + /** + * Merge the properties of object b into object a, and adjust properties + * not supported by Graph (for example replace "shape" with "style" + * @param {Object} a + * @param {Object} b + * @param {Array} [ignore] Optional array with property names to be ignored + */ + function merge (a, b, ignore) { + for (var prop in b) { + if (b.hasOwnProperty(prop) && (!ignore || ignore.indexOf(prop) == -1)) { + a[prop] = b[prop]; + } + } + + // Convert aliases to configuration settings supported by Graph + if (a.label) { + a.text = a.label; + delete a.label; + } + if (a.shape) { + a.style = a.shape; + delete a.shape; + } + } + + dotData.nodes.forEach(function (node) { + if (node.id.toLowerCase() == 'graph') { + merge(graphData.options, node.attr); + } + else if (node.id.toLowerCase() == 'node') { + merge(graphData.options.nodes, node.attr); + } + else if (node.id.toLowerCase() == 'edge') { + merge(graphData.options.edges, node.attr); + } + else { + var graphNode = {}; + graphNode.id = node.id; + graphNode.text = node.id; + merge(graphNode, node.attr); + graphData.nodes.push(graphNode); + } + }); + + dotData.edges.forEach(function (edge) { + var graphEdge = {}; + graphEdge.from = edge.from; + graphEdge.to = edge.to; + graphEdge.text = edge.id; + graphEdge.style = (edge.type == '->') ? 'arrow-end' : 'line'; + merge(graphEdge, edge.attr); + graphData.edges.push(graphEdge); + }); + + return graphData; +}; + +/** + * vis.js module exports + */ +var vis = { + util: util, + events: events, + + Controller: Controller, + DataSet: DataSet, + DataView: DataView, + Range: Range, + Stack: Stack, + TimeStep: TimeStep, + EventBus: EventBus, + + components: { + items: { + Item: Item, + ItemBox: ItemBox, + ItemPoint: ItemPoint, + ItemRange: ItemRange + }, + + Component: Component, + Panel: Panel, + RootPanel: RootPanel, + ItemSet: ItemSet, + TimeAxis: TimeAxis + }, + + Timeline: Timeline, + Graph: Graph +}; + +/** + * CommonJS module exports + */ +if (typeof exports !== 'undefined') { + exports = vis; +} +if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') { + module.exports = vis; +} + +/** + * AMD module exports + */ +if (typeof(define) === 'function') { + define(function () { + return vis; + }); +} + +/** + * Window exports + */ +if (typeof window !== 'undefined') { + // attach the module to the window, load as a regular javascript file + window['vis'] = vis; +} + +// inject css +util.loadCss("/* vis.js stylesheet */\n\n.graph {\n position: relative;\n border: 1px solid #bfbfbf;\n}\n\n.graph .panel {\n position: absolute;\n}\n\n.graph .groupset {\n position: absolute;\n padding: 0;\n margin: 0;\n}\n\n\n.graph .itemset {\n position: absolute;\n padding: 0;\n margin: 0;\n overflow: hidden;\n}\n\n.graph .background {\n}\n\n.graph .foreground {\n}\n\n.graph .itemset-axis {\n position: absolute;\n}\n\n.graph .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.graph .groupset .itemset-axis:last-child {\n border-top: none;\n}\n*/\n\n\n.graph .item {\n position: absolute;\n color: #1A1A1A;\n border-color: #97B0F8;\n background-color: #D5DDF6;\n display: inline-block;\n}\n\n.graph .item.selected {\n border-color: #FFC200;\n background-color: #FFF785;\n z-index: 999;\n}\n\n.graph .item.cluster {\n /* TODO: use another color or pattern? */\n background: #97B0F8 url('img/cluster_bg.png');\n color: white;\n}\n.graph .item.cluster.point {\n border-color: #D5DDF6;\n}\n\n.graph .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.graph .item.point {\n background: none;\n}\n\n.graph .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.graph .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.graph .item.range .drag-left {\n cursor: w-resize;\n z-index: 1000;\n}\n\n.graph .item.range .drag-right {\n cursor: e-resize;\n z-index: 1000;\n}\n\n.graph .item.range .content {\n position: relative;\n display: inline-block;\n}\n\n.graph .item.line {\n position: absolute;\n width: 0;\n border-left-width: 1px;\n border-left-style: solid;\n}\n\n.graph .item .content {\n margin: 5px;\n white-space: nowrap;\n overflow: hidden;\n}\n\n/* TODO: better css name, 'graph' is way to generic */\n\n.graph {\n overflow: hidden;\n}\n\n.graph .axis {\n position: relative;\n}\n\n.graph .axis .text {\n position: absolute;\n color: #4d4d4d;\n padding: 3px;\n white-space: nowrap;\n}\n\n.graph .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.graph .axis .grid.vertical {\n position: absolute;\n width: 0;\n border-right: 1px solid;\n}\n\n.graph .axis .grid.horizontal {\n position: absolute;\n left: 0;\n width: 100%;\n height: 0;\n border-bottom: 1px solid;\n}\n\n.graph .axis .grid.minor {\n border-color: #e5e5e5;\n}\n\n.graph .axis .grid.major {\n border-color: #bfbfbf;\n}\n\n"); + +})() },{"moment":2}],2:[function(require,module,exports){ (function(){// moment.js // version : 2.0.0 diff --git a/vis.min.js b/vis.min.js index 866ee356..0dbdae9b 100644 --- a/vis.min.js +++ b/vis.min.js @@ -22,6 +22,8 @@ * License for the specific language governing permissions and limitations under * the License. */ -(function(t){if("function"==typeof bootstrap)bootstrap("vis",t);else if("object"==typeof exports)module.exports=t();else if("function"==typeof define&&define.amd)define(t);else if("undefined"!=typeof ses){if(!ses.ok())return;ses.makeVis=t}else"undefined"!=typeof window?window.vis=t():global.vis=t()})(function(){var t;return function(t,e,i){function n(i,o){if(!e[i]){if(!t[i]){var s="function"==typeof require&&require;if(!o&&s)return s(i,!0);if(r)return r(i,!0);throw Error("Cannot find module '"+i+"'")}var a=e[i]={exports:{}};t[i][0].call(a.exports,function(e){var r=t[i][1][e];return n(r?r:e)},a,a.exports)}return e[i].exports}for(var r="function"==typeof require&&require,o=0;i.length>o;o++)n(i[o]);return n}({1:[function(e,i,n){function r(t){if(this.id=E.randomUUID(),this.options=t||{},this.data={},this.fieldId=this.options.fieldId||"id",this.fieldTypes={},this.options.fieldTypes)for(var e in this.options.fieldTypes)if(this.options.fieldTypes.hasOwnProperty(e)){var i=this.options.fieldTypes[e];this.fieldTypes[e]="Date"==i||"ISODate"==i||"ASPDate"==i?"Date":i}this.subscribers={},this.internalIds={}}function o(t,e){this.id=E.randomUUID(),this.data=null,this.ids={},this.options=e||{},this.fieldId="id",this.subscribers={};var i=this;this.listener=function(){i._onEvent.apply(i,arguments)},this.setData(t)}function s(t,e){this.parent=t,this.options=e||{},this.defaultOptions={order:function(t,e){if(t instanceof y){if(e instanceof y){var i=t.data.end-t.data.start,n=e.data.end-e.data.start;return i-n||t.data.start-e.data.start}return-1}return e instanceof y?1:t.data.start-e.data.start},margin:{item:10}},this.ordered=[]}function a(t){this.id=E.randomUUID(),this.start=0,this.end=0,this.options={min:null,max:null,zoomMin:null,zoomMax:null},this.listeners=[],this.setOptions(t)}function h(){this.subscriptions=[]}function p(){this.id=E.randomUUID(),this.components={},this.repaintTimer=void 0,this.reflowTimer=void 0}function u(){this.id=null,this.parent=null,this.depends=null,this.controller=null,this.options=null,this.frame=null,this.top=0,this.left=0,this.width=0,this.height=0}function c(t,e,i){this.id=E.randomUUID(),this.parent=t,this.depends=e,this.options=i||{}}function d(t,e){this.id=E.randomUUID(),this.container=t,this.options=e||{},this.defaultOptions={autoResize:!0},this.listeners={}}function l(t,e,i){this.id=E.randomUUID(),this.parent=t,this.depends=e,this.dom={majorLines:[],majorTexts:[],minorLines:[],minorTexts:[],redundant:{majorLines:[],majorTexts:[],minorLines:[],minorTexts:[]}},this.props={range:{start:0,end:0,minimumStep:0},lineTop:0},this.options=i||{},this.defaultOptions={orientation:"bottom",showMinorLabels:!0,showMajorLabels:!0},this.conversion=null,this.range=null}function f(t,e,i){this.id=E.randomUUID(),this.parent=t,this.depends=e,this.options=i||{},this.defaultOptions={style:"box",align:"center",orientation:"bottom",margin:{axis:20,item:10},padding:5},this.dom={};var n=this;this.itemsData=null,this.range=null,this.listeners={add:function(t,e,i){i!=n.id&&n._onAdd(e.items)},update:function(t,e,i){i!=n.id&&n._onUpdate(e.items)},remove:function(t,e,i){i!=n.id&&n._onRemove(e.items)}},this.items={},this.queue={},this.stack=new s(this,Object.create(this.options)),this.conversion=null}function m(t,e,i,n){this.parent=t,this.data=e,this.dom=null,this.options=i||{},this.defaultOptions=n||{},this.selected=!1,this.visible=!1,this.top=0,this.left=0,this.width=0,this.height=0}function g(t,e,i,n){this.props={dot:{left:0,top:0,width:0,height:0},line:{top:0,left:0,width:0,height:0}},m.call(this,t,e,i,n)}function v(t,e,i,n){this.props={dot:{top:0,width:0,height:0},content:{height:0,marginLeft:0}},m.call(this,t,e,i,n)}function y(t,e,i,n){this.props={content:{left:0,width:0}},m.call(this,t,e,i,n)}function w(t,e,i){this.id=E.randomUUID(),this.parent=t,this.groupId=e,this.itemsData=null,this.itemset=null,this.options=i||{},this.options.top=0,this.top=0,this.left=0,this.width=0,this.height=0}function b(t,e,i){this.id=E.randomUUID(),this.parent=t,this.depends=e,this.options=i||{},this.range=null,this.itemsData=null,this.groupsData=null,this.groups={},this.queue={};var n=this;this.listeners={add:function(t,e){n._onAdd(e.items)},update:function(t,e){n._onUpdate(e.items)},remove:function(t,e){n._onRemove(e.items)}}}function S(t,e,i){var n=this;if(this.options=E.extend({orientation:"bottom",min:null,max:null,zoomMin:10,zoomMax:31536e10,moveable:!0,zoomable:!0,showMinorLabels:!0,showMajorLabels:!0,autoResize:!1},i),this.controller=new p,!t)throw Error("No container element provided");var r=Object.create(this.options);r.height=function(){return n.options.height?n.options.height:n.timeaxis.height+n.content.height},this.root=new d(t,r),this.controller.add(this.root);var o=T().hours(0).minutes(0).seconds(0).milliseconds(0);this.range=new a({start:o.clone().add("days",-3).valueOf(),end:o.clone().add("days",4).valueOf()}),this.range.subscribe(this.root,"move","horizontal"),this.range.subscribe(this.root,"zoom","horizontal"),this.range.on("rangechange",function(){var t=!0;n.controller.requestReflow(t)}),this.range.on("rangechanged",function(){var t=!0;n.controller.requestReflow(t)});var s=Object.create(r);s.range=this.range,this.timeaxis=new l(this.root,[],s),this.timeaxis.setRange(this.range),this.controller.add(this.timeaxis),this.setGroups(null),this.itemsData=null,this.groupsData=null,e&&this.setItems(e)}var T=e("moment"),E={};E.isNumber=function(t){return t instanceof Number||"number"==typeof t},E.isString=function(t){return t instanceof String||"string"==typeof t},E.isDate=function(t){if(t instanceof Date)return!0;if(E.isString(t)){var e=M.exec(t);if(e)return!0;if(!isNaN(Date.parse(t)))return!0}return!1},E.isDataTable=function(t){return"undefined"!=typeof google&&google.visualization&&google.visualization.DataTable&&t instanceof google.visualization.DataTable},E.randomUUID=function(){var t=function(){return Math.floor(65536*Math.random()).toString(16)};return t()+t()+"-"+t()+"-"+t()+"-"+t()+"-"+t()+t()+t()},E.extend=function(t){for(var e=1,i=arguments.length;i>e;e++){var n=arguments[e];for(var r in n)n.hasOwnProperty(r)&&void 0!==n[r]&&(t[r]=n[r])}return t},E.cast=function(t,e){var i;if(void 0===t)return void 0;if(null===t)return null;if(!e)return t;if("string"!=typeof e&&!(e instanceof String))throw Error("Type must be a string");switch(e){case"boolean":case"Boolean":return Boolean(t);case"number":case"Number":return Number(t);case"string":case"String":return t+"";case"Date":if(E.isNumber(t))return new Date(t);if(t instanceof Date)return new Date(t.valueOf());if(T.isMoment(t))return new Date(t.valueOf());if(E.isString(t))return i=M.exec(t),i?new Date(Number(i[1])):T(t).toDate();throw Error("Cannot cast object of type "+E.getType(t)+" to type Date");case"Moment":if(E.isNumber(t))return T(t);if(t instanceof Date)return T(t.valueOf());if(T.isMoment(t))return T.clone();if(E.isString(t))return i=M.exec(t),i?T(Number(i[1])):T(t);throw Error("Cannot cast object of type "+E.getType(t)+" to type Date");case"ISODate":if(t instanceof Date)return t.toISOString();if(T.isMoment(t))return t.toDate().toISOString();if(E.isNumber(t)||E.isString(t))return T(t).toDate().toISOString();throw Error("Cannot cast object of type "+E.getType(t)+" to type ISODate");case"ASPDate":if(t instanceof Date)return"/Date("+t.valueOf()+")/";if(E.isNumber(t)||E.isString(t))return"/Date("+T(t).valueOf()+")/";throw Error("Cannot cast object of type "+E.getType(t)+" to type ASPDate");default:throw Error("Cannot cast object of type "+E.getType(t)+' to type "'+e+'"')}};var M=/^\/?Date\((\-?\d+)/i;if(E.getType=function(t){var e=typeof t;return"object"==e?null==t?"null":t instanceof Boolean?"Boolean":t instanceof Number?"Number":t instanceof String?"String":t instanceof Array?"Array":t instanceof Date?"Date":"Object":"number"==e?"Number":"boolean"==e?"Boolean":"string"==e?"String":e},E.getAbsoluteLeft=function(t){for(var e=document.documentElement,i=document.body,n=t.offsetLeft,r=t.offsetParent;null!=r&&r!=i&&r!=e;)n+=r.offsetLeft,n-=r.scrollLeft,r=r.offsetParent;return n},E.getAbsoluteTop=function(t){for(var e=document.documentElement,i=document.body,n=t.offsetTop,r=t.offsetParent;null!=r&&r!=i&&r!=e;)n+=r.offsetTop,n-=r.scrollTop,r=r.offsetParent;return n},E.getPageY=function(t){if("pageY"in t)return t.pageY;var e;e="targetTouches"in t&&t.targetTouches.length?t.targetTouches[0].clientY:t.clientY;var i=document.documentElement,n=document.body;return e+(i&&i.scrollTop||n&&n.scrollTop||0)-(i&&i.clientTop||n&&n.clientTop||0)},E.getPageX=function(t){if("pageY"in t)return t.pageX;var e;e="targetTouches"in t&&t.targetTouches.length?t.targetTouches[0].clientX:t.clientX;var i=document.documentElement,n=document.body;return e+(i&&i.scrollLeft||n&&n.scrollLeft||0)-(i&&i.clientLeft||n&&n.clientLeft||0)},E.addClassName=function(t,e){var i=t.className.split(" ");-1==i.indexOf(e)&&(i.push(e),t.className=i.join(" "))},E.removeClassName=function(t,e){var i=t.className.split(" "),n=i.indexOf(e);-1!=n&&(i.splice(n,1),t.className=i.join(" "))},E.forEach=function(t,e){var i,n;if(t instanceof Array)for(i=0,n=t.length;n>i;i++)e(t[i],i,t);else for(i in t)t.hasOwnProperty(i)&&e(t[i],i,t)},E.updateProperty=function(t,e,i){return t[e]!==i?(t[e]=i,!0):!1},E.addEventListener=function(t,e,i,n){t.addEventListener?(void 0===n&&(n=!1),"mousewheel"===e&&navigator.userAgent.indexOf("Firefox")>=0&&(e="DOMMouseScroll"),t.addEventListener(e,i,n)):t.attachEvent("on"+e,i)},E.removeEventListener=function(t,e,i,n){t.removeEventListener?(void 0===n&&(n=!1),"mousewheel"===e&&navigator.userAgent.indexOf("Firefox")>=0&&(e="DOMMouseScroll"),t.removeEventListener(e,i,n)):t.detachEvent("on"+e,i)},E.getTarget=function(t){t||(t=window.event);var e;return t.target?e=t.target:t.srcElement&&(e=t.srcElement),void 0!=e.nodeType&&3==e.nodeType&&(e=e.parentNode),e},E.stopPropagation=function(t){t||(t=window.event),t.stopPropagation?t.stopPropagation():t.cancelBubble=!0},E.preventDefault=function(t){t||(t=window.event),t.preventDefault?t.preventDefault():t.returnValue=!1},E.option={},E.option.asBoolean=function(t,e){return"function"==typeof t&&(t=t()),null!=t?0!=t:e||null},E.option.asNumber=function(t,e){return"function"==typeof t&&(t=t()),null!=t?Number(t)||e||null:e||null},E.option.asString=function(t,e){return"function"==typeof t&&(t=t()),null!=t?t+"":e||null},E.option.asSize=function(t,e){return"function"==typeof t&&(t=t()),E.isString(t)?t:E.isNumber(t)?t+"px":e||null},E.option.asElement=function(t,e){return"function"==typeof t&&(t=t()),t||e||null},E.loadCss=function(t){if("undefined"!=typeof document){var e=document.createElement("style");e.type="text/css",e.styleSheet?e.styleSheet.cssText=t:e.appendChild(document.createTextNode(t)),document.getElementsByTagName("head")[0].appendChild(e)}},!Array.prototype.indexOf){Array.prototype.indexOf=function(t){for(var e=0;this.length>e;e++)if(this[e]==t)return e;return-1};try{console.log("Warning: Ancient browser detected. Please update your browser")}catch(D){}}Array.prototype.forEach||(Array.prototype.forEach=function(t,e){for(var i=0,n=this.length;n>i;++i)t.call(e||this,this[i],i,this)}),Array.prototype.map||(Array.prototype.map=function(t,e){var i,n,r;if(null==this)throw new TypeError(" this is null or not defined");var o=Object(this),s=o.length>>>0;if("function"!=typeof t)throw new TypeError(t+" is not a function");for(e&&(i=e),n=Array(s),r=0;s>r;){var a,h;r in o&&(a=o[r],h=t.call(i,a,r,o),n[r]=h),r++}return n}),Array.prototype.filter||(Array.prototype.filter=function(t){"use strict";if(null==this)throw new TypeError;var e=Object(this),i=e.length>>>0;if("function"!=typeof t)throw new TypeError;for(var n=[],r=arguments[1],o=0;i>o;o++)if(o in e){var s=e[o];t.call(r,s,o,e)&&n.push(s)}return n}),Object.keys||(Object.keys=function(){var t=Object.prototype.hasOwnProperty,e=!{toString:null}.propertyIsEnumerable("toString"),i=["toString","toLocaleString","valueOf","hasOwnProperty","isPrototypeOf","propertyIsEnumerable","constructor"],n=i.length;return function(r){if("object"!=typeof r&&"function"!=typeof r||null===r)throw new TypeError("Object.keys called on non-object");var o=[];for(var s in r)t.call(r,s)&&o.push(s);if(e)for(var a=0;n>a;a++)t.call(r,i[a])&&o.push(i[a]);return o}}()),Array.isArray||(Array.isArray=function(t){return"[object Array]"===Object.prototype.toString.call(t)}),Function.prototype.bind||(Function.prototype.bind=function(t){if("function"!=typeof this)throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable");var e=Array.prototype.slice.call(arguments,1),i=this,n=function(){},r=function(){return i.apply(this instanceof n&&t?this:t,e.concat(Array.prototype.slice.call(arguments)))};return n.prototype=this.prototype,r.prototype=new n,r}),Object.create||(Object.create=function(t){function e(){}if(arguments.length>1)throw Error("Object.create implementation only accepts the first parameter.");return e.prototype=t,new e});var _={listeners:[],indexOf:function(t){for(var e=this.listeners,i=0,n=this.listeners.length;n>i;i++){var r=e[i];if(r&&r.object==t)return i}return-1},addListener:function(t,e,i){var n=this.indexOf(t),r=this.listeners[n];r||(r={object:t,events:{}},this.listeners.push(r));var o=r.events[e];o||(o=[],r.events[e]=o),-1==o.indexOf(i)&&o.push(i)},removeListener:function(t,e,i){var n=this.indexOf(t),r=this.listeners[n];if(r){var o=r.events[e];o&&(n=o.indexOf(i),-1!=n&&o.splice(n,1),0==o.length&&delete r.events[e]);var s=0,a=r.events;for(var h in a)a.hasOwnProperty(h)&&s++;0==s&&delete this.listeners[n]}},removeAllListeners:function(){this.listeners=[]},trigger:function(t,e,i){var n=this.indexOf(t),r=this.listeners[n];if(r){var o=r.events[e];if(o)for(var s=0,a=o.length;a>s;s++)o[s](i)}}};TimeStep=function(t,e,i){this.current=new Date,this._start=new Date,this._end=new Date,this.autoScale=!0,this.scale=TimeStep.SCALE.DAY,this.step=1,this.setRange(t,e,i)},TimeStep.SCALE={MILLISECOND:1,SECOND:2,MINUTE:3,HOUR:4,DAY:5,WEEKDAY:6,MONTH:7,YEAR:8},TimeStep.prototype.setRange=function(t,e,i){t instanceof Date&&e instanceof Date&&(this._start=void 0!=t?new Date(t.valueOf()):new Date,this._end=void 0!=e?new Date(e.valueOf()):new Date,this.autoScale&&this.setMinimumStep(i))},TimeStep.prototype.first=function(){this.current=new Date(this._start.valueOf()),this.roundToMinor()},TimeStep.prototype.roundToMinor=function(){switch(this.scale){case TimeStep.SCALE.YEAR:this.current.setFullYear(this.step*Math.floor(this.current.getFullYear()/this.step)),this.current.setMonth(0);case TimeStep.SCALE.MONTH:this.current.setDate(1);case TimeStep.SCALE.DAY:case TimeStep.SCALE.WEEKDAY:this.current.setHours(0);case TimeStep.SCALE.HOUR:this.current.setMinutes(0);case TimeStep.SCALE.MINUTE:this.current.setSeconds(0);case TimeStep.SCALE.SECOND:this.current.setMilliseconds(0)}if(1!=this.step)switch(this.scale){case TimeStep.SCALE.MILLISECOND:this.current.setMilliseconds(this.current.getMilliseconds()-this.current.getMilliseconds()%this.step);break;case TimeStep.SCALE.SECOND:this.current.setSeconds(this.current.getSeconds()-this.current.getSeconds()%this.step);break;case TimeStep.SCALE.MINUTE:this.current.setMinutes(this.current.getMinutes()-this.current.getMinutes()%this.step);break;case TimeStep.SCALE.HOUR:this.current.setHours(this.current.getHours()-this.current.getHours()%this.step);break;case TimeStep.SCALE.WEEKDAY:case TimeStep.SCALE.DAY:this.current.setDate(this.current.getDate()-1-(this.current.getDate()-1)%this.step+1);break;case TimeStep.SCALE.MONTH:this.current.setMonth(this.current.getMonth()-this.current.getMonth()%this.step);break;case TimeStep.SCALE.YEAR:this.current.setFullYear(this.current.getFullYear()-this.current.getFullYear()%this.step);break;default:}},TimeStep.prototype.hasNext=function(){return this.current.valueOf()<=this._end.valueOf()},TimeStep.prototype.next=function(){var t=this.current.valueOf();if(6>this.current.getMonth())switch(this.scale){case TimeStep.SCALE.MILLISECOND:this.current=new Date(this.current.valueOf()+this.step);break;case TimeStep.SCALE.SECOND:this.current=new Date(this.current.valueOf()+1e3*this.step);break;case TimeStep.SCALE.MINUTE:this.current=new Date(this.current.valueOf()+60*1e3*this.step);break;case TimeStep.SCALE.HOUR:this.current=new Date(this.current.valueOf()+60*60*1e3*this.step);var e=this.current.getHours();this.current.setHours(e-e%this.step);break;case TimeStep.SCALE.WEEKDAY:case TimeStep.SCALE.DAY:this.current.setDate(this.current.getDate()+this.step);break;case TimeStep.SCALE.MONTH:this.current.setMonth(this.current.getMonth()+this.step);break;case TimeStep.SCALE.YEAR:this.current.setFullYear(this.current.getFullYear()+this.step);break;default:}else switch(this.scale){case TimeStep.SCALE.MILLISECOND:this.current=new Date(this.current.valueOf()+this.step);break;case TimeStep.SCALE.SECOND:this.current.setSeconds(this.current.getSeconds()+this.step);break;case TimeStep.SCALE.MINUTE:this.current.setMinutes(this.current.getMinutes()+this.step);break;case TimeStep.SCALE.HOUR:this.current.setHours(this.current.getHours()+this.step);break;case TimeStep.SCALE.WEEKDAY:case TimeStep.SCALE.DAY:this.current.setDate(this.current.getDate()+this.step);break;case TimeStep.SCALE.MONTH:this.current.setMonth(this.current.getMonth()+this.step);break;case TimeStep.SCALE.YEAR:this.current.setFullYear(this.current.getFullYear()+this.step);break;default:}if(1!=this.step)switch(this.scale){case TimeStep.SCALE.MILLISECOND:this.current.getMilliseconds()0&&(this.step=e),this.autoScale=!1},TimeStep.prototype.setAutoScale=function(t){this.autoScale=t},TimeStep.prototype.setMinimumStep=function(t){if(void 0!=t){var e=31104e6,i=2592e6,n=864e5,r=36e5,o=6e4,s=1e3,a=1;1e3*e>t&&(this.scale=TimeStep.SCALE.YEAR,this.step=1e3),500*e>t&&(this.scale=TimeStep.SCALE.YEAR,this.step=500),100*e>t&&(this.scale=TimeStep.SCALE.YEAR,this.step=100),50*e>t&&(this.scale=TimeStep.SCALE.YEAR,this.step=50),10*e>t&&(this.scale=TimeStep.SCALE.YEAR,this.step=10),5*e>t&&(this.scale=TimeStep.SCALE.YEAR,this.step=5),e>t&&(this.scale=TimeStep.SCALE.YEAR,this.step=1),3*i>t&&(this.scale=TimeStep.SCALE.MONTH,this.step=3),i>t&&(this.scale=TimeStep.SCALE.MONTH,this.step=1),5*n>t&&(this.scale=TimeStep.SCALE.DAY,this.step=5),2*n>t&&(this.scale=TimeStep.SCALE.DAY,this.step=2),n>t&&(this.scale=TimeStep.SCALE.DAY,this.step=1),n/2>t&&(this.scale=TimeStep.SCALE.WEEKDAY,this.step=1),4*r>t&&(this.scale=TimeStep.SCALE.HOUR,this.step=4),r>t&&(this.scale=TimeStep.SCALE.HOUR,this.step=1),15*o>t&&(this.scale=TimeStep.SCALE.MINUTE,this.step=15),10*o>t&&(this.scale=TimeStep.SCALE.MINUTE,this.step=10),5*o>t&&(this.scale=TimeStep.SCALE.MINUTE,this.step=5),o>t&&(this.scale=TimeStep.SCALE.MINUTE,this.step=1),15*s>t&&(this.scale=TimeStep.SCALE.SECOND,this.step=15),10*s>t&&(this.scale=TimeStep.SCALE.SECOND,this.step=10),5*s>t&&(this.scale=TimeStep.SCALE.SECOND,this.step=5),s>t&&(this.scale=TimeStep.SCALE.SECOND,this.step=1),200*a>t&&(this.scale=TimeStep.SCALE.MILLISECOND,this.step=200),100*a>t&&(this.scale=TimeStep.SCALE.MILLISECOND,this.step=100),50*a>t&&(this.scale=TimeStep.SCALE.MILLISECOND,this.step=50),10*a>t&&(this.scale=TimeStep.SCALE.MILLISECOND,this.step=10),5*a>t&&(this.scale=TimeStep.SCALE.MILLISECOND,this.step=5),a>t&&(this.scale=TimeStep.SCALE.MILLISECOND,this.step=1)}},TimeStep.prototype.snap=function(t){if(this.scale==TimeStep.SCALE.YEAR){var e=t.getFullYear()+Math.round(t.getMonth()/12);t.setFullYear(Math.round(e/this.step)*this.step),t.setMonth(0),t.setDate(0),t.setHours(0),t.setMinutes(0),t.setSeconds(0),t.setMilliseconds(0)}else if(this.scale==TimeStep.SCALE.MONTH)t.getDate()>15?(t.setDate(1),t.setMonth(t.getMonth()+1)):t.setDate(1),t.setHours(0),t.setMinutes(0),t.setSeconds(0),t.setMilliseconds(0);else if(this.scale==TimeStep.SCALE.DAY||this.scale==TimeStep.SCALE.WEEKDAY){switch(this.step){case 5:case 2:t.setHours(24*Math.round(t.getHours()/24));break;default:t.setHours(12*Math.round(t.getHours()/12))}t.setMinutes(0),t.setSeconds(0),t.setMilliseconds(0)}else if(this.scale==TimeStep.SCALE.HOUR){switch(this.step){case 4:t.setMinutes(60*Math.round(t.getMinutes()/60));break;default:t.setMinutes(30*Math.round(t.getMinutes()/30))}t.setSeconds(0),t.setMilliseconds(0)}else if(this.scale==TimeStep.SCALE.MINUTE){switch(this.step){case 15:case 10:t.setMinutes(5*Math.round(t.getMinutes()/5)),t.setSeconds(0);break;case 5:t.setSeconds(60*Math.round(t.getSeconds()/60));break;default:t.setSeconds(30*Math.round(t.getSeconds()/30))}t.setMilliseconds(0)}else if(this.scale==TimeStep.SCALE.SECOND)switch(this.step){case 15:case 10:t.setSeconds(5*Math.round(t.getSeconds()/5)),t.setMilliseconds(0);break;case 5:t.setMilliseconds(1e3*Math.round(t.getMilliseconds()/1e3));break;default:t.setMilliseconds(500*Math.round(t.getMilliseconds()/500))}else if(this.scale==TimeStep.SCALE.MILLISECOND){var i=this.step>5?this.step/2:1;t.setMilliseconds(Math.round(t.getMilliseconds()/i)*i)}},TimeStep.prototype.isMajor=function(){switch(this.scale){case TimeStep.SCALE.MILLISECOND:return 0==this.current.getMilliseconds();case TimeStep.SCALE.SECOND:return 0==this.current.getSeconds();case TimeStep.SCALE.MINUTE:return 0==this.current.getHours()&&0==this.current.getMinutes();case TimeStep.SCALE.HOUR:return 0==this.current.getHours();case TimeStep.SCALE.WEEKDAY:case TimeStep.SCALE.DAY:return 1==this.current.getDate();case TimeStep.SCALE.MONTH:return 0==this.current.getMonth();case TimeStep.SCALE.YEAR:return!1;default:return!1}},TimeStep.prototype.getLabelMinor=function(t){switch(void 0==t&&(t=this.current),this.scale){case TimeStep.SCALE.MILLISECOND:return T(t).format("SSS");case TimeStep.SCALE.SECOND:return T(t).format("s");case TimeStep.SCALE.MINUTE:return T(t).format("HH:mm");case TimeStep.SCALE.HOUR:return T(t).format("HH:mm");case TimeStep.SCALE.WEEKDAY:return T(t).format("ddd D");case TimeStep.SCALE.DAY:return T(t).format("D");case TimeStep.SCALE.MONTH:return T(t).format("MMM");case TimeStep.SCALE.YEAR:return T(t).format("YYYY");default:return""}},TimeStep.prototype.getLabelMajor=function(t){switch(void 0==t&&(t=this.current),this.scale){case TimeStep.SCALE.MILLISECOND:return T(t).format("HH:mm:ss");case TimeStep.SCALE.SECOND:return T(t).format("D MMMM HH:mm");case TimeStep.SCALE.MINUTE:case TimeStep.SCALE.HOUR:return T(t).format("ddd D MMMM");case TimeStep.SCALE.WEEKDAY:case TimeStep.SCALE.DAY:return T(t).format("MMMM YYYY");case TimeStep.SCALE.MONTH:return T(t).format("YYYY");case TimeStep.SCALE.YEAR:return"";default:return""}},r.prototype.subscribe=function(t,e,i){var n=this.subscribers[t];n||(n=[],this.subscribers[t]=n),n.push({id:i?i+"":null,callback:e})},r.prototype.unsubscribe=function(t,e){var i=this.subscribers[t];i&&(this.subscribers[t]=i.filter(function(t){return t.callback!=e}))},r.prototype._trigger=function(t,e,i){if("*"==t)throw Error("Cannot trigger event *");var n=[];t in this.subscribers&&(n=n.concat(this.subscribers[t])),"*"in this.subscribers&&(n=n.concat(this.subscribers["*"]));for(var r=0;n.length>r;r++){var o=n[r];o.callback&&o.callback(t,e,i||null)}},r.prototype.add=function(t,e){var i,n=[],r=this;if(t instanceof Array)for(var o=0,s=t.length;s>o;o++)i=r._addItem(t[o]),n.push(i);else if(E.isDataTable(t))for(var a=this._getColumnNames(t),h=0,p=t.getNumberOfRows();p>h;h++){for(var u={},c=0,d=a.length;d>c;c++){var l=a[c];u[l]=t.getValue(h,c)}i=r._addItem(u),n.push(i)}else{if(!(t instanceof Object))throw Error("Unknown dataType");i=r._addItem(t),n.push(i)}n.length&&this._trigger("add",{items:n},e)},r.prototype.update=function(t,e){var i=[],n=[],r=this,o=r.fieldId,s=function(t){var e=t[o];r.data[e]?(e=r._updateItem(t),n.push(e)):(e=r._addItem(t),i.push(e))};if(t instanceof Array)for(var a=0,h=t.length;h>a;a++)s(t[a]);else if(E.isDataTable(t))for(var p=this._getColumnNames(t),u=0,c=t.getNumberOfRows();c>u;u++){for(var d={},l=0,f=p.length;f>l;l++){var m=p[l];d[m]=t.getValue(u,l)}s(d)}else{if(!(t instanceof Object))throw Error("Unknown dataType");s(t)}i.length&&this._trigger("add",{items:i},e),n.length&&this._trigger("update",{items:n},e)},r.prototype.get=function(){var t,e,i,n,r=this,o=E.getType(arguments[0]);"String"==o||"Number"==o?(t=arguments[0],i=arguments[1],n=arguments[2]):"Array"==o?(e=arguments[0],i=arguments[1],n=arguments[2]):(i=arguments[0],n=arguments[1]);var s;if(i&&i.type){if(s="DataTable"==i.type?"DataTable":"Array",n&&s!=E.getType(n))throw Error('Type of parameter "data" ('+E.getType(n)+") "+"does not correspond with specified options.type ("+i.type+")");if("DataTable"==s&&!E.isDataTable(n))throw Error('Parameter "data" must be a DataTable when options.type is "DataTable"')}else s=n?"DataTable"==E.getType(n)?"DataTable":"Array":"Array";var a,h,p,u,c=i&&i.fieldTypes||this.options.fieldTypes,d=i&&i.filter,l=[];if(void 0!=t)a=r._getItem(t,c),d&&!d(a)&&(a=null);else if(void 0!=e)for(p=0,u=e.length;u>p;p++)a=r._getItem(e[p],c),(!d||d(a))&&l.push(a);else for(h in this.data)this.data.hasOwnProperty(h)&&(a=r._getItem(h,c),(!d||d(a))&&l.push(a));if(i&&i.order&&void 0==t&&this._sort(l,i.order),i&&i.fields){var f=i.fields;if(void 0!=t)a=this._filterFields(a,f);else for(p=0,u=l.length;u>p;p++)l[p]=this._filterFields(l[p],f)}if("DataTable"==s){var m=this._getColumnNames(n);if(void 0!=t)r._appendRow(n,m,a);else for(p=0,u=l.length;u>p;p++)r._appendRow(n,m,l[p]);return n}if(void 0!=t)return a;if(n){for(p=0,u=l.length;u>p;p++)n.push(l[p]);return n}return l},r.prototype.getIds=function(t){var e,i,n,r,o,s=this.data,a=t&&t.filter,h=t&&t.order,p=t&&t.fieldTypes||this.options.fieldTypes,u=[];if(a)if(h){o=[];for(n in s)s.hasOwnProperty(n)&&(r=this._getItem(n,p),a(r)&&o.push(r));for(this._sort(o,h),e=0,i=o.length;i>e;e++)u[e]=o[e][this.fieldId]}else for(n in s)s.hasOwnProperty(n)&&(r=this._getItem(n,p),a(r)&&u.push(r[this.fieldId]));else if(h){o=[];for(n in s)s.hasOwnProperty(n)&&o.push(s[n]);for(this._sort(o,h),e=0,i=o.length;i>e;e++)u[e]=o[e][this.fieldId]}else for(n in s)s.hasOwnProperty(n)&&(r=s[n],u.push(r[this.fieldId]));return u},r.prototype.forEach=function(t,e){var i,n,r=e&&e.filter,o=e&&e.fieldTypes||this.options.fieldTypes,s=this.data;if(e&&e.order)for(var a=this.get(e),h=0,p=a.length;p>h;h++)i=a[h],n=i[this.fieldId],t(i,n);else for(n in s)s.hasOwnProperty(n)&&(i=this._getItem(n,o),(!r||r(i))&&t(i,n))},r.prototype.map=function(t,e){var i,n=e&&e.filter,r=e&&e.fieldTypes||this.options.fieldTypes,o=[],s=this.data;for(var a in s)s.hasOwnProperty(a)&&(i=this._getItem(a,r),(!n||n(i))&&o.push(t(i,a)));return e&&e.order&&this._sort(o,e.order),o},r.prototype._filterFields=function(t,e){var i={};for(var n in t)t.hasOwnProperty(n)&&-1!=e.indexOf(n)&&(i[n]=t[n]);return i},r.prototype._sort=function(t,e){if(E.isString(e)){var i=e;t.sort(function(t,e){var n=t[i],r=e[i];return n>r?1:r>n?-1:0})}else{if("function"!=typeof e)throw new TypeError("Order must be a function or a string");t.sort(e)}},r.prototype.remove=function(t,e){var i,n,r=[];if(E.isNumber(t)||E.isString(t))delete this.data[t],delete this.internalIds[t],r.push(t);else if(t instanceof Array){for(i=0,n=t.length;n>i;i++)this.remove(t[i]);r=items.concat(t)}else if(t instanceof Object)for(i in this.data)this.data.hasOwnProperty(i)&&this.data[i]==t&&(delete this.data[i],delete this.internalIds[i],r.push(i));r.length&&this._trigger("remove",{items:r},e)},r.prototype.clear=function(t){var e=Object.keys(this.data);this.data={},this.internalIds={},this._trigger("remove",{items:e},t)},r.prototype.max=function(t){var e=this.data,i=null,n=null;for(var r in e)if(e.hasOwnProperty(r)){var o=e[r],s=o[t];null!=s&&(!i||s>n)&&(i=o,n=s)}return i},r.prototype.min=function(t){var e=this.data,i=null,n=null;for(var r in e)if(e.hasOwnProperty(r)){var o=e[r],s=o[t];null!=s&&(!i||n>s)&&(i=o,n=s)}return i},r.prototype.distinct=function(t){var e=this.data,i=[],n=this.options.fieldTypes[t],r=0;for(var o in e)if(e.hasOwnProperty(o)){for(var s=e[o],a=E.cast(s[t],n),h=!1,p=0;r>p;p++)if(i[p]==a){h=!0;break}h||(i[r]=a,r++)}return i},r.prototype._addItem=function(t){var e=t[this.fieldId];if(void 0!=e){if(this.data[e])throw Error("Cannot add item: item with id "+e+" already exists")}else e=E.randomUUID(),t[this.fieldId]=e,this.internalIds[e]=t;var i={};for(var n in t)if(t.hasOwnProperty(n)){var r=this.fieldTypes[n];i[n]=E.cast(t[n],r)}return this.data[e]=i,e},r.prototype._getItem=function(t,e){var i,n,r=this.data[t];if(!r)return null;var o={},s=this.fieldId,a=this.internalIds;if(e)for(i in r)r.hasOwnProperty(i)&&(n=r[i],i==s&&n in a||(o[i]=E.cast(n,e[i])));else for(i in r)r.hasOwnProperty(i)&&(n=r[i],i==s&&n in a||(o[i]=n));return o},r.prototype._updateItem=function(t){var e=t[this.fieldId];if(void 0==e)throw Error("Cannot update item: item has no id (item: "+JSON.stringify(t)+")");var i=this.data[e];if(!i)throw Error("Cannot update item: no item with id "+e+" found");for(var n in t)if(t.hasOwnProperty(n)){var r=this.fieldTypes[n];i[n]=E.cast(t[n],r)}return e},r.prototype._getColumnNames=function(t){for(var e=[],i=0,n=t.getNumberOfColumns();n>i;i++)e[i]=t.getColumnId(i)||t.getColumnLabel(i);return e},r.prototype._appendRow=function(t,e,i){for(var n=t.addRow(),r=0,o=e.length;o>r;r++){var s=e[r];t.setValue(n,r,i[s])}},o.prototype.setData=function(t){var e,i,n;if(this.data){this.data.unsubscribe&&this.data.unsubscribe("*",this.listener),e=[];for(var r in this.ids)this.ids.hasOwnProperty(r)&&e.push(r);this.ids={},this._trigger("remove",{items:e})}if(this.data=t,this.data){for(this.fieldId=this.options.fieldId||this.data&&this.data.options&&this.data.options.fieldId||"id",e=this.data.getIds({filter:this.options&&this.options.filter}),i=0,n=e.length;n>i;i++)r=e[i],this.ids[r]=!0;this._trigger("add",{items:e}),this.data.subscribe&&this.data.subscribe("*",this.listener)}},o.prototype.get=function(){var t,e,i,n=this,r=E.getType(arguments[0]);"String"==r||"Number"==r||"Array"==r?(t=arguments[0],e=arguments[1],i=arguments[2]):(e=arguments[0],i=arguments[1]);var o=E.extend({},this.options,e);this.options.filter&&e&&e.filter&&(o.filter=function(t){return n.options.filter(t)&&e.filter(t)});var s=[];return void 0!=t&&s.push(t),s.push(o),s.push(i),this.data&&this.data.get.apply(this.data,s)},o.prototype.getIds=function(t){var e;if(this.data){var i,n=this.options.filter;i=t&&t.filter?n?function(e){return n(e)&&t.filter(e)}:t.filter:n,e=this.data.getIds({filter:i,order:t&&t.order})}else e=[];return e},o.prototype._onEvent=function(t,e,i){var n,r,o,s,a=e&&e.items,h=this.data,p=[],u=[],c=[];if(a&&h){switch(t){case"add":for(n=0,r=a.length;r>n;n++)o=a[n],s=this.get(o),s&&(this.ids[o]=!0,p.push(o));break;case"update":for(n=0,r=a.length;r>n;n++)o=a[n],s=this.get(o),s?this.ids[o]?u.push(o):(this.ids[o]=!0,p.push(o)):this.ids[o]&&(delete this.ids[o],c.push(o));break;case"remove":for(n=0,r=a.length;r>n;n++)o=a[n],this.ids[o]&&(delete this.ids[o],c.push(o))}p.length&&this._trigger("add",{items:p},i),u.length&&this._trigger("update",{items:u},i),c.length&&this._trigger("remove",{items:c},i)}},o.prototype.subscribe=r.prototype.subscribe,o.prototype.unsubscribe=r.prototype.unsubscribe,o.prototype._trigger=r.prototype._trigger,s.prototype.setOptions=function(t){E.extend(this.options,t)},s.prototype.update=function(){this._order(),this._stack()},s.prototype._order=function(){var t=this.parent.items;if(!t)throw Error("Cannot stack items: parent does not contain items"); -var e=[],i=0;E.forEach(t,function(t){t.visible&&(e[i]=t,i++)});var n=this.options.order||this.defaultOptions.order;if("function"!=typeof n)throw Error("Option order must be a function");e.sort(n),this.ordered=e},s.prototype._stack=function(){var t,e,i,n=this.ordered,r=this.options,o=r.orientation||this.defaultOptions.orientation,s="top"==o;for(i=r.margin&&void 0!==r.margin.item?r.margin.item:this.defaultOptions.margin.item,t=0,e=n.length;e>t;t++){var a=n[t],h=null;do h=this.checkOverlap(n,t,0,t-1,i),null!=h&&(a.top=s?h.top+h.height+i:h.top-a.height-i);while(h)}},s.prototype.checkOverlap=function(t,e,i,n,r){for(var o=this.collision,s=t[e],a=n;a>=i;a--){var h=t[a];if(o(s,h,r)&&a!=e)return h}return null},s.prototype.collision=function(t,e,i){return t.left-ie.left&&t.top-ie.top},a.prototype.setOptions=function(t){E.extend(this.options,t),(null!=t.start||null!=t.end)&&this.setRange(t.start,t.end)},a.prototype.subscribe=function(t,e,i){var n,r=this;if("horizontal"!=i&&"vertical"!=i)throw new TypeError('Unknown direction "'+i+'". '+'Choose "horizontal" or "vertical".');if("move"==e)n={component:t,event:e,direction:i,callback:function(t){r._onMouseDown(t,n)},params:{}},t.on("mousedown",n.callback),r.listeners.push(n);else{if("zoom"!=e)throw new TypeError('Unknown event "'+e+'". '+'Choose "move" or "zoom".');n={component:t,event:e,direction:i,callback:function(t){r._onMouseWheel(t,n)},params:{}},t.on("mousewheel",n.callback),r.listeners.push(n)}},a.prototype.on=function(t,e){_.addListener(this,t,e)},a.prototype._trigger=function(t){_.trigger(this,t,{start:this.start,end:this.end})},a.prototype.setRange=function(t,e){var i=this._applyRange(t,e);i&&(this._trigger("rangechange"),this._trigger("rangechanged"))},a.prototype._applyRange=function(t,e){var i,n=null!=t?E.cast(t,"Number"):this.start,r=null!=e?E.cast(e,"Number"):this.end;if(isNaN(n))throw Error('Invalid start "'+t+'"');if(isNaN(r))throw Error('Invalid end "'+e+'"');if(n>r&&(r=n),null!=this.options.min){var o=this.options.min.valueOf();o>n&&(i=o-n,n+=i,r+=i)}if(null!=this.options.max){var s=this.options.max.valueOf();r>s&&(i=r-s,n-=i,r-=i)}if(null!=this.options.zoomMin){var a=this.options.zoomMin.valueOf();0>a&&(a=0),a>r-n&&(this.end-this.start>a?(i=a-(r-n),n-=i/2,r+=i/2):(n=this.start,r=this.end))}if(null!=this.options.zoomMax){var h=this.options.zoomMax.valueOf();0>h&&(h=0),r-n>h&&(h>this.end-this.start?(i=r-n-h,n+=i/2,r-=i/2):(n=this.start,r=this.end))}var p=this.start!=n||this.end!=r;return this.start=n,this.end=r,p},a.prototype.getRange=function(){return{start:this.start,end:this.end}},a.prototype.conversion=function(t){return this.start,this.end,a.conversion(this.start,this.end,t)},a.conversion=function(t,e,i){return 0!=i&&0!=e-t?{offset:t,factor:i/(e-t)}:{offset:0,factor:1}},a.prototype._onMouseDown=function(t,e){t=t||window.event;var i=e.params,n=t.which?1==t.which:1==t.button;if(n){i.mouseX=E.getPageX(t),i.mouseY=E.getPageY(t),i.previousLeft=0,i.previousOffset=0,i.moved=!1,i.start=this.start,i.end=this.end;var r=e.component.frame;r&&(r.style.cursor="move");var o=this;i.onMouseMove||(i.onMouseMove=function(t){o._onMouseMove(t,e)},E.addEventListener(document,"mousemove",i.onMouseMove)),i.onMouseUp||(i.onMouseUp=function(t){o._onMouseUp(t,e)},E.addEventListener(document,"mouseup",i.onMouseUp)),E.preventDefault(t)}},a.prototype._onMouseMove=function(t,e){t=t||window.event;var i=e.params,n=E.getPageX(t),r=E.getPageY(t);void 0==i.mouseX&&(i.mouseX=n),void 0==i.mouseY&&(i.mouseY=r);var o=n-i.mouseX,s=r-i.mouseY,a="horizontal"==e.direction?o:s;Math.abs(a)>=1&&(i.moved=!0);var h=i.end-i.start,p="horizontal"==e.direction?e.component.width:e.component.height,u=-a/p*h;this._applyRange(i.start+u,i.end+u),this._trigger("rangechange"),E.preventDefault(t)},a.prototype._onMouseUp=function(t,e){t=t||window.event;var i=e.params;e.component.frame&&(e.component.frame.style.cursor="auto"),i.onMouseMove&&(E.removeEventListener(document,"mousemove",i.onMouseMove),i.onMouseMove=null),i.onMouseUp&&(E.removeEventListener(document,"mouseup",i.onMouseUp),i.onMouseUp=null),i.moved&&this._trigger("rangechanged")},a.prototype._onMouseWheel=function(t,e){t=t||window.event;var i=0;if(t.wheelDelta?i=t.wheelDelta/120:t.detail&&(i=-t.detail/3),i){var n=this,r=function(){var r=i/5,o=null,s=e.component.frame;if(s){var a,h;if("horizontal"==e.direction){a=e.component.width,h=n.conversion(a);var p=E.getAbsoluteLeft(s),u=E.getPageX(t);o=(u-p)/h.factor+h.offset}else{a=e.component.height,h=n.conversion(a);var c=E.getAbsoluteTop(s),d=E.getPageY(t);o=(c+a-d-c)/h.factor+h.offset}}n.zoom(r,o)};r()}E.preventDefault(t)},a.prototype.zoom=function(t,e){null==e&&(e=(this.start+this.end)/2),t>=1&&(t=.9),-1>=t&&(t=-.9),0>t&&(t/=1+t);var i=this.start-e,n=this.end-e,r=this.start-i*t,o=this.end-n*t;this.setRange(r,o)},a.prototype.move=function(t){var e=this.end-this.start,i=this.start+e*t,n=this.end+e*t;this.start=i,this.end=n},h.prototype.on=function(t,e,i){var n=t instanceof RegExp?t:RegExp(t.replace("*","\\w+")),r={id:E.randomUUID(),event:t,regexp:n,callback:"function"==typeof e?e:null,target:i};return this.subscriptions.push(r),r.id},h.prototype.off=function(t){for(var e=0;this.subscriptions.length>e;){var i=this.subscriptions[e],n=!0;if(t instanceof Object)for(var r in t)t.hasOwnProperty(r)&&t[r]!==i[r]&&(n=!1);else n=i.id==t;n?this.subscriptions.splice(e,1):e++}},h.prototype.emit=function(t,e,i){for(var n=0;this.subscriptions.length>n;n++){var r=this.subscriptions[n];r.regexp.test(t)&&r.callback&&r.callback(t,e,i)}},p.prototype.add=function(t){if(void 0==t.id)throw Error("Component has no field id");if(!(t instanceof u||t instanceof p))throw new TypeError("Component must be an instance of prototype Component or Controller");t.controller=this,this.components[t.id]=t},p.prototype.remove=function(t){var e;for(e in this.components)if(this.components.hasOwnProperty(e)&&(e==t||this.components[e]==t))break;e&&delete this.components[e]},p.prototype.requestReflow=function(t){if(t)this.reflow();else if(!this.reflowTimer){var e=this;this.reflowTimer=setTimeout(function(){e.reflowTimer=void 0,e.reflow()},0)}},p.prototype.requestRepaint=function(t){if(t)this.repaint();else if(!this.repaintTimer){var e=this;this.repaintTimer=setTimeout(function(){e.repaintTimer=void 0,e.repaint()},0)}},p.prototype.repaint=function(){function t(n,r){r in i||(n.depends&&n.depends.forEach(function(e){t(e,e.id)}),n.parent&&t(n.parent,n.parent.id),e=n.repaint()||e,i[r]=!0)}var e=!1;this.repaintTimer&&(clearTimeout(this.repaintTimer),this.repaintTimer=void 0);var i={};E.forEach(this.components,t),e&&this.reflow()},p.prototype.reflow=function(){function t(n,r){r in i||(n.depends&&n.depends.forEach(function(e){t(e,e.id)}),n.parent&&t(n.parent,n.parent.id),e=n.reflow()||e,i[r]=!0)}var e=!1;this.reflowTimer&&(clearTimeout(this.reflowTimer),this.reflowTimer=void 0);var i={};E.forEach(this.components,t),e&&this.repaint()},u.prototype.setOptions=function(t){t&&(E.extend(this.options,t),this.controller&&(this.requestRepaint(),this.requestReflow()))},u.prototype.getOption=function(t){var e;return this.options&&(e=this.options[t]),void 0===e&&this.defaultOptions&&(e=this.defaultOptions[t]),e},u.prototype.getContainer=function(){return null},u.prototype.getFrame=function(){return this.frame},u.prototype.repaint=function(){return!1},u.prototype.reflow=function(){return!1},u.prototype.hide=function(){return this.frame&&this.frame.parentNode?(this.frame.parentNode.removeChild(this.frame),!0):!1},u.prototype.show=function(){return this.frame&&this.frame.parentNode?!1:this.repaint()},u.prototype.requestRepaint=function(){if(!this.controller)throw Error("Cannot request a repaint: no controller configured");this.controller.requestRepaint()},u.prototype.requestReflow=function(){if(!this.controller)throw Error("Cannot request a reflow: no controller configured");this.controller.requestReflow()},c.prototype=new u,c.prototype.setOptions=u.prototype.setOptions,c.prototype.getContainer=function(){return this.frame},c.prototype.repaint=function(){var t=0,e=E.updateProperty,i=E.option.asSize,n=this.options,r=this.frame;if(!r){r=document.createElement("div"),r.className="panel";var o=n.className;o&&("function"==typeof o?E.addClassName(r,o()+""):E.addClassName(r,o+"")),this.frame=r,t+=1}if(!r.parentNode){if(!this.parent)throw Error("Cannot repaint panel: no parent attached");var s=this.parent.getContainer();if(!s)throw Error("Cannot repaint panel: parent has no container element");s.appendChild(r),t+=1}return t+=e(r.style,"top",i(n.top,"0px")),t+=e(r.style,"left",i(n.left,"0px")),t+=e(r.style,"width",i(n.width,"100%")),t+=e(r.style,"height",i(n.height,"100%")),t>0},c.prototype.reflow=function(){var t=0,e=E.updateProperty,i=this.frame;return i?(t+=e(this,"top",i.offsetTop),t+=e(this,"left",i.offsetLeft),t+=e(this,"width",i.offsetWidth),t+=e(this,"height",i.offsetHeight)):t+=1,t>0},d.prototype=new c,d.prototype.setOptions=u.prototype.setOptions,d.prototype.repaint=function(){var t=0,e=E.updateProperty,i=E.option.asSize,n=this.options,r=this.frame;if(!r){r=document.createElement("div"),r.className="graph panel";var o=n.className;o&&E.addClassName(r,E.option.asString(o)),this.frame=r,t+=1}if(!r.parentNode){if(!this.container)throw Error("Cannot repaint root panel: no container attached");this.container.appendChild(r),t+=1}return t+=e(r.style,"top",i(n.top,"0px")),t+=e(r.style,"left",i(n.left,"0px")),t+=e(r.style,"width",i(n.width,"100%")),t+=e(r.style,"height",i(n.height,"100%")),this._updateEventEmitters(),this._updateWatch(),t>0},d.prototype.reflow=function(){var t=0,e=E.updateProperty,i=this.frame;return i?(t+=e(this,"top",i.offsetTop),t+=e(this,"left",i.offsetLeft),t+=e(this,"width",i.offsetWidth),t+=e(this,"height",i.offsetHeight)):t+=1,t>0},d.prototype._updateWatch=function(){var t=this.getOption("autoResize");t?this._watch():this._unwatch()},d.prototype._watch=function(){var t=this;this._unwatch();var e=function(){var e=t.getOption("autoResize");return e?(t.frame&&(t.frame.clientWidth!=t.width||t.frame.clientHeight!=t.height)&&t.requestReflow(),void 0):(t._unwatch(),void 0)};E.addEventListener(window,"resize",e),this.watchTimer=setInterval(e,1e3)},d.prototype._unwatch=function(){this.watchTimer&&(clearInterval(this.watchTimer),this.watchTimer=void 0)},d.prototype.on=function(t,e){var i=this.listeners[t];i||(i=[],this.listeners[t]=i),i.push(e),this._updateEventEmitters()},d.prototype._updateEventEmitters=function(){if(this.listeners){var t=this;E.forEach(this.listeners,function(e,i){if(t.emitters||(t.emitters={}),!(i in t.emitters)){var n=t.frame;if(n){var r=function(t){e.forEach(function(e){e(t)})};t.emitters[i]=r,E.addEventListener(n,i,r)}}})}},l.prototype=new u,l.prototype.setOptions=u.prototype.setOptions,l.prototype.setRange=function(t){if(!(t instanceof a||t&&t.start&&t.end))throw new TypeError("Range must be an instance of Range, or an object containing start and end.");this.range=t},l.prototype.toTime=function(t){var e=this.conversion;return new Date(t/e.factor+e.offset)},l.prototype.toScreen=function(t){var e=this.conversion;return(t.valueOf()-e.offset)*e.factor},l.prototype.repaint=function(){var t=0,e=E.updateProperty,i=E.option.asSize,n=this.options,r=this.getOption("orientation"),o=this.props,s=this.step,a=this.frame;if(a||(a=document.createElement("div"),this.frame=a,t+=1),a.className="axis "+r,!a.parentNode){if(!this.parent)throw Error("Cannot repaint time axis: no parent attached");var h=this.parent.getContainer();if(!h)throw Error("Cannot repaint time axis: parent has no container element");h.appendChild(a),t+=1}var p=a.parentNode;if(p){var u=a.nextSibling;p.removeChild(a);var c="bottom"==r&&this.props.parentHeight&&this.height?this.props.parentHeight-this.height+"px":"0px";if(t+=e(a.style,"top",i(n.top,c)),t+=e(a.style,"left",i(n.left,"0px")),t+=e(a.style,"width",i(n.width,"100%")),t+=e(a.style,"height",i(n.height,this.height+"px")),this._repaintMeasureChars(),this.step){this._repaintStart(),s.first();for(var d=void 0,l=0;s.hasNext()&&1e3>l;){l++;var f=s.getCurrent(),m=this.toScreen(f),g=s.isMajor();this.getOption("showMinorLabels")&&this._repaintMinorText(m,s.getLabelMinor()),g&&this.getOption("showMajorLabels")?(m>0&&(void 0==d&&(d=m),this._repaintMajorText(m,s.getLabelMajor())),this._repaintMajorLine(m)):this._repaintMinorLine(m),s.next()}if(this.getOption("showMajorLabels")){var v=this.toTime(0),y=s.getLabelMajor(v),w=y.length*(o.majorCharWidth||10)+10;(void 0==d||d>w)&&this._repaintMajorText(0,y)}this._repaintEnd()}this._repaintLine(),u?p.insertBefore(a,u):p.appendChild(a)}return t>0},l.prototype._repaintStart=function(){var t=this.dom,e=t.redundant;e.majorLines=t.majorLines,e.majorTexts=t.majorTexts,e.minorLines=t.minorLines,e.minorTexts=t.minorTexts,t.majorLines=[],t.majorTexts=[],t.minorLines=[],t.minorTexts=[]},l.prototype._repaintEnd=function(){E.forEach(this.dom.redundant,function(t){for(;t.length;){var e=t.pop();e&&e.parentNode&&e.parentNode.removeChild(e)}})},l.prototype._repaintMinorText=function(t,e){var i=this.dom.redundant.minorTexts.shift();if(!i){var n=document.createTextNode("");i=document.createElement("div"),i.appendChild(n),i.className="text minor",this.frame.appendChild(i)}this.dom.minorTexts.push(i),i.childNodes[0].nodeValue=e,i.style.left=t+"px",i.style.top=this.props.minorLabelTop+"px"},l.prototype._repaintMajorText=function(t,e){var i=this.dom.redundant.majorTexts.shift();if(!i){var n=document.createTextNode(e);i=document.createElement("div"),i.className="text major",i.appendChild(n),this.frame.appendChild(i)}this.dom.majorTexts.push(i),i.childNodes[0].nodeValue=e,i.style.top=this.props.majorLabelTop+"px",i.style.left=t+"px"},l.prototype._repaintMinorLine=function(t){var e=this.dom.redundant.minorLines.shift();e||(e=document.createElement("div"),e.className="grid vertical minor",this.frame.appendChild(e)),this.dom.minorLines.push(e);var i=this.props;e.style.top=i.minorLineTop+"px",e.style.height=i.minorLineHeight+"px",e.style.left=t-i.minorLineWidth/2+"px"},l.prototype._repaintMajorLine=function(t){var e=this.dom.redundant.majorLines.shift();e||(e=document.createElement("DIV"),e.className="grid vertical major",this.frame.appendChild(e)),this.dom.majorLines.push(e);var i=this.props;e.style.top=i.majorLineTop+"px",e.style.left=t-i.majorLineWidth/2+"px",e.style.height=i.majorLineHeight+"px"},l.prototype._repaintLine=function(){var t=this.dom.line,e=this.frame;this.options,this.getOption("showMinorLabels")||this.getOption("showMajorLabels")?(t?(e.removeChild(t),e.appendChild(t)):(t=document.createElement("div"),t.className="grid horizontal major",e.appendChild(t),this.dom.line=t),t.style.top=this.props.lineTop+"px"):t&&axis.parentElement&&(e.removeChild(axis.line),delete this.dom.line)},l.prototype._repaintMeasureChars=function(){var t,e=this.dom;if(!e.measureCharMinor){t=document.createTextNode("0");var i=document.createElement("DIV");i.className="text minor measure",i.appendChild(t),this.frame.appendChild(i),e.measureCharMinor=i}if(!e.measureCharMajor){t=document.createTextNode("0");var n=document.createElement("DIV");n.className="text major measure",n.appendChild(t),this.frame.appendChild(n),e.measureCharMajor=n}},l.prototype.reflow=function(){var t=0,e=E.updateProperty,i=this.frame,n=this.range;if(!n)throw Error("Cannot repaint time axis: no range configured");if(i){t+=e(this,"top",i.offsetTop),t+=e(this,"left",i.offsetLeft);var r=this.props,o=this.getOption("showMinorLabels"),s=this.getOption("showMajorLabels"),a=this.dom.measureCharMinor,h=this.dom.measureCharMajor;a&&(r.minorCharHeight=a.clientHeight,r.minorCharWidth=a.clientWidth),h&&(r.majorCharHeight=h.clientHeight,r.majorCharWidth=h.clientWidth);var p=i.parentNode?i.parentNode.offsetHeight:0;switch(p!=r.parentHeight&&(r.parentHeight=p,t+=1),this.getOption("orientation")){case"bottom":r.minorLabelHeight=o?r.minorCharHeight:0,r.majorLabelHeight=s?r.majorCharHeight:0,r.minorLabelTop=0,r.majorLabelTop=r.minorLabelTop+r.minorLabelHeight,r.minorLineTop=-this.top,r.minorLineHeight=Math.max(this.top+r.majorLabelHeight,0),r.minorLineWidth=1,r.majorLineTop=-this.top,r.majorLineHeight=Math.max(this.top+r.minorLabelHeight+r.majorLabelHeight,0),r.majorLineWidth=1,r.lineTop=0;break;case"top":r.minorLabelHeight=o?r.minorCharHeight:0,r.majorLabelHeight=s?r.majorCharHeight:0,r.majorLabelTop=0,r.minorLabelTop=r.majorLabelTop+r.majorLabelHeight,r.minorLineTop=r.minorLabelTop,r.minorLineHeight=Math.max(p-r.majorLabelHeight-this.top),r.minorLineWidth=1,r.majorLineTop=0,r.majorLineHeight=Math.max(p-this.top),r.majorLineWidth=1,r.lineTop=r.majorLabelHeight+r.minorLabelHeight;break;default:throw Error('Unkown orientation "'+this.getOption("orientation")+'"')}var u=r.minorLabelHeight+r.majorLabelHeight;t+=e(this,"width",i.offsetWidth),t+=e(this,"height",u),this._updateConversion();var c=E.cast(n.start,"Date"),d=E.cast(n.end,"Date"),l=this.toTime(5*(r.minorCharWidth||10))-this.toTime(0);this.step=new TimeStep(c,d,l),t+=e(r.range,"start",c.valueOf()),t+=e(r.range,"end",d.valueOf()),t+=e(r.range,"minimumStep",l.valueOf())}return t>0},l.prototype._updateConversion=function(){var t=this.range;if(!t)throw Error("No range configured");this.conversion=t.conversion?t.conversion(this.width):a.conversion(t.start,t.end,this.width)},f.prototype=new c,f.types={box:g,range:y,point:v},f.prototype.setOptions=u.prototype.setOptions,f.prototype.setRange=function(t){if(!(t instanceof a||t&&t.start&&t.end))throw new TypeError("Range must be an instance of Range, or an object containing start and end.");this.range=t},f.prototype.repaint=function(){var t=0,e=E.updateProperty,i=E.option.asSize,n=this.options,r=this.getOption("orientation"),o=this.defaultOptions,s=this.frame;if(!s){s=document.createElement("div"),s.className="itemset";var a=n.className;a&&E.addClassName(s,E.option.asString(a));var h=document.createElement("div");h.className="background",s.appendChild(h),this.dom.background=h;var p=document.createElement("div");p.className="foreground",s.appendChild(p),this.dom.foreground=p;var u=document.createElement("div");u.className="itemset-axis",this.dom.axis=u,this.frame=s,t+=1}if(!this.parent)throw Error("Cannot repaint itemset: no parent attached");var c=this.parent.getContainer();if(!c)throw Error("Cannot repaint itemset: parent has no container element");s.parentNode||(c.appendChild(s),t+=1),this.dom.axis.parentNode||(c.appendChild(this.dom.axis),t+=1),t+=e(s.style,"left",i(n.left,"0px")),t+=e(s.style,"top",i(n.top,"0px")),t+=e(s.style,"width",i(n.width,"100%")),t+=e(s.style,"height",i(n.height,this.height+"px")),t+=e(this.dom.axis.style,"left",i(n.left,"0px")),t+=e(this.dom.axis.style,"width",i(n.width,"100%")),t+="bottom"==r?e(this.dom.axis.style,"top",this.height+this.top+"px"):e(this.dom.axis.style,"top",this.top+"px"),this._updateConversion();var d=this,l=this.queue,m=this.itemsData,g=this.items,v={fields:[m&&m.fieldId||"id","start","end","content","type"]};return Object.keys(l).forEach(function(e){var i=l[e],r=g[e];switch(i){case"add":case"update":var s=m&&m.get(e,v);if(s){var a=s.type||s.start&&s.end&&"range"||"box",h=f.types[a];if(r&&(h&&r instanceof h?(r.data=s,t++):(t+=r.hide(),r=null)),!r){if(!h)throw new TypeError('Unknown item type "'+a+'"');r=new h(d,s,n,o),t++}r.repaint(),g[e]=r}delete l[e];break;case"remove":r&&(t+=r.hide()),delete g[e],delete l[e];break;default:console.log('Error: unknown action "'+i+'"')}}),E.forEach(this.items,function(e){e.visible?(t+=e.show(),e.reposition()):t+=e.hide()}),t>0},f.prototype.getForeground=function(){return this.dom.foreground},f.prototype.getBackground=function(){return this.dom.background},f.prototype.getAxis=function(){return this.dom.axis},f.prototype.reflow=function(){var t=0,e=this.options,i=e.margin&&e.margin.axis||this.defaultOptions.margin.axis,n=e.margin&&e.margin.item||this.defaultOptions.margin.item,r=E.updateProperty,o=E.option.asNumber,s=E.option.asSize,a=this.frame;if(a){this._updateConversion(),E.forEach(this.items,function(e){t+=e.reflow()}),this.stack.update();var h,p=o(e.maxHeight),u=null!=s(e.height);if(u)h=a.offsetHeight;else{var c=this.stack.ordered;if(c.length){var d=c[0].top,l=c[0].top+c[0].height;E.forEach(c,function(t){d=Math.min(d,t.top),l=Math.max(l,t.top+t.height)}),h=l-d+i+n}else h=i+n}null!=p&&(h=Math.min(h,p)),t+=r(this,"height",h),t+=r(this,"top",a.offsetTop),t+=r(this,"left",a.offsetLeft),t+=r(this,"width",a.offsetWidth)}else t+=1;return t>0},f.prototype.hide=function(){var t=!1;return this.frame&&this.frame.parentNode&&(this.frame.parentNode.removeChild(this.frame),t=!0),this.dom.axis&&this.dom.axis.parentNode&&(this.dom.axis.parentNode.removeChild(this.dom.axis),t=!0),t},f.prototype.setItems=function(t){var e,i=this,n=this.itemsData;if(n&&(E.forEach(this.listeners,function(t,e){n.unsubscribe(e,t)}),e=n.getIds(),this._onRemove(e)),t){if(!(t instanceof r||t instanceof o))throw new TypeError("Data must be an instance of DataSet");this.itemsData=t}else this.itemsData=null;if(this.itemsData){var s=this.id;E.forEach(this.listeners,function(t,e){i.itemsData.subscribe(e,t,s)}),e=this.itemsData.getIds(),this._onAdd(e)}},f.prototype.getItems=function(){return this.itemsData},f.prototype._onUpdate=function(t){this._toQueue("update",t)},f.prototype._onAdd=function(t){this._toQueue("add",t)},f.prototype._onRemove=function(t){this._toQueue("remove",t)},f.prototype._toQueue=function(t,e){var i=this.queue;e.forEach(function(e){i[e]=t}),this.controller&&this.requestRepaint()},f.prototype._updateConversion=function(){var t=this.range;if(!t)throw Error("No range configured");this.conversion=t.conversion?t.conversion(this.width):a.conversion(t.start,t.end,this.width)},f.prototype.toTime=function(t){var e=this.conversion;return new Date(t/e.factor+e.offset)},f.prototype.toScreen=function(t){var e=this.conversion;return(t.valueOf()-e.offset)*e.factor},m.prototype.select=function(){this.selected=!0},m.prototype.unselect=function(){this.selected=!1},m.prototype.show=function(){return!1},m.prototype.hide=function(){return!1},m.prototype.repaint=function(){return!1},m.prototype.reflow=function(){return!1},g.prototype=new m(null,null),g.prototype.select=function(){this.selected=!0},g.prototype.unselect=function(){this.selected=!1},g.prototype.repaint=function(){var t=!1,e=this.dom;if(e||(this._create(),e=this.dom,t=!0),e){if(!this.parent)throw Error("Cannot repaint item: no parent attached");var i=this.parent.getForeground();if(!i)throw Error("Cannot repaint time axis: parent has no foreground container element");var n=this.parent.getBackground();if(!n)throw Error("Cannot repaint time axis: parent has no background container element");var r=this.parent.getAxis();if(!n)throw Error("Cannot repaint time axis: parent has no axis container element");if(e.box.parentNode||(i.appendChild(e.box),t=!0),e.line.parentNode||(n.appendChild(e.line),t=!0),e.dot.parentNode||(r.appendChild(e.dot),t=!0),this.data.content!=this.content){if(this.content=this.data.content,this.content instanceof Element)e.content.innerHTML="",e.content.appendChild(this.content);else{if(void 0==this.data.content)throw Error('Property "content" missing in item '+this.data.id);e.content.innerHTML=this.content}t=!0}var o=(this.data.className?" "+this.data.className:"")+(this.selected?" selected":"");this.className!=o&&(this.className=o,e.box.className="item box"+o,e.line.className="item line"+o,e.dot.className="item dot"+o,t=!0)}return t},g.prototype.show=function(){return this.dom&&this.dom.box.parentNode?!1:this.repaint()},g.prototype.hide=function(){var t=!1,e=this.dom;return e&&(e.box.parentNode&&(e.box.parentNode.removeChild(e.box),t=!0),e.line.parentNode&&e.line.parentNode.removeChild(e.line),e.dot.parentNode&&e.dot.parentNode.removeChild(e.dot)),t},g.prototype.reflow=function(){var t,e,i,n,r,o,s,a,h,p,u,c,d=0;if(void 0==this.data.start)throw Error('Property "start" missing in item '+this.data.id);if(u=this.data,c=this.parent&&this.parent.range,this.visible=u&&c?u.start>c.start&&u.start0},g.prototype._create=function(){var t=this.dom;t||(this.dom=t={},t.box=document.createElement("DIV"),t.content=document.createElement("DIV"),t.content.className="content",t.box.appendChild(t.content),t.line=document.createElement("DIV"),t.line.className="line",t.dot=document.createElement("DIV"),t.dot.className="dot")},g.prototype.reposition=function(){var t=this.dom,e=this.props,i=this.options.orientation||this.defaultOptions.orientation;if(t){var n=t.box,r=t.line,o=t.dot;n.style.left=this.left+"px",n.style.top=this.top+"px",r.style.left=e.line.left+"px","top"==i?(r.style.top="0px",r.style.height=this.top+"px"):(r.style.top=this.top+this.height+"px",r.style.height=Math.max(this.parent.height-this.top-this.height+this.props.dot.height/2,0)+"px"),o.style.left=e.dot.left+"px",o.style.top=e.dot.top+"px"}},v.prototype=new m(null,null),v.prototype.select=function(){this.selected=!0},v.prototype.unselect=function(){this.selected=!1},v.prototype.repaint=function(){var t=!1,e=this.dom;if(e||(this._create(),e=this.dom,t=!0),e){if(!this.parent)throw Error("Cannot repaint item: no parent attached");var i=this.parent.getForeground();if(!i)throw Error("Cannot repaint time axis: parent has no foreground container element");if(e.point.parentNode||(i.appendChild(e.point),i.appendChild(e.point),t=!0),this.data.content!=this.content){if(this.content=this.data.content,this.content instanceof Element)e.content.innerHTML="",e.content.appendChild(this.content);else{if(void 0==this.data.content)throw Error('Property "content" missing in item '+this.data.id);e.content.innerHTML=this.content}t=!0}var n=(this.data.className?" "+this.data.className:"")+(this.selected?" selected":"");this.className!=n&&(this.className=n,e.point.className="item point"+n,t=!0)}return t},v.prototype.show=function(){return this.dom&&this.dom.point.parentNode?!1:this.repaint()},v.prototype.hide=function(){var t=!1,e=this.dom;return e&&e.point.parentNode&&(e.point.parentNode.removeChild(e.point),t=!0),t},v.prototype.reflow=function(){var t,e,i,n,r,o,s,a,h,p,u=0;if(void 0==this.data.start)throw Error('Property "start" missing in item '+this.data.id);if(h=this.data,p=this.parent&&this.parent.range,this.visible=h&&p?h.start>p.start&&h.start0},v.prototype._create=function(){var t=this.dom;t||(this.dom=t={},t.point=document.createElement("div"),t.content=document.createElement("div"),t.content.className="content",t.point.appendChild(t.content),t.dot=document.createElement("div"),t.dot.className="dot",t.point.appendChild(t.dot))},v.prototype.reposition=function(){var t=this.dom,e=this.props;t&&(t.point.style.top=this.top+"px",t.point.style.left=this.left+"px",t.content.style.marginLeft=e.content.marginLeft+"px",t.dot.style.top=e.dot.top+"px")},y.prototype=new m(null,null),y.prototype.select=function(){this.selected=!0},y.prototype.unselect=function(){this.selected=!1},y.prototype.repaint=function(){var t=!1,e=this.dom;if(e||(this._create(),e=this.dom,t=!0),e){if(!this.parent)throw Error("Cannot repaint item: no parent attached");var i=this.parent.getForeground();if(!i)throw Error("Cannot repaint time axis: parent has no foreground container element");if(e.box.parentNode||(i.appendChild(e.box),t=!0),this.data.content!=this.content){if(this.content=this.data.content,this.content instanceof Element)e.content.innerHTML="",e.content.appendChild(this.content);else{if(void 0==this.data.content)throw Error('Property "content" missing in item '+this.data.id);e.content.innerHTML=this.content}t=!0}var n=this.data.className?""+this.data.className:"";this.className!=n&&(this.className=n,e.box.className="item range"+n,t=!0)}return t},y.prototype.show=function(){return this.dom&&this.dom.box.parentNode?!1:this.repaint()},y.prototype.hide=function(){var t=!1,e=this.dom;return e&&e.box.parentNode&&(e.box.parentNode.removeChild(e.box),t=!0),t},y.prototype.reflow=function(){var t,e,i,n,r,o,s,a,h,p,u,c,d,l,f,m,g=0;if(void 0==this.data.start)throw Error('Property "start" missing in item '+this.data.id);if(void 0==this.data.end)throw Error('Property "end" missing in item '+this.data.id);return h=this.data,p=this.parent&&this.parent.range,this.visible=h&&p?h.startp.start:!1,this.visible&&(t=this.dom,t?(e=this.props,i=this.options,o=this.parent,s=o.toScreen(this.data.start),a=o.toScreen(this.data.end),u=E.updateProperty,c=t.box,d=o.width,f=i.orientation||this.defaultOptions.orientation,n=i.margin&&i.margin.axis||this.defaultOptions.margin.axis,r=i.padding||this.defaultOptions.padding,g+=u(e.content,"width",t.content.offsetWidth),g+=u(this,"height",c.offsetHeight),-d>s&&(s=-d),a>2*d&&(a=2*d),l=0>s?Math.min(-s,a-s-e.content.width-2*r):0,g+=u(e.content,"left",l),"top"==f?(m=n,g+=u(this,"top",m)):(m=o.height-this.height-n,g+=u(this,"top",m)),g+=u(this,"left",s),g+=u(this,"width",Math.max(a-s,1))):g+=1),g>0},y.prototype._create=function(){var t=this.dom;t||(this.dom=t={},t.box=document.createElement("div"),t.content=document.createElement("div"),t.content.className="content",t.box.appendChild(t.content))},y.prototype.reposition=function(){var t=this.dom,e=this.props;t&&(t.box.style.top=this.top+"px",t.box.style.left=this.left+"px",t.box.style.width=this.width+"px",t.content.style.left=e.content.left+"px")},w.prototype=new u,w.prototype.setOptions=u.prototype.setOptions,w.prototype.getContainer=function(){return this.parent.getContainer()},w.prototype.setItems=function(t){if(this.itemset&&(this.itemset.hide(),this.itemset.setItems(),this.parent.controller.remove(this.itemset),this.itemset=null),t){var e=this.groupId,i=Object.create(this.options);this.itemset=new f(this,null,i),this.itemset.setRange(this.parent.range),this.view=new o(t,{filter:function(t){return t.group==e}}),this.itemset.setItems(this.view),this.parent.controller.add(this.itemset)}},w.prototype.repaint=function(){return!1},w.prototype.reflow=function(){var t=0,e=E.updateProperty;return t+=e(this,"top",this.itemset?this.itemset.top:0),t+=e(this,"height",this.itemset?this.itemset.height:0),t>0},b.prototype=new c,b.prototype.setOptions=u.prototype.setOptions,b.prototype.setRange=function(){},b.prototype.setItems=function(t){this.itemsData=t;for(var e in this.groups)if(this.groups.hasOwnProperty(e)){var i=this.groups[e];i.setItems(t)}},b.prototype.getItems=function(){return this.itemsData},b.prototype.setRange=function(t){this.range=t},b.prototype.setGroups=function(t){var e,i=this;if(this.groupsData&&(E.forEach(this.listeners,function(t,e){i.groupsData.unsubscribe(e,t) -}),e=this.groupsData.getIds(),this._onRemove(e)),t?t instanceof r?this.groupsData=t:(this.groupsData=new r({fieldTypes:{start:"Date",end:"Date"}}),this.groupsData.add(t)):this.groupsData=null,this.groupsData){var n=this.id;E.forEach(this.listeners,function(t,e){i.groupsData.subscribe(e,t,n)}),e=this.groupsData.getIds(),this._onAdd(e)}},b.prototype.getGroups=function(){return this.groupsData},b.prototype.repaint=function(){var t=0,e=E.updateProperty,i=E.option.asSize,n=this.options,r=this.frame;if(!r){r=document.createElement("div"),r.className="groupset";var o=n.className;o&&E.addClassName(r,E.option.asString(o)),this.frame=r,t+=1}if(!this.parent)throw Error("Cannot repaint groupset: no parent attached");var s=this.parent.getContainer();if(!s)throw Error("Cannot repaint groupset: parent has no container element");r.parentNode||(s.appendChild(r),t+=1),t+=e(r.style,"height",i(n.height,this.height+"px")),t+=e(r.style,"top",i(n.top,"0px")),t+=e(r.style,"left",i(n.left,"0px")),t+=e(r.style,"width",i(n.width,"100%"));var a=this,h=this.queue,p=this.groups,u=this.groupsData,c=Object.keys(h);if(c.length){c.forEach(function(t){var e=h[t],i=p[t];switch(e){case"add":case"update":if(!i){var n=Object.create(a.options);i=new w(a,t,n),i.setItems(a.itemsData),p[t]=i,a.controller.add(i)}i.data=u.get(t),delete h[t];break;case"remove":i&&(i.setItems(),delete p[t],a.controller.remove(i)),delete h[t];break;default:console.log('Error: unknown action "'+e+'"')}});for(var d=this.groupsData.getIds({order:this.options.groupsOrder}),l=0;d.length>l;l++)(function(t,e){var i=0;e&&(i=function(){return e.top+e.height}),t.setOptions({top:i})})(p[d[l]],p[d[l-1]]);t++}return t>0},b.prototype.getContainer=function(){return this.frame},b.prototype.reflow=function(){var t=0,e=this.options,i=E.updateProperty,n=E.option.asNumber,r=E.option.asSize,o=this.frame;if(o){var s,a=n(e.maxHeight),h=null!=r(e.height);if(h)s=o.offsetHeight;else{s=0;for(var p in this.groups)if(this.groups.hasOwnProperty(p)){var u=this.groups[p];s+=u.height}}null!=a&&(s=Math.min(s,a)),t+=i(this,"height",s),t+=i(this,"top",o.offsetTop),t+=i(this,"left",o.offsetLeft),t+=i(this,"width",o.offsetWidth)}return t>0},b.prototype.hide=function(){return this.frame&&this.frame.parentNode?(this.frame.parentNode.removeChild(this.frame),!0):!1},b.prototype.show=function(){return this.frame&&this.frame.parentNode?!1:this.repaint()},b.prototype._onUpdate=function(t){this._toQueue(t,"update")},b.prototype._onAdd=function(t){this._toQueue(t,"add")},b.prototype._onRemove=function(t){this._toQueue(t,"remove")},b.prototype._toQueue=function(t,e){var i=this.queue;t.forEach(function(t){i[t]=e}),this.controller&&this.requestRepaint()},S.prototype.setOptions=function(t){t&&E.extend(this.options,t),this.controller.reflow(),this.controller.repaint()},S.prototype.setItems=function(t){var e,i=null==this.itemsData;if(t?t instanceof r&&(e=t):e=null,t instanceof r||(e=new r({fieldTypes:{start:"Date",end:"Date"}}),e.add(t)),this.itemsData=e,this.content.setItems(e),i&&(void 0==this.options.start||void 0==this.options.end)){var n=this.getItemRange(),o=n.min,s=n.max;if(null!=o&&null!=s){var a=s.valueOf()-o.valueOf();o=new Date(o.valueOf()-.05*a),s=new Date(s.valueOf()+.05*a)}void 0!=this.options.start&&(o=new Date(this.options.start.valueOf())),void 0!=this.options.end&&(s=new Date(this.options.end.valueOf())),(null!=o||null!=s)&&this.range.setRange(o,s)}},S.prototype.setGroups=function(t){var e=this;this.groupsData=t;var i=this.groupsData?b:f;if(!(this.content instanceof i)){this.content&&(this.content.hide(),this.content.setItems&&this.content.setItems(),this.content.setGroups&&this.content.setGroups(),this.controller.remove(this.content));var n=Object.create(this.options);E.extend(n,{top:function(){return"top"==e.options.orientation?e.timeaxis.height:e.root.height-e.timeaxis.height-e.content.height},height:function(){return e.options.height?e.root.height-e.timeaxis.height:null},maxHeight:function(){if(e.options.maxHeight){if(!E.isNumber(e.options.maxHeight))throw new TypeError("Number expected for property maxHeight");return e.options.maxHeight-e.timeaxis.height}return null}}),this.content=new i(this.root,[this.timeaxis],n),this.content.setRange&&this.content.setRange(this.range),this.content.setItems&&this.content.setItems(this.itemsData),this.content.setGroups&&this.content.setGroups(this.groupsData),this.controller.add(this.content)}},S.prototype.getItemRange=function(){var t=this.itemsData,e=null,i=null;if(t){var n=t.min("start");e=n?n.start.valueOf():null;var r=t.max("start");r&&(i=r.start.valueOf());var o=t.max("end");o&&(i=null==i?o.end.valueOf():Math.max(i,o.end.valueOf()))}return{min:null!=e?new Date(e):null,max:null!=i?new Date(i):null}};var C={util:E,events:_,Controller:p,DataSet:r,DataView:o,Range:a,Stack:s,TimeStep:TimeStep,EventBus:h,components:{items:{Item:m,ItemBox:g,ItemPoint:v,ItemRange:y},Component:u,Panel:c,RootPanel:d,ItemSet:f,TimeAxis:l},Timeline:S};n!==void 0&&(n=C),i!==void 0&&i.exports!==void 0&&(i.exports=C),"function"==typeof t&&t(function(){return C}),"undefined"!=typeof window&&(window.vis=C),E.loadCss("/* vis.js stylesheet */\n\n.graph {\n position: relative;\n border: 1px solid #bfbfbf;\n}\n\n.graph .panel {\n position: absolute;\n}\n\n.graph .groupset {\n position: absolute;\n padding: 0;\n margin: 0;\n}\n\n\n.graph .itemset {\n position: absolute;\n padding: 0;\n margin: 0;\n overflow: hidden;\n}\n\n.graph .background {\n}\n\n.graph .foreground {\n}\n\n.graph .itemset-axis {\n position: absolute;\n}\n\n.graph .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.graph .groupset .itemset-axis:last-child {\n border-top: none;\n}\n*/\n\n\n.graph .item {\n position: absolute;\n color: #1A1A1A;\n border-color: #97B0F8;\n background-color: #D5DDF6;\n display: inline-block;\n}\n\n.graph .item.selected {\n border-color: #FFC200;\n background-color: #FFF785;\n z-index: 999;\n}\n\n.graph .item.cluster {\n /* TODO: use another color or pattern? */\n background: #97B0F8 url('img/cluster_bg.png');\n color: white;\n}\n.graph .item.cluster.point {\n border-color: #D5DDF6;\n}\n\n.graph .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.graph .item.point {\n background: none;\n}\n\n.graph .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.graph .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.graph .item.range .drag-left {\n cursor: w-resize;\n z-index: 1000;\n}\n\n.graph .item.range .drag-right {\n cursor: e-resize;\n z-index: 1000;\n}\n\n.graph .item.range .content {\n position: relative;\n display: inline-block;\n}\n\n.graph .item.line {\n position: absolute;\n width: 0;\n border-left-width: 1px;\n border-left-style: solid;\n}\n\n.graph .item .content {\n margin: 5px;\n white-space: nowrap;\n overflow: hidden;\n}\n\n/* TODO: better css name, 'graph' is way to generic */\n\n.graph {\n overflow: hidden;\n}\n\n.graph .axis {\n position: relative;\n}\n\n.graph .axis .text {\n position: absolute;\n color: #4d4d4d;\n padding: 3px;\n white-space: nowrap;\n}\n\n.graph .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.graph .axis .grid.vertical {\n position: absolute;\n width: 0;\n border-right: 1px solid;\n}\n\n.graph .axis .grid.horizontal {\n position: absolute;\n left: 0;\n width: 100%;\n height: 0;\n border-bottom: 1px solid;\n}\n\n.graph .axis .grid.minor {\n border-color: #e5e5e5;\n}\n\n.graph .axis .grid.major {\n border-color: #bfbfbf;\n}\n\n")},{moment:2}],2:[function(e,i){(function(){(function(n){function r(t,e){return function(i){return c(t.call(this,i),e)}}function o(t){return function(e){return this.lang().ordinal(t.call(this,e))}}function s(){}function a(t){p(this,t)}function h(t){var e=this._data={},i=t.years||t.year||t.y||0,n=t.months||t.month||t.M||0,r=t.weeks||t.week||t.w||0,o=t.days||t.day||t.d||0,s=t.hours||t.hour||t.h||0,a=t.minutes||t.minute||t.m||0,h=t.seconds||t.second||t.s||0,p=t.milliseconds||t.millisecond||t.ms||0;this._milliseconds=p+1e3*h+6e4*a+36e5*s,this._days=o+7*r,this._months=n+12*i,e.milliseconds=p%1e3,h+=u(p/1e3),e.seconds=h%60,a+=u(h/60),e.minutes=a%60,s+=u(a/60),e.hours=s%24,o+=u(s/24),o+=7*r,e.days=o%30,n+=u(o/30),e.months=n%12,i+=u(n/12),e.years=i}function p(t,e){for(var i in e)e.hasOwnProperty(i)&&(t[i]=e[i]);return t}function u(t){return 0>t?Math.ceil(t):Math.floor(t)}function c(t,e){for(var i=t+"";e>i.length;)i="0"+i;return i}function d(t,e,i){var n,r=e._milliseconds,o=e._days,s=e._months;r&&t._d.setTime(+t+r*i),o&&t.date(t.date()+o*i),s&&(n=t.date(),t.date(1).month(t.month()+s*i).date(Math.min(n,t.daysInMonth())))}function l(t){return"[object Array]"===Object.prototype.toString.call(t)}function f(t,e){var i,n=Math.min(t.length,e.length),r=Math.abs(t.length-e.length),o=0;for(i=0;n>i;i++)~~t[i]!==~~e[i]&&o++;return o+r}function m(t,e){return e.abbr=t,R[t]||(R[t]=new s),R[t].set(e),R[t]}function g(t){return t?(!R[t]&&U&&e("./lang/"+t),R[t]):I.fn._lang}function v(t){return t.match(/\[.*\]/)?t.replace(/^\[|\]$/g,""):t.replace(/\\/g,"")}function y(t){var e,i,n=t.match(F);for(e=0,i=n.length;i>e;e++)n[e]=ae[n[e]]?ae[n[e]]:v(n[e]);return function(r){var o="";for(e=0;i>e;e++)o+="function"==typeof n[e].call?n[e].call(r,t):n[e];return o}}function w(t,e){function i(e){return t.lang().longDateFormat(e)||e}for(var n=5;n--&&z.test(e);)e=e.replace(z,i);return re[e]||(re[e]=y(e)),re[e](t)}function b(t){switch(t){case"DDDD":return V;case"YYYY":return B;case"YYYYY":return Z;case"S":case"SS":case"SSS":case"DDD":return q;case"MMM":case"MMMM":case"dd":case"ddd":case"dddd":case"a":case"A":return X;case"X":return J;case"Z":case"ZZ":return K;case"T":return G;case"MM":case"DD":case"YY":case"HH":case"hh":case"mm":case"ss":case"M":case"D":case"d":case"H":case"h":case"m":case"s":return W;default:return RegExp(t.replace("\\",""))}}function S(t,e,i){var n,r=i._a;switch(t){case"M":case"MM":r[1]=null==e?0:~~e-1;break;case"MMM":case"MMMM":n=g(i._l).monthsParse(e),null!=n?r[1]=n:i._isValid=!1;break;case"D":case"DD":case"DDD":case"DDDD":null!=e&&(r[2]=~~e);break;case"YY":r[0]=~~e+(~~e>68?1900:2e3);break;case"YYYY":case"YYYYY":r[0]=~~e;break;case"a":case"A":i._isPm="pm"===(e+"").toLowerCase();break;case"H":case"HH":case"h":case"hh":r[3]=~~e;break;case"m":case"mm":r[4]=~~e;break;case"s":case"ss":r[5]=~~e;break;case"S":case"SS":case"SSS":r[6]=~~(1e3*("0."+e));break;case"X":i._d=new Date(1e3*parseFloat(e));break;case"Z":case"ZZ":i._useUTC=!0,n=(e+"").match(ee),n&&n[1]&&(i._tzh=~~n[1]),n&&n[2]&&(i._tzm=~~n[2]),n&&"+"===n[0]&&(i._tzh=-i._tzh,i._tzm=-i._tzm)}null==e&&(i._isValid=!1)}function T(t){var e,i,n=[];if(!t._d){for(e=0;7>e;e++)t._a[e]=n[e]=null==t._a[e]?2===e?1:0:t._a[e];n[3]+=t._tzh||0,n[4]+=t._tzm||0,i=new Date(0),t._useUTC?(i.setUTCFullYear(n[0],n[1],n[2]),i.setUTCHours(n[3],n[4],n[5],n[6])):(i.setFullYear(n[0],n[1],n[2]),i.setHours(n[3],n[4],n[5],n[6])),t._d=i}}function E(t){var e,i,n=t._f.match(F),r=t._i;for(t._a=[],e=0;n.length>e;e++)i=(b(n[e]).exec(r)||[])[0],i&&(r=r.slice(r.indexOf(i)+i.length)),ae[n[e]]&&S(n[e],i,t);t._isPm&&12>t._a[3]&&(t._a[3]+=12),t._isPm===!1&&12===t._a[3]&&(t._a[3]=0),T(t)}function M(t){for(var e,i,n,r,o=99;t._f.length;){if(e=p({},t),e._f=t._f.pop(),E(e),i=new a(e),i.isValid()){n=i;break}r=f(e._a,i.toArray()),o>r&&(o=r,n=i)}p(t,n)}function D(t){var e,i=t._i;if(Q.exec(i)){for(t._f="YYYY-MM-DDT",e=0;4>e;e++)if(te[e][1].exec(i)){t._f+=te[e][0];break}K.exec(i)&&(t._f+=" Z"),E(t)}else t._d=new Date(i)}function _(t){var e=t._i,i=P.exec(e);e===n?t._d=new Date:i?t._d=new Date(+i[1]):"string"==typeof e?D(t):l(e)?(t._a=e.slice(0),T(t)):t._d=e instanceof Date?new Date(+e):new Date(e)}function C(t,e,i,n,r){return r.relativeTime(e||1,!!i,t,n)}function x(t,e,i){var n=j(Math.abs(t)/1e3),r=j(n/60),o=j(r/60),s=j(o/24),a=j(s/365),h=45>n&&["s",n]||1===r&&["m"]||45>r&&["mm",r]||1===o&&["h"]||22>o&&["hh",o]||1===s&&["d"]||25>=s&&["dd",s]||45>=s&&["M"]||345>s&&["MM",j(s/30)]||1===a&&["y"]||["yy",a];return h[2]=e,h[3]=t>0,h[4]=i,C.apply({},h)}function L(t,e,i){var n=i-e,r=i-t.day();return r>n&&(r-=7),n-7>r&&(r+=7),Math.ceil(I(t).add("d",r).dayOfYear()/7)}function O(t){var e=t._i,i=t._f;return null===e||""===e?null:("string"==typeof e&&(t._i=e=g().preparse(e)),I.isMoment(e)?(t=p({},e),t._d=new Date(+e._d)):i?l(i)?M(t):E(t):_(t),new a(t))}function A(t,e){I.fn[t]=I.fn[t+"s"]=function(t){var i=this._isUTC?"UTC":"";return null!=t?(this._d["set"+i+e](t),this):this._d["get"+i+e]()}}function N(t){I.duration.fn[t]=function(){return this._data[t]}}function Y(t,e){I.duration.fn["as"+t]=function(){return+this/e}}for(var I,k,H="2.0.0",j=Math.round,R={},U=i!==n&&i.exports,P=/^\/?Date\((\-?\d+)/i,F=/(\[[^\[]*\])|(\\)?(Mo|MM?M?M?|Do|DDDo|DD?D?D?|ddd?d?|do?|w[o|w]?|W[o|W]?|YYYYY|YYYY|YY|a|A|hh?|HH?|mm?|ss?|SS?S?|X|zz?|ZZ?|.)/g,z=/(\[[^\[]*\])|(\\)?(LT|LL?L?L?|l{1,4})/g,W=/\d\d?/,q=/\d{1,3}/,V=/\d{3}/,B=/\d{1,4}/,Z=/[+\-]?\d{1,6}/,X=/[0-9]*[a-z\u00A0-\u05FF\u0700-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+|[\u0600-\u06FF]+\s*?[\u0600-\u06FF]+/i,K=/Z|[\+\-]\d\d:?\d\d/i,G=/T/i,J=/[\+\-]?\d+(\.\d{1,3})?/,Q=/^\s*\d{4}-\d\d-\d\d((T| )(\d\d(:\d\d(:\d\d(\.\d\d?\d?)?)?)?)?([\+\-]\d\d:?\d\d)?)?/,$="YYYY-MM-DDTHH:mm:ssZ",te=[["HH:mm:ss.S",/(T| )\d\d:\d\d:\d\d\.\d{1,3}/],["HH:mm:ss",/(T| )\d\d:\d\d:\d\d/],["HH:mm",/(T| )\d\d:\d\d/],["HH",/(T| )\d\d/]],ee=/([\+\-]|\d\d)/gi,ie="Month|Date|Hours|Minutes|Seconds|Milliseconds".split("|"),ne={Milliseconds:1,Seconds:1e3,Minutes:6e4,Hours:36e5,Days:864e5,Months:2592e6,Years:31536e6},re={},oe="DDD w W M D d".split(" "),se="M D H h m s w W".split(" "),ae={M:function(){return this.month()+1},MMM:function(t){return this.lang().monthsShort(this,t)},MMMM:function(t){return this.lang().months(this,t)},D:function(){return this.date()},DDD:function(){return this.dayOfYear()},d:function(){return this.day()},dd:function(t){return this.lang().weekdaysMin(this,t)},ddd:function(t){return this.lang().weekdaysShort(this,t)},dddd:function(t){return this.lang().weekdays(this,t)},w:function(){return this.week()},W:function(){return this.isoWeek()},YY:function(){return c(this.year()%100,2)},YYYY:function(){return c(this.year(),4)},YYYYY:function(){return c(this.year(),5)},a:function(){return this.lang().meridiem(this.hours(),this.minutes(),!0)},A:function(){return this.lang().meridiem(this.hours(),this.minutes(),!1)},H:function(){return this.hours()},h:function(){return this.hours()%12||12},m:function(){return this.minutes()},s:function(){return this.seconds()},S:function(){return~~(this.milliseconds()/100)},SS:function(){return c(~~(this.milliseconds()/10),2)},SSS:function(){return c(this.milliseconds(),3)},Z:function(){var t=-this.zone(),e="+";return 0>t&&(t=-t,e="-"),e+c(~~(t/60),2)+":"+c(~~t%60,2)},ZZ:function(){var t=-this.zone(),e="+";return 0>t&&(t=-t,e="-"),e+c(~~(10*t/6),4)},X:function(){return this.unix()}};oe.length;)k=oe.pop(),ae[k+"o"]=o(ae[k]);for(;se.length;)k=se.pop(),ae[k+k]=r(ae[k],2);for(ae.DDDD=r(ae.DDD,3),s.prototype={set:function(t){var e,i;for(i in t)e=t[i],"function"==typeof e?this[i]=e:this["_"+i]=e},_months:"January_February_March_April_May_June_July_August_September_October_November_December".split("_"),months:function(t){return this._months[t.month()]},_monthsShort:"Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec".split("_"),monthsShort:function(t){return this._monthsShort[t.month()]},monthsParse:function(t){var e,i,n;for(this._monthsParse||(this._monthsParse=[]),e=0;12>e;e++)if(this._monthsParse[e]||(i=I([2e3,e]),n="^"+this.months(i,"")+"|^"+this.monthsShort(i,""),this._monthsParse[e]=RegExp(n.replace(".",""),"i")),this._monthsParse[e].test(t))return e},_weekdays:"Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),weekdays:function(t){return this._weekdays[t.day()]},_weekdaysShort:"Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"),weekdaysShort:function(t){return this._weekdaysShort[t.day()]},_weekdaysMin:"Su_Mo_Tu_We_Th_Fr_Sa".split("_"),weekdaysMin:function(t){return this._weekdaysMin[t.day()]},_longDateFormat:{LT:"h:mm A",L:"MM/DD/YYYY",LL:"MMMM D YYYY",LLL:"MMMM D YYYY LT",LLLL:"dddd, MMMM D YYYY LT"},longDateFormat:function(t){var e=this._longDateFormat[t];return!e&&this._longDateFormat[t.toUpperCase()]&&(e=this._longDateFormat[t.toUpperCase()].replace(/MMMM|MM|DD|dddd/g,function(t){return t.slice(1)}),this._longDateFormat[t]=e),e},meridiem:function(t,e,i){return t>11?i?"pm":"PM":i?"am":"AM"},_calendar:{sameDay:"[Today at] LT",nextDay:"[Tomorrow at] LT",nextWeek:"dddd [at] LT",lastDay:"[Yesterday at] LT",lastWeek:"[last] dddd [at] LT",sameElse:"L"},calendar:function(t,e){var i=this._calendar[t];return"function"==typeof i?i.apply(e):i},_relativeTime:{future:"in %s",past:"%s ago",s:"a few seconds",m:"a minute",mm:"%d minutes",h:"an hour",hh:"%d hours",d:"a day",dd:"%d days",M:"a month",MM:"%d months",y:"a year",yy:"%d years"},relativeTime:function(t,e,i,n){var r=this._relativeTime[i];return"function"==typeof r?r(t,e,i,n):r.replace(/%d/i,t)},pastFuture:function(t,e){var i=this._relativeTime[t>0?"future":"past"];return"function"==typeof i?i(e):i.replace(/%s/i,e)},ordinal:function(t){return this._ordinal.replace("%d",t)},_ordinal:"%d",preparse:function(t){return t},postformat:function(t){return t},week:function(t){return L(t,this._week.dow,this._week.doy)},_week:{dow:0,doy:6}},I=function(t,e,i){return O({_i:t,_f:e,_l:i,_isUTC:!1})},I.utc=function(t,e,i){return O({_useUTC:!0,_isUTC:!0,_l:i,_i:t,_f:e})},I.unix=function(t){return I(1e3*t)},I.duration=function(t,e){var i,n=I.isDuration(t),r="number"==typeof t,o=n?t._data:r?{}:t;return r&&(e?o[e]=t:o.milliseconds=t),i=new h(o),n&&t.hasOwnProperty("_lang")&&(i._lang=t._lang),i},I.version=H,I.defaultFormat=$,I.lang=function(t,e){return t?(e?m(t,e):R[t]||g(t),I.duration.fn._lang=I.fn._lang=g(t),n):I.fn._lang._abbr},I.langData=function(t){return t&&t._lang&&t._lang._abbr&&(t=t._lang._abbr),g(t)},I.isMoment=function(t){return t instanceof a},I.isDuration=function(t){return t instanceof h},I.fn=a.prototype={clone:function(){return I(this)},valueOf:function(){return+this._d},unix:function(){return Math.floor(+this._d/1e3)},toString:function(){return this.format("ddd MMM DD YYYY HH:mm:ss [GMT]ZZ")},toDate:function(){return this._d},toJSON:function(){return I.utc(this).format("YYYY-MM-DD[T]HH:mm:ss.SSS[Z]")},toArray:function(){var t=this;return[t.year(),t.month(),t.date(),t.hours(),t.minutes(),t.seconds(),t.milliseconds()]},isValid:function(){return null==this._isValid&&(this._isValid=this._a?!f(this._a,(this._isUTC?I.utc(this._a):I(this._a)).toArray()):!isNaN(this._d.getTime())),!!this._isValid},utc:function(){return this._isUTC=!0,this},local:function(){return this._isUTC=!1,this},format:function(t){var e=w(this,t||I.defaultFormat);return this.lang().postformat(e)},add:function(t,e){var i;return i="string"==typeof t?I.duration(+e,t):I.duration(t,e),d(this,i,1),this},subtract:function(t,e){var i;return i="string"==typeof t?I.duration(+e,t):I.duration(t,e),d(this,i,-1),this},diff:function(t,e,i){var n,r,o=this._isUTC?I(t).utc():I(t).local(),s=6e4*(this.zone()-o.zone());return e&&(e=e.replace(/s$/,"")),"year"===e||"month"===e?(n=432e5*(this.daysInMonth()+o.daysInMonth()),r=12*(this.year()-o.year())+(this.month()-o.month()),r+=(this-I(this).startOf("month")-(o-I(o).startOf("month")))/n,"year"===e&&(r/=12)):(n=this-o-s,r="second"===e?n/1e3:"minute"===e?n/6e4:"hour"===e?n/36e5:"day"===e?n/864e5:"week"===e?n/6048e5:n),i?r:u(r)},from:function(t,e){return I.duration(this.diff(t)).lang(this.lang()._abbr).humanize(!e)},fromNow:function(t){return this.from(I(),t)},calendar:function(){var t=this.diff(I().startOf("day"),"days",!0),e=-6>t?"sameElse":-1>t?"lastWeek":0>t?"lastDay":1>t?"sameDay":2>t?"nextDay":7>t?"nextWeek":"sameElse";return this.format(this.lang().calendar(e,this))},isLeapYear:function(){var t=this.year();return 0===t%4&&0!==t%100||0===t%400},isDST:function(){return this.zone()+I(t).startOf(e)},isBefore:function(t,e){return e=e!==n?e:"millisecond",+this.clone().startOf(e)<+I(t).startOf(e)},isSame:function(t,e){return e=e!==n?e:"millisecond",+this.clone().startOf(e)===+I(t).startOf(e)},zone:function(){return this._isUTC?0:this._d.getTimezoneOffset()},daysInMonth:function(){return I.utc([this.year(),this.month()+1,0]).date()},dayOfYear:function(t){var e=j((I(this).startOf("day")-I(this).startOf("year"))/864e5)+1;return null==t?e:this.add("d",t-e)},isoWeek:function(t){var e=L(this,1,4);return null==t?e:this.add("d",7*(t-e))},week:function(t){var e=this.lang().week(this);return null==t?e:this.add("d",7*(t-e))},lang:function(t){return t===n?this._lang:(this._lang=g(t),this)}},k=0;ie.length>k;k++)A(ie[k].toLowerCase().replace(/s$/,""),ie[k]);A("year","FullYear"),I.fn.days=I.fn.day,I.fn.weeks=I.fn.week,I.fn.isoWeeks=I.fn.isoWeek,I.duration.fn=h.prototype={weeks:function(){return u(this.days()/7)},valueOf:function(){return this._milliseconds+864e5*this._days+2592e6*this._months},humanize:function(t){var e=+this,i=x(e,!t,this.lang());return t&&(i=this.lang().pastFuture(e,i)),this.lang().postformat(i)},lang:I.fn.lang};for(k in ne)ne.hasOwnProperty(k)&&(Y(k,ne[k]),N(k.toLowerCase()));Y("Weeks",6048e5),I.lang("en",{ordinal:function(t){var e=t%10,i=1===~~(t%100/10)?"th":1===e?"st":2===e?"nd":3===e?"rd":"th";return t+i}}),U&&(i.exports=I),"undefined"==typeof ender&&(this.moment=I),"function"==typeof t&&t.amd&&t("moment",[],function(){return I})}).call(this)})()},{}]},{},[1])(1)}); \ No newline at end of file +(function(t){if("function"==typeof bootstrap)bootstrap("vis",t);else if("object"==typeof exports)module.exports=t();else if("function"==typeof define&&define.amd)define(t);else if("undefined"!=typeof ses){if(!ses.ok())return;ses.makeVis=t}else"undefined"!=typeof window?window.vis=t():global.vis=t()})(function(){var t;return function(t,e,i){function s(i,n){if(!e[i]){if(!t[i]){var r="function"==typeof require&&require;if(!n&&r)return r(i,!0);if(o)return o(i,!0);throw Error("Cannot find module '"+i+"'")}var a=e[i]={exports:{}};t[i][0].call(a.exports,function(e){var o=t[i][1][e];return s(o?o:e)},a,a.exports)}return e[i].exports}for(var o="function"==typeof require&&require,n=0;i.length>n;n++)s(i[n]);return s}({1:[function(e,i,s){(function(){function o(t){if(this.id=M.randomUUID(),this.options=t||{},this.data={},this.fieldId=this.options.fieldId||"id",this.fieldTypes={},this.options.fieldTypes)for(var e in this.options.fieldTypes)if(this.options.fieldTypes.hasOwnProperty(e)){var i=this.options.fieldTypes[e];this.fieldTypes[e]="Date"==i||"ISODate"==i||"ASPDate"==i?"Date":i}this.subscribers={},this.internalIds={}}function n(t,e){this.id=M.randomUUID(),this.data=null,this.ids={},this.options=e||{},this.fieldId="id",this.subscribers={};var i=this;this.listener=function(){i._onEvent.apply(i,arguments)},this.setData(t)}function r(t,e){this.parent=t,this.options=e||{},this.defaultOptions={order:function(t,e){if(t instanceof y){if(e instanceof y){var i=t.data.end-t.data.start,s=e.data.end-e.data.start;return i-s||t.data.start-e.data.start}return-1}return e instanceof y?1:t.data.start-e.data.start},margin:{item:10}},this.ordered=[]}function a(t){this.id=M.randomUUID(),this.start=0,this.end=0,this.options={min:null,max:null,zoomMin:null,zoomMax:null},this.listeners=[],this.setOptions(t)}function h(){this.subscriptions=[]}function d(){this.id=M.randomUUID(),this.components={},this.repaintTimer=void 0,this.reflowTimer=void 0}function l(){this.id=null,this.parent=null,this.depends=null,this.controller=null,this.options=null,this.frame=null,this.top=0,this.left=0,this.width=0,this.height=0}function p(t,e,i){this.id=M.randomUUID(),this.parent=t,this.depends=e,this.options=i||{}}function u(t,e){this.id=M.randomUUID(),this.container=t,this.options=e||{},this.defaultOptions={autoResize:!0},this.listeners={}}function c(t,e,i){this.id=M.randomUUID(),this.parent=t,this.depends=e,this.dom={majorLines:[],majorTexts:[],minorLines:[],minorTexts:[],redundant:{majorLines:[],majorTexts:[],minorLines:[],minorTexts:[]}},this.props={range:{start:0,end:0,minimumStep:0},lineTop:0},this.options=i||{},this.defaultOptions={orientation:"bottom",showMinorLabels:!0,showMajorLabels:!0},this.conversion=null,this.range=null}function f(t,e,i){this.id=M.randomUUID(),this.parent=t,this.depends=e,this.options=i||{},this.defaultOptions={style:"box",align:"center",orientation:"bottom",margin:{axis:20,item:10},padding:5},this.dom={};var s=this;this.itemsData=null,this.range=null,this.listeners={add:function(t,e,i){i!=s.id&&s._onAdd(e.items)},update:function(t,e,i){i!=s.id&&s._onUpdate(e.items)},remove:function(t,e,i){i!=s.id&&s._onRemove(e.items)}},this.items={},this.queue={},this.stack=new r(this,Object.create(this.options)),this.conversion=null}function m(t,e,i,s){this.parent=t,this.data=e,this.dom=null,this.options=i||{},this.defaultOptions=s||{},this.selected=!1,this.visible=!1,this.top=0,this.left=0,this.width=0,this.height=0}function g(t,e,i,s){this.props={dot:{left:0,top:0,width:0,height:0},line:{top:0,left:0,width:0,height:0}},m.call(this,t,e,i,s)}function v(t,e,i,s){this.props={dot:{top:0,width:0,height:0},content:{height:0,marginLeft:0}},m.call(this,t,e,i,s)}function y(t,e,i,s){this.props={content:{left:0,width:0}},m.call(this,t,e,i,s)}function w(t,e,i){this.id=M.randomUUID(),this.parent=t,this.groupId=e,this.itemsData=null,this.itemset=null,this.options=i||{},this.options.top=0,this.top=0,this.left=0,this.width=0,this.height=0}function b(t,e,i){this.id=M.randomUUID(),this.parent=t,this.depends=e,this.options=i||{},this.range=null,this.itemsData=null,this.groupsData=null,this.groups={},this.queue={};var s=this;this.listeners={add:function(t,e){s._onAdd(e.items)},update:function(t,e){s._onUpdate(e.items)},remove:function(t,e){s._onRemove(e.items)}}}function _(t,e,i){var s=this;if(this.options=M.extend({orientation:"bottom",min:null,max:null,zoomMin:10,zoomMax:31536e10,moveable:!0,zoomable:!0,showMinorLabels:!0,showMajorLabels:!0,autoResize:!1},i),this.controller=new d,!t)throw Error("No container element provided");var o=Object.create(this.options);o.height=function(){return s.options.height?s.options.height:s.timeaxis.height+s.content.height},this.root=new u(t,o),this.controller.add(this.root);var n=T().hours(0).minutes(0).seconds(0).milliseconds(0);this.range=new a({start:n.clone().add("days",-3).valueOf(),end:n.clone().add("days",4).valueOf()}),this.range.subscribe(this.root,"move","horizontal"),this.range.subscribe(this.root,"zoom","horizontal"),this.range.on("rangechange",function(){var t=!0;s.controller.requestReflow(t)}),this.range.on("rangechanged",function(){var t=!0;s.controller.requestReflow(t)});var r=Object.create(o);r.range=this.range,this.timeaxis=new c(this.root,[],r),this.timeaxis.setRange(this.range),this.controller.add(this.timeaxis),this.setGroups(null),this.itemsData=null,this.groupsData=null,e&&this.setItems(e)}function x(t){this.containerElement=t,this.width="100%",this.height="100%",this.refreshRate=50,this.stabilize=!0,this.selectable=!0,this.constants={nodes:{radiusMin:5,radiusMax:20,radius:5,distance:100,style:"rect",image:void 0,widthMin:16,widthMax:64,fontColor:"black",fontSize:14,fontFace:"arial",borderColor:"#2B7CE9",backgroundColor:"#97C2FC",highlightColor:"#D2E5FF",group:void 0},edges:{widthMin:1,widthMax:15,width:1,style:"line",color:"#343434",fontColor:"#343434",fontSize:14,fontFace:"arial",length:100,dashlength:10,dashgap:5},packages:{radius:5,radiusMin:5,radiusMax:10,style:"dot",color:"#2B7CE9",image:void 0,widthMin:16,widthMax:64,duration:1},minForce:.05,minVelocity:.02,maxIterations:1e3},this.nodes=[],this.edges=[],this.packages=[],this.images=new x.Images,this.groups=new x.Groups,this.hasMovingEdges=!1,this.hasMovingNodes=!1,this.hasMovingPackages=!1,this.selection=[],this.timer=void 0,this._create()}var T=e("moment"),M={};M.isNumber=function(t){return t instanceof Number||"number"==typeof t},M.isString=function(t){return t instanceof String||"string"==typeof t},M.isDate=function(t){if(t instanceof Date)return!0;if(M.isString(t)){var e=S.exec(t);if(e)return!0;if(!isNaN(Date.parse(t)))return!0}return!1},M.isDataTable=function(t){return"undefined"!=typeof google&&google.visualization&&google.visualization.DataTable&&t instanceof google.visualization.DataTable},M.randomUUID=function(){var t=function(){return Math.floor(65536*Math.random()).toString(16)};return t()+t()+"-"+t()+"-"+t()+"-"+t()+"-"+t()+t()+t()},M.extend=function(t){for(var e=1,i=arguments.length;i>e;e++){var s=arguments[e];for(var o in s)s.hasOwnProperty(o)&&void 0!==s[o]&&(t[o]=s[o])}return t},M.cast=function(t,e){var i;if(void 0===t)return void 0;if(null===t)return null;if(!e)return t;if("string"!=typeof e&&!(e instanceof String))throw Error("Type must be a string");switch(e){case"boolean":case"Boolean":return Boolean(t);case"number":case"Number":return Number(t);case"string":case"String":return t+"";case"Date":if(M.isNumber(t))return new Date(t);if(t instanceof Date)return new Date(t.valueOf());if(T.isMoment(t))return new Date(t.valueOf());if(M.isString(t))return i=S.exec(t),i?new Date(Number(i[1])):T(t).toDate();throw Error("Cannot cast object of type "+M.getType(t)+" to type Date");case"Moment":if(M.isNumber(t))return T(t);if(t instanceof Date)return T(t.valueOf());if(T.isMoment(t))return T.clone();if(M.isString(t))return i=S.exec(t),i?T(Number(i[1])):T(t);throw Error("Cannot cast object of type "+M.getType(t)+" to type Date");case"ISODate":if(t instanceof Date)return t.toISOString();if(T.isMoment(t))return t.toDate().toISOString();if(M.isNumber(t)||M.isString(t))return T(t).toDate().toISOString();throw Error("Cannot cast object of type "+M.getType(t)+" to type ISODate");case"ASPDate":if(t instanceof Date)return"/Date("+t.valueOf()+")/";if(M.isNumber(t)||M.isString(t))return"/Date("+T(t).valueOf()+")/";throw Error("Cannot cast object of type "+M.getType(t)+" to type ASPDate");default:throw Error("Cannot cast object of type "+M.getType(t)+' to type "'+e+'"')}};var S=/^\/?Date\((\-?\d+)/i;if(M.getType=function(t){var e=typeof t;return"object"==e?null==t?"null":t instanceof Boolean?"Boolean":t instanceof Number?"Number":t instanceof String?"String":t instanceof Array?"Array":t instanceof Date?"Date":"Object":"number"==e?"Number":"boolean"==e?"Boolean":"string"==e?"String":e},M.getAbsoluteLeft=function(t){for(var e=document.documentElement,i=document.body,s=t.offsetLeft,o=t.offsetParent;null!=o&&o!=i&&o!=e;)s+=o.offsetLeft,s-=o.scrollLeft,o=o.offsetParent;return s},M.getAbsoluteTop=function(t){for(var e=document.documentElement,i=document.body,s=t.offsetTop,o=t.offsetParent;null!=o&&o!=i&&o!=e;)s+=o.offsetTop,s-=o.scrollTop,o=o.offsetParent;return s},M.getPageY=function(t){if("pageY"in t)return t.pageY;var e;e="targetTouches"in t&&t.targetTouches.length?t.targetTouches[0].clientY:t.clientY;var i=document.documentElement,s=document.body;return e+(i&&i.scrollTop||s&&s.scrollTop||0)-(i&&i.clientTop||s&&s.clientTop||0)},M.getPageX=function(t){if("pageY"in t)return t.pageX;var e;e="targetTouches"in t&&t.targetTouches.length?t.targetTouches[0].clientX:t.clientX;var i=document.documentElement,s=document.body;return e+(i&&i.scrollLeft||s&&s.scrollLeft||0)-(i&&i.clientLeft||s&&s.clientLeft||0)},M.addClassName=function(t,e){var i=t.className.split(" ");-1==i.indexOf(e)&&(i.push(e),t.className=i.join(" "))},M.removeClassName=function(t,e){var i=t.className.split(" "),s=i.indexOf(e);-1!=s&&(i.splice(s,1),t.className=i.join(" "))},M.forEach=function(t,e){var i,s;if(t instanceof Array)for(i=0,s=t.length;s>i;i++)e(t[i],i,t);else for(i in t)t.hasOwnProperty(i)&&e(t[i],i,t)},M.updateProperty=function(t,e,i){return t[e]!==i?(t[e]=i,!0):!1},M.addEventListener=function(t,e,i,s){t.addEventListener?(void 0===s&&(s=!1),"mousewheel"===e&&navigator.userAgent.indexOf("Firefox")>=0&&(e="DOMMouseScroll"),t.addEventListener(e,i,s)):t.attachEvent("on"+e,i)},M.removeEventListener=function(t,e,i,s){t.removeEventListener?(void 0===s&&(s=!1),"mousewheel"===e&&navigator.userAgent.indexOf("Firefox")>=0&&(e="DOMMouseScroll"),t.removeEventListener(e,i,s)):t.detachEvent("on"+e,i)},M.getTarget=function(t){t||(t=window.event);var e;return t.target?e=t.target:t.srcElement&&(e=t.srcElement),void 0!=e.nodeType&&3==e.nodeType&&(e=e.parentNode),e},M.stopPropagation=function(t){t||(t=window.event),t.stopPropagation?t.stopPropagation():t.cancelBubble=!0},M.preventDefault=function(t){t||(t=window.event),t.preventDefault?t.preventDefault():t.returnValue=!1},M.option={},M.option.asBoolean=function(t,e){return"function"==typeof t&&(t=t()),null!=t?0!=t:e||null},M.option.asNumber=function(t,e){return"function"==typeof t&&(t=t()),null!=t?Number(t)||e||null:e||null},M.option.asString=function(t,e){return"function"==typeof t&&(t=t()),null!=t?t+"":e||null},M.option.asSize=function(t,e){return"function"==typeof t&&(t=t()),M.isString(t)?t:M.isNumber(t)?t+"px":e||null},M.option.asElement=function(t,e){return"function"==typeof t&&(t=t()),t||e||null},M.loadCss=function(t){if("undefined"!=typeof document){var e=document.createElement("style");e.type="text/css",e.styleSheet?e.styleSheet.cssText=t:e.appendChild(document.createTextNode(t)),document.getElementsByTagName("head")[0].appendChild(e)}},!Array.prototype.indexOf){Array.prototype.indexOf=function(t){for(var e=0;this.length>e;e++)if(this[e]==t)return e;return-1};try{console.log("Warning: Ancient browser detected. Please update your browser")}catch(C){}}Array.prototype.forEach||(Array.prototype.forEach=function(t,e){for(var i=0,s=this.length;s>i;++i)t.call(e||this,this[i],i,this)}),Array.prototype.map||(Array.prototype.map=function(t,e){var i,s,o;if(null==this)throw new TypeError(" this is null or not defined");var n=Object(this),r=n.length>>>0;if("function"!=typeof t)throw new TypeError(t+" is not a function");for(e&&(i=e),s=Array(r),o=0;r>o;){var a,h;o in n&&(a=n[o],h=t.call(i,a,o,n),s[o]=h),o++}return s}),Array.prototype.filter||(Array.prototype.filter=function(t){"use strict";if(null==this)throw new TypeError;var e=Object(this),i=e.length>>>0;if("function"!=typeof t)throw new TypeError;for(var s=[],o=arguments[1],n=0;i>n;n++)if(n in e){var r=e[n];t.call(o,r,n,e)&&s.push(r)}return s}),Object.keys||(Object.keys=function(){var t=Object.prototype.hasOwnProperty,e=!{toString:null}.propertyIsEnumerable("toString"),i=["toString","toLocaleString","valueOf","hasOwnProperty","isPrototypeOf","propertyIsEnumerable","constructor"],s=i.length;return function(o){if("object"!=typeof o&&"function"!=typeof o||null===o)throw new TypeError("Object.keys called on non-object");var n=[];for(var r in o)t.call(o,r)&&n.push(r);if(e)for(var a=0;s>a;a++)t.call(o,i[a])&&n.push(i[a]);return n}}()),Array.isArray||(Array.isArray=function(t){return"[object Array]"===Object.prototype.toString.call(t)}),Function.prototype.bind||(Function.prototype.bind=function(t){if("function"!=typeof this)throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable");var e=Array.prototype.slice.call(arguments,1),i=this,s=function(){},o=function(){return i.apply(this instanceof s&&t?this:t,e.concat(Array.prototype.slice.call(arguments)))};return s.prototype=this.prototype,o.prototype=new s,o}),Object.create||(Object.create=function(t){function e(){}if(arguments.length>1)throw Error("Object.create implementation only accepts the first parameter.");return e.prototype=t,new e});var E={listeners:[],indexOf:function(t){for(var e=this.listeners,i=0,s=this.listeners.length;s>i;i++){var o=e[i];if(o&&o.object==t)return i}return-1},addListener:function(t,e,i){var s=this.indexOf(t),o=this.listeners[s];o||(o={object:t,events:{}},this.listeners.push(o));var n=o.events[e];n||(n=[],o.events[e]=n),-1==n.indexOf(i)&&n.push(i)},removeListener:function(t,e,i){var s=this.indexOf(t),o=this.listeners[s];if(o){var n=o.events[e];n&&(s=n.indexOf(i),-1!=s&&n.splice(s,1),0==n.length&&delete o.events[e]);var r=0,a=o.events;for(var h in a)a.hasOwnProperty(h)&&r++;0==r&&delete this.listeners[s]}},removeAllListeners:function(){this.listeners=[]},trigger:function(t,e,i){var s=this.indexOf(t),o=this.listeners[s];if(o){var n=o.events[e];if(n)for(var r=0,a=n.length;a>r;r++)n[r](i)}}};TimeStep=function(t,e,i){this.current=new Date,this._start=new Date,this._end=new Date,this.autoScale=!0,this.scale=TimeStep.SCALE.DAY,this.step=1,this.setRange(t,e,i)},TimeStep.SCALE={MILLISECOND:1,SECOND:2,MINUTE:3,HOUR:4,DAY:5,WEEKDAY:6,MONTH:7,YEAR:8},TimeStep.prototype.setRange=function(t,e,i){t instanceof Date&&e instanceof Date&&(this._start=void 0!=t?new Date(t.valueOf()):new Date,this._end=void 0!=e?new Date(e.valueOf()):new Date,this.autoScale&&this.setMinimumStep(i))},TimeStep.prototype.first=function(){this.current=new Date(this._start.valueOf()),this.roundToMinor()},TimeStep.prototype.roundToMinor=function(){switch(this.scale){case TimeStep.SCALE.YEAR:this.current.setFullYear(this.step*Math.floor(this.current.getFullYear()/this.step)),this.current.setMonth(0);case TimeStep.SCALE.MONTH:this.current.setDate(1);case TimeStep.SCALE.DAY:case TimeStep.SCALE.WEEKDAY:this.current.setHours(0);case TimeStep.SCALE.HOUR:this.current.setMinutes(0);case TimeStep.SCALE.MINUTE:this.current.setSeconds(0);case TimeStep.SCALE.SECOND:this.current.setMilliseconds(0)}if(1!=this.step)switch(this.scale){case TimeStep.SCALE.MILLISECOND:this.current.setMilliseconds(this.current.getMilliseconds()-this.current.getMilliseconds()%this.step);break;case TimeStep.SCALE.SECOND:this.current.setSeconds(this.current.getSeconds()-this.current.getSeconds()%this.step);break;case TimeStep.SCALE.MINUTE:this.current.setMinutes(this.current.getMinutes()-this.current.getMinutes()%this.step);break;case TimeStep.SCALE.HOUR:this.current.setHours(this.current.getHours()-this.current.getHours()%this.step);break;case TimeStep.SCALE.WEEKDAY:case TimeStep.SCALE.DAY:this.current.setDate(this.current.getDate()-1-(this.current.getDate()-1)%this.step+1);break;case TimeStep.SCALE.MONTH:this.current.setMonth(this.current.getMonth()-this.current.getMonth()%this.step);break;case TimeStep.SCALE.YEAR:this.current.setFullYear(this.current.getFullYear()-this.current.getFullYear()%this.step);break;default:}},TimeStep.prototype.hasNext=function(){return this.current.valueOf()<=this._end.valueOf()},TimeStep.prototype.next=function(){var t=this.current.valueOf();if(6>this.current.getMonth())switch(this.scale){case TimeStep.SCALE.MILLISECOND:this.current=new Date(this.current.valueOf()+this.step);break;case TimeStep.SCALE.SECOND:this.current=new Date(this.current.valueOf()+1e3*this.step);break;case TimeStep.SCALE.MINUTE:this.current=new Date(this.current.valueOf()+60*1e3*this.step);break;case TimeStep.SCALE.HOUR:this.current=new Date(this.current.valueOf()+60*60*1e3*this.step);var e=this.current.getHours();this.current.setHours(e-e%this.step);break;case TimeStep.SCALE.WEEKDAY:case TimeStep.SCALE.DAY:this.current.setDate(this.current.getDate()+this.step);break;case TimeStep.SCALE.MONTH:this.current.setMonth(this.current.getMonth()+this.step);break;case TimeStep.SCALE.YEAR:this.current.setFullYear(this.current.getFullYear()+this.step);break;default:}else switch(this.scale){case TimeStep.SCALE.MILLISECOND:this.current=new Date(this.current.valueOf()+this.step);break;case TimeStep.SCALE.SECOND:this.current.setSeconds(this.current.getSeconds()+this.step);break;case TimeStep.SCALE.MINUTE:this.current.setMinutes(this.current.getMinutes()+this.step);break;case TimeStep.SCALE.HOUR:this.current.setHours(this.current.getHours()+this.step);break;case TimeStep.SCALE.WEEKDAY:case TimeStep.SCALE.DAY:this.current.setDate(this.current.getDate()+this.step);break;case TimeStep.SCALE.MONTH:this.current.setMonth(this.current.getMonth()+this.step);break;case TimeStep.SCALE.YEAR:this.current.setFullYear(this.current.getFullYear()+this.step);break;default:}if(1!=this.step)switch(this.scale){case TimeStep.SCALE.MILLISECOND:this.current.getMilliseconds()0&&(this.step=e),this.autoScale=!1},TimeStep.prototype.setAutoScale=function(t){this.autoScale=t},TimeStep.prototype.setMinimumStep=function(t){if(void 0!=t){var e=31104e6,i=2592e6,s=864e5,o=36e5,n=6e4,r=1e3,a=1;1e3*e>t&&(this.scale=TimeStep.SCALE.YEAR,this.step=1e3),500*e>t&&(this.scale=TimeStep.SCALE.YEAR,this.step=500),100*e>t&&(this.scale=TimeStep.SCALE.YEAR,this.step=100),50*e>t&&(this.scale=TimeStep.SCALE.YEAR,this.step=50),10*e>t&&(this.scale=TimeStep.SCALE.YEAR,this.step=10),5*e>t&&(this.scale=TimeStep.SCALE.YEAR,this.step=5),e>t&&(this.scale=TimeStep.SCALE.YEAR,this.step=1),3*i>t&&(this.scale=TimeStep.SCALE.MONTH,this.step=3),i>t&&(this.scale=TimeStep.SCALE.MONTH,this.step=1),5*s>t&&(this.scale=TimeStep.SCALE.DAY,this.step=5),2*s>t&&(this.scale=TimeStep.SCALE.DAY,this.step=2),s>t&&(this.scale=TimeStep.SCALE.DAY,this.step=1),s/2>t&&(this.scale=TimeStep.SCALE.WEEKDAY,this.step=1),4*o>t&&(this.scale=TimeStep.SCALE.HOUR,this.step=4),o>t&&(this.scale=TimeStep.SCALE.HOUR,this.step=1),15*n>t&&(this.scale=TimeStep.SCALE.MINUTE,this.step=15),10*n>t&&(this.scale=TimeStep.SCALE.MINUTE,this.step=10),5*n>t&&(this.scale=TimeStep.SCALE.MINUTE,this.step=5),n>t&&(this.scale=TimeStep.SCALE.MINUTE,this.step=1),15*r>t&&(this.scale=TimeStep.SCALE.SECOND,this.step=15),10*r>t&&(this.scale=TimeStep.SCALE.SECOND,this.step=10),5*r>t&&(this.scale=TimeStep.SCALE.SECOND,this.step=5),r>t&&(this.scale=TimeStep.SCALE.SECOND,this.step=1),200*a>t&&(this.scale=TimeStep.SCALE.MILLISECOND,this.step=200),100*a>t&&(this.scale=TimeStep.SCALE.MILLISECOND,this.step=100),50*a>t&&(this.scale=TimeStep.SCALE.MILLISECOND,this.step=50),10*a>t&&(this.scale=TimeStep.SCALE.MILLISECOND,this.step=10),5*a>t&&(this.scale=TimeStep.SCALE.MILLISECOND,this.step=5),a>t&&(this.scale=TimeStep.SCALE.MILLISECOND,this.step=1)}},TimeStep.prototype.snap=function(t){if(this.scale==TimeStep.SCALE.YEAR){var e=t.getFullYear()+Math.round(t.getMonth()/12);t.setFullYear(Math.round(e/this.step)*this.step),t.setMonth(0),t.setDate(0),t.setHours(0),t.setMinutes(0),t.setSeconds(0),t.setMilliseconds(0)}else if(this.scale==TimeStep.SCALE.MONTH)t.getDate()>15?(t.setDate(1),t.setMonth(t.getMonth()+1)):t.setDate(1),t.setHours(0),t.setMinutes(0),t.setSeconds(0),t.setMilliseconds(0);else if(this.scale==TimeStep.SCALE.DAY||this.scale==TimeStep.SCALE.WEEKDAY){switch(this.step){case 5:case 2:t.setHours(24*Math.round(t.getHours()/24));break;default:t.setHours(12*Math.round(t.getHours()/12))}t.setMinutes(0),t.setSeconds(0),t.setMilliseconds(0)}else if(this.scale==TimeStep.SCALE.HOUR){switch(this.step){case 4:t.setMinutes(60*Math.round(t.getMinutes()/60));break;default:t.setMinutes(30*Math.round(t.getMinutes()/30))}t.setSeconds(0),t.setMilliseconds(0)}else if(this.scale==TimeStep.SCALE.MINUTE){switch(this.step){case 15:case 10:t.setMinutes(5*Math.round(t.getMinutes()/5)),t.setSeconds(0);break;case 5:t.setSeconds(60*Math.round(t.getSeconds()/60));break;default:t.setSeconds(30*Math.round(t.getSeconds()/30))}t.setMilliseconds(0)}else if(this.scale==TimeStep.SCALE.SECOND)switch(this.step){case 15:case 10:t.setSeconds(5*Math.round(t.getSeconds()/5)),t.setMilliseconds(0);break;case 5:t.setMilliseconds(1e3*Math.round(t.getMilliseconds()/1e3));break;default:t.setMilliseconds(500*Math.round(t.getMilliseconds()/500))}else if(this.scale==TimeStep.SCALE.MILLISECOND){var i=this.step>5?this.step/2:1;t.setMilliseconds(Math.round(t.getMilliseconds()/i)*i)}},TimeStep.prototype.isMajor=function(){switch(this.scale){case TimeStep.SCALE.MILLISECOND:return 0==this.current.getMilliseconds();case TimeStep.SCALE.SECOND:return 0==this.current.getSeconds();case TimeStep.SCALE.MINUTE:return 0==this.current.getHours()&&0==this.current.getMinutes();case TimeStep.SCALE.HOUR:return 0==this.current.getHours();case TimeStep.SCALE.WEEKDAY:case TimeStep.SCALE.DAY:return 1==this.current.getDate();case TimeStep.SCALE.MONTH:return 0==this.current.getMonth();case TimeStep.SCALE.YEAR:return!1;default:return!1}},TimeStep.prototype.getLabelMinor=function(t){switch(void 0==t&&(t=this.current),this.scale){case TimeStep.SCALE.MILLISECOND:return T(t).format("SSS");case TimeStep.SCALE.SECOND:return T(t).format("s");case TimeStep.SCALE.MINUTE:return T(t).format("HH:mm");case TimeStep.SCALE.HOUR:return T(t).format("HH:mm");case TimeStep.SCALE.WEEKDAY:return T(t).format("ddd D");case TimeStep.SCALE.DAY:return T(t).format("D");case TimeStep.SCALE.MONTH:return T(t).format("MMM");case TimeStep.SCALE.YEAR:return T(t).format("YYYY");default:return""}},TimeStep.prototype.getLabelMajor=function(t){switch(void 0==t&&(t=this.current),this.scale){case TimeStep.SCALE.MILLISECOND:return T(t).format("HH:mm:ss");case TimeStep.SCALE.SECOND:return T(t).format("D MMMM HH:mm");case TimeStep.SCALE.MINUTE:case TimeStep.SCALE.HOUR:return T(t).format("ddd D MMMM");case TimeStep.SCALE.WEEKDAY:case TimeStep.SCALE.DAY:return T(t).format("MMMM YYYY");case TimeStep.SCALE.MONTH:return T(t).format("YYYY");case TimeStep.SCALE.YEAR:return"";default:return""}},o.prototype.subscribe=function(t,e,i){var s=this.subscribers[t];s||(s=[],this.subscribers[t]=s),s.push({id:i?i+"":null,callback:e})},o.prototype.unsubscribe=function(t,e){var i=this.subscribers[t];i&&(this.subscribers[t]=i.filter(function(t){return t.callback!=e}))},o.prototype._trigger=function(t,e,i){if("*"==t)throw Error("Cannot trigger event *");var s=[];t in this.subscribers&&(s=s.concat(this.subscribers[t])),"*"in this.subscribers&&(s=s.concat(this.subscribers["*"]));for(var o=0;s.length>o;o++){var n=s[o];n.callback&&n.callback(t,e,i||null)}},o.prototype.add=function(t,e){var i,s=[],o=this;if(t instanceof Array)for(var n=0,r=t.length;r>n;n++)i=o._addItem(t[n]),s.push(i);else if(M.isDataTable(t))for(var a=this._getColumnNames(t),h=0,d=t.getNumberOfRows();d>h;h++){for(var l={},p=0,u=a.length;u>p;p++){var c=a[p];l[c]=t.getValue(h,p)}i=o._addItem(l),s.push(i)}else{if(!(t instanceof Object))throw Error("Unknown dataType");i=o._addItem(t),s.push(i)}s.length&&this._trigger("add",{items:s},e)},o.prototype.update=function(t,e){var i=[],s=[],o=this,n=o.fieldId,r=function(t){var e=t[n];o.data[e]?(e=o._updateItem(t),s.push(e)):(e=o._addItem(t),i.push(e))};if(t instanceof Array)for(var a=0,h=t.length;h>a;a++)r(t[a]);else if(M.isDataTable(t))for(var d=this._getColumnNames(t),l=0,p=t.getNumberOfRows();p>l;l++){for(var u={},c=0,f=d.length;f>c;c++){var m=d[c];u[m]=t.getValue(l,c)}r(u)}else{if(!(t instanceof Object))throw Error("Unknown dataType");r(t)}i.length&&this._trigger("add",{items:i},e),s.length&&this._trigger("update",{items:s},e)},o.prototype.get=function(){var t,e,i,s,o=this,n=M.getType(arguments[0]);"String"==n||"Number"==n?(t=arguments[0],i=arguments[1],s=arguments[2]):"Array"==n?(e=arguments[0],i=arguments[1],s=arguments[2]):(i=arguments[0],s=arguments[1]);var r;if(i&&i.type){if(r="DataTable"==i.type?"DataTable":"Array",s&&r!=M.getType(s))throw Error('Type of parameter "data" ('+M.getType(s)+") "+"does not correspond with specified options.type ("+i.type+")");if("DataTable"==r&&!M.isDataTable(s))throw Error('Parameter "data" must be a DataTable when options.type is "DataTable"')}else r=s?"DataTable"==M.getType(s)?"DataTable":"Array":"Array";var a,h,d,l,p=i&&i.fieldTypes||this.options.fieldTypes,u=i&&i.filter,c=[];if(void 0!=t)a=o._getItem(t,p),u&&!u(a)&&(a=null);else if(void 0!=e)for(d=0,l=e.length;l>d;d++)a=o._getItem(e[d],p),(!u||u(a))&&c.push(a);else for(h in this.data)this.data.hasOwnProperty(h)&&(a=o._getItem(h,p),(!u||u(a))&&c.push(a));if(i&&i.order&&void 0==t&&this._sort(c,i.order),i&&i.fields){var f=i.fields;if(void 0!=t)a=this._filterFields(a,f);else for(d=0,l=c.length;l>d;d++)c[d]=this._filterFields(c[d],f)}if("DataTable"==r){var m=this._getColumnNames(s);if(void 0!=t)o._appendRow(s,m,a);else for(d=0,l=c.length;l>d;d++)o._appendRow(s,m,c[d]);return s}if(void 0!=t)return a;if(s){for(d=0,l=c.length;l>d;d++)s.push(c[d]);return s}return c},o.prototype.getIds=function(t){var e,i,s,o,n,r=this.data,a=t&&t.filter,h=t&&t.order,d=t&&t.fieldTypes||this.options.fieldTypes,l=[];if(a)if(h){n=[];for(s in r)r.hasOwnProperty(s)&&(o=this._getItem(s,d),a(o)&&n.push(o));for(this._sort(n,h),e=0,i=n.length;i>e;e++)l[e]=n[e][this.fieldId]}else for(s in r)r.hasOwnProperty(s)&&(o=this._getItem(s,d),a(o)&&l.push(o[this.fieldId]));else if(h){n=[];for(s in r)r.hasOwnProperty(s)&&n.push(r[s]);for(this._sort(n,h),e=0,i=n.length;i>e;e++)l[e]=n[e][this.fieldId]}else for(s in r)r.hasOwnProperty(s)&&(o=r[s],l.push(o[this.fieldId]));return l},o.prototype.forEach=function(t,e){var i,s,o=e&&e.filter,n=e&&e.fieldTypes||this.options.fieldTypes,r=this.data;if(e&&e.order)for(var a=this.get(e),h=0,d=a.length;d>h;h++)i=a[h],s=i[this.fieldId],t(i,s);else for(s in r)r.hasOwnProperty(s)&&(i=this._getItem(s,n),(!o||o(i))&&t(i,s))},o.prototype.map=function(t,e){var i,s=e&&e.filter,o=e&&e.fieldTypes||this.options.fieldTypes,n=[],r=this.data;for(var a in r)r.hasOwnProperty(a)&&(i=this._getItem(a,o),(!s||s(i))&&n.push(t(i,a)));return e&&e.order&&this._sort(n,e.order),n},o.prototype._filterFields=function(t,e){var i={};for(var s in t)t.hasOwnProperty(s)&&-1!=e.indexOf(s)&&(i[s]=t[s]);return i},o.prototype._sort=function(t,e){if(M.isString(e)){var i=e;t.sort(function(t,e){var s=t[i],o=e[i];return s>o?1:o>s?-1:0})}else{if("function"!=typeof e)throw new TypeError("Order must be a function or a string");t.sort(e)}},o.prototype.remove=function(t,e){var i,s,o=[];if(M.isNumber(t)||M.isString(t))delete this.data[t],delete this.internalIds[t],o.push(t);else if(t instanceof Array){for(i=0,s=t.length;s>i;i++)this.remove(t[i]);o=items.concat(t)}else if(t instanceof Object)for(i in this.data)this.data.hasOwnProperty(i)&&this.data[i]==t&&(delete this.data[i],delete this.internalIds[i],o.push(i));o.length&&this._trigger("remove",{items:o},e)},o.prototype.clear=function(t){var e=Object.keys(this.data);this.data={},this.internalIds={},this._trigger("remove",{items:e},t)},o.prototype.max=function(t){var e=this.data,i=null,s=null;for(var o in e)if(e.hasOwnProperty(o)){var n=e[o],r=n[t];null!=r&&(!i||r>s)&&(i=n,s=r)}return i},o.prototype.min=function(t){var e=this.data,i=null,s=null;for(var o in e)if(e.hasOwnProperty(o)){var n=e[o],r=n[t];null!=r&&(!i||s>r)&&(i=n,s=r)}return i},o.prototype.distinct=function(t){var e=this.data,i=[],s=this.options.fieldTypes[t],o=0;for(var n in e)if(e.hasOwnProperty(n)){for(var r=e[n],a=M.cast(r[t],s),h=!1,d=0;o>d;d++)if(i[d]==a){h=!0;break}h||(i[o]=a,o++)}return i},o.prototype._addItem=function(t){var e=t[this.fieldId];if(void 0!=e){if(this.data[e])throw Error("Cannot add item: item with id "+e+" already exists")}else e=M.randomUUID(),t[this.fieldId]=e,this.internalIds[e]=t;var i={};for(var s in t)if(t.hasOwnProperty(s)){var o=this.fieldTypes[s];i[s]=M.cast(t[s],o)}return this.data[e]=i,e},o.prototype._getItem=function(t,e){var i,s,o=this.data[t];if(!o)return null;var n={},r=this.fieldId,a=this.internalIds;if(e)for(i in o)o.hasOwnProperty(i)&&(s=o[i],i==r&&s in a||(n[i]=M.cast(s,e[i])));else for(i in o)o.hasOwnProperty(i)&&(s=o[i],i==r&&s in a||(n[i]=s));return n},o.prototype._updateItem=function(t){var e=t[this.fieldId];if(void 0==e)throw Error("Cannot update item: item has no id (item: "+JSON.stringify(t)+")");var i=this.data[e];if(!i)throw Error("Cannot update item: no item with id "+e+" found");for(var s in t)if(t.hasOwnProperty(s)){var o=this.fieldTypes[s];i[s]=M.cast(t[s],o)}return e},o.prototype._getColumnNames=function(t){for(var e=[],i=0,s=t.getNumberOfColumns();s>i;i++)e[i]=t.getColumnId(i)||t.getColumnLabel(i);return e},o.prototype._appendRow=function(t,e,i){for(var s=t.addRow(),o=0,n=e.length;n>o;o++){var r=e[o];t.setValue(s,o,i[r])}},n.prototype.setData=function(t){var e,i,s;if(this.data){this.data.unsubscribe&&this.data.unsubscribe("*",this.listener),e=[];for(var o in this.ids)this.ids.hasOwnProperty(o)&&e.push(o);this.ids={},this._trigger("remove",{items:e})}if(this.data=t,this.data){for(this.fieldId=this.options.fieldId||this.data&&this.data.options&&this.data.options.fieldId||"id",e=this.data.getIds({filter:this.options&&this.options.filter}),i=0,s=e.length;s>i;i++)o=e[i],this.ids[o]=!0;this._trigger("add",{items:e}),this.data.subscribe&&this.data.subscribe("*",this.listener)}},n.prototype.get=function(){var t,e,i,s=this,o=M.getType(arguments[0]);"String"==o||"Number"==o||"Array"==o?(t=arguments[0],e=arguments[1],i=arguments[2]):(e=arguments[0],i=arguments[1]);var n=M.extend({},this.options,e);this.options.filter&&e&&e.filter&&(n.filter=function(t){return s.options.filter(t)&&e.filter(t)});var r=[];return void 0!=t&&r.push(t),r.push(n),r.push(i),this.data&&this.data.get.apply(this.data,r)},n.prototype.getIds=function(t){var e;if(this.data){var i,s=this.options.filter;i=t&&t.filter?s?function(e){return s(e)&&t.filter(e)}:t.filter:s,e=this.data.getIds({filter:i,order:t&&t.order})}else e=[];return e +},n.prototype._onEvent=function(t,e,i){var s,o,n,r,a=e&&e.items,h=this.data,d=[],l=[],p=[];if(a&&h){switch(t){case"add":for(s=0,o=a.length;o>s;s++)n=a[s],r=this.get(n),r&&(this.ids[n]=!0,d.push(n));break;case"update":for(s=0,o=a.length;o>s;s++)n=a[s],r=this.get(n),r?this.ids[n]?l.push(n):(this.ids[n]=!0,d.push(n)):this.ids[n]&&(delete this.ids[n],p.push(n));break;case"remove":for(s=0,o=a.length;o>s;s++)n=a[s],this.ids[n]&&(delete this.ids[n],p.push(n))}d.length&&this._trigger("add",{items:d},i),l.length&&this._trigger("update",{items:l},i),p.length&&this._trigger("remove",{items:p},i)}},n.prototype.subscribe=o.prototype.subscribe,n.prototype.unsubscribe=o.prototype.unsubscribe,n.prototype._trigger=o.prototype._trigger,r.prototype.setOptions=function(t){M.extend(this.options,t)},r.prototype.update=function(){this._order(),this._stack()},r.prototype._order=function(){var t=this.parent.items;if(!t)throw Error("Cannot stack items: parent does not contain items");var e=[],i=0;M.forEach(t,function(t){t.visible&&(e[i]=t,i++)});var s=this.options.order||this.defaultOptions.order;if("function"!=typeof s)throw Error("Option order must be a function");e.sort(s),this.ordered=e},r.prototype._stack=function(){var t,e,i,s=this.ordered,o=this.options,n=o.orientation||this.defaultOptions.orientation,r="top"==n;for(i=o.margin&&void 0!==o.margin.item?o.margin.item:this.defaultOptions.margin.item,t=0,e=s.length;e>t;t++){var a=s[t],h=null;do h=this.checkOverlap(s,t,0,t-1,i),null!=h&&(a.top=r?h.top+h.height+i:h.top-a.height-i);while(h)}},r.prototype.checkOverlap=function(t,e,i,s,o){for(var n=this.collision,r=t[e],a=s;a>=i;a--){var h=t[a];if(n(r,h,o)&&a!=e)return h}return null},r.prototype.collision=function(t,e,i){return t.left-ie.left&&t.top-ie.top},a.prototype.setOptions=function(t){M.extend(this.options,t),(null!=t.start||null!=t.end)&&this.setRange(t.start,t.end)},a.prototype.subscribe=function(t,e,i){var s,o=this;if("horizontal"!=i&&"vertical"!=i)throw new TypeError('Unknown direction "'+i+'". '+'Choose "horizontal" or "vertical".');if("move"==e)s={component:t,event:e,direction:i,callback:function(t){o._onMouseDown(t,s)},params:{}},t.on("mousedown",s.callback),o.listeners.push(s);else{if("zoom"!=e)throw new TypeError('Unknown event "'+e+'". '+'Choose "move" or "zoom".');s={component:t,event:e,direction:i,callback:function(t){o._onMouseWheel(t,s)},params:{}},t.on("mousewheel",s.callback),o.listeners.push(s)}},a.prototype.on=function(t,e){E.addListener(this,t,e)},a.prototype._trigger=function(t){E.trigger(this,t,{start:this.start,end:this.end})},a.prototype.setRange=function(t,e){var i=this._applyRange(t,e);i&&(this._trigger("rangechange"),this._trigger("rangechanged"))},a.prototype._applyRange=function(t,e){var i,s=null!=t?M.cast(t,"Number"):this.start,o=null!=e?M.cast(e,"Number"):this.end;if(isNaN(s))throw Error('Invalid start "'+t+'"');if(isNaN(o))throw Error('Invalid end "'+e+'"');if(s>o&&(o=s),null!=this.options.min){var n=this.options.min.valueOf();n>s&&(i=n-s,s+=i,o+=i)}if(null!=this.options.max){var r=this.options.max.valueOf();o>r&&(i=o-r,s-=i,o-=i)}if(null!=this.options.zoomMin){var a=this.options.zoomMin.valueOf();0>a&&(a=0),a>o-s&&(this.end-this.start>a?(i=a-(o-s),s-=i/2,o+=i/2):(s=this.start,o=this.end))}if(null!=this.options.zoomMax){var h=this.options.zoomMax.valueOf();0>h&&(h=0),o-s>h&&(h>this.end-this.start?(i=o-s-h,s+=i/2,o-=i/2):(s=this.start,o=this.end))}var d=this.start!=s||this.end!=o;return this.start=s,this.end=o,d},a.prototype.getRange=function(){return{start:this.start,end:this.end}},a.prototype.conversion=function(t){return this.start,this.end,a.conversion(this.start,this.end,t)},a.conversion=function(t,e,i){return 0!=i&&0!=e-t?{offset:t,factor:i/(e-t)}:{offset:0,factor:1}},a.prototype._onMouseDown=function(t,e){t=t||window.event;var i=e.params,s=t.which?1==t.which:1==t.button;if(s){i.mouseX=M.getPageX(t),i.mouseY=M.getPageY(t),i.previousLeft=0,i.previousOffset=0,i.moved=!1,i.start=this.start,i.end=this.end;var o=e.component.frame;o&&(o.style.cursor="move");var n=this;i.onMouseMove||(i.onMouseMove=function(t){n._onMouseMove(t,e)},M.addEventListener(document,"mousemove",i.onMouseMove)),i.onMouseUp||(i.onMouseUp=function(t){n._onMouseUp(t,e)},M.addEventListener(document,"mouseup",i.onMouseUp)),M.preventDefault(t)}},a.prototype._onMouseMove=function(t,e){t=t||window.event;var i=e.params,s=M.getPageX(t),o=M.getPageY(t);void 0==i.mouseX&&(i.mouseX=s),void 0==i.mouseY&&(i.mouseY=o);var n=s-i.mouseX,r=o-i.mouseY,a="horizontal"==e.direction?n:r;Math.abs(a)>=1&&(i.moved=!0);var h=i.end-i.start,d="horizontal"==e.direction?e.component.width:e.component.height,l=-a/d*h;this._applyRange(i.start+l,i.end+l),this._trigger("rangechange"),M.preventDefault(t)},a.prototype._onMouseUp=function(t,e){t=t||window.event;var i=e.params;e.component.frame&&(e.component.frame.style.cursor="auto"),i.onMouseMove&&(M.removeEventListener(document,"mousemove",i.onMouseMove),i.onMouseMove=null),i.onMouseUp&&(M.removeEventListener(document,"mouseup",i.onMouseUp),i.onMouseUp=null),i.moved&&this._trigger("rangechanged")},a.prototype._onMouseWheel=function(t,e){t=t||window.event;var i=0;if(t.wheelDelta?i=t.wheelDelta/120:t.detail&&(i=-t.detail/3),i){var s=this,o=function(){var o=i/5,n=null,r=e.component.frame;if(r){var a,h;if("horizontal"==e.direction){a=e.component.width,h=s.conversion(a);var d=M.getAbsoluteLeft(r),l=M.getPageX(t);n=(l-d)/h.factor+h.offset}else{a=e.component.height,h=s.conversion(a);var p=M.getAbsoluteTop(r),u=M.getPageY(t);n=(p+a-u-p)/h.factor+h.offset}}s.zoom(o,n)};o()}M.preventDefault(t)},a.prototype.zoom=function(t,e){null==e&&(e=(this.start+this.end)/2),t>=1&&(t=.9),-1>=t&&(t=-.9),0>t&&(t/=1+t);var i=this.start-e,s=this.end-e,o=this.start-i*t,n=this.end-s*t;this.setRange(o,n)},a.prototype.move=function(t){var e=this.end-this.start,i=this.start+e*t,s=this.end+e*t;this.start=i,this.end=s},h.prototype.on=function(t,e,i){var s=t instanceof RegExp?t:RegExp(t.replace("*","\\w+")),o={id:M.randomUUID(),event:t,regexp:s,callback:"function"==typeof e?e:null,target:i};return this.subscriptions.push(o),o.id},h.prototype.off=function(t){for(var e=0;this.subscriptions.length>e;){var i=this.subscriptions[e],s=!0;if(t instanceof Object)for(var o in t)t.hasOwnProperty(o)&&t[o]!==i[o]&&(s=!1);else s=i.id==t;s?this.subscriptions.splice(e,1):e++}},h.prototype.emit=function(t,e,i){for(var s=0;this.subscriptions.length>s;s++){var o=this.subscriptions[s];o.regexp.test(t)&&o.callback&&o.callback(t,e,i)}},d.prototype.add=function(t){if(void 0==t.id)throw Error("Component has no field id");if(!(t instanceof l||t instanceof d))throw new TypeError("Component must be an instance of prototype Component or Controller");t.controller=this,this.components[t.id]=t},d.prototype.remove=function(t){var e;for(e in this.components)if(this.components.hasOwnProperty(e)&&(e==t||this.components[e]==t))break;e&&delete this.components[e]},d.prototype.requestReflow=function(t){if(t)this.reflow();else if(!this.reflowTimer){var e=this;this.reflowTimer=setTimeout(function(){e.reflowTimer=void 0,e.reflow()},0)}},d.prototype.requestRepaint=function(t){if(t)this.repaint();else if(!this.repaintTimer){var e=this;this.repaintTimer=setTimeout(function(){e.repaintTimer=void 0,e.repaint()},0)}},d.prototype.repaint=function(){function t(s,o){o in i||(s.depends&&s.depends.forEach(function(e){t(e,e.id)}),s.parent&&t(s.parent,s.parent.id),e=s.repaint()||e,i[o]=!0)}var e=!1;this.repaintTimer&&(clearTimeout(this.repaintTimer),this.repaintTimer=void 0);var i={};M.forEach(this.components,t),e&&this.reflow()},d.prototype.reflow=function(){function t(s,o){o in i||(s.depends&&s.depends.forEach(function(e){t(e,e.id)}),s.parent&&t(s.parent,s.parent.id),e=s.reflow()||e,i[o]=!0)}var e=!1;this.reflowTimer&&(clearTimeout(this.reflowTimer),this.reflowTimer=void 0);var i={};M.forEach(this.components,t),e&&this.repaint()},l.prototype.setOptions=function(t){t&&(M.extend(this.options,t),this.controller&&(this.requestRepaint(),this.requestReflow()))},l.prototype.getOption=function(t){var e;return this.options&&(e=this.options[t]),void 0===e&&this.defaultOptions&&(e=this.defaultOptions[t]),e},l.prototype.getContainer=function(){return null},l.prototype.getFrame=function(){return this.frame},l.prototype.repaint=function(){return!1},l.prototype.reflow=function(){return!1},l.prototype.hide=function(){return this.frame&&this.frame.parentNode?(this.frame.parentNode.removeChild(this.frame),!0):!1},l.prototype.show=function(){return this.frame&&this.frame.parentNode?!1:this.repaint()},l.prototype.requestRepaint=function(){if(!this.controller)throw Error("Cannot request a repaint: no controller configured");this.controller.requestRepaint()},l.prototype.requestReflow=function(){if(!this.controller)throw Error("Cannot request a reflow: no controller configured");this.controller.requestReflow()},p.prototype=new l,p.prototype.setOptions=l.prototype.setOptions,p.prototype.getContainer=function(){return this.frame},p.prototype.repaint=function(){var t=0,e=M.updateProperty,i=M.option.asSize,s=this.options,o=this.frame;if(!o){o=document.createElement("div"),o.className="panel";var n=s.className;n&&("function"==typeof n?M.addClassName(o,n()+""):M.addClassName(o,n+"")),this.frame=o,t+=1}if(!o.parentNode){if(!this.parent)throw Error("Cannot repaint panel: no parent attached");var r=this.parent.getContainer();if(!r)throw Error("Cannot repaint panel: parent has no container element");r.appendChild(o),t+=1}return t+=e(o.style,"top",i(s.top,"0px")),t+=e(o.style,"left",i(s.left,"0px")),t+=e(o.style,"width",i(s.width,"100%")),t+=e(o.style,"height",i(s.height,"100%")),t>0},p.prototype.reflow=function(){var t=0,e=M.updateProperty,i=this.frame;return i?(t+=e(this,"top",i.offsetTop),t+=e(this,"left",i.offsetLeft),t+=e(this,"width",i.offsetWidth),t+=e(this,"height",i.offsetHeight)):t+=1,t>0},u.prototype=new p,u.prototype.setOptions=l.prototype.setOptions,u.prototype.repaint=function(){var t=0,e=M.updateProperty,i=M.option.asSize,s=this.options,o=this.frame;if(!o){o=document.createElement("div"),o.className="graph panel";var n=s.className;n&&M.addClassName(o,M.option.asString(n)),this.frame=o,t+=1}if(!o.parentNode){if(!this.container)throw Error("Cannot repaint root panel: no container attached");this.container.appendChild(o),t+=1}return t+=e(o.style,"top",i(s.top,"0px")),t+=e(o.style,"left",i(s.left,"0px")),t+=e(o.style,"width",i(s.width,"100%")),t+=e(o.style,"height",i(s.height,"100%")),this._updateEventEmitters(),this._updateWatch(),t>0},u.prototype.reflow=function(){var t=0,e=M.updateProperty,i=this.frame;return i?(t+=e(this,"top",i.offsetTop),t+=e(this,"left",i.offsetLeft),t+=e(this,"width",i.offsetWidth),t+=e(this,"height",i.offsetHeight)):t+=1,t>0},u.prototype._updateWatch=function(){var t=this.getOption("autoResize");t?this._watch():this._unwatch()},u.prototype._watch=function(){var t=this;this._unwatch();var e=function(){var e=t.getOption("autoResize");return e?(t.frame&&(t.frame.clientWidth!=t.width||t.frame.clientHeight!=t.height)&&t.requestReflow(),void 0):(t._unwatch(),void 0)};M.addEventListener(window,"resize",e),this.watchTimer=setInterval(e,1e3)},u.prototype._unwatch=function(){this.watchTimer&&(clearInterval(this.watchTimer),this.watchTimer=void 0)},u.prototype.on=function(t,e){var i=this.listeners[t];i||(i=[],this.listeners[t]=i),i.push(e),this._updateEventEmitters()},u.prototype._updateEventEmitters=function(){if(this.listeners){var t=this;M.forEach(this.listeners,function(e,i){if(t.emitters||(t.emitters={}),!(i in t.emitters)){var s=t.frame;if(s){var o=function(t){e.forEach(function(e){e(t)})};t.emitters[i]=o,M.addEventListener(s,i,o)}}})}},c.prototype=new l,c.prototype.setOptions=l.prototype.setOptions,c.prototype.setRange=function(t){if(!(t instanceof a||t&&t.start&&t.end))throw new TypeError("Range must be an instance of Range, or an object containing start and end.");this.range=t},c.prototype.toTime=function(t){var e=this.conversion;return new Date(t/e.factor+e.offset)},c.prototype.toScreen=function(t){var e=this.conversion;return(t.valueOf()-e.offset)*e.factor},c.prototype.repaint=function(){var t=0,e=M.updateProperty,i=M.option.asSize,s=this.options,o=this.getOption("orientation"),n=this.props,r=this.step,a=this.frame;if(a||(a=document.createElement("div"),this.frame=a,t+=1),a.className="axis "+o,!a.parentNode){if(!this.parent)throw Error("Cannot repaint time axis: no parent attached");var h=this.parent.getContainer();if(!h)throw Error("Cannot repaint time axis: parent has no container element");h.appendChild(a),t+=1}var d=a.parentNode;if(d){var l=a.nextSibling;d.removeChild(a);var p="bottom"==o&&this.props.parentHeight&&this.height?this.props.parentHeight-this.height+"px":"0px";if(t+=e(a.style,"top",i(s.top,p)),t+=e(a.style,"left",i(s.left,"0px")),t+=e(a.style,"width",i(s.width,"100%")),t+=e(a.style,"height",i(s.height,this.height+"px")),this._repaintMeasureChars(),this.step){this._repaintStart(),r.first();for(var u=void 0,c=0;r.hasNext()&&1e3>c;){c++;var f=r.getCurrent(),m=this.toScreen(f),g=r.isMajor();this.getOption("showMinorLabels")&&this._repaintMinorText(m,r.getLabelMinor()),g&&this.getOption("showMajorLabels")?(m>0&&(void 0==u&&(u=m),this._repaintMajorText(m,r.getLabelMajor())),this._repaintMajorLine(m)):this._repaintMinorLine(m),r.next()}if(this.getOption("showMajorLabels")){var v=this.toTime(0),y=r.getLabelMajor(v),w=y.length*(n.majorCharWidth||10)+10;(void 0==u||u>w)&&this._repaintMajorText(0,y)}this._repaintEnd()}this._repaintLine(),l?d.insertBefore(a,l):d.appendChild(a)}return t>0},c.prototype._repaintStart=function(){var t=this.dom,e=t.redundant;e.majorLines=t.majorLines,e.majorTexts=t.majorTexts,e.minorLines=t.minorLines,e.minorTexts=t.minorTexts,t.majorLines=[],t.majorTexts=[],t.minorLines=[],t.minorTexts=[]},c.prototype._repaintEnd=function(){M.forEach(this.dom.redundant,function(t){for(;t.length;){var e=t.pop();e&&e.parentNode&&e.parentNode.removeChild(e)}})},c.prototype._repaintMinorText=function(t,e){var i=this.dom.redundant.minorTexts.shift();if(!i){var s=document.createTextNode("");i=document.createElement("div"),i.appendChild(s),i.className="text minor",this.frame.appendChild(i)}this.dom.minorTexts.push(i),i.childNodes[0].nodeValue=e,i.style.left=t+"px",i.style.top=this.props.minorLabelTop+"px"},c.prototype._repaintMajorText=function(t,e){var i=this.dom.redundant.majorTexts.shift();if(!i){var s=document.createTextNode(e);i=document.createElement("div"),i.className="text major",i.appendChild(s),this.frame.appendChild(i)}this.dom.majorTexts.push(i),i.childNodes[0].nodeValue=e,i.style.top=this.props.majorLabelTop+"px",i.style.left=t+"px"},c.prototype._repaintMinorLine=function(t){var e=this.dom.redundant.minorLines.shift();e||(e=document.createElement("div"),e.className="grid vertical minor",this.frame.appendChild(e)),this.dom.minorLines.push(e);var i=this.props;e.style.top=i.minorLineTop+"px",e.style.height=i.minorLineHeight+"px",e.style.left=t-i.minorLineWidth/2+"px"},c.prototype._repaintMajorLine=function(t){var e=this.dom.redundant.majorLines.shift();e||(e=document.createElement("DIV"),e.className="grid vertical major",this.frame.appendChild(e)),this.dom.majorLines.push(e);var i=this.props;e.style.top=i.majorLineTop+"px",e.style.left=t-i.majorLineWidth/2+"px",e.style.height=i.majorLineHeight+"px"},c.prototype._repaintLine=function(){var t=this.dom.line,e=this.frame;this.options,this.getOption("showMinorLabels")||this.getOption("showMajorLabels")?(t?(e.removeChild(t),e.appendChild(t)):(t=document.createElement("div"),t.className="grid horizontal major",e.appendChild(t),this.dom.line=t),t.style.top=this.props.lineTop+"px"):t&&axis.parentElement&&(e.removeChild(axis.line),delete this.dom.line)},c.prototype._repaintMeasureChars=function(){var t,e=this.dom;if(!e.measureCharMinor){t=document.createTextNode("0");var i=document.createElement("DIV");i.className="text minor measure",i.appendChild(t),this.frame.appendChild(i),e.measureCharMinor=i}if(!e.measureCharMajor){t=document.createTextNode("0");var s=document.createElement("DIV");s.className="text major measure",s.appendChild(t),this.frame.appendChild(s),e.measureCharMajor=s}},c.prototype.reflow=function(){var t=0,e=M.updateProperty,i=this.frame,s=this.range;if(!s)throw Error("Cannot repaint time axis: no range configured");if(i){t+=e(this,"top",i.offsetTop),t+=e(this,"left",i.offsetLeft);var o=this.props,n=this.getOption("showMinorLabels"),r=this.getOption("showMajorLabels"),a=this.dom.measureCharMinor,h=this.dom.measureCharMajor;a&&(o.minorCharHeight=a.clientHeight,o.minorCharWidth=a.clientWidth),h&&(o.majorCharHeight=h.clientHeight,o.majorCharWidth=h.clientWidth);var d=i.parentNode?i.parentNode.offsetHeight:0;switch(d!=o.parentHeight&&(o.parentHeight=d,t+=1),this.getOption("orientation")){case"bottom":o.minorLabelHeight=n?o.minorCharHeight:0,o.majorLabelHeight=r?o.majorCharHeight:0,o.minorLabelTop=0,o.majorLabelTop=o.minorLabelTop+o.minorLabelHeight,o.minorLineTop=-this.top,o.minorLineHeight=Math.max(this.top+o.majorLabelHeight,0),o.minorLineWidth=1,o.majorLineTop=-this.top,o.majorLineHeight=Math.max(this.top+o.minorLabelHeight+o.majorLabelHeight,0),o.majorLineWidth=1,o.lineTop=0;break;case"top":o.minorLabelHeight=n?o.minorCharHeight:0,o.majorLabelHeight=r?o.majorCharHeight:0,o.majorLabelTop=0,o.minorLabelTop=o.majorLabelTop+o.majorLabelHeight,o.minorLineTop=o.minorLabelTop,o.minorLineHeight=Math.max(d-o.majorLabelHeight-this.top),o.minorLineWidth=1,o.majorLineTop=0,o.majorLineHeight=Math.max(d-this.top),o.majorLineWidth=1,o.lineTop=o.majorLabelHeight+o.minorLabelHeight;break;default:throw Error('Unkown orientation "'+this.getOption("orientation")+'"')}var l=o.minorLabelHeight+o.majorLabelHeight;t+=e(this,"width",i.offsetWidth),t+=e(this,"height",l),this._updateConversion();var p=M.cast(s.start,"Date"),u=M.cast(s.end,"Date"),c=this.toTime(5*(o.minorCharWidth||10))-this.toTime(0);this.step=new TimeStep(p,u,c),t+=e(o.range,"start",p.valueOf()),t+=e(o.range,"end",u.valueOf()),t+=e(o.range,"minimumStep",c.valueOf())}return t>0},c.prototype._updateConversion=function(){var t=this.range;if(!t)throw Error("No range configured");this.conversion=t.conversion?t.conversion(this.width):a.conversion(t.start,t.end,this.width)},f.prototype=new p,f.types={box:g,range:y,point:v},f.prototype.setOptions=l.prototype.setOptions,f.prototype.setRange=function(t){if(!(t instanceof a||t&&t.start&&t.end))throw new TypeError("Range must be an instance of Range, or an object containing start and end.");this.range=t},f.prototype.repaint=function(){var t=0,e=M.updateProperty,i=M.option.asSize,s=this.options,o=this.getOption("orientation"),n=this.defaultOptions,r=this.frame;if(!r){r=document.createElement("div"),r.className="itemset";var a=s.className;a&&M.addClassName(r,M.option.asString(a));var h=document.createElement("div");h.className="background",r.appendChild(h),this.dom.background=h;var d=document.createElement("div");d.className="foreground",r.appendChild(d),this.dom.foreground=d;var l=document.createElement("div");l.className="itemset-axis",this.dom.axis=l,this.frame=r,t+=1}if(!this.parent)throw Error("Cannot repaint itemset: no parent attached");var p=this.parent.getContainer();if(!p)throw Error("Cannot repaint itemset: parent has no container element");r.parentNode||(p.appendChild(r),t+=1),this.dom.axis.parentNode||(p.appendChild(this.dom.axis),t+=1),t+=e(r.style,"left",i(s.left,"0px")),t+=e(r.style,"top",i(s.top,"0px")),t+=e(r.style,"width",i(s.width,"100%")),t+=e(r.style,"height",i(s.height,this.height+"px")),t+=e(this.dom.axis.style,"left",i(s.left,"0px")),t+=e(this.dom.axis.style,"width",i(s.width,"100%")),t+="bottom"==o?e(this.dom.axis.style,"top",this.height+this.top+"px"):e(this.dom.axis.style,"top",this.top+"px"),this._updateConversion();var u=this,c=this.queue,m=this.itemsData,g=this.items,v={fields:[m&&m.fieldId||"id","start","end","content","type"]};return Object.keys(c).forEach(function(e){var i=c[e],o=g[e];switch(i){case"add":case"update":var r=m&&m.get(e,v);if(r){var a=r.type||r.start&&r.end&&"range"||"box",h=f.types[a];if(o&&(h&&o instanceof h?(o.data=r,t++):(t+=o.hide(),o=null)),!o){if(!h)throw new TypeError('Unknown item type "'+a+'"');o=new h(u,r,s,n),t++}o.repaint(),g[e]=o}delete c[e];break;case"remove":o&&(t+=o.hide()),delete g[e],delete c[e];break;default:console.log('Error: unknown action "'+i+'"')}}),M.forEach(this.items,function(e){e.visible?(t+=e.show(),e.reposition()):t+=e.hide()}),t>0},f.prototype.getForeground=function(){return this.dom.foreground},f.prototype.getBackground=function(){return this.dom.background},f.prototype.getAxis=function(){return this.dom.axis},f.prototype.reflow=function(){var t=0,e=this.options,i=e.margin&&e.margin.axis||this.defaultOptions.margin.axis,s=e.margin&&e.margin.item||this.defaultOptions.margin.item,o=M.updateProperty,n=M.option.asNumber,r=M.option.asSize,a=this.frame;if(a){this._updateConversion(),M.forEach(this.items,function(e){t+=e.reflow()}),this.stack.update();var h,d=n(e.maxHeight),l=null!=r(e.height);if(l)h=a.offsetHeight;else{var p=this.stack.ordered;if(p.length){var u=p[0].top,c=p[0].top+p[0].height;M.forEach(p,function(t){u=Math.min(u,t.top),c=Math.max(c,t.top+t.height)}),h=c-u+i+s}else h=i+s}null!=d&&(h=Math.min(h,d)),t+=o(this,"height",h),t+=o(this,"top",a.offsetTop),t+=o(this,"left",a.offsetLeft),t+=o(this,"width",a.offsetWidth)}else t+=1;return t>0},f.prototype.hide=function(){var t=!1;return this.frame&&this.frame.parentNode&&(this.frame.parentNode.removeChild(this.frame),t=!0),this.dom.axis&&this.dom.axis.parentNode&&(this.dom.axis.parentNode.removeChild(this.dom.axis),t=!0),t},f.prototype.setItems=function(t){var e,i=this,s=this.itemsData;if(s&&(M.forEach(this.listeners,function(t,e){s.unsubscribe(e,t)}),e=s.getIds(),this._onRemove(e)),t){if(!(t instanceof o||t instanceof n))throw new TypeError("Data must be an instance of DataSet");this.itemsData=t}else this.itemsData=null;if(this.itemsData){var r=this.id;M.forEach(this.listeners,function(t,e){i.itemsData.subscribe(e,t,r)}),e=this.itemsData.getIds(),this._onAdd(e)}},f.prototype.getItems=function(){return this.itemsData},f.prototype._onUpdate=function(t){this._toQueue("update",t)},f.prototype._onAdd=function(t){this._toQueue("add",t)},f.prototype._onRemove=function(t){this._toQueue("remove",t)},f.prototype._toQueue=function(t,e){var i=this.queue;e.forEach(function(e){i[e]=t}),this.controller&&this.requestRepaint()},f.prototype._updateConversion=function(){var t=this.range;if(!t)throw Error("No range configured");this.conversion=t.conversion?t.conversion(this.width):a.conversion(t.start,t.end,this.width)},f.prototype.toTime=function(t){var e=this.conversion;return new Date(t/e.factor+e.offset)},f.prototype.toScreen=function(t){var e=this.conversion;return(t.valueOf()-e.offset)*e.factor},m.prototype.select=function(){this.selected=!0},m.prototype.unselect=function(){this.selected=!1},m.prototype.show=function(){return!1},m.prototype.hide=function(){return!1},m.prototype.repaint=function(){return!1},m.prototype.reflow=function(){return!1},g.prototype=new m(null,null),g.prototype.select=function(){this.selected=!0},g.prototype.unselect=function(){this.selected=!1},g.prototype.repaint=function(){var t=!1,e=this.dom;if(e||(this._create(),e=this.dom,t=!0),e){if(!this.parent)throw Error("Cannot repaint item: no parent attached");var i=this.parent.getForeground();if(!i)throw Error("Cannot repaint time axis: parent has no foreground container element");var s=this.parent.getBackground();if(!s)throw Error("Cannot repaint time axis: parent has no background container element");var o=this.parent.getAxis();if(!s)throw Error("Cannot repaint time axis: parent has no axis container element");if(e.box.parentNode||(i.appendChild(e.box),t=!0),e.line.parentNode||(s.appendChild(e.line),t=!0),e.dot.parentNode||(o.appendChild(e.dot),t=!0),this.data.content!=this.content){if(this.content=this.data.content,this.content instanceof Element)e.content.innerHTML="",e.content.appendChild(this.content);else{if(void 0==this.data.content)throw Error('Property "content" missing in item '+this.data.id);e.content.innerHTML=this.content}t=!0}var n=(this.data.className?" "+this.data.className:"")+(this.selected?" selected":"");this.className!=n&&(this.className=n,e.box.className="item box"+n,e.line.className="item line"+n,e.dot.className="item dot"+n,t=!0)}return t},g.prototype.show=function(){return this.dom&&this.dom.box.parentNode?!1:this.repaint()},g.prototype.hide=function(){var t=!1,e=this.dom;return e&&(e.box.parentNode&&(e.box.parentNode.removeChild(e.box),t=!0),e.line.parentNode&&e.line.parentNode.removeChild(e.line),e.dot.parentNode&&e.dot.parentNode.removeChild(e.dot)),t},g.prototype.reflow=function(){var t,e,i,s,o,n,r,a,h,d,l,p,u=0;if(void 0==this.data.start)throw Error('Property "start" missing in item '+this.data.id);if(l=this.data,p=this.parent&&this.parent.range,this.visible=l&&p?l.start>p.start&&l.start0},g.prototype._create=function(){var t=this.dom;t||(this.dom=t={},t.box=document.createElement("DIV"),t.content=document.createElement("DIV"),t.content.className="content",t.box.appendChild(t.content),t.line=document.createElement("DIV"),t.line.className="line",t.dot=document.createElement("DIV"),t.dot.className="dot")},g.prototype.reposition=function(){var t=this.dom,e=this.props,i=this.options.orientation||this.defaultOptions.orientation;if(t){var s=t.box,o=t.line,n=t.dot;s.style.left=this.left+"px",s.style.top=this.top+"px",o.style.left=e.line.left+"px","top"==i?(o.style.top="0px",o.style.height=this.top+"px"):(o.style.top=this.top+this.height+"px",o.style.height=Math.max(this.parent.height-this.top-this.height+this.props.dot.height/2,0)+"px"),n.style.left=e.dot.left+"px",n.style.top=e.dot.top+"px"}},v.prototype=new m(null,null),v.prototype.select=function(){this.selected=!0},v.prototype.unselect=function(){this.selected=!1},v.prototype.repaint=function(){var t=!1,e=this.dom;if(e||(this._create(),e=this.dom,t=!0),e){if(!this.parent)throw Error("Cannot repaint item: no parent attached");var i=this.parent.getForeground();if(!i)throw Error("Cannot repaint time axis: parent has no foreground container element");if(e.point.parentNode||(i.appendChild(e.point),i.appendChild(e.point),t=!0),this.data.content!=this.content){if(this.content=this.data.content,this.content instanceof Element)e.content.innerHTML="",e.content.appendChild(this.content);else{if(void 0==this.data.content)throw Error('Property "content" missing in item '+this.data.id);e.content.innerHTML=this.content}t=!0}var s=(this.data.className?" "+this.data.className:"")+(this.selected?" selected":"");this.className!=s&&(this.className=s,e.point.className="item point"+s,t=!0)}return t},v.prototype.show=function(){return this.dom&&this.dom.point.parentNode?!1:this.repaint()},v.prototype.hide=function(){var t=!1,e=this.dom;return e&&e.point.parentNode&&(e.point.parentNode.removeChild(e.point),t=!0),t},v.prototype.reflow=function(){var t,e,i,s,o,n,r,a,h,d,l=0;if(void 0==this.data.start)throw Error('Property "start" missing in item '+this.data.id);if(h=this.data,d=this.parent&&this.parent.range,this.visible=h&&d?h.start>d.start&&h.start0},v.prototype._create=function(){var t=this.dom;t||(this.dom=t={},t.point=document.createElement("div"),t.content=document.createElement("div"),t.content.className="content",t.point.appendChild(t.content),t.dot=document.createElement("div"),t.dot.className="dot",t.point.appendChild(t.dot))},v.prototype.reposition=function(){var t=this.dom,e=this.props;t&&(t.point.style.top=this.top+"px",t.point.style.left=this.left+"px",t.content.style.marginLeft=e.content.marginLeft+"px",t.dot.style.top=e.dot.top+"px")},y.prototype=new m(null,null),y.prototype.select=function(){this.selected=!0},y.prototype.unselect=function(){this.selected=!1},y.prototype.repaint=function(){var t=!1,e=this.dom;if(e||(this._create(),e=this.dom,t=!0),e){if(!this.parent)throw Error("Cannot repaint item: no parent attached");var i=this.parent.getForeground();if(!i)throw Error("Cannot repaint time axis: parent has no foreground container element");if(e.box.parentNode||(i.appendChild(e.box),t=!0),this.data.content!=this.content){if(this.content=this.data.content,this.content instanceof Element)e.content.innerHTML="",e.content.appendChild(this.content);else{if(void 0==this.data.content)throw Error('Property "content" missing in item '+this.data.id);e.content.innerHTML=this.content}t=!0}var s=this.data.className?""+this.data.className:"";this.className!=s&&(this.className=s,e.box.className="item range"+s,t=!0)}return t},y.prototype.show=function(){return this.dom&&this.dom.box.parentNode?!1:this.repaint()},y.prototype.hide=function(){var t=!1,e=this.dom;return e&&e.box.parentNode&&(e.box.parentNode.removeChild(e.box),t=!0),t},y.prototype.reflow=function(){var t,e,i,s,o,n,r,a,h,d,l,p,u,c,f,m,g=0;if(void 0==this.data.start)throw Error('Property "start" missing in item '+this.data.id);if(void 0==this.data.end)throw Error('Property "end" missing in item '+this.data.id);return h=this.data,d=this.parent&&this.parent.range,this.visible=h&&d?h.startd.start:!1,this.visible&&(t=this.dom,t?(e=this.props,i=this.options,n=this.parent,r=n.toScreen(this.data.start),a=n.toScreen(this.data.end),l=M.updateProperty,p=t.box,u=n.width,f=i.orientation||this.defaultOptions.orientation,s=i.margin&&i.margin.axis||this.defaultOptions.margin.axis,o=i.padding||this.defaultOptions.padding,g+=l(e.content,"width",t.content.offsetWidth),g+=l(this,"height",p.offsetHeight),-u>r&&(r=-u),a>2*u&&(a=2*u),c=0>r?Math.min(-r,a-r-e.content.width-2*o):0,g+=l(e.content,"left",c),"top"==f?(m=s,g+=l(this,"top",m)):(m=n.height-this.height-s,g+=l(this,"top",m)),g+=l(this,"left",r),g+=l(this,"width",Math.max(a-r,1))):g+=1),g>0},y.prototype._create=function(){var t=this.dom;t||(this.dom=t={},t.box=document.createElement("div"),t.content=document.createElement("div"),t.content.className="content",t.box.appendChild(t.content))},y.prototype.reposition=function(){var t=this.dom,e=this.props;t&&(t.box.style.top=this.top+"px",t.box.style.left=this.left+"px",t.box.style.width=this.width+"px",t.content.style.left=e.content.left+"px")},w.prototype=new l,w.prototype.setOptions=l.prototype.setOptions,w.prototype.getContainer=function(){return this.parent.getContainer()},w.prototype.setItems=function(t){if(this.itemset&&(this.itemset.hide(),this.itemset.setItems(),this.parent.controller.remove(this.itemset),this.itemset=null),t){var e=this.groupId,i=Object.create(this.options); +this.itemset=new f(this,null,i),this.itemset.setRange(this.parent.range),this.view=new n(t,{filter:function(t){return t.group==e}}),this.itemset.setItems(this.view),this.parent.controller.add(this.itemset)}},w.prototype.repaint=function(){return!1},w.prototype.reflow=function(){var t=0,e=M.updateProperty;return t+=e(this,"top",this.itemset?this.itemset.top:0),t+=e(this,"height",this.itemset?this.itemset.height:0),t>0},b.prototype=new p,b.prototype.setOptions=l.prototype.setOptions,b.prototype.setRange=function(){},b.prototype.setItems=function(t){this.itemsData=t;for(var e in this.groups)if(this.groups.hasOwnProperty(e)){var i=this.groups[e];i.setItems(t)}},b.prototype.getItems=function(){return this.itemsData},b.prototype.setRange=function(t){this.range=t},b.prototype.setGroups=function(t){var e,i=this;if(this.groupsData&&(M.forEach(this.listeners,function(t,e){i.groupsData.unsubscribe(e,t)}),e=this.groupsData.getIds(),this._onRemove(e)),t?t instanceof o?this.groupsData=t:(this.groupsData=new o({fieldTypes:{start:"Date",end:"Date"}}),this.groupsData.add(t)):this.groupsData=null,this.groupsData){var s=this.id;M.forEach(this.listeners,function(t,e){i.groupsData.subscribe(e,t,s)}),e=this.groupsData.getIds(),this._onAdd(e)}},b.prototype.getGroups=function(){return this.groupsData},b.prototype.repaint=function(){var t=0,e=M.updateProperty,i=M.option.asSize,s=this.options,o=this.frame;if(!o){o=document.createElement("div"),o.className="groupset";var n=s.className;n&&M.addClassName(o,M.option.asString(n)),this.frame=o,t+=1}if(!this.parent)throw Error("Cannot repaint groupset: no parent attached");var r=this.parent.getContainer();if(!r)throw Error("Cannot repaint groupset: parent has no container element");o.parentNode||(r.appendChild(o),t+=1),t+=e(o.style,"height",i(s.height,this.height+"px")),t+=e(o.style,"top",i(s.top,"0px")),t+=e(o.style,"left",i(s.left,"0px")),t+=e(o.style,"width",i(s.width,"100%"));var a=this,h=this.queue,d=this.groups,l=this.groupsData,p=Object.keys(h);if(p.length){p.forEach(function(t){var e=h[t],i=d[t];switch(e){case"add":case"update":if(!i){var s=Object.create(a.options);i=new w(a,t,s),i.setItems(a.itemsData),d[t]=i,a.controller.add(i)}i.data=l.get(t),delete h[t];break;case"remove":i&&(i.setItems(),delete d[t],a.controller.remove(i)),delete h[t];break;default:console.log('Error: unknown action "'+e+'"')}});for(var u=this.groupsData.getIds({order:this.options.groupsOrder}),c=0;u.length>c;c++)(function(t,e){var i=0;e&&(i=function(){return e.top+e.height}),t.setOptions({top:i})})(d[u[c]],d[u[c-1]]);t++}return t>0},b.prototype.getContainer=function(){return this.frame},b.prototype.reflow=function(){var t=0,e=this.options,i=M.updateProperty,s=M.option.asNumber,o=M.option.asSize,n=this.frame;if(n){var r,a=s(e.maxHeight),h=null!=o(e.height);if(h)r=n.offsetHeight;else{r=0;for(var d in this.groups)if(this.groups.hasOwnProperty(d)){var l=this.groups[d];r+=l.height}}null!=a&&(r=Math.min(r,a)),t+=i(this,"height",r),t+=i(this,"top",n.offsetTop),t+=i(this,"left",n.offsetLeft),t+=i(this,"width",n.offsetWidth)}return t>0},b.prototype.hide=function(){return this.frame&&this.frame.parentNode?(this.frame.parentNode.removeChild(this.frame),!0):!1},b.prototype.show=function(){return this.frame&&this.frame.parentNode?!1:this.repaint()},b.prototype._onUpdate=function(t){this._toQueue(t,"update")},b.prototype._onAdd=function(t){this._toQueue(t,"add")},b.prototype._onRemove=function(t){this._toQueue(t,"remove")},b.prototype._toQueue=function(t,e){var i=this.queue;t.forEach(function(t){i[t]=e}),this.controller&&this.requestRepaint()},_.prototype.setOptions=function(t){t&&M.extend(this.options,t),this.controller.reflow(),this.controller.repaint()},_.prototype.setItems=function(t){var e,i=null==this.itemsData;if(t?t instanceof o&&(e=t):e=null,t instanceof o||(e=new o({fieldTypes:{start:"Date",end:"Date"}}),e.add(t)),this.itemsData=e,this.content.setItems(e),i&&(void 0==this.options.start||void 0==this.options.end)){var s=this.getItemRange(),n=s.min,r=s.max;if(null!=n&&null!=r){var a=r.valueOf()-n.valueOf();n=new Date(n.valueOf()-.05*a),r=new Date(r.valueOf()+.05*a)}void 0!=this.options.start&&(n=new Date(this.options.start.valueOf())),void 0!=this.options.end&&(r=new Date(this.options.end.valueOf())),(null!=n||null!=r)&&this.range.setRange(n,r)}},_.prototype.setGroups=function(t){var e=this;this.groupsData=t;var i=this.groupsData?b:f;if(!(this.content instanceof i)){this.content&&(this.content.hide(),this.content.setItems&&this.content.setItems(),this.content.setGroups&&this.content.setGroups(),this.controller.remove(this.content));var s=Object.create(this.options);M.extend(s,{top:function(){return"top"==e.options.orientation?e.timeaxis.height:e.root.height-e.timeaxis.height-e.content.height},height:function(){return e.options.height?e.root.height-e.timeaxis.height:null},maxHeight:function(){if(e.options.maxHeight){if(!M.isNumber(e.options.maxHeight))throw new TypeError("Number expected for property maxHeight");return e.options.maxHeight-e.timeaxis.height}return null}}),this.content=new i(this.root,[this.timeaxis],s),this.content.setRange&&this.content.setRange(this.range),this.content.setItems&&this.content.setItems(this.itemsData),this.content.setGroups&&this.content.setGroups(this.groupsData),this.controller.add(this.content)}},_.prototype.getItemRange=function(){var t=this.itemsData,e=null,i=null;if(t){var s=t.min("start");e=s?s.start.valueOf():null;var o=t.max("start");o&&(i=o.start.valueOf());var n=t.max("end");n&&(i=null==i?n.end.valueOf():Math.max(i,n.end.valueOf()))}return{min:null!=e?new Date(e):null,max:null!=i?new Date(i):null}},x.prototype.draw=function(t,e,i,s){var o,n,r;if(void 0!=s?(o=t,n=e,r=i):void 0!=i?(o=t,n=e,r=void 0,s=i):void 0!=e?(o=t,n=void 0,r=void 0,s=e):void 0!=t&&(o=void 0,n=void 0,r=void 0,s=t),void 0!=s){if(void 0!=s.width&&(this.width=s.width),void 0!=s.height&&(this.height=s.height),void 0!=s.stabilize&&(this.stabilize=s.stabilize),void 0!=s.selectable&&(this.selectable=s.selectable),s.edges){for(var a in s.edges)s.edges.hasOwnProperty(a)&&(this.constants.edges[a]=s.edges[a]);void 0!=s.edges.length&&s.nodes&&void 0==s.nodes.distance&&(this.constants.edges.length=s.edges.length,this.constants.nodes.distance=1.25*s.edges.length),s.edges.fontColor||(this.constants.edges.fontColor=s.edges.color),void 0!=s.edges.dashlength&&(this.constants.edges.dashlength=s.edges.dashlength),void 0!=s.edges.dashgap&&(this.constants.edges.dashgap=s.edges.dashgap),void 0!=s.edges.altdashlength&&(this.constants.edges.altdashlength=s.edges.altdashlength)}if(s.nodes)for(a in s.nodes)s.nodes.hasOwnProperty(a)&&(this.constants.nodes[a]=s.nodes[a]);if(s.packages)for(a in s.packages)s.packages.hasOwnProperty(a)&&(this.constants.packages[a]=s.packages[a]);if(s.groups)for(var h in s.groups)if(s.groups.hasOwnProperty(h)){var d=s.groups[h];this.groups.add(h,d)}}this._setBackgroundColor(s.backgroundColor),this._setSize(this.width,this.height),this._setTranslation(0,0),this._setScale(1),this.hasTimestamps=!1,this.setNodes(o),this.setEdges(n),this.setPackages(r),this._reposition(),this.stabilize&&this._doStabilize(),this.start();var l=this,p=function(){l._redraw()};this.images.setOnloadCallback(p),this.trigger("ready")},x.prototype.trigger=function(t,e){E.trigger(this,t,e),google&&google.visualization&&google.visualization.events&&google.visualization.events.trigger(this,t,e)},x.prototype._create=function(){for(;this.containerElement.hasChildNodes();)this.containerElement.removeChild(this.containerElement.firstChild);if(this.frame=document.createElement("div"),this.frame.className="network-frame",this.frame.style.position="relative",this.frame.style.overflow="hidden",this.frame.canvas=document.createElement("canvas"),this.frame.canvas.style.position="relative",this.frame.appendChild(this.frame.canvas),!this.frame.canvas.getContext){var t=document.createElement("DIV");t.style.color="red",t.style.fontWeight="bold",t.style.padding="10px",t.innerHTML="Error: your browser does not support HTML canvas",this.frame.canvas.appendChild(t)}var e=this,i=function(t){e._onMouseDown(t)},s=function(t){e._onMouseMoveTitle(t)},o=function(t){e._onMouseWheel(t)},n=function(t){e._onTouchStart(t)};x.addEventListener(this.frame.canvas,"mousedown",i),x.addEventListener(this.frame.canvas,"mousemove",s),x.addEventListener(this.frame.canvas,"mousewheel",o),x.addEventListener(this.frame.canvas,"touchstart",n),this.containerElement.appendChild(this.frame)},x.prototype._setBackgroundColor=function(t){var e="white",i="lightgray",s=1;if("string"==typeof t)e=t,i="none",s=0;else if("object"==typeof t)void 0!=t.fill&&(e=t.fill),void 0!=t.stroke&&(i=t.stroke),void 0!=t.strokeWidth&&(s=t.strokeWidth);else if(void 0!=t)throw"Unsupported type of backgroundColor";this.frame.style.boxSizing="border-box",this.frame.style.backgroundColor=e,this.frame.style.borderColor=i,this.frame.style.borderWidth=s+"px",this.frame.style.borderStyle="solid"},x.prototype._onMouseDown=function(t){if(t=t||window.event,this.selectable&&(this.leftButtonDown&&this._onMouseUp(t),this.leftButtonDown=t.which?1==t.which:1==t.button,this.leftButtonDown||this.touchDown)){var e=this;this.onmousemove||(this.onmousemove=function(t){e._onMouseMove(t)},x.addEventListener(document,"mousemove",e.onmousemove)),this.onmouseup||(this.onmouseup=function(t){e._onMouseUp(t)},x.addEventListener(document,"mouseup",e.onmouseup)),x.preventDefault(t),this.startMouseX=t.clientX||t.targetTouches[0].clientX,this.startMouseY=t.clientY||t.targetTouches[0].clientY,this.startFrameLeft=x._getAbsoluteLeft(this.frame.canvas),this.startFrameTop=x._getAbsoluteTop(this.frame.canvas),this.startTranslation=this._getTranslation(),this.ctrlKeyDown=t.ctrlKey,this.shiftKeyDown=t.shiftKey;var i={left:this._xToCanvas(this.startMouseX-this.startFrameLeft),top:this._yToCanvas(this.startMouseY-this.startFrameTop),right:this._xToCanvas(this.startMouseX-this.startFrameLeft),bottom:this._yToCanvas(this.startMouseY-this.startFrameTop)},s=this._getNodesOverlappingWith(i);if(this.startClickedObj=s.length>0?s[s.length-1]:void 0,this.startClickedObj){var o=this.nodes[this.startClickedObj.row];this.startClickedObj.xFixed=o.xFixed,this.startClickedObj.yFixed=o.yFixed,o.xFixed=!0,o.yFixed=!0,this.ctrlKeyDown&&o.isSelected()?this._unselectNodes([this.startClickedObj]):this._selectNodes([this.startClickedObj],this.ctrlKeyDown),this.hasMovingNodes||this._redraw()}else this.shiftKeyDown||(this.moved=!1)}},x.prototype._onMouseMove=function(t){if(t=t||window.event,this.selectable){var e=t.clientX||t.targetTouches&&t.targetTouches[0].clientX||0,i=t.clientY||t.targetTouches&&t.targetTouches[0].clientY||0;if(this.mouseX=e,this.mouseY=i,this.startClickedObj){var s=this.nodes[this.startClickedObj.row];this.startClickedObj.xFixed||(s.x=this._xToCanvas(e-this.startFrameLeft)),this.startClickedObj.yFixed||(s.y=this._yToCanvas(i-this.startFrameTop)),this.hasMovingNodes||(this.hasMovingNodes=!0,this.start())}else if(this.shiftKeyDown){void 0==this.frame.selRect&&(this.frame.selRect=document.createElement("DIV"),this.frame.appendChild(this.frame.selRect),this.frame.selRect.style.position="absolute",this.frame.selRect.style.border="1px dashed red");var o=Math.min(this.startMouseX,e)-this.startFrameLeft,n=Math.min(this.startMouseY,i)-this.startFrameTop,r=Math.max(this.startMouseX,e)-this.startFrameLeft,a=Math.max(this.startMouseY,i)-this.startFrameTop;this.frame.selRect.style.left=o+"px",this.frame.selRect.style.top=n+"px",this.frame.selRect.style.width=r-o+"px",this.frame.selRect.style.height=a-n+"px"}else{var h=e-this.startMouseX,d=i-this.startMouseY;this._setTranslation(this.startTranslation.x+h,this.startTranslation.y+d),this._redraw(),this.moved=!0}x.preventDefault(t)}},x.prototype._onMouseUp=function(t){if(t=t||window.event,this.selectable){this.onmousemove&&(x.removeEventListener(document,"mousemove",this.onmousemove),this.onmousemove=void 0),this.onmouseup&&(x.removeEventListener(document,"mouseup",this.onmouseup),this.onmouseup=void 0),x.preventDefault(t);var e=t.clientX||this.mouseX||0,i=t.clientY||this.mouseY||0,s=t?t.ctrlKey:window.event.ctrlKey;if(this.startClickedObj){var o=this.nodes[this.startClickedObj.row];o.xFixed=this.startClickedObj.xFixed,o.yFixed=this.startClickedObj.yFixed}else if(this.shiftKeyDown){var n={left:this._xToCanvas(Math.min(this.startMouseX,e)-this.startFrameLeft),top:this._yToCanvas(Math.min(this.startMouseY,i)-this.startFrameTop),right:this._xToCanvas(Math.max(this.startMouseX,e)-this.startFrameLeft),bottom:this._yToCanvas(Math.max(this.startMouseY,i)-this.startFrameTop)},r=this._getNodesOverlappingWith(n);this._selectNodes(r,s),this.redraw(),this.frame.selRect&&(this.frame.removeChild(this.frame.selRect),this.frame.selRect=void 0)}else this.ctrlKeyDown||this.moved||(this._unselectNodes(),this._redraw());this.leftButtonDown=!1,this.ctrlKeyDown=!1}},x.prototype._onMouseWheel=function(t){t=t||window.event;var e=t.clientX,i=t.clientY,s=0;if(t.wheelDelta?s=t.wheelDelta/120:t.detail&&(s=-t.detail/3),s){var o=s/10;0>s&&(o/=1-o);var n=this._getScale(),r=n*(1+o);.01>r&&(r=.01),r>10&&(r=10);var a=x._getAbsoluteLeft(this.frame.canvas),h=x._getAbsoluteTop(this.frame.canvas),d=e-a,l=i-h,p=this._getTranslation(),u=r/n,c=(1-u)*d+p.x*u,f=(1-u)*l+p.y*u;this._setScale(r),this._setTranslation(c,f),this._redraw()}x.preventDefault(t)},x.prototype._onMouseMoveTitle=function(t){t=t||window.event;var e=t.clientX,i=t.clientY;this.startFrameLeft=this.startFrameLeft||x._getAbsoluteLeft(this.frame.canvas),this.startFrameTop=this.startFrameTop||x._getAbsoluteTop(this.frame.canvas);var s=e-this.startFrameLeft,o=i-this.startFrameTop;this.popupNode&&this._checkHidePopup(s,o);var n=this,r=function(){n._checkShowPopup(s,o)};this.popupTimer&&clearInterval(this.popupTimer),this.leftButtonDown||(this.popupTimer=setTimeout(r,300))},x.prototype._checkShowPopup=function(t,e){var i,s,o={left:this._xToCanvas(t),top:this._yToCanvas(e),right:this._xToCanvas(t),bottom:this._yToCanvas(e)},n=this.popupNode;if(void 0==this.popupNode)for(i=0,s=this.packages.length;s>i;i++){var r=this.packages[i];if(void 0!=r.getTitle()&&r.isOverlappingWith(o)){this.popupNode=r;break}}if(void 0==this.popupNode){var a=this.nodes;for(i=a.length-1;i>=0;i--){var h=a[i];if(void 0!=h.getTitle()&&h.isOverlappingWith(o)){this.popupNode=h;break}}}if(void 0==this.popupNode){var d=this.edges;for(i=0,s=d.length;s>i;i++){var l=d[i];if(void 0!=l.getTitle()&&l.isOverlappingWith(o)){this.popupNode=l;break}}}if(this.popupNode){if(this.popupNode!=n){var p=this;p.popup||(p.popup=new x.Popup(p.frame)),p.popup.setPosition(t-3,e-3),p.popup.setText(p.popupNode.getTitle()),p.popup.show()}}else this.popup&&this.popup.hide()},x.prototype._checkHidePopup=function(t,e){var i={left:t,top:e,right:t,bottom:e};this.popupNode&&this.popupNode.isOverlappingWith(i)||(this.popupNode=void 0,this.popup&&this.popup.hide())},x.prototype._onTouchStart=function(t){if(x.preventDefault(t),!this.touchDown){this.touchDown=!0;var e=this;this.ontouchmove||(this.ontouchmove=function(t){e._onTouchMove(t)},x.addEventListener(document,"touchmove",this.ontouchmove)),this.ontouchend||(this.ontouchend=function(t){e._onTouchEnd(t)},x.addEventListener(document,"touchend",this.ontouchend)),this._onMouseDown(t)}},x.prototype._onTouchMove=function(t){x.preventDefault(t),this._onMouseMove(t)},x.prototype._onTouchEnd=function(t){x.preventDefault(t),this.touchDown=!1,this.ontouchmove&&(x.removeEventListener(document,"touchmove",this.ontouchmove),this.ontouchmove=void 0),this.ontouchend&&(x.removeEventListener(document,"touchend",this.ontouchend),this.ontouchend=void 0),this._onMouseUp(t)},x.prototype._unselectNodes=function(t,e){var i,s,o,n=!1;if(t)for(i=0,s=t.length;s>i;i++){o=t[i].row,this.nodes[o].unselect();for(var r=0;this.selection.length>r;)this.selection[r].row==o?(this.selection.splice(r,1),n=!0):r++}else if(this.selection&&this.selection.length){for(i=0,s=this.selection.length;s>i;i++)o=this.selection[i].row,this.nodes[o].unselect(),n=!0;this.selection=[]}return!n||1!=e&&void 0!=e||this.trigger("select"),n},x.prototype._selectNodes=function(t,e){var i,s,o=!1,n=!0;if(t.length!=this.selection.length)n=!1;else for(i=0,s=Math.min(t.length,this.selection.length);s>i;i++)if(t[i].row!=this.selection[i].row){n=!1;break}if(n)return o;if(void 0==e||0==e){var r=!1;o=this._unselectNodes(void 0,r)}for(i=0,s=t.length;s>i;i++){for(var a=t[i].row,h=!1,d=0,l=this.selection.length;l>d;d++)if(this.selection[d].row==a){h=!0;break}h||(this.nodes[a].select(),this.selection.push(t[i]),o=!0)}return o&&this.trigger("select"),o},x.prototype._getNodesOverlappingWith=function(t){for(var e=[],i=0;this.nodes.length>i;i++)if(this.nodes[i].isOverlappingWith(t)){var s={row:i};e.push(s)}return e},x.prototype.getSelection=function(){for(var t=[],e=0;this.selection.length>e;e++){var i=this.selection[e].row;t.push({row:i})}return t},x.prototype.setSelection=function(t){var e,i,s;if(void 0==t.length)throw"Selection must be an array with objects";for(e=0,i=this.selection.length;i>e;e++)s=this.selection[e].row,this.nodes[s].unselect();for(this.selection=[],e=0,i=t.length;i>e;e++){if(s=t[e].row,void 0==s)throw"Parameter row missing in selection object";if(s>this.nodes.length-1)throw"Parameter row out of range";var o={row:s};this.selection.push(o),this.nodes[s].select()}this.redraw()},x.prototype._getConnectionCount=function(t){function e(t){for(var e=[],s=0,o=t.length;o>s;s++)for(var n=t[s],r=0,a=i.length;a>r;r++){var h=null;i[r].from==n?h=i[r].to:i[r].to==n&&(h=i[r].from);var d,l;if(h)for(d=0,l=t.length;l>d;d++)if(t[d]==h){h=null;break}if(h)for(d=0,l=e.length;l>d;d++)if(e[d]==h){h=null;break}h&&e.push(h)}return e}var i=this.edges;void 0==t&&(t=1);var s,o,n=[],r=this.nodes;for(s=0,o=r.length;o>s;s++){for(var a=[r[s]],h=0;t>h;h++)a=a.concat(e(a));n.push(a)}var d=[];for(s=0,len=n.length;len>s;s++)d.push(n[s].length);return d},x.prototype._setSize=function(t,e){this.frame.style.width=t,this.frame.style.height=e,this.frame.canvas.style.width="100%",this.frame.canvas.style.height="100%",this.frame.canvas.width=this.frame.canvas.clientWidth,this.frame.canvas.height=this.frame.canvas.clientHeight,this.slider&&this.slider.redraw()},x.tableToArray=function(t){var e,i=[],s=t.getNumberOfColumns(),o={};for(e=0;s>e;e++){var n=t.getColumnLabel(e);o[n]=e}for(var r=t.getNumberOfRows(),a=0;r>a;a++){var h={};for(e in o)o.hasOwnProperty(e)&&(h[e]=t.getValue(a,o[e]));i.push(h)}return i},x.prototype.addNodes=function(t){var e;if(google&&google.visualization&&google.visualization.DataTable&&t instanceof google.visualization.DataTable)e=x.tableToArray(t);else{if(!x.isArray(t))return;e=t}for(var i=!1,s=e.length,o=0;s>o;o++){var n=e[o];if(void 0!=n.value&&(i=!0),void 0==n.id)throw"Column 'id' missing in table with nodes (row "+o+")";this._createNode(n)}i&&this._updateValueRange(this.nodes),this.start()},x.prototype.setNodes=function(t){var e;if(google&&google.visualization&&google.visualization.DataTable&&t instanceof google.visualization.DataTable)e=x.tableToArray(t);else{if(!x.isArray(t))return;e=t}this.hasMovingNodes=!1,this.nodesTable=e,this.nodes=[],this.selection=[];for(var i=!1,s=e.length,o=0;s>o;o++){var n=e[o];if(void 0!=n.value&&(i=!0),n.timestamp&&(this.hasTimestamps=this.hasTimestamps||n.timestamp),void 0==n.id)throw"Column 'id' missing in table with nodes (row "+o+")";this._createNode(n)}i&&this._updateValueRange(this.nodes)},x.prototype._filterNodes=function(t){if(void 0!=this.nodesTable){if(void 0!==t)for(var e=this.nodes,i=0;e.length>i;){var s=e[i].timestamp;void 0!==s&&s>t?e.splice(i,1):i++}for(var o=this.nodesTable,n=o.length,r=0;n>r;r++){var a=o[r];if(void 0===a.id)throw"Column 'id' missing in table with nodes (row "+r+")";var h=a.timestamp?a.timestamp:void 0,d=!0;void 0!==h&&void 0!==t&&h>t&&(d=!1),d&&this._createNode(a)}this.start()}},x.prototype._createNode=function(t){var e,i,s,o,n=t.action?t.action:"update";if("create"===n)s=new x.Node(t,this.images,this.groups,this.constants),e=t.id,i=void 0!==e?this._findNode(e):void 0,void 0!==i?(o=this.nodes[i],this.nodes[i]=s,o.selected&&this._unselectNodes([{row:i}],!1)):this.nodes.push(s),s.isFixed()||(this.hasMovingNodes=!0);else if("update"===n){if(e=t.id,void 0===e)throw"Cannot update a node without id";i=this._findNode(e),void 0!==i?this.nodes[i].setProperties(t,this.constants):(s=new x.Node(t,this.images,this.groups,this.constants),this.nodes.push(s),s.isFixed()||(this.hasMovingNodes=!0))}else{if("delete"!==n)throw"Unknown action "+n+". Choose 'create', 'update', or 'delete'.";if(e=t.id,void 0===e)throw"Cannot delete node without its id";if(i=this._findNode(e),void 0===i)throw"Node with id "+e+" not found";o=this.nodes[i],o.selected&&this._unselectNodes([{row:i}],!1),this.nodes.splice(i,1)}},x.prototype._findNode=function(t){for(var e=this.nodes,i=0,s=e.length;s>i;i++)if(e[i].id===t)return i;return void 0},x.prototype._findNodeByRow=function(t){return this.nodes[t]},x.prototype.setEdges=function(t){var e;if(google&&google.visualization&&google.visualization.DataTable&&t instanceof google.visualization.DataTable)e=x.tableToArray(t);else{if(!x.isArray(t))return;e=t}this.edgesTable=e,this.edges=[],this.hasMovingEdges=!1;for(var i=!1,s=e.length,o=0;s>o;o++){var n=e[o];if(void 0===n.from)throw"Column 'from' missing in table with edges (row "+o+")";if(void 0===n.to)throw"Column 'to' missing in table with edges (row "+o+")";void 0!=n.timestamp&&(this.hasTimestamps=this.hasTimestamps||n.timestamp),void 0!=n.value&&(i=!0),this._createEdge(n)}i&&this._updateValueRange(this.edges)},x.prototype.addEdges=function(t){var e;if(google&&google.visualization&&google.visualization.DataTable&&t instanceof google.visualization.DataTable)e=x.tableToArray(t);else{if(!x.isArray(t))return;e=t}for(var i=!1,s=e.length,o=0;s>o;o++){var n=e[o];if(void 0===n.from)throw"Column 'from' missing in table with edges (row "+o+")";if(void 0===n.to)throw"Column 'to' missing in table with edges (row "+o+")";void 0!=n.value&&(i=!0),this._createEdge(n)}i&&this._updateValueRange(this.edges),this.start()},x.prototype._filterEdges=function(t){if(void 0!=this.edgesTable){if(void 0!==t)for(var e=this.edges,i=0;e.length>i;){var s=e[i].timestamp;void 0!==s&&s>t?e.splice(i,1):i++}for(var o=this.edgesTable,n=o.length,r=0;n>r;r++){var a=o[r];if(void 0===a.from)throw"Column 'from' missing in table with edges (row "+r+")";if(void 0===a.to)throw"Column 'to' missing in table with edges (row "+r+")";var h=a.timestamp?a.timestamp:void 0,d=!0;void 0!==h&&void 0!==t&&h>t&&(d=!1),d&&this._createEdge(a)}this.start()}},x.prototype._createEdge=function(t){var e,i,s,o,n=t.action?t.action:"create";if("create"===n)e=t.id,i=void 0!==e?this._findEdge(e):void 0,s=new x.Edge(t,this,this.constants),void 0!==i?(o=this.edges[i],o.from.detachEdge(o),o.to.detachEdge(o),this.edges[i]=s):this.edges.push(s),s.from.attachEdge(s),s.to.attachEdge(s),s.isMoving()&&(this.hasMovingEdges=!0);else if("update"===n){if(e=t.id,void 0===e)throw"Cannot update a edge without id";i=this._findEdge(e),void 0!==i?(s=this.edges[i],s.from.detachEdge(s),s.to.detachEdge(s),s.setProperties(t,this.constants),s.from.attachEdge(s),s.to.attachEdge(s)):(s=new x.Edge(t,this,this.constants),s.from.attachEdge(s),s.to.attachEdge(s),this.edges.push(s),s.isMoving()&&(this.hasMovingEdges=!0))}else{if("delete"!==n)throw"Unknown action "+n+". Choose 'create', 'update', or 'delete'.";if(e=t.id,void 0===e)throw"Cannot delete edge without its id";if(i=this._findEdge(e),void 0===i)throw"Edge with id "+e+" not found";o=this.edges[e],s.from.detachEdge(o),s.to.detachEdge(o),this.edges.splice(i,1)}},x.prototype._updateNodeReferences=function(t,e){for(var i=[this.edges,this.packages],s=0,o=i.length;o>s;s++)for(var n=i[s],r=0,a=n.length;a>r;r++)n.from===t&&(n.from=e),n.to===t&&(n.to=e)},x.prototype._findEdge=function(t){for(var e=this.edges,i=0,s=e.length;s>i;i++)if(e[i].id===t)return i;return void 0},x.prototype._findEdgeByRow=function(t){return this.edges[t]},x.prototype.addPackages=function(t){var e;if(google&&google.visualization&&google.visualization.DataTable&&t instanceof google.visualization.DataTable)e=x.tableToArray(t);else{if(!x.isArray(t))return;e=t}for(var i=e.length,s=0;i>s;s++){var o=e[s];if(void 0===o.from)throw"Column 'from' missing in table with packages (row "+s+")";if(void 0===o.to)throw"Column 'to' missing in table with packages (row "+s+")";this._createPackage(o)}this._updateValueRange(this.packages),this.start()},x.prototype.setPackages=function(t){var e;if(google&&google.visualization&&google.visualization.DataTable&&t instanceof google.visualization.DataTable)e=x.tableToArray(t);else{if(!x.isArray(t))return;e=t}this.packagesTable=e,this.packages=[];for(var i=e.length,s=0;i>s;s++){var o=e[s];if(void 0===o.from)throw"Column 'from' missing in table with packages (row "+s+")";if(void 0===o.to)throw"Column 'to' missing in table with packages (row "+s+")";o.timestamp&&(this.hasTimestamps=this.hasTimestamps||o.timestamp),this._createPackage(o)}this._updateValueRange(this.packages)},x.prototype._filterPackages=function(t){if(void 0!=this.packagesTable){this.packages=[];for(var e=this.packagesTable,i=e.length,s=0;i>s;s++){var o=e[s];if(void 0===o.from)throw"Column 'from' missing in table with packages (row "+s+")";if(void 0===o.to)throw"Column 'to' missing in table with packages (row "+s+")";var n=o.timestamp?o.timestamp:void 0,r=!0;if(void 0!==n&&void 0!==t&&n>t&&(r=!1),r===!0&&void 0==o.progress){var a=o.duration||this.constants.packages.duration,h=(t.getTime()-n.getTime())/1e3;if(a>h){var d=o;o={};for(var l in d)d.hasOwnProperty(l)&&(o[l]=d[l]);o.progress=h/a}else r=!1}r===!0&&this._createPackage(o)}this.start()}},x.prototype._createPackage=function(t){var e,i,s,o=t.action?t.action:"create";if("create"===o)e=t.id,i=void 0!==e?this._findPackage(e):void 0,s=new x.Package(t,this,this.images,this.constants),void 0!==i?this.packages[i]=s:this.packages.push(s),s.isMoving()&&(this.hasMovingPackages=!0);else if("update"===o){if(e=t.id,void 0===e)throw"Cannot update a edge without id";i=this._findPackage(e),void 0!==i?this.packages[i].setProperties(t,this.constants):(s=new x.Package(t,this,this.images,this.constants),this.packages.push(s),s.isMoving()&&(this.hasMovingPackages=!0))}else{if("delete"!==o)throw"Unknown action "+o+". Choose 'create', 'update', or 'delete'.";if(e=t.id,void 0===e)throw"Cannot delete package without its id";if(i=this._findPackage(e),void 0===i)throw"Package with id "+e+" not found";this.packages.splice(i,1)}},x.prototype._findPackage=function(t){for(var e=this.packages,i=0,s=e.length;s>i;i++)if(e[i].id===t)return i;return void 0},x.prototype._findPackageByRow=function(t){return this.packages[t]},x.prototype._getColumnNames=function(t){for(var e=t.getNumberOfColumns(),i={},s=0;e>s;s++){var o=t.getColumnLabel(s);i[o]=s}return i},x.prototype._updateValueRange=function(t){var e,i=t.length,s=void 0,o=void 0;for(e=0;i>e;e++){var n=t[e].getValue();void 0!==n&&(s=void 0===s?n:Math.min(n,s),o=void 0===o?n:Math.max(n,o))}if(void 0!==s&&void 0!==o)for(e=0;i>e;e++)t[e].setValueRange(s,o)},x.prototype.setTimestamp=function(t){this._filterNodes(t),this._filterEdges(t),this._filterPackages(t)},x.prototype._getRange=function(){for(var t={start:void 0,end:void 0},e=[this.nodesTable,this.edgesTable],i=0,s=e.length;s>i;i++){var o=e[i];if(void 0!==o)for(var n=0,r=o.length;r>n;n++){var a=o[n].timestamp;a&&(a instanceof Date&&(a=a.getTime()),t.start=t.start?Math.min(a,t.start):a,t.end=t.end?Math.max(a,t.end):a)}}if(this.packagesTable)for(var h=this.packagesTable,d=0,l=h.length;l>d;d++){var p=h[d],a=p.timestamp,u=p.progress,c=p.duration||this.constants.packages.duration;if(a instanceof Date&&(a=a.getTime()),void 0!=a){var f=a,m=u?a:a+1e3*c;t.start=t.start?Math.min(f,t.start):f,t.end=t.end?Math.max(m,t.end):m}}var g={start:new Date(t.start),end:new Date(t.end)};return g},x.prototype.animationStart=function(){this.slider&&this.slider.play()},x.prototype.animationStop=function(){this.slider&&this.slider.stop()},x.prototype.setAnimationFramerate=function(t){this.slider&&this.slider.setFramerate(t)},x.prototype.setAnimationDuration=function(t){this.slider&&this.slider.setDuration(t)},x.prototype.setAnimationAcceleration=function(t){this.slider&&this.slider.setAcceleration(t)},x.prototype.redraw=function(){this._setSize(this.width,this.height),this._redraw()},x.prototype._redraw=function(){var t=this.frame.canvas.getContext("2d"),e=this.frame.canvas.width,i=this.frame.canvas.height;t.clearRect(0,0,e,i),t.save(),t.translate(this.translation.x,this.translation.y),t.scale(this.scale,this.scale),this._drawEdges(t),this._drawNodes(t),this._drawPackages(t),this._drawSlider(),t.restore()},x.prototype._setTranslation=function(t,e){void 0===this.translation&&(this.translation={x:0,y:0}),void 0!==t&&(this.translation.x=t),void 0!==e&&(this.translation.y=e)},x.prototype._getTranslation=function(){return{x:this.translation.x,y:this.translation.y}},x.prototype._setScale=function(t){this.scale=t},x.prototype._getScale=function(){return this.scale},x.prototype._xToCanvas=function(t){return(t-this.translation.x)/this.scale},x.prototype._canvasToX=function(t){return t*this.scale+this.translation.x},x.prototype._yToCanvas=function(t){return(t-this.translation.y)/this.scale},x.prototype._canvasToY=function(t){return t*this.scale+this.translation.y},x.prototype._getNode=function(t){for(var e=0;this.nodes.length>e;e++)if(this.nodes[e].id==t)return this.nodes[e];return null},x.prototype._drawNodes=function(t){for(var e=this.nodes,i=[],s=0,o=e.length;o>s;s++)e[s].isSelected()?i.push(s):e[s].draw(t);for(var n=0,r=i.length;r>n;n++)e[i[n]].draw(t)},x.prototype._drawEdges=function(t){for(var e=this.edges,i=0,s=e.length;s>i;i++)e[i].draw(t)},x.prototype._drawPackages=function(t){for(var e=this.packages,i=0,s=e.length;s>i;i++)e[i].draw(t)},x.prototype._drawSlider=function(){var t;if(this.hasTimestamps){if(t=this.frame.slider,void 0===t){t=document.createElement("div"),t.style.position="absolute",t.style.bottom="0px",t.style.left="0px",t.style.right="0px",t.style.backgroundColor="rgba(255, 255, 255, 0.7)",this.frame.slider=t,this.frame.slider.style.padding="10px",this.frame.appendChild(t);var e=this._getRange();this.slider=new x.Slider(t),this.slider.setLoop(!1),this.slider.setRange(e.start,e.end);var i=this,s=function(){var t=i.slider.getValue();i.setTimestamp(t),i.redraw()};this.slider.setOnChangeCallback(s),s()}}else t=this.frame.slider,void 0!==t&&(this.frame.removeChild(t),this.frame.slider=void 0,this.slider=void 0)},x.prototype._reposition=function(){for(var t=2*this.constants.edges.length,e=this.frame.canvas.clientWidth/2,i=this.frame.canvas.clientHeight/2,s=0;this.nodes.length>s;s++){var o=2*Math.PI*(s/this.nodes.length);this.nodes[s].xFixed||(this.nodes[s].x=e+t*Math.cos(o)),this.nodes[s].yFixed||(this.nodes[s].y=i+t*Math.sin(o))}},x.prototype._doStabilize=function(){new Date;for(var t=0,e=this.constants.minVelocity,i=!1;!i&&this.constants.maxIterations>t;)this._calculateForces(),this._discreteStepNodes(),i=!this.isMoving(e),t++;new Date},x.prototype._calculateForces=function(){for(var t=this.nodes,e=this.edges,i=.01,s=this.frame.canvas.clientWidth/2,o=this.frame.canvas.clientHeight/2,n=0;t.length>n;n++){var r=s-t[n].x,a=o-t[n].y,h=Math.atan2(a,r),d=Math.cos(h)*i,l=Math.sin(h)*i;this.nodes[n]._setForce(d,l)}for(var p=this.constants.nodes.distance,u=10,n=0;t.length>n;n++)for(var c=n+1;this.nodes.length>c;c++){var r=t[c].x-t[n].x,a=t[c].y-t[n].y,f=Math.sqrt(r*r+a*a),h=Math.atan2(a,r),m=1/(1+Math.exp((f/p-1)*u)),d=Math.cos(h)*m,l=Math.sin(h)*m;this.nodes[n]._addForce(-d,-l),this.nodes[c]._addForce(d,l)}for(var g=0,v=e.length;v>g;g++){var y=e[g],r=y.to.x-y.from.x,a=y.to.y-y.from.y,w=y.length,b=Math.sqrt(r*r+a*a),h=Math.atan2(a,r),_=y.stiffness*(w-b),d=Math.cos(h)*_,l=Math.sin(h)*_;y.from._addForce(-d,-l),y.to._addForce(d,l)}},x.prototype.isMoving=function(t){for(var e=this.nodes,i=0,s=e.length;s>i;i++)if(e[i].isMoving(t))return!0;return!1},x.prototype._discreteStepNodes=function(){for(var t=this.refreshRate/1e3,e=this.nodes,i=0,s=e.length;s>i;i++)e[i].discreteStep(t) +},x.prototype._discreteStepPackages=function(){for(var t=this.refreshRate/1e3,e=this.packages,i=0,s=e.length;s>i;i++)e[i].discreteStep(t)},x.prototype._deleteFinishedPackages=function(){for(var t=0,e=!1;this.packages.length>t;)this.packages[t].isFinished()?(this.packages.splice(t,1),t--):this.packages[t].isMoving()&&(e=!0),t++;this.hasMovingPackages=e},x.prototype.start=function(){if(this.hasMovingNodes){this._calculateForces(),this._discreteStepNodes();var t=this.constants.minVelocity;this.hasMovingNodes=this.isMoving(t)}if(this.hasMovingPackages&&(this._discreteStepPackages(),this._deleteFinishedPackages()),this.hasMovingNodes||this.hasMovingEdges||this.hasMovingPackages){if(!this.timer){var e=this;this.timer=window.setTimeout(function(){e.timer=void 0,e.start(),e._redraw()},this.refreshRate)}}else this._redraw()},x.prototype.stop=function(){this.timer&&(window.clearInterval(this.timer),this.timer=void 0)},x.addEventListener=function(t,e,i,s){t.addEventListener?(void 0===s&&(s=!1),"mousewheel"===e&&navigator.userAgent.indexOf("Firefox")>=0&&(e="DOMMouseScroll"),t.addEventListener(e,i,s)):t.attachEvent("on"+e,i)},x.removeEventListener=function(t,e,i,s){t.removeEventListener?(void 0===s&&(s=!1),"mousewheel"===e&&navigator.userAgent.indexOf("Firefox")>=0&&(e="DOMMouseScroll"),t.removeEventListener(e,i,s)):t.detachEvent("on"+e,i)},x.stopPropagation=function(t){t||(t=window.event),t.stopPropagation?t.stopPropagation():t.cancelBubble=!0},x.preventDefault=function(t){t||(t=window.event),t.preventDefault?t.preventDefault():t.returnValue=!1},x._getAbsoluteLeft=function(t){for(var e=0;null!=t;)e+=t.offsetLeft,e-=t.scrollLeft,t=t.offsetParent;return!document.body.scrollLeft&&window.pageXOffset&&(e-=window.pageXOffset),e},x._getAbsoluteTop=function(t){for(var e=0;null!=t;)e+=t.offsetTop,e-=t.scrollTop,t=t.offsetParent;return!document.body.scrollTop&&window.pageYOffset&&(e-=window.pageYOffset),e},x.Node=function(t,e,i,s){this.selected=!1,this.edges=[],this.group=s.nodes.group,this.fontSize=s.nodes.fontSize,this.fontFace=s.nodes.fontFace,this.fontColor=s.nodes.fontColor,this.borderColor=s.nodes.borderColor,this.backgroundColor=s.nodes.backgroundColor,this.highlightColor=s.nodes.highlightColor,this.id=void 0,this.style=s.nodes.style,this.image=s.nodes.image,this.x=0,this.y=0,this.xFixed=!1,this.yFixed=!1,this.radius=s.nodes.radius,this.radiusFixed=!1,this.radiusMin=s.nodes.radiusMin,this.radiusMax=s.nodes.radiusMax,this.imagelist=e,this.grouplist=i,this.setProperties(t,s),this.mass=50,this.fx=0,this.fy=0,this.vx=0,this.vy=0,this.minForce=s.minForce,this.damping=.9},x.Node.prototype.attachEdge=function(t){this.edges.push(t),this._updateMass()},x.Node.prototype.detachEdge=function(t){var e=this.edges.indexOf(t);-1!=e&&this.edges.splice(e,1),this._updateMass()},x.Node.prototype._updateMass=function(){this.mass=50+20*this.edges.length},x.Node.prototype.setProperties=function(t,e){if(t){if(void 0!=t.id&&(this.id=t.id),void 0!=t.text&&(this.text=t.text),void 0!=t.title&&(this.title=t.title),void 0!=t.group&&(this.group=t.group),void 0!=t.x&&(this.x=t.x),void 0!=t.y&&(this.y=t.y),void 0!=t.value&&(this.value=t.value),void 0!=t.timestamp&&(this.timestamp=t.timestamp),void 0===this.id)throw"Node must have an id";if(this.group){var i=this.grouplist.get(this.group);for(var s in i)i.hasOwnProperty(s)&&(this[s]=i[s])}if(void 0!=t.style&&(this.style=t.style),void 0!=t.image&&(this.image=t.image),void 0!=t.radius&&(this.radius=t.radius),void 0!=t.borderColor&&(this.borderColor=t.borderColor),void 0!=t.backgroundColor&&(this.backgroundColor=t.backgroundColor),void 0!=t.highlightColor&&(this.highlightColor=t.highlightColor),void 0!=t.fontColor&&(this.fontColor=t.fontColor),void 0!=t.fontSize&&(this.fontSize=t.fontSize),void 0!=t.fontFace&&(this.fontFace=t.fontFace),void 0!=this.image){if(!this.imagelist)throw"No imagelist provided";this.imageObj=this.imagelist.load(this.image)}this.xFixed=this.xFixed||void 0!=t.x,this.yFixed=this.yFixed||void 0!=t.y,this.radiusFixed=this.radiusFixed||void 0!=t.radius,"image"==this.style&&(this.radiusMin=e.nodes.widthMin,this.radiusMax=e.nodes.widthMax);var o=this.style;switch(o){case"database":this.draw=this._drawDatabase,this.resize=this._resizeDatabase;break;case"rect":this.draw=this._drawRect,this.resize=this._resizeRect;break;case"circle":this.draw=this._drawCircle,this.resize=this._resizeCircle;break;case"image":this.draw=this._drawImage,this.resize=this._resizeImage;break;case"text":this.draw=this._drawText,this.resize=this._resizeText;break;case"dot":this.draw=this._drawDot,this.resize=this._resizeShape;break;case"square":this.draw=this._drawSquare,this.resize=this._resizeShape;break;case"triangle":this.draw=this._drawTriangle,this.resize=this._resizeShape;break;case"triangleDown":this.draw=this._drawTriangleDown,this.resize=this._resizeShape;break;case"star":this.draw=this._drawStar,this.resize=this._resizeShape;break;default:this.draw=this._drawRect,this.resize=this._resizeRect}this._reset()}},x.Node.prototype.select=function(){this.selected=!0,this._reset()},x.Node.prototype.unselect=function(){this.selected=!1,this._reset()},x.Node.prototype._reset=function(){this.width=void 0,this.height=void 0},x.Node.prototype.getTitle=function(){return this.title},x.Node.prototype.distanceToBorder=function(t,e){var i=1;switch(this.width||this.resize(t),this.style){case"circle":case"dot":return this.radius+i;case"rect":case"image":case"text":default:return this.width?Math.min(Math.abs(this.width/2/Math.cos(e)),Math.abs(this.height/2/Math.sin(e)))+i:0}},x.Node.prototype._setForce=function(t,e){this.fx=t,this.fy=e},x.Node.prototype._addForce=function(t,e){this.fx+=t,this.fy+=e},x.Node.prototype.discreteStep=function(t){if(!this.xFixed){var e=-this.damping*this.vx,i=(this.fx+e)/this.mass;this.vx+=i/t,this.x+=this.vx/t}if(!this.yFixed){var s=-this.damping*this.vy,o=(this.fy+s)/this.mass;this.vy+=o/t,this.y+=this.vy/t}},x.Node.prototype.isFixed=function(){return this.xFixed&&this.yFixed},x.Node.prototype.isMoving=function(t){return Math.abs(this.vx)>t||Math.abs(this.vy)>t||!this.xFixed&&Math.abs(this.fx)>this.minForce||!this.yFixed&&Math.abs(this.fy)>this.minForce},x.Node.prototype.isSelected=function(){return this.selected},x.Node.prototype.getValue=function(){return this.value},x.Node.prototype.getDistance=function(t,e){var i=this.x-t,s=this.y-e;return Math.sqrt(i*i+s*s)},x.Node.prototype.setValueRange=function(t,e){if(!this.radiusFixed&&void 0!==this.value){var i=(this.radiusMax-this.radiusMin)/(e-t);this.radius=(this.value-t)*i+this.radiusMin}},x.Node.prototype.draw=function(){throw"Draw method not initialized for node"},x.Node.prototype.resize=function(){throw"Resize method not initialized for node"},x.Node.prototype.isOverlappingWith=function(t){return this.leftt.left&&this.topt.top},x.Node.prototype._resizeImage=function(){if(!this.width){var t,e;if(this.value){var i=this.imageObj.height/this.imageObj.width;t=this.radius||this.imageObj.width,e=this.radius*i||this.imageObj.height}else t=this.imageObj.width,e=this.imageObj.height;this.width=t,this.height=e}},x.Node.prototype._drawImage=function(t){this._resizeImage(t),this.left=this.x-this.width/2,this.top=this.y-this.height/2;var e;this.imageObj?(t.drawImage(this.imageObj,this.left,this.top,this.width,this.height),e=this.y+this.height/2):e=this.y,this._text(t,this.text,this.x,e,void 0,"top")},x.Node.prototype._resizeRect=function(t){if(!this.width){var e=5,i=this.getTextSize(t);this.width=i.width+2*e,this.height=i.height+2*e}},x.Node.prototype._drawRect=function(t){this._resizeRect(t),this.left=this.x-this.width/2,this.top=this.y-this.height/2,t.strokeStyle=this.borderColor,t.fillStyle=this.selected?this.highlightColor:this.backgroundColor,t.lineWidth=this.selected?2:1,t.roundRect(this.left,this.top,this.width,this.height,this.radius),t.fill(),t.stroke(),this._text(t,this.text,this.x,this.y)},x.Node.prototype._resizeDatabase=function(t){if(!this.width){var e=5,i=this.getTextSize(t),s=i.width+2*e;this.width=s,this.height=s}},x.Node.prototype._drawDatabase=function(t){this._resizeDatabase(t),this.left=this.x-this.width/2,this.top=this.y-this.height/2,t.strokeStyle=this.borderColor,t.fillStyle=this.selected?this.highlightColor:this.backgroundColor,t.lineWidth=this.selected?2:1,t.database(this.x-this.width/2,this.y-.5*this.height,this.width,this.height),t.fill(),t.stroke(),this._text(t,this.text,this.x,this.y)},x.Node.prototype._resizeCircle=function(t){if(!this.width){var e=5,i=this.getTextSize(t),s=Math.max(i.width,i.height)+2*e;this.radius=s/2,this.width=s,this.height=s}},x.Node.prototype._drawCircle=function(t){this._resizeCircle(t),this.left=this.x-this.width/2,this.top=this.y-this.height/2,t.strokeStyle=this.borderColor,t.fillStyle=this.selected?this.highlightColor:this.backgroundColor,t.lineWidth=this.selected?2:1,t.circle(this.x,this.y,this.radius),t.fill(),t.stroke(),this._text(t,this.text,this.x,this.y)},x.Node.prototype._drawDot=function(t){this._drawShape(t,"circle")},x.Node.prototype._drawTriangle=function(t){this._drawShape(t,"triangle")},x.Node.prototype._drawTriangleDown=function(t){this._drawShape(t,"triangleDown")},x.Node.prototype._drawSquare=function(t){this._drawShape(t,"square")},x.Node.prototype._drawStar=function(t){this._drawShape(t,"star")},x.Node.prototype._resizeShape=function(){if(!this.width){var t=2*this.radius;this.width=t,this.height=t}},x.Node.prototype._drawShape=function(t,e){this._resizeShape(t),this.left=this.x-this.width/2,this.top=this.y-this.height/2,t.strokeStyle=this.borderColor,t.fillStyle=this.selected?this.highlightColor:this.backgroundColor,t.lineWidth=this.selected?2:1,t[e](this.x,this.y,this.radius),t.fill(),t.stroke(),this.text&&this._text(t,this.text,this.x,this.y+this.height/2,void 0,"top")},x.Node.prototype._resizeText=function(t){if(!this.width){var e=5,i=this.getTextSize(t);this.width=i.width+2*e,this.height=i.height+2*e}},x.Node.prototype._drawText=function(t){this._resizeText(t),this.left=this.x-this.width/2,this.top=this.y-this.height/2,this._text(t,this.text,this.x,this.y)},x.Node.prototype._text=function(t,e,i,s,o,n){if(e){t.font=(this.selected?"bold ":"")+this.fontSize+"px "+this.fontFace,t.fillStyle=this.fontColor||"black",t.textAlign=o||"center",t.textBaseline=n||"middle";for(var r=e.split("\n"),a=r.length,h=this.fontSize+4,d=s+(1-a)/2*h,l=0;a>l;l++)t.fillText(r[l],i,d),d+=h}},x.Node.prototype.getTextSize=function(t){if(void 0!=this.text){t.font=(this.selected?"bold ":"")+this.fontSize+"px "+this.fontFace;for(var e=this.text.split("\n"),i=(this.fontSize+4)*e.length,s=0,o=0,n=e.length;n>o;o++)s=Math.max(s,t.measureText(e[o]).width);return{width:s,height:i}}return{width:0,height:0}},x.Edge=function(t,e,i){if(!e)throw"No graph provided";this.graph=e,this.widthMin=i.edges.widthMin,this.widthMax=i.edges.widthMax,this.id=void 0,this.style=i.edges.style,this.title=void 0,this.width=i.edges.width,this.value=void 0,this.length=i.edges.length,this.dashlength=i.edges.dashlength,this.dashgap=i.edges.dashgap,this.altdashlength=i.edges.altdashlength,this.stiffness=void 0,this.color=i.edges.color,this.timestamp=void 0,this.widthFixed=!1,this.lengthFixed=!1,this.setProperties(t,i)},x.Edge.prototype.setProperties=function(t,e){if(t){if(void 0!=t.from&&(this.from=this.graph._getNode(t.from)),void 0!=t.to&&(this.to=this.graph._getNode(t.to)),void 0!=t.id&&(this.id=t.id),void 0!=t.style&&(this.style=t.style),void 0!=t.text&&(this.text=t.text),this.text&&(this.fontSize=e.edges.fontSize,this.fontFace=e.edges.fontFace,this.fontColor=e.edges.fontColor,void 0!=t.fontColor&&(this.fontColor=t.fontColor),void 0!=t.fontSize&&(this.fontSize=t.fontSize),void 0!=t.fontFace&&(this.fontFace=t.fontFace)),void 0!=t.title&&(this.title=t.title),void 0!=t.width&&(this.width=t.width),void 0!=t.value&&(this.value=t.value),void 0!=t.length&&(this.length=t.length),void 0!=t.dashlength&&(this.dashlength=t.dashlength),void 0!=t.dashgap&&(this.dashgap=t.dashgap),void 0!=t.altdashlength&&(this.altdashlength=t.altdashlength),void 0!=t.color&&(this.color=t.color),void 0!=t.timestamp&&(this.timestamp=t.timestamp),!this.from)throw"Node with id "+t.from+" not found";if(!this.to)throw"Node with id "+t.to+" not found";if(this.widthFixed=this.widthFixed||void 0!=t.width,this.lengthFixed=this.lengthFixed||void 0!=t.length,this.stiffness=1/this.length,"arrow"===this.style)this.arrows=[.5],this.animation=!1;else if("arrow-end"===this.style)this.animation=!1;else if("moving-arrows"===this.style){this.arrows=[];for(var i=3,s=0;i>s;s++)this.arrows.push(s/i);this.animation=!0}else"moving-dot"===this.style?(this.dot=0,this.animation=!0):this.animation=!1;switch(this.style){case"line":this.draw=this._drawLine;break;case"arrow":this.draw=this._drawArrow;break;case"arrow-end":this.draw=this._drawArrowEnd;break;case"moving-arrows":this.draw=this._drawMovingArrows;break;case"moving-dot":this.draw=this._drawMovingDot;break;case"dash-line":this.draw=this._drawDashLine;break;default:this.draw=this._drawLine}}},x.Edge.prototype.isMoving=function(){return this.animation},x.Edge.prototype.getTitle=function(){return this.title},x.Edge.prototype.getValue=function(){return this.value},x.Edge.prototype.setValueRange=function(t,e){if(!this.widthFixed&&void 0!==this.value){var i=(this.widthMax-this.widthMin)/(e-t);this.width=(this.value-t)*i+this.widthMin}},x.Edge.prototype.isLengthFixed=function(){return this.lengthFixed},x.Edge.prototype.getLength=function(){return this.length},x.Edge.prototype.setLength=function(t){this.lengthFixed||(this.length=t)},x.Edge.prototype.getDashLength=function(){return this.dashlength},x.Edge.prototype.setDashLength=function(t){this.dashlength=t},x.Edge.prototype.getDashGap=function(){return this.dashgap},x.Edge.prototype.setDashGap=function(t){this.dashgap=t},x.Edge.prototype.getAltDashLength=function(){return this.altdashlength},x.Edge.prototype.setAltDashLength=function(t){this.altdashlength=t},x.Edge.prototype.draw=function(){throw"Method draw not initialized in edge"},x.Edge.prototype.isOverlappingWith=function(t){var e=10,i=this.from.x,s=this.from.y,o=this.to.x,n=this.to.y,r=t.left,a=t.top,h=x._dist(i,s,o,n,r,a);return e>h},x._dist=function(t,e,i,s,o,n){var r=i-t,a=s-e,h=r*r+a*a,d=((o-t)*r+(n-e)*a)/h;d>1?d=1:0>d&&(d=0);var l=t+d*r,p=e+d*a,u=l-o,c=p-n;return Math.sqrt(u*u+c*c)},x.Edge.prototype._drawLine=function(t){t.strokeStyle=this.color,t.lineWidth=this._getLineWidth();var e;if(this.from!=this.to)this._line(t),this.text&&(e=this._pointOnLine(.5),this._text(t,this.text,e.x,e.y));else{var i,s,o=this.length/2/Math.PI,n=this.from;n.width||n.resize(t),n.width>n.height?(i=n.x+n.width/2,s=n.y-o):(i=n.x+o,s=n.y-n.height/2),this._circle(t,i,s,o),e=this._pointOnCircle(i,s,o,.5),this._text(t,this.text,e.x,e.y)}},x.Edge.prototype._getLineWidth=function(){return this.from.selected||this.to.selected?Math.min(2*this.width,this.widthMax):this.width},x.Edge.prototype._line=function(t){t.beginPath(),t.moveTo(this.from.x,this.from.y),t.lineTo(this.to.x,this.to.y),t.stroke()},x.Edge.prototype._circle=function(t,e,i,s){t.beginPath(),t.arc(e,i,s,0,2*Math.PI,!1),t.stroke()},x.Edge.prototype._text=function(t,e,i,s){if(e){t.font=(this.from.selected||this.to.selected?"bold ":"")+this.fontSize+"px "+this.fontFace,t.fillStyle="white";var o=t.measureText(this.text).width,n=this.fontSize,r=i-o/2,a=s-n/2;t.fillRect(r,a,o,n),t.fillStyle=this.fontColor||"black",t.textAlign="left",t.textBaseline="top",t.fillText(this.text,r,a)}};var D="undefined"!=typeof window&&window.CanvasRenderingContext2D&&CanvasRenderingContext2D.prototype;D&&D.lineTo&&(D.dashedLine=function(t,e,i,s,o){o||(o=[10,5]),0==u&&(u=.001);var n=o.length;this.moveTo(t,e);for(var r=i-t,a=s-e,h=a/r,d=Math.sqrt(r*r+a*a),l=0,p=!0;d>=.1;){var u=o[l++%n];u>d&&(u=d);var c=Math.sqrt(u*u/(1+h*h));0>r&&(c=-c),t+=c,e+=h*c,this[p?"lineTo":"moveTo"](t,e),d-=u,p=!p}}),x.Edge.prototype._drawDashLine=function(t){if(t.strokeStyle=this.color,t.lineWidth=this._getLineWidth(),t.beginPath(),t.lineCap="round",void 0!=this.altdashlength?t.dashedLine(this.from.x,this.from.y,this.to.x,this.to.y,[this.dashlength,this.dashgap,this.altdashlength,this.dashgap]):void 0!=this.dashlength&&void 0!=this.dashgap?t.dashedLine(this.from.x,this.from.y,this.to.x,this.to.y,[this.dashlength,this.dashgap]):(t.moveTo(this.from.x,this.from.y),t.lineTo(this.to.x,this.to.y)),t.stroke(),this.text){var e=this._pointOnLine(.5);this._text(t,this.text,e.x,e.y)}},x.Edge.prototype._pointOnLine=function(t){return{x:(1-t)*this.from.x+t*this.to.x,y:(1-t)*this.from.y+t*this.to.y}},x.Edge.prototype._pointOnCircle=function(t,e,i,s){var o=2*(s-3/8)*Math.PI;return{x:t+i*Math.cos(o),y:e-i*Math.sin(o)}},x.Edge.prototype._drawMovingArrows=function(t){this._drawArrow(t);for(var e in this.arrows)this.arrows.hasOwnProperty(e)&&(this.arrows[e]+=.02,this.arrows[e]>1&&(this.arrows[e]=0))},x.Edge.prototype._drawMovingDot=function(t){t.strokeStyle=this.color,t.fillStyle=this.color,t.lineWidth=this._getLineWidth();var e;if(this.from!=this.to){this._line(t);var i=4+2*this.width;e=this._pointOnLine(this.dot),t.circle(e.x,e.y,i),t.fill(),this.dot+=.05,this.dot>1&&(this.dot=0),this.text&&(e=this._pointOnLine(.5),this._text(t,this.text,e.x,e.y))}},x.Edge.prototype._drawArrow=function(t){var e;if(t.strokeStyle=this.color,t.fillStyle=this.color,t.lineWidth=this._getLineWidth(),this.from!=this.to){this._line(t);var i=Math.atan2(this.to.y-this.from.y,this.to.x-this.from.x),s=10+5*this.width;for(var o in this.arrows)this.arrows.hasOwnProperty(o)&&(e=this._pointOnLine(this.arrows[o]),t.arrow(e.x,e.y,i,s),t.fill(),t.stroke());this.text&&(e=this._pointOnLine(.5),this._text(t,this.text,e.x,e.y))}else{var n,r,a=this.length/2/Math.PI,h=this.from;h.width||h.resize(t),h.width>h.height?(n=h.x+h.width/2,r=h.y-a):(n=h.x+a,r=h.y-h.height/2),this._circle(t,n,r,a);var i=.2*Math.PI,s=10+5*this.width;for(var o in this.arrows)this.arrows.hasOwnProperty(o)&&(e=this._pointOnCircle(n,r,a,this.arrows[o]),t.arrow(e.x,e.y,i,s),t.fill(),t.stroke());this.text&&(e=this._pointOnCircle(n,r,a,.5),this._text(t,this.text,e.x,e.y))}},x.Edge.prototype._drawArrowEnd=function(t){t.strokeStyle=this.color,t.fillStyle=this.color,t.lineWidth=this._getLineWidth();var e,i;if(this.from!=this.to){e=Math.atan2(this.to.y-this.from.y,this.to.x-this.from.x);var s=this.to.x-this.from.x,o=this.to.y-this.from.y,n=Math.sqrt(s*s+o*o),r=this.to.distanceToBorder(t,e+Math.PI),a=(n-r)/n,h=a*this.from.x+(1-a)*this.to.x,d=a*this.from.y+(1-a)*this.to.y,l=this.to.distanceToBorder(t,e),p=(n-l)/n,u=(1-p)*this.from.x+p*this.to.x,c=(1-p)*this.from.y+p*this.to.y;if(t.beginPath(),t.moveTo(h,d),t.lineTo(u,c),t.stroke(),i=10+5*this.width,t.arrow(u,c,e,i),t.fill(),t.stroke(),this.text){var f=this._pointOnLine(.5);this._text(t,this.text,f.x,f.y)}}else{var m,g,v,y=this.length/2/Math.PI,w=this.from;w.width||w.resize(t),w.width>w.height?(m=w.x+w.width/2,g=w.y-y,v={x:m,y:w.y,angle:.9*Math.PI}):(m=w.x+y,g=w.y-w.height/2,v={x:w.x,y:g,angle:.6*Math.PI}),t.beginPath(),t.arc(m,g,y,0,2*Math.PI,!1),t.stroke(),i=10+5*this.width,t.arrow(v.x,v.y,v.angle,i),t.fill(),t.stroke(),this.text&&(f=this._pointOnCircle(m,g,y,.5),this._text(t,this.text,f.x,f.y))}},x.Images=function(){this.images={},this.callback=void 0},x.Images.prototype.setOnloadCallback=function(t){this.callback=t},x.Images.prototype.load=function(t){var e=this.images[t];if(void 0==e){var i=this;e=new Image,this.images[t]=e,e.onload=function(){i.callback&&i.callback(this)},e.src=t}return e},x.Package=function(t,e,i,s){if(void 0==e)throw"No graph provided";this.radiusMin=s.packages.radiusMin,this.radiusMax=s.packages.radiusMax,this.imagelist=i,this.graph=e,this.id=void 0,this.from=void 0,this.to=void 0,this.title=void 0,this.style=s.packages.style,this.radius=s.packages.radius,this.color=s.packages.color,this.image=s.packages.image,this.value=void 0,this.progress=0,this.timestamp=void 0,this.duration=s.packages.duration,this.autoProgress=!0,this.radiusFixed=!1,this.setProperties(t,s)},x.Package.DEFAULT_DURATION=1,x.Package.prototype.setProperties=function(t,e){if(t){if(void 0!=t.from&&(this.from=this.graph._getNode(t.from)),void 0!=t.to&&(this.to=this.graph._getNode(t.to)),!this.from)throw"Node with id "+t.from+" not found";if(!this.to)throw"Node with id "+t.to+" not found";if(void 0!=t.id&&(this.id=t.id),void 0!=t.title&&(this.title=t.title),void 0!=t.style&&(this.style=t.style),void 0!=t.radius&&(this.radius=t.radius),void 0!=t.value&&(this.value=t.value),void 0!=t.image&&(this.image=t.image),void 0!=t.color&&(this.color=t.color),void 0!=t.dashlength&&(this.dashlength=t.dashlength),void 0!=t.dashgap&&(this.dashgap=t.dashgap),void 0!=t.altdashlength&&(this.altdashlength=t.altdashlength),void 0!=t.progress&&(this.progress=t.progress),void 0!=t.timestamp&&(this.timestamp=t.timestamp),void 0!=t.duration&&(this.duration=t.duration),this.radiusFixed=this.radiusFixed||void 0!=t.radius,this.autoProgress=1==this.autoProgress?void 0==t.progress:!1,"image"==this.style&&(this.radiusMin=e.packages.widthMin,this.radiusMax=e.packages.widthMax),0>this.progress&&(this.progress=0),this.progress>1&&(this.progress=1),void 0!=this.image){if(!this.imagelist)throw"No imagelist provided";this.imageObj=this.imagelist.load(this.image)}switch(this.style){case"dot":this.draw=this._drawDot;break;case"square":this.draw=this._drawSquare;break;case"triangle":this.draw=this._drawTriangle;break;case"triangleDown":this.draw=this._drawTriangleDown;break;case"star":this.draw=this._drawStar;break;case"image":this.draw=this._drawImage;break;default:this.draw=this._drawDot}}},x.Package.prototype.setProgress=function(t){this.progress=t,this.autoProgress=!1},x.Package.prototype.isFinished=function(){return 1==this.autoProgress&&this.progress>=1},x.Package.prototype.isMoving=function(){return this.autoProgress||this.isFinished()},x.Package.prototype.discreteStep=function(t){1==this.autoProgress&&(this.progress+=parseFloat(t)/this.duration,this.progress>1&&(this.progress=1))},x.Package.prototype.draw=function(){throw"Draw method not initialized for package"},x.Package.prototype.isOverlappingWith=function(t){var e=Math.max(this.radius,10),i=this._getPosition();return i.x-et.left&&i.y-et.top},x.Package.prototype._getPosition=function(){return{x:(1-this.progress)*this.from.x+this.progress*this.to.x,y:(1-this.progress)*this.from.y+this.progress*this.to.y}},x.Package.prototype.getTitle=function(){return this.title},x.Package.prototype.getValue=function(){return this.value},x.Package.prototype.getDistance=function(t,e){var i=this._getPosition(),s=i.x-t,o=i.y-e;return Math.sqrt(s*s+o*o)},x.Package.prototype.setValueRange=function(t,e){if(!this.radiusFixed&&void 0!==this.value){var i=(this.radiusMax-this.radiusMin)/(e-t);this.radius=(this.value-t)*i+this.radiusMin}},x.Package.prototype._drawDot=function(t){this._drawShape(t,"circle")},x.Package.prototype._drawTriangle=function(t){this._drawShape(t,"triangle")},x.Package.prototype._drawTriangleDown=function(t){this._drawShape(t,"triangleDown")},x.Package.prototype._drawSquare=function(t){this._drawShape(t,"square")},x.Package.prototype._drawStar=function(t){this._drawShape(t,"star")},x.Package.prototype._drawShape=function(t,e){t.fillStyle=this.color;var i=this._getPosition();t[e](i.x,i.y,this.radius),t.fill()},x.Package.prototype._drawImage=function(t){if(this.imageObj){var e,i;if(this.value){var s=this.imageObj.height/this.imageObj.width;e=this.radius||this.imageObj.width,i=this.radius*s||this.imageObj.height}else e=this.imageObj.width,i=this.imageObj.height;var o=this._getPosition();t.drawImage(this.imageObj,o.x-e/2,o.y-i/2,e,i)}else console.log("image still loading...")},x.Groups=function(){this.clear(),this.defaultIndex=0},x.Groups.DEFAULT=[{borderColor:"#2B7CE9",backgroundColor:"#97C2FC",highlightColor:"#D2E5FF"},{borderColor:"#FFA500",backgroundColor:"#FFFF00",highlightColor:"#FFFFA3"},{borderColor:"#FA0A10",backgroundColor:"#FB7E81",highlightColor:"#FFAFB1"},{borderColor:"#41A906",backgroundColor:"#7BE141",highlightColor:"#A1EC76"},{borderColor:"#E129F0",backgroundColor:"#EB7DF4",highlightColor:"#F0B3F5"},{borderColor:"#7C29F0",backgroundColor:"#AD85E4",highlightColor:"#D3BDF0"},{borderColor:"#C37F00",backgroundColor:"#FFA807",highlightColor:"#FFCA66"},{borderColor:"#4220FB",backgroundColor:"#6E6EFD",highlightColor:"#9B9BFD"},{borderColor:"#FD5A77",backgroundColor:"#FFC0CB",highlightColor:"#FFD1D9"},{borderColor:"#4AD63A",backgroundColor:"#C2FABC",highlightColor:"#E6FFE3"}],x.Groups.prototype.clear=function(){this.groups={},this.groups.length=function(){var t=0;for(var e in this)this.hasOwnProperty(e)&&t++;return t}},x.Groups.prototype.get=function(t){var e=this.groups[t];if(void 0==e){var i=this.defaultIndex%x.Groups.DEFAULT.length;this.defaultIndex++,e={},e.borderColor=x.Groups.DEFAULT[i].borderColor,e.backgroundColor=x.Groups.DEFAULT[i].backgroundColor,e.highlightColor=x.Groups.DEFAULT[i].highlightColor,this.groups[t]=e}return e},x.Groups.prototype.add=function(t,e){return this.groups[t]=e,e},x.isArray=function(t){return t instanceof Array?!0:"[object Array]"===Object.prototype.toString.call(t)},x.Slider=function(t){if(void 0===t)throw"Error: No container element defined";this.container=t,this.frame=document.createElement("DIV"),this.frame.style.width="100%",this.frame.style.position="relative",this.title=document.createElement("DIV"),this.title.style.margin="2px",this.title.style.marginBottom="5px",this.title.innerHTML="",this.container.appendChild(this.title),this.frame.prev=document.createElement("INPUT"),this.frame.prev.type="BUTTON",this.frame.prev.value="Prev",this.frame.appendChild(this.frame.prev),this.frame.play=document.createElement("INPUT"),this.frame.play.type="BUTTON",this.frame.play.value="Play",this.frame.appendChild(this.frame.play),this.frame.next=document.createElement("INPUT"),this.frame.next.type="BUTTON",this.frame.next.value="Next",this.frame.appendChild(this.frame.next),this.frame.bar=document.createElement("INPUT"),this.frame.bar.type="BUTTON",this.frame.bar.style.position="absolute",this.frame.bar.style.border="1px solid red",this.frame.bar.style.width="100px",this.frame.bar.style.height="6px",this.frame.bar.style.borderRadius="2px",this.frame.bar.style.MozBorderRadius="2px",this.frame.bar.style.border="1px solid #7F7F7F",this.frame.bar.style.backgroundColor="#E5E5E5",this.frame.appendChild(this.frame.bar),this.frame.slide=document.createElement("INPUT"),this.frame.slide.type="BUTTON",this.frame.slide.style.margin="0px",this.frame.slide.value=" ",this.frame.slide.style.position="relative",this.frame.slide.style.left="-100px",this.frame.appendChild(this.frame.slide);var e=this;this.frame.slide.onmousedown=function(t){e._onMouseDown(t)},this.frame.prev.onclick=function(t){e.prev(t)},this.frame.play.onclick=function(t){e.togglePlay(t)},this.frame.next.onclick=function(t){e.next(t)},this.container.appendChild(this.frame),this.onChangeCallback=void 0,this.playTimeout=void 0,this.framerate=20,this.duration=10,this.doLoop=!0,this.start=0,this.end=0,this.value=0,this.step=0,this.rangeIsDate=!1,this.redraw()},x.Slider.prototype._updateStep=function(){var t=this.end-this.start,e=this.duration*this.framerate;this.step=t/e},x.Slider.prototype.prev=function(){this._setValue(this.value-this.step)},x.Slider.prototype.next=function(){this._setValue(this.value+this.step)},x.Slider.prototype.playNext=function(){var t=new Date;if(!this.leftButtonDown)if(this.value+this.stepthis.start?(t-this.start)/(this.end-this.start)*i:0;var s=e+this.offset;return s},x.Slider.prototype._limitValue=function(t){return this.start>t&&(t=this.start),t>this.end&&(t=this.end),t},x.Slider.prototype._onMouseDown=function(t){if(this.leftButtonDown=t.which?1===t.which:1===t.button,this.leftButtonDown){this.startClientX=t.clientX,this.startSlideX=parseFloat(this.frame.slide.style.left),this.frame.style.cursor="move";var e=this;this.onmousemove=function(t){e._onMouseMove(t)},this.onmouseup=function(t){e._onMouseUp(t)},x.addEventListener(document,"mousemove",this.onmousemove),x.addEventListener(document,"mouseup",this.onmouseup),x.preventDefault(t)}},x.Slider.prototype._onMouseMove=function(t){var e=t.clientX-this.startClientX,i=this.startSlideX+e,s=this._leftToValue(i);this._setValue(s),x.preventDefault(t)},x.Slider.prototype._onMouseUp=function(t){this.frame.style.cursor="auto",this.leftButtonDown=!1,x.removeEventListener(document,"mousemove",this.onmousemove),x.removeEventListener(document,"mouseup",this.onmouseup),x.preventDefault(t)},x.Popup=function(t,e,i,s){this.container=t?t:document.body,this.x=0,this.y=0,this.padding=5,void 0!==e&&void 0!==i&&this.setPosition(e,i),void 0!==s&&this.setText(s),this.frame=document.createElement("div");var o=this.frame.style;o.position="absolute",o.visibility="hidden",o.border="1px solid #666",o.color="black",o.padding=this.padding+"px",o.backgroundColor="#FFFFC6",o.borderRadius="3px",o.MozBorderRadius="3px",o.WebkitBorderRadius="3px",o.boxShadow="3px 3px 10px rgba(128, 128, 128, 0.5)",o.whiteSpace="nowrap",this.container.appendChild(this.frame)},x.Popup.prototype.setPosition=function(t,e){this.x=parseInt(t),this.y=parseInt(e)},x.Popup.prototype.setText=function(t){this.frame.innerHTML=t},x.Popup.prototype.show=function(t){if(void 0===t&&(t=!0),t){var e=this.frame.clientHeight,i=this.frame.clientWidth,s=this.frame.parentNode.clientHeight,o=this.frame.parentNode.clientWidth,n=this.y-e; +n+e+this.padding>s&&(n=s-e-this.padding),this.padding>n&&(n=this.padding);var r=this.x;r+i+this.padding>o&&(r=o-i-this.padding),this.padding>r&&(r=this.padding),this.frame.style.left=r+"px",this.frame.style.top=n+"px",this.frame.style.visibility="visible"}else this.hide()},x.Popup.prototype.hide=function(){this.frame.style.visibility="hidden"},"undefined"!=typeof CanvasRenderingContext2D&&(CanvasRenderingContext2D.prototype.circle=function(t,e,i){this.beginPath(),this.arc(t,e,i,0,2*Math.PI,!1)},CanvasRenderingContext2D.prototype.square=function(t,e,i){this.beginPath(),this.rect(t-i,e-i,2*i,2*i)},CanvasRenderingContext2D.prototype.triangle=function(t,e,i){this.beginPath();var s=2*i,o=s/2,n=Math.sqrt(3)/6*s,r=Math.sqrt(s*s-o*o);this.moveTo(t,e-(r-n)),this.lineTo(t+o,e+n),this.lineTo(t-o,e+n),this.lineTo(t,e-(r-n)),this.closePath()},CanvasRenderingContext2D.prototype.triangleDown=function(t,e,i){this.beginPath();var s=2*i,o=s/2,n=Math.sqrt(3)/6*s,r=Math.sqrt(s*s-o*o);this.moveTo(t,e+(r-n)),this.lineTo(t+o,e-n),this.lineTo(t-o,e-n),this.lineTo(t,e+(r-n)),this.closePath()},CanvasRenderingContext2D.prototype.star=function(t,e,i){this.beginPath();for(var s=0;10>s;s++){var o=0===s%2?1.3*i:.5*i;this.lineTo(t+o*Math.sin(2*s*Math.PI/10),e-o*Math.cos(2*s*Math.PI/10))}this.closePath()},CanvasRenderingContext2D.prototype.roundRect=function(t,e,i,s,o){var n=Math.PI/180;0>i-2*o&&(o=i/2),0>s-2*o&&(o=s/2),this.beginPath(),this.moveTo(t+o,e),this.lineTo(t+i-o,e),this.arc(t+i-o,e+o,o,270*n,360*n,!1),this.lineTo(t+i,e+s-o),this.arc(t+i-o,e+s-o,o,0,90*n,!1),this.lineTo(t+o,e+s),this.arc(t+o,e+s-o,o,90*n,180*n,!1),this.lineTo(t,e+o),this.arc(t+o,e+o,o,180*n,270*n,!1)},CanvasRenderingContext2D.prototype.ellipse=function(t,e,i,s){var o=.5522848,n=i/2*o,r=s/2*o,a=t+i,h=e+s,d=t+i/2,l=e+s/2;this.beginPath(),this.moveTo(t,l),this.bezierCurveTo(t,l-r,d-n,e,d,e),this.bezierCurveTo(d+n,e,a,l-r,a,l),this.bezierCurveTo(a,l+r,d+n,h,d,h),this.bezierCurveTo(d-n,h,t,l+r,t,l)},CanvasRenderingContext2D.prototype.database=function(t,e,i,s){var o=1/3,n=i,r=s*o,a=.5522848,h=n/2*a,d=r/2*a,l=t+n,p=e+r,u=t+n/2,c=e+r/2,f=e+(s-r/2),m=e+s;this.beginPath(),this.moveTo(l,c),this.bezierCurveTo(l,c+d,u+h,p,u,p),this.bezierCurveTo(u-h,p,t,c+d,t,c),this.bezierCurveTo(t,c-d,u-h,e,u,e),this.bezierCurveTo(u+h,e,l,c-d,l,c),this.lineTo(l,f),this.bezierCurveTo(l,f+d,u+h,m,u,m),this.bezierCurveTo(u-h,m,t,f+d,t,f),this.lineTo(t,c)},CanvasRenderingContext2D.prototype.arrow=function(t,e,i,s){var o=t-s*Math.cos(i),n=e-s*Math.sin(i),r=t-.9*s*Math.cos(i),a=e-.9*s*Math.sin(i),h=o+s/3*Math.cos(i+.5*Math.PI),d=n+s/3*Math.sin(i+.5*Math.PI),l=o+s/3*Math.cos(i-.5*Math.PI),p=n+s/3*Math.sin(i-.5*Math.PI);this.beginPath(),this.moveTo(t,e),this.lineTo(h,d),this.lineTo(r,a),this.lineTo(l,p),this.closePath()}),x.util={},x.util.parseDOT=function(t){function e(t){return" "==t||" "==t||"\n"==t||"\r"==t}function i(t){return-1!="[]{}();,=->".indexOf(t)}function s(){f++,m=t[f]}function o(){return"(char "+f+")"}function n(){for(;m&&e(m);)s();var i=t[f+1],o=t[f-1],r=m+i;if("/*"==r){for(;m&&("*"!=m||"/"!=t[f+1]);)s();s(),s(),n()}else if("//"==r||"#"==m&&"\n"==o){for(;m&&"\n"!=m;)s();s(),n()}}function r(){n();var t="";if('"'==m){for(s();m&&'"'!=m;)t+=m,s();s()}else{for(;m&&!e(m)&&!i(m);)t+=m,s();var o=Number(t);isNaN(o)?"true"==t?t=!0:"false"==t?t=!1:"null"==t&&(t=null):t=o}return t}function a(){if(n(),'"'==m)return r();var t=r();if(void 0!=t){var e=Number(t);isNaN(e)?"true"==t?t=!0:"false"==t?t=!1:"null"==t&&(t=null):t=e}return t}function h(){if(n(),"["==m){s();for(var t={};m&&"]"!=m;){n();var e=r();if(!e)throw new SyntaxError("Attribute name expected "+o());if(n(),"="!=m)throw new SyntaxError("Equal sign = expected "+o());s();var i=a();if(!i)throw new SyntaxError("Attribute value expected "+o());t[e]=i,n(),","==m&&s()}return s(),t}return void 0}function d(){if(n(),"-"==m){if(s(),">"==m||"-"==m){var t="-"+m;return s(),t}throw new SyntaxError('Arrow "->" or "--" expected '+o())}return void 0}function l(){return n(),";"==m?(s(),";"):void 0}function p(t,e){if(t&&e)for(var i in e)e.hasOwnProperty(i)&&(t[i]=e[i])}function u(t,e){var i={id:t+"",attr:e||{}};g[t]?p(g[t].attr,i.attr):g[t]=i}function c(t,e,i,s){v.push({from:t+"",to:e+"",type:i,attr:s||{}})}var f=-1,m="",g={},v=[];for(s();m&&"{"!=m;)s();if("{"!=m)throw new SyntaxError("Invalid data. Curly bracket { expected "+o());for(s();m&&"}"!=m;){var y=r();if(void 0==y)throw new SyntaxError("String with id expected "+o());var w=h();u(y,w);for(var b=d();b;){var _=y;if(y=r(),void 0==y)throw new SyntaxError("String with id expected "+o());u(y),w=h(),c(_,y,b,w),b=d()}l(),n()}if("}"!=m)throw new SyntaxError("Invalid data. Curly bracket } expected");var x=t.indexOf("{"),T=t.indexOf("}",x),M=-1!=x&&-1!=T?t.substring(x+1,T):void 0;if(!M)throw Error("Invalid data. no curly brackets containing data found");var S=[];for(y in g)g.hasOwnProperty(y)&&S.push(g[y]);return{nodes:S,edges:v}},x.util.DOTToGraph=function(t){function e(t,e,i){for(var s in e)!e.hasOwnProperty(s)||i&&-1!=i.indexOf(s)||(t[s]=e[s]);t.label&&(t.text=t.label,delete t.label),t.shape&&(t.style=t.shape,delete t.shape)}var i=x.util.parseDOT(t),s={nodes:[],edges:[],options:{nodes:{},edges:{}}};return i.nodes.forEach(function(t){if("graph"==t.id.toLowerCase())e(s.options,t.attr);else if("node"==t.id.toLowerCase())e(s.options.nodes,t.attr);else if("edge"==t.id.toLowerCase())e(s.options.edges,t.attr);else{var i={};i.id=t.id,i.text=t.id,e(i,t.attr),s.nodes.push(i)}}),i.edges.forEach(function(t){var i={};i.from=t.from,i.to=t.to,i.text=t.id,i.style="->"==t.type?"arrow-end":"line",e(i,t.attr),s.edges.push(i)}),s};var k={util:M,events:E,Controller:d,DataSet:o,DataView:n,Range:a,Stack:r,TimeStep:TimeStep,EventBus:h,components:{items:{Item:m,ItemBox:g,ItemPoint:v,ItemRange:y},Component:l,Panel:p,RootPanel:u,ItemSet:f,TimeAxis:c},Timeline:_,Graph:x};s!==void 0&&(s=k),i!==void 0&&i.exports!==void 0&&(i.exports=k),"function"==typeof t&&t(function(){return k}),"undefined"!=typeof window&&(window.vis=k),M.loadCss("/* vis.js stylesheet */\n\n.graph {\n position: relative;\n border: 1px solid #bfbfbf;\n}\n\n.graph .panel {\n position: absolute;\n}\n\n.graph .groupset {\n position: absolute;\n padding: 0;\n margin: 0;\n}\n\n\n.graph .itemset {\n position: absolute;\n padding: 0;\n margin: 0;\n overflow: hidden;\n}\n\n.graph .background {\n}\n\n.graph .foreground {\n}\n\n.graph .itemset-axis {\n position: absolute;\n}\n\n.graph .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.graph .groupset .itemset-axis:last-child {\n border-top: none;\n}\n*/\n\n\n.graph .item {\n position: absolute;\n color: #1A1A1A;\n border-color: #97B0F8;\n background-color: #D5DDF6;\n display: inline-block;\n}\n\n.graph .item.selected {\n border-color: #FFC200;\n background-color: #FFF785;\n z-index: 999;\n}\n\n.graph .item.cluster {\n /* TODO: use another color or pattern? */\n background: #97B0F8 url('img/cluster_bg.png');\n color: white;\n}\n.graph .item.cluster.point {\n border-color: #D5DDF6;\n}\n\n.graph .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.graph .item.point {\n background: none;\n}\n\n.graph .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.graph .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.graph .item.range .drag-left {\n cursor: w-resize;\n z-index: 1000;\n}\n\n.graph .item.range .drag-right {\n cursor: e-resize;\n z-index: 1000;\n}\n\n.graph .item.range .content {\n position: relative;\n display: inline-block;\n}\n\n.graph .item.line {\n position: absolute;\n width: 0;\n border-left-width: 1px;\n border-left-style: solid;\n}\n\n.graph .item .content {\n margin: 5px;\n white-space: nowrap;\n overflow: hidden;\n}\n\n/* TODO: better css name, 'graph' is way to generic */\n\n.graph {\n overflow: hidden;\n}\n\n.graph .axis {\n position: relative;\n}\n\n.graph .axis .text {\n position: absolute;\n color: #4d4d4d;\n padding: 3px;\n white-space: nowrap;\n}\n\n.graph .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.graph .axis .grid.vertical {\n position: absolute;\n width: 0;\n border-right: 1px solid;\n}\n\n.graph .axis .grid.horizontal {\n position: absolute;\n left: 0;\n width: 100%;\n height: 0;\n border-bottom: 1px solid;\n}\n\n.graph .axis .grid.minor {\n border-color: #e5e5e5;\n}\n\n.graph .axis .grid.major {\n border-color: #bfbfbf;\n}\n\n")})()},{moment:2}],2:[function(e,i){(function(){(function(s){function o(t,e){return function(i){return p(t.call(this,i),e)}}function n(t){return function(e){return this.lang().ordinal(t.call(this,e))}}function r(){}function a(t){d(this,t)}function h(t){var e=this._data={},i=t.years||t.year||t.y||0,s=t.months||t.month||t.M||0,o=t.weeks||t.week||t.w||0,n=t.days||t.day||t.d||0,r=t.hours||t.hour||t.h||0,a=t.minutes||t.minute||t.m||0,h=t.seconds||t.second||t.s||0,d=t.milliseconds||t.millisecond||t.ms||0;this._milliseconds=d+1e3*h+6e4*a+36e5*r,this._days=n+7*o,this._months=s+12*i,e.milliseconds=d%1e3,h+=l(d/1e3),e.seconds=h%60,a+=l(h/60),e.minutes=a%60,r+=l(a/60),e.hours=r%24,n+=l(r/24),n+=7*o,e.days=n%30,s+=l(n/30),e.months=s%12,i+=l(s/12),e.years=i}function d(t,e){for(var i in e)e.hasOwnProperty(i)&&(t[i]=e[i]);return t}function l(t){return 0>t?Math.ceil(t):Math.floor(t)}function p(t,e){for(var i=t+"";e>i.length;)i="0"+i;return i}function u(t,e,i){var s,o=e._milliseconds,n=e._days,r=e._months;o&&t._d.setTime(+t+o*i),n&&t.date(t.date()+n*i),r&&(s=t.date(),t.date(1).month(t.month()+r*i).date(Math.min(s,t.daysInMonth())))}function c(t){return"[object Array]"===Object.prototype.toString.call(t)}function f(t,e){var i,s=Math.min(t.length,e.length),o=Math.abs(t.length-e.length),n=0;for(i=0;s>i;i++)~~t[i]!==~~e[i]&&n++;return n+o}function m(t,e){return e.abbr=t,z[t]||(z[t]=new r),z[t].set(e),z[t]}function g(t){return t?(!z[t]&&R&&e("./lang/"+t),z[t]):F.fn._lang}function v(t){return t.match(/\[.*\]/)?t.replace(/^\[|\]$/g,""):t.replace(/\\/g,"")}function y(t){var e,i,s=t.match(j);for(e=0,i=s.length;i>e;e++)s[e]=ae[s[e]]?ae[s[e]]:v(s[e]);return function(o){var n="";for(e=0;i>e;e++)n+="function"==typeof s[e].call?s[e].call(o,t):s[e];return n}}function w(t,e){function i(e){return t.lang().longDateFormat(e)||e}for(var s=5;s--&&U.test(e);)e=e.replace(U,i);return oe[e]||(oe[e]=y(e)),oe[e](t)}function b(t){switch(t){case"DDDD":return B;case"YYYY":return q;case"YYYYY":return X;case"S":case"SS":case"SSS":case"DDD":return V;case"MMM":case"MMMM":case"dd":case"ddd":case"dddd":case"a":case"A":return K;case"X":return J;case"Z":case"ZZ":return G;case"T":return Z;case"MM":case"DD":case"YY":case"HH":case"hh":case"mm":case"ss":case"M":case"D":case"d":case"H":case"h":case"m":case"s":return W;default:return RegExp(t.replace("\\",""))}}function _(t,e,i){var s,o=i._a;switch(t){case"M":case"MM":o[1]=null==e?0:~~e-1;break;case"MMM":case"MMMM":s=g(i._l).monthsParse(e),null!=s?o[1]=s:i._isValid=!1;break;case"D":case"DD":case"DDD":case"DDDD":null!=e&&(o[2]=~~e);break;case"YY":o[0]=~~e+(~~e>68?1900:2e3);break;case"YYYY":case"YYYYY":o[0]=~~e;break;case"a":case"A":i._isPm="pm"===(e+"").toLowerCase();break;case"H":case"HH":case"h":case"hh":o[3]=~~e;break;case"m":case"mm":o[4]=~~e;break;case"s":case"ss":o[5]=~~e;break;case"S":case"SS":case"SSS":o[6]=~~(1e3*("0."+e));break;case"X":i._d=new Date(1e3*parseFloat(e));break;case"Z":case"ZZ":i._useUTC=!0,s=(e+"").match(ee),s&&s[1]&&(i._tzh=~~s[1]),s&&s[2]&&(i._tzm=~~s[2]),s&&"+"===s[0]&&(i._tzh=-i._tzh,i._tzm=-i._tzm)}null==e&&(i._isValid=!1)}function x(t){var e,i,s=[];if(!t._d){for(e=0;7>e;e++)t._a[e]=s[e]=null==t._a[e]?2===e?1:0:t._a[e];s[3]+=t._tzh||0,s[4]+=t._tzm||0,i=new Date(0),t._useUTC?(i.setUTCFullYear(s[0],s[1],s[2]),i.setUTCHours(s[3],s[4],s[5],s[6])):(i.setFullYear(s[0],s[1],s[2]),i.setHours(s[3],s[4],s[5],s[6])),t._d=i}}function T(t){var e,i,s=t._f.match(j),o=t._i;for(t._a=[],e=0;s.length>e;e++)i=(b(s[e]).exec(o)||[])[0],i&&(o=o.slice(o.indexOf(i)+i.length)),ae[s[e]]&&_(s[e],i,t);t._isPm&&12>t._a[3]&&(t._a[3]+=12),t._isPm===!1&&12===t._a[3]&&(t._a[3]=0),x(t)}function M(t){for(var e,i,s,o,n=99;t._f.length;){if(e=d({},t),e._f=t._f.pop(),T(e),i=new a(e),i.isValid()){s=i;break}o=f(e._a,i.toArray()),n>o&&(n=o,s=i)}d(t,s)}function S(t){var e,i=t._i;if(Q.exec(i)){for(t._f="YYYY-MM-DDT",e=0;4>e;e++)if(te[e][1].exec(i)){t._f+=te[e][0];break}G.exec(i)&&(t._f+=" Z"),T(t)}else t._d=new Date(i)}function C(t){var e=t._i,i=H.exec(e);e===s?t._d=new Date:i?t._d=new Date(+i[1]):"string"==typeof e?S(t):c(e)?(t._a=e.slice(0),x(t)):t._d=e instanceof Date?new Date(+e):new Date(e)}function E(t,e,i,s,o){return o.relativeTime(e||1,!!i,t,s)}function D(t,e,i){var s=Y(Math.abs(t)/1e3),o=Y(s/60),n=Y(o/60),r=Y(n/24),a=Y(r/365),h=45>s&&["s",s]||1===o&&["m"]||45>o&&["mm",o]||1===n&&["h"]||22>n&&["hh",n]||1===r&&["d"]||25>=r&&["dd",r]||45>=r&&["M"]||345>r&&["MM",Y(r/30)]||1===a&&["y"]||["yy",a];return h[2]=e,h[3]=t>0,h[4]=i,E.apply({},h)}function k(t,e,i){var s=i-e,o=i-t.day();return o>s&&(o-=7),s-7>o&&(o+=7),Math.ceil(F(t).add("d",o).dayOfYear()/7)}function L(t){var e=t._i,i=t._f;return null===e||""===e?null:("string"==typeof e&&(t._i=e=g().preparse(e)),F.isMoment(e)?(t=d({},e),t._d=new Date(+e._d)):i?c(i)?M(t):T(t):C(t),new a(t))}function O(t,e){F.fn[t]=F.fn[t+"s"]=function(t){var i=this._isUTC?"UTC":"";return null!=t?(this._d["set"+i+e](t),this):this._d["get"+i+e]()}}function N(t){F.duration.fn[t]=function(){return this._data[t]}}function A(t,e){F.duration.fn["as"+t]=function(){return+this/e}}for(var F,P,I="2.0.0",Y=Math.round,z={},R=i!==s&&i.exports,H=/^\/?Date\((\-?\d+)/i,j=/(\[[^\[]*\])|(\\)?(Mo|MM?M?M?|Do|DDDo|DD?D?D?|ddd?d?|do?|w[o|w]?|W[o|W]?|YYYYY|YYYY|YY|a|A|hh?|HH?|mm?|ss?|SS?S?|X|zz?|ZZ?|.)/g,U=/(\[[^\[]*\])|(\\)?(LT|LL?L?L?|l{1,4})/g,W=/\d\d?/,V=/\d{1,3}/,B=/\d{3}/,q=/\d{1,4}/,X=/[+\-]?\d{1,6}/,K=/[0-9]*[a-z\u00A0-\u05FF\u0700-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+|[\u0600-\u06FF]+\s*?[\u0600-\u06FF]+/i,G=/Z|[\+\-]\d\d:?\d\d/i,Z=/T/i,J=/[\+\-]?\d+(\.\d{1,3})?/,Q=/^\s*\d{4}-\d\d-\d\d((T| )(\d\d(:\d\d(:\d\d(\.\d\d?\d?)?)?)?)?([\+\-]\d\d:?\d\d)?)?/,$="YYYY-MM-DDTHH:mm:ssZ",te=[["HH:mm:ss.S",/(T| )\d\d:\d\d:\d\d\.\d{1,3}/],["HH:mm:ss",/(T| )\d\d:\d\d:\d\d/],["HH:mm",/(T| )\d\d:\d\d/],["HH",/(T| )\d\d/]],ee=/([\+\-]|\d\d)/gi,ie="Month|Date|Hours|Minutes|Seconds|Milliseconds".split("|"),se={Milliseconds:1,Seconds:1e3,Minutes:6e4,Hours:36e5,Days:864e5,Months:2592e6,Years:31536e6},oe={},ne="DDD w W M D d".split(" "),re="M D H h m s w W".split(" "),ae={M:function(){return this.month()+1},MMM:function(t){return this.lang().monthsShort(this,t)},MMMM:function(t){return this.lang().months(this,t)},D:function(){return this.date()},DDD:function(){return this.dayOfYear()},d:function(){return this.day()},dd:function(t){return this.lang().weekdaysMin(this,t)},ddd:function(t){return this.lang().weekdaysShort(this,t)},dddd:function(t){return this.lang().weekdays(this,t)},w:function(){return this.week()},W:function(){return this.isoWeek()},YY:function(){return p(this.year()%100,2)},YYYY:function(){return p(this.year(),4)},YYYYY:function(){return p(this.year(),5)},a:function(){return this.lang().meridiem(this.hours(),this.minutes(),!0)},A:function(){return this.lang().meridiem(this.hours(),this.minutes(),!1)},H:function(){return this.hours()},h:function(){return this.hours()%12||12},m:function(){return this.minutes()},s:function(){return this.seconds()},S:function(){return~~(this.milliseconds()/100)},SS:function(){return p(~~(this.milliseconds()/10),2)},SSS:function(){return p(this.milliseconds(),3)},Z:function(){var t=-this.zone(),e="+";return 0>t&&(t=-t,e="-"),e+p(~~(t/60),2)+":"+p(~~t%60,2)},ZZ:function(){var t=-this.zone(),e="+";return 0>t&&(t=-t,e="-"),e+p(~~(10*t/6),4)},X:function(){return this.unix()}};ne.length;)P=ne.pop(),ae[P+"o"]=n(ae[P]);for(;re.length;)P=re.pop(),ae[P+P]=o(ae[P],2);for(ae.DDDD=o(ae.DDD,3),r.prototype={set:function(t){var e,i;for(i in t)e=t[i],"function"==typeof e?this[i]=e:this["_"+i]=e},_months:"January_February_March_April_May_June_July_August_September_October_November_December".split("_"),months:function(t){return this._months[t.month()]},_monthsShort:"Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec".split("_"),monthsShort:function(t){return this._monthsShort[t.month()]},monthsParse:function(t){var e,i,s;for(this._monthsParse||(this._monthsParse=[]),e=0;12>e;e++)if(this._monthsParse[e]||(i=F([2e3,e]),s="^"+this.months(i,"")+"|^"+this.monthsShort(i,""),this._monthsParse[e]=RegExp(s.replace(".",""),"i")),this._monthsParse[e].test(t))return e},_weekdays:"Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),weekdays:function(t){return this._weekdays[t.day()]},_weekdaysShort:"Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"),weekdaysShort:function(t){return this._weekdaysShort[t.day()]},_weekdaysMin:"Su_Mo_Tu_We_Th_Fr_Sa".split("_"),weekdaysMin:function(t){return this._weekdaysMin[t.day()]},_longDateFormat:{LT:"h:mm A",L:"MM/DD/YYYY",LL:"MMMM D YYYY",LLL:"MMMM D YYYY LT",LLLL:"dddd, MMMM D YYYY LT"},longDateFormat:function(t){var e=this._longDateFormat[t];return!e&&this._longDateFormat[t.toUpperCase()]&&(e=this._longDateFormat[t.toUpperCase()].replace(/MMMM|MM|DD|dddd/g,function(t){return t.slice(1)}),this._longDateFormat[t]=e),e},meridiem:function(t,e,i){return t>11?i?"pm":"PM":i?"am":"AM"},_calendar:{sameDay:"[Today at] LT",nextDay:"[Tomorrow at] LT",nextWeek:"dddd [at] LT",lastDay:"[Yesterday at] LT",lastWeek:"[last] dddd [at] LT",sameElse:"L"},calendar:function(t,e){var i=this._calendar[t];return"function"==typeof i?i.apply(e):i},_relativeTime:{future:"in %s",past:"%s ago",s:"a few seconds",m:"a minute",mm:"%d minutes",h:"an hour",hh:"%d hours",d:"a day",dd:"%d days",M:"a month",MM:"%d months",y:"a year",yy:"%d years"},relativeTime:function(t,e,i,s){var o=this._relativeTime[i];return"function"==typeof o?o(t,e,i,s):o.replace(/%d/i,t)},pastFuture:function(t,e){var i=this._relativeTime[t>0?"future":"past"];return"function"==typeof i?i(e):i.replace(/%s/i,e)},ordinal:function(t){return this._ordinal.replace("%d",t)},_ordinal:"%d",preparse:function(t){return t},postformat:function(t){return t},week:function(t){return k(t,this._week.dow,this._week.doy)},_week:{dow:0,doy:6}},F=function(t,e,i){return L({_i:t,_f:e,_l:i,_isUTC:!1})},F.utc=function(t,e,i){return L({_useUTC:!0,_isUTC:!0,_l:i,_i:t,_f:e})},F.unix=function(t){return F(1e3*t)},F.duration=function(t,e){var i,s=F.isDuration(t),o="number"==typeof t,n=s?t._data:o?{}:t;return o&&(e?n[e]=t:n.milliseconds=t),i=new h(n),s&&t.hasOwnProperty("_lang")&&(i._lang=t._lang),i},F.version=I,F.defaultFormat=$,F.lang=function(t,e){return t?(e?m(t,e):z[t]||g(t),F.duration.fn._lang=F.fn._lang=g(t),s):F.fn._lang._abbr},F.langData=function(t){return t&&t._lang&&t._lang._abbr&&(t=t._lang._abbr),g(t)},F.isMoment=function(t){return t instanceof a},F.isDuration=function(t){return t instanceof h},F.fn=a.prototype={clone:function(){return F(this)},valueOf:function(){return+this._d},unix:function(){return Math.floor(+this._d/1e3)},toString:function(){return this.format("ddd MMM DD YYYY HH:mm:ss [GMT]ZZ")},toDate:function(){return this._d},toJSON:function(){return F.utc(this).format("YYYY-MM-DD[T]HH:mm:ss.SSS[Z]")},toArray:function(){var t=this;return[t.year(),t.month(),t.date(),t.hours(),t.minutes(),t.seconds(),t.milliseconds()]},isValid:function(){return null==this._isValid&&(this._isValid=this._a?!f(this._a,(this._isUTC?F.utc(this._a):F(this._a)).toArray()):!isNaN(this._d.getTime())),!!this._isValid},utc:function(){return this._isUTC=!0,this},local:function(){return this._isUTC=!1,this},format:function(t){var e=w(this,t||F.defaultFormat);return this.lang().postformat(e)},add:function(t,e){var i;return i="string"==typeof t?F.duration(+e,t):F.duration(t,e),u(this,i,1),this},subtract:function(t,e){var i;return i="string"==typeof t?F.duration(+e,t):F.duration(t,e),u(this,i,-1),this},diff:function(t,e,i){var s,o,n=this._isUTC?F(t).utc():F(t).local(),r=6e4*(this.zone()-n.zone());return e&&(e=e.replace(/s$/,"")),"year"===e||"month"===e?(s=432e5*(this.daysInMonth()+n.daysInMonth()),o=12*(this.year()-n.year())+(this.month()-n.month()),o+=(this-F(this).startOf("month")-(n-F(n).startOf("month")))/s,"year"===e&&(o/=12)):(s=this-n-r,o="second"===e?s/1e3:"minute"===e?s/6e4:"hour"===e?s/36e5:"day"===e?s/864e5:"week"===e?s/6048e5:s),i?o:l(o)},from:function(t,e){return F.duration(this.diff(t)).lang(this.lang()._abbr).humanize(!e)},fromNow:function(t){return this.from(F(),t)},calendar:function(){var t=this.diff(F().startOf("day"),"days",!0),e=-6>t?"sameElse":-1>t?"lastWeek":0>t?"lastDay":1>t?"sameDay":2>t?"nextDay":7>t?"nextWeek":"sameElse";return this.format(this.lang().calendar(e,this))},isLeapYear:function(){var t=this.year();return 0===t%4&&0!==t%100||0===t%400},isDST:function(){return this.zone()+F(t).startOf(e)},isBefore:function(t,e){return e=e!==s?e:"millisecond",+this.clone().startOf(e)<+F(t).startOf(e)},isSame:function(t,e){return e=e!==s?e:"millisecond",+this.clone().startOf(e)===+F(t).startOf(e)},zone:function(){return this._isUTC?0:this._d.getTimezoneOffset()},daysInMonth:function(){return F.utc([this.year(),this.month()+1,0]).date()},dayOfYear:function(t){var e=Y((F(this).startOf("day")-F(this).startOf("year"))/864e5)+1;return null==t?e:this.add("d",t-e)},isoWeek:function(t){var e=k(this,1,4);return null==t?e:this.add("d",7*(t-e))},week:function(t){var e=this.lang().week(this);return null==t?e:this.add("d",7*(t-e))},lang:function(t){return t===s?this._lang:(this._lang=g(t),this)}},P=0;ie.length>P;P++)O(ie[P].toLowerCase().replace(/s$/,""),ie[P]);O("year","FullYear"),F.fn.days=F.fn.day,F.fn.weeks=F.fn.week,F.fn.isoWeeks=F.fn.isoWeek,F.duration.fn=h.prototype={weeks:function(){return l(this.days()/7)},valueOf:function(){return this._milliseconds+864e5*this._days+2592e6*this._months},humanize:function(t){var e=+this,i=D(e,!t,this.lang());return t&&(i=this.lang().pastFuture(e,i)),this.lang().postformat(i)},lang:F.fn.lang};for(P in se)se.hasOwnProperty(P)&&(A(P,se[P]),N(P.toLowerCase()));A("Weeks",6048e5),F.lang("en",{ordinal:function(t){var e=t%10,i=1===~~(t%100/10)?"th":1===e?"st":2===e?"nd":3===e?"rd":"th";return t+i}}),R&&(i.exports=F),"undefined"==typeof ender&&(this.moment=F),"function"==typeof t&&t.amd&&t("moment",[],function(){return F})}).call(this)})()},{}]},{},[1])(1)}); \ No newline at end of file