From 5ed75fb9eb2a53dd4b050ae91b5adbb141986948 Mon Sep 17 00:00:00 2001 From: Alex de Mulder Date: Thu, 8 Jan 2015 11:39:10 +0100 Subject: [PATCH] added showcase --- featureRequests.html | 16 +- graph2d_examples.html | 1 + graph3d_examples.html | 1 + index.html | 9 +- network_examples.html | 1 + showcase.html | 47 - showcase/images/midas.png | Bin 0 -> 127399 bytes showcase/index.html | 93 + .../midas/arumAgents/agentGenerator.js | 216 + .../projects/midas/arumAgents/durationData.js | 40 + .../midas/arumAgents/durationStats.js | 142 + .../midas/arumAgents/eventGenerator.js | 2037 + .../projects/midas/arumAgents/genericAgent.js | 85 + showcase/projects/midas/arumAgents/job.js | 116 + .../projects/midas/arumAgents/jobAgent.js | 434 + .../midas/arumAgents/jobAgentGenerator.js | 169 + .../projects/midas/arumAgents/jobManager.js | 428 + showcase/projects/midas/css/global.css | 301 + showcase/projects/midas/css/vis.css | 745 + showcase/projects/midas/images/arum.png | Bin 0 -> 27740 bytes showcase/projects/midas/images/arum_small.png | Bin 0 -> 31761 bytes .../projects/midas/images/control_pause.png | Bin 0 -> 598 bytes .../midas/images/control_pause_blue.png | Bin 0 -> 721 bytes .../projects/midas/images/control_play.png | Bin 0 -> 592 bytes .../midas/images/control_play_blue.png | Bin 0 -> 717 bytes showcase/projects/midas/images/midas_big.png | Bin 0 -> 36594 bytes .../projects/midas/images/midas_small.png | Bin 0 -> 33174 bytes showcase/projects/midas/images/moon.png | Bin 0 -> 18243 bytes showcase/projects/midas/images/shaded.png | Bin 0 -> 17987 bytes showcase/projects/midas/images/sun.png | Bin 0 -> 18155 bytes showcase/projects/midas/images/toggleOff.png | Bin 0 -> 20039 bytes showcase/projects/midas/images/toggleOn.png | Bin 0 -> 20281 bytes showcase/projects/midas/index.html | 86 + showcase/projects/midas/js/evejs.js | 31161 ++++++++++++++ showcase/projects/midas/js/init.js | 350 + showcase/projects/midas/js/vis.js | 33780 ++++++++++++++++ showcase/projects/midas/js/vis_eve_init.js | 105 + timeline_examples.html | 1 + 38 files changed, 70307 insertions(+), 57 deletions(-) delete mode 100644 showcase.html create mode 100644 showcase/images/midas.png create mode 100644 showcase/index.html create mode 100644 showcase/projects/midas/arumAgents/agentGenerator.js create mode 100644 showcase/projects/midas/arumAgents/durationData.js create mode 100644 showcase/projects/midas/arumAgents/durationStats.js create mode 100644 showcase/projects/midas/arumAgents/eventGenerator.js create mode 100644 showcase/projects/midas/arumAgents/genericAgent.js create mode 100644 showcase/projects/midas/arumAgents/job.js create mode 100644 showcase/projects/midas/arumAgents/jobAgent.js create mode 100644 showcase/projects/midas/arumAgents/jobAgentGenerator.js create mode 100644 showcase/projects/midas/arumAgents/jobManager.js create mode 100644 showcase/projects/midas/css/global.css create mode 100644 showcase/projects/midas/css/vis.css create mode 100644 showcase/projects/midas/images/arum.png create mode 100644 showcase/projects/midas/images/arum_small.png create mode 100644 showcase/projects/midas/images/control_pause.png create mode 100644 showcase/projects/midas/images/control_pause_blue.png create mode 100644 showcase/projects/midas/images/control_play.png create mode 100644 showcase/projects/midas/images/control_play_blue.png create mode 100644 showcase/projects/midas/images/midas_big.png create mode 100644 showcase/projects/midas/images/midas_small.png create mode 100644 showcase/projects/midas/images/moon.png create mode 100644 showcase/projects/midas/images/shaded.png create mode 100644 showcase/projects/midas/images/sun.png create mode 100644 showcase/projects/midas/images/toggleOff.png create mode 100644 showcase/projects/midas/images/toggleOn.png create mode 100644 showcase/projects/midas/index.html create mode 100644 showcase/projects/midas/js/evejs.js create mode 100644 showcase/projects/midas/js/init.js create mode 100644 showcase/projects/midas/js/vis.js create mode 100644 showcase/projects/midas/js/vis_eve_init.js diff --git a/featureRequests.html b/featureRequests.html index 5ce7ce92..f5102531 100644 --- a/featureRequests.html +++ b/featureRequests.html @@ -20,6 +20,7 @@ - - - -

Showcase projects

- -
- - MIDAS - - Git - - Cool examples (?) -
- - - - - - - - \ No newline at end of file diff --git a/showcase/images/midas.png b/showcase/images/midas.png new file mode 100644 index 0000000000000000000000000000000000000000..4c69a91352c03fb838d6d2049d23b41dd211ccde GIT binary patch literal 127399 zcmeFZbyQW~7B+kk6hsLL2}M9U58WjtDN@oP98%)Y4F{2skdSUAr3Iv0NT>UO+%dlQ{pTI-F^IL-o^$Q>?78Ne^O^gcfTvF$W1thGgFqk*X(@3f5D3`^ z1VRc!y9S(b+2{QN{JCi@rC|pG-NL>6K?21m5`sYJ3T9$rPoJ7X?V)z2P-`-2F)=c0 zTd1*_g%JqkJeH(vqN2QVSMX?RUsU$BSEMXd2_KD2Ni@g{FPxg5{3f={Yx4J__==5p zBqUJoeR=m9IV8j@7+;a`b|Cr)>O6UB$g8y1fgSK!w>0yinxn<8i_&4CrM$hQk{;9s zwA;~A?24RTx3fhb+?jdR*4#Qf&n)s1ozfbFjaFhvZg)dypm$6l|k=NK+iiN26LdN zOrYm9u*H6mSJH8u8xlw>k{loDT{wsg*XXr4$dn(H*Yhk;0;J9Y!ZVg_<^_!~fmo!~ zjHN&y%Rvp@*td#7XtzNuim!d?KqzjY=U*r&oIyTuAUw%kHGv(50_-Jv0I9fQfl5kV ziI-YvOg5-$YRnI4yQK*j@VNA^>BoyOe|Cu@VtdJfHM92>1bP>a56pJ&+_@XGpu3wZ zqzqG!X|?_a;-P`T+{HpquC)jVG;8nKd%*}UA@<_G;bnP|O0|h%s(b65!%>iNF_uUv z=-tYg+Me|#HWKf{Dn~~b=I2LbnnktrJJdWbOg`&1sGVD!cnX{y9n92yr+mq#^YSsu z@yzG1`*Iogd*0skF_>5keSB7a3vou$|FBuwpi-S4Z%z@%CPX|wc#kdPVSwmcx)!QF zgyHvTMmVz2G$-ie3n3)8aHbDB5adHy8fyLSOZfcp zJ1UTuvGiLT5J*>?l3AsrM7RwN1QLJulK$hPd;5)qj14#LH(sA;ymj`R%SV*5`Ligt zD7vB7ecR`BdETP5eqA3)=%0fJL`XOq6ixjitTCBCt5#qNT4SB+qa`=e*1Wul^5_d1 zu0HwLYb1j}b&3!)f;it}@Z&^@&4dPQtp4g!j1)sSp}1gd zx7T9L+>|&0Vr(>(Vb`RK(pCC!`Xu|TlqgmhvlBR^h$$l4#|+JjQF$X7$r!%)&L_<` z&9l$nndvM)t$+x2Tx+Ysr7ZPJYJ4jM&5V2jp2v? zs_nH2El~m?DxnFH6JcSjTqaR}5K)^73peXp>{1+798^`EzKq@-KfD7yN+yar>K;8< zB|9fGXDa8G>Yi$+N@%vOvPXfWD!cMr_RWFlT-BVUr)$dhlufevKltaEe1NJ5r+aFg z2i(wTl50{BOILhXq&HX7N_GoERN~A?ZjO=b=kSuVnYyTJ)ZLN3I`*8TI!G`l`#rB1 zy9~QpiPB!Au`udftj%$v{gazZv{^BGo`F+%TXf&)?qzbwRk9jso99HwM62gY!%ZxlMUv{B}2?Kntf0RDcBBHol`M zrqd)MmZ2Y|A2aI{UfC;e)nv7*1$|{Vci{~{nH8#s?tb8?jH8Rvy zHFZiP^ON$ahN|=LF^l_)Gy)3(&=;Ol3jq!pq%Q6pr69WR1@|J?8 z&4}6vk1CHh=Ypsh{-c<=B$_0icAsqV_@41i#djxe3VXEdN3VcQ)O)adM%d@da69Rm z&61Q8`X8GRm{i)m+It-oAnV=993#Y>aIbapOYOXj{eg|ntZUyvQbI|>=Zz2Jg_~TP zW}E&KDPK8K=#n+q?Y=dA5t~t5=oP+|62)RZo#&|_rts3Fbcq5C0f(#Q4}UDraLPH- z-y#q?6tYd36Pgp?o?@HQ(EG~WQ2wR7Q=ti(tmiu|2BSb2B3u!@pe!#cBopL-mycd* zU4tLHx|7$32=k2_9TtynJPLkS`3V0}sCT~iLSsI;>6i7eeDb5E*zWLdGfdy7p36hu zQ1ut}gPJ^V)A;)Ps`&|rSie>$?q!YPbn2ZDvzrz_6^|C5N-$wn;4Ng5;iMPJVSXVn z#$ClxCEUtvtnqbFJxN{KLiRo{GkpPU}*TrcM9g$*vT=yzATb z@ET4R$`@n^$URj)CO4h=R&WTx!!L-5{9s7ha{p{1m!J))BIHrqs zy!us{SY=whKt+vFR(p3BzpzTW%D&2}3fE@eWU2n}u~qNpJ-3=ej`hKDt(v2Aq4v6Q zRU}pULE}LqxHP=vNVp&%XXQ8=ZmE<1#L{M?4Y?2b9F+pIY`Ah@kfcvgMJaTMVeK(y z8)@A|(v}kw1{a3vo5a@d6xQM{^D9R;r;i?B@QpbJyXDQI_LDl2n2>;9h{o^6OC~7C z!?g^wsuaEo^$x90xt={9C&fzS5YTXu*;8pZ8(Z=2k1*No2~LVi^6+FnnRq^)TIRL{ z>7IUp1>LH`wx8{jKm;xE*YJ01?^f9F7LPCBs#U1P7xL*@uG+5(TtxIMv{ffo#}`y; zn^$hS)|R>%AFbR==I)-{uXWkEix?VA<`tZ*-E)N=wHzHx-<|Zdr8)2JtR8iu+03s| zsL|UO*?2T6Ht9botaULlvNZvF(ctuMuXp0{(GA~MJs05zp;%(qL*qg_{V@F2l6i!9 zdA|$Wp0Au`iS-!vP$v^yq+WCu5ZncO9uKY!clXBjzK_|BiKw@9&p2OgSR*PNZ~ff* zpswASomF@L8GNJBX~KzQD{UG}Vcwk#MvE{#DLPR8mIaR>iikjT)UM#`>FR3NVA5MINF!b0c*&U}CZOCx)IGG|K*D?2`C!G}Ne@&W%} zo(4Z8`*DfAx!}V`mjjZi%ReO(gW4LAaWZo;L0DKh$hdi#Svk45dAS+M*nl>eg$K;W z!of7u8FIxWP+N*k?hGxbt|C5%#ul!dX^74GrR(AIKRuCg; zaUo#t%w}eWd^`pah_M00n2C*3pM%MOhr@u$fQ`q1$&l56g~x!Cmz|Y`=f@=fRs7#n zi$fs}mxctW{<-iCp%6g*U*^GQ%)5HfH73 zXZ>@ae<=I6ffQ`b0KcSf@sDdR=VAy9#-Y#3&BkG<&%|b^Z^*=M%)!RQt8c`~WC$_f zVB_R6GUnFj{d0)_pyuC(k}|UctjXmcRtDJG56`1)Wb?0Q|Lm|Z`{6;X^=<8pE*GZY z!#`K%UoPh_k8&yVV}bMOLoR)f5aiN)jSL0A|8n;4+xkoL4>7ZUf1Ljt1V8ovL;Syw z;bdxL^(*uLsp`j7|6Pe4)Y#rh-`41n31A`r%gX$z^gzS_ai;_u8 zh;gyh(}^P>HQ7Y6+nL7{0-Ny@ww9b8?GyW z{JQxYu3zJGrS~^nR{;5S^EX_-#^*}!Z@8`i^6TbrxPFb#mEPZQT><3R&EIhS8lNk@ zzu~$9$gi8f;rcZ`S9*WLbp?=LH-E$RYkaQs{)X!cAir+@hU?e(T{TiPuy}#kQ0?4nMzv22dK395w!*vCaUpIfl^=o{t^!|qH3Lw93{)X$<_+07z z4c8Swe%<^H*RS!p()$~(D}emE`5UfZ<8!6=H(XZ$`E~P`aH0SGK~N(r;M1Q@z{fnX zRm^Sx9}y*kNGZvKK(16Ekk=~^=mY`$T>*g{SU{lF=fI~+V?iJS=v%!ONf1(Aq_p@W z73Z<_TBk%E)k*{+uNZE|ZkJj9HJv6MTZ(+(DL#?}wnVs_ViCi%d9`pOgG8fki<_e2 zK0O*et&-1ET+M4MH`9;48nVUdJ`pyFQT2qax9g)(#z~9zpL@#hj+yjmnirNH#+L}g zHF{j{zEu(x`SWO~M#8N68v^V!q(6_BgcxXl9nGeH3W$(NB47LKU?s->^LX9s_17U1 zd;{gr!+_&IsYkfa+g6G{+)0=Xu7*$EWK&>kCY|H?Ws~vtq6hm6Xvlub@C^DbX&Z zh|MQVg?@qbo*(Pjdfg^tw6$|;uPFpFcXqPZ1K`mAgg-wu_4~2odLit5%>#aj)ZkPP zpIl0TC1dY$wSRNlsy!;G-W;p2f$Yq`rIMW@VkJKQ^NHPOkd0w>xW5$s_`&EWv;Hr3 zrJp{X9l;Ps+wA4jo^HS>tj}6Gy1o}>nzgBW8)@{K4iq!W_fM@0hsk7cSxq<(>p1B+ zjvCo5b)Sz7*Ky@M;1QUdoOGJ7d8|k*wB3LVZL4+LtX%5mTgwB(PCA>SK4h9zJ8rm| zEcQ&JM)yLFNL-w9FuZ;m&5WLR8!7BQX!qm{aj}KCn8kt->KANzY`Lx2R~_$6*!9$Y z4`c{=RYAmM=4p?++@kFhriVBkrb%%xp5rOAT(Ye+WE<`6?d`ifip}~nLqafHV^f^B zrfTa?2Md84N$&C3v;z#n>BWl|Aw}eX1%TaLT?0vB$4#WV?mI2Lru#fo6)9))G~83p zX>&Xmk5yF__%}WUf$N;7YFu!s<(y4WMtXyKb*ddLwd?iaWtQjX9-`inO8o;ikT&^v zwv+S-1x#O2)C?zAc$k^3FEP~par@F96B82%hhk!2Vm+bXu5!ItO;@A#g@f$cRH*K0 zco1`?;W5LPi$h1c46}Iot}cqLOl_|5p&{yY6?~)h3L8|SD)^~ub#)aI+1;Q)ZVnI# zV$l+a->IuHR0M-jCsF(Oq=mD-d1JTOog`=yl1dA$^RXNUgFRfja#Z0*$+{!xa>`tQM|h^*dvTMGbudz8hI0UuJ>)9)F(#bAVOnCJmw&AS<%_k)p+(N zf*Xf`t#pTU8{CcVV&dOO6fB>t%c?FZ$u)O2yT@&n$Y;|RdwNQwoMkmxr7CmMJa?-{ z;9$U6TAC4$3;&?p>D<`)RH8?Szb?D7Am_9=)VDO+}!it*lFQOCt3d?IObGb4IwSyu^Y8Y0Y*Efu6tz zpBTWV#7K{y8=2Km-VgBo>>2&qEXs!hgR?Y7*S5W+M}Xp|tGZa>F@1les;1-6%;+mR zJ0GF&AbE{J?<6pCm!ST*r3p*eJ@}w+o$%1aqL8-mVsS-gURru3qi`?h7GP1HkNJzl%qs>V-?aeU^?un;)tH(QYkNdus z2lSo^^lN8$~Lk{RTu7vhHT8+@`kedU|R9K?PJD6zpjP0Gm$DmTED)l7R`JRYqG5A z;^Ly?;o%@JPl>aWCXlCDx*DOTy|b5cE+;i>f~cRqIP3`3DKb8IA;+R>YGx)aucISx zY3btPn`u_C#;jL%wwf#CvG<1CHJPAB4E1t@I%D4%frMfpj%QwB)g6wR`}gd^rxBij z<8KLdaBu)tJcK@~>SQrtt9q*rlGkJ1MBjR>E<^afKV6?N2Jmxz8M5_9wObhF37)6@ zDN~hpONuJbdy)m6raj7G?sIRrFSh-K_d`)pQDJ9uq@-{rts3W@P|tk;wvI@;pu0?( zAD=f?b9zd$+SC}F>_R=}62{A{-bki~e7f0alH@QYU1Fk;B2=52s*P4zm~Yt@vcV8t zs9tJIwPY||SEitI(*9W2*-96dxzR1WT*}E9VFo{xKY&%5>nZe5_BR7xqbB<9sp!ps zs7OYam5c;vmGq!WYsFd>q>YO^l!V*z^wTR2s*Fhb$dEC=P}+>Z)Ufh+=EsD}G8Bqy zCoMfp?q@r4^l$F)t*u|b-23+9*zt2fUy_rjfE5DYoLYKOeGR-b}Hp=aotxjgKyrBg@ z0st81jT<-af^`MXg4e2q&sP+%O2#tN(&nPItcW}p>o3;oJ32e-&o`i0w{J7*RM}70 zd0=2*JO*2ZjGF8(e{(oT6+V6>EiK*qVYI)0FKNqtsrNA{$wu>05A4ED_?w4^$2sS) zwiw;{Xpd)^NzVgN1$?tw7;(~*;`}|}v5o?H^G5kJv(GksX$d$R%L*}Q1FUUxh@4`tEMH3@mZS20rsMY!8{GIdE|p>63!bzO!$SoCp8^CU zT?@kHri#iQVn!NgeL-NNL5}nIL38R55;a9l$DtLIbmf&-p78PMv&_^_789|VnWuhZ z9XWpSW7B7+!-;#`F(vxvr*?N4KQIP`X6Hk#EY9{)5bmvK%DLb17pSNZTWflx_wT#g zMb}N$t<@ctuI>i1Ivl7j;NK!(zP)JwBd)vPr9JhCj*fnkz-1_`r0M`fRsL}(q|Dt< z1P@et_J;Dc)b_W|1`9nM7*(r)H5243d@!I4I3TsU-KbRk1KPzx#K!bRzB-bG;!uGO zf6h9vG0+y&p6rCe1SnO2+c$`d(Y9?b+V7dFb!(das5by$jT6vZ&F>>=SZ!r)UcH&C zcTbLzL)GCv+`oL%vB%@6JDLB8NwqHZ8>2RL;vW1wuqAinteG>yaL~wC>`#?b>Q=R;PeO8XpeaPh+h)b1>)s_j7f0p=ymIk=et!4rjs&Mk z?&5y0w1PmI4yNG`?z=WC%hbCyVWEYBGVYwo+lu3tLZnrR}y?tpOXZ0`E4 zyI)g;VL?AAMH3w!pn4{VxC=)6X zT*p8a3~b+<6O}EYX}0$!f#?*7qqj(5J3)GkVDM3>eE(Sw;;bh%`J@rc^J#D!hbG`J zf;4&Kfzi6fkhj!l7vO#kRxiZFYKLERaGH)W{h{N$TKQ-l~V*#>-Tt0#g&6 zrd-h81JO>w2Ywi^eLRS~sDp26RoR>EKr}Q)-*8W4tbYso-~mXE+ue7i!HT=4`zb? z0HzaHL130N(fadEPnK?3aIO7No_3TFbD zKmufjdGm&MsHBeKw}(Z;!mEo?t0Jl+9~Vngo6iO~Q~K%YQ4?qJmFv&)*J7BfzaWlZ zX<6x7?_@lq<;bZ*0*THia8I3m?o(50_kk^5z_;`&Z5NIZYxT@kyYymd*Rx`ARBpd7 zcflCB6F6N2X{#zV$It<5|J`2ix?%%p*}^~Yrlmy44n_&w;0_``gt$67?n86^mf+^Y6q$RydNRl#oD zvixK`5B@cgcW+G({vxn3luI@Nsr7l(Ky(jp`PNE3l8Cu%C!!j*GjI#sNj3jvQYZt3 zA>=Vj2CGnXvreTh1R9n8%TyIL-^@ePMiQ;)IDOywyt& zt0QHr^)Ey&?wHx)z8E-4;!L%HBM)CZXLs&i8G=$YN}u+dWj#+C#Pq6j!tUD=TAG=i0PcqfG48hLe%_jSV#05>Z6VFFj3UEf z%F-K*LwHY!n1AOEujphDr0dwXr%n6q+#0AQew$pFk6w9Xr zr@+RtFFw5Sqo^vd>&9sMkW=95geXXlEK*-VgS;h*&d$zRaxHrMR-KXSEn~FV>CiVh zz72nipPYzu%5Dck;j!I5$a>b7;BJo|kn*-Phy@XV51r6;fIL9QX_8X_uHbnFkr;en z9^=79hN2C^WjRSzz{j=lJcC|)1)`)3KJ&j72F268h`VDPgEK`qeV-CUDt{dXG{;s1 zRV(r_J;&=oM)J=s!SNeX&2?mS{nEp>k*4N!xPqJ~%}H5pb_=bF5bLw#zjm0n_$|@60EA zmb+P6^M1`c)A<+HIk_bzd)YL}EEp7ev~nrDC8@=>pE4+dRb_lFDZD}CX5&S6-OF;f zxdw*KQMs;X1d6gO21`5)zw<<^-h*K-9h-_GA__E9$$aP`gH0xoN}ruW9O!cv)8gKU zS@-2fbTL&g0HiAyjp@=6e*U#8st3XsbHZm3a-aJ9OQ&mvq;)fkfeZya2=7~(b{})F z9@0NY!9(t8jCC<-UTuY&jj1s|*|21nBLktl`EGJdL6p|JAUK2rvL|7KA2~sZG`%Do za;+BcuYg+oze=zwz1BXcCVlxF^t{FV$@zCDv}+80dCMPD2tT7y>sSxnjJYlCNHbb} zh*h3&hs;Y-Nl;UOENm-=3PV}g<_MQy;yB29aQj1n`?;Ry$&y0iV5OB=#ci}}Nh0D% zL85LCJWFsdDjtuWIb)_b$@e=s+FOj5sSbY97ShvGbHdA@@Uf;|I-m(lMH?P`sK{rs zICD{^(Ny*V-}@FF5;?gKp~iiG$ju|`=C_YG+XAqObH2RJrSK82vywoA;JJtY+&C48 z2nn5cS_sbP*n0#@D)`2ic00j{%MjLPPEz#CWir_Iz-@gcD~0)ji}=mOOmqTO0|h)C zM27kx{cFT*;Ihviv#v)^4^l%rwG*T3d?fhHKf(W~lR{bM+ry8Gb@hBJj}USPpGFsO zTlwRfj94Mxsw~=*2}qB6A~}Mm5o1awD_WMLw2z9c+hZktK1N;x4GAAt6U89hPL7G5 z+zlC#Z@PZ#^K%m_>W)MhVl4d@vyR;sc=+O^+=`jg9;Q_$=)B<%MjSM~$x5`O$q)yD zsF9GiZNdAz+Zi|8x?=XFTF5A=r07Debne`EUhDBh0jrk-996)FSnvM$9;v4BOW-#* zQxtN_DXt9ufuZt}v!8t7+@6%c+m9M$78<=s`)_5Ld6&!m^xfCArmId@a|>&BI{==6 z0NL>LtR8WMpq5MgWD6?GBFh2YuD)BMzk6LRuXYQwkr!m)u?w}kI5+osN+)@`FOC|( zOpeW094*nbOLo(CS>|ytDe0 zyOx%4J)PYnFf-B1bP#BJ{j9V8bhd5tl)ob_wN-?IJ(3)S%&>rT-+I!8`^<1vf)sWN z*PBF)p(DzlaGlH_q|`6Ys;YqJk{Lq+aqu0}=Mfv;g>vHbLYS}Kgruh`$+U*{k!g)g zcGz9l%u)0SmKE9_Hc7tt7H7f$RUqu8poIiuqX&ooWN+JE`jdK&2%DOk(zc?KbQ=Mg zDgua=?EB>b4i6qe!XLOqqmXSx?rPtKAGKtPk88xEIC2;P>pJ* z$y_7$#gVvhqQ_J$SQh29R7N2I9Iw2&4aQ?oLSM3fg>rz&x*857A96mGKuJC=+{&yT zKf|33C|P4ZHr)bAVQxQ@k&!`*dY>e#YpQYhl~#{Hw{SM31#3(9RCIyPWa&moUo%Y! z4x1D--eKh&d^Wd+7^-tNm4MkIl9_shS@!~AubS7Ow{gfaKW9>&rI8fyJYXooqLHM_ zKvxwQrt-0ObbKJOf<{*BSc}qLQhCa*;5JBh?^)jIVNAWP`vs=t==6zuGV9&tKiKJT zQ#KdVSKk{N8uogGw@6bQh5!b0zspRn-+j9g^=NAv25?)L!k54OL}y|F%zcF6!{4wd#>uZmm?kfc6Y%7%~>fDfj4^e&QkDCUGk6-+n03X0rppPso`*dGoWx`P@ zS5G3)Tf2OnUF~8qd0O7m;U*0ngzkKqOk*0l-+a!P& z!>lvb+dE4N+kQnVpi*qj<9>cH3@4xPV6HpaXqn@a9d;W+(=OOLTkB>uchVhfn>3k@qIi7JX(12piA}5KGmtqnWP4tx3C1 zrgHZaF%u+^x_@Wsae!~N8pcNFaU4L)V*!*b8bjra!5sE(o+bw!&`722$_zW~e7Q~7 z<-U~%r6V=!*e*5b+RP~P@%Lj$?ab^Hn?cct_jMDy zZch1efD;ZoeSi^nkieEuwrgRv-X^r0YMt!1UX;lS2{|Z4h9E2{652=7G{UC(#~FNh zg1-v{Iw-#M`~l4|&0wm38qZ_W(6$~v`}sECTLc$#qzJ+Sd1&pyGXd|%CVLe-mz+JU z#-nG-BpGI-66G|q_Hs8VZEUG{W(Rp}<^Xi;zw}u6v^Pr_-jXgjPriKu@D+adtauEV zTSwlMpO4_{J=4TqDFQqk3O${WXUFtdJnl&e1`&G_p9W{3yUzA-HURvtDQwliC16{B242ygy~q z%B%_zZO5phqJk}oMh-9~uU@^P=&5+Ikb}ItQ^7?3w(&*!52p)bJv}`QwA(jHv}t`G z%>(f3uLedg%O(J|{zeL&V!t=k^V9JMkC~UKlB!}t7VmFC)=i0dSf@2`tp(rFdIo~GIbBykAFR@RY9yEZ5cJ`Sm`M@B{l z*9#bCWB7_LJ#c2=DZSsFFFL9}DGfruR+br}dW+;y@X+vZip!z5`}=n}t6Q+6>g3*a zt@z2B{J9HM{5MZd7eb*Yowe0m7H9xNzSeN+=eOH|?(2$9+E*?wgbX3hj*pneNza-Djdm)qzmFhp045J4 z%(qUwBe#$01$jQJW5KG9rq1CA0;G;>W z6ok##Co}Fz`~Gbd#p|s;VO{=tt~g#~%O#KQCuHw@G1yO!cyhX4I*f^VTRgSb|Kj3E z%c{zS@~Cv9pt|~j$MbFg*9r+wfVyD}Xklq>>!frUHHwj@l<(KBG41s@nsRF1UmGq& zoI_y*n7%}FjWJR`s+$dp{lmj*6QQ2K8f)q@ZmQgAy@z^3cRKNW9bi7nVes-^-8CTi zA}tt~6q*&92-sY1Hf1FcrNu)(F>!Tsi(%3_1X8W}n!WA>LAS%p^!?NDYSl#x*wbdT z*ho#=rUeZg-nnqe6yFv0T#?n|tKwHuQ*)Yh7{(U{rcn>>hKD8sRYAS8?*u>r?V!B# znpb9K<|RV})EQATR9J79Gd@JQ?zw#0G4o~${`mfVAhwON_0$n+!s@~57ZxF7Y|T^ zi|$!f=sd&sV42g_)b|tRC%g;EZkusDHs7TRLVAlf^*B<) z?_fUmJg=T+;(@P=vk?V|KX9IT8LH=aBK*L`{vB*)#_3MC8>#0*4}{np48E4KIl9x5 zTgaudp$61<0%*GU4l`sW@KkFL+Js+Nn0~ap2RVDQRscuTohJd@5obdCX)*|hETv~_ zv=&bD!;K$gTWXaP`R$~ooK;k1ww7&(1pO?B-zRBVvF+!$tN?SVs3GsXmZc_|KC*K74LZI{j&xq0GlM3MC;_^IS)B8ar)*~)@ z>3a(b3TCZtr@$wcFd5Bxi90$v0KWmn69%^Api62SD}^db#$z`kC6Uv72q<>CwUIdC zMSaN0(L3o-OLkq&7NhI^1ivAc8HxQ~Y$rJ+K75m&7_cOGXG;C~8>rCkH?gV$=y=(A zr{1yXUWE-{l}vO@2Lg_L>k-VA^EjVO!1H#NDHkIOo;$)l!UyxsMRONNKvL4+OM14Z zhgpt^N%DODAeqCg4>4)iGYk}6H^$2(9?rUL*1@-cc{)y1oSyPEu?Y68t*@I}nr&=t z9cioeebC`PCm$m0hmvoPqI3zarsk7i%nC19m z8y#WiC9S@R2?5x}+2y+dz~a3ZMX+xD!1H*vtn9r`JUs)0@JT!54VLTWo&^*`fg%qL zNIlvi_LN|n>Tq+i+N39W7@s_Y04UjW*PnC&!O7gyN=>$p3=9XzcgF8{(Y){A8M5IF zWZF#YUDl~b)GIHh*tQ|2it1~Vu#3&xfGDF*Q%w&MRp{XW81y+ZG6R=Y1w6n%<*aSa zXHUW7d&idNmS47V)-@681Ziqq?;!Wt8B>+S9TmPLnGD*DG~4x9L6bvdtc#7sdr;TK z>?}gasGRZSSgHAN-cEV!W7I>VBp#RDBWTSIGJ)sE!a{JATqfI!Js;D2qinXt_>&B! z;5~O{{=Q-AVOi=3$Hs2MJ__vwDj%t={%n*t z(KeTtX_>K(PNt6P)RdL&2GkM(*Ef)@%xyKXEG%}kKFW^6`hrkWRTaNgU55AV=qk5x z9{lw54ANO_P^4~TwBU0S*BB@fyL;=5*wS)atuQcD#4jWgwZ9hJFeyXY~g$6=hw(fU;XV6UR8PE^kVVyQIkdu?M3Al&L7ffImC(tL`O{5pr$9H)z0V)Lw=ZtCt92;pdo|FmFuub4i zmw_yy!EZdN`QNsVrxEQ-wrCK#1=9@QLPt+ecu#ShoQ8nzGtIjJlHZMNPN^5H8Ji7? z3OzQ=3H0{gCsi`0b(?h(hBM5x@O$5;eu=T6bG)Z^?t(Ykz z_|``cDg`n0(Q*84dlq;p;Zr{jDP3It&{W3WdRV$)K3c+`)V@AZBsTmeK0Y1@DFY&C zYF#!4`MuVvM&+o(rFcSQ(U^l`VxppMl+6B-)FurL4FOlby3NXpd0Frrrh{%S@&24d^2sK?Q5Eu7wPrFe$M5* z&SZoS0x6OU$37KPL^*aSrGCWf#Y;9#>63k9LeTnwbeLGZq?F9fSTBQz+zc7wO{Mp* zNpm#o&|Mc*ks%j(%l%sLrZ_T6gElA(jl2bg4h3y}v`^1E1e=zx!q-5IQdCNYcId8h z*fo?rm~dXL)HN@>cgwG7XAI{*;;%z{nR1YlF>olQ$k0EF%iR1*7kvXQHZna{k+q={ zd2l+_#Ddw)k_;`dAhiys^tun7HtZTYLmQv-wI6SGZC_*_b3XWb{d!H%*=4N&;8lW1 z=XQ<(PMesx?d*lWHaFT`q87|xG;+BUKmV<`-A0D0vRZ0ii)0wJ*F3W%3U;&-8F>Ri zg^KTFr|!1xsEvtd*o#0lkT{4u1ltT|DX-B#@>C<+1o@eWEg8D?Vril7GeJn%gR*o` zAxQkX-1Q?CyY$re_DNV#O!#k_%Xs;0;1_FiJU@%~T-&YsmGG81(ing|V#2yZ8_0d+{%$qq7NM+O`eo zF^Se#EJjL7bY3|)pqs6lYIW+c@ArMuVnu`o2kRE84% z?So&sOw|p7Ia6Fu>q^%;Xyi&{D7=wN9O($y9x0~kmSd6_@uN#Z!s6)gpjXoEp3ozz} zmQ=fjCeN)}2${f8cBtFf1&>*grMgxOY8|R$UQ9b};5DXX?6l zm-9hS-gGk?ug}x>`I97a*kYX)6CLcloVsI(Go|#W<%KZo(HHrt0<|{my$96S36F7Z zY;s2?R~F5?3Z>;fkkvyPQn@mV{J4RMA}g0pJ1ofaD0#?h6mHI^%UjtgTsED;7i0BC zmV*_3n+FWI)#Z{5^r&Uf5BJAEne;;7Ksm>`^&~E&>`)aP^{nlP*`o?RXf#{=6u#Sn z%+JNy!>g()1_u{it2*wP^AFb+!v^1W*vP}9g{LbQsTf^q>M(Dm({C1-Ai`ga z3oT68!OJbDE}G97V`#G(N2~2wzcU+ehR~3Haa<2j1t_u&%RyY4HhS~hxX-2zN^&v- z15A>(GT8W2Tt+sxf>oPQ9j$sXT1eJqHzxfg76;v6Q!M8`&6|@^@`FrZp@fl=N?VJr zLmI?TwM`sRpzn7Zo9Di~*RXNe9%v7Fb$<{Lnp+Yimn_k8+B`ql>gQL zqMWWOhxgJL>=`>v*uh@R4SgAq4p4h0VJyD|v!e6*x;3I>I_ZxsldpX*!3}4K1GMbA z{eHf~p}DwkjXO(k{Db)CIE;k7>ci&}MD8{kR?vj)n*Az_IC-rAGed5x(pD~g`HhOD zi@uVcWlux$w_%#)Q=99Q@k5SF2x*_ZOi2S`(6`+=L}UEW`B9sULQ*xNuD(cIDZp>s z^T3VH=b1*)meUc^wb{{Zhc#IR{*0~cmM2bnrhUgc(}+87ByZfU{lLw+FLbK1LX@?R zIQPi5bSE$MR6bkp?_H(cq|*kbrx-4eO{Y!s@l| z@_bViT6-OMW8AU2vSz6Q^wi_?{KaIoo0HWu(W!<_lEH{J`>l~yjYpj7N72KdITt3x z=yBhkx9Zk)X4`swYgT#g7DH3KYS~(d_&B}K;5-IUQ!Diju&m%wPo;@WiQ1CzG&)>lstN-c>^GXRfp!zSVPW{t?^Yz}pR`?Czo6#rV3( z>NVpoYP`b&&9S;oqZiA)tHmMmg+tx3QDf7i#or}RYQp3eBVNsR^ohGw8%LS;9#^pU z*w~>V(VG{HpM3v*HuLd>MlB@BP!Vq1DUPA*d31$?!SzmK&E%jJ^?$upB>Xg-`rd!G3%h9Ga$3H3L?v`-=e_4@d4#2h*x{b_NtggsGaEvlG9F zbiR*mQ0e|+K@$fP$qP?3|HHc5CKc7QRSasMxe5>D*Vv6CGjEOh_G0Ip*4=m`G$Q_!({Oupur@pm zyj|JUGvPRHnQ0_A%{0FE97lvDkTgt0Rmx$V*8D}H{iep(?rVmJ`5sgBLG&-El$gXJ z;kz<2io=T=Rzj$Z8CoxL%wf~r`I4U=YVuEY8l!3+%Fgn(`$RB3_i?QM#Hg*RCbj=8 zBqecU15M4(&cm%~;qF@=l?M)N?IFX@Vz@(bA3Uez=PnROVVivQKF}sH_kG{#=NXYI zh6LGt-wuZKn6-&dZz>Cn<6L>h>`zf2;=l1z?GVIahJHD49B)UV7C}p|=4VfGD-+!N zmTuR>Et{I7!FTahmNug@`N^g&g_0us#y}3;9y;=?S8mmJDtryuCI_>Y^#!$)Ew-%f zp5zrsa*ArtNU4%sv)*urR~ebOALJV6F%I&2`S#a2y0~j68EZzfT1#1-hw+D5#H zrS-Kq(w%*nL&l%9K3Y~}yeh|pmqu)jW#;kL5 zu%=D|R~!{5VRm>9IyhA39*s2?rS~=e$XhlTb$oYpo_YxQf2)QPMFnFp>lFCN<<~Gy z_dQ!)&N=&bQb|&sT|glpRWQEaRd|i69p4-n$0YzqP^nOPt?_0Iyi{>n(~S>%A}Yvp(&QoVNmIsLS{g= zz7BU=s6!Rsz;4Ayl4f=1)A{|@d~wy6*mOR+T1>3i&9XfLuS_*Px9XPLODH8Z&G3{{ zqht7!CVS)hy$e772$hdGm44)E<7Ji%$tM6U>Isw$%9myflE^4oZWa)>gwixRK4QxL z_@eD5&Q0#%{_nVA2VL(^cb;wBoNc{HIre-|{#$Nqgtc_be!rBV+y~DZ9CMbO??pDs zU1K}5e$@UH`FZU*xl11?`ve|=p|nKY<2gyM^XUy+gI9~5Z8#HWV}_dHa8yChT{V>y zIoCdU2i*!8=;+wrEs|6%(PeGTF}uzEr0=_pzqzqdiK-y$1dr;AXD0(L#)Eii#nB9z znq$~6V&AC+E6TJsx612(3DZn}{z_3*fucxij-$4CG%v|pjGE2BxJx`E{p*KB?3_Z} z!1rpwH#tLWcok(6^2UfIWegx$nxzpU+y-+dW;;8^8|s`>hR;In1X>)$EhZ>S^LYC> z;>0_uwGOkC<68Tf>618ul)dufaR(3|I<_S-3W}LXeLcfBV?5Y{1_`_1;jSA~IyeHP~tL~+rWC~4L7}g^Ds%nCe z-i>)-4Wu$8Jgs#}eB4`I?Xzo=v2<9B;KU0gOQwOIJK6V2#rla^A;?mFKIHTdZ(&eAEXg$CvUp$g zx>r4Ae2|`m-~O{mIl7r8E4(U6C)=s?$avQf+R$lijQ1RU-Rjk z_hWK($WS6iZq0~{rJrE_kU`a0u72tH8sPCj4fGhGYr&qu;tzq0F%jU1WG6pbVdtf3 znJ`ghO38$2<6O77A$}dDK~RLy&oBrj)q94Fm+ZDT@}P*!3?}w<5R%i&7eFTp3(_Vg z)gE#g1&R{rS%b96?m1JDA8tD3g(r^+YNKC)Hwzk|r@c{c4-@V6HSbs3eg(^g?nk%8 zQ9vTjI3N}Yainf9Hynt#3ifRj)SPQ}FLTI{Dv-sm)A`HH-NwGw=(kG7^ZXjwqia&k zNaUg~UEb2au6&Yd{xw7hEDL*Fn!Q$*zebErJfKd!d~# zHEnKy$XcF$LV_g;N

B&}UTZ8=t4>rdA8gBXiBY@MCRXje_il}%_O2>wl+tSd z`TV}G_r-s%ofmS;c^*0PJf7omf292{NsxQ=QqgPt|HSwfqSsFKI-O|>|A*KBfy=U3 zm&^ak+KP=Gd%?%-Ti8gCx{kZ3^TSMO^|Bo1Io$V{$!DM=`LN|Z+=Ev~agu_t~ z2UDLhf2;n~O7)eEpxQYx93fXICYPe;&VQK5RkybJ6dmk)t6F{xBr9i0&VbeiwA}Jp zfhG*#!ef8r+9$2ND@0}XH9mp^JzDR9mxSSb|F$>9gKY7NI|xvZg!YG{=l?zBy*E&Y zQQu=dW1Wn8ccq{p@R=fktrM_3Wzqvur4*rHL$Bd=mLrPaqxGHBU!;o0ZUkl(5nXeP z2Df~&c4%PB?D8z=GNP*x1Z6hki6s)delh;18d`a6+@F?+?p+;8Qfla`fr6B75y0 z#f6yFrd6j#0+_j-)y?}h%L{)=+T1Mum&aNEltMyL9p?E^jSX@2A(-KUw_v(%NxsO7 z^A#%Hph&A+ojErq*C+;@>!&^@h+A!d`~-=_V69W)qQFv*ZtH-CwtOx^xX|C5pGx%p z2Wl-3*d+xgl+2I)prG0^8hI=9{_j{c!|UHQFQ4q8f7e{kCp&vvKno@32?KKjLF5Pk z)azgRayaHa=0%szPZ#FOV>zC37LZ zfV%J0L=!w9%m_(yA4F;c^@D6X?6DdP%PK?4laLrmO%S*r3d4Z|=$5RkFy=-GxEW}! z<-GRJdtrOODmpM?*DoF14W)|={BOSnr}bPwXyF8IoylaZ0dN?0oEY2pyWw!l?5Ae# z%E3d)xsmC}himBqyh~YMR{Apx=$($0?UCj-3F|_I>Il9L!M2*)f>ok!Mtp?=M=aNw zD`ohwf6G@DjN5z0 zv1fCZX%^1X3sM43_=H}j;s_}UL9-J_WLVyCKmFl1ArOAq@Gqdyl z9AiUxjk9TBIwb%Ymd;``tSMIj%mTXpEa zkNZMY_F1>C_fMxLekZ?a5gRr6NF!`Bt1R36MK_#T+|Kpp%gS~~!%TKc0uX1in$=_V zN?HrpKjxGimsasP`_Y4kysRn_XXe#01;ql6v0yzZa~yH>I(vUz@oWqKLfZnJ8Btpi z|2F|Rm&2*ARk^^&8o0Q5-K76BiScT8Fi+unEiWsBIw)j`*7op4Yp4VKse-GnA?WXfQR*Yf!K zKSVdsNfF^(gc$dp+1wZo-Q@NLH5gOii&{7hv|BaL-6Ks!oA_vxDU*@M39eQ?ViE{Y zBOvBJ5Rll!rIm_oRi8hyY>?%BViK!jE#U))g4w*ydHBL@z(5dCpbGf`{L-WTNr?-D z55P@<;EB){C`Pu{CK|+Omun6^E;gn~ck3oW3nHCTow+3DQ-b3q5291K z!N`kkNkXRY1lpyoty8l8MKQ9tpcL)ORv(?yscChOTl{ZiWf)y@W zJBbdvqg8ZbOcC_k3X}uVWnc{ye$GpzfDdQ-;DJ;>BF9K#_wnwyVeG0jktn>g>~~g0 zo4TobX7oGM2h5pYN%!jC#4ElHLK25SvAyKTjkjNa^rEmyYSdnxhN?bb>T&u4#ji1* z{0m@mElRi8KlDB_X2EpjB`zFj(QB{YJo^+^34dNEwDO|^yuO;)Q}c_KEpL{;+4^HR zMIS{D#UEGCy?B{3@s&kIxPQu$*J9ogK8onM$%ecU-97!Thyv+rb>NOU6h(fKURIC#Ga>r@sGRyewkL z190QKtPF#~!tG99%uQ2=gAqV-o)S%=9Ny6Z#Q@HM!?g1ey@o>^67-4m&UgQNrywLx zW0P!o&qJxxI7Om%^{OqDJ+7(Xzgr|bq zF^A{cbxbC2m_Psc$|04`-TqbKNl@*><#L7ztobl$^;Gfg24X2w{wk?I7(l0c9R@us zg?|;F^l?>Xo(EpN%z=`!Jc&D`Cz7M*{nbwo`n6+I`^k4DT;|oq{->*N`$8vCW6Ea~ zo&!rXq%$*6kR}3=w`}s9&u;qPuo{TOG4iuSqnr8%j~W<>B_)`E-?c_UwG9n)uw~vy zA%P92r>FFcjO}~Z++gY?#8R0n1$D5}aW7+xs|tCTiuqnl7mrF#Ng1%K3LW;0={aAx zf9c5gE~Hqd=k=ZD4F74nJqyz>RU)k$rOf1g*Gawn7qFC(rV2M`{{oMuWPiQK`RVEO z%nS^dPhmFOxaSoOL0PI&jBKvJ7^cH(|CZj@*MByD!J<0j_m*W=tP#&7!Vu}BOOyJQ zxUL{)g8)$xGQGzsXZ+O6zzA#;W&y6bo8W2I@RE-bF?iBCK0ZD*4`8M!$xk6Cfj(3MDoWFn0JCpQT+I+|2U*Gv~uC!ox zlm71~77Ayq>;3vG>J!XgBg)|+3d#3yVEK6ggZ?$hCDkQT*wqZr2ZC*qUE!h;jXOHg zog(g*?e?KOYITEUm=j_*(l{NAq(rlj^;6&oP*$As#7R#*D)4mZW zDLJB$nYGo0&x7);;B|FixBhu$AZ93R?_pIs=EO0gcy+N-Fc%`-53t2nZ35vGjjIS2 zEalGM7w0b;AlavdE|)PTQ_g$I=JJR8*>XEck=5W0RS7S#5@gdJ4V=t&(=njIPfkCT z+jq!*l9FaEP{s~kGTt^K=U$a2RDM(byccc}2m~jv*kYNz#>vpws#VU?m>;PW>JnZD zbS^*8>6gzn;B}%SC(51beIZln4!>&x&)~kg^z_h|U=|oc(pQ;A6?B^~_)OrUvC4nG z{V$d}Q%%Rg)WqxjV1F;5Rx0evk+hRR*;WNL^kov1l>$o>E!hu1AajkjgFwu9el#Z; zpb(PDQ2IpY)1--`VNn9<#+l_4IXRQee)4znBkVp%-{*~YR@Xmg2+|h6U8lg_p4kVb zW~TXnx(ZG4wpf3Goxy9|<4rr5EI}c$#j7`!D@W(DgL_t|zpgO(H3+HrVV$*kE-eto zV0|d~MVp6pLh&QVmB^yMr%WZ^l5A$HRK}s`tUV<7<<)aW_Jr4ZJ`&O`11paprrxu#A`Fab+-#A6#;>3@h!=ohu;GCOa3)qe9rH& z4n8m}eYQ^?<|j3jzd|cO=JT>V^lyjt)t{6Os~RVX`G%`;>*b@2V6JaNUVy-@s+JeO zTc~4fGok2;9Wt}#&!T_6vH$IOrWw$7JX^PL<^o|j{fWt3sOJp_^q0G3V>Z`{RLVer zmJSl<*mxB0@3E9&UTM+fF(P8v+T5l;^|Rh=qP5Yvz!3@!-^g^10rizh9o7k}eq0NMn%$ES=7Q*0Wg#52xDjj=++H3iV zL#+_3H<@zH*RBqa{o(utxxZIcDIU>(jQ4`F_Po0??eA#Gwy%FmdDh^>yuOfJ5wx{M zQn8~){WHm3+H9WwKVO#Bo7;oQ#xM%B96gQgBa{_W9k-R!Wj|Roe_tf3;4yKjzhmX|sb9k|NiWOX z*}8^A|IXk~cKW~E3i7V;vN?9yaes1TKcd-aS5Yzfr|{5f9}z zR>c2(94m(31+`sUfT*4@PUy36IS zHh4*rkfwEKGrlEs*LQDe>fUJ=NMa$rZfrx5 zE#vG=?sj8{RFJK)*504A4gU9tyHli>LVIq&MDu=rd-7?m6Qt5@wqNDB;CS{zo8wki zNTKU!{^GC`%Aaxg;9=d1S@_QLuhYA%se55b8gIrGo$#=-RriUJ%AeW6KZ|bYwNwS& zLpJ=|xb-^N3Vq!8af<2Qle_sse^Eq zk4)S$>f4%SEJJe_s?yRNem{9U6Ca+qqqp*FQI|YPUO%&WzeJE@=BV85{NP|HJK(5b z>f6Z8IPdWovjmh>KVU=s_xS9YTlU4JmbXvn((tEOFZzJ7T!3`dxxQ?@kxX7(lwb0h{-46Rb z!HKA~nT5yP9Pd6h?Om|X(pz54$6YU6ZV&dKgwwe>I{vF?drTtp+nyWU^Y4ewvi|KS z3-|tSH^1Bb|912L>8+GAG+o8-2KTRrDnDiQo%*I>_;fGv;)BXBzlWQS%nnUkP zld@HU+Y59iek$xB=shqU-`2FOAI^4BJ(?}s6^g6c5^w)d32MOu86hX zd<>NhAJyNcG!GErf&zuLo0bllHS{)pb8~Y`*VhEpZvgPp#Rn9I-^e= zKeqTW4}Mqaj-OxF$G!{c$8XlYiH1B|I;cwtC0ese+0MqgvRdFyr7^UF9U z<;%g#7xJI#T+rC(KkzlWdOXpSRp^;S-OmSKn*Z)PS$h+(s+VnJ%2c44H#hJ3595qP zpX7Fb+Zs8^e2UpG*z%aSv^!@VndSJDcFRga@l!+Sco`+S)NV1E;y zGhbY$zW-fbjz;H>`dSY3Bn46`iI+<)&Z|yp$HyW3vh;7j zKnt7pr$4{3{?toOnMhywv-j`!R=$aNInm7gc5sgFd!4NutBfb`=`kf^m!_@HE1xIY z01K^`6dp%Yucs@*ho{688|J@%`D15mW4QRf^0hGx%szTM|J~$>xXRIp`HJGvX`EhJ zmhQN!#ziSmy%^2|F+(XsC1Gx_!v86%Fq+!?w3x+3gMsmlQk-%VSzyB(gqyNdoMxn^$enHs#z zPvf{3?kBh|r>q%A>t~Oe9^6 zHV;+)X=2U6+uTqa@bDdkc!c=~*M ze&@0I-FXj#b}wX^|3bBxb4?pZkHpAgmwB>y+Ou`_{OKpP?}Mg9bW3lRyy{oC*;s#Y zRxmZ9yse|!bK|G@`28!1Xzpz26V>`>?KlBFNLN0G2 z-~~xsKTX5#@pnT{iXh8}=;cFJ1gJm;^x-D>2csHEGg{d2C&FW)xk9?1e>{A&(VvnslO?gyE0v`GV{q;Zs?>J4$+y2qD!0BVU5B5Gj(mpp=?il=-2lVt z{MLG6eQteoMOm$1h?Rr2r?vmdG!43x*INVo%-(G5-EbSw&+uMP-OmWnOYKaN~E1^<*^sAQAiQ!S?T3DpVhNoOk;60dZFn$JQjZ{7>`dF~nD>2ad2Z(`W8 z`qDVL!8P5?3}?s2LD}Dw>w08S)>P6Qu)-)W=kj+ndQrqkMJDy@{7k*4#P0O;>_Dk7 zhnpL6SU6*3Y`pzDu7so>8R2n&jR=?Al)rBWk$oGU@B8g~kr~dcUje|!LUXUy#L3A? z2tqpF#lH3H-=817Rqs75%30tv0dHEGhZ(gA`k< zThjG8MA#FQD-3Qbz4)ok^RWGS8qmd^C|+7dtQ4*+t0c}a;a-ajBfE6=!JIWlSch?SM3{6D;*d^GxcJXMx* zhuXd1d;UW~VWI@j7Du4E$U8YS%0;J@sH|S@?CY!VV5R)Xjwd`Sha?yFa64QDyEqfY zDvL|R#7iqnXIo95z%vv*>|B&dCR%v~3`bw2%D&F&QyFAG|Gv@)910b+Fwd;D=?BUT^8-hurTKJX0iUp z#_9{_Wh$nhni+Zm=qigGqrm;TS(2EXw}3*GSRpOQWHDok5>iO7VC7R00E5JssmLt4 z66Pyjfn@1#(vWfA9Jr$h-n+9BD%k(>Z8f9?&uxv``5 zOchrggJotJ^-VMKXL#v%Gma?i>mjCU#x~*0&y8fm7Jyl7N0kjhCEp^YHg!5J<4TF$ z%I4g;ZoUP`PoDF>z0-qWyr`0uQFzOh%vj$ z`QVuS-;YmCvMI0+66bmg%i8v$y&lhk@Ut?9^jRmB>|M6}=A#7#b`XZ_g$g{)GmWjOFj);G9w%V? zruX1;Le;wYLXKJ@*Va%D=-sO;7A=tfvu6T{ItmpzMJv^jyuByN{i&fJg3k|f!*`a5 zTxh*1nE~$*)?E^I#T3NBR(hTsk7P0nw@Z#U^~5uP2lbmW&J6}W<>yZyn2`f z&)+ss1R~Ow)0H(mH`4*n&=L14C|+T`#ZvMOcs+_3)TnV5_2p?`y8P_^k4O^UP9)|S zMM*}2kKa7;d}>uCVmV8iCuEFWMt1Uk4(0Pu78{D)cw8L%lGGMu(||*0*Svm7JfnuG z3nWU?_nfr0x8GgaU&}ts`O5TGn-BHqQTmu8Ll5XX$=~(Wjz?af(GUQUX3mlJ(3ep8u19vqklM+=jh*J~9OD}WZ3 z=5w;-g#lolSp&nv>qmi^l5&gHQ2-b|M$Q=LMVjc)lcP_QL!Ay0WFIAy1QFaQSn)(+(?K7Z58)kgglv&8OInJGI6FlTng5H%IS1xps zNS`Ll5BYHJ)qO|x(%qG{)z_YTbzsEO^JqQWK~?}MaEB$?eSJs?h_b_Bbt%-mqa<;oQbs0mujNgG=y&uLumNl}_yab>Z^b%suktn2*y0{WA#O@qRQx&Xka zFp7KpiV|?~d<7$&RXiPx-ed<)eoP6nS(m5Afz{(1r!vd;#yULiULJ1YYAGL zGZ5kg0^qoi=K{^`%-IHj;9%|ne+BkrhfAdbMmI*A4kbXove6Lv z4ekqeY8;~sHB2=uOp-Jou}$rhL&9Ixs}jOK?s-5^x#SBqcKEfLoeQ~W^W>F@YK{MX?l@HJdFy@)TYo!XRnKMrNTKEMmF8y5i9#BAP^@B z&?Ge=fn;Zvj*RqD2bcNqG)0u|!R%s(1Ig8-kJ&=`qIsMg z$Masd#~a01JdTb+GN6)~YfFFww%69TPXFuAz@NYzrIRck3@SlWJB zFt?X+av6J|27FPRz+JHKLl$W}*VIZR_MFQFU-gaFGYibS-+hbZ#b4c)UKVA%59|in zIB#mh`@#*#0ij7u3`BC?55F|Yjv11_%cs|^tpCYa(O5|(^|K3BHAHGUA~HDV&2y8g zJ32a=o4=gv1&+W$E0Rg5FZ1&>hpUB^P*#Fv>rmmnV=Atj|DSnp3xVrN9!VtL%4&C- z9rroqX_sgYk>~(7%~{--=R{}f!p&kv2}rnu*u=I6e%C)g~-__SS|dp|(%(Iz$**nt97K%{}6S{G=6GOIxaMhTQ)Al?v8Ac#vW*djpq zPN@>2h<2`J)_km>Dhp0Dk(eHX8{AIYkb?2|4OT0FvM!6r&lYCU@ARrwwPr>YEsw3a1WkUq@SFO@$=&BT(Hc zX(EkyI>LM5IS@R_XgkJod@DgW+K+9o5D7Z7jGqX$1mRRoqHN(%+W}_E3~x!l5g9dp z(7c@ZoSt?#SWYJQ1}#u%Zt+g<7Y)N#8$M)kIBLx4l{*?5x@!3 z*1bSjDrsPKjZ-0o{w0wH|}kPt+V1!to~M#RjTC#H23$HeS1=By8Z9_G$56bDCbijQ*_u3JTv` zQR?zYAo0ljcrMFq`WqRLp zLzx3H7~;^Lj(P@3gk z3lByjj1}Cdn0#B}sWvv=!DuWWV6)R267H+Ug6*a<6Q#@-+O){&Y~rWsXHG;VV!;=p zBE^;#s2l}%V>|FFPlf{{8xJ2FrPl2jKcJqy)TkK_q^sW*PXKa|r4-d<<B82BQTSERS4_-50V)&E#?yyhCFaJb4*2)Fs&nxdaDH1f4Dl%PRn*qJ*71tb7l$ z)zRo;PPBUoWSuYD5klY!kluXx+;74}q!hP>5I|?zSc_>-w7>wT*`Uy*8B0B=SpAOj zyR9q%K5n(HoDV>VcZSo+s2RWJ;K!)zxvKFg>=&&&E-bwEuBJ?^s&gYXnPaJp&~Va9 znF7KS?|tON-+oCHAOk2(&{B@;bAw*#BlKx4zhJTg8YTGq_}rQ8ggQ0u4!B2*fQ_&{ zg;;-}-sbUQ4k9ui;oXZ;MPS0+katvUp&48Mem)l$3NSJ3fn3 z6oAiWc8?$+JI0Y6#VIgQLMAr@BH>t{QlmHv;FU1Vy$;E5DCvP;m(QsXevxXsxKRDb zKQVPKi=aw|w*P8sUP9nG1TlG+@TjYrz?qdxI(u~zgFlG zqIa`&nzllrahXwWu_+Iq@@^1TBnhQ0R6C1_cA7 z8%gf?0*Yo$xe9X60KBl6NCQ0~`m@@4{~ckDe)B#(;%a+ai&dVjkIr>=6lf@wFA0nX zb^Si~I1KxehAvg4S2PAA3v6CEkaDk!<^2q5TD=+Gv5kIBdg1&eOCSsi>uSQm`ULX% za6s*CZ*@@DT=tkMNV2~wmqmQ$b9HG*w+@4`88>!ul;qWt8TJi8u=k>2Z0vp*ld>4BNhu$Yz2`gxirI7VBNFtV<2 zjSCH8MS{ZY@i+5~e#$nBw_j;^hTD-O5axYCh)%ll;NQg3JS&y?Z;i`uSJAHJGnHlU zw>F&gOI8}(9vgm<4RqyRgOCuXwgcRD%zkCL4#MvYPa-*5zO*E6=_mTN%svoFC*}e= zrLQJBe%PJp6!k@acV7Cml7XLzHIw#e3Ht6LZ760|O){|GH{22&O*e6fmK}NU!+C7c zh4ry~#}_lB?a?g9^SZYBf;!?Zjs}gCuY8o;`3WPae>m4(?*QH&RZ-06yxc=!9ljAA zni&n89(KLuE+OVRYa5%bLjr|&nz(@PiP#=n4Pzsi;H2W3B!#L!n-(u^jzTtizST>G zBI?nd=tww1A;mGR^;AJqRg_$-hc--45L3aO)(KksaK9TyDKYHSYX5*muZaLljJ&O( zop$}yryi1EqsMcNA&r)uN2T*PHYowDmkaBYq1lmAS>19cs0zU(btUP)k>Ucno}8r^ zVZ>MF%;D{xHI9jWrOM?T8$(C(bMo$ZlaA%JjSv>f-bl0cz^g+LMq(qHCz1 z0s<7xNmBw#ln|J5aa2NSBWS8cT{&pcQtwV_Nm+t0Gy)M*!8*H^#u-4;Ai%rU2)Qo3 z-hl!G?vbh}eNz0&6`HHZ2n_t$E~cec$>0p9uQ5x5mP&Mr-IJ$&cx}dXeNOo;b97mhglFS5XSbC@Z)0<- zU2*$lK!u>s^@^_vc4frgX*f*5>7-Qz2k2DipOcBA&~|ET6@#*x_H{~!-G*9HTZDg= z`Qh_Eq96$$Z28`lkyDH$8-}SBMH*1PM}B zP^~ybzpD^SDCWDyqn)47gnec9HV9j+DjNYtLaAFw-)+S%N0)^;O^!_VSdmYys3Sdn zp=5tli(*;onvvXFW~4eZrNGj9fj}oRJ zJGLB(eCDTyvq4rRMPWKA6Y8BJWcc`D7Qpqxcp|~ z9A`w&1K7;MyizK1fuA}3+uoN2MVnsYo`~Ab+rg8uqLnvZg2-Yw2|zVy-rv|OnMcf$ zQHR1W!H2g-9~h=b1T`FdsSzohZ0#Wxuo}6gO1sl?Z%o?$UM(rQTFkJ;$9{fLLF(&$ zbU&{@oL>?A@W9F>d7}EVzqx%WxSQ?BLZ>8hG5O{~uyk2F^#bbx*42jJ%2YSbbWTQ%)~g!?M18HxFSiJ{Nm%V_P^F!>wlqr#b89kaM1*9 zrB(YRF+;MhOYf}ysrz2^XZ&qu5>MO^q}p%J@%yZPyWp-ZinU<++yOOKy<3B*tay}T zYH%-S*ll;TF>T!Z{FyJI*~MFlL9;JE8g0=WuvgKh_r?IaQZ}{m0hj~zQTo6CWH}@o z5lT>!tKj4;epL1Jd@=1O&7-LfH;sEGIFHiLl1{V3JA2#KrbRxU0Sk2El{fBf_3n>k zKb}oac>0A+xyb?UktHvgf9}WhWo>Rxd=Tq|N=rYU9Mfe-^khoL6Nr8DpZV#my{_rW zC6{|HiQEH7n?slG1$PRkN&`;ic7MoEq}{W8J^WlsP@q;%#?aY@lhDGg0Y zY7Zs}8j2||bp*|AfCK~Y!Tj_{D}kDa180$riFwy#^}17QJ^CAawTTdoo|p=|lxX|x zskCcs#!{*4bu*g1JkP@UYevy5ydNWzB*)n;?(9rxyv$j1*3N-A`98Tb0%gb9nDE9+ z6i_%+KNT2gqQDYz@|?BrgC?)bKVG%e#v2XtFzdzxqH8l6njkD_(#`dnHWFUyRIYl3 z#wglN5_GP!lDkx{(!%-q73`QtjFjV;3Y8bwSX2vy?2Od}D@IHnItByDP!I@%i4}ir zPlDn$1_*Eyj=@3n`6I9?wcq~Zsq=Num_(MOC`XZ8$7 zP^F>0-Ydn{-)l2mV+AJ@3ja`&N_D23rR_VA2zU>MR6+CyDnuq9Zc7rlO(Sr(HB6#6 zKKNw?>v5~5V|id2#@acZ)s*9n6gWQy4ZR3d=17fYxb)h>U1K42n*Ju`kuR2@Xl6N6 z&T654p^TGB#c}ApIV*4uad_C{cJSk~dPB}xb)lbDwkl#Wc>Oo2h22ehJ1W~W(R^L% zCeanF0;HO8%cKA2y8E8Nk7qAiZYe~B?}O7Q*rdt$5QG2?Mql-U9NQ@))0mqnJ2!_S zlEO@57+#`CR|||1WKdrHK4O8_t) z?ZD2PBqJfA{5|3OAVskT($GvB#uky z&3sQ1bOof6OH4M8a$LM3><|R-NU;eo`{nt1^e+ab)1-ER3#5j(C*Hx5>U`*of zBGsmrVmNiE1P~E2U=-?l(}XBIxZG07Yq^3?-P>XfZl=~Hmj3?onBEuJ$Y+qCa{G3< z88Z+7C9~mEa1z@)-2Lp;YwD*rNcb(@h4AVJ6i06yA1LiB|y@1z-#b9}d3q)UqZ9rNy_p5ef_u!_XVC|U@c zl<1+C7+>Fe7ny5jVK(AB1;?DM^VQPxPLX2wWT_C!$M#wnliehrGWg(haRoEZSYUva zJfh%+G!6B(UfkF_cVNl|=Sbl@ttZz(DG{vEi{oTl)f^*3!D-6umaf9WRggZuMYc52 z5v5E!Bca$-(;N$JZ6_62Z+-d8wnEI-A`~DYVB#|2t;ih(<#RVG<6hX_<@8md0W>3d zU_`jW;SIVb3V<4KJR#SJ^pp0>@+OUo)OeY7jX}fPq^zJ3fa{!gkx{47+%2n|CPPd3 zvs4KIlZHQJjy0-M@Q!ek!XZjP>S;SSjsj~u_kN8azVf_{F6li%N3F0sl~X$~cA$VF z0x|eL`M#CF<<#gAA9p^AlMx;nd2@v2chtc3<`0Y_}*jw+DSO5)JbN=4@| z#|^1}_E(`{fglmrW@Oi!mD3!#SW!IsU4{3VCdtzaStx#AQkh2JZ!CPzHibu9#2~%9 z*6PKxz>`9<475;MxfA(N4QOQ6PB$5feD@x_5%ZcJ4Us}Y3#{+$g4y(*bpa1J~SzKyt3;g`P`2qaaf$ox&q zZOfsET3^g~8iKI586m;k-EBQDek|5g;NXzJb~z>IODTlPo0fp+6Z{@%i@2}*3BQfD zpzTVKj4a;9V0o|r(0$szuugOW)2^<1bjK4eNr9^r3W_MBd?Oq&!OCh^w$;NGkw)ok0rw>N&9|>{Lur({4 z)t#SUH%b<*<9`);lIqd>VQK|OBxiQ}2wmL~*)RQc^X_7M34RuF{q!1~9ZM$Ux_*O*c;vTRPrx3KlY4{_4JkZikb$dw;ZGl~reTL1VYS+IUl;AWuc zaX)A#$ed&AC_$YMdmq@@|5kkNh}H03&NToZ&(Rl>2U~x}>H)n5NV{L7xkKF4%GjK3 zQieBQ1n9;0K(R?Dc(u7K7EU8-+I$yum!A&&jI6wI$J^{vz&6n!sj-45u?d!Gb}U4LiM{!+O&#!6bRL}VPntYKmw4YMzpL>eE)V6GX zZ9UdYET?3xGjAxQR!EciT*lYEmS6{wav97;blMExRu@lAMA$0WP#Lk;R3~q~Ip%n?_1e*fefy%or_`E>BWb zq1T0DBOwvFAz1|{i?j8S2Dz{ZmZUMvE|3)nqbQ8!O~%q7cPzvv8Y%GcC3)J~l&U58 zl;EYa=$=9zK4f8AD=!WL*&yg3lDiwV^INeZ5s>ztQWxPYLB>=cW_}?%mL5~f6ehm3 z4Idh96R)}4bfi_BpikBw4d5e$rK2+DHxjsMG}CkDlC(Ql zpLMSK5k5@9i6I7*7knh{;7P#w0jv04=wm4Iy-b)sQ`zvI=)a*|xlj|vN92(j=%vSnEtPcfua*K1CEvEDbZO!DN=g{$xR5c z41=PjAoAC33+E83shr_$pC78m(LDPhx%ajnq@)kYPy=B3gam2k7&C)7^ZohSdFRwe zj}aazy3wk%jNj&Tq;CkHTDn$lNToy@l?hKSFEC-AJ-%(+U=n7k{E|xSjf`p72}SOt>q>R{eqNeoU{M+C|d!Dus<* zAYGqb^k=aFHs&|<=6NsF=~mc&j=7FnclmO%A9;S`gb$a)5ml%3;hOvpx-{^Q3B zcaluahd-6lmm7j$%M!-if8L_GNvt{Twj?a_OlNJBwU;O=erPMh`QQU~EzsE*5p`Gq z9*hVJYdGpV{c-=03F50tutRA&pl9+8Ci4egGXjs$gqB$ZOCFhcIeI2;8&+*b!aRFFvT_KC2@ zR$bD{ej6TdAZKM$P#{Tf<786LU8NnR*i=SdHMnNyomudK&E>rYa%+CRO$kcuGbSh56eRj~dpmemh< z@?EW_$IdLn&2=3&R5c@B)QzrtlR--C| zjvVPfWBFQE@zoJ=4RP6K@$<@!axZnmK;5y==f%N zlExpcd)H#^jQv{q^-6;BG)}ao4XG5;A~lADGSF$tCr&>|jVlC%H)#md1l&RJ;1KO;tV4duTeOzQ z=%bH1FbCn0V>xyfp~w%13&R>xBko!t0A=0>4bem?Ljm8c4djXiw5r&}XqI)CeCl#~ zwuQr6IcwZ2g+^7oK!}ADQ)pvH?XBX-0l(;4O(}QW)5e0@qeqO40|(nhq$uOOLyuS) zA1B#liu`cTB4H$id$V!i;Z2M7&WI!^uZf_oNjxA;d17Ia$;SRVpFIlZaA(B5L81Jf z{qIrxhh1y{RSCw2)6>xd>ZJa@A(jH^J;)bnpb}S9v*^GAE~bbOC6c;j8gc_x0%WRr zLiTVX*4^Ck7=HrO^&0G@NOZ6GP%>4lbdoITKhsD3Bdh-GXYd!gi%~_viQ@9|w}98T zI$j-y(wZzW5erIsWrP6Lq;f1%R~0G~;VkS;_Z0R2W9ciS;%J(-chN-xEbc6X-~@sN z&*B!`-Q7L7ySuvw3+@s$xGye&0Ko|a5Af~%eDA-RIdjf*Pj_`yT_snJ^KC?*LnS}~ z*JBgKgg_in!)Z455gdnx_FUz=ytW3*3se2#mQ_Z9t? z7ax7VBMYhKBwF&_K&I=dvy)x|+AJ4qsVQ~XXyWLHJp@&WcUsG6)GN;7TVN0#l5~;Q z0)uo?6EXlC2t_~liY`}DN2FfJ>K2bKX6QuEKLM%}EM1b*=P*OQO3;)mL?Jnmd_zpR z3B?VAP=oh&K2})0Bf$0g{JXQNk_o{qn;+h(bjWv|ejwMC5sCVX6FVa<|F>95_7Nm8 zo;-#9Eb#l5$n}EBs%(p}V*K2#EhH-q85X+=Tpp=xYa8)Sl@`pUK0(PJ(}#&Rj7X*8 zfq;-nGuN+R4MxHZ8Ot6e-eQ#>bf~B*UYr^>D(5mnc1Muh-g5&~D{YCi@y4F0U*~6C z)pXMoc&u*pnZ~3^(m$rntLn;7AN6LLm}N0Ka!KOXqRvyRVH%^Hh?^nW^g0@Kmm5$H zqW6q!aXKz52Usey63Xzlwf5^+5rd8f{i$PS{}I(4{SmxaNh1;ENRh=8GC2xe&6++3 z!gKm)is>J_lH*m>2^Id{c}IrRfNAtf!&%=vH4HW$am~{Z-Xv>FEky3^N=KHfgh?-s zmveeESm4xmRd+$8ySwM-e|*UH3B!TYKfQRM15y9DS5<+TZrUs!NzST8d_5J*FdnR$ zmE`J$FHC1>L0mE6EW=#*j{Ur^cn$Zg#cWc*$&7?V{B5@l7^wOS|T4YG*h!Z-if8uD3$~D;PFFnq6j?ngQIZRZI_P$428hau0$GOGRgM) z-x2)9#xdycQy9}dJO1uzJvh|pVZyO1uxPL-#16cmqz5;|^wv`VDrLNODqSVKkD1ZCP6(1UsIEHcziM+kSMLL#Pu1@TZ46{XaZ0j{+TF>Hcp_` z+Rqz7yH;CR4tiFm7z+lU_uNpZby#W%5P=rcEFgmC`~#6Wtx0CyQfkqmp~prM6{ z3i1@3SRvN+Mx5`%Y3}~s-CdlVjtkhm8gb-|P3Q1`-wr|me$nH_;$&2cWeNF8>aGuu zAtvtV2Fr)Qe}sL~7^fsWGX^x+?*J-5JjPc75fk*^%IE~+7y*`3##j(PGY*GKvgiY2 zuXy0_7&1aAW*RXHa-grse*u32^*MrKYhu89yjvy?f|~jp+vswm_x-MApN#9O#8o4U zaoW5yjK5yv}SDpy;}oSq880jc%HDOsrq+ z_ZmwXGvX_e*}S!Vt37OyV%M0nX?Jo?qN- zdH0klI&Cv`a0?N-`U*PKc$+gw{QLpdB_7&UP(lU1+iE}QV!H-Pn~jL<_B`!Mez_<7 zSx~Sq5bNAi1&UP`Hyhw>+N}{YPuCoP;sN;CV27u2mH5Q9ICEZ3`K~qr5|!N4h1a`6 z7)CfqAl&e9P#)2AQ~`*fNP}NS1v(ta5*#Vc_gz2-+t~C$2``JJ%HywBRR!N;h9PhPliUIkX(BsIfk4N%1oo9;qM+p18w75%E_O5ZIHmb zbJD55UMRf~Q0HbHtP_DeP0d9=^T-iLon5r+OeMp8-Q{;{*e8E0Yre#SPuu=K&u(*w zQhY9IqzvA-{KC6q6}pGM<@nd631`IF;;r2Co4Wi~)YbZJ&nO^{lV+WW#VD~osZBw*orK;U*ESRpZ zo*X|LJwYY=F19V0`tNkSis^rlmyT;zUjY4B

4Rqb5oG;4p1(<+Ovw_DkOIpVV} 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 + + + + + + Feature Requests + + + + + + + + + + + + + +

Showcase projects

+ + + + + + + + + + \ No newline at end of file diff --git a/showcase/projects/midas/arumAgents/agentGenerator.js b/showcase/projects/midas/arumAgents/agentGenerator.js new file mode 100644 index 00000000..038257cb --- /dev/null +++ b/showcase/projects/midas/arumAgents/agentGenerator.js @@ -0,0 +1,216 @@ +'use strict'; + +function AgentGenerator(id) { + // execute super constructor + eve.Agent.call(this, id); + this.rpc = this.loadModule('rpc', this.rpcFunctions); + var me = this; + this.amountOfEvents = 0; + this.eventNumber = 0; + this.eventsToFire = 0; + this.lastEndOfDayTime = null; + this.events = []; + + conn = this.connect(eve.system.transports.getAll()); + + // connect to websocket if not online only + if (conn[0].connect !== undefined) { + conn[0].connect(EVENTS_AGENT_ADDRESS) + .then(function () { + console.log('Connected to ', EVENTS_AGENT_ADDRESS); + me.rpc.request(EVENTS_AGENT_ADDRESS, { + method: "loadEvents", + params: {filename: "events.csv", actuallySend: true} + }).done(function (reply) { + me.amountOfEvents = reply; + me.getEvents(AMOUNT_OF_INITIAL_EVENTS); + }); + }) + .catch(function (err) { + console.log('Error: Failed to connect to the conductor agent'); + console.log(err); + + // keep trying until the conductor agent is online + setTimeout(connect, RECONNECT_DELAY); + }); + } + //use local connection + else { + EVENTS_AGENT_ADDRESS = 'eventGenerator'; + setTimeout(function() { + me.rpc.request(EVENTS_AGENT_ADDRESS, { + method: "loadEvents", + params: {filename: "events.csv", actuallySend: true} + }).done(function (reply) { + me.amountOfEvents = reply; + me.getEvents(AMOUNT_OF_INITIAL_EVENTS); + }); + },40); + } +} + +// extend the eve.Agent prototype +AgentGenerator.prototype = Object.create(eve.Agent.prototype); +AgentGenerator.prototype.constructor = AgentGenerator; + +// define RPC functions, preferably in a separated object to clearly distinct +// exposed functions from local functions. +AgentGenerator.prototype.rpcFunctions = {}; + +AgentGenerator.prototype.rpcFunctions.receiveEvent = function(params) { + // setup timeline + this.events.push(JSON.stringify(params)); + + if (params.performedBy == "global") { + this.imposeWorkingHours(params); + } + else { + if (agentList[params.performedBy] === undefined) { + agentList[params.performedBy] = new GenericAgent(params.performedBy, params.type); + } + this.rpc.request(params.performedBy, {method: "newEvent", params: params}); + } + + // check if we need to get another event, its done here to avoid raceconditions + if (this.eventsToFire != 0) { + var me = this; + setTimeout(function() { + me.eventNumber += 1; + eventCounter.innerHTML = me.eventNumber +""; // make string so it works + me.rpc.request(EVENTS_AGENT_ADDRESS, {method:'nextEvent', params:{}}).done(); + me.eventsToFire -= 1; + },EVENT_DELAY); + if (INCREASE_SPEED == true) { + EVENT_DELAY = Math.max(0, 1000 - (1000 * (me.eventNumber / 205))); + } + } +}; + +AgentGenerator.prototype.getEvents = function (count) { + if (this.eventNumber + count > this.amountOfEvents) { + count = this.amountOfEvents - this.eventNumber; + } + if (count != 0) { + this.eventsToFire = count - 1; + this.rpc.request(EVENTS_AGENT_ADDRESS, {method: 'nextEvent', params: {}}).done(); + this.eventNumber += 1; + eventCounter.innerHTML = this.eventNumber + ""; // make string so it works + } +}; + +AgentGenerator.prototype.rpcFunctions.updateOpenJobs = function(params) { + var skipJob = params.jobId; + var time = params.time; + this.moveTimeline(params); + for (var agentId in agentList) { + if (agentList.hasOwnProperty(agentId)) { + agentList[agentId].jobs.updateJobs(time, skipJob); + } + } +}; + +AgentGenerator.prototype.moveTimeline = function(params) { + timeline.setCustomTime(params.time); + var range = timeline.getWindow(); + var duration = range.end - range.start; + var hiddenDates = timeline.body.hiddenDates; + var DateUtil = vis.timeline.DateUtil; + var hiddenDuration = DateUtil.getHiddenDurationBetween(hiddenDates, range.start, range.end); + var visibleDuration = duration - hiddenDuration; + + + var fraction = 0.15; + var requiredStartDuration = (1-fraction) * visibleDuration; + var requiredEndDuration = fraction * visibleDuration; + var convertedTime = new Date(params.time).getTime(); + var newStart; + var newEnd; + + var elapsedDuration = 0; + var previousPoint = convertedTime; + for (var i = hiddenDates.length-1; i > 0; i--) { + var startDate = hiddenDates[i].start; + var endDate = hiddenDates[i].end; + // if time after the cutout, and the + if (endDate <= convertedTime) { + elapsedDuration += previousPoint - endDate; + previousPoint = startDate; + if (elapsedDuration >= requiredStartDuration) { + newStart = endDate + (elapsedDuration - requiredStartDuration); + break; + } + } + } + if (newStart === undefined) { + newStart = endDate - (requiredStartDuration - elapsedDuration); + } + + elapsedDuration = 0; + previousPoint = convertedTime; + for (var i = 0; i < hiddenDates.length; i++) { + var startDate = hiddenDates[i].start; + var endDate = hiddenDates[i].end; + // if time after the cutout, and the + if (startDate >= convertedTime) { + elapsedDuration += startDate - previousPoint; + previousPoint = endDate; + if (elapsedDuration >= requiredEndDuration) { + newEnd = startDate - (elapsedDuration - requiredEndDuration); + break; + } + } + } + if (newEnd === undefined) { + newEnd = endDate + (requiredEndDuration - elapsedDuration); + } + + timeline.setWindow(newStart, newEnd, {animate:Math.min(100,EVENT_DELAY)}); +}; + +AgentGenerator.prototype.imposeWorkingHours = function(params) { + var time = params.time; + var operation = params.operation; + + for (var agentId in agentList) { + if (agentList.hasOwnProperty(agentId)) { + var agent = agentList[agentId]; + for (var jobId in agent.jobs.openJobs) { + if (agent.jobs.openJobs.hasOwnProperty(jobId)) { + var job = agent.jobs.openJobs[jobId]; + agent.updateAssignment(jobId, job.type, time, operation); + } + } + } + } + + if (operation == 'endOfDay') { + this.lastEndOfDayTime = time; + } + else { + if (this.lastEndOfDayTime !== null) { + timelineItems.update({ + id: 'night' + uuid(), + start: this.lastEndOfDayTime, + end: time, + type: 'background', + className: 'night' + }); + this.lastEndOfDayTime = null; + } + } + + +}; + +AgentGenerator.prototype.printEvents = function() { + var str = ""; + str += "["; + for (var i = 0; i < this.events.length; i++) { + str += this.events[i]; + if (i < this.events.length - 1) { + str += "," + } + } + str += "]"; + console.log(str); +} \ No newline at end of file diff --git a/showcase/projects/midas/arumAgents/durationData.js b/showcase/projects/midas/arumAgents/durationData.js new file mode 100644 index 00000000..65dbfa0b --- /dev/null +++ b/showcase/projects/midas/arumAgents/durationData.js @@ -0,0 +1,40 @@ +/** + * Created by Alex on 9/25/2014. + */ + +function DurationData() { + this.fields = ['duration','durationWithPause','durationWithStartup','durationWithBoth']; + for (var i = 0; i < this.fields.length; i++) { + this[this.fields[i]] = 0; + } +} + +DurationData.prototype.setData = function(otherData) { + for (var i = 0; i < this.fields.length; i++) { + this[this.fields[i]] = otherData[this.fields[i]]; + } +}; + +DurationData.prototype.getData = function() { + var dataObj = {}; + for (var i = 0; i < this.fields.length; i++) { + dataObj[this.fields[i]] = this[this.fields[i]]; + } + return dataObj; +}; + +DurationData.prototype.useHighest = function(otherData) { + for (var i = 0; i < this.fields.length; i++) { + var field = this.fields[i]; + if (this[field] < otherData[field]) { + this[field] = otherData[field]; + } + } +}; + +DurationData.prototype.calculateDuration = function(time, timeStart, elapsedTime, elapsedTimeWithPause, startupTime) { + this.duration = elapsedTime; + this.durationWithPause = elapsedTimeWithPause; + this.durationWithStartup = elapsedTime + startupTime.durationWithStartup; + this.durationWithBoth = elapsedTimeWithPause + startupTime.durationWithBoth; +}; diff --git a/showcase/projects/midas/arumAgents/durationStats.js b/showcase/projects/midas/arumAgents/durationStats.js new file mode 100644 index 00000000..f2a57f18 --- /dev/null +++ b/showcase/projects/midas/arumAgents/durationStats.js @@ -0,0 +1,142 @@ +/** + * Created by Alex on 9/25/2014. + */ + +function DurationStats() { + this.fields = ['duration','durationWithPause','durationWithStartup','durationWithBoth']; + for (var i = 0; i < this.fields.length; i++) { + this[this.fields[i]] = {mean: 0, std: 0}; + } +} + +DurationStats.prototype.clearStats = function() { + for (var i = 0; i < this.fields.length; i++) { + var field = this.fields[i]; + this[field].mean = 0; + this[field].std = 0; + } +}; + +DurationStats.prototype.sumStats = function(otherData) { + for (var i = 0; i < this.fields.length; i++) { + var field = this.fields[i]; + this[field].mean += otherData[field].mean; + this[field].std += Math.pow(otherData[field].std,2); + } +}; +DurationStats.prototype.averageStats = function(datapoints) { + for (var i = 0; i < this.fields.length; i++) { + var field = this.fields[i]; + this[field].mean /= datapoints; + this[field].std = Math.sqrt(this[field].std / datapoints); + } +}; + +DurationStats.prototype.getMeanData = function() { + var dataObj = {}; + for (var i = 0; i < this.fields.length; i++) { + dataObj[this.fields[i]] = this[this.fields[i]].mean; + } + return dataObj; +}; + +DurationStats.prototype.getData = function() { + var dataObj = {}; + for (var i = 0; i < this.fields.length; i++) { + dataObj[this.fields[i]] = {}; + dataObj[this.fields[i]].mean = this[this.fields[i]].mean; + dataObj[this.fields[i]].std = this[this.fields[i]].std; + } + + return dataObj; +}; + +DurationStats.prototype.setData = function(otherData) { + for (var i = 0; i < this.fields.length; i++) { + var field = this.fields[i]; + this[field].mean = otherData[field].mean; + this[field].std = otherData[field].std; + } +}; + +DurationStats.prototype.generateData = function(otherData) { + for (var i = 0; i < this.fields.length; i++) { + var field = this.fields[i]; + this[field].mean = otherData[i] * 3600000; + this[field].std = otherData[i] * 0.1; + } +}; + + +DurationStats.prototype.useHighest = function(otherData) { + for (var i = 0; i < this.fields.length; i++) { + var field = this.fields[i]; + if (this[field].mean < otherData[field].mean) { + this[field].mean = otherData[field].mean; + this[field].std = otherData[field].std; + } + } +}; + +DurationStats.prototype.getFakeStats = function(type) { + switch (type) { + case "Assemble Coffeemaker": + this.generateData([1.3,1.3,1.3,1.3]); + break; + case "Discuss potential NC": + this.generateData([0.5,0.5,0.9,0.9]); + break; + case "Drilling rework": + this.generateData([5,5,8,8]); + break; + case "Go to station": + var a = 0.3; + this.generateData([a,a,a,a]); + break; + case "Inspect finished Coffeemaker": + var a = 2; + this.generateData([a,a,a,a]); + break; + case "Inspect potential NC": + var a = 0.5; + this.generateData([a,a,a,a]); + break; + case "Kitting Coffeemaker": + var a = 1.2; + this.generateData([a,a,a,a]); + break; + case "NC Meeting": + var a = 15; + this.generateData([3,3.5,a,a]); + break; + case "Go to NC meeting": + var a = 12; + this.generateData([a,a,a,a]); + break; + case "Organise drilling rework": + var a = 2; + this.generateData([a,a,3,3]); + break; + case "Produce Coffeemaker": + var a = 35; + this.generateData([a,a,a,a]); + break; + case "Transport to delivery": + var a = 0.4; + this.generateData([a,a,a,a]); + break; + default: + console.log("CANNOT MATCH", type); + break; + + } + + + + + + + + + +}; \ No newline at end of file diff --git a/showcase/projects/midas/arumAgents/eventGenerator.js b/showcase/projects/midas/arumAgents/eventGenerator.js new file mode 100644 index 00000000..7750fa03 --- /dev/null +++ b/showcase/projects/midas/arumAgents/eventGenerator.js @@ -0,0 +1,2037 @@ +'use strict'; + +if (typeof window === 'undefined') { + var eve = require('evejs'); + var GenericAgent = require('./GenericAgent') +} + +function EventGenerator(id) { + // execute super constructor + eve.Agent.call(this, id); + this.rpc = this.loadModule('rpc', this.rpcFunctions); + this.connect(eve.system.transports.getAll()); + this.eventCounter = 0; + this.events = [ + { + "jobId": "100", + "time": "2014-09-16T09:25:00.000+02:00", + "performedBy": "Francesco", + "type": "worker", + "assignment": "Produce Coffeemaker", + "productId": "7", + "operation": "start" + }, + { + "jobId": "101", + "time": "2014-09-16T09:25:00.000+02:00", + "performedBy": "Francesco", + "type": "worker", + "assignment": "Kitting Coffeemaker", + "productId": "7", + "operation": "start" + }, + { + "jobId": "101", + "time": "2014-09-16T10:49:03.000+02:00", + "performedBy": "Francesco", + "type": "worker", + "assignment": "Kitting Coffeemaker", + "productId": "7", + "operation": "finish" + }, + { + "jobId": "102", + "time": "2014-09-16T10:52:13.000+02:00", + "performedBy": "Francesco", + "type": "worker", + "assignment": "Assemble Coffeemaker", + "productId": "7", + "operation": "start" + }, + { + "jobId": "999", + "time": "2014-09-16T18:00:00.000+02:00", + "performedBy": "global", + "type": "global", + "assignment": "", + "productId": "", + "operation": "endOfDay" + }, + { + "jobId": "999", + "time": "2014-09-17T08:30:00.000+02:00", + "performedBy": "global", + "type": "global", + "assignment": "", + "productId": "", + "operation": "startOfDay" + }, + { + "jobId": "81", + "time": "2014-09-17T09:23:00.000+02:00", + "performedBy": "Biagio", + "type": "worker", + "assignment": "Produce Coffeemaker", + "productId": "6", + "operation": "start" + }, + { + "jobId": "82", + "time": "2014-09-17T09:23:00.000+02:00", + "performedBy": "Biagio", + "type": "worker", + "assignment": "Kitting Coffeemaker", + "productId": "6", + "operation": "start" + }, + { + "jobId": "111", + "time": "2014-09-17T09:25:00.000+02:00", + "performedBy": "Fredrico", + "type": "worker", + "assignment": "Produce Coffeemaker", + "productId": "8", + "operation": "start" + }, + { + "jobId": "112", + "time": "2014-09-17T09:25:00.000+02:00", + "performedBy": "Fredrico", + "type": "worker", + "assignment": "Kitting Coffeemaker", + "productId": "8", + "operation": "start" + }, + { + "jobId": "82", + "time": "2014-09-17T10:29:03.000+02:00", + "performedBy": "Biagio", + "type": "worker", + "assignment": "Kitting Coffeemaker", + "productId": "6", + "operation": "finish" + }, + { + "jobId": "83", + "time": "2014-09-17T10:32:12.000+02:00", + "performedBy": "Biagio", + "type": "worker", + "assignment": "Assemble Coffeemaker", + "productId": "6", + "operation": "start" + }, + { + "jobId": "112", + "time": "2014-09-17T11:16:03.000+02:00", + "performedBy": "Fredrico", + "type": "worker", + "assignment": "Kitting Coffeemaker", + "productId": "8", + "operation": "finish" + }, + { + "jobId": "113", + "time": "2014-09-17T11:18:03.000+02:00", + "performedBy": "Fredrico", + "type": "worker", + "assignment": "Assemble Coffeemaker", + "productId": "8", + "operation": "start" + }, + { + "jobId": "113", + "time": "2014-09-17T11:23:56.000+02:00", + "performedBy": "Fredrico", + "type": "worker", + "assignment": "Assemble Coffeemaker", + "productId": "8", + "operation": "pause" + }, + { + "jobId": "114", + "time": "2014-09-17T11:23:56.000+02:00", + "performedBy": "Paolo", + "type": "rao", + "assignment": "Go to station", + "productId": "8", + "operation": "start" + }, + { + "jobId": "114", + "time": "2014-09-17T11:28:16.000+02:00", + "performedBy": "Paolo", + "type": "rao", + "assignment": "Go to station", + "productId": "8", + "operation": "finish" + }, + { + "jobId": "115", + "time": "2014-09-17T11:28:19.000+02:00", + "performedBy": "Paolo", + "type": "rao", + "assignment": "Inspect potential NC", + "productId": "8", + "operation": "start", + "prerequisites": [ + { + "type": "Go to station", + "agentId": "Paolo" + } + ] + }, + { + "jobId": "115", + "time": "2014-09-17T11:43:21.000+02:00", + "performedBy": "Paolo", + "type": "rao", + "assignment": "Inspect potential NC", + "productId": "8", + "operation": "finish" + }, + { + "jobId": "113", + "time": "2014-09-17T11:44:21.000+02:00", + "performedBy": "Fredrico", + "type": "worker", + "assignment": "Assemble Coffeemaker", + "productId": "8", + "operation": "resume" + }, + { + "jobId": "102", + "time": "2014-09-17T12:14:39.000+02:00", + "performedBy": "Francesco", + "type": "worker", + "assignment": "Assemble Coffeemaker", + "productId": "7", + "operation": "finish" + }, + { + "jobId": "103", + "time": "2014-09-17T12:14:42.000+02:00", + "performedBy": "Paolo", + "type": "rao", + "assignment": "Go to station", + "productId": "7", + "operation": "start" + }, + { + "jobId": "103", + "time": "2014-09-17T12:24:39.000+02:00", + "performedBy": "Paolo", + "type": "rao", + "assignment": "Go to station", + "productId": "7", + "operation": "finish" + }, + { + "jobId": "104", + "time": "2014-09-17T12:24:45.000+02:00", + "performedBy": "Paolo", + "type": "rao", + "assignment": "Inspect finished Coffeemaker", + "productId": "7", + "operation": "start", + "prerequisites": [ + { + "type": "Go to station", + "agentId": "Paolo" + } + ] + }, + { + "jobId": "104", + "time": "2014-09-17T14:40:01.000+02:00", + "performedBy": "Paolo", + "type": "rao", + "assignment": "Inspect finished Coffeemaker", + "productId": "7", + "operation": "finish" + }, + { + "jobId": "105", + "time": "2014-09-17T14:41:01.000+02:00", + "performedBy": "Francesco", + "type": "worker", + "assignment": "Transport to delivery", + "productId": "7", + "operation": "start" + }, + { + "jobId": "105", + "time": "2014-09-17T15:12:12.000+02:00", + "performedBy": "Francesco", + "type": "worker", + "assignment": "Transport to delivery", + "productId": "7", + "operation": "finish" + }, + { + "jobId": "100", + "time": "2014-09-17T15:12:12.000+02:00", + "performedBy": "Francesco", + "type": "worker", + "assignment": "Produce Coffeemaker", + "productId": "7", + "operation": "finish" + }, + { + "jobId": "83", + "time": "2014-09-17T15:45:21.000+02:00", + "performedBy": "Biagio", + "type": "worker", + "assignment": "Assemble Coffeemaker", + "productId": "6", + "operation": "pause" + }, + { + "jobId": "84", + "time": "2014-09-17T15:45:21.000+02:00", + "performedBy": "Paolo", + "type": "rao", + "assignment": "Go to station", + "productId": "6", + "operation": "start" + }, + { + "jobId": "84", + "time": "2014-09-17T15:55:11.000+02:00", + "performedBy": "Paolo", + "type": "rao", + "assignment": "Go to station", + "productId": "6", + "operation": "finish" + }, + { + "jobId": "85", + "time": "2014-09-17T15:58:11.000+02:00", + "performedBy": "Paolo", + "type": "rao", + "assignment": "Inspect potential NC", + "productId": "6", + "operation": "start", + "prerequisites": [ + { + "type": "Go to station", + "agentId": "Paolo" + } + ] + }, + { + "jobId": "85", + "time": "2014-09-17T16:11:12.000+02:00", + "performedBy": "Paolo", + "type": "rao", + "assignment": "Inspect potential NC", + "productId": "6", + "operation": "finish" + }, + { + "jobId": "86", + "time": "2014-09-17T16:11:12.000+02:00", + "performedBy": "Marcelo", + "type": "pm", + "assignment": "Go to station", + "productId": "6", + "operation": "start" + }, + { + "jobId": "86", + "time": "2014-09-17T16:51:12.000+02:00", + "performedBy": "Marcelo", + "type": "pm", + "assignment": "Go to station", + "productId": "6", + "operation": "finish" + }, + { + "jobId": "87", + "time": "2014-09-17T16:52:12.000+02:00", + "performedBy": "Marcelo", + "type": "pm", + "assignment": "Discuss potential NC", + "productId": "6", + "operation": "start", + "prerequisites": [ + { + "type": "Go to station", + "agentId": "Marcelo" + } + ] + }, + { + "jobId": "87", + "time": "2014-09-17T17:12:12.000+02:00", + "performedBy": "Marcelo", + "type": "pm", + "assignment": "Discuss potential NC", + "productId": "6", + "operation": "finish" + }, + { + "jobId": "88", + "time": "2014-09-17T17:12:12.000+02:00", + "performedBy": "Marcelo", + "type": "pm", + "assignment": "Go to NC meeting", + "productId": "6", + "operation": "start" + }, + { + "jobId": "89", + "time": "2014-09-17T17:12:12.000+02:00", + "performedBy": "Giovanni", + "type": "mt", + "assignment": "Go to NC meeting", + "productId": "6", + "operation": "start" + }, + { + "jobId": "90", + "time": "2014-09-17T17:12:12.000+02:00", + "performedBy": "Cristiana", + "type": "mt", + "assignment": "Go to NC meeting", + "productId": "6", + "operation": "start" + }, + { + "jobId": "91", + "time": "2014-09-17T17:12:12.000+02:00", + "performedBy": "Claudio", + "type": "mt", + "assignment": "Go to NC meeting", + "productId": "6", + "operation": "start" + }, + { + "jobId": "999", + "time": "2014-09-17T18:00:00.000+02:00", + "performedBy": "global", + "type": "global", + "assignment": "", + "productId": "", + "operation": "endOfDay" + }, + { + "jobId": "999", + "time": "2014-09-18T08:30:00.000+02:00", + "performedBy": "global", + "type": "global", + "assignment": "", + "productId": "", + "operation": "startOfDay" + }, + { + "jobId": "41", + "time": "2014-09-18T09:25:00.000+02:00", + "performedBy": "Francesco", + "type": "worker", + "assignment": "Produce Coffeemaker", + "productId": "5", + "operation": "start" + }, + { + "jobId": "42", + "time": "2014-09-18T09:25:00.000+02:00", + "performedBy": "Francesco", + "type": "worker", + "assignment": "Kitting Coffeemaker", + "productId": "5", + "operation": "start" + }, + { + "jobId": "42", + "time": "2014-09-18T10:49:03.000+02:00", + "performedBy": "Francesco", + "type": "worker", + "assignment": "Kitting Coffeemaker", + "productId": "5", + "operation": "finish" + }, + { + "jobId": "43", + "time": "2014-09-18T10:52:13.000+02:00", + "performedBy": "Francesco", + "type": "worker", + "assignment": "Assemble Coffeemaker", + "productId": "5", + "operation": "start" + }, + { + "jobId": "113", + "time": "2014-09-18T12:10:29.000+02:00", + "performedBy": "Fredrico", + "type": "worker", + "assignment": "Assemble Coffeemaker", + "productId": "8", + "operation": "finish" + }, + { + "jobId": "116", + "time": "2014-09-18T12:10:29.000+02:00", + "performedBy": "Paolo", + "type": "rao", + "assignment": "Go to station", + "productId": "8", + "operation": "start" + }, + { + "jobId": "116", + "time": "2014-09-18T12:28:29.000+02:00", + "performedBy": "Paolo", + "type": "rao", + "assignment": "Go to station", + "productId": "8", + "operation": "finish" + }, + { + "jobId": "117", + "time": "2014-09-18T12:35:29.000+02:00", + "performedBy": "Paolo", + "type": "rao", + "assignment": "Inspect finished Coffeemaker", + "productId": "8", + "operation": "start", + "prerequisites": [ + { + "type": "Go to station", + "agentId": "Paolo" + } + ] + }, + { + "jobId": "117", + "time": "2014-09-18T15:12:01.000+02:00", + "performedBy": "Paolo", + "type": "rao", + "assignment": "Inspect finished Coffeemaker", + "productId": "8", + "operation": "finish" + }, + { + "jobId": "118", + "time": "2014-09-18T15:14:01.000+02:00", + "performedBy": "Fredrico", + "type": "worker", + "assignment": "Transport to delivery", + "productId": "8", + "operation": "start" + }, + { + "jobId": "118", + "time": "2014-09-18T15:31:01.000+02:00", + "performedBy": "Fredrico", + "type": "worker", + "assignment": "Transport to delivery", + "productId": "8", + "operation": "finish" + }, + { + "jobId": "111", + "time": "2014-09-18T15:31:01.000+02:00", + "performedBy": "Fredrico", + "type": "worker", + "assignment": "Produce Coffeemaker", + "productId": "8", + "operation": "finish" + }, + { + "jobId": "999", + "time": "2014-09-18T18:00:00.000+02:00", + "performedBy": "global", + "type": "global", + "assignment": "", + "productId": "", + "operation": "endOfDay" + }, + { + "jobId": "999", + "time": "2014-09-19T08:30:00.000+02:00", + "performedBy": "global", + "type": "global", + "assignment": "", + "productId": "", + "operation": "startOfDay" + }, + { + "jobId": "88", + "time": "2014-09-19T09:56:00.000+02:00", + "performedBy": "Marcelo", + "type": "pm", + "assignment": "Go to NC meeting", + "productId": "6", + "operation": "finish" + }, + { + "jobId": "89", + "time": "2014-09-19T09:56:00.000+02:00", + "performedBy": "Giovanni", + "type": "mt", + "assignment": "Go to NC meeting", + "productId": "6", + "operation": "finish" + }, + { + "jobId": "90", + "time": "2014-09-19T09:56:00.000+02:00", + "performedBy": "Cristiana", + "type": "mt", + "assignment": "Go to NC meeting", + "productId": "6", + "operation": "finish" + }, + { + "jobId": "91", + "time": "2014-09-19T09:56:00.000+02:00", + "performedBy": "Claudio", + "type": "mt", + "assignment": "Go to NC meeting", + "productId": "6", + "operation": "finish" + }, + { + "jobId": "92", + "time": "2014-09-19T10:01:00.000+02:00", + "performedBy": "Marcelo", + "type": "pm", + "assignment": "NC Meeting", + "productId": "6", + "operation": "start", + "prerequisites": [ + { + "type": "Go to NC meeting" + } + ] + }, + { + "jobId": "93", + "time": "2014-09-19T10:01:00.000+02:00", + "performedBy": "Giovanni", + "type": "mt", + "assignment": "NC Meeting", + "productId": "6", + "operation": "start", + "prerequisites": [ + { + "type": "Go to NC meeting" + } + ] + }, + { + "jobId": "94", + "time": "2014-09-19T10:01:00.000+02:00", + "performedBy": "Cristiana", + "type": "mt", + "assignment": "NC Meeting", + "productId": "6", + "operation": "start", + "prerequisites": [ + { + "type": "Go to NC meeting" + } + ] + }, + { + "jobId": "95", + "time": "2014-09-19T10:01:00.000+02:00", + "performedBy": "Claudio", + "type": "mt", + "assignment": "NC Meeting", + "productId": "6", + "operation": "start", + "prerequisites": [ + { + "type": "Go to NC meeting" + } + ] + }, + { + "jobId": "44", + "time": "2014-09-19T12:10:32.000+02:00", + "performedBy": "Paolo", + "type": "rao", + "assignment": "Go to station", + "productId": "5", + "operation": "start" + }, + { + "jobId": "43", + "time": "2014-09-19T12:10:39.000+02:00", + "performedBy": "Francesco", + "type": "worker", + "assignment": "Assemble Coffeemaker", + "productId": "5", + "operation": "finish" + }, + { + "jobId": "44", + "time": "2014-09-19T12:24:39.000+02:00", + "performedBy": "Paolo", + "type": "rao", + "assignment": "Go to station", + "productId": "5", + "operation": "finish" + }, + { + "jobId": "45", + "time": "2014-09-19T12:24:45.000+02:00", + "performedBy": "Paolo", + "type": "rao", + "assignment": "Inspect finished Coffeemaker", + "productId": "5", + "operation": "start", + "prerequisites": [ + { + "type": "Go to station", + "agentId": "Paolo" + } + ] + }, + { + "jobId": "92", + "time": "2014-09-19T13:14:00.000+02:00", + "performedBy": "Marcelo", + "type": "pm", + "assignment": "NC Meeting", + "productId": "6", + "operation": "finish" + }, + { + "jobId": "93", + "time": "2014-09-19T13:14:00.000+02:00", + "performedBy": "Giovanni", + "type": "mt", + "assignment": "NC Meeting", + "productId": "6", + "operation": "finish" + }, + { + "jobId": "94", + "time": "2014-09-19T13:14:00.000+02:00", + "performedBy": "Cristiana", + "type": "mt", + "assignment": "NC Meeting", + "productId": "6", + "operation": "finish" + }, + { + "jobId": "95", + "time": "2014-09-19T13:14:00.000+02:00", + "performedBy": "Claudio", + "type": "mt", + "assignment": "NC Meeting", + "productId": "6", + "operation": "finish" + }, + { + "jobId": "45", + "time": "2014-09-19T14:43:01.000+02:00", + "performedBy": "Paolo", + "type": "rao", + "assignment": "Inspect finished Coffeemaker", + "productId": "5", + "operation": "finish" + }, + { + "jobId": "46", + "time": "2014-09-19T14:43:01.000+02:00", + "performedBy": "Francesco", + "type": "worker", + "assignment": "Transport to delivery", + "productId": "5", + "operation": "start" + }, + { + "jobId": "46", + "time": "2014-09-19T14:59:12.000+02:00", + "performedBy": "Francesco", + "type": "worker", + "assignment": "Transport to delivery", + "productId": "5", + "operation": "finish" + }, + { + "jobId": "41", + "time": "2014-09-19T14:59:12.000+02:00", + "performedBy": "Francesco", + "type": "worker", + "assignment": "Produce Coffeemaker", + "productId": "5", + "operation": "finish" + }, + { + "jobId": "999", + "time": "2014-09-19T18:00:00.000+02:00", + "performedBy": "global", + "type": "global", + "assignment": "", + "productId": "", + "operation": "endOfDay" + }, + { + "jobId": "999", + "time": "2014-09-22T08:30:00.000+02:00", + "performedBy": "global", + "type": "global", + "assignment": "", + "productId": "", + "operation": "startOfDay" + }, + { + "jobId": "83", + "time": "2014-09-22T09:04:39.000+02:00", + "performedBy": "Biagio", + "type": "worker", + "assignment": "Assemble Coffeemaker", + "productId": "6", + "operation": "resume" + }, + { + "jobId": "31", + "time": "2014-09-22T09:08:00.000+02:00", + "performedBy": "Fredrico", + "type": "worker", + "assignment": "Produce Coffeemaker", + "productId": "3", + "operation": "start" + }, + { + "jobId": "32", + "time": "2014-09-22T09:08:00.000+02:00", + "performedBy": "Fredrico", + "type": "worker", + "assignment": "Kitting Coffeemaker", + "productId": "3", + "operation": "start" + }, + { + "jobId": "32", + "time": "2014-09-22T10:36:03.000+02:00", + "performedBy": "Fredrico", + "type": "worker", + "assignment": "Kitting Coffeemaker", + "productId": "3", + "operation": "finish" + }, + { + "jobId": "33", + "time": "2014-09-22T10:38:32.000+02:00", + "performedBy": "Fredrico", + "type": "worker", + "assignment": "Assemble Coffeemaker", + "productId": "3", + "operation": "start" + }, + { + "jobId": "83", + "time": "2014-09-22T14:42:39.000+02:00", + "performedBy": "Biagio", + "type": "worker", + "assignment": "Assemble Coffeemaker", + "productId": "6", + "operation": "finish" + }, + { + "jobId": "96", + "time": "2014-09-22T14:42:39.000+02:00", + "performedBy": "Paolo", + "type": "rao", + "assignment": "Go to station", + "productId": "6", + "operation": "start" + }, + { + "jobId": "96", + "time": "2014-09-22T14:52:39.000+02:00", + "performedBy": "Paolo", + "type": "rao", + "assignment": "Go to station", + "productId": "6", + "operation": "finish" + }, + { + "jobId": "97", + "time": "2014-09-22T14:54:39.000+02:00", + "performedBy": "Paolo", + "type": "rao", + "assignment": "Inspect finished Coffeemaker", + "productId": "6", + "operation": "start", + "prerequisites": [ + { + "type": "Go to station", + "agentId": "Paolo" + } + ] + }, + { + "jobId": "97", + "time": "2014-09-22T17:27:39.000+02:00", + "performedBy": "Paolo", + "type": "rao", + "assignment": "Inspect finished Coffeemaker", + "productId": "6", + "operation": "finish" + }, + { + "jobId": "98", + "time": "2014-09-22T17:29:39.000+02:00", + "performedBy": "Biagio", + "type": "worker", + "assignment": "Transport to delivery", + "productId": "6", + "operation": "start" + }, + { + "jobId": "98", + "time": "2014-09-22T17:47:39.000+02:00", + "performedBy": "Biagio", + "type": "worker", + "assignment": "Transport to delivery", + "productId": "6", + "operation": "finish" + }, + { + "jobId": "81", + "time": "2014-09-22T17:47:39.000+02:00", + "performedBy": "Biagio", + "type": "worker", + "assignment": "Produce Coffeemaker", + "productId": "6", + "operation": "finish" + }, + { + "jobId": "999", + "time": "2014-09-22T18:00:00.000+02:00", + "performedBy": "global", + "type": "global", + "assignment": "", + "productId": "", + "operation": "endOfDay" + }, + { + "jobId": "999", + "time": "2014-09-23T08:30:00.000+02:00", + "performedBy": "global", + "type": "global", + "assignment": "", + "productId": "", + "operation": "startOfDay" + }, + { + "jobId": "51", + "time": "2014-09-23T09:25:00.000+02:00", + "performedBy": "Francesco", + "type": "worker", + "assignment": "Produce Coffeemaker", + "productId": "4", + "operation": "start" + }, + { + "jobId": "52", + "time": "2014-09-23T09:25:00.000+02:00", + "performedBy": "Francesco", + "type": "worker", + "assignment": "Kitting Coffeemaker", + "productId": "4", + "operation": "start" + }, + { + "jobId": "52", + "time": "2014-09-23T10:49:03.000+02:00", + "performedBy": "Francesco", + "type": "worker", + "assignment": "Kitting Coffeemaker", + "productId": "4", + "operation": "finish" + }, + { + "jobId": "53", + "time": "2014-09-23T10:52:13.000+02:00", + "performedBy": "Francesco", + "type": "worker", + "assignment": "Assemble Coffeemaker", + "productId": "4", + "operation": "start" + }, + { + "jobId": "53", + "time": "2014-09-23T11:45:21.000+02:00", + "performedBy": "Francesco", + "type": "worker", + "assignment": "Assemble Coffeemaker", + "productId": "4", + "operation": "pause" + }, + { + "jobId": "54", + "time": "2014-09-23T11:45:21.000+02:00", + "performedBy": "Paolo", + "type": "rao", + "assignment": "Go to station", + "productId": "4", + "operation": "start" + }, + { + "jobId": "54", + "time": "2014-09-23T12:03:21.000+02:00", + "performedBy": "Paolo", + "type": "rao", + "assignment": "Go to station", + "productId": "4", + "operation": "finish" + }, + { + "jobId": "55", + "time": "2014-09-23T12:03:56.000+02:00", + "performedBy": "Paolo", + "type": "rao", + "assignment": "Inspect potential NC", + "productId": "4", + "operation": "start", + "prerequisites": [ + { + "type": "Go to station", + "agentId": "Paolo" + } + ] + }, + { + "jobId": "55", + "time": "2014-09-23T14:01:02.000+02:00", + "performedBy": "Paolo", + "type": "rao", + "assignment": "Inspect potential NC", + "productId": "4", + "operation": "finish" + }, + { + "jobId": "56", + "time": "2014-09-23T14:01:02.000+02:00", + "performedBy": "Marcelo", + "type": "pm", + "assignment": "Go to station", + "productId": "4", + "operation": "start" + }, + { + "jobId": "33", + "time": "2014-09-23T14:10:29.000+02:00", + "performedBy": "Fredrico", + "type": "worker", + "assignment": "Assemble Coffeemaker", + "productId": "3", + "operation": "finish" + }, + { + "jobId": "34", + "time": "2014-09-23T14:10:29.000+02:00", + "performedBy": "Paolo", + "type": "rao", + "assignment": "Go to station", + "productId": "3", + "operation": "start" + }, + { + "jobId": "34", + "time": "2014-09-23T14:12:29.000+02:00", + "performedBy": "Paolo", + "type": "rao", + "assignment": "Go to station", + "productId": "3", + "operation": "finish" + }, + { + "jobId": "35", + "time": "2014-09-23T14:12:34.000+02:00", + "performedBy": "Paolo", + "type": "rao", + "assignment": "Inspect finished Coffeemaker", + "productId": "3", + "operation": "start", + "prerequisites": [ + { + "type": "Go to station", + "agentId": "Paolo" + } + ] + }, + { + "jobId": "56", + "time": "2014-09-23T14:25:12.000+02:00", + "performedBy": "Marcelo", + "type": "pm", + "assignment": "Go to station", + "productId": "4", + "operation": "finish" + }, + { + "jobId": "57", + "time": "2014-09-23T14:25:32.000+02:00", + "performedBy": "Marcelo", + "type": "pm", + "assignment": "Discuss potential NC", + "productId": "4", + "operation": "start", + "prerequisites": [ + { + "type": "Go to station", + "agentId": "Marcelo" + } + ] + }, + { + "jobId": "57", + "time": "2014-09-23T15:05:12.000+02:00", + "performedBy": "Marcelo", + "type": "pm", + "assignment": "Discuss potential NC", + "productId": "4", + "operation": "finish" + }, + { + "jobId": "58", + "time": "2014-09-23T15:05:15.000+02:00", + "performedBy": "Marcelo", + "type": "pm", + "assignment": "Go to NC meeting", + "productId": "4", + "operation": "start" + }, + { + "jobId": "59", + "time": "2014-09-23T15:05:15.000+02:00", + "performedBy": "Pascale", + "type": "mt", + "assignment": "Go to NC meeting", + "productId": "4", + "operation": "start" + }, + { + "jobId": "60", + "time": "2014-09-23T15:05:15.000+02:00", + "performedBy": "Giovanni", + "type": "mt", + "assignment": "Go to NC meeting", + "productId": "4", + "operation": "start" + }, + { + "jobId": "61", + "time": "2014-09-23T15:05:15.000+02:00", + "performedBy": "Cristiana", + "type": "mt", + "assignment": "Go to NC meeting", + "productId": "4", + "operation": "start" + }, + { + "jobId": "62", + "time": "2014-09-23T15:05:15.000+02:00", + "performedBy": "Claudio", + "type": "mt", + "assignment": "Go to NC meeting", + "productId": "4", + "operation": "start" + }, + { + "jobId": "35", + "time": "2014-09-23T15:32:01.000+02:00", + "performedBy": "Paolo", + "type": "rao", + "assignment": "Inspect finished Coffeemaker", + "productId": "3", + "operation": "finish" + }, + { + "jobId": "36", + "time": "2014-09-23T15:32:01.000+02:00", + "performedBy": "Fredrico", + "type": "worker", + "assignment": "Transport to delivery", + "productId": "3", + "operation": "start" + }, + { + "jobId": "36", + "time": "2014-09-23T15:49:12.000+02:00", + "performedBy": "Fredrico", + "type": "worker", + "assignment": "Transport to delivery", + "productId": "3", + "operation": "finish" + }, + { + "jobId": "31", + "time": "2014-09-23T15:49:12.000+02:00", + "performedBy": "Fredrico", + "type": "worker", + "assignment": "Produce Coffeemaker", + "productId": "3", + "operation": "finish" + }, + { + "jobId": "999", + "time": "2014-09-23T18:00:00.000+02:00", + "performedBy": "global", + "type": "global", + "assignment": "", + "productId": "", + "operation": "endOfDay" + }, + { + "jobId": "999", + "time": "2014-09-24T08:30:00.000+02:00", + "performedBy": "global", + "type": "global", + "assignment": "", + "productId": "", + "operation": "startOfDay" + }, + { + "jobId": "21", + "time": "2014-09-24T09:08:00.000+02:00", + "performedBy": "Fredrico", + "type": "worker", + "assignment": "Produce Coffeemaker", + "productId": "2", + "operation": "start" + }, + { + "jobId": "22", + "time": "2014-09-24T09:08:00.000+02:00", + "performedBy": "Fredrico", + "type": "worker", + "assignment": "Kitting Coffeemaker", + "productId": "2", + "operation": "start" + }, + { + "jobId": "22", + "time": "2014-09-24T10:36:03.000+02:00", + "performedBy": "Fredrico", + "type": "worker", + "assignment": "Kitting Coffeemaker", + "productId": "2", + "operation": "finish" + }, + { + "jobId": "23", + "time": "2014-09-24T10:38:32.000+02:00", + "performedBy": "Fredrico", + "type": "worker", + "assignment": "Assemble Coffeemaker", + "productId": "2", + "operation": "start" + }, + { + "jobId": "23", + "time": "2014-09-24T11:03:56.000+02:00", + "performedBy": "Fredrico", + "type": "worker", + "assignment": "Assemble Coffeemaker", + "productId": "2", + "operation": "pause" + }, + { + "jobId": "24", + "time": "2014-09-24T11:03:56.000+02:00", + "performedBy": "Paolo", + "type": "rao", + "assignment": "Go to station", + "productId": "2", + "operation": "start" + }, + { + "jobId": "24", + "time": "2014-09-24T11:28:16.000+02:00", + "performedBy": "Paolo", + "type": "rao", + "assignment": "Go to station", + "productId": "2", + "operation": "finish" + }, + { + "jobId": "25", + "time": "2014-09-24T11:28:19.000+02:00", + "performedBy": "Paolo", + "type": "rao", + "assignment": "Inspect potential NC", + "productId": "2", + "operation": "start", + "prerequisites": [ + { + "type": "Go to station", + "agentId": "Paolo" + } + ] + }, + { + "jobId": "25", + "time": "2014-09-24T11:35:21.000+02:00", + "performedBy": "Paolo", + "type": "rao", + "assignment": "Inspect potential NC", + "productId": "2", + "operation": "finish" + }, + { + "jobId": "23", + "time": "2014-09-24T11:35:21.000+02:00", + "performedBy": "Fredrico", + "type": "worker", + "assignment": "Assemble Coffeemaker", + "productId": "2", + "operation": "resume" + }, + { + "jobId": "61", + "time": "2014-09-24T15:51:00.000+02:00", + "performedBy": "Cristiana", + "type": "mt", + "assignment": "Go to NC meeting", + "productId": "4", + "operation": "finish" + }, + { + "jobId": "62", + "time": "2014-09-24T15:56:00.000+02:00", + "performedBy": "Claudio", + "type": "mt", + "assignment": "Go to NC meeting", + "productId": "4", + "operation": "finish" + }, + { + "jobId": "58", + "time": "2014-09-24T15:57:00.000+02:00", + "performedBy": "Marcelo", + "type": "pm", + "assignment": "Go to NC meeting", + "productId": "4", + "operation": "finish" + }, + { + "jobId": "59", + "time": "2014-09-24T15:58:00.000+02:00", + "performedBy": "Pascale", + "type": "mt", + "assignment": "Go to NC meeting", + "productId": "4", + "operation": "finish" + }, + { + "jobId": "60", + "time": "2014-09-24T15:59:00.000+02:00", + "performedBy": "Giovanni", + "type": "mt", + "assignment": "Go to NC meeting", + "productId": "4", + "operation": "finish" + }, + { + "jobId": "64", + "time": "2014-09-24T16:00:01.000+02:00", + "performedBy": "Marcelo", + "type": "pm", + "assignment": "NC Meeting", + "productId": "4", + "operation": "start", + "prerequisites": [ + { + "type": "Go to NC meeting" + } + ] + }, + { + "jobId": "65", + "time": "2014-09-24T16:00:01.000+02:00", + "performedBy": "Pascale", + "type": "mt", + "assignment": "NC Meeting", + "productId": "4", + "operation": "start", + "prerequisites": [ + { + "type": "Go to NC meeting" + } + ] + }, + { + "jobId": "66", + "time": "2014-09-24T16:00:01.000+02:00", + "performedBy": "Giovanni", + "type": "mt", + "assignment": "NC Meeting", + "productId": "4", + "operation": "start", + "prerequisites": [ + { + "type": "Go to NC meeting" + } + ] + }, + { + "jobId": "67", + "time": "2014-09-24T16:00:01.000+02:00", + "performedBy": "Cristiana", + "type": "mt", + "assignment": "NC Meeting", + "productId": "4", + "operation": "start", + "prerequisites": [ + { + "type": "Go to NC meeting" + } + ] + }, + { + "jobId": "68", + "time": "2014-09-24T16:00:01.000+02:00", + "performedBy": "Claudio", + "type": "mt", + "assignment": "NC Meeting", + "productId": "4", + "operation": "start", + "prerequisites": [ + { + "type": "Go to NC meeting" + } + ] + }, + { + "jobId": "66", + "time": "2014-09-24T17:24:30.000+02:00", + "performedBy": "Giovanni", + "type": "mt", + "assignment": "NC Meeting", + "productId": "4", + "operation": "finish" + }, + { + "jobId": "67", + "time": "2014-09-24T17:24:30.000+02:00", + "performedBy": "Cristiana", + "type": "mt", + "assignment": "NC Meeting", + "productId": "4", + "operation": "finish" + }, + { + "jobId": "64", + "time": "2014-09-24T17:49:30.000+02:00", + "performedBy": "Marcelo", + "type": "pm", + "assignment": "NC Meeting", + "productId": "4", + "operation": "finish" + }, + { + "jobId": "65", + "time": "2014-09-24T17:49:30.000+02:00", + "performedBy": "Pascale", + "type": "mt", + "assignment": "NC Meeting", + "productId": "4", + "operation": "finish" + }, + { + "jobId": "68", + "time": "2014-09-24T17:49:30.000+02:00", + "performedBy": "Claudio", + "type": "mt", + "assignment": "NC Meeting", + "productId": "4", + "operation": "finish" + }, + { + "jobId": "999", + "time": "2014-09-24T18:00:00.000+02:00", + "performedBy": "global", + "type": "global", + "assignment": "", + "productId": "", + "operation": "endOfDay" + }, + { + "jobId": "999", + "time": "2014-09-25T08:30:00.000+02:00", + "performedBy": "global", + "type": "global", + "assignment": "", + "productId": "", + "operation": "startOfDay" + }, + { + "jobId": "1", + "time": "2014-09-25T09:05:23.000+02:00", + "performedBy": "Biagio", + "type": "worker", + "assignment": "Produce Coffeemaker", + "productId": "1", + "operation": "start" + }, + { + "jobId": "2", + "time": "2014-09-25T09:05:23.000+02:00", + "performedBy": "Biagio", + "type": "worker", + "assignment": "Kitting Coffeemaker", + "productId": "1", + "operation": "start" + }, + { + "jobId": "2", + "time": "2014-09-25T10:44:23.000+02:00", + "performedBy": "Biagio", + "type": "worker", + "assignment": "Kitting Coffeemaker", + "productId": "1", + "operation": "finish" + }, + { + "jobId": "3", + "time": "2014-09-25T10:44:56.000+02:00", + "performedBy": "Biagio", + "type": "worker", + "assignment": "Assemble Coffeemaker", + "productId": "1", + "operation": "start" + }, + { + "jobId": "23", + "time": "2014-09-25T13:10:29.000+02:00", + "performedBy": "Fredrico", + "type": "worker", + "assignment": "Assemble Coffeemaker", + "productId": "2", + "operation": "finish" + }, + { + "jobId": "28", + "time": "2014-09-25T13:10:29.000+02:00", + "performedBy": "Paolo", + "type": "rao", + "assignment": "Go to station", + "productId": "2", + "operation": "start" + }, + { + "jobId": "28", + "time": "2014-09-25T13:12:29.000+02:00", + "performedBy": "Paolo", + "type": "rao", + "assignment": "Go to station", + "productId": "2", + "operation": "finish" + }, + { + "jobId": "26", + "time": "2014-09-25T13:12:32.000+02:00", + "performedBy": "Paolo", + "type": "rao", + "assignment": "Inspect finished Coffeemaker", + "productId": "2", + "operation": "start", + "prerequisites": [ + { + "type": "Go to station", + "agentId": "Paolo" + } + ] + }, + { + "jobId": "3", + "time": "2014-09-25T13:45:21.000+02:00", + "performedBy": "Biagio", + "type": "worker", + "assignment": "Assemble Coffeemaker", + "productId": "1", + "operation": "pause" + }, + { + "jobId": "4", + "time": "2014-09-25T13:45:21.000+02:00", + "performedBy": "Paolo", + "type": "rao", + "assignment": "Go to station", + "productId": "1", + "operation": "start" + }, + { + "jobId": "26", + "time": "2014-09-25T13:47:37.000+02:00", + "performedBy": "Paolo", + "type": "rao", + "assignment": "Inspect finished Coffeemaker", + "productId": "2", + "operation": "pause", + "prerequisites": [ + { + "type": "Inspect potential NC", + "agentId": "Paolo" + } + ] + }, + { + "jobId": "4", + "time": "2014-09-25T13:54:02.000+02:00", + "performedBy": "Paolo", + "type": "rao", + "assignment": "Go to station", + "productId": "1", + "operation": "finish" + }, + { + "jobId": "5", + "time": "2014-09-25T13:54:03.000+02:00", + "performedBy": "Paolo", + "type": "rao", + "assignment": "Inspect potential NC", + "productId": "1", + "operation": "start", + "prerequisites": [ + { + "type": "Go to station", + "agentId": "Paolo" + } + ] + }, + { + "jobId": "5", + "time": "2014-09-25T14:01:02.000+02:00", + "performedBy": "Paolo", + "type": "rao", + "assignment": "Inspect potential NC", + "productId": "1", + "operation": "finish" + }, + { + "jobId": "3", + "time": "2014-09-25T14:01:23.000+02:00", + "performedBy": "Biagio", + "type": "worker", + "assignment": "Assemble Coffeemaker", + "productId": "1", + "operation": "resume" + }, + { + "jobId": "26", + "time": "2014-09-25T14:12:02.000+02:00", + "performedBy": "Paolo", + "type": "rao", + "assignment": "Inspect finished Coffeemaker", + "productId": "2", + "operation": "resume" + }, + { + "jobId": "26", + "time": "2014-09-25T14:32:01.000+02:00", + "performedBy": "Paolo", + "type": "rao", + "assignment": "Inspect finished Coffeemaker", + "productId": "2", + "operation": "finish" + }, + { + "jobId": "27", + "time": "2014-09-25T14:32:01.000+02:00", + "performedBy": "Fredrico", + "type": "worker", + "assignment": "Transport to delivery", + "productId": "2", + "operation": "start" + }, + { + "jobId": "27", + "time": "2014-09-25T14:49:12.000+02:00", + "performedBy": "Fredrico", + "type": "worker", + "assignment": "Transport to delivery", + "productId": "2", + "operation": "finish" + }, + { + "jobId": "21", + "time": "2014-09-25T14:49:12.000+02:00", + "performedBy": "Fredrico", + "type": "worker", + "assignment": "Produce Coffeemaker", + "productId": "2", + "operation": "finish" + }, + { + "jobId": "999", + "time": "2014-09-25T18:00:00.000+02:00", + "performedBy": "global", + "type": "global", + "assignment": "", + "productId": "", + "operation": "endOfDay" + }, + { + "jobId": "999", + "time": "2014-09-26T08:30:00.000+02:00", + "performedBy": "global", + "type": "global", + "assignment": "", + "productId": "", + "operation": "startOfDay" + }, + { + "jobId": "53", + "time": "2014-09-26T09:10:39.000+02:00", + "performedBy": "Francesco", + "type": "worker", + "assignment": "Assemble Coffeemaker", + "productId": "4", + "operation": "resume" + }, + { + "jobId": "3", + "time": "2014-09-26T10:13:49.000+02:00", + "performedBy": "Biagio", + "type": "worker", + "assignment": "Assemble Coffeemaker", + "productId": "1", + "operation": "pause" + }, + { + "jobId": "6", + "time": "2014-09-26T10:13:49.000+02:00", + "performedBy": "Paolo", + "type": "rao", + "assignment": "Go to station", + "productId": "1", + "operation": "start" + }, + { + "jobId": "6", + "time": "2014-09-26T10:25:23.000+02:00", + "performedBy": "Paolo", + "type": "rao", + "assignment": "Go to station", + "productId": "1", + "operation": "finish" + }, + { + "jobId": "7", + "time": "2014-09-26T10:25:24.000+02:00", + "performedBy": "Paolo", + "type": "rao", + "assignment": "Inspect potential NC", + "productId": "1", + "operation": "start", + "prerequisites": [ + { + "type": "Go to station", + "agentId": "Paolo" + } + ] + }, + { + "jobId": "7", + "time": "2014-09-26T10:32:42.000+02:00", + "performedBy": "Paolo", + "type": "rao", + "assignment": "Inspect potential NC", + "productId": "1", + "operation": "finish" + }, + { + "jobId": "9", + "time": "2014-09-26T10:32:42.000+02:00", + "performedBy": "Marcelo", + "type": "pm", + "assignment": "Go to station", + "productId": "1", + "operation": "start" + }, + { + "jobId": "9", + "time": "2014-09-26T10:58:32.000+02:00", + "performedBy": "Marcelo", + "type": "pm", + "assignment": "Go to station", + "productId": "1", + "operation": "finish" + }, + { + "jobId": "10", + "time": "2014-09-26T10:58:33.000+02:00", + "performedBy": "Marcelo", + "type": "pm", + "assignment": "Discuss potential NC", + "productId": "1", + "operation": "start", + "prerequisites": [ + { + "type": "Go to station", + "agentId": "Marcelo" + } + ] + }, + { + "jobId": "10", + "time": "2014-09-26T11:05:12.000+02:00", + "performedBy": "Marcelo", + "type": "pm", + "assignment": "Discuss potential NC", + "productId": "1", + "operation": "finish" + }, + { + "jobId": "11", + "time": "2014-09-26T11:05:12.000+02:00", + "performedBy": "Paolo", + "type": "rao", + "assignment": "Organise drilling rework", + "productId": "1", + "operation": "start", + "prerequisites": [ + { + "type": "Discuss potential NC" + } + ] + }, + { + "jobId": "11", + "time": "2014-09-26T13:25:18.000+02:00", + "performedBy": "Paolo", + "type": "rao", + "assignment": "Organise drilling rework", + "productId": "1", + "operation": "finish" + }, + { + "jobId": "8", + "time": "2014-09-26T13:27:58.000+02:00", + "performedBy": "Fredrico", + "type": "worker", + "assignment": "Drilling rework", + "productId": "1", + "operation": "start", + "prerequisites": [ + { + "type": "Organise drilling rework" + } + ] + }, + { + "jobId": "53", + "time": "2014-09-26T14:13:39.000+02:00", + "performedBy": "Francesco", + "type": "worker", + "assignment": "Assemble Coffeemaker", + "productId": "4", + "operation": "finish" + }, + { + "jobId": "69", + "time": "2014-09-26T14:13:39.000+02:00", + "performedBy": "Paolo", + "type": "rao", + "assignment": "Go to station", + "productId": "4", + "operation": "start" + }, + { + "jobId": "69", + "time": "2014-09-26T14:19:39.000+02:00", + "performedBy": "Paolo", + "type": "rao", + "assignment": "Go to station", + "productId": "4", + "operation": "finish" + }, + { + "jobId": "70", + "time": "2014-09-26T14:19:56.000+02:00", + "performedBy": "Paolo", + "type": "rao", + "assignment": "Inspect finished Coffeemaker", + "productId": "4", + "operation": "start", + "prerequisites": [ + { + "type": "Go to station", + "agentId": "Paolo" + } + ] + }, + { + "jobId": "70", + "time": "2014-09-26T17:13:39.000+02:00", + "performedBy": "Paolo", + "type": "rao", + "assignment": "Inspect finished Coffeemaker", + "productId": "4", + "operation": "finish" + }, + { + "jobId": "71", + "time": "2014-09-26T17:13:39.000+02:00", + "performedBy": "Francesco", + "type": "worker", + "assignment": "Transport to delivery", + "productId": "4", + "operation": "start" + }, + { + "jobId": "71", + "time": "2014-09-26T17:29:39.000+02:00", + "performedBy": "Francesco", + "type": "worker", + "assignment": "Transport to delivery", + "productId": "4", + "operation": "finish" + }, + { + "jobId": "51", + "time": "2014-09-26T17:29:39.000+02:00", + "performedBy": "Francesco", + "type": "worker", + "assignment": "Produce Coffeemaker", + "productId": "4", + "operation": "finish" + }, + { + "jobId": "8", + "time": "2014-09-26T17:45:21.000+02:00", + "performedBy": "Fredrico", + "type": "worker", + "assignment": "Drilling rework", + "productId": "1", + "operation": "finish" + }, + { + "jobId": "999", + "time": "2014-09-26T18:00:00.000+02:00", + "performedBy": "global", + "type": "global", + "assignment": "", + "productId": "", + "operation": "endOfDay" + }, + { + "jobId": "999", + "time": "2014-09-29T08:30:00.000+02:00", + "performedBy": "global", + "type": "global", + "assignment": "", + "productId": "", + "operation": "startOfDay" + }, + { + "jobId": "3", + "time": "2014-09-29T09:01:23.000+02:00", + "performedBy": "Biagio", + "type": "worker", + "assignment": "Assemble Coffeemaker", + "productId": "1", + "operation": "resume" + }, + { + "jobId": "3", + "time": "2014-09-29T12:11:34.000+02:00", + "performedBy": "Biagio", + "type": "worker", + "assignment": "Assemble Coffeemaker", + "productId": "1", + "operation": "finish" + }, + { + "jobId": "12", + "time": "2014-09-29T12:11:34.000+02:00", + "performedBy": "Paolo", + "type": "rao", + "assignment": "Go to station", + "productId": "1", + "operation": "start" + }, + { + "jobId": "12", + "time": "2014-09-29T12:12:34.000+02:00", + "performedBy": "Paolo", + "type": "rao", + "assignment": "Go to station", + "productId": "1", + "operation": "finish" + }, + { + "jobId": "13", + "time": "2014-09-29T12:12:35.000+02:00", + "performedBy": "Paolo", + "type": "rao", + "assignment": "Inspect finished Coffeemaker", + "productId": "1", + "operation": "start", + "prerequisites": [ + { + "type": "Go to station", + "agentId": "Paolo" + } + ] + }, + { + "jobId": "13", + "time": "2014-09-29T14:01:32.000+02:00", + "performedBy": "Paolo", + "type": "rao", + "assignment": "Inspect finished Coffeemaker", + "productId": "1", + "operation": "finish" + }, + { + "jobId": "14", + "time": "2014-09-29T14:01:32.000+02:00", + "performedBy": "Biagio", + "type": "worker", + "assignment": "Transport to delivery", + "productId": "1", + "operation": "start" + }, + { + "jobId": "14", + "time": "2014-09-29T15:34:10.000+02:00", + "performedBy": "Biagio", + "type": "worker", + "assignment": "Transport to delivery", + "productId": "1", + "operation": "finish" + }, + { + "jobId": "1", + "time": "2014-09-29T15:34:11.000+02:00", + "performedBy": "Biagio", + "type": "worker", + "assignment": "Produce Coffeemaker", + "productId": "1", + "operation": "finish" + } + ]; + +} + +// extend the eve.Agent prototype +EventGenerator.prototype = Object.create(eve.Agent.prototype); +EventGenerator.prototype.constructor = EventGenerator; + +// define RPC functions, preferably in a separated object to clearly distinct +// exposed functions from local functions. +EventGenerator.prototype.rpcFunctions = {}; + +EventGenerator.prototype.rpcFunctions.loadEvents = function() { + return this.events.length - 1; +} + +EventGenerator.prototype.rpcFunctions.nextEvent = function() { + this.rpc.request("agentGenerator",{method:'receiveEvent', params:this.events[this.eventCounter]}).done(); + this.eventCounter += 1; +} + +if (typeof window === 'undefined') { + module.exports = EventGenerator; +} \ No newline at end of file diff --git a/showcase/projects/midas/arumAgents/genericAgent.js b/showcase/projects/midas/arumAgents/genericAgent.js new file mode 100644 index 00000000..74b3efc3 --- /dev/null +++ b/showcase/projects/midas/arumAgents/genericAgent.js @@ -0,0 +1,85 @@ +'use strict'; + +if (typeof window === 'undefined') { + var eve = require('evejs'); +} + +function GenericAgent(id, type) { + // execute super constructor + eve.Agent.call(this, id); + + this.id = id; + this.rpc = this.loadModule('rpc', this.rpcFunctions); + this.connect(eve.system.transports.getAll()); + this.type = type; + this.jobs = new JobManager(this); + this.timelineDataset = timelineItems; + + timelineGroups.add({id:id, content:type + ": " + id, className: 'timelineGroup ' + type}); + + this.availableSubgroups = [0,1,2,3,4,5,6,7,8,9,10]; + this.freeSubgroups = {}; + for (var i = 0; i < this.availableSubgroups.length; i++) { + this.freeSubgroups[this.availableSubgroups[i]] = true; + } + this.usedSubgroups = {}; +} + +// extend the eve.Agent prototype +GenericAgent.prototype = Object.create(eve.Agent.prototype); +GenericAgent.prototype.constructor = GenericAgent; + +// define RPC functions, preferably in a separated object to clearly distinct +// exposed functions from local functions. +GenericAgent.prototype.rpcFunctions = {}; + +GenericAgent.prototype.allocateSubgroup = function(type) { + for (var i = 0; i < this.availableSubgroups.length; i++) { + if (this.freeSubgroups[this.availableSubgroups[i]] == true) { + this.usedSubgroups[type] = i; + this.freeSubgroups[this.availableSubgroups[i]] = false; + break; + } + } +}; + +GenericAgent.prototype.freeSubgroup = function(type) { + this.freeSubgroups[this.usedSubgroups[type]] = true; + delete this.usedSubgroups[type]; +}; + +GenericAgent.prototype.newAssignment = function(id, type, time, prerequisites) { + this.allocateSubgroup(type); + this.jobs.add(id, type, time, prerequisites); +}; + +/** + * @param id + * @param time + * @param type + */ +GenericAgent.prototype.finishAssignment = function(id, type, time) { + this.jobs.finish(id, type, time); +}; + + +GenericAgent.prototype.updateAssignment = function(id, type, time, operation) { + this.jobs.update(id, type, time, operation); +}; + +GenericAgent.prototype.rpcFunctions.newEvent = function(params) { + // handle events + if (params.operation == 'start') { + this.newAssignment(params.jobId, params.assignment, params.time, params.prerequisites) + } + else if (params.operation == 'finish') { + this.finishAssignment(params.jobId, params.assignment, params.time); + } + else if (params.operation == 'pause' || params.operation == 'resume') { + this.updateAssignment(params.jobId,params.assignment,params.time, params.operation); + } +}; + +if (typeof window === 'undefined') { + module.exports = GenericAgent; +} \ No newline at end of file diff --git a/showcase/projects/midas/arumAgents/job.js b/showcase/projects/midas/arumAgents/job.js new file mode 100644 index 00000000..f04e0fc8 --- /dev/null +++ b/showcase/projects/midas/arumAgents/job.js @@ -0,0 +1,116 @@ +'use strict'; + +function uuid() { + return (Math.random()*1e15).toString(32) + "-" + (Math.random()*1e15).toString(32); +} + +/** + * This is a local assignment, this keeps track on how long an assignment takes THIS worker. + * + * @param id + * @param type + * @param timeStart + * @constructor + */ +function Job(id, type, timeStart, agentId, prerequisites) { + this.id = id; + this.type = type; + this.agentId = agentId; + + this.timeStart = timeStart; + this.timeResumed = timeStart; + this.timePaused = 0; + this.elapsedTime = 0; + this.elapsedTimeWithPause = 0; + this.endOfDayPause = false; + + this.paused = false; + this.finished = false; + + this.duration = new DurationData(); + this.prediction = new DurationStats(); + this.startupTime = new DurationData(); + this.predictedStartupTime = new DurationStats(); + + this.prerequisites = prerequisites; +} + +Job.prototype.prerequisiteFinished = function(params) { + var uuid = params.uuid; + for (var i = 0; i < this.prerequisites.length; i++) { + var prereq = this.prerequisites[i]; + if (prereq.uuid == uuid) { + prereq.times.setData(params.duration); + break; + } + } +}; + +Job.prototype.watchingPrerequisite = function(preliminaryStats, uuid) { + for (var i = 0; i < this.prerequisites.length; i++) { + var prereq = this.prerequisites[i]; + if (prereq.uuid == uuid) { + prereq.stats.setData(preliminaryStats); + this.predictedStartupTime.useHighest(preliminaryStats); + break; + } + } +}; + +Job.prototype.finalizePrerequisites = function() { + for (var i = 0; i < this.prerequisites.length; i++) { + this.startupTime.useHighest(this.prerequisites[i].times); + } +}; + +Job.prototype.finish = function(time) { + this.finished = true; + this.elapsedTime += new Date(time).getTime() - new Date(this.timeResumed).getTime(); + this.elapsedTimeWithPause += new Date(time).getTime() - new Date(this.timeResumed).getTime(); + this.finalizePrerequisites(); + + this.duration.calculateDuration(time, this.timeStart, this.elapsedTime, this.elapsedTimeWithPause, this.startupTime); +}; + +Job.prototype.pause = function(time, endOfDay) { + // if this is the endOfDay AND the job is paused, count the pause time and set the endOfDay pause to true + if (endOfDay == true && this.paused == true) { + this.elapsedTimeWithPause += new Date(time).getTime() - new Date(this.timePaused).getTime(); + this.endOfDayPause = true; + } + // if this is the endOfDay AND the job is NOT paused, pause the job, increment the timers + else if (endOfDay == true && this.paused == false) { + this.elapsedTimeWithPause += new Date(time).getTime() - new Date(this.timeResumed).getTime(); + this.elapsedTime += new Date(time).getTime() - new Date(this.timeResumed).getTime(); + this.endOfDayPause = true; + } + else if (this.paused == false) { + this.elapsedTimeWithPause += new Date(time).getTime() - new Date(this.timeResumed).getTime(); + this.elapsedTime += new Date(time).getTime() - new Date(this.timeResumed).getTime(); + this.timePaused = time; + this.paused = true; + } +}; + +Job.prototype.resume = function(time, startOfDay) { + // if the job was paused because of the endOfDay, resume it and set the timeResumed to now + if (this.endOfDayPause == true && startOfDay == true && this.paused == false) { + this.timeResumed = time; + this.endOfDayPause = false; + } + // if the job was paused before the endOfDay, keep it paused, but set the paused time to now. + else if (this.endOfDayPause == true && startOfDay == true && this.paused == true) { + this.timePaused = time; + this.endOfDayPause = false; + } + // if this is NOT the start of day and the job was paused, resume job, increment + else if (startOfDay == false && this.paused == true) { + this.elapsedTimeWithPause += new Date(time).getTime() - new Date(this.timePaused).getTime(); + this.timeResumed = time; + this.paused = false; + } + + +}; + + diff --git a/showcase/projects/midas/arumAgents/jobAgent.js b/showcase/projects/midas/arumAgents/jobAgent.js new file mode 100644 index 00000000..b382791d --- /dev/null +++ b/showcase/projects/midas/arumAgents/jobAgent.js @@ -0,0 +1,434 @@ +'use strict'; + + + +function JobAgent(id) { + // execute super constructor + eve.Agent.call(this, id); + this.rpc = this.loadModule('rpc', this.rpcFunctions); + this.connect(eve.system.transports.getAll()); + + this.id = id; + this.type = this.id.replace('job_',''); + this.globalStats = new DurationStats(); + this.globalStats.getFakeStats(id); + + this.agentStats = {}; + + this.allJobs = {}; // used to quickly look up a job ID + this.openJobs = {}; // used to check if there is a watcher to + this.closedJobs = {};// keeps a list of agentIds containing jobs, used for stats collection + // optimization would be nice with running averages, but N samples are needed, wait for demo data. + + this.watchers = {}; +} + +// extend the eve.Agent prototype +JobAgent.prototype = Object.create(eve.Agent.prototype); +JobAgent.prototype.constructor = JobAgent; + +// define RPC functions, preferably in a separated object to clearly distinct +// exposed functions from local functions. +JobAgent.prototype.rpcFunctions = {}; + + +JobAgent.prototype.expandPrerequisites = function(prerequisites) { + var expanded = []; + if (prerequisites !== undefined) { + for (var i = 0; i < prerequisites.length; i++) { + var prereq = prerequisites[i]; + if (typeof prereq == 'string') { + expanded.push({ + jobId: prereq, + uuid: uuid(), + times : new DurationData(), + stats: new DurationStats() + }); + } + else if (typeof prereq == 'object' && prereq.type !== undefined) { //&& prereq.agentId !== undefined not needed since the same items will be added when only type exists + prereq.uuid = uuid(); + prereq.times = new DurationData(); + prereq.stats = new DurationStats(); + expanded.push(prereq); + } + else { + console.log('ERROR: cannot use the prerequisites! Not in array of strings or array of objects with correct fields format.'); + throw new Error('ERROR: cannot use the prerequisites! Not in array of strings or array of objects with correct fields format.'); + } + } + } + return expanded; +}; +/** + * Create new Job for agent + * @param params + */ +JobAgent.prototype.rpcFunctions.add = function(params) { + var agentId = params.agentId; + var jobId = params.jobId; + + // create stats if not yet exits + if (this.agentStats[agentId] === undefined) { + this.agentStats[agentId] = new DurationStats(); + } + + // create open job + if (this.openJobs[agentId] === undefined) { + this.openJobs[agentId] = {}; + } + if (this.openJobs[agentId][jobId] !== undefined) { + console.log('cannot start new job, jobId:', jobId, ' already exists!'); + throw new Error('cannot start new job, jobId:' + jobId + ' already exists!'); + } + var prerequisites = this.expandPrerequisites(params.prerequisites); + this.openJobs[agentId][jobId] = new Job(jobId, this.id, params.time, agentId, prerequisites); + this.allJobs[jobId] = this.openJobs[agentId][jobId]; + this.addWatchers(jobId, prerequisites); + + + // return prediction + var statsData; + if (this.agentStats[agentId].duration.mean == 0) { + statsData = this.globalStats.getData(); + } + else { + statsData = this.agentStats[agentId].getData(); + } + return statsData; +}; + +/** + * finish the job of an agent + * @param params + */ +JobAgent.prototype.rpcFunctions.finish = function(params) { + var agentId = params.agentId; + var jobId = params.jobId; + var job = this.openJobs[agentId][jobId]; + + // finish job + job.finish(params.time); + // notify watchers that a job is finished. + if (this.watchers[jobId] !== undefined) { + for (var i = 0; i < this.watchers[jobId].length; i++) { + var val = this.watchers[jobId][i]; + var address = val.address; + var parentJobId = val.parentJobId; + var uuid = val.uuid; + this.rpc.request(address, {method:'watchedJobFinished', params:{ + uuid: uuid, + parentJobId: parentJobId, + duration: job.duration.getData() + }}).done(); + } + } + // cleanup watchers + delete this.watchers[jobId]; + + // move from open to closed jobs. + if (this.closedJobs[agentId] === undefined) { + this.closedJobs[agentId] = {}; + } + if (this.closedJobs[agentId][jobId] !== undefined) { + console.log('cannot close job, jobId:', jobId, ' already exists!'); + throw new Error('cannot close job, jobId:' + jobId + ' already exists!'); + } + this.closedJobs[agentId][jobId] = this.openJobs[agentId][jobId]; + + delete this.openJobs[agentId][jobId]; + + this.updateStats(); + + return { + elapsedTime: this.closedJobs[agentId][jobId].elapsedTime, + elapsedTimeWithPause: this.closedJobs[agentId][jobId].elapsedTimeWithPause, + duration: this.closedJobs[agentId][jobId].duration.getData(), + prediction: this.globalStats.getData() + }; +}; + +/** + * update the job of an agent + * @param params + */ +JobAgent.prototype.rpcFunctions.update = function(params) { + var agentId = params.agentId; + var jobId = params.jobId; + var job = this.openJobs[agentId][jobId]; + var operation = params.operation; + + switch (operation) { + case 'pause': + job.pause(params.time, false); + break; + case 'endOfDay': + job.pause(params.time, true); + break; + case 'startOfDay': + job.resume(params.time, true); + break; + case 'resume': + job.resume(params.time, false); + break; + } + return {jobId: jobId, type: this.type, elapsedTime: job.elapsedTime, elapsedTimeWithPause: job.elapsedTimeWithPause}; +}; + +/** + * return agent stats + * @param params + * @returns {*} + */ +JobAgent.prototype.rpcFunctions.getAgentStats = function(params) { + return this.agentStats[params.agentId]; +}; + + +/** + * return global stats + * @param params + * @returns {{mean: number, std: number}|*} + */ +JobAgent.prototype.rpcFunctions.getGlobalStats = function(params) { + return this.globalStats; +}; + +JobAgent.prototype.rpcFunctions.watchedJobFinished = function(params) { + var jobId = params.parentJobId; + this.allJobs[jobId].prerequisiteFinished(params); +}; + + +/** + * + * @param params + * @param sender + * @returns {*} + */ +JobAgent.prototype.rpcFunctions.addWatcherOnJobId = function(params, sender) { + var jobId = params.jobId; + var uuid = params.uuid; + var parentJobId = params.parentJobId; + var job = this.allJobs[jobId]; + + // if the job is already finished, call the finished callback + if (job.finished == true) { + this.rpc.request(sender, {method:'watchedJobFinished', params:{ + uuid: uuid, + parentJobId: parentJobId, + duration: job.duration.getData() // we need the pure json data, not the class + }}).done(); + } + else { + // we will create a watcher on a job which will alert the watcher when the job is finished with all the times. + if (this.watchers[jobId] === undefined) { + this.watchers[jobId] = []; + } + this.watchers[jobId].push({address: params.address, uuid: uuid}); + } + + // return the best prediction we have + if (this.agentStats[job.agentId].mean == 0) { + return this.globalStats.getData(); // we need the pure json data, not the class + } + return this.agentStats[job.agentId].getData(); // we need the pure json data, not the class +}; + + +/** + * + * @param params + * @param sender + * @returns {*} + */ +JobAgent.prototype.rpcFunctions.addWatcherByAgentID = function(params, sender) { + var agentId = params.agentId; + var parentJobId = params.parentJobId; + var jobId = null; + var uuid = params.uuid; + var returnStats; + + // see which statistics collection we will need to return. + if (this.agentStats[agentId].duration.mean == 0) { + returnStats = this.globalStats; + } + else { + returnStats = this.agentStats[agentId]; + } + // see if we have an open job with that agent of this type + if (this.openJobs[agentId] !== undefined) { + for (var jId in this.openJobs[agentId]) { + if (this.openJobs[agentId].hasOwnProperty(jId)) { + jobId = jId; + break; + } + } + } + // there is no open job from supplied agent of this type. return the mean of the return stats + if (jobId === null) { + this.rpc.request(params.address, {method:'watchedJobFinished', params:{ + uuid: uuid, + parentJobId: parentJobId, + duration: returnStats.getMeanData(), // we need the pure json data, not the class + oldData: true + }}).done(); + } + else { + params.jobId = jobId; + this.rpcFunctions.addWatcherOnJobId.call(this, params, sender); + } + + // return the best prediction we have + return returnStats.getData(); // we need the pure json data, not the class +}; + +/** + * + * @param params + * @param sender + * @returns {*} + */ +JobAgent.prototype.rpcFunctions.addWatcherByType = function(params, sender) { + // since we cannot watch a global type, we return the global stats at that point. + this.rpc.request(params.address, {method:'watchedJobFinished', params:{ + uuid: params.uuid, + parentJobId: params.parentJobId, + duration: this.globalStats.getMeanData(), // we need the pure json data, not the class + oldData: true + }}).done(); + return this.globalStats.getData(); // we need the pure json data, not the class +}; + + +/** + * + * @param parentJobId | ID from the job that wants to WATCH other jobs + * @param prerequisites + */ +JobAgent.prototype.addWatchers = function(parentJobId, prerequisites) { + for (var i = 0; i < prerequisites.length; i++) { + var prereq = prerequisites[i]; + var params = { + uuid: prereq.uuid, + address: this.id, // this is the callback address + parentJobId: parentJobId// this is the job that wants to watch the other one. + }; + var me = this; + if (prereq.jobId !== undefined) { + // we now have a parentJobId to watch + // we first need to find the type of job this belongs to. + this.rpc.request('JobAgentGenerator', {method: 'returnJobAddress', params: {jobId: prereq.jobId}}) + // now that we have an address, set a watcher on the job id + .done(function (address) { + if (address != 'doesNotExist') { + params.jobId = prereq.jobId; // this is the job we want to watch + me.rpc.request(address, {method: 'addWatcherOnJobId', params: params}) + .done(function (preliminaryStats) { + me.allJobs[parentJobId].watchingPrerequisite(preliminaryStats, prereq.uuid); + }) + } + else { + console.log('ERROR: watch job does not exist.'); + throw new Error('ERROR: watch job does not exist.'); + } + }); + } + else if (prereq.agentId !== undefined && prereq.type !== undefined) { + // we now have an agentId and a jobType to watch. + params.agentId = prereq.agentId; // this is the job we want to watch + this.rpc.request(prereq.type, {method: 'addWatcherByAgentID', params: params}) + .done(function (preliminaryStats) { + me.allJobs[parentJobId].watchingPrerequisite(preliminaryStats, prereq.uuid); + }) + } + else if (prereq.type !== undefined) { + this.rpc.request(prereq.type, {method: 'addWatcherByType', params: params}) + .done(function (preliminaryStats) { + me.allJobs[parentJobId].watchingPrerequisite(preliminaryStats, prereq.uuid); + }) + } + } +}; + +JobAgent.prototype.updatePredictedStartup = function(jobId, prediction) { + var jobPrediction = this.allJobs[jobId].predictedStartupTime; + jobPrediction.mean = Math.max(jobPrediction.mean, prediction.mean); + jobPrediction.std = Math.sqrt(Math.pow(jobPrediction.std,2) + Math.pow(prediction.std,2)); + + this.allJobs[jobId].prerequisitesCount += 1; +}; + + +/** + * Update all statistics + * + */ +JobAgent.prototype.updateStats = function() { + this.globalStats.clearStats(); + + var count = 0; + for (var agentId in this.closedJobs) { + if (this.closedJobs.hasOwnProperty(agentId)) { + var collection = this.closedJobs[agentId]; + // could be optimised with rolling average for efficient memory management + this.agentStats[agentId].setData(this.updateStatsIn(collection)); + this.globalStats.sumStats(this.agentStats[agentId]); + count += 1; + } + } + this.globalStats.averageStats(count); +}; + + +/** + * + * @param collection + * @returns {{duration: *, durationWithPause: *, durationWithStartup: *, durationWithBoth: *}} + */ +JobAgent.prototype.updateStatsIn = function(collection) { + var stats = {}; + for (var i = 0; i < this.globalStats.fields.length; i++) { + var field = this.globalStats.fields[i]; + stats[field] = this.collectStatsIn(collection, field); + } + return stats; +}; + + +JobAgent.prototype.collectStatsIn = function(collection, field) { + var total = 0; + var mean = 0; + var std = 0; + var minVal = 1e16; + var maxVal = 0; + var count = 0; + + for (var jobId in collection) { + if (collection.hasOwnProperty(jobId)) { + var value = collection[jobId].duration[field]; + maxVal = value > maxVal ? value : maxVal; + minVal = value < minVal ? value : minVal; + + total += collection[jobId].duration[field]; + count += 1; + } + } + if (count > 0) { + mean = total / count; + for (var jobId in collection) { + if (collection.hasOwnProperty(jobId)) { + std += Math.pow(collection[jobId].duration[field] - mean,2); + } + } + + std = Math.sqrt(std/count); + return {mean: mean, std: std, min: minVal, max: maxVal}; + } + else { + return {mean: 0, std: 0, min: 0, max: 0}; + } +}; + +JobAgent.prototype.hasJob = function(params) { + return this.allJobs[params.jobId] !== undefined; +}; diff --git a/showcase/projects/midas/arumAgents/jobAgentGenerator.js b/showcase/projects/midas/arumAgents/jobAgentGenerator.js new file mode 100644 index 00000000..8d3659f8 --- /dev/null +++ b/showcase/projects/midas/arumAgents/jobAgentGenerator.js @@ -0,0 +1,169 @@ +'use strict'; + +function JobAgentGenerator(id) { + // execute super constructor + eve.Agent.call(this, id); + this.rpc = this.loadModule('rpc', this.rpcFunctions); + this.connect(eve.system.transports.getAll()); +} + +// extend the eve.Agent prototype +JobAgentGenerator.prototype = Object.create(eve.Agent.prototype); +JobAgentGenerator.prototype.constructor = AgentGenerator; + +// define RPC functions, preferably in a separated object to clearly distinct +// exposed functions from local functions. +JobAgentGenerator.prototype.rpcFunctions = {}; + + +JobAgentGenerator.prototype.rpcFunctions.createJob = function(params) { + var jobAgentName = params.type; + if (jobList[jobAgentName] === undefined) { + jobList[jobAgentName] = new JobAgent(jobAgentName); + + graph2dGroups.add([ + { + id: jobAgentName+'_pred_duration_std_lower', + content: "prediction", + className: 'prediction_std', + options: {drawPoints:false} + }, + { + id: jobAgentName+'_pred_durationWithPause_std_lower', + content: "predWithPause", + className: 'prediction_std', + options: {drawPoints:false} + }, + { + id: jobAgentName+'_pred_durationWithStartup_std_lower', + content: "predWithStartup", + className: 'prediction_std', + options: {drawPoints:false} + }, + { + id: jobAgentName+'_pred_durationWithBoth_std_lower', + content: "predWithBoth", + className: 'prediction_std', + options: {drawPoints:false} + }, + { + id: jobAgentName+'_pred_duration_std_higher', + content: "prediction", + className: 'prediction_std', + options: {drawPoints:false} + }, + { + id: jobAgentName+'_pred_durationWithPause_std_higher', + content: "predWithPause", + className: 'prediction_std', + options: {drawPoints:false} + }, + { + id: jobAgentName+'_pred_durationWithStartup_std_higher', + content: "predWithStartup", + className: 'prediction_std', + options: {drawPoints:false} + }, + { + id: jobAgentName+'_pred_durationWithBoth_std_higher', + content: "predWithBoth", + className: 'prediction_std', + options: {drawPoints:false} + },{ + id: jobAgentName+'_pred_duration_original', + content: "prediction", + className: 'prediction_original' + }, + { + id: jobAgentName+'_pred_durationWithPause_original', + content: "predWithPause", + className: 'prediction_original' + }, + { + id: jobAgentName+'_pred_durationWithStartup_original', + content: "predWithStartup", + className: 'prediction_original' + }, + { + id: jobAgentName+'_pred_durationWithBoth_original', + content: "predWithBoth", + className: 'prediction_original' + }, + { + id: jobAgentName+'_pred_duration', + content: "prediction", + className: 'prediction' + }, + { + id: jobAgentName+'_pred_durationWithPause', + content: "predWithPause", + className: 'prediction' + }, + { + id: jobAgentName+'_pred_durationWithStartup', + content: "predWithStartup", + className: 'prediction' + }, + { + id: jobAgentName+'_pred_durationWithBoth', + content: "predWithBoth", + className: 'prediction' + }, + { + id: jobAgentName+'_duration', + content: "duration", + className: 'duration' + }, + { + id: jobAgentName+'_durationWithPause', + content: "durationWithPause", + className: 'duration' + }, + { + id: jobAgentName+'_durationWithStartup', + content: "durationWithStartup", + className: 'duration' + }, + { + id: jobAgentName+'_durationWithBoth', + content: "durationWithBoth", + className: 'duration' + } + ]); + var visibilityUpdate = {}; + //visibilityUpdate[jobAgentName+'_pred'] = false; + //visibilityUpdate[jobAgentName+'_predWithPause'] = false; + //visibilityUpdate[jobAgentName+'_predWithStartup'] = false; + //visibilityUpdate[jobAgentName+'_predWithBoth'] = false; + //visibilityUpdate[jobAgentName+'_duration'] = false; + //visibilityUpdate[jobAgentName+'_durationWithPause'] = false; + //visibilityUpdate[jobAgentName+'_durationWithStartup'] = false; + //visibilityUpdate[jobAgentName+'_durationWithBoth'] = false; + graph2d.setOptions({groups:{visible:visibilityUpdate}}); + refreshJobs(); + } +}; + +JobAgentGenerator.prototype.rpcFunctions.returnJobAddress = function(params) { + var instanceId = params.instanceId; + var hasJob = false; + for (var jobAgentName in jobList) { + if (jobList.hasOwnProperty(jobAgentName)) { + hasJob = jobList[jobAgentName].hasJob(instanceId); + if (hasJob == true) { + return jobAgentName; + } + } + } + return "doesNotExist"; +}; + +JobAgentGenerator.prototype.getAllJobNames = function() { + var list = []; + for (var jobAgentName in jobList) { + if (jobList.hasOwnProperty(jobAgentName)) { + list.push(jobAgentName); + } + } + return list; +}; diff --git a/showcase/projects/midas/arumAgents/jobManager.js b/showcase/projects/midas/arumAgents/jobManager.js new file mode 100644 index 00000000..7626c5ce --- /dev/null +++ b/showcase/projects/midas/arumAgents/jobManager.js @@ -0,0 +1,428 @@ +'use strict'; + +function JobManager(agent) { + this.agent = agent; + this.jobs = { + id:{}, + type:{ + open:{}, + closed:{} + } + }; + this.openJobs = {}; +} + +JobManager.prototype.add = function(id, type, time, prerequisites) { + var me = this; + + // create job agent. This agent will keep track of the global job stats. Jobs per type. + this.agent.rpc.request('jobAgentGenerator',{method:'createJob', params:{type:type}}).done(); + + this.jobs.id[id] = { + type: type, + startTime: time, + prediction: null, + predictionCounter: 0, + elapsedTime: 0, + elapsedTimeWithPause: 0, + pauseCount: 0, + endOfDay: false, + paused: false, + pauseAreaID: "" + }; + + // assign agent to job. + this.agent.rpc.request(type, {method:'add', params:{ + agentId: this.agent.id, + time:time, + jobId: id, + prerequisites: prerequisites + }}) + .done(function (prediction) { + if (prediction.duration.mean != 0) { + me.agent.timelineDataset.update({ + id: id + "_predMean0", + start: time, + end: new Date(time).getTime() + prediction.duration.mean, + group: me.agent.id, + type: 'range', + content: "", + subgroup: me.agent.usedSubgroups[type], + className: 'prediction' + }); + } + me.jobs.id[id].prediction = prediction; + }); + + if (this.jobs.type.open[type] === undefined) {this.jobs.type.open[type] = {}} + this.jobs.type.open[type][id] = time; + this.openJobs[id] = this.jobs.id[id]; + + + var addQuery = [{id:id, start:time, content:"started: "+ type, group:this.agent.id, subgroup: this.agent.usedSubgroups[type]}]; + this.agent.timelineDataset.add(addQuery); + this.agent.rpc.request("agentGenerator", {method: 'updateOpenJobs', params:{jobId: id, time: time}}).done(); +}; + +JobManager.prototype.finish = function(id, type, time) { + var me = this; + // finish the job. + this.agent.rpc.request(type, {method:'finish', params:{ + agentId: this.agent.id, + time: time, + jobId: id + }}) + .done(function (reply) { + var prediction = reply.prediction; + var originalPrediction = me.jobs.id[id].prediction; + me.jobs.id[id].elapsedTime = reply.elapsedTime; + me.jobs.id[id].elapsedTimeWithPause = reply.elapsedTimeWithPause; + me.updateDataSetsFinish(id, type, time, me.jobs.id[id].prediction); + + graph2dDataset.push({x: time, y: reply.duration.duration/3600000 ,group: type + '_duration', type: type}); + graph2dDataset.push({x: time, y: reply.duration.durationWithPause/3600000 ,group: type + '_durationWithPause', type: type}); + graph2dDataset.push({x: time, y: reply.duration.durationWithStartup/3600000 ,group: type + '_durationWithStartup', type: type}); + graph2dDataset.push({x: time, y: reply.duration.durationWithBoth/3600000 ,group: type + '_durationWithBoth', type: type}); + graph2dDataset.push({x: time, y: prediction.duration.mean/3600000 ,group: type + '_pred_duration', type: type}); + graph2dDataset.push({x: time, y: prediction.durationWithPause.mean/3600000 ,group: type + '_pred_durationWithPause', type: type}); + graph2dDataset.push({x: time, y: prediction.durationWithStartup.mean/3600000 ,group: type + '_pred_durationWithStartup', type: type}); + graph2dDataset.push({x: time, y: prediction.durationWithBoth.mean/3600000 ,group: type + '_pred_durationWithBoth', type: type}); + graph2dDataset.push({x: time, y: (prediction.duration.mean + prediction.duration.std)/3600000 ,group: type + '_pred_duration_std_higher', type: type}); + graph2dDataset.push({x: time, y: (prediction.durationWithPause.mean + prediction.durationWithPause.std)/3600000 ,group: type + '_pred_durationWithPause_std_higher', type: type}); + graph2dDataset.push({x: time, y: (prediction.durationWithStartup.mean + prediction.durationWithStartup.std)/3600000 ,group: type + '_pred_durationWithStartup_std_higher', type: type}); + graph2dDataset.push({x: time, y: (prediction.durationWithBoth.mean + prediction.durationWithBoth.std)/3600000 ,group: type + '_pred_durationWithBoth_std_higher', type: type}); + graph2dDataset.push({x: time, y: (prediction.duration.mean - prediction.duration.std)/3600000 ,group: type + '_pred_duration_std_lower', type: type}); + graph2dDataset.push({x: time, y: (prediction.durationWithPause.mean - prediction.durationWithPause.std)/3600000 ,group: type + '_pred_durationWithPause_std_lower', type: type}); + graph2dDataset.push({x: time, y: (prediction.durationWithStartup.mean - prediction.durationWithStartup.std)/3600000 ,group: type + '_pred_durationWithStartup_std_lower', type: type}); + graph2dDataset.push({x: time, y: (prediction.durationWithBoth.mean - prediction.durationWithBoth.std)/3600000 ,group: type + '_pred_durationWithBoth_std_lower', type: type}); + graph2dDataset.push({x: time, y: originalPrediction.duration.mean/3600000 ,group: type + '_pred_duration_original', type: type}); + graph2dDataset.push({x: time, y: originalPrediction.durationWithPause.mean/3600000 ,group: type + '_pred_durationWithPause_original', type: type}); + graph2dDataset.push({x: time, y: originalPrediction.durationWithStartup.mean/3600000 ,group: type + '_pred_durationWithStartup_original', type: type}); + graph2dDataset.push({x: time, y: originalPrediction.durationWithBoth.mean/3600000 ,group: type + '_pred_durationWithBoth_original', type: type}); + }); + + delete this.jobs.type.open[type][id]; + delete this.openJobs[id]; + if (this.jobs.type.closed[type] === undefined) {this.jobs.type.closed[type] = {}} + this.jobs.type.closed[type][id] = time; +}; + +JobManager.prototype.update = function(id, type, time, operation) { + var me = this; + var eventId = uuid(); + if (operation == 'endOfDay' || operation == 'startOfDay') { + for (var jobId in this.openJobs) { + if (this.openJobs.hasOwnProperty(jobId)) { + var type = this.openJobs[jobId].type; + this.agent.rpc.request(type, {method:'update', params:{ + agentId: this.agent.id, + time: time, + jobId: jobId, + operation: operation + }}) + .done(function (reply) { + me.jobs.id[reply.jobId].elapsedTime = reply.elapsedTime; + me.jobs.id[reply.jobId].elapsedTimeWithPause = reply.elapsedTimeWithPause; + me.updateDataSetsOperation(reply.jobId, reply.type, time, operation, eventId); + }); + + } + } + } + else { + this.agent.rpc.request(type, {method:'update', params:{ + agentId: this.agent.id, + time: time, + jobId: id, + operation: operation + }}) + .done(function (reply) { + me.jobs.id[id].elapsedTime = reply.elapsedTime; + me.jobs.id[id].elapsedTimeWithPause = reply.elapsedTimeWithPause; + me.updateDataSetsOperation(id, type, time, operation, eventId); + }); + } +}; + + +JobManager.prototype.updateDataSetsOperation = function(id, type, time, operation, eventId) { + switch (operation) { + case 'pause': + this.updateDataSetsPause(id, type, time, operation, this.jobs.id[id].prediction, eventId); + break; + case 'endOfDay': + this.updateDataSetsPause(id, type, time, operation, this.jobs.id[id].prediction, eventId); + break; + case 'startOfDay': + this.updateDataSetsResume(id, type, time, operation, this.jobs.id[id].prediction, eventId); + break; + case 'resume': + this.updateDataSetsResume(id, type, time, operation, this.jobs.id[id].prediction, eventId); + break; + } +}; + +JobManager.prototype.updateDataSetsFinish = function(id, type, time, prediction) { + var updateQuery = []; + var field = 'duration'; + var elapsedTime = this.jobs.id[id].elapsedTime; + + // gather statistic indicator data + if (this.jobs.id[id].pauseCount > 0) { + field = 'durationWithPause'; + elapsedTime = this.jobs.id[id].elapsedTimeWithPause; + } + + // generate indicator + var predictedTimeLeft = 0; + if (prediction[field].mean != 0) { + predictedTimeLeft = prediction[field].mean - elapsedTime; + this.updatePredictionOnFinish(id, type, time, prediction[field], elapsedTime); + } + + updateQuery.push({ + id: id, + end: time, + content: type, + type: 'range', + className: 'finished', + style: 'background-color: ' + this.generateColors(predictedTimeLeft, elapsedTime) + ' !important;' + }); + + this.agent.freeSubgroup(type); + this.agent.timelineDataset.update(updateQuery); + this.agent.rpc.request("agentGenerator", {method: 'updateOpenJobs', params:{jobId: id, time: time}}).done(); +}; + +JobManager.prototype.updateDataSetsPause = function(id, type, time, operation, prediction, eventId) { + var updateQuery = []; + var image = ''; + var flagId = id + "_pauseNotifier" + eventId; + var title = 'Calling RAO for possible NC'; + + // this causes there to be only one flag for the end of day as well as a moon icon + if (operation == 'endOfDay') { + // don't end-of-day jobs twice + if (this.jobs.id[id].endOfDay == true) { + return; + } + this.jobs.id[id].endOfDay = true; + image = ''; + flagId = id + "endOfDayNotifier" + eventId; + title = "End of working day" + } + else { + this.jobs.id[id].pauseCount += 1; + this.jobs.id[id].pauseAreaID = this.agent.id + "_" + "_pauseArea_" + eventId; + var shadedPauseArea = { + id: this.jobs.id[id].pauseAreaID, + start: time, + end: time, + content: '', + group: this.agent.id, + subgroup: this.agent.usedSubgroups[type], + className: 'pausedArea', + title: 'Job paused.' + }; + updateQuery.push(shadedPauseArea); + } + + var field = 'duration'; + var elapsedTime = this.jobs.id[id].elapsedTime; + if (this.jobs.id[id].pauseCount > 0) { + field = 'durationWithPause'; + elapsedTime = this.jobs.id[id].elapsedTimeWithPause; + } + + updateQuery.push({id: id, end: time, content: type, type: 'range'}); + var imageUpdate = { + id: flagId, + start: time, + end: time, + content: image, + group: this.agent.id, + subgroup: this.agent.usedSubgroups[type], + className: 'pause', + title: title + }; + + + if (this.agent.id == "Paolo") { + imageUpdate.title = "Going to inspect possible NC."; + } + updateQuery.push(imageUpdate); + + var predictedTimeLeft = prediction[field].mean - elapsedTime; + var predictionExists = prediction[field].mean != 0; + + // update the predicted line if the job is not ALREADY pauseds + if (predictedTimeLeft > 0 && predictionExists == true && this.jobs.id[id].paused != true) { + updateQuery.push({ + id: id + "_predMean" + this.jobs.id[id].predictionCounter, + end: time, + group: this.agent.id, + className: 'prediction' + }) + } + + // set the status to paused if needed + if (operation != 'endOfDay') { + this.jobs.id[id].paused = true; + } + this.agent.timelineDataset.update(updateQuery); + this.agent.rpc.request("agentGenerator", {method: 'updateOpenJobs', params:{jobId: id, time: time}}) +}; + +JobManager.prototype.updateDataSetsResume = function(id, type, time, operation, prediction, eventId) { + var updateQuery = []; + var image = ''; + var flagId = id + "_resumeNotifier" + eventId; + + // this causes there to be only one flag for the start of day as well as a sun icon + if (operation == 'startOfDay') { + // don't start-of-day jobs twice + if (this.jobs.id[id].endOfDay == false) { + return; + } + this.jobs.id[id].endOfDay = false; + image = ''; + flagId = id + "startOfDayNotifier_" + eventId; + } + else { + updateQuery.push({id: this.jobs.id[id].pauseAreaID, end: time, content: '', type: 'range'}); + this.jobs.id[id].pauseAreaID = ""; + this.jobs.id[id].pauseCount += 1; + } + + var field = 'duration'; + var elapsedTime = this.jobs.id[id].elapsedTime; + if (this.jobs.id[id].pauseCount > 0) { + field = 'durationWithPause'; + elapsedTime = this.jobs.id[id].elapsedTimeWithPause; + } + + updateQuery.push({id: id, end: time, content: type, type: 'range'}); + updateQuery.push({ + id: flagId, + start: time, + end: time, + content: image, + group: this.agent.id, + subgroup: this.agent.usedSubgroups[type], + className: 'pause', + title: 'Resuming job' + }); + + var predictedTimeLeft = prediction[field].mean - elapsedTime; + // no not increase the prediction line at the start of the day if the job is PAUSED + if (predictedTimeLeft > 0 && !(operation == 'startOfDay' && this.jobs.id[id].paused == true)) { + this.jobs.id[id].predictionCounter += 1; + updateQuery.push({ + id: id + "_predMean" + this.jobs.id[id].predictionCounter, + start: time, + end: new Date(time).getTime() + predictedTimeLeft, + group: this.agent.id, + content: "", + subgroup: this.agent.usedSubgroups[type], + type: 'range', + className: 'prediction' + }); + } + + // resume if needed + if (operation != 'startOfDay'){ + this.jobs.id[id].paused = false; + } + this.agent.timelineDataset.update(updateQuery); + this.agent.rpc.request("agentGenerator", {method: 'updateOpenJobs', params:{jobId: id, time: time}}) +}; + +JobManager.prototype.updatePredictionOnFinish = function(id,type,time,prediction,elapsedTime) { + if (prediction.mean != 0) { + var predictedTimeLeft = prediction.mean - elapsedTime; + if (predictedTimeLeft > 0) { + this.agent.timelineDataset.remove({id: id + "_predMean" + this.jobs.id[id].predictionCounter}) + } + } +}; + +JobManager.prototype.updateJobs = function(time, skipId) { + var updateQuery = []; + for (var jobId in this.openJobs) { + if (this.openJobs.hasOwnProperty(jobId) && jobId != skipId) { + var type = this.openJobs[jobId].type; + var prediction = this.openJobs[jobId].prediction; + var predictedTimeLeft = 0; + // never been paused before + if (this.jobs.id[jobId].pauseCount == 0) { + if (prediction !== null && prediction.duration !== null) { + predictedTimeLeft = prediction.duration.mean - this.jobs.id[jobId].elapsedTime; + } + } + else { + if (prediction !== null && prediction.durationWithPause !== null) { + predictedTimeLeft = prediction.durationWithPause.mean - this.jobs.id[jobId].elapsedTimeWithPause; + } + } + updateQuery.push({id: jobId, end: time, content: type, type: 'range'}); + if (this.openJobs[jobId].paused == true) { + updateQuery.push({id: this.openJobs[jobId].pauseAreaID, end: time}); + } + } + } + + this.agent.timelineDataset.update(updateQuery); +}; + + +JobManager.prototype.generateColors = function(predictedTime, elapsedTime) { + var ratio = (elapsedTime + predictedTime) / elapsedTime; + if (ratio > 1) { + ratio = Math.min(2,ratio) - 1; // 2 -- 1 + var rgb = HSVToRGB(94/360,ratio*0.6 + 0.2,1); + } + else { + ratio = Math.max(0.5,ratio) - 0.5; // 1 -- 0.5 + var rgb = HSVToRGB(40/360,(1-(2*ratio))*0.6 + 0.1 ,1); + } + return "rgb(" + rgb.r + "," + rgb.g + "," + rgb.b + ")"; +}; + +function HSVToRGB(h, s, v) { + var r, g, b; + + var i = Math.floor(h * 6); + var f = h * 6 - i; + var p = v * (1 - s); + var q = v * (1 - f * s); + var t = v * (1 - (1 - f) * s); + + switch (i % 6) { + case 0: r = v, g = t, b = p; break; + case 1: r = q, g = v, b = p; break; + case 2: r = p, g = v, b = t; break; + case 3: r = p, g = q, b = v; break; + case 4: r = t, g = p, b = v; break; + case 5: r = v, g = p, b = q; break; + } + + return {r:Math.floor(r * 255), g:Math.floor(g * 255), b:Math.floor(b * 255) }; +} +function RGBToHSV (red,green,blue) { + red=red/255; green=green/255; blue=blue/255; + var minRGB = Math.min(red,Math.min(green,blue)); + var maxRGB = Math.max(red,Math.max(green,blue)); + + // Black-gray-white + if (minRGB == maxRGB) { + return {h:0,s:0,v:minRGB}; + } + + // Colors other than black-gray-white: + var d = (red==minRGB) ? green-blue : ((blue==minRGB) ? red-green : blue-red); + var h = (red==minRGB) ? 3 : ((blue==minRGB) ? 1 : 5); + var hue = 60*(h - d/(maxRGB - minRGB))/360; + var saturation = (maxRGB - minRGB)/maxRGB; + var value = maxRGB; + return {h:hue,s:saturation,v:value}; +} \ No newline at end of file diff --git a/showcase/projects/midas/css/global.css b/showcase/projects/midas/css/global.css new file mode 100644 index 00000000..6f297980 --- /dev/null +++ b/showcase/projects/midas/css/global.css @@ -0,0 +1,301 @@ +body { + font-family : Verdana, "Bitstream Vera Sans", "DejaVu Sans", Tahoma, Geneva, Arial, Sans-serif; + font-size: 13px; + min-width:980px; +} + +div.group { + background-color: rgba(0, 0, 0, 0.005); +} + +/*.vis.timeline .item.background {*/ +/*background-color: rgba(40, 148, 255, 0.20) !important;*/ +/*z-index:9999 !important;*/ +/*}*/ +.vis.timeline .item.range.prediction { + border: 1px solid rgba(0, 164, 255, 0.50) !important; + background-color: rgba(28, 183, 255, 0.10) !important; + box-shadow: rgba(0, 83, 128, 0.15) 0px 0px 10px !important; + z-index:-1 !important; + height:34px !important; +} +.vis.timeline .item.background.night { + background-color: rgba(60, 60, 60, 0.3) !important; + z-index:9999 !important; +} +.vis.timeline .labelset .timelineGroup .inner { + /*height:100px !important;*/ + width:140px !important; +} + +.vis.timeline .labelset .timelineGroup.worker{ + background-color: #ffffff; +} +.vis.timeline .labelset .timelineGroup.rao{ + background-color: #f5f5f5; +} +.vis.timeline .labelset .timelineGroup.pm{ + background-color: #dfeaf2; +} +.vis.timeline .labelset .timelineGroup.mt{ + background-color: #b8d6e6; +} + +.vis.timeline .item.range { + height:34px !important; +} + +.vis.timeline .item.range.pause{ + height:34px !important; + width:26px !important; + box-shadow: rgba(0,0,0,0.2) 0px 0px 10px !important; + padding:3px !important; + z-index:999999; + background-color: #ffffff !important +} + +.vis.timeline .item.range.pausedArea{ + height:34px !important; + padding:3px !important; + z-index:999950; + border-width: 0px !important; + background: url('../images/shaded.png'); + background-color: rgba(0,0,0,0) !important; +} + +.vis.timeline .item { + border-radius: 0px !important; + border-color: #7d807d !important; + background-color: #ffffff !important; + /*background-color: #c4e0ff !important;*/ + +} +.vis.timeline .item.box { + height:22px !important; + border-radius: 0px !important; + border-color: #7d807d !important; + background-color: #ffffff !important; + box-shadow: rgba(0, 0, 0, 0.52) 0px 0px 12px 0px; +} + +img.icon{ + width:16px; + height:16px; +} + +#multiselect { + position:relative; + top:1px; + width:200px; + height:452px; +} + +table.wrapper { + position:relative; + height:500px; + width:100%; +} + +/*table.wrapper td {*/ + /*border:1px solid #ff0000;*/ +/*}*/ + +#selectTD { + width:200px; +} + +div.typeButtons { + height:50px; + width:100%; + overflow:hidden; +} + +div.typeButton { + display:inline-block; + width:150px; + height:24px; + border:2px solid #ffffff; + border-radius: 40px; + text-align:center; + font-size:12px; + color: #848484; + padding-top:8px; + background-color: #dedede; + -webkit-touch-callout: none; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + cursor:pointer; + margin:5px; + box-shadow: rgba(50,50,50,0.3) 0px 0px 3px; +} + +div.typeButton.selected { + background-color: #9aff4d; + color: #000000; +} + +div.toggleButton { + background-image: url("../images/toggleOff.png"); + background-repeat: no-repeat; + background-position: 2px 2px; + display:inline-block; + width:138px; + height:24px; + font-size:12px; + border:1px solid #d0d0d0; + text-align:left; + padding-top:10px; + padding-left:60px; + padding-right:0px; + border-radius: 40px; + background-color: #ffffff; + -webkit-touch-callout: none; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + cursor:pointer; +} + +div.toggleButton.square{ + height:28px; + border-radius: 0px; +} + +div.toggleButton.selected { + background-image: url("../images/toggleOn.png"); + background-color: #ffffff; +} + +div.graph2dContent{ + height:450px; + width:100%; +} + +#graphWrapper { + display:none; + border: 1px solid #dddddd; + padding:5px; +} + +#timelineWrapper { + display:block; + border: 1px solid #dddddd; + padding:5px; +} + +.prediction_original { + fill: rgb(123, 199, 255); + fill-opacity:0; + stroke-width:2px; + stroke: rgb(123, 199, 255); +} + +.prediction { + fill: #77da2e; + fill-opacity:0; + stroke-width:2px; + stroke: #77da2e; +} + +.duration { + fill: rgb(170, 34, 206); + fill-opacity:0; + stroke-width:2px; + stroke: rgb(170, 34, 206); +} + +.fill { + fill-opacity:0.1; + stroke: none; +} + + +.bar { + fill-opacity:0.5; + stroke-width:1px; +} + +.point { + stroke-width:2px; + fill-opacity:1.0; +} + + +.legendBackground { + stroke-width:1px; + fill-opacity:0.9; + fill: #ffffff; + stroke: #c2c2c2; +} + + +.outline { + stroke-width:1px; + fill-opacity:1; + fill: #ffffff; + stroke: #e5e5e5; +} + +.iconFill { + fill-opacity:0.3; + stroke: none; +} + +div.descriptionContainer { + float:left; + height:30px; + min-width:160px; + padding-left:5px; + padding-right:5px; + line-height: 30px; +} + +div.iconContainer { + float:left; +} + +div.legendElementContainer { + display:inline-block; + height:30px; + border-style:solid; + border-width:1px; + border-color: #d0d0d0; + background-color: #ffffff; + margin-right:6px; + padding:4px; + -webkit-touch-callout: none; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + cursor:pointer; +} + +svg.legendIcon { + width:30px; + height:30px; +} + +div.externalLegend { + position:relative; + top:1px; + width: 700px; +} + +.differencePositive { + fill: #afff33; + stroke-width:2px; + stroke: #96db22; +} + +.differenceNegative { + fill: #ff9f00; + stroke-width:2px; + stroke: #e58f00; +} \ No newline at end of file diff --git a/showcase/projects/midas/css/vis.css b/showcase/projects/midas/css/vis.css new file mode 100644 index 00000000..2bbe58fb --- /dev/null +++ b/showcase/projects/midas/css/vis.css @@ -0,0 +1,745 @@ +.vis .overlay { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + + /* Must be displayed above for example selected Timeline items */ + z-index: 10; +} + +.vis-active { + box-shadow: 0 0 10px #86d5f8; +} + +.vis.timeline { +} + + +.vis.timeline.root { + position: relative; + border: 1px solid #bfbfbf; + + overflow: hidden; + padding: 0; + margin: 0; + + box-sizing: border-box; +} + +.vis.timeline .vispanel { + position: absolute; + + padding: 0; + margin: 0; + + box-sizing: border-box; +} + +.vis.timeline .vispanel.center, +.vis.timeline .vispanel.left, +.vis.timeline .vispanel.right, +.vis.timeline .vispanel.top, +.vis.timeline .vispanel.bottom { + border: 1px #bfbfbf; +} + +.vis.timeline .vispanel.center, +.vis.timeline .vispanel.left, +.vis.timeline .vispanel.right { + border-top-style: solid; + border-bottom-style: solid; + overflow: hidden; +} + +.vis.timeline .vispanel.center, +.vis.timeline .vispanel.top, +.vis.timeline .vispanel.bottom { + border-left-style: solid; + border-right-style: solid; +} + +.vis.timeline .background { + overflow: hidden; +} + +.vis.timeline .vispanel > .content { + position: relative; +} + +.vis.timeline .vispanel .shadow { + position: absolute; + width: 100%; + height: 1px; + box-shadow: 0 0 10px rgba(0,0,0,0.8); + /* TODO: find a nice way to ensure shadows are drawn on top of items + z-index: 1; + */ +} + +.vis.timeline .vispanel .shadow.top { + top: -1px; + left: 0; +} + +.vis.timeline .vispanel .shadow.bottom { + bottom: -1px; + left: 0; +} + +.vis.timeline .labelset { + position: relative; + width: 100%; + + overflow: hidden; + + box-sizing: border-box; +} + +.vis.timeline .labelset .vlabel { + position: relative; + left: 0; + top: 0; + width: 100%; + color: #4d4d4d; + + box-sizing: border-box; +} + +.vis.timeline .labelset .vlabel { + border-bottom: 1px solid #bfbfbf; +} + +.vis.timeline .labelset .vlabel:last-child { + border-bottom: none; +} + +.vis.timeline .labelset .vlabel .inner { + display: inline-block; + padding: 5px; +} + +.vis.timeline .labelset .vlabel .inner.hidden { + padding: 0; +} + + +.vis.timeline .itemset { + position: relative; + padding: 0; + margin: 0; + + box-sizing: border-box; +} + +.vis.timeline .itemset .background, +.vis.timeline .itemset .foreground { + position: absolute; + width: 100%; + height: 100%; + overflow: visible; +} + +.vis.timeline .axis { + position: absolute; + width: 100%; + height: 0; + left: 0; + z-index: 1; +} + +.vis.timeline .foreground .group { + position: relative; + box-sizing: border-box; + border-bottom: 1px solid #bfbfbf; +} + +.vis.timeline .foreground .group:last-child { + border-bottom: none; +} + + +.vis.timeline .item { + position: absolute; + color: #1A1A1A; + border-color: #97B0F8; + border-width: 1px; + background-color: #D5DDF6; + display: inline-block; + padding: 5px; +} + +.vis.timeline .item.selected { + border-color: #FFC200; + background-color: #FFF785; + + /* z-index must be higher than the z-index of custom time bar and current time bar */ + z-index: 2; +} + +.vis.timeline .editable .item.selected { + cursor: move; +} + +.vis.timeline .item.point.selected { + background-color: #FFF785; +} + +.vis.timeline .item.box { + text-align: center; + border-style: solid; + border-radius: 2px; +} + +.vis.timeline .item.point { + background: none; +} + +.vis.timeline .item.dot { + position: absolute; + padding: 0; + border-width: 4px; + border-style: solid; + border-radius: 4px; +} + +.vis.timeline .item.range { + border-style: solid; + border-radius: 2px; + box-sizing: border-box; +} + +.vis.timeline .item.background { + overflow: hidden; + border: none; + background-color: rgba(213, 221, 246, 0.4); + box-sizing: border-box; + padding: 0; + margin: 0; +} + +.vis.timeline .item.range .content { + position: relative; + display: inline-block; + overflow: hidden; + max-width: 100%; +} + +.vis.timeline .item.background .content { + position: absolute; + display: inline-block; + overflow: hidden; + max-width: 100%; + margin: 5px; +} + +.vis.timeline .item.line { + padding: 0; + position: absolute; + width: 0; + border-left-width: 1px; + border-left-style: solid; +} + +.vis.timeline .item .content { + white-space: nowrap; + overflow: hidden; +} + +.vis.timeline .item .delete { + background: url('img/timeline/delete.png') no-repeat top center; + position: absolute; + width: 24px; + height: 24px; + top: 0; + right: -24px; + cursor: pointer; +} + +.vis.timeline .item.range .drag-left { + position: absolute; + width: 24px; + height: 100%; + top: 0; + left: -4px; + + cursor: w-resize; +} + +.vis.timeline .item.range .drag-right { + position: absolute; + width: 24px; + height: 100%; + top: 0; + right: -4px; + + cursor: e-resize; +} + +.vis.timeline .timeaxis { + position: relative; + overflow: hidden; +} + +.vis.timeline .timeaxis.foreground { + top: 0; + left: 0; + width: 100%; +} + +.vis.timeline .timeaxis.background { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; +} + +.vis.timeline .timeaxis .text { + position: absolute; + color: #4d4d4d; + padding: 3px; + white-space: nowrap; +} + +.vis.timeline .timeaxis .text.measure { + position: absolute; + padding-left: 0; + padding-right: 0; + margin-left: 0; + margin-right: 0; + visibility: hidden; +} + +.vis.timeline .timeaxis .grid.vertical { + position: absolute; + width: 0; + border-right: 1px solid; +} + +.vis.timeline .timeaxis .grid.minor { + border-color: #e5e5e5; +} + +.vis.timeline .timeaxis .grid.major { + border-color: #bfbfbf; +} + +.vis.timeline .currenttime { + background-color: #FF7F6E; + width: 2px; + z-index: 1; +} +.vis.timeline .customtime { + background-color: #6E94FF; + width: 2px; + cursor: move; + z-index: 1; +} +.vis.timeline.root { + /* + -webkit-transition: height .4s ease-in-out; + transition: height .4s ease-in-out; + */ +} + +.vis.timeline .vispanel { + /* + -webkit-transition: height .4s ease-in-out, top .4s ease-in-out; + transition: height .4s ease-in-out, top .4s ease-in-out; + */ +} + +.vis.timeline .axis { + /* + -webkit-transition: top .4s ease-in-out; + transition: top .4s ease-in-out; + */ +} + +/* TODO: get animation working nicely + +.vis.timeline .item { + -webkit-transition: top .4s ease-in-out; + transition: top .4s ease-in-out; +} + +.vis.timeline .item.line { + -webkit-transition: height .4s ease-in-out, top .4s ease-in-out; + transition: height .4s ease-in-out, top .4s ease-in-out; +} +/**/ + +.vis.timeline .vispanel.background.horizontal .grid.horizontal { + position: absolute; + width: 100%; + height: 0; + border-bottom: 1px solid; +} + +.vis.timeline .vispanel.background.horizontal .grid.minor { + border-color: #e5e5e5; +} + +.vis.timeline .vispanel.background.horizontal .grid.major { + border-color: #bfbfbf; +} + + +.vis.timeline .dataaxis .yAxis.major { + width: 100%; + position: absolute; + color: #4d4d4d; + white-space: nowrap; +} + +.vis.timeline .dataaxis .yAxis.major.measure{ + padding: 0px 0px 0px 0px; + margin: 0px 0px 0px 0px; + visibility: hidden; + width: auto; +} + + +.vis.timeline .dataaxis .yAxis.minor{ + position: absolute; + width: 100%; + color: #bebebe; + white-space: nowrap; +} + +.vis.timeline .dataaxis .yAxis.minor.measure{ + padding: 0px 0px 0px 0px; + margin: 0px 0px 0px 0px; + visibility: hidden; + width: auto; +} + + +.vis.timeline .legend { + background-color: rgba(247, 252, 255, 0.65); + padding: 5px; + border-color: #b3b3b3; + border-style:solid; + border-width: 1px; + box-shadow: 2px 2px 10px rgba(154, 154, 154, 0.55); +} + +.vis.timeline .legendText { + /*font-size: 10px;*/ + white-space: nowrap; + display: inline-block +} +.vis.timeline .graphGroup0 { + fill:#4f81bd; + fill-opacity:0; + stroke-width:2px; + stroke: #4f81bd; +} + +.vis.timeline .graphGroup1 { + fill:#f79646; + fill-opacity:0; + stroke-width:2px; + stroke: #f79646; +} + +.vis.timeline .graphGroup2 { + fill: #8c51cf; + fill-opacity:0; + stroke-width:2px; + stroke: #8c51cf; +} + +.vis.timeline .graphGroup3 { + fill: #75c841; + fill-opacity:0; + stroke-width:2px; + stroke: #75c841; +} + +.vis.timeline .graphGroup4 { + fill: #ff0100; + fill-opacity:0; + stroke-width:2px; + stroke: #ff0100; +} + +.vis.timeline .graphGroup5 { + fill: #37d8e6; + fill-opacity:0; + stroke-width:2px; + stroke: #37d8e6; +} + +.vis.timeline .graphGroup6 { + fill: #042662; + fill-opacity:0; + stroke-width:2px; + stroke: #042662; +} + +.vis.timeline .graphGroup7 { + fill:#00ff26; + fill-opacity:0; + stroke-width:2px; + stroke: #00ff26; +} + +.vis.timeline .graphGroup8 { + fill:#ff00ff; + fill-opacity:0; + stroke-width:2px; + stroke: #ff00ff; +} + +.vis.timeline .graphGroup9 { + fill: #8f3938; + fill-opacity:0; + stroke-width:2px; + stroke: #8f3938; +} + +.vis.timeline .fill { + fill-opacity:0.1; + stroke: none; +} + + +.vis.timeline .bar { + fill-opacity:0.5; + stroke-width:1px; +} + +.vis.timeline .point { + stroke-width:2px; + fill-opacity:1.0; +} + + +.vis.timeline .legendBackground { + stroke-width:1px; + fill-opacity:0.9; + fill: #ffffff; + stroke: #c2c2c2; +} + + +.vis.timeline .outline { + stroke-width:1px; + fill-opacity:1; + fill: #ffffff; + stroke: #e5e5e5; +} + +.vis.timeline .iconFill { + fill-opacity:0.3; + stroke: none; +} + + + +div.network-manipulationDiv { + border-width: 0; + border-bottom: 1px; + border-style:solid; + border-color: #d6d9d8; + background: #ffffff; /* Old browsers */ + background: -moz-linear-gradient(top, #ffffff 0%, #fcfcfc 48%, #fafafa 50%, #fcfcfc 100%); /* FF3.6+ */ + background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#ffffff), color-stop(48%,#fcfcfc), color-stop(50%,#fafafa), color-stop(100%,#fcfcfc)); /* Chrome,Safari4+ */ + background: -webkit-linear-gradient(top, #ffffff 0%,#fcfcfc 48%,#fafafa 50%,#fcfcfc 100%); /* Chrome10+,Safari5.1+ */ + background: -o-linear-gradient(top, #ffffff 0%,#fcfcfc 48%,#fafafa 50%,#fcfcfc 100%); /* Opera 11.10+ */ + background: -ms-linear-gradient(top, #ffffff 0%,#fcfcfc 48%,#fafafa 50%,#fcfcfc 100%); /* IE10+ */ + background: linear-gradient(to bottom, #ffffff 0%,#fcfcfc 48%,#fafafa 50%,#fcfcfc 100%); /* W3C */ + filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#ffffff', endColorstr='#fcfcfc',GradientType=0 ); /* IE6-9 */ + + position: absolute; + left: 0; + top: 0; + width: 100%; + height: 30px; +} + +div.network-manipulation-editMode { + position:absolute; + left: 0; + top: 0; + height: 30px; + margin-top:20px; +} + +div.network-manipulation-closeDiv { + position:absolute; + right: 0; + top: 0; + width: 30px; + height: 30px; + + background-position: 20px 3px; + background-repeat: no-repeat; + background-image: url("img/network/cross.png"); + cursor: pointer; + -webkit-touch-callout: none; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} + +div.network-manipulation-closeDiv:hover { + opacity: 0.6; +} + +span.network-manipulationUI { + font-family: verdana; + font-size: 12px; + -moz-border-radius: 15px; + border-radius: 15px; + display:inline-block; + background-position: 0px 0px; + background-repeat:no-repeat; + height:24px; + margin: -14px 0px 0px 10px; + vertical-align:middle; + cursor: pointer; + padding: 0px 8px 0px 8px; + -webkit-touch-callout: none; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} + +span.network-manipulationUI:hover { + box-shadow: 1px 1px 8px rgba(0, 0, 0, 0.20); +} + +span.network-manipulationUI:active { + box-shadow: 1px 1px 8px rgba(0, 0, 0, 0.50); +} + +span.network-manipulationUI.back { + background-image: url("img/network/backIcon.png"); +} + +span.network-manipulationUI.none:hover { + box-shadow: 1px 1px 8px rgba(0, 0, 0, 0.0); + cursor: default; +} +span.network-manipulationUI.none:active { + box-shadow: 1px 1px 8px rgba(0, 0, 0, 0.0); +} +span.network-manipulationUI.none { + padding: 0; +} +span.network-manipulationUI.notification{ + margin: 2px; + font-weight: bold; +} + +span.network-manipulationUI.add { + background-image: url("img/network/addNodeIcon.png"); +} + +span.network-manipulationUI.edit { + background-image: url("img/network/editIcon.png"); +} + +span.network-manipulationUI.edit.editmode { + background-color: #fcfcfc; + border-style:solid; + border-width:1px; + border-color: #cccccc; +} + +span.network-manipulationUI.connect { + background-image: url("img/network/connectIcon.png"); +} + +span.network-manipulationUI.delete { + background-image: url("img/network/deleteIcon.png"); +} +/* top right bottom left */ +span.network-manipulationLabel { + margin: 0px 0px 0px 23px; + line-height: 25px; +} +div.network-seperatorLine { + display:inline-block; + width:1px; + height:20px; + background-color: #bdbdbd; + margin: 5px 7px 0px 15px; +} + +div.network-navigation_wrapper { + position: absolute; + left: 0; + top: 0; + width: 100%; + height: 100%; +} +div.network-navigation { + width:34px; + height:34px; + -moz-border-radius: 17px; + border-radius: 17px; + position:absolute; + display:inline-block; + background-position: 2px 2px; + background-repeat:no-repeat; + cursor: pointer; + -webkit-touch-callout: none; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} + +div.network-navigation:hover { + box-shadow: 0px 0px 3px 3px rgba(56, 207, 21, 0.30); +} + +div.network-navigation:active { + box-shadow: 0px 0px 1px 3px rgba(56, 207, 21, 0.95); +} + +div.network-navigation.up { + background-image: url("img/network/upArrow.png"); + bottom:50px; + left:55px; +} +div.network-navigation.down { + background-image: url("img/network/downArrow.png"); + bottom:10px; + left:55px; +} +div.network-navigation.left { + background-image: url("img/network/leftArrow.png"); + bottom:10px; + left:15px; +} +div.network-navigation.right { + background-image: url("img/network/rightArrow.png"); + bottom:10px; + left:95px; +} +div.network-navigation.zoomIn { + background-image: url("img/network/plus.png"); + bottom:10px; + right:15px; +} +div.network-navigation.zoomOut { + background-image: url("img/network/minus.png"); + bottom:10px; + right:55px; +} +div.network-navigation.zoomExtends { + background-image: url("img/network/zoomExtends.png"); + bottom:50px; + right:15px; +} \ No newline at end of file diff --git a/showcase/projects/midas/images/arum.png b/showcase/projects/midas/images/arum.png new file mode 100644 index 0000000000000000000000000000000000000000..2f6a223a8caa3d84873a809e078e75a1f3b017d5 GIT binary patch literal 27740 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 O00kKp=^{y!fd32hGnz#J literal 0 HcmV?d00001 diff --git a/showcase/projects/midas/images/arum_small.png b/showcase/projects/midas/images/arum_small.png new file mode 100644 index 0000000000000000000000000000000000000000..fa252bdef0f4f1da8051583b69bb6b708df48640 GIT binary patch literal 31761 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 DW6MO* literal 0 HcmV?d00001 diff --git a/showcase/projects/midas/images/control_play.png b/showcase/projects/midas/images/control_play.png new file mode 100644 index 0000000000000000000000000000000000000000..0846555d0ca84cb99d4c70dad80144a232604041 GIT binary patch literal 592 zcmV-W0k7R5;6} zlgp~&KoEw{L*wq6jM9+RMU)_iNO6K~b@$|K=nj zb2!5=4Mjq_zrU*f>U2#vSVnMZ9ja4cY`AdOM*t}k^goWqfa3Iq(>2kSH;P81hAqIyBm_{t1>+!KRdtb~{1AK7>C~ zD-Nov`UX!X6ET_La7f{B_|*cRuZGR%^C`gjd@jmW6h*+;8;{2{8ja|Fzf-YTq+l@k zGLg?!DijI~9$+CG0*bxx-)TO!p6i!Q&%N5)kR|pVJuNho7M&@5o@ZpsgYXg z6pCpqAEgW*0v6~{n89ISU>M4A&V?DNy7MOQX68Ka`MBqf0l?U(ZY+X9l}VLZ)#Om- z;Wxvd@uT21RmKNz1dH$Bj0zp6>Db9B7mX*l{i7uPYA;7kd3g)QVC)rxA$;8vC|jea zS%$3%AWB_OF8f4{mJFo|55c22v$T`7VytGO85j|cC%=pBjskcjxd*)11x{77(<9$R zNrwG!M09PX(8Nd#urDkdGiz{FkfHKZhPUAguyq;A^$wKyj&8EE8)WXSvDl6Q4NN}z z2Zd#i!U*1a7=Vq#3W3jRZ6Z9$+&MVBAqrVEFbBV-XzUqFMNrFnb9RsDb%-T!q1pza zrNBj9g5~vhG_q(g8Ht^6IILQuOJK}cdY)autaf$;u_Hxz{;liNSF+ zP7JVo4aPtM&mF+{jGz3=veK}ME-bIS)D6sEz9#6p*nx(m=)Gd##2kGE&YZW%&7_NU zaJbxh3nsTeLlsIj==Xtu`3s4ZJ3jM?zzC+xEl8DG(CzhM_b>rg=Lda=hWnnX#UBVW zoG_R&W`Q?ax-3JI?gr8ns1oY(%Y_9|I!G4+85=MXv^z2{WgQJlI?w zaoVxIj zy$|ns{oikV-#`BGkKr1_<(kiY=6vS7=H2(b79k39;;2ZEkRT8Us-%R75(IKv1AMMV zybazz5q4^W|L)mHXxc*{$e7pvZb6b#@E{N*d2?Z51qCx}2WxvXYa6JfurSod&f3KM zg)s!;GMTDus-nDx!+ZSwKuG$fPmHv+5;h`KNhs6@E0Thm_#V2{OXAE4Y{ga#QBipO zu8fy(VPQV6u@z}iULlPmED~ph`Ded;)pIuIo^3JOc)a}OvSy5LrDQ*~svn^R5hYH7 zS&`KTrBLWG#;pIRj*oMT^a8#}WHu0V#400V`wP-rkP|N+9vYGsgb$EgF2l%(kQUkG z78pVB)%|s$#HY7>0&cZ9L~=+Y`d~o>+`@$NAOd2yd{PrAlpz`LkY}F_4Hh5@bdYD1 z-pj)fpVX5?_gfI{7-H;O8IceurtwP=h#5Deq+jioC`1DW!7`EV;Dn6RL12>VCK8ah zb&!@mbmU41A_@ei_%e_R0`Cra)~{acr4OyFua7OP7EOzBai~eYkUTZF(*07cWk|c+QUxXFtr7`7-JFio>7Gw)Gy! z4TqNbV+drg!MgS96GS*4Gyl1M=c^t53-Jtch>wY6 zlr03JD?&!E(o@C%2@wJj$?&CqD~Ny4ibvaWm!S2|bSv`tGqwOBvW_+(bRi@o9|F5) zR3&~wPlCR@C8U1F@Ku12rA5&!DB1>%zD@NV8n4a$pZbVttxp<#@4*XpA!6ziPrkfm z@JfR;3=uam@PxQk0{%fL8MON)mLiFCB&!6Y(km6Hss!O4uib6V;HP5mBe}aEOn&D< zjpB?kzLko+Ur}m=>kHT)1&C!K4;VQxCWYffSsar% zzxd-wO6Qhl#ST9*!!RM_K?8qLZSgBZwSbS;*mBOODqVGk> zs{5*+Rl*B(l|9SERGF0*3h#Z5D^@K^RoGC*S2iu=E(k6%EwEPM&+*c{2)V1-F4HbA zoTHdgp|{ZZ5sGa1u*!v&*aEdU$kCUzgQDWggohJ#L&61NLnv=iVJ4?AvlO#>mC}BU z2|vO@g6&C)gWSE9C;9RFUa!7m?NH5A;pef))H51CwJ3^M-)Leh+saSP zYZ7hHJo7~p{3J#Blv3O@yzfrxit>c=w|u0099dqj!!r6Ji(LH71`+3GtNiVRv=6#I zX)@ON-(8zleiC>xT&P@F{>(oVN71-jg+g|B3Z)N^r<2vznun0#)6@?|VV!mX;avS# z{rI_n$ofH^>=CgyV)tL0e|)+?wqW_u@?$o|T?%3fY1wtz?Ib1H`aG&UZW(TwPhWb! zWPKS;WKHH}9AjEZUP&HHo~e7L<)TGfTTuJXSh8|aD^GK~w$Un3i&HaKLsd(sN~|=s zlzg_iOzg{j{t~hPfwEgXm+V0xIx`%I<#_vv*)mqki>YEzin$~rewQol^MSH&Y zd~?BzfEql3R!pcx=w)ARhsCAF^*yOCWt-o#^B``G!BnFky?>l}p%(KqRfl=1a`Lda zDXwX~z5o8p&=6_A&-C$p^vU=izjZY&N;w?b>dd(f9i}E%CAV!AOqFl5ZJTcgPp9{? zq*JA7GTRTdcL~obE)DV{r^mu9W=g!|h2?!sYgR}Z3>hNTOUK^U+j(59r4+v zFYqn!uzzRzuBq3{-cr|9_gTK(I!!NdM%bJ5%IM1NY7mm|Lw?H?F2q;RSNrzaiJJ#; za~MC@l<`sJ#FpUejCw(A!EnD)zophvVzaKzh*IL?m4v>?K6A7{1+Uf70R;VJ{m^zV z6w1KhK=mO0Fq@Yej|LgzS)B)`h3#iVeu~72d`~uIl;}&lDX1wlANHHOuSr8jQ&xP5w=1_#In(~rt7B5CQhGAi~}n#JFPU4w}%S|QEIV*W=U8j&1u)HPE1y#&Zt#y9Uh5JF~om)GS%Q;D_o!5 z%=50%IKR8^3pc+?j>>_`PZhT9p>H+%V<%RF+xYH{M=YBoQ`(Kk7ku3xrc`gK%8r({S+1+O;@RXm2UINHNW9#)4l%Jsj;b^Ui7Ea&!)0!-B%3zW}e@- z-g%GiFgGN66}rOR$ldpJ@14V5<S!FWhg`PmzXunRWTO3>SyN>ttkOtZy)3Ff)EHKDyb`BllvxW#eJ_)W^1uk3V#~ zFf-~NsGV)KI!`+j?qtv0mtXXNdOx``JFPfW9>_n7ei$8n1rL|7e{RT3HwA%k=bNi& zIB3Yqa2Z-#((4;p8yM5OSlR$X6awMlcd^kov@mvn8W@|JTk(?YG&GYy&5d|T)LCU= zvNpoTX66!ZcE-wXaw>*y7KWTgB>a3xJT6>d0!w2DeW;7&3oCmr7haM-=H&vPuP-x@ zK>xVK!Gf1W@cMyJ4Os=Ku(h2rl$D;9&Jf1L2xaG_XJlpJWMrg;GQk*`85m&v{DJtQ{SANl30A^sm=H z_GM}FuLoJ#{{uUKB7=**4Fe-RjN!i-BrE&tVV0KvaiqP2s1sPpKX઄=QYGcfx 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 z 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

tVs*-xD|!I~^MHH3sVVZJ_6$0;f2>22Q5al+)Xn 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 IzQ@7;08QhAkN^Mx literal 0 HcmV?d00001 diff --git a/showcase/projects/midas/images/toggleOn.png b/showcase/projects/midas/images/toggleOn.png new file mode 100644 index 0000000000000000000000000000000000000000..b8c5d9a5dcadc5d649b6a25259d5aeabdfe1879c GIT binary patch literal 20281 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 E0ev|1DF6Tf literal 0 HcmV?d00001 diff --git a/showcase/projects/midas/index.html b/showcase/projects/midas/index.html new file mode 100644 index 00000000..94a49212 --- /dev/null +++ b/showcase/projects/midas/index.html @@ -0,0 +1,86 @@ + + + + ARUM - MIDAS + + + + + + + + + + + + + + + + + + + + + + + +
+

Show by Timeline
+
Show by Job
+
+ + eventNo:
-1
+ + +
+
+
+ +
+ + + + + + + + + + + + + + +
+
Prediction
+
  +
Ideal
+
Ideal + Pause
+
Ideal + Prerequisites
+
Total Time
+
+ + +
+
+
+
+
Switch View
+
+
+
+
+ +

+

+ + diff --git a/showcase/projects/midas/js/evejs.js b/showcase/projects/midas/js/evejs.js new file mode 100644 index 00000000..1a53cc7b --- /dev/null +++ b/showcase/projects/midas/js/evejs.js @@ -0,0 +1,31161 @@ +!function(e){if("object"==typeof exports)module.exports=e();else if("function"==typeof define&&define.amd)define(e);else{var f;"undefined"!=typeof window?f=window:"undefined"!=typeof global?f=global:"undefined"!=typeof self&&(f=self),f.eve=e()}}(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);throw new Error("Cannot find module '"+o+"'")}var f=n[o]={exports:{}};t[o][0].call(f.exports,function(e){var n=t[o][1][e];return s(n?n:e)},f,f.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o} Returns a promise resolving with the response message + */ +RequestModule.prototype.request = function (to, message) { + var me = this; + return new Promise(function (resolve, reject) { + // put the data in an envelope with id + var id = uuid(); + var envelope = { + type: 'request', + id: id, + message: message + }; + + // add the request to the list with requests in progress + me.queue[id] = { + resolve: resolve, + reject: reject, + timeout: setTimeout(function () { + delete me.queue[id]; + reject(new Error('Timeout')); + }, me.timeout) + }; + + me.agent.send(to, envelope) + .catch(function (err) { + reject(err); + }); + }); +}; + +/** + * Get a map with mixin functions + * @return {{_receive: function, request: function}} + * Returns mixin function, which can be used to extend the agent. + */ +RequestModule.prototype.mixin = function () { + return { + _receive: this.receive.bind(this), + request: this.request.bind(this) + } +}; + +module.exports = RequestModule; + +},{"../util":23,"promise":114,"uuid-v4":125}],9:[function(_dereq_,module,exports){ +'use strict'; + +var Promise = _dereq_('promise'); + +/** + * An abstract Transport connection + * @param {Transport} transport + * @param {string} id + * @param {function} receive + * @constructor + * @abstract + */ +function Connection (transport, id, receive) { + throw new Error('Cannot create an abstract Connection'); +} + +Connection.prototype.ready = Promise.reject(new Error('Cannot get abstract property ready')); + +/** + * Send a message to an agent. + * @param {string} to + * @param {*} message + * @return {Promise} returns a promise which resolves when the message has been sent + */ +Connection.prototype.send = function (to, message) { + throw new Error('Cannot call abstract function send'); +}; + +/** + * Close the connection, disconnect from the transport. + */ +Connection.prototype.close = function () { + throw new Error('Cannot call abstract function "close"'); +}; + +module.exports = Connection; + +},{"promise":114}],10:[function(_dereq_,module,exports){ +'use strict'; + +/** + * Abstract prototype of a transport + * @param {Object} [config] + * @constructor + */ +function Transport(config) { + this.id = config && config.id || null; + this['default'] = config && config['default'] || false; +} + +Transport.prototype.type = null; + +/** + * Connect an agent + * @param {String} id + * @param {Function} receive Invoked as receive(from, message) + * @return {Connection} Returns a connection + */ +Transport.prototype.connect = function(id, receive) { + throw new Error('Cannot invoke abstract function "connect"'); +}; + +/** + * Close the transport + */ +Transport.prototype.close = function() { + throw new Error('Cannot invoke abstract function "close"'); +}; + +module.exports = Transport; + +},{}],11:[function(_dereq_,module,exports){ +'use strict'; + +var Promise = _dereq_('promise'); +var Connection = _dereq_('../Connection'); + +/** + * A local connection. + * @param {AMQPTransport} transport + * @param {string | number} id + * @param {function} receive + * @constructor + */ +function AMQPConnection(transport, id, receive) { + this.transport = transport; + this.id = id; + + // ready state + this.ready = this.transport._connect(id, receive); +} + +/** + * Send a message to an agent. + * @param {string} to + * @param {*} message + * @return {Promise} returns a promise which resolves when the message has been sent + */ +AMQPConnection.prototype.send = function (to, message) { + var me = this; + return new Promise(function (resolve, reject) { + var msg = { + body: { + from: me.id, + to: to, + message: message + } + }; + var options = { + //immediate: true + }; + + me.transport.exchange.publish(to, msg, options, function () { + // FIXME: callback is not called. + //console.log('sent', arguments) + }); + + resolve(); + }); +}; + +/** + * Close the connection + */ +AMQPConnection.prototype.close = function () { + this.transport._close(this.id); +}; + +module.exports = AMQPConnection; + +},{"../Connection":9,"promise":114}],12:[function(_dereq_,module,exports){ +'use strict'; + +var Promise = _dereq_('promise'); +var Transport = _dereq_('./../Transport'); +var AMQPConnection = _dereq_('./AMQPConnection'); + +/** + * Use AMQP as transport + * @param {Object} config Config can contain the following properties: + * - `id: string` + * - `url: string` + * - `host: string` + * The config must contain either `url` or `host`. + * For example: {url: 'amqp://localhost'} or + * {host: 'dev.rabbitmq.com'} + * @constructor + */ +function AMQPTransport(config) { + this.id = config.id || null; + this.networkId = config.url || config.host || null; + this['default'] = config['default'] || false; + + this.config = config; + this.connection = null; + this.exchange = null; + + this.subscriptions = []; +} + +AMQPTransport.prototype = new Transport(); + +AMQPTransport.prototype.type = 'amqp'; + +/** + * Connect an agent + * @param {String} id + * @param {Function} receive Invoked as receive(from, message) + * @return {AMQPConnection} Returns a connection. + */ +AMQPTransport.prototype.connect = function(id, receive) { + return new AMQPConnection(this, id, receive); +}; + +/** + * Get an AMQP connection. If there is not yet a connection, a connection will + * be made. + * @param {Function} callback Invoked as callback(connection) + * @private + */ +AMQPTransport.prototype._getConnection = function(callback) { + var me = this; + + if (this.connection) { + // connection is available + callback(this.connection); + } + else { + if (this._onConnected) { + // connection is being opened but not yet ready + this._onConnected.push(callback); + } + else { + // no connection, create one + this._onConnected = [callback]; + + var amqp = _dereq_('amqp'); // lazy load the amqp library + var connection = amqp.createConnection(this.config); + connection.on('ready', function () { + var exchange = connection.exchange('', {confirm: true}, function () { + var _onConnected = me._onConnected; + delete me._onConnected; + + me.connection = connection; + me.exchange = exchange; + + _onConnected.forEach(function (callback) { + callback(me.connection); + }); + }); + }); + } + } +}; + +/** + * Open a connection + * @param {string} id + * @param {Function} receive Invoked as receive(from, message) + */ +AMQPTransport.prototype._connect = function(id, receive) { + var me = this; + + return new Promise(function (resolve, reject) { + function subscribe(connection) { + var queue = connection.queue(id, {}, function() { + queue + .subscribe(function(message) { + var body = message.body; + receive(body.from, body.message); + }) + .addCallback(function (ok) { + // register this subscription + me.subscriptions.push({ + id: id, + consumerTag: ok.consumerTag + }); + + resolve(me); + }); + }); + } + + me._getConnection(subscribe); + }); +}; + +/** + * Close a connection an agent by its id + * @param {String} id + */ +AMQPTransport.prototype._close = function(id) { + var i = 0; + while (i < this.subscriptions.length) { + var subscription = this.subscriptions[i]; + if (subscription.id == id) { + // remove this entry + this.subscriptions.splice(i, 1); + } + else { + i++; + } + } + + if (this.subscriptions.length == 0) { + // fully disconnect if there are no subscribers left + this.exchange.destroy(); + this.connection.disconnect(); + + this.connection = null; + this.exchange = null; + } +}; + +/** + * Close the transport. + */ +AMQPTransport.prototype.close = function() { + this.connection.destroy(); + this.connection = null; +}; + +module.exports = AMQPTransport; + +},{"./../Transport":10,"./AMQPConnection":11,"amqp":24,"promise":114}],13:[function(_dereq_,module,exports){ +'use strict'; + +var Promise = _dereq_('promise'); +var Connection = _dereq_('../Connection'); + +/** + * A local connection. + * @param {DistribusTransport} transport + * @param {string | number} id + * @param {function} receive + * @constructor + */ +function DistribusConnection(transport, id, receive) { + this.transport = transport; + this.id = id; + + // create a peer + var peer = this.transport.host.create(id); + peer.on('message', receive); + + // ready state + this.ready = Promise.resolve(this); +} + +/** + * Send a message to an agent. + * @param {string} to + * @param {*} message + * @return {Promise} returns a promise which resolves when the message has been sent + */ +DistribusConnection.prototype.send = function (to, message) { + return this.transport.host.send(this.id, to, message); +}; + +/** + * Close the connection + */ +DistribusConnection.prototype.close = function () { + this.transport.host.remove(this.id); +}; + +module.exports = DistribusConnection; + +},{"../Connection":9,"promise":114}],14:[function(_dereq_,module,exports){ +'use strict'; + +var distribus = _dereq_('distribus'); +var Transport = _dereq_('./../Transport'); +var DistribusConnection = _dereq_('./DistribusConnection'); + +/** + * Use distribus as transport + * @param {Object} config Config can contain the following properties: + * - `id: string`. Optional + * - `host: distribus.Host`. Optional + * If `host` is not provided, + * a new local distribus Host is created. + * @constructor + */ +function DistribusTransport(config) { + this.id = config && config.id || null; + this['default'] = config && config['default'] || false; + this.host = config && config.host || new distribus.Host(config); + + this.networkId = this.host.networkId; // FIXME: networkId can change when host connects to another host. +} + +DistribusTransport.prototype = new Transport(); + +DistribusTransport.prototype.type = 'distribus'; + +/** + * Connect an agent + * @param {String} id + * @param {Function} receive Invoked as receive(from, message) + * @return {DistribusConnection} Returns a connection. + */ +DistribusTransport.prototype.connect = function(id, receive) { + return new DistribusConnection(this, id, receive); +}; + +/** + * Close the transport. + */ +DistribusTransport.prototype.close = function() { + this.host.close(); + this.host = null; +}; + +module.exports = DistribusTransport; + +},{"./../Transport":10,"./DistribusConnection":13,"distribus":67}],15:[function(_dereq_,module,exports){ +'use strict'; + +var Promise = _dereq_('promise'); +var Connection = _dereq_('../Connection'); + +/** + * A HTTP connection. + * @param {HTTPTransport} transport + * @param {string | number} id + * @param {function} receive + * @constructor + */ +function HTTPConnection(transport, id, receive) { + this.transport = transport; + this.id = id; + + // register the agents receive function + if (this.id in this.transport.agents) { + throw new Error('Agent with id ' + id + ' already exists'); + } + this.transport.agents[this.id] = receive; + + // ready state + this.ready = Promise.resolve(this); +} + +/** + * Send a message to an agent. + * @param {string} to + * @param {*} message + */ +HTTPConnection.prototype.send = function (to, message) { + var fromURL = this.transport.url.replace(':id', this.id); + + var isURL = to.indexOf('://') !== -1; + var toURL; + if (isURL) { + toURL = to; + } + else { + if (this.transport.remoteUrl !== undefined) { + toURL = this.transport.remoteUrl.replace(':id', to); + } + else { + console.log('ERROR: no remote URL specified. Cannot send over HTTP.', to); + } + } + + return this.transport.send(fromURL, toURL, message); +}; + +/** + * Close the connection + */ +HTTPConnection.prototype.close = function () { + delete this.transport.agents[this.id]; +}; + +module.exports = HTTPConnection; + +},{"../Connection":9,"promise":114}],16:[function(_dereq_,module,exports){ +'use strict'; + +var http = _dereq_('http'); +var Promise = _dereq_('promise'); +var Transport = _dereq_('./../Transport'); +var HTTPConnection = _dereq_('./HTTPConnection'); +var uuid = _dereq_('uuid-v4'); + +/** + * HTTP Transport layer: + * + * Supported Options: + * + * {Number} config.port Port to listen on. + * {String} config.path Path, with or without leading and trailing slash (/) + * {Boolean} config.localShortcut If the agentId exists locally, use local transport. (local) + * + * Address: http://127.0.0.1:PORTNUMBER/PATH + */ +function HTTPTransport(config) { + this.id = config && config.id || null; + this.networkId = null; + + this.agents = {}; + this.outstandingRequests = {}; // these are received messages that are expecting a response + this.outstandingMessages = {}; + + this.url = config && config.url || "http://127.0.0.1:3000/agents/:id"; + this.remoteUrl = config && config.remoteUrl; + this.localShortcut = (config && config.localShortcut === false) ? false : true; + + this.httpTimeout = config && config.httpTimeout || 2000; // 1 second - timeout to send message + this.httpResponseTimeout = config && config.httpResponseTimeout || 200; // 0.5 second - timeout to expect reply after delivering request + this.regexHosts = /[http]{4}s?:\/\/([a-z\-\.A-Z0-9]*):?([0-9]*)(\/[a-z\/:A-Z0-9._\-% \\\(\)\*\+\.\^\$]*)/; + this.urlHostData = this.regexHosts.exec(this.url); + + this.regexPath = this.getRegEx(this.urlHostData[3]); + this.port = config && config.port || this.urlHostData[2] || 3000; + this.path = this.urlHostData[3].replace(':id', ''); +} + +HTTPTransport.prototype = new Transport(); +HTTPTransport.prototype.type = 'http'; + +HTTPTransport.prototype.getRegEx = function(url) { + return new RegExp(url.replace(/[\\\(\)\*\+\.\^\$]/g,function(match) {return '\\' + match;}).replace(':id','([:a-zA-Z_0-9]*)')); +}; + + +function askAgent(url,method,params,callback, async) { + if (async === undefined) { + async = true; + } + // create post request + var POSTrequest = JSON.stringify({"id":0, "method": method, "params": params}); + + // create XMLHttpRequest object to send the POST request + var http = new XMLHttpRequest(); + + // insert the callback function. This is called when the message has been delivered and a response has been received + http.onreadystatechange = function () { + if (http.readyState == 4 && http.status == 200) { + if (callback === undefined || callback === null) { + } + else { + // launch callback function + callback(JSON.parse(http.responseText)); + } + } + else if (http.readyState == 4 && http.status != 200) { + console.log("Make sure that the Node server has started."); + } + }; + + // open an asynchronous POST connection + http.open("POST", url, async); + // include header so the receiving code knows its a JSON object + http.setRequestHeader("Content-type", "application/json"); + // send + http.send(POSTrequest); +} + + +/** + * Connect an agent + * @param {String} id + * @param {Function} receive Invoked as receive(from, message) + * @return {HTTPConnection} Returns a connection. + */ +HTTPTransport.prototype.connect = function(id, receive) { + if (this.server === undefined) { + this.initiateServer(); + } + this.outstandingRequests[id] = {}; + this.outstandingMessages[id] = {}; + return new HTTPConnection(this, id, receive); +}; + +/** + * Send a message to an agent + * @param {String} from Id of sender + * @param {String} to Id of addressed peer + * @param {String} message + */ +HTTPTransport.prototype.send = function(from, to, message) { + var me = this; + return new Promise(function (resolve,reject) { + var hostData = me.regexHosts.exec(to); + var fromRegexpCheck = me.regexPath.exec(from); + var fromAgentId = fromRegexpCheck[1]; + var outstandingMessageID = uuid(); + + // check for local shortcut possibility + if (me.localShortcut == true) { + var toRegexpCheck = me.regexPath.exec(to); + var toAgentId = toRegexpCheck[1]; + var toPath = hostData[3].replace(toAgentId,""); + + // check if the "to" address is on the same URL, port and path as the "from" + if ((hostData[1] == '127.0.0.1' && hostData[2] == me.urlHostData[2] && toPath == me.path) || + (me.urlHostData[1] == hostData[1] && hostData[2] == me.urlHostData[2] && toPath == me.path)) { + // by definition true but check anyway + if (me.agents[toAgentId] !== undefined) { + me.agents[toAgentId](fromAgentId, message); + resolve(); + return; + } + } + } + + // stringify the message. If the message is an object, it can have an ID so it may be part of a req/rep. + if (typeof message == 'object') { + + // check if the send is a reply to an outstanding request and if so, deliver + var outstanding = me.outstandingRequests[fromAgentId]; + if (outstanding[message.id] !== undefined) { + var callback = outstanding[message.id]; + callback.response.end(JSON.stringify(message)); + clearTimeout(callback.timeout); + delete outstanding[message.id]; + resolve(); + return; + } + // stringify the message. + message = JSON.stringify(message) + } + + // all post options + var options = { + host: hostData[1], + port: hostData[2], + path: hostData[3], + method: 'POST', + headers: { + 'x-eve-senderurl' : from, // used to get senderID + 'Content-type' : 'text/plain' + } + }; + var request = http.request(options, function(res) { + res.setEncoding('utf8'); + // message was delivered, clear the cannot deliver timeout. + clearTimeout(me.outstandingMessages[fromAgentId][outstandingMessageID].timeout); + // listen to incoming data + res.on('data', function (response) { + var parsedResponse; + try {parsedResponse = JSON.parse(response);} catch (err) {parsedResponse = response;} + if (typeof parsedResponse == 'object') { + if (parsedResponse.__httpError__ !== undefined) { + reject(new Error(parsedResponse.__httpError__)); + return; + } + } + me.agents[fromAgentId](to, parsedResponse); + resolve(); + }); + }); + + me.outstandingMessages[fromAgentId][outstandingMessageID] = { + timeout: setTimeout(function () { + request.abort(); + reject(new Error("Cannot connect to " + to)) + }, me.httpTimeout), + reject: reject + }; + + request.on('error', function(e) { + reject(e); + }); + + // write data to request body + request.write(message); + request.end(); + }); +}; + + +/** + * This is the HTTP equivalent of receiveMessage. + * + * @param request + * @param response + */ +HTTPTransport.prototype.processRequest = function(request, response) { + var url = request.url; + + // define headers + var headers = {}; + headers['Access-Control-Allow-Origin'] = '*'; + headers['Access-Control-Allow-Credentials'] = true; + headers['Content-Type'] = 'text/plain'; + + var regexpCheck = this.regexPath.exec(url); + if (regexpCheck !== null) { + var agentId = regexpCheck[1]; + var senderId = 'unknown'; + if (request.headers['x-eve-senderurl'] !== undefined) { + senderId = request.headers['x-eve-senderurl']; + } + var body = ''; + request.on('data', function (data) { + body += data; + if (body.length > 1e6) { // 1e6 == 1MB + request.connection.destroy(); // FLOOD ATTACK OR FAULTY CLIENT, NUKE REQUEST + } + }); + + + var me = this; + request.on('end', function () { + var expectReply = false; + var message; + try {message = JSON.parse(body);} catch (err) {message = body;} + + // check if JSON RPC + expectReply = message.jsonrpc && message.jsonrpc == '2.0' || expectReply; + // check if type == 'request' + expectReply = message.type && message.type == 'request' || expectReply; + + response.writeHead(200, headers); + // construct callback + var callback = me.agents[agentId]; + if (callback === undefined) { + var error = new Error('Agent: "' + agentId + '" does not exist.'); + response.end(JSON.stringify({__httpError__:error.message || error.toString()})); + } + else { + if (expectReply == true) { + me.outstandingRequests[agentId][message.id] = { + response: response, + timeout: setTimeout(function () { + response.end("timeout"); + delete me.outstandingRequests[agentId][message.id]; + }, me.httpResponseTimeout) + }; + callback(senderId, message); + } + else { + // if we're not expecting a response, we first close the connection, then receive the message + response.end(''); + if (callback !== undefined) { + callback(senderId, message); + } + } + } + }); + } +}; + +/** + * Configure a HTTP server listener + */ +HTTPTransport.prototype.initiateServer = function() { + if (this.server === undefined) { + var me = this; + this.server = http.createServer(function (request, response) { + if (request.method == 'OPTIONS') { + var headers = {}; + headers['Access-Control-Allow-Origin'] = '*'; + headers['Access-Control-Allow-Methods'] = 'POST, OPTIONS'; + headers['Access-Control-Allow-Credentials'] = true; + headers['Access-Control-Max-Age'] = '86400'; // 24 hours + headers['Access-Control-Allow-Headers'] = 'X-Requested-With, Access-Control-Allow-Origin, X-HTTP-Method-Override, Content-Type, Authorization, Accept'; + // respond to the request + response.writeHead(200, headers); + response.end(); + } + else if (request.method == 'POST') { + me.processRequest(request, response); + } + }); + + this.server.on('error', function(err) { + if (err.code == 'EADDRINUSE') { + throw new Error('ERROR: Could not start HTTP server. Port ' + me.port + ' is occupied.'); + } + else { + throw new Error(err); + } + }); + + // Listen on port (default: 3000), IP defaults to 127.0.0.1 + this.server.listen(this.port, function() { + // Put a friendly message on the terminal + console.log('Server listening at ', me.url); + }); + + + } + else { + this.server.close(); + this.server = undefined; + this.initiateServer(); + } +}; + + +/** + * Close the HTTP server + */ +HTTPTransport.prototype.close = function() { + // close all open connections + for (var agentId in this.outstandingRequests) { + if (this.outstandingRequests.hasOwnProperty(agentId)) { + var agentRequests = this.outstandingRequests[agentId]; + for (var messageId in agentRequests) { + if (agentRequests.hasOwnProperty(messageId)) { + var openMessage = agentRequests[messageId]; + var error = new Error('Server shutting down.'); + openMessage.response.end(JSON.stringify({__httpError__:error.message || error.toString()})); + } + } + } + } + // close server + if (this.server) { + this.server.close(); + } + this.server = null; +}; + + +module.exports = HTTPTransport; + + +},{"./../Transport":10,"./HTTPConnection":15,"http":138,"promise":114,"uuid-v4":125}],17:[function(_dereq_,module,exports){ +'use strict'; + +var Promise = _dereq_('promise'); +var Connection = _dereq_('../Connection'); + +/** + * A local connection. + * @param {LocalTransport} transport + * @param {string | number} id + * @param {function} receive + * @constructor + */ +function LocalConnection(transport, id, receive) { + this.transport = transport; + this.id = id; + + // register the agents receive function + if (this.id in this.transport.agents) { + throw new Error('Agent with id ' + id + ' already exists'); + } + this.transport.agents[this.id] = receive; + + // ready state + this.ready = Promise.resolve(this); +} + +/** + * Send a message to an agent. + * @param {string} to + * @param {*} message + * @return {Promise} returns a promise which resolves when the message has been sent + */ +LocalConnection.prototype.send = function (to, message) { + var callback = this.transport.agents[to]; + if (!callback) { + throw new Error('Agent with id ' + to + ' not found'); + } + + // invoke the agents receiver as callback(from, message) + callback(this.id, message); + + return Promise.resolve(); +}; + +/** + * Close the connection + */ +LocalConnection.prototype.close = function () { + delete this.transport.agents[this.id]; +}; + +module.exports = LocalConnection; + +},{"../Connection":9,"promise":114}],18:[function(_dereq_,module,exports){ +'use strict'; + +var Transport = _dereq_('./../Transport'); +var LocalConnection = _dereq_('./LocalConnection'); + +/** + * Create a local transport. + * @param {Object} config Config can contain the following properties: + * - `id: string`. Optional + * @constructor + */ +function LocalTransport(config) { + this.id = config && config.id || null; + this.networkId = this.id || null; + this['default'] = config && config['default'] || false; + this.agents = {}; +} + +LocalTransport.prototype = new Transport(); + +LocalTransport.prototype.type = 'local'; + +/** + * Connect an agent + * @param {String} id + * @param {Function} receive Invoked as receive(from, message) + * @return {LocalConnection} Returns a promise which resolves when + * connected. + */ +LocalTransport.prototype.connect = function(id, receive) { + return new LocalConnection(this, id, receive); +}; + +/** + * Close the transport. Removes all agent connections. + */ +LocalTransport.prototype.close = function() { + this.agents = {}; +}; + +module.exports = LocalTransport; + +},{"./../Transport":10,"./LocalConnection":17}],19:[function(_dereq_,module,exports){ +'use strict'; + +var Promise = _dereq_('promise'); +var Connection = _dereq_('../Connection'); + +/** + * A connection. The connection is ready when the property .ready resolves. + * @param {PubNubTransport} transport + * @param {string | number} id + * @param {function} receive + * @constructor + */ +function PubNubConnection(transport, id, receive) { + this.id = id; + this.transport = transport; + + // ready state + var me = this; + this.ready = new Promise(function (resolve, reject) { + transport.pubnub.subscribe({ + channel: id, + message: function (message) { + receive(message.from, message.message); + }, + connect: function () { + resolve(me); + } + }); + }); +} + +/** + * Send a message to an agent. + * @param {string} to + * @param {*} message + * @return {Promise} returns a promise which resolves when the message has been sent + */ +PubNubConnection.prototype.send = function (to, message) { + var me = this; + return new Promise(function (resolve, reject) { + me.transport.pubnub.publish({ + channel: to, + message: { + from: me.id, + to: to, + message: message + }, + callback: resolve, + error: reject + }); + }); +}; + +/** + * Close the connection + */ +PubNubConnection.prototype.close = function () { + this.transport.pubnub.unsubscribe({ + channel: this.id + }); +}; + +module.exports = PubNubConnection; + +},{"../Connection":9,"promise":114}],20:[function(_dereq_,module,exports){ +'use strict'; + +var Transport = _dereq_('./../Transport'); +var PubNubConnection = _dereq_('./PubNubConnection'); + +/** + * Use pubnub as transport + * @param {Object} config Config can contain the following properties: + * - `id: string`. Optional + * - `publish_key: string`. Required + * - `subscribe_key: string`. Required + * @constructor + */ +function PubNubTransport(config) { + this.id = config.id || null; + this.networkId = config.publish_key || null; + this['default'] = config['default'] || false; + this.pubnub = PUBNUB().init(config); +} + +PubNubTransport.prototype = new Transport(); + +PubNubTransport.prototype.type = 'pubnub'; + +/** + * Connect an agent + * @param {String} id + * @param {Function} receive Invoked as receive(from, message) + * @return {PubNubConnection} Returns a connection + */ +PubNubTransport.prototype.connect = function(id, receive) { + return new PubNubConnection(this, id, receive) +}; + +/** + * Close the transport. + */ +PubNubTransport.prototype.close = function() { + // FIXME: how to correctly close a pubnub connection? + this.pubnub = null; +}; + +/** + * Load the PubNub library + * @returns {Object} PUBNUB + */ +function PUBNUB() { + if (typeof window !== 'undefined') { + // browser + if (typeof window['PUBNUB'] === 'undefined') { + throw new Error('Please load pubnub first in the browser'); + } + return window['PUBNUB']; + } + else { + // node.js + return _dereq_('pubnub'); + } +} + +module.exports = PubNubTransport; + +},{"./../Transport":10,"./PubNubConnection":19,"pubnub":123}],21:[function(_dereq_,module,exports){ +'use strict'; + +var uuid = _dereq_('uuid-v4'); +var Promise = _dereq_('promise'); +var WebSocket = (typeof window !== 'undefined' && typeof window.WebSocket !== 'undefined') ? + window.WebSocket : + _dereq_('ws'); + +var util = _dereq_('../../util'); +var Connection = _dereq_('../Connection'); + +/** + * A websocket connection. + * @param {WebSocketTransport} transport + * @param {string | number | null} url The url of the agent. The url must match + * the url of the WebSocket server. + * If url is null, a UUID id is generated as url. + * @param {function} receive + * @constructor + */ +function WebSocketConnection(transport, url, receive) { + this.transport = transport; + this.url = url ? util.normalizeURL(url) : uuid(); + this.receive = receive; + + this.sockets = {}; + this.closed = false; + this.reconnectTimers = {}; + + // ready state + this.ready = Promise.resolve(this); +} + +/** + * Send a message to an agent. + * @param {string} to The WebSocket url of the receiver + * @param {*} message + * @return {Promise} Returns a promise which resolves when the message is sent, + * and rejects when sending the message failed + */ +WebSocketConnection.prototype.send = function (to, message) { + //console.log('send', this.url, to, message); // TODO: cleanup + + // deliver locally when possible + if (this.transport.localShortcut) { + var agent = this.transport.agents[to]; + if (agent) { + try { + agent.receive(this.url, message); + return Promise.resolve(); + } + catch (err) { + return Promise.reject(err); + } + } + } + + // get or create a connection + var conn = this.sockets[to]; + if (conn) { + try { + if (conn.readyState == conn.CONNECTING) { + // the connection is still opening + return new Promise(function (resolve, reject) { + conn.onopen.callback.push(function () { + conn.send(JSON.stringify(message)); + resolve(); + }) + }); + } + else if (conn.readyState == conn.OPEN) { + conn.send(JSON.stringify(message)); + return Promise.resolve(); + } + else { + // remove the connection + conn = null; + } + } + catch (err) { + return Promise.reject(err); + } + } + + if (!conn) { + // try to open a connection + var me = this; + return new Promise(function (resolve, reject) { + me._connect(to, function (conn) { + conn.send(JSON.stringify(message)); + resolve(); + }, function (err) { + reject(new Error('Failed to connect to agent "' + to + '"')); + }); + }) + } +}; + +/** + * Open a websocket connection to an other agent. No messages are sent. + * @param {string} to Url of the remote agent. + * @returns {Promise.} + * Returns a promise which resolves when the connection is + * established and rejects in case of an error. + */ +WebSocketConnection.prototype.connect = function (to) { + var me = this; + return new Promise(function (resolve, reject) { + me._connect(to, function () { + resolve(me); + }, reject); + }); +}; + +/** + * Open a websocket connection + * @param {String} to Url of the remote agent + * @param {function} [callback] + * @param {function} [errback] + * @param {boolean} [doReconnect=false] + * @returns {WebSocket} + * @private + */ +WebSocketConnection.prototype._connect = function (to, callback, errback, doReconnect) { + var me = this; + var conn = new WebSocket(to + '?id=' + this.url); + + // register the new socket + me.sockets[to] = conn; + + conn.onopen = function () { + // Change doReconnect to true as soon as we have had an open connection + doReconnect = true; + + conn.onopen.callbacks.forEach(function (cb) { + cb(conn); + }); + conn.onopen.callbacks = []; + }; + conn.onopen.callbacks = callback ? [callback] : []; + + conn.onmessage = function (event) { + me.receive(to, JSON.parse(event.data)); + }; + + conn.onclose = function () { + delete me.sockets[to]; + if (doReconnect) { + me._reconnect(to); + } + }; + + conn.onerror = function (err) { + delete me.sockets[to]; + if (errback) { + errback(err); + } + }; + + return conn; +}; + +/** + * Auto reconnect a broken connection + * @param {String} to Url of the remote agent + * @private + */ +WebSocketConnection.prototype._reconnect = function (to) { + var me = this; + var doReconnect = true; + if (me.closed == false && me.reconnectTimers[to] == null) { + me.reconnectTimers[to] = setTimeout(function () { + delete me.reconnectTimers[to]; + me._connect(to, null, null, doReconnect); + }, me.transport.reconnectDelay); + } +}; + +/** + * Register a websocket connection + * @param {String} from Url of the remote agent + * @param {WebSocket} conn WebSocket connection + * @returns {WebSocket} Returns the websocket itself + * @private + */ +WebSocketConnection.prototype._onConnection = function (from, conn) { + var me = this; + + conn.onmessage = function (event) { + me.receive(from, JSON.parse(event.data)); + }; + + conn.onclose = function () { + // remove this connection from the sockets list + delete me.sockets[from]; + }; + + conn.onerror = function (err) { + // TODO: what to do with errors? + delete me.sockets[from]; + }; + + if (this.sockets[from]) { + // there is already a connection open with remote agent + // TODO: what to do with overwriting existing sockets? + this.sockets[from].close(); + } + + // register new connection + this.sockets[from] = conn; + + return conn; +}; + +/** + * Get a list with all open sockets + * @return {String[]} Returns all open sockets + */ +WebSocketConnection.prototype.list = function () { + return Object.keys(this.sockets); +}; + +/** + * Close the connection. All open sockets will be closed and the agent will + * be unregistered from the WebSocketTransport. + */ +WebSocketConnection.prototype.close = function () { + this.closed = true; + + // close all connections + for (var id in this.sockets) { + if (this.sockets.hasOwnProperty(id)) { + this.sockets[id].close(); + } + } + this.sockets = {}; + + delete this.transport.agents[this.url]; +}; + +module.exports = WebSocketConnection; + +},{"../../util":23,"../Connection":9,"promise":114,"uuid-v4":125,"ws":126}],22:[function(_dereq_,module,exports){ +'use strict'; + +var urlModule = _dereq_('url'); +var uuid = _dereq_('uuid-v4'); +var Promise = _dereq_('promise'); +var WebSocketServer = _dereq_('ws').Server; + +var util = _dereq_('../../util'); +var Transport = _dereq_('../Transport'); +var WebSocketConnection = _dereq_('./WebSocketConnection'); + +/** + * Create a web socket transport. + * @param {Object} config Config can contain the following properties: + * - `id: string`. Optional + * - `default: boolean`. Optional + * - `url: string`. Optional. If provided, + * A WebSocket server is started on given + * url. + * - `localShortcut: boolean`. Optional. If true + * (default), messages to local agents are not + * send via WebSocket but delivered immediately + * - `reconnectDelay: number` Optional. Delay in + * milliseconds for reconnecting a broken + * connection. 10000 ms by default. Connections + * are only automatically reconnected after + * there has been an established connection. + * @constructor + */ +function WebSocketTransport(config) { + this.id = config && config.id || null; + this.networkId = this.id || null; + this['default'] = config && config['default'] || false; + this.localShortcut = (config && config.localShortcut === false) ? false : true; + this.reconnectDelay = config && config.reconnectDelay || 10000; + + this.url = config && config.url || null; + this.server = null; + + if (this.url != null) { + var urlParts = urlModule.parse(this.url); + + if (urlParts.protocol != 'ws:') throw new Error('Invalid protocol, "ws:" expected'); + if (this.url.indexOf(':id') == -1) throw new Error('":id" placeholder missing in url'); + + this.address = urlParts.protocol + '//' + urlParts.host; // the url without path, for example 'ws://localhost:3000' + this.ready = this._initServer(this.url); + } + else { + this.address = null; + this.ready = Promise.resolve(this); + } + + this.agents = {}; // WebSocketConnections of all registered agents. The keys are the urls of the agents +} + +WebSocketTransport.prototype = new Transport(); + +WebSocketTransport.prototype.type = 'ws'; + +/** + * Build an url for given id. Example: + * var url = getUrl('agent1'); // 'ws://localhost:3000/agents/agent1' + * @param {String} id + * @return {String} Returns the url, or returns null when no url placeholder + * is defined. + */ +WebSocketTransport.prototype.getUrl = function (id) { + return this.url ? this.url.replace(':id', id) : null; +}; + +/** + * Initialize a server on given url + * @param {String} url For example 'http://localhost:3000' + * @return {Promise} Returns a promise which resolves when the server is up + * and running + * @private + */ +WebSocketTransport.prototype._initServer = function (url) { + var urlParts = urlModule.parse(url); + var port = urlParts.port || 80; + + var me = this; + return new Promise(function (resolve, reject) { + me.server = new WebSocketServer({port: port}, function () { + resolve(me); + }); + + me.server.on('connection', me._onConnection.bind(me)); + + me.server.on('error', function (err) { + reject(err) + }); + }) +}; + +/** + * Handle a new connection. The connection is added to the addressed agent. + * @param {WebSocket} conn + * @private + */ +WebSocketTransport.prototype._onConnection = function (conn) { + var url = conn.upgradeReq.url; + var urlParts = urlModule.parse(url, true); + var toPath = urlParts.pathname; + var to = util.normalizeURL(this.address + toPath); + + // read sender id from query parameters or generate a random uuid + var queryParams = urlParts.query; + var from = queryParams.id || uuid(); + // TODO: make a config option to allow/disallow anonymous connections? + //console.log('onConnection, to=', to, ', from=', from, ', agents:', Object.keys(this.agents)); // TODO: cleanup + + var agent = this.agents[to]; + if (agent) { + agent._onConnection(from, conn); + } + else { + // reject the connection + // conn.send('Error: Agent with id "' + to + '" not found'); // TODO: can we send back a message before closing? + conn.close(); + } +}; + +/** + * Connect an agent + * @param {string} id The id or url of the agent. In case of an + * url, this url should match the url of the + * WebSocket server. + * @param {Function} receive Invoked as receive(from, message) + * @return {WebSocketConnection} Returns a promise which resolves when + * connected. + */ +WebSocketTransport.prototype.connect = function(id, receive) { + var isURL = (id.indexOf('://') !== -1); + + // FIXME: it's confusing right now what the final url will be based on the provided id... + var url = isURL ? id : (this.getUrl(id) || id); + if (url) url = util.normalizeURL(url); + + // register the agents receive function + if (this.agents[url]) { + throw new Error('Agent with id ' + this.id + ' already exists'); + } + + var conn = new WebSocketConnection(this, url, receive); + this.agents[conn.url] = conn; // use conn.url, url can be changed when it was null + + return conn; +}; + +/** + * Close the transport. Removes all agent connections. + */ +WebSocketTransport.prototype.close = function() { + // close all connections + for (var id in this.agents) { + if (this.agents.hasOwnProperty(id)) { + this.agents[id].close(); + } + } + this.agents = {}; + + // close the server + if (this.server) { + this.server.close(); + } +}; + +module.exports = WebSocketTransport; + +},{"../../util":23,"../Transport":10,"./WebSocketConnection":21,"promise":114,"url":157,"uuid-v4":125,"ws":126}],23:[function(_dereq_,module,exports){ +'use strict'; + +/** + * Test whether the provided value is a Promise. + * A value is marked as a Promise when it is an object containing functions + * `then` and `catch`. + * @param {*} value + * @return {boolean} Returns true when `value` is a Promise + */ +exports.isPromise = function (value) { + return value && + typeof value['then'] === 'function' && + typeof value['catch'] === 'function' +}; + +/** + * Splits an url like "protocol://domain/path" + * @param {string} url + * @return {{protocol: string, domain: string, path: string} | null} + * Returns an object with properties protocol, domain, and path + * when there is a match. Returns null if no valid url. + * + */ +exports.parseUrl = function (url) { + // match an url like "protocol://domain/path" + var match = /^([A-z]+):\/\/([^\/]+)(\/(.*)$|$)/.exec(url); + if (match) { + return { + protocol: match[1], + domain: match[2], + path: match[4] + } + } + + return null; +}; + +/** + * Normalize a url. Removes trailing slash + * @param {string} url + * @return {string} Returns the normalized url + */ +exports.normalizeURL = function (url) { + if (url[url.length - 1] == '/') { + return url.substring(0, url.length - 1); + } + else { + return url; + } +}; + +},{}],24:[function(_dereq_,module,exports){ +'use strict'; +var Connection = _dereq_('./lib/connection'); + +module.exports = { + Connection: Connection, + createConnection: function (options, implOptions, readyCallback) { + var c = new Connection(options, implOptions, readyCallback); + c.connect(); + return c; + } +}; + +},{"./lib/connection":28}],25:[function(_dereq_,module,exports){ +// Copyright (c) 2008, Fair Oaks Labs, Inc. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, this list +// of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, this +// list of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// * Neither the name of Fair Oaks Labs, Inc. nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +// Modified from original JSPack +exports.jspack = function (bigEndian) { + this.bigEndian = bigEndian; +} + +exports.jspack.prototype._DeArray = function (a, p, l) { + return [a.slice(p, p + l)]; +}; + +exports.jspack.prototype._EnArray = function (a, p, l, v) { + for (var i = 0; i < l; ++i) { + a[p + i] = v[i] ? v[i] : 0; + } +}; + +exports.jspack.prototype._DeChar = function (a, p) { + return String.fromCharCode(a[p]); +}; + +exports.jspack.prototype._EnChar = function (a, p, v) { + a[p] = v.charCodeAt(0); +}; + +exports.jspack.prototype._DeInt = function (a, p) { + var lsb = bigEndian ? format.len - 1 : 0; + var nsb = bigEndian ? -1 : 1; + var stp = lsb + nsb * format.len, + rv; + var ret = 0; + + var i = lsb; + var f = 1; + while (i != stp) { + rv += a[p + i] * f; + i += nsb; + f *= 256; + } + + if (format.signed) { + if ((rv & Math.pow(2, format.len * 8 - 1)) != 0) { + rv -= Math.pow(2, format.len * 8); + } + } + + return rv; +}; + +exports.jspack.prototype._EnInt = function (a, p, v) { + var lsb = bigEndian ? format.len - 1 : 0; + var nsb = bigEndian ? -1 : 1; + var stp = lsb + nsb * format.len; + + v = v < format.min ? format.min : ((v > format.max) ? format.max : v); + + var i = lsb; + while (i != stp) { + a[p + i] = v & 0xff; + i += nsb; + v >>= 8; + } +}; + +exports.jspack.prototype._DeString = function (a, p, l) { + var rv = new Array(1); + + for (i = 0; i < l; i++) { + rv[i] = String.fromCharCode(a[p + i]); + } + + return rv.join(''); +}; + +exports.jspack.prototype._EnString = function (a, p, l, v) { + for (var t, i = 0; i < l; ++i) { + t = v.charCodeAt(i); + if (!t) t = 0; + + a[p + i] = t; + } +}; + +exports.jspack.prototype._De754 = function (a, p) { + var s, e, m, i, d, bits, bit, len, bias, max; + + bit = format.bit; + len = format.len * 8 - format.bit - 1; + max = (1 << len) - 1; + bias = max >> 1; + + i = bigEndian ? 0 : format.len - 1; + d = bigEndian ? 1 : -1;; + s = a[p + i]; + i = i + d; + + bits = -7; + + e = s & ((1 << -bits) - 1); + s >>= -bits; + + for (bits += len; bits > 0; bits -= 8) { + e = e * 256 + a[p + i]; + i += d; + } + + m = e & ((1 << -bits) - 1); + e >>= -bits; + + for (bits += bit; bits > 0; bits -= 8) { + m = m * 256 + a[p + i]; + i += d; + } + + switch (e) { + case 0: + // Zero, or denormalized number + e = 1 - bias; + break; + + case max: + // NaN, or +/-Infinity + return m ? NaN : ((s ? -1 : 1) * Infinity); + + default: + // Normalized number + m = m + Math.pow(2, bit); + e = e - bias; + break; + } + + return (s ? -1 : 1) * m * Math.pow(2, e - bit); +}; + +exports.jspack.prototype._En754 = function (a, p, v) { + var s, e, m, i, d, c, bit, len, bias, max; + + bit = format.bit; + len = format.len * 8 - format.bit - 1; + max = (1 << len) - 1; + bias = max >> 1; + + s = v < 0 ? 1 : 0; + v = Math.abs(v); + + if (isNaN(v) || (v == Infinity)) { + m = isNaN(v) ? 1 : 0; + e = max; + } else { + e = Math.floor(Math.log(v) / Math.LN2); // Calculate log2 of the value + c = Math.pow(2, -e); + if (v * c < 1) { + e--; + c = c * 2; + } + + // Round by adding 1/2 the significand's LSD + if (e + bias >= 1) { + v += format.rt / c; // Normalized: bit significand digits + } else { + v += format.rt * Math.pow(2, 1 - bias); // Denormalized: <= bit significand digits + } + if (v * c >= 2) { + e++; + c = c / 2; // Rounding can increment the exponent + } + + if (e + bias >= max) { // overflow + m = 0; + e = max; + } else if (e + bias >= 1) { // normalized + m = (v * c - 1) * Math.pow(2, bit); // do not reorder this expression + e = e + bias; + } else { + // Denormalized - also catches the '0' case, somewhat by chance + m = v * Math.pow(2, bias - 1) * Math.pow(2, bit); + e = 0; + } + } + + i = bigEndian ? format.len - 1 : 0; + d = bigEndian ? -1 : 1;; + + while (bit >= 8) { + a[p + i] = m & 0xff; + i += d; + m /= 256; + bit -= 8; + } + + e = (e << bit) | m; + for (len += bit; len > 0; len -= 8) { + a[p + i] = e & 0xff; + i += d; + e /= 256; + } + + a[p + i - d] |= s * 128; +}; + +// Unpack a series of n formatements of size s from array a at offset p with fxn +exports.jspack.prototype._UnpackSeries = function (n, s, a, p) { + var fxn = format.de; + + var ret = []; + for (var i = 0; i < n; i++) { + ret.push(fxn(a, p + i * s)); + } + + return ret; +}; + +// Pack a series of n formatements of size s from array v at offset i to array a at offset p with fxn +exports.jspack.prototype._PackSeries = function (n, s, a, p, v, i) { + var fxn = format.en; + + for (o = 0; o < n; o++) { + fxn(a, p + o * s, v[i + o]); + } +}; + +// Unpack the octet array a, beginning at offset p, according to the fmt string +exports.jspack.prototype.Unpack = function (fmt, a, p) { + bigEndian = fmt.charAt(0) != '<'; + + if (p == undefined || p == null) p = 0; + + var re = new RegExp(this._sPattern, 'g'); + + var ret = []; + + for (var m; m = re.exec(fmt); /* */ ) { + var n; + if (m[1] == undefined || m[1] == '') n = 1; + else n = parseInt(m[1]); + + var s = this._lenLut[m[2]]; + + if ((p + n * s) > a.length) return undefined; + + switch (m[2]) { + case 'A': + case 's': + rv.push(this._formatLut[m[2]].de(a, p, n)); + break; + case 'c': + case 'b': + case 'B': + case 'h': + case 'H': + case 'i': + case 'I': + case 'l': + case 'L': + case 'f': + case 'd': + format = this._formatLut[m[2]]; + ret.push(this._UnpackSeries(n, s, a, p)); + break; + } + + p += n * s; + } + + return Array.prototype.concat.apply([], ret); +}; + +// Pack the supplied values into the octet array a, beginning at offset p, according to the fmt string +exports.jspack.prototype.PackTo = function (fmt, a, p, values) { + bigEndian = (fmt.charAt(0) != '<'); + + var re = new RegExp(this._sPattern, 'g'); + + for (var m, i = 0; m = re.exec(fmt); /* */ ) { + var n; + if (m[1] == undefined || m[1] == '') n = 1; + else n = parseInt(m[1]); + + var s = this._lenLut[m[2]]; + + if ((p + n * s) > a.length) return false; + + switch (m[2]) { + case 'A': + case 's': + if ((i + 1) > values.length) return false; + + this._formatLut[m[2]].en(a, p, n, values[i]); + + i += 1; + break; + + case 'c': + case 'b': + case 'B': + case 'h': + case 'H': + case 'i': + case 'I': + case 'l': + case 'L': + case 'f': + case 'd': + format = this._formatLut[m[2]]; + + if (i + n > values.length) return false; + + this._PackSeries(n, s, a, p, values, i); + + i += n; + break; + + case 'x': + for (var j = 0; j < n; j++) { + a[p + j] = 0; + } + break; + } + + p += n * s; + } + + return a; +}; + +// Pack the supplied values into a new octet array, according to the fmt string +exports.jspack.prototype.Pack = function (fmt, values) { + return this.PackTo(fmt, new Array(this.CalcLength(fmt)), 0, values); +}; + +// Determine the number of bytes represented by the format string +exports.jspack.prototype.CalcLength = function (fmt) { + var re = new RegExp(this._sPattern, 'g'); + var sz = 0; + + while (match = re.exec(fmt)) { + var n; + if (match[1] == undefined || match[1] == '') n = 1; + else n = parseInt(match[1]); + + sz += n * this._lenLut[match[2]]; + } + + return sz; +}; + +// Regular expression for counting digits +exports.jspack.prototype._sPattern = '(\\d+)?([AxcbBhHsfdiIlL])'; + +// Byte widths for associated formats +exports.jspack.prototype._lenLut = { + 'A': 1, + 'x': 1, + 'c': 1, + 'b': 1, + 'B': 1, + 'h': 2, + 'H': 2, + 's': 1, + 'f': 4, + 'd': 8, + 'i': 4, + 'I': 4, + 'l': 4, + 'L': 4 +}; + +exports.jspack.prototype._formatLut = { + 'A': { + en: exports.jspack.prototype._EnArray, + de: exports.jspack.prototype._DeArray + }, + 's': { + en: exports.jspack.prototype._EnString, + de: exports.jspack.prototype._DeString + }, + 'c': { + en: exports.jspack.prototype._EnChar, + de: exports.jspack.prototype._DeChar + }, + 'b': { + en: exports.jspack.prototype._EnInt, + de: exports.jspack.prototype._DeInt, + len: 1, + signed: true, + min: -Math.pow(2, 7), + max: Math.pow(2, 7) - 1 + }, + 'B': { + en: exports.jspack.prototype._EnInt, + de: exports.jspack.prototype._DeInt, + len: 1, + signed: false, + min: 0, + max: Math.pow(2, 8) - 1 + }, + 'h': { + en: exports.jspack.prototype._EnInt, + de: exports.jspack.prototype._DeInt, + len: 2, + signed: true, + min: -Math.pow(2, 15), + max: Math.pow(2, 15) - 1 + }, + 'H': { + en: exports.jspack.prototype._EnInt, + de: exports.jspack.prototype._DeInt, + len: 2, + signed: false, + min: 0, + max: Math.pow(2, 16) - 1 + }, + 'i': { + en: exports.jspack.prototype._EnInt, + de: exports.jspack.prototype._DeInt, + len: 4, + signed: true, + min: -Math.pow(2, 31), + max: Math.pow(2, 31) - 1 + }, + 'I': { + en: exports.jspack.prototype._EnInt, + de: exports.jspack.prototype._DeInt, + len: 4, + signed: false, + min: 0, + max: Math.pow(2, 32) - 1 + }, + 'l': { + en: exports.jspack.prototype._EnInt, + de: exports.jspack.prototype._DeInt, + len: 4, + signed: true, + min: -Math.pow(2, 31), + max: Math.pow(2, 31) - 1 + }, + 'L': { + en: exports.jspack.prototype._EnInt, + de: exports.jspack.prototype._DeInt, + len: 4, + signed: false, + min: 0, + max: Math.pow(2, 32) - 1 + }, + 'f': { + en: exports.jspack.prototype._En754, + de: exports.jspack.prototype._De754, + len: 4, + bit: 23, + rt: Math.pow(2, -24) - Math.pow(2, -77) + }, + 'd': { + en: exports.jspack.prototype._En754, + de: exports.jspack.prototype._De754, + len: 8, + bit: 52, + rt: 0 + } +}; + +},{}],26:[function(_dereq_,module,exports){ +exports.constants = [ + [1, "frameMethod"], + [2, "frameHeader"], + [3, "frameBody"], + [8, "frameHeartbeat"], + [200, "replySuccess"], + [206, "frameEnd"], + [311, "contentTooLarge"], + [313, "noConsumers"], + [320, "connectionForced"], + [402, "invalidPath"], + [403, "accessRefused"], + [404, "notFound"], + [405, "resourceLocked"], + [406, "preconditionFailed"], + [501, "frameError"], + [502, "syntaxError"], + [503, "commandInvalid"], + [504, "channelError"], + [505, "unexpectedFrame"], + [506, "resourceError"], + [530, "notAllowed"], + [540, "notImplemented"], + [541, "internalError"], + [4096, "frameMinSize"] +]; +exports.classes = [{ + "name": "connection", + "index": 10, + "fields": [], + "methods": [{ + "name": "start", + "index": 10, + "fields": [{ + "name": "versionMajor", + "domain": "octet" + }, { + "name": "versionMinor", + "domain": "octet" + }, { + "name": "serverProperties", + "domain": "table" + }, { + "name": "mechanisms", + "domain": "longstr" + }, { + "name": "locales", + "domain": "longstr" + }] + }, { + "name": "startOk", + "index": 11, + "fields": [{ + "name": "clientProperties", + "domain": "table" + }, { + "name": "mechanism", + "domain": "shortstr" + }, { + "name": "response", + "domain": "longstr" + }, { + "name": "locale", + "domain": "shortstr" + }] + }, { + "name": "secure", + "index": 20, + "fields": [{ + "name": "challenge", + "domain": "longstr" + }] + }, { + "name": "secureOk", + "index": 21, + "fields": [{ + "name": "response", + "domain": "longstr" + }] + }, { + "name": "tune", + "index": 30, + "fields": [{ + "name": "channelMax", + "domain": "short" + }, { + "name": "frameMax", + "domain": "long" + }, { + "name": "heartbeat", + "domain": "short" + }] + }, { + "name": "tuneOk", + "index": 31, + "fields": [{ + "name": "channelMax", + "domain": "short" + }, { + "name": "frameMax", + "domain": "long" + }, { + "name": "heartbeat", + "domain": "short" + }] + }, { + "name": "open", + "index": 40, + "fields": [{ + "name": "virtualHost", + "domain": "shortstr" + }, { + "name": "reserved1", + "domain": "shortstr" + }, { + "name": "reserved2", + "domain": "bit" + }] + }, { + "name": "openOk", + "index": 41, + "fields": [{ + "name": "reserved1", + "domain": "shortstr" + }] + }, { + "name": "close", + "index": 50, + "fields": [{ + "name": "replyCode", + "domain": "short" + }, { + "name": "replyText", + "domain": "shortstr" + }, { + "name": "classId", + "domain": "short" + }, { + "name": "methodId", + "domain": "short" + }] + }, { + "name": "closeOk", + "index": 51, + "fields": [] + }] +}, { + "name": "channel", + "index": 20, + "fields": [], + "methods": [{ + "name": "open", + "index": 10, + "fields": [{ + "name": "reserved1", + "domain": "shortstr" + }] + }, { + "name": "openOk", + "index": 11, + "fields": [{ + "name": "reserved1", + "domain": "longstr" + }] + }, { + "name": "flow", + "index": 20, + "fields": [{ + "name": "active", + "domain": "bit" + }] + }, { + "name": "flowOk", + "index": 21, + "fields": [{ + "name": "active", + "domain": "bit" + }] + }, { + "name": "close", + "index": 40, + "fields": [{ + "name": "replyCode", + "domain": "short" + }, { + "name": "replyText", + "domain": "shortstr" + }, { + "name": "classId", + "domain": "short" + }, { + "name": "methodId", + "domain": "short" + }] + }, { + "name": "closeOk", + "index": 41, + "fields": [] + }] +}, { + "name": "exchange", + "index": 40, + "fields": [], + "methods": [{ + "name": "declare", + "index": 10, + "fields": [{ + "name": "reserved1", + "domain": "short" + }, { + "name": "exchange", + "domain": "shortstr" + }, { + "name": "type", + "domain": "shortstr" + }, { + "name": "passive", + "domain": "bit" + }, { + "name": "durable", + "domain": "bit" + }, { + "name": "autoDelete", + "domain": "bit" + }, { + "name": "reserved2", + "domain": "bit" + }, { + "name": "reserved3", + "domain": "bit" + }, { + "name": "noWait", + "domain": "bit" + }, { + "name": "arguments", + "domain": "table" + }] + }, { + "name": "declareOk", + "index": 11, + "fields": [] + }, { + "name": "delete", + "index": 20, + "fields": [{ + "name": "reserved1", + "domain": "short" + }, { + "name": "exchange", + "domain": "shortstr" + }, { + "name": "ifUnused", + "domain": "bit" + }, { + "name": "noWait", + "domain": "bit" + }] + }, { + "name": "deleteOk", + "index": 21, + "fields": [] + }, { + "name": "bind", + "index": 30, + "fields": [{ + "name": "reserved1", + "domain": "short" + }, { + "name": "destination", + "domain": "shortstr" + }, { + "name": "source", + "domain": "shortstr" + }, { + "name": "routingKey", + "domain": "shortstr" + }, { + "name": "noWait", + "domain": "bit" + }, { + "name": "arguments", + "domain": "table" + }] + }, { + "name": "bindOk", + "index": 31, + "fields": [] + }, { + "name": "unbind", + "index": 40, + "fields": [{ + "name": "reserved1", + "domain": "short" + }, { + "name": "destination", + "domain": "shortstr" + }, { + "name": "source", + "domain": "shortstr" + }, { + "name": "routingKey", + "domain": "shortstr" + }, { + "name": "noWait", + "domain": "bit" + }, { + "name": "arguments", + "domain": "table" + }] + }, { + "name": "unbindOk", + "index": 51, + "fields": [] + }] +}, { + "name": "queue", + "index": 50, + "fields": [], + "methods": [{ + "name": "declare", + "index": 10, + "fields": [{ + "name": "reserved1", + "domain": "short" + }, { + "name": "queue", + "domain": "shortstr" + }, { + "name": "passive", + "domain": "bit" + }, { + "name": "durable", + "domain": "bit" + }, { + "name": "exclusive", + "domain": "bit" + }, { + "name": "autoDelete", + "domain": "bit" + }, { + "name": "noWait", + "domain": "bit" + }, { + "name": "arguments", + "domain": "table" + }] + }, { + "name": "declareOk", + "index": 11, + "fields": [{ + "name": "queue", + "domain": "shortstr" + }, { + "name": "messageCount", + "domain": "long" + }, { + "name": "consumerCount", + "domain": "long" + }] + }, { + "name": "bind", + "index": 20, + "fields": [{ + "name": "reserved1", + "domain": "short" + }, { + "name": "queue", + "domain": "shortstr" + }, { + "name": "exchange", + "domain": "shortstr" + }, { + "name": "routingKey", + "domain": "shortstr" + }, { + "name": "noWait", + "domain": "bit" + }, { + "name": "arguments", + "domain": "table" + }] + }, { + "name": "bindOk", + "index": 21, + "fields": [] + }, { + "name": "unbind", + "index": 50, + "fields": [{ + "name": "reserved1", + "domain": "short" + }, { + "name": "queue", + "domain": "shortstr" + }, { + "name": "exchange", + "domain": "shortstr" + }, { + "name": "routingKey", + "domain": "shortstr" + }, { + "name": "arguments", + "domain": "table" + }] + }, { + "name": "unbindOk", + "index": 51, + "fields": [] + }, { + "name": "purge", + "index": 30, + "fields": [{ + "name": "reserved1", + "domain": "short" + }, { + "name": "queue", + "domain": "shortstr" + }, { + "name": "noWait", + "domain": "bit" + }] + }, { + "name": "purgeOk", + "index": 31, + "fields": [{ + "name": "messageCount", + "domain": "long" + }] + }, { + "name": "delete", + "index": 40, + "fields": [{ + "name": "reserved1", + "domain": "short" + }, { + "name": "queue", + "domain": "shortstr" + }, { + "name": "ifUnused", + "domain": "bit" + }, { + "name": "ifEmpty", + "domain": "bit" + }, { + "name": "noWait", + "domain": "bit" + }] + }, { + "name": "deleteOk", + "index": 41, + "fields": [{ + "name": "messageCount", + "domain": "long" + }] + }] +}, { + "name": "basic", + "index": 60, + "fields": [{ + "name": "contentType", + "domain": "shortstr" + }, { + "name": "contentEncoding", + "domain": "shortstr" + }, { + "name": "headers", + "domain": "table" + }, { + "name": "deliveryMode", + "domain": "octet" + }, { + "name": "priority", + "domain": "octet" + }, { + "name": "correlationId", + "domain": "shortstr" + }, { + "name": "replyTo", + "domain": "shortstr" + }, { + "name": "expiration", + "domain": "shortstr" + }, { + "name": "messageId", + "domain": "shortstr" + }, { + "name": "timestamp", + "domain": "timestamp" + }, { + "name": "type", + "domain": "shortstr" + }, { + "name": "userId", + "domain": "shortstr" + }, { + "name": "appId", + "domain": "shortstr" + }, { + "name": "reserved", + "domain": "shortstr" + }], + "methods": [{ + "name": "qos", + "index": 10, + "fields": [{ + "name": "prefetchSize", + "domain": "long" + }, { + "name": "prefetchCount", + "domain": "short" + }, { + "name": "global", + "domain": "bit" + }] + }, { + "name": "qosOk", + "index": 11, + "fields": [] + }, { + "name": "consume", + "index": 20, + "fields": [{ + "name": "reserved1", + "domain": "short" + }, { + "name": "queue", + "domain": "shortstr" + }, { + "name": "consumerTag", + "domain": "shortstr" + }, { + "name": "noLocal", + "domain": "bit" + }, { + "name": "noAck", + "domain": "bit" + }, { + "name": "exclusive", + "domain": "bit" + }, { + "name": "noWait", + "domain": "bit" + }, { + "name": "arguments", + "domain": "table" + }] + }, { + "name": "consumeOk", + "index": 21, + "fields": [{ + "name": "consumerTag", + "domain": "shortstr" + }] + }, { + "name": "cancel", + "index": 30, + "fields": [{ + "name": "consumerTag", + "domain": "shortstr" + }, { + "name": "noWait", + "domain": "bit" + }] + }, { + "name": "cancelOk", + "index": 31, + "fields": [{ + "name": "consumerTag", + "domain": "shortstr" + }] + }, { + "name": "publish", + "index": 40, + "fields": [{ + "name": "reserved1", + "domain": "short" + }, { + "name": "exchange", + "domain": "shortstr" + }, { + "name": "routingKey", + "domain": "shortstr" + }, { + "name": "mandatory", + "domain": "bit" + }, { + "name": "immediate", + "domain": "bit" + }] + }, { + "name": "return", + "index": 50, + "fields": [{ + "name": "replyCode", + "domain": "short" + }, { + "name": "replyText", + "domain": "shortstr" + }, { + "name": "exchange", + "domain": "shortstr" + }, { + "name": "routingKey", + "domain": "shortstr" + }] + }, { + "name": "deliver", + "index": 60, + "fields": [{ + "name": "consumerTag", + "domain": "shortstr" + }, { + "name": "deliveryTag", + "domain": "longlong" + }, { + "name": "redelivered", + "domain": "bit" + }, { + "name": "exchange", + "domain": "shortstr" + }, { + "name": "routingKey", + "domain": "shortstr" + }] + }, { + "name": "get", + "index": 70, + "fields": [{ + "name": "reserved1", + "domain": "short" + }, { + "name": "queue", + "domain": "shortstr" + }, { + "name": "noAck", + "domain": "bit" + }] + }, { + "name": "getOk", + "index": 71, + "fields": [{ + "name": "deliveryTag", + "domain": "longlong" + }, { + "name": "redelivered", + "domain": "bit" + }, { + "name": "exchange", + "domain": "shortstr" + }, { + "name": "routingKey", + "domain": "shortstr" + }, { + "name": "messageCount", + "domain": "long" + }] + }, { + "name": "getEmpty", + "index": 72, + "fields": [{ + "name": "reserved1", + "domain": "shortstr" + }] + }, { + "name": "ack", + "index": 80, + "fields": [{ + "name": "deliveryTag", + "domain": "longlong" + }, { + "name": "multiple", + "domain": "bit" + }] + }, { + "name": "reject", + "index": 90, + "fields": [{ + "name": "deliveryTag", + "domain": "longlong" + }, { + "name": "requeue", + "domain": "bit" + }] + }, { + "name": "recoverAsync", + "index": 100, + "fields": [{ + "name": "requeue", + "domain": "bit" + }] + }, { + "name": "recover", + "index": 110, + "fields": [{ + "name": "requeue", + "domain": "bit" + }] + }, { + "name": "recoverOk", + "index": 111, + "fields": [] + }] +}, { + "name": "tx", + "index": 90, + "fields": [], + "methods": [{ + "name": "select", + "index": 10, + "fields": [] + }, { + "name": "selectOk", + "index": 11, + "fields": [] + }, { + "name": "commit", + "index": 20, + "fields": [] + }, { + "name": "commitOk", + "index": 21, + "fields": [] + }, { + "name": "rollback", + "index": 30, + "fields": [] + }, { + "name": "rollbackOk", + "index": 31, + "fields": [] + }] +}, { + "name": "confirm", + "index": 85, + "fields": [], + "methods": [{ + "name": "select", + "index": 10, + "fields": [{ + "name": "noWait", + "domain": "bit" + }] + }, { + "name": "selectOk", + "index": 11, + "fields": [] + }] +}]; +},{}],27:[function(_dereq_,module,exports){ +'use strict'; +var events = _dereq_('events'); +var util = _dereq_('util'); +var fs = _dereq_('fs'); +var Promise = _dereq_('./promise').Promise; +var definitions = _dereq_('./definitions'); +var methods = definitions.methods; + +// This class is not exposed to the user. Queue and Exchange are subclasses +// of Channel. This just provides a task queue. +var Channel = module.exports = function Channel (connection, channel) { + events.EventEmitter.call(this); + + // Unlimited listeners. Helps when e.g. publishing high-volume messages, + // 10 is far too low. + this.setMaxListeners(0); + + this.channel = channel; + this.connection = connection; + this._tasks = []; + + this.reconnect(); +}; +util.inherits(Channel, events.EventEmitter); + +Channel.prototype.closeOK = function() { + this.connection._sendMethod(this.channel, methods.channelCloseOk, {reserved1: ""}); +}; + +Channel.prototype.reconnect = function () { + this.connection._sendMethod(this.channel, methods.channelOpen, {reserved1: ""}); +}; + +Channel.prototype._taskPush = function (reply, cb) { + var promise = new Promise(); + this._tasks.push({ + promise: promise, + reply: reply, + sent: false, + cb: cb + }); + this._tasksFlush(); + return promise; +}; + +Channel.prototype._tasksFlush = function () { + if (this.state != 'open') return; + + for (var i = 0; i < this._tasks.length; i++) { + var task = this._tasks[i]; + if (task.sent) continue; + task.cb(); + task.sent = true; + if (!task.reply) { + // if we don't expect a reply, just delete it now + this._tasks.splice(i, 1); + i = i-1; + } + } +}; + +Channel.prototype._handleTaskReply = function (channel, method, args) { + var task, i; + + for (i = 0; i < this._tasks.length; i++) { + if (this._tasks[i].reply == method) { + task = this._tasks[i]; + this._tasks.splice(i, 1); + task.promise.emitSuccess(args); + this._tasksFlush(); + return true; + } + } + + return false; +}; + +Channel.prototype._onChannelMethod = function(channel, method, args) { + switch (method) { + case methods.channelCloseOk: + delete this.connection.channels[this.channel]; + this.state = 'closed'; + // TODO should this be falling through? + default: + this._onMethod(channel, method, args); + } +}; + +Channel.prototype.close = function(reason) { + this.state = 'closing'; + this.connection._sendMethod(this.channel, methods.channelClose, + {'replyText': reason ? reason : 'Goodbye from node', + 'replyCode': 200, + 'classId': 0, + 'methodId': 0}); +}; + +},{"./definitions":31,"./promise":35,"events":137,"fs":127,"util":159}],28:[function(_dereq_,module,exports){ +(function (process,Buffer){ +'use strict'; +var net = _dereq_('net'); +var tls = _dereq_('tls'); +var fs = _dereq_('fs'); +var URL = _dereq_('url'); +var _ = _dereq_('lodash'); +var debug = _dereq_('./debug'); +var EventEmitter = _dereq_('events').EventEmitter; +var util = _dereq_('util'); +var serializer = _dereq_('./serializer'); +var definitions = _dereq_('./definitions'); +var methods = definitions.methods; +var methodTable = definitions.methodTable; +var classes = definitions.classes; +var Exchange = _dereq_('./exchange'); +var Queue = _dereq_('./queue'); +var AMQPParser = _dereq_('./parser'); +var nodeAMQPVersion = _dereq_('../package').version; + +var maxFrameBuffer = 131072; // 128k, same as rabbitmq (which was + // copying qpid) + +var defaultPorts = { 'amqp': 5672, 'amqps': 5671 }; + +var defaultOptions = { + host: 'localhost', + port: defaultPorts['amqp'], + login: 'guest', + password: 'guest', + authMechanism: 'AMQPLAIN', + vhost: '/', + connectionTimeout: 10000, + ssl: { + enabled: false + } +}; + +var defaultSslOptions = { + port: defaultPorts['amqps'], + ssl: { + rejectUnauthorized: true + } +}; + +var defaultImplOptions = { + defaultExchangeName: '', + reconnect: true, + reconnectBackoffStrategy: 'linear', + reconnectExponentialLimit: 120000, + reconnectBackoffTime: 1000 +}; + +var defaultClientProperties = { + version: nodeAMQPVersion, + platform: 'node-' + process.version, + product: 'node-amqp' +}; + +var Connection = module.exports = function Connection (connectionArgs, options, readyCallback) { + EventEmitter.call(this); + this.setOptions(connectionArgs); + this.setImplOptions(options); + + if (typeof readyCallback === 'function') { + this._readyCallback = readyCallback; + } + + this.connectionAttemptScheduled = false; + this._defaultExchange = null; + this.channelCounter = 0; + this._sendBuffer = new Buffer(maxFrameBuffer); +}; +util.inherits(Connection, EventEmitter); + + + +Connection.prototype.setOptions = function (options) { + var urlo = (options && options.url) ? this._parseURLOptions(options.url) : {}; + var sslo = (options && options.ssl && options.ssl.enabled) ? defaultSslOptions : {}; + this.options = _.extend({}, defaultOptions, sslo, urlo, options || {}); + this.options.clientProperties = _.extend({}, defaultClientProperties, (options && options.clientProperties) || {}); +}; + +Connection.prototype.setImplOptions = function (options) { + this.implOptions = _.extend({}, defaultImplOptions, options || {}); +}; + +Connection.prototype.connect = function () { + // If this is our first connection, add listeners. + if (!this.socket) this.addAllListeners(); + + this._createSocket(); + this._startHandshake(); +}; + +Connection.prototype.reconnect = function () { + // Suspend activity on channels + for (var channel in this.channels) { + this.channels[channel].state = 'closed'; + } + debug && debug("Connection lost, reconnecting..."); + // Terminate socket activity + if (this.socket) this.socket.end(); + this.connect(); +}; + +Connection.prototype.disconnect = function () { + debug && debug("Sending disconnect request to server"); + this._sendMethod(0, methods.connectionClose, { + 'replyText': 'client disconnect', + 'replyCode': 200, + 'classId': 0, + 'methodId': 0 + }); +}; + +Connection.prototype.addAllListeners = function() { + var self = this; + var connectEvent = this.options.ssl.enabled ? 'secureConnect' : 'connect'; + + + self.addListener(connectEvent, function() { + // In the case where this is a reconnection, do not trample on the existing + // channels. + // For your reference, channel 0 is the control channel. + self.channels = self.channels || {0:self}; + self.queues = self.queues || {}; + self.exchanges = self.exchanges || {}; + + self.parser = new AMQPParser('0-9-1', 'client'); + + self.parser.onMethod = function (channel, method, args) { + self._onMethod(channel, method, args); + }; + + self.parser.onContent = function (channel, data) { + debug && debug(channel + " > content " + data.length); + if (self.channels[channel] && self.channels[channel]._onContent) { + self.channels[channel]._onContent(channel, data); + } else { + debug && debug("unhandled content: " + data); + } + }; + + self.parser.onContentHeader = function (channel, classInfo, weight, properties, size) { + debug && debug(channel + " > content header " + JSON.stringify([classInfo.name, weight, properties, size])); + if (self.channels[channel] && self.channels[channel]._onContentHeader) { + self.channels[channel]._onContentHeader(channel, classInfo, weight, properties, size); + } else { + debug && debug("unhandled content header"); + } + }; + + self.parser.onHeartBeat = function () { + self.emit("heartbeat"); + debug && debug("heartbeat"); + }; + + self.parser.onError = function (e) { + self.emit("error", e); + self.emit("close"); + }; + + // Remove readyEmitted flag so we can detect an auth error. + self.readyEmitted = false; + }); + + self.addListener('data', function (data) { + if(self.parser != null){ + try { + self.parser.execute(data); + } catch (exception) { + self.emit('error', exception); + return; + } + } + self._inboundHeartbeatTimerReset(); + }); + + var backoffTime = null; + self.addListener('error', function backoff(e) { + if (self._inboundHeartbeatTimer !== null) { + clearTimeout(self._inboundHeartbeatTimer); + self._inboundHeartbeatTimer = null; + } + if (self._outboundHeartbeatTimer !== null) { + clearTimeout(self._outboundHeartbeatTimer); + self._outboundHeartbeatTimer = null; + } + + if (!self.connectionAttemptScheduled) { + // Set to true, as we are presently in the process of scheduling one. + self.connectionAttemptScheduled = true; + + // Kill the socket, if it hasn't been killed already. + self.socket.end(); + + // Reset parser state + self.parser = null; + + // In order for our reconnection to be seamless, we have to notify the + // channels that they are no longer connected so that nobody attempts + // to send messages which would be doomed to fail. + for (var channel in self.channels) { + if (channel !== 0) { + self.channels[channel].state = 'closed'; + } + } + // Queues are channels (so we have already marked them as closed), but + // queues have special needs, since the subscriptions will no longer + // be known to the server when we reconnect. Mark the subscriptions as + // closed so that we can resubscribe them once we are reconnected. + for (var queue in self.queues) { + for (var index in self.queues[queue].consumerTagOptions) { + self.queues[queue].consumerTagOptions[index]['state'] = 'closed'; + } + } + + // Begin reconnection attempts + if (self.implOptions.reconnect) { + // Don't thrash, use a backoff strategy. + if (backoffTime === null) { + // This is the first time we've failed since a successful connection, + // so use the configured backoff time without any modification. + backoffTime = self.implOptions.reconnectBackoffTime; + } else if (self.implOptions.reconnectBackoffStrategy === 'exponential') { + // If you've configured exponential backoff, we'll double the + // backoff time each subsequent attempt until success. + backoffTime *= 2; + // limit the maxium timeout, to avoid potentially unlimited stalls + if(backoffTime > self.implOptions.reconnectExponentialLimit){ + backoffTime = self.implOptions.reconnectExponentialLimit; + } + + } else if (self.implOptions.reconnectBackoffStrategy === 'linear') { + // Linear strategy is the default. In this case, we will retry at a + // constant interval, so there's no need to change the backoff time + // between attempts. + } else { + // TODO should we warn people if they picked a nonexistent strategy? + } + + setTimeout(function () { + // Set to false, so that if we fail in the reconnect attempt, we can + // schedule another one. + self.connectionAttemptScheduled = false; + self.reconnect(); + }, backoffTime); + } else { + self.removeListener('error', backoff); + } + } + }); + + self.addListener('ready', function () { + // Reset the backoff time since we have successfully connected. + backoffTime = null; + + if (self.implOptions.reconnect) { + // Reconnect any channels which were open. + _.each(self.channels, function(channel, index) { + // FIXME why is the index "0" instead of 0? + if (index !== "0") channel.reconnect(); + }); + } + + // Set 'ready' flag for auth failure detection. + this.readyEmitted = true; + + // Restart the heartbeat to the server + self._outboundHeartbeatTimerReset(); + }); + + // Apparently, it is not possible to determine if an authentication error + // has occurred, but when the connection closes then we can HINT that a + // possible authentication error has occured. Although this may be a bug + // in the spec, handling it as a possible error is considerably better than + // failing silently. + self.addListener('end', function (){ + if (!this.readyEmitted){ + this.emit('error', new Error( + 'Connection ended: possibly due to an authentication failure.' + )); + } + }); +}; + +Connection.prototype.heartbeat = function () { + if(this.socket.writable) this.write(new Buffer([8,0,0,0,0,0,0,206])); +}; + +// connection.exchange('my-exchange', { type: 'topic' }); +// Options +// - type 'fanout', 'direct', or 'topic' (default) +// - passive (boolean) +// - durable (boolean) +// - autoDelete (boolean, default true) +Connection.prototype.exchange = function (name, options, openCallback) { + if (name === undefined) name = this.implOptions.defaultExchangeName; + + if (!options) options = {}; + if (name !== '' && options.type === undefined) options.type = 'topic'; + + try{ + var channel = this.generateChannelId(); + }catch(exception){ + this.emit("error", exception); + return; + } + var exchange = new Exchange(this, channel, name, options, openCallback); + this.channels[channel] = exchange; + this.exchanges[name] = exchange; + return exchange; +}; + +// remove an exchange when it's closed (called from Exchange) +Connection.prototype.exchangeClosed = function (name) { + if (this.exchanges[name]) delete this.exchanges[name]; +}; + +// Options +// - passive (boolean) +// - durable (boolean) +// - exclusive (boolean) +// - autoDelete (boolean, default true) +Connection.prototype.queue = function (name /* options, openCallback */) { + var options, callback; + if (typeof arguments[1] == 'object') { + options = arguments[1]; + callback = arguments[2]; + } else { + callback = arguments[1]; + } + + try{ + var channel = this.generateChannelId(); + }catch(exception){ + this.emit("error", exception); + return; + } + + var q = new Queue(this, channel, name, options, callback); + this.channels[channel] = q; + return q; +}; + +// remove a queue when it's closed (called from Queue) +Connection.prototype.queueClosed = function (name) { + if (this.queues[name]) delete this.queues[name]; +}; + +// Publishes a message to the default exchange. +Connection.prototype.publish = function (routingKey, body, options, callback) { + if (!this._defaultExchange) this._defaultExchange = this.exchange(); + return this._defaultExchange.publish(routingKey, body, options, callback); +}; + +Connection.prototype._bodyToBuffer = function (body) { + // Handles 3 cases + // - body is utf8 string + // - body is instance of Buffer + // - body is an object and its JSON representation is sent + // Does not handle the case for streaming bodies. + // Returns buffer. + if (typeof(body) == 'string') { + return [null, new Buffer(body, 'utf8')]; + } else if (body instanceof Buffer) { + return [null, body]; + } else { + var jsonBody = JSON.stringify(body); + + debug && debug('sending json: ' + jsonBody); + + var props = {contentType: 'application/json'}; + return [props, new Buffer(jsonBody, 'utf8')]; + } +}; + +Connection.prototype._inboundHeartbeatTimerReset = function () { + if (this._inboundHeartbeatTimer !== null) { + clearTimeout(this._inboundHeartbeatTimer); + this._inboundHeartbeatTimer = null; + } + if (this.options.heartbeat) { + var self = this; + var gracePeriod = 2 * this.options.heartbeat; + this._inboundHeartbeatTimer = setTimeout(function () { + if(self.socket.readable) + self.emit('error', new Error('no heartbeat or data in last ' + gracePeriod + ' seconds')); + }, gracePeriod * 1000); + } +}; + +Connection.prototype._outboundHeartbeatTimerReset = function () { + if (this._outboundHeartbeatTimer !== null) { + clearTimeout(this._outboundHeartbeatTimer); + this._outboundHeartbeatTimer = null; + } + if (this.socket.writable && this.options.heartbeat) { + var self = this; + this._outboundHeartbeatTimer = setTimeout(function () { + self.heartbeat(); + self._outboundHeartbeatTimerReset(); + }, 1000 * this.options.heartbeat); + } +}; + +Connection.prototype._onMethod = function (channel, method, args) { + debug && debug(channel + " > " + method.name + " " + JSON.stringify(args)); + + // Channel 0 is the control channel. If not zero then delegate to + // one of the channel objects. + + if (channel > 0) { + if (!this.channels[channel]) { + debug && debug("Received message on untracked channel."); + return; + } + if (!this.channels[channel]._onChannelMethod) { + throw new Error('Channel ' + channel + ' has no _onChannelMethod method.'); + } + this.channels[channel]._onChannelMethod(channel, method, args); + return; + } + + // channel 0 + + switch (method) { + // 2. The server responds, after the version string, with the + // 'connectionStart' method (contains various useless information) + case methods.connectionStart: + // We check that they're serving us AMQP 0-9 + if (args.versionMajor !== 0 && args.versionMinor != 9) { + this.socket.end(); + this.emit('error', new Error("Bad server version")); + return; + } + this.serverProperties = args.serverProperties; + // 3. Then we reply with StartOk, containing our useless information. + this._sendMethod(0, methods.connectionStartOk, { + clientProperties: this.options.clientProperties, + mechanism: this.options.authMechanism, + response: { + LOGIN: this.options.login, + PASSWORD: this.options.password + }, + locale: 'en_US' + }); + break; + + // 4. The server responds with a connectionTune request + case methods.connectionTune: + if (args.frameMax) { + debug && debug("tweaking maxFrameBuffer to " + args.frameMax); + maxFrameBuffer = args.frameMax; + } + // 5. We respond with connectionTuneOk + this._sendMethod(0, methods.connectionTuneOk, { + channelMax: 0, + frameMax: maxFrameBuffer, + heartbeat: this.options.heartbeat || 0 + }); + // 6. Then we have to send a connectionOpen request + this._sendMethod(0, methods.connectionOpen, { + virtualHost: this.options.vhost + // , capabilities: '' + // , insist: true + , + reserved1: '', + reserved2: true + }); + break; + + + case methods.connectionOpenOk: + // 7. Finally they respond with connectionOpenOk + // Whew! That's why they call it the Advanced MQP. + if (this._readyCallback) { + this._readyCallback(this); + this._readyCallback = null; + } + this.emit('ready'); + break; + + case methods.connectionClose: + var e = new Error(args.replyText); + e.code = args.replyCode; + if (!this.listeners('close').length) { + console.log('Unhandled connection error: ' + args.replyText); + } + this.socket.destroy(e); + break; + + case methods.connectionCloseOk: + debug && debug("Received close-ok from server, closing socket"); + this.socket.end(); + break; + + default: + throw new Error("Uncaught method '" + method.name + "' with args " + + JSON.stringify(args)); + } +}; + +// Generate connection options from URI string formatted with amqp scheme. +Connection.prototype._parseURLOptions = function(connectionString) { + var opts = {}; + opts.ssl = {}; + var url = URL.parse(connectionString); + var scheme = url.protocol.substring(0, url.protocol.lastIndexOf(':')); + if (scheme != 'amqp' && scheme != 'amqps') { + throw new Error('Connection URI must use amqp or amqps scheme. ' + + 'For example, "amqp://bus.megacorp.internal:5766".'); + } + opts.ssl.enabled = ('amqps' === scheme); + opts.host = url.hostname; + opts.port = url.port || defaultPorts[scheme]; + if (url.auth) { + var auth = url.auth.split(':'); + auth[0] && (opts.login = auth[0]); + auth[1] && (opts.password = auth[1]); + } + if (url.pathname) { + opts.vhost = unescape(url.pathname.substr(1)); + } + return opts; +}; + +/* + * + * Connect helpers + * + */ + +// If you pass a array of hosts, lets choose a random host or the preferred host number, or then next one. +Connection.prototype._chooseHost = function() { + if(Array.isArray(this.options.host)){ + if(this.hosti == null){ + if(typeof this.options.hostPreference == 'number') { + this.hosti = (this.options.hostPreference < this.options.host.length) ? + this.options.hostPreference : this.options.host.length-1; + } else { + this.hosti = parseInt(Math.random() * this.options.host.length, 10); + } + } else { + // If this is already set, it looks like we want to choose another one. + // Add one to hosti but don't overflow it. + this.hosti = (this.hosti + 1) % this.options.host.length; + } + return this.options.host[this.hosti]; + } else { + return this.options.host; + } +}; + +Connection.prototype._createSocket = function() { + var hostName = this._chooseHost(), self = this; + + var options = { + port: this.options.port, + host: hostName + }; + + var resetConnectionTimeout = function () { + debug && debug('connected so resetting connection timeout'); + this.setTimeout(0); + }; + + // Connect socket + if (this.options.ssl.enabled) { + debug && debug('making ssl connection'); + options = _.extend(options, this._getSSLOptions()); + this.socket = tls.connect(options, resetConnectionTimeout); + } else { + debug && debug('making non-ssl connection'); + this.socket = net.connect(options, resetConnectionTimeout); + } + var connTimeout = this.options.connectionTimeout; + if (connTimeout) { + debug && debug('setting connection timeout to ' + connTimeout); + this.socket.setTimeout(connTimeout, function () { + debug && debug('connection timeout'); + this.destroy(); + var e = new Error('connection timeout'); + e.name = 'TimeoutError'; + self.emit('error', e); + }); + } + + // Proxy events. + // Note that if we don't attach a 'data' event, no data will flow. + var events = ['close', 'connect', 'data', 'drain', 'error', 'end', 'secureConnect', 'timeout']; + _.each(events, function(event){ + self.socket.on(event, self.emit.bind(self, event)); + }); + + // Proxy a few methods that we use / previously used. + var methods = ['end', 'destroy', 'write', 'pause', 'resume', 'setEncoding', 'ref', 'unref', 'address']; + _.each(methods, function(method){ + self[method] = function(){ + self.socket[method].apply(self.socket, arguments); + }; + }); + +}; + +Connection.prototype._getSSLOptions = function() { + if (this.sslConnectionOptions) return this.sslConnectionOptions; + this.sslConnectionOptions = {}; + if (this.options.ssl.keyFile) { + this.sslConnectionOptions.key = fs.readFileSync(this.options.ssl.keyFile); + } + if (this.options.ssl.certFile) { + this.sslConnectionOptions.cert = fs.readFileSync(this.options.ssl.certFile); + } + if (this.options.ssl.caFile) { + this.sslConnectionOptions.ca = fs.readFileSync(this.options.ssl.caFile); + } + this.sslConnectionOptions.rejectUnauthorized = this.options.ssl.rejectUnauthorized; + return this.sslConnectionOptions; +}; + +// Time to start the AMQP 7-way connection initialization handshake! +// 1. The client sends the server a version string +Connection.prototype._startHandshake = function() { + debug && debug("Initiating handshake..."); + this.write("AMQP" + String.fromCharCode(0,0,9,1)); +}; + +/* + * + * Parse helpers + * + */ + +Connection.prototype._sendBody = function (channel, body, properties) { + var r = this._bodyToBuffer(body); + var props = r[0], buffer = r[1]; + + properties = _.extend(props || {}, properties); + + this._sendHeader(channel, buffer.length, properties); + + var pos = 0, len = buffer.length; + var metaSize = 8; // headerBytes = 7, frameEndBytes = 1 + var maxBodySize = maxFrameBuffer - metaSize; + + while (len > 0) { + var bodySize = len < maxBodySize ? len : maxBodySize; + var frameSize = bodySize + metaSize; + + var b = new Buffer(frameSize); + b.used = 0; + b[b.used++] = 3; // constants.frameBody + serializer.serializeInt(b, 2, channel); + serializer.serializeInt(b, 4, bodySize); + buffer.copy(b, b.used, pos, pos+bodySize); + b.used += bodySize; + b[b.used++] = 206; // constants.frameEnd; + this.write(b); + + len -= bodySize; + pos += bodySize; + } + return; +}; + +// connection: the connection +// channel: the channel to send this on +// size: size in bytes of the following message +// properties: an object containing any of the following: +// - contentType (default 'application/octet-stream') +// - contentEncoding +// - headers +// - deliveryMode +// - priority (0-9) +// - correlationId +// - replyTo +// - expiration +// - messageId +// - timestamp +// - userId +// - appId +// - clusterId +Connection.prototype._sendHeader = function(channel, size, properties) { + var b = new Buffer(maxFrameBuffer); // FIXME allocating too much. + // use freelist? + b.used = 0; + + var classInfo = classes[60]; // always basic class. + + // 7 OCTET FRAME HEADER + + b[b.used++] = 2; // constants.frameHeader + + serializer.serializeInt(b, 2, channel); + + var lengthStart = b.used; + + serializer.serializeInt(b, 4, 0 /*dummy*/); // length + + var bodyStart = b.used; + + // HEADER'S BODY + + serializer.serializeInt(b, 2, classInfo.index); // class 60 for Basic + serializer.serializeInt(b, 2, 0); // weight, always 0 for rabbitmq + serializer.serializeInt(b, 8, size); // byte size of body + + // properties - first propertyFlags + properties = _.defaults(properties || {}, {contentType: 'application/octet-stream'}); + var propertyFlags = 0; + for (var i = 0; i < classInfo.fields.length; i++) { + if (properties[classInfo.fields[i].name]) propertyFlags |= 1 << (15-i); + } + serializer.serializeInt(b, 2, propertyFlags); + // now the actual properties. + serializer.serializeFields(b, classInfo.fields, properties, false); + + //serializeTable(b, properties); + + var bodyEnd = b.used; + + // Go back to the header and write in the length now that we know it. + b.used = lengthStart; + serializer.serializeInt(b, 4, bodyEnd - bodyStart); + b.used = bodyEnd; + + // 1 OCTET END + + b[b.used++] = 206; // constants.frameEnd; + + var s = new Buffer(b.used); + b.copy(s); + + //debug && debug('header sent: ' + JSON.stringify(s)); + + this.write(s); +}; + +Connection.prototype._sendMethod = function (channel, method, args) { + debug && debug(channel + " < " + method.name + " " + JSON.stringify(args)); + var b = this._sendBuffer; + b.used = 0; + + b[b.used++] = 1; // constants.frameMethod + + serializer.serializeInt(b, 2, channel); + + var lengthIndex = b.used; + + serializer.serializeInt(b, 4, 42); // replace with actual length. + + var startIndex = b.used; + + + serializer.serializeInt(b, 2, method.classIndex); // short, classId + serializer.serializeInt(b, 2, method.methodIndex); // short, methodId + + serializer.serializeFields(b, method.fields, args, true); + + var endIndex = b.used; + + // write in the frame length now that we know it. + b.used = lengthIndex; + serializer.serializeInt(b, 4, endIndex - startIndex); + b.used = endIndex; + + b[b.used++] = 206; // constants.frameEnd; + + var c = new Buffer(b.used); + b.copy(c); + + debug && debug("sending frame: " + c.toJSON()); + + this.write(c); + + this._outboundHeartbeatTimerReset(); +}; + +// tries to find the next available id slot for a channel +Connection.prototype.generateChannelId = function () { + // start from the last used slot id + var channelId = this.channelCounter; + while(true){ + // use values in range of 1..65535 + channelId = channelId % 65535 + 1; + if(!this.channels[channelId]){ + break; + } + // after a full loop throw an Error + if(channelId == this.channelCounter){ + throw new Error("No valid Channel Id values available"); + } + } + this.channelCounter = channelId; + return this.channelCounter; +}; + +}).call(this,_dereq_("C:\\Users\\Alex\\AppData\\Roaming\\npm\\node_modules\\browserify\\node_modules\\insert-module-globals\\node_modules\\process\\browser.js"),_dereq_("buffer").Buffer) +},{"../package":39,"./debug":30,"./definitions":31,"./exchange":32,"./parser":34,"./queue":36,"./serializer":37,"C:\\Users\\Alex\\AppData\\Roaming\\npm\\node_modules\\browserify\\node_modules\\insert-module-globals\\node_modules\\process\\browser.js":144,"buffer":128,"events":137,"fs":127,"lodash":38,"net":127,"tls":127,"url":157,"util":159}],29:[function(_dereq_,module,exports){ +module.exports = { + AMQPTypes: Object.freeze({ + STRING: 'S'.charCodeAt(0) + , INTEGER: 'I'.charCodeAt(0) + , HASH: 'F'.charCodeAt(0) + , TIME: 'T'.charCodeAt(0) + , DECIMAL: 'D'.charCodeAt(0) + , BOOLEAN: 't'.charCodeAt(0) + , SIGNED_8BIT: 'b'.charCodeAt(0) + , SIGNED_16BIT: 's'.charCodeAt(0) + , SIGNED_64BIT: 'l'.charCodeAt(0) + , _32BIT_FLOAT: 'f'.charCodeAt(0) + , _64BIT_FLOAT: 'd'.charCodeAt(0) + , VOID: 'v'.charCodeAt(0) + , BYTE_ARRAY: 'x'.charCodeAt(0) + , ARRAY: 'A'.charCodeAt(0) + , TEN: '10'.charCodeAt(0) + , BOOLEAN_TRUE: '\x01' + , BOOLEAN_FALSE:'\x00' + + }) + , Indicators: Object.freeze({ + FRAME_END: 206 + }) + , FrameType: Object.freeze({ + METHOD: 1 + , HEADER: 2 + , BODY: 3 + , HEARTBEAT: 8 + }) +} + + +},{}],30:[function(_dereq_,module,exports){ +(function (process){ +'use strict'; + +var DEBUG = process.env['NODE_DEBUG_AMQP']; + +// only define debug function in debugging mode +if (DEBUG) { + module.exports = function debug () { + console.error.apply(null, arguments); + }; +} else { + module.exports = null; +} + + +}).call(this,_dereq_("C:\\Users\\Alex\\AppData\\Roaming\\npm\\node_modules\\browserify\\node_modules\\insert-module-globals\\node_modules\\process\\browser.js")) +},{"C:\\Users\\Alex\\AppData\\Roaming\\npm\\node_modules\\browserify\\node_modules\\insert-module-globals\\node_modules\\process\\browser.js":144}],31:[function(_dereq_,module,exports){ +'use strict'; + +var protocol = _dereq_('./amqp-definitions-0-9-1'); + +// a look up table for methods recieved +// indexed on class id, method id +var methodTable = {}; + +// methods keyed on their name +var methods = {}; + +// classes keyed on their index +var classes = {}; + +(function () { // anon scope for init + //debug("initializing amqp methods..."); + + for (var i = 0; i < protocol.classes.length; i++) { + var classInfo = protocol.classes[i]; + classes[classInfo.index] = classInfo; + + for (var j = 0; j < classInfo.methods.length; j++) { + var methodInfo = classInfo.methods[j]; + + var name = classInfo.name + + methodInfo.name[0].toUpperCase() + + methodInfo.name.slice(1); + + //debug(name); + + var method = { + name: name, + fields: methodInfo.fields, + methodIndex: methodInfo.index, + classIndex: classInfo.index + }; + + if (!methodTable[classInfo.index]) methodTable[classInfo.index] = {}; + methodTable[classInfo.index][methodInfo.index] = method; + methods[name] = method; + } + } +})(); + +module.exports = {methods: methods, classes: classes, methodTable: methodTable}; + +},{"./amqp-definitions-0-9-1":26}],32:[function(_dereq_,module,exports){ +'use strict'; +var events = _dereq_('events'); +var util = _dereq_('util'); +var net = _dereq_('net'); +var tls = _dereq_('tls'); +var fs = _dereq_('fs'); +var _ = _dereq_('lodash'); +var methods = _dereq_('./definitions').methods; +var Channel = _dereq_('./channel'); + +var Exchange = module.exports = function Exchange (connection, channel, name, options, openCallback) { + Channel.call(this, connection, channel); + this.name = name; + this.binds = 0; // keep track of queues bound + this.exchangeBinds = 0; // keep track of exchanges bound + this.sourceExchanges = {}; + this.options = _.defaults(options || {}, {autoDelete: true}); + this._openCallback = openCallback; + + this._sequence = null; + this._unAcked = {}; + this._addedExchangeErrorHandler = false; +}; +util.inherits(Exchange, Channel); + +// creates an error handler scoped to the given `exchange` +function createExchangeErrorHandlerFor (exchange) { + return function (err) { + if (!exchange.options.confirm) return; + + for (var id in exchange._unAcked) { + var task = exchange._unAcked[id]; + task.emit('ack error', err); + delete exchange._unAcked[id]; + } + } +}; + +Exchange.prototype._onMethod = function (channel, method, args) { + this.emit(method.name, args); + + if (this._handleTaskReply.apply(this, arguments)) + return true; + + var cb; + + switch (method) { + case methods.channelOpenOk: + // Pre-baked exchanges don't need to be declared + if (/^$|(amq\.)/.test(this.name)) { + this.state = 'open'; + // - issue #33 fix + if (this._openCallback) { + this._openCallback(this); + this._openCallback = null; + } + // -- + this.emit('open'); + + // For if we want to delete a exchange, + // we dont care if all of the options match. + } else if (this.options.noDeclare) { + this.state = 'open'; + + if (this._openCallback) { + this._openCallback(this); + this._openCallback = null; + } + + this.emit('open'); + } else { + this.connection._sendMethod(channel, methods.exchangeDeclare, + { reserved1: 0 + , reserved2: false + , reserved3: false + , exchange: this.name + , type: this.options.type || 'topic' + , passive: !!this.options.passive + , durable: !!this.options.durable + , autoDelete: !!this.options.autoDelete + , internal: !!this.options.internal + , noWait: false + , "arguments":this.options.arguments || {} + }); + this.state = 'declaring'; + } + break; + + case methods.exchangeDeclareOk: + + if (this.options.confirm) { + this.connection._sendMethod(channel, methods.confirmSelect, + { noWait: false }); + } else { + + this.state = 'open'; + this.emit('open'); + if (this._openCallback) { + this._openCallback(this); + this._openCallback = null; + } + } + + break; + + case methods.confirmSelectOk: + this._sequence = 1; + + this.state = 'open'; + this.emit('open'); + if (this._openCallback) { + this._openCallback(this); + this._openCallback = null; + } + break; + + case methods.channelClose: + this.state = "closed"; + this.closeOK(); + this.connection.exchangeClosed(this.name); + var e = new Error(args.replyText); + e.code = args.replyCode; + this.emit('error', e); + this.emit('close'); + break; + + case methods.channelCloseOk: + this.connection.exchangeClosed(this.name); + this.emit('close'); + break; + + + case methods.basicAck: + this.emit('basic-ack', args); + var sequenceNumber = args.deliveryTag.readUInt32BE(4), tag; + + if (sequenceNumber === 0 && args.multiple === true) { + // we must ack everything + for (tag in this._unAcked) { + this._unAcked[tag].emit('ack'); + delete this._unAcked[tag]; + } + } else if (sequenceNumber !== 0 && args.multiple === true) { + // we must ack everything before the delivery tag + for (tag in this._unAcked) { + if (tag <= sequenceNumber) { + this._unAcked[tag].emit('ack'); + delete this._unAcked[tag]; + } + } + } else if (this._unAcked[sequenceNumber] && args.multiple === false) { + // simple single ack + this._unAcked[sequenceNumber].emit('ack'); + delete this._unAcked[sequenceNumber]; + } + break; + + case methods.basicReturn: + this.emit('basic-return', args); + break; + + case methods.exchangeBindOk: + if (this._bindCallback) { + // setting this._bindCallback to null before calling the callback allows for a subsequent bind within the callback + cb = this._bindCallback; + this._bindCallback = null; + cb(this); + } + break; + + case methods.exchangeUnbindOk: + if (this._unbindCallback) { + cb = this._unbindCallback; + this._unbindCallback = null; + cb(this); + } + break; + + default: + throw new Error("Uncaught method '" + method.name + "' with args " + + JSON.stringify(args)); + } + + this._tasksFlush(); +}; + +// exchange.publish('routing.key', 'body'); +// +// the third argument can specify additional options +// - mandatory (boolean, default false) +// - immediate (boolean, default false) +// - contentType (default 'application/octet-stream') +// - contentEncoding +// - headers +// - deliveryMode +// - priority (0-9) +// - correlationId +// - replyTo +// - expiration +// - messageId +// - timestamp +// - userId +// - appId +// - clusterId +// +// the callback is optional and is only used when confirm is turned on for the exchange + +Exchange.prototype.publish = function (routingKey, data, options, callback) { + var self = this; + + options = _.extend({}, options || {}); + options.routingKey = routingKey; + options.exchange = self.name; + options.mandatory = options.mandatory ? true : false; + options.immediate = options.immediate ? true : false; + options.reserved1 = 0; + + var task = this._taskPush(null, function () { + self.connection._sendMethod(self.channel, methods.basicPublish, options); + // This interface is probably not appropriate for streaming large files. + // (Of course it's arguable about whether AMQP is the appropriate + // transport for large files.) The content header wants to know the size + // of the data before sending it - so there's no point in trying to have a + // general streaming interface - streaming messages of unknown size simply + // isn't possible with AMQP. This is all to say, don't send big messages. + // If you need to stream something large, chunk it yourself. + self.connection._sendBody(self.channel, data, options); + }); + + if (self.options.confirm) self._awaitConfirm(task, callback); + return task; +}; + +// registers tasks for confirms +Exchange.prototype._awaitConfirm = function _awaitConfirm (task, callback) { + if (!this._addedExchangeErrorHandler) { + this.on('error', createExchangeErrorHandlerFor(this)); + this._addedExchangeErrorHandler = true; + } + + task.sequence = this._sequence; + this._unAcked[this._sequence] = task; + this._sequence++; + + if ('function' != typeof callback) return; + + task.once('ack error', function (err) { + task.removeAllListeners(); + callback(true, err); + }); + + task.once('ack', function () { + task.removeAllListeners(); + callback(false); + }); +}; + +// do any necessary cleanups eg. after queue destruction +Exchange.prototype.cleanup = function() { + if (this.binds === 0) { // don't keep reference open if unused + this.connection.exchangeClosed(this.name); + } +}; + +Exchange.prototype.destroy = function (ifUnused) { + var self = this; + return this._taskPush(methods.exchangeDeleteOk, function () { + self.connection.exchangeClosed(self.name); + self.connection._sendMethod(self.channel, methods.exchangeDelete, + { reserved1: 0 + , exchange: self.name + , ifUnused: ifUnused ? true : false + , noWait: false + }); + }); +}; + +// E2E Unbind +// support RabbitMQ's exchange-to-exchange binding extension +// http://www.rabbitmq.com/e2e.html +Exchange.prototype.unbind = function (/* exchange, routingKey [, bindCallback] */) { + var self = this; + + // Both arguments are required. The binding to the destination + // exchange/routingKey will be unbound. + + var exchange = arguments[0] + , routingKey = arguments[1] + , callback = arguments[2] + ; + + if (callback) this._unbindCallback = callback; + + return this._taskPush(methods.exchangeUnbindOk, function () { + var source = exchange instanceof Exchange ? exchange.name : exchange; + var destination = self.name; + + if (source in self.connection.exchanges) { + delete self.sourceExchanges[source]; + self.connection.exchanges[source].exchangeBinds--; + } + + self.connection._sendMethod(self.channel, methods.exchangeUnbind, + { reserved1: 0 + , destination: destination + , source: source + , routingKey: routingKey + , noWait: false + , "arguments": {} + }); + }); +}; + +// E2E Bind +// support RabbitMQ's exchange-to-exchange binding extension +// http://www.rabbitmq.com/e2e.html +Exchange.prototype.bind = function (/* exchange, routingKey [, bindCallback] */) { + var self = this; + + // Two arguments are required. The binding to the destination + // exchange/routingKey will be established. + + var exchange = arguments[0] + , routingKey = arguments[1] + , callback = arguments[2] + ; + + if (callback) this._bindCallback = callback; + + var source = exchange instanceof Exchange ? exchange.name : exchange; + var destination = self.name; + + if(source in self.connection.exchanges) { + self.sourceExchanges[source] = self.connection.exchanges[source]; + self.connection.exchanges[source].exchangeBinds++; + } + + self.connection._sendMethod(self.channel, methods.exchangeBind, + { reserved1: 0 + , destination: destination + , source: source + , routingKey: routingKey + , noWait: false + , "arguments": {} + }); + +}; + +// E2E Bind +// support RabbitMQ's exchange-to-exchange binding extension +// http://www.rabbitmq.com/e2e.html +Exchange.prototype.bind_headers = function (/* exchange, routing [, bindCallback] */) { + var self = this; + + // Two arguments are required. The binding to the destination + // exchange/routingKey will be established. + + var exchange = arguments[0] + , routing = arguments[1] + , callback = arguments[2] + ; + + if (callback) this._bindCallback = callback; + + var source = exchange instanceof Exchange ? exchange.name : exchange; + var destination = self.name; + + if (source in self.connection.exchanges) { + self.sourceExchanges[source] = self.connection.exchanges[source]; + self.connection.exchanges[source].exchangeBinds++; + } + + self.connection._sendMethod(self.channel, methods.exchangeBind, + { reserved1: 0 + , destination: destination + , source: source + , routingKey: '' + , noWait: false + , "arguments": routing + }); +}; + +},{"./channel":27,"./definitions":31,"events":137,"fs":127,"lodash":38,"net":127,"tls":127,"util":159}],33:[function(_dereq_,module,exports){ +'use strict'; +var events = _dereq_('events'), + util = _dereq_('util'), + fs = _dereq_('fs'), + protocol, + definitions = _dereq_('./definitions'); + +// Properties: +// - routingKey +// - size +// - deliveryTag +// +// - contentType (default 'application/octet-stream') +// - contentEncoding +// - headers +// - deliveryMode +// - priority (0-9) +// - correlationId +// - replyTo +// - experation +// - messageId +// - timestamp +// - userId +// - appId +// - clusterId +var Message = module.exports = function Message (queue, args) { + var msgProperties = definitions.classes[60].fields; + + events.EventEmitter.call(this); + + this.queue = queue; + + this.deliveryTag = args.deliveryTag; + this.redelivered = args.redelivered; + this.exchange = args.exchange; + this.routingKey = args.routingKey; + this.consumerTag = args.consumerTag; + + for (var i=0, l=msgProperties.length; i= fh.length) { + fh.read = 0; + frameType = fh[fh.read++]; + frameChannel = parseInt(fh, 2); + var frameSize = parseInt(fh, 4); + fh.used = 0; // for reuse + if (frameSize > maxFrameBuffer) { + self.throwError("Oversized frame " + frameSize); + } + frameBuffer = new Buffer(frameSize); + frameBuffer.used = 0; + return frame(data.slice(needed)); + } + else { // need more! + return header; + } + } + + function frame(data) { + var fb = frameBuffer; + var needed = fb.length - fb.used; + var sourceEnd = (fb.length > data.length) ? data.length : fb.length; + data.copy(fb, fb.used, 0, sourceEnd); + fb.used += data.length; + if (data.length > needed) { + return frameEnd(data.slice(needed)); + } + else if (data.length == needed) { + return frameEnd; + } + else { + return frame; + } + } + + function frameEnd(data) { + if (data.length > 0) { + if (data[0] === Indicators.FRAME_END) { + switch (frameType) { + case FrameType.METHOD: + self._parseMethodFrame(frameChannel, frameBuffer); + break; + case FrameType.HEADER: + self._parseHeaderFrame(frameChannel, frameBuffer); + break; + case FrameType.BODY: + if (self.onContent) { + self.onContent(frameChannel, frameBuffer); + } + break; + case FrameType.HEARTBEAT: + debug && debug("heartbeat"); + if (self.onHeartBeat) self.onHeartBeat(); + break; + default: + self.throwError("Unhandled frame type " + frameType); + break; + } + return header(data.slice(1)); + } + else { + self.throwError("Missing frame end marker"); + } + } + else { + return frameEnd; + } + } + + self.parse = header; +} + +// If there's an error in the parser, call the onError handler or throw +AMQPParser.prototype.throwError = function (error) { + if (this.onError) this.onError(error); + else throw new Error(error); +}; + +// Everytime data is recieved on the socket, pass it to this function for +// parsing. +AMQPParser.prototype.execute = function (data) { + // This function only deals with dismantling and buffering the frames. + // It delegates to other functions for parsing the frame-body. + debug && debug('execute: ' + data.toString('hex')); + this.parse = this.parse(data); +}; + + +// parse Network Byte Order integers. size can be 1,2,4,8 +function parseInt (buffer, size) { + switch (size) { + case 1: + return buffer[buffer.read++]; + + case 2: + return (buffer[buffer.read++] << 8) + buffer[buffer.read++]; + + case 4: + return (buffer[buffer.read++] << 24) + (buffer[buffer.read++] << 16) + + (buffer[buffer.read++] << 8) + buffer[buffer.read++]; + + case 8: + return (buffer[buffer.read++] << 56) + (buffer[buffer.read++] << 48) + + (buffer[buffer.read++] << 40) + (buffer[buffer.read++] << 32) + + (buffer[buffer.read++] << 24) + (buffer[buffer.read++] << 16) + + (buffer[buffer.read++] << 8) + buffer[buffer.read++]; + + default: + throw new Error("cannot parse ints of that size"); + } +} + + +function parseShortString (buffer) { + var length = buffer[buffer.read++]; + var s = buffer.toString('utf8', buffer.read, buffer.read+length); + buffer.read += length; + return s; +} + + +function parseLongString (buffer) { + var length = parseInt(buffer, 4); + var s = buffer.slice(buffer.read, buffer.read + length); + buffer.read += length; + return s.toString(); +} + + +function parseSignedInteger (buffer) { + var int = parseInt(buffer, 4); + if (int & 0x80000000) { + int |= 0xEFFFFFFF; + int = -int; + } + return int; +} + +function parseValue (buffer) { + switch (buffer[buffer.read++]) { + case AMQPTypes.STRING: + return parseLongString(buffer); + + case AMQPTypes.INTEGER: + return parseInt(buffer, 4); + + case AMQPTypes.DECIMAL: + var dec = parseInt(buffer, 1); + var num = parseInt(buffer, 4); + return num / (dec * 10); + + case AMQPTypes._64BIT_FLOAT: + var b = []; + for (var i = 0; i < 8; ++i) + b[i] = buffer[buffer.read++]; + + return (new jspack(true)).Unpack('d', b); + + case AMQPTypes._32BIT_FLOAT: + var b = []; + for (var i = 0; i < 4; ++i) + b[i] = buffer[buffer.read++]; + + return (new jspack(true)).Unpack('f', b); + + case AMQPTypes.TIME: + var int = parseInt(buffer, 8); + return (new Date()).setTime(int * 1000); + + case AMQPTypes.HASH: + return parseTable(buffer); + + case AMQPTypes.SIGNED_64BIT: + return parseInt(buffer, 8); + + case AMQPTypes.BOOLEAN: + return (parseInt(buffer, 1) > 0); + + case AMQPTypes.BYTE_ARRAY: + var len = parseInt(buffer, 4); + var buf = new Buffer(len); + buffer.copy(buf, 0, buffer.read, buffer.read + len); + buffer.read += len; + return buf; + + case AMQPTypes.ARRAY: + var len = parseInt(buffer, 4); + var end = buffer.read + len; + var arr = []; + + while (buffer.read < end) { + arr.push(parseValue(buffer)); + } + + return arr; + + default: + throw new Error("Unknown field value type " + buffer[buffer.read-1]); + } +} + +function parseTable (buffer) { + var length = buffer.read + parseInt(buffer, 4); + var table = {}; + + while (buffer.read < length) { + table[parseShortString(buffer)] = parseValue(buffer); + } + + return table; +} + +function parseFields (buffer, fields) { + var args = {}; + var bitIndex = 0; + var value; + + for (var i = 0; i < fields.length; i++) { + var field = fields[i]; + + //debug && debug("parsing field " + field.name + " of type " + field.domain); + + switch (field.domain) { + case 'bit': + // 8 bits can be packed into one octet. + + // XXX check if bitIndex greater than 7? + + value = (buffer[buffer.read] & (1 << bitIndex)) ? true : false; + + if (fields[i+1] && fields[i+1].domain == 'bit') { + bitIndex++; + } else { + bitIndex = 0; + buffer.read++; + } + break; + + case 'octet': + value = buffer[buffer.read++]; + break; + + case 'short': + value = parseInt(buffer, 2); + break; + + case 'long': + value = parseInt(buffer, 4); + break; + + // In a previous version this shared code with 'longlong', which caused problems when passed Date + // integers. Nobody expects to pass a Buffer here, 53 bits is still 28 million years after 1970, we'll be fine. + case 'timestamp': + value = parseInt(buffer, 8); + break; + + // JS doesn't support 64-bit Numbers, so we expect if you're using 'longlong' that you've + // used a Buffer instead + case 'longlong': + value = new Buffer(8); + for (var j = 0; j < 8; j++) { + value[j] = buffer[buffer.read++]; + } + break; + + case 'shortstr': + value = parseShortString(buffer); + break; + + case 'longstr': + value = parseLongString(buffer); + break; + + case 'table': + value = parseTable(buffer); + break; + + default: + throw new Error("Unhandled parameter type " + field.domain); + } + //debug && debug("got " + value); + args[field.name] = value; + } + + return args; +} + + +AMQPParser.prototype._parseMethodFrame = function (channel, buffer) { + buffer.read = 0; + var classId = parseInt(buffer, 2), + methodId = parseInt(buffer, 2); + + // Make sure that this is a method that we understand. + if (!methodTable[classId] || !methodTable[classId][methodId]) { + this.throwError("Received unknown [classId, methodId] pair [" + + classId + ", " + methodId + "]"); + } + + var method = methodTable[classId][methodId]; + + if (!method) this.throwError("bad method?"); + + var args = parseFields(buffer, method.fields); + + if (this.onMethod) { + debug && debug("Executing method", channel, method, args); + this.onMethod(channel, method, args); + } +}; + + +AMQPParser.prototype._parseHeaderFrame = function (channel, buffer) { + buffer.read = 0; + + var classIndex = parseInt(buffer, 2); + var weight = parseInt(buffer, 2); + var size = parseInt(buffer, 8); + + var classInfo = classes[classIndex]; + + if (classInfo.fields.length > 15) { + this.throwError("TODO: support more than 15 properties"); + } + + var propertyFlags = parseInt(buffer, 2); + + var fields = []; + for (var i = 0; i < classInfo.fields.length; i++) { + var field = classInfo.fields[i]; + // groan. + if (propertyFlags & (1 << (15-i))) fields.push(field); + } + + var properties = parseFields(buffer, fields); + + if (this.onContentHeader) { + this.onContentHeader(channel, classInfo, weight, properties, size); + } +}; + +}).call(this,_dereq_("buffer").Buffer) +},{"../jspack":25,"./constants":29,"./debug":30,"./definitions":31,"buffer":128,"events":137,"fs":127,"net":127,"tls":127,"util":159}],35:[function(_dereq_,module,exports){ +(function (process){ +var events = _dereq_('events'); +var inherits = _dereq_('util').inherits; + +exports.Promise = function () { + events.EventEmitter.call(this); + this._blocking = false; + this.hasFired = false; + this.hasAcked = false; + this._values = undefined; +}; +inherits(exports.Promise, events.EventEmitter); + +exports.Promise.prototype.timeout = function(timeout) { + if (!timeout) { + return this._timeoutDuration; + } + + this._timeoutDuration = timeout; + + if (this.hasFired) return; + this._clearTimeout(); + + var self = this; + this._timer = setTimeout(function() { + self._timer = null; + if (self.hasFired) { + return; + } + + self.emitError(new Error('timeout')); + }, timeout); + + return this; +}; + +exports.Promise.prototype._clearTimeout = function() { + if (!this._timer) return; + + clearTimeout(this._timer); + this._timer = null; +} + +exports.Promise.prototype.emitSuccess = function() { + if (this.hasFired) return; + this.hasFired = 'success'; + this._clearTimeout(); + + this._values = Array.prototype.slice.call(arguments); + this.emit.apply(this, ['success'].concat(this._values)); +}; + +exports.Promise.prototype.emitError = function() { + if (this.hasFired) return; + this.hasFired = 'error'; + this._clearTimeout(); + + this._values = Array.prototype.slice.call(arguments); + this.emit.apply(this, ['error'].concat(this._values)); + + if (this.listeners('error').length == 0) { + var self = this; + process.nextTick(function() { + if (self.listeners('error').length == 0) { + throw (self._values[0] instanceof Error) + ? self._values[0] + : new Error('Unhandled emitError: '+JSON.stringify(self._values)); + } + }); + } +}; + +exports.Promise.prototype.addCallback = function (listener) { + if (this.hasFired === 'success') { + listener.apply(this, this._values); + } + + return this.addListener("success", listener); +}; + +exports.Promise.prototype.addErrback = function (listener) { + if (this.hasFired === 'error') { + listener.apply(this, this._values); + } + + return this.addListener("error", listener); +}; + +}).call(this,_dereq_("C:\\Users\\Alex\\AppData\\Roaming\\npm\\node_modules\\browserify\\node_modules\\insert-module-globals\\node_modules\\process\\browser.js")) +},{"C:\\Users\\Alex\\AppData\\Roaming\\npm\\node_modules\\browserify\\node_modules\\insert-module-globals\\node_modules\\process\\browser.js":144,"events":137,"util":159}],36:[function(_dereq_,module,exports){ +(function (process,Buffer){ +'use strict'; +var util = _dereq_('util'); +var fs = _dereq_('fs'); +var _ = _dereq_('lodash'); +var Channel = _dereq_('./channel'); +var Exchange = _dereq_('./exchange'); +var Message = _dereq_('./message'); +var debug = _dereq_('./debug'); +var definitions = _dereq_('./definitions'); +var methods = definitions.methods; +var classes = definitions.classes; + +var Queue = module.exports = function Queue (connection, channel, name, options, callback) { + Channel.call(this, connection, channel); + + var self = this; + this.name = name; + this._bindings = {}; + this.consumerTagListeners = {}; + this.consumerTagOptions = {}; + + // route messages to subscribers based on consumerTag + this.on('rawMessage', function(message) { + if (message.consumerTag && self.consumerTagListeners[message.consumerTag]) { + self.consumerTagListeners[message.consumerTag](message); + } + }); + + this.options = { autoDelete: true, closeChannelOnUnsubscribe: false }; + _.extend(this.options, options || {}); + + this._openCallback = callback; +}; +util.inherits(Queue, Channel); + +Queue.prototype.subscribeRaw = function (options, messageListener) { + var self = this; + + // multiple method signatures + if (typeof options === "function") { + messageListener = options; + options = {}; + } + + var consumerTag = 'node-amqp-' + process.pid + '-' + Math.random(); + this.consumerTagListeners[consumerTag] = messageListener; + + options = options || {}; + options['state'] = 'opening'; + this.consumerTagOptions[consumerTag] = options; + if (options.prefetchCount !== undefined) { + self.connection._sendMethod(self.channel, methods.basicQos, + { reserved1: 0 + , prefetchSize: 0 + , prefetchCount: options.prefetchCount + , global: false + }); + } + + return this._taskPush(methods.basicConsumeOk, function () { + self.connection._sendMethod(self.channel, methods.basicConsume, + { reserved1: 0 + , queue: self.name + , consumerTag: consumerTag + , noLocal: !!options.noLocal + , noAck: !!options.noAck + , exclusive: !!options.exclusive + , noWait: false + , "arguments": {} + }); + self.consumerTagOptions[consumerTag]['state'] = 'open'; + }); +}; + +Queue.prototype.unsubscribe = function(consumerTag) { + var self = this; + return this._taskPush(methods.basicCancelOk, function () { + self.connection._sendMethod(self.channel, methods.basicCancel, + { reserved1: 0, + consumerTag: consumerTag, + noWait: false }); + }) + .addCallback(function () { + if (self.options.closeChannelOnUnsubscribe) { + self.close(); + } + delete self.consumerTagListeners[consumerTag]; + delete self.consumerTagOptions[consumerTag]; + }); +}; + +Queue.prototype.subscribe = function (options, messageListener) { + var self = this; + + // Optional options + if (typeof options === "function") { + messageListener = options; + options = {}; + } + + options = _.defaults(options || {}, { + ack: false, + prefetchCount: 1, + routingKeyInPayload: self.connection.options.routingKeyInPayload, + deliveryTagInPayload: self.connection.options.deliveryTagInPayload + }); + + // basic consume + var rawOptions = { + noAck: !options.ack, + exclusive: options.exclusive + }; + + if (options.ack) { + rawOptions['prefetchCount'] = options.prefetchCount; + } + + return this.subscribeRaw(rawOptions, function (m) { + var contentType = m.contentType; + + if (contentType == null && m.headers && m.headers.properties) { + contentType = m.headers.properties.content_type; + } + + var isJSON = contentType == 'text/json' || + contentType == 'application/json'; + + var buffer; + + if (isJSON) { + buffer = ""; + } else { + buffer = new Buffer(m.size); + buffer.used = 0; + } + + self._lastMessage = m; + + m.addListener('data', function (d) { + if (isJSON) { + buffer += d.toString(); + } else { + d.copy(buffer, buffer.used); + buffer.used += d.length; + } + }); + + m.addListener('end', function () { + var json, deliveryInfo = {}, msgProperties = classes[60].fields, i, l; + + if (isJSON) { + try { + json = JSON.parse(buffer); + } catch (e) { + json = null; + deliveryInfo.parseError = e; + deliveryInfo.rawData = buffer; + } + } else { + json = { data: buffer, contentType: m.contentType }; + } + + for (i = 0, l = msgProperties.length; i b.length) { + throw new Error("write out of bounds"); + } + + // Only 4 cases - just going to be explicit instead of looping. + + switch (size) { + // octet + case 1: + b[b.used++] = int; + break; + + // short + case 2: + b[b.used++] = (int & 0xFF00) >> 8; + b[b.used++] = (int & 0x00FF) >> 0; + break; + + // long + case 4: + b[b.used++] = (int & 0xFF000000) >> 24; + b[b.used++] = (int & 0x00FF0000) >> 16; + b[b.used++] = (int & 0x0000FF00) >> 8; + b[b.used++] = (int & 0x000000FF) >> 0; + break; + + + // long long + case 8: + b[b.used++] = (int & 0xFF00000000000000) >> 56; + b[b.used++] = (int & 0x00FF000000000000) >> 48; + b[b.used++] = (int & 0x0000FF0000000000) >> 40; + b[b.used++] = (int & 0x000000FF00000000) >> 32; + b[b.used++] = (int & 0x00000000FF000000) >> 24; + b[b.used++] = (int & 0x0000000000FF0000) >> 16; + b[b.used++] = (int & 0x000000000000FF00) >> 8; + b[b.used++] = (int & 0x00000000000000FF) >> 0; + break; + + default: + throw new Error("Bad size"); + } + }, + + + serializeShortString: function (b, string) { + if (typeof(string) != "string") { + throw new Error("param must be a string"); + } + var byteLength = Buffer.byteLength(string, 'utf8'); + if (byteLength > 0xFF) { + throw new Error("String too long for 'shortstr' parameter"); + } + if (1 + byteLength + b.used >= b.length) { + throw new Error("Not enough space in buffer for 'shortstr'"); + } + b[b.used++] = byteLength; + b.write(string, b.used, 'utf8'); + b.used += byteLength; + }, + + serializeLongString: function(b, string) { + // we accept string, object, or buffer for this parameter. + // in the case of string we serialize it to utf8. + if (typeof(string) == 'string') { + var byteLength = Buffer.byteLength(string, 'utf8'); + serializer.serializeInt(b, 4, byteLength); + b.write(string, b.used, 'utf8'); + b.used += byteLength; + } else if (typeof(string) == 'object') { + serializer.serializeTable(b, string); + } else { + // data is Buffer + var byteLength = string.length; + serializer.serializeInt(b, 4, byteLength); + b.write(string, b.used); // memcpy + b.used += byteLength; + } + }, + + serializeDate: function(b, date) { + serializer.serializeInt(b, 8, date.valueOf() / 1000); + }, + + serializeBuffer: function(b, buffer) { + serializer.serializeInt(b, 4, buffer.length); + buffer.copy(b, b.used, 0); + b.used += buffer.length; + }, + + serializeBase64: function(b, buffer) { + serializer.serializeLongString(b, buffer.toString('base64')); + }, + + isBigInt: function(value) { + return value > 0xffffffff; + }, + + getCode: function(dec) { + var hexArray = "0123456789ABCDEF".split(''); + var code1 = Math.floor(dec / 16); + var code2 = dec - code1 * 16; + return hexArray[code2]; + }, + + isFloat: function(value){ + return value === +value && value !== (value|0); + }, + + serializeValue: function(b, value) { + switch (typeof(value)) { + case 'string': + b[b.used++] = 'S'.charCodeAt(0); + serializer.serializeLongString(b, value); + break; + + case 'number': + if (!serializer.isFloat(value)) { + if (serializer.isBigInt(value)) { + // 64-bit uint + b[b.used++] = 'l'.charCodeAt(0); + serializer.serializeInt(b, 8, value); + } else { + //32-bit uint + b[b.used++] = 'I'.charCodeAt(0); + serializer.serializeInt(b, 4, value); + } + } else { + //64-bit float + b[b.used++] = 'd'.charCodeAt(0); + serializer.serializeFloat(b, 8, value); + } + break; + + case 'boolean': + b[b.used++] = 't'.charCodeAt(0); + b[b.used++] = value; + break; + + default: + if (value instanceof Date) { + b[b.used++] = 'T'.charCodeAt(0); + serializer.serializeDate(b, value); + } else if (value instanceof Buffer) { + b[b.used++] = 'x'.charCodeAt(0); + serializer.serializeBuffer(b, value); + } else if (Array.isArray(value)) { + b[b.used++] = 'A'.charCodeAt(0); + serializer.serializeArray(b, value); + } else if (typeof(value) === 'object') { + b[b.used++] = 'F'.charCodeAt(0); + serializer.serializeTable(b, value); + } else { + throw new Error("unsupported type in amqp table: " + typeof(value)); + } + } + }, + + serializeTable: function(b, object) { + if (typeof(object) != "object") { + throw new Error("param must be an object"); + } + + // Save our position so that we can go back and write the length of this table + // at the beginning of the packet (once we know how many entries there are). + var lengthIndex = b.used; + b.used += 4; // sizeof long + var startIndex = b.used; + + for (var key in object) { + if (!object.hasOwnProperty(key)) continue; + serializer.serializeShortString(b, key); + serializer.serializeValue(b, object[key]); + } + + var endIndex = b.used; + b.used = lengthIndex; + serializer.serializeInt(b, 4, endIndex - startIndex); + b.used = endIndex; + }, + + serializeArray: function(b, arr) { + // Save our position so that we can go back and write the byte length of this array + // at the beginning of the packet (once we have serialized all elements). + var lengthIndex = b.used; + b.used += 4; // sizeof long + var startIndex = b.used; + + var len = arr.length; + for (var i = 0; i < len; i++) { + serializer.serializeValue(b, arr[i]); + } + + var endIndex = b.used; + b.used = lengthIndex; + serializer.serializeInt(b, 4, endIndex - startIndex); + b.used = endIndex; + }, + + serializeFields: function(buffer, fields, args, strict) { + var bitField = 0; + var bitIndex = 0; + for (var i = 0; i < fields.length; i++) { + var field = fields[i]; + var domain = field.domain; + if (!(field.name in args)) { + if (strict) { + throw new Error("Missing field '" + field.name + "' of type '" + domain + "' while executing AMQP method '" + + arguments.callee.caller.arguments[1].name + "'"); + } + continue; + } + + var param = args[field.name]; + + //debug("domain: " + domain + " param: " + param); + + switch (domain) { + case 'bit': + if (typeof(param) != "boolean") { + throw new Error("Unmatched field " + JSON.stringify(field)); + } + + if (param) bitField |= (1 << bitIndex); + bitIndex++; + + if (!fields[i+1] || fields[i+1].domain != 'bit') { + //debug('SET bit field ' + field.name + ' 0x' + bitField.toString(16)); + buffer[buffer.used++] = bitField; + bitField = 0; + bitIndex = 0; + } + break; + + case 'octet': + if (typeof(param) != "number" || param > 0xFF) { + throw new Error("Unmatched field " + JSON.stringify(field)); + } + buffer[buffer.used++] = param; + break; + + case 'short': + if (typeof(param) != "number" || param > 0xFFFF) { + throw new Error("Unmatched field " + JSON.stringify(field)); + } + serializer.serializeInt(buffer, 2, param); + break; + + case 'long': + if (typeof(param) != "number" || param > 0xFFFFFFFF) { + throw new Error("Unmatched field " + JSON.stringify(field)); + } + serializer.serializeInt(buffer, 4, param); + break; + + // In a previous version this shared code with 'longlong', which caused problems when passed Date + // integers. Nobody expects to pass a Buffer here, 53 bits is still 28 million years after 1970, we'll be fine. + case 'timestamp': + serializer.serializeInt(buffer, 8, param); + break; + + case 'longlong': + for (var j = 0; j < 8; j++) { + buffer[buffer.used++] = param[j]; + } + break; + + case 'shortstr': + if (typeof(param) != "string" || param.length > 0xFF) { + throw new Error("Unmatched field " + JSON.stringify(field)); + } + serializer.serializeShortString(buffer, param); + break; + + case 'longstr': + serializer.serializeLongString(buffer, param); + break; + + case 'table': + if (typeof(param) != "object") { + throw new Error("Unmatched field " + JSON.stringify(field)); + } + serializer.serializeTable(buffer, param); + break; + + default: + throw new Error("Unknown domain value type " + domain); + } + } + } +}; + +}).call(this,_dereq_("buffer").Buffer) +},{"../jspack":25,"buffer":128}],38:[function(_dereq_,module,exports){ +(function (global){ +/** + * @license + * Lo-Dash 1.3.1 (Custom Build) + * Build: `lodash modern -o ./dist/lodash.js` + * Copyright 2012-2013 The Dojo Foundation + * Based on Underscore.js 1.4.4 + * Copyright 2009-2013 Jeremy Ashkenas, DocumentCloud Inc. + * Available under MIT license + */ +;(function(window) { + + /** Used as a safe reference for `undefined` in pre ES5 environments */ + var undefined; + + /** Used to pool arrays and objects used internally */ + var arrayPool = [], + objectPool = []; + + /** Used to generate unique IDs */ + var idCounter = 0; + + /** Used internally to indicate various things */ + var indicatorObject = {}; + + /** Used to prefix keys to avoid issues with `__proto__` and properties on `Object.prototype` */ + var keyPrefix = +new Date + ''; + + /** Used as the size when optimizations are enabled for large arrays */ + var largeArraySize = 75; + + /** Used as the max size of the `arrayPool` and `objectPool` */ + var maxPoolSize = 40; + + /** Used to match empty string literals in compiled template source */ + var reEmptyStringLeading = /\b__p \+= '';/g, + reEmptyStringMiddle = /\b(__p \+=) '' \+/g, + reEmptyStringTrailing = /(__e\(.*?\)|\b__t\)) \+\n'';/g; + + /** Used to match HTML entities */ + var reEscapedHtml = /&(?:amp|lt|gt|quot|#39);/g; + + /** + * Used to match ES6 template delimiters + * http://people.mozilla.org/~jorendorff/es6-draft.html#sec-7.8.6 + */ + var reEsTemplate = /\$\{([^\\}]*(?:\\.[^\\}]*)*)\}/g; + + /** Used to match regexp flags from their coerced string values */ + var reFlags = /\w*$/; + + /** Used to match "interpolate" template delimiters */ + var reInterpolate = /<%=([\s\S]+?)%>/g; + + /** Used to detect functions containing a `this` reference */ + var reThis = (reThis = /\bthis\b/) && reThis.test(runInContext) && reThis; + + /** Used to detect and test whitespace */ + var whitespace = ( + // whitespace + ' \t\x0B\f\xA0\ufeff' + + + // line terminators + '\n\r\u2028\u2029' + + + // unicode category "Zs" space separators + '\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000' + ); + + /** Used to match leading whitespace and zeros to be removed */ + var reLeadingSpacesAndZeros = RegExp('^[' + whitespace + ']*0+(?=.$)'); + + /** Used to ensure capturing order of template delimiters */ + var reNoMatch = /($^)/; + + /** Used to match HTML characters */ + var reUnescapedHtml = /[&<>"']/g; + + /** Used to match unescaped characters in compiled string literals */ + var reUnescapedString = /['\n\r\t\u2028\u2029\\]/g; + + /** Used to assign default `context` object properties */ + var contextProps = [ + 'Array', 'Boolean', 'Date', 'Function', 'Math', 'Number', 'Object', + 'RegExp', 'String', '_', 'attachEvent', 'clearTimeout', 'isFinite', 'isNaN', + 'parseInt', 'setImmediate', 'setTimeout' + ]; + + /** Used to make template sourceURLs easier to identify */ + var templateCounter = 0; + + /** `Object#toString` result shortcuts */ + var argsClass = '[object Arguments]', + arrayClass = '[object Array]', + boolClass = '[object Boolean]', + dateClass = '[object Date]', + errorClass = '[object Error]', + funcClass = '[object Function]', + numberClass = '[object Number]', + objectClass = '[object Object]', + regexpClass = '[object RegExp]', + stringClass = '[object String]'; + + /** Used to identify object classifications that `_.clone` supports */ + var cloneableClasses = {}; + cloneableClasses[funcClass] = false; + cloneableClasses[argsClass] = cloneableClasses[arrayClass] = + cloneableClasses[boolClass] = cloneableClasses[dateClass] = + cloneableClasses[numberClass] = cloneableClasses[objectClass] = + cloneableClasses[regexpClass] = cloneableClasses[stringClass] = true; + + /** Used to determine if values are of the language type Object */ + var objectTypes = { + 'boolean': false, + 'function': true, + 'object': true, + 'number': false, + 'string': false, + 'undefined': false + }; + + /** Used to escape characters for inclusion in compiled string literals */ + var stringEscapes = { + '\\': '\\', + "'": "'", + '\n': 'n', + '\r': 'r', + '\t': 't', + '\u2028': 'u2028', + '\u2029': 'u2029' + }; + + /** Detect free variable `exports` */ + var freeExports = objectTypes[typeof exports] && exports; + + /** Detect free variable `module` */ + var freeModule = objectTypes[typeof module] && module && module.exports == freeExports && module; + + /** Detect free variable `global`, from Node.js or Browserified code, and use it as `window` */ + var freeGlobal = objectTypes[typeof global] && global; + if (freeGlobal && (freeGlobal.global === freeGlobal || freeGlobal.window === freeGlobal)) { + window = freeGlobal; + } + + /*--------------------------------------------------------------------------*/ + + /** + * A basic implementation of `_.indexOf` without support for binary searches + * or `fromIndex` constraints. + * + * @private + * @param {Array} array The array to search. + * @param {Mixed} value The value to search for. + * @param {Number} [fromIndex=0] The index to search from. + * @returns {Number} Returns the index of the matched value or `-1`. + */ + function basicIndexOf(array, value, fromIndex) { + var index = (fromIndex || 0) - 1, + length = array.length; + + while (++index < length) { + if (array[index] === value) { + return index; + } + } + return -1; + } + + /** + * An implementation of `_.contains` for cache objects that mimics the return + * signature of `_.indexOf` by returning `0` if the value is found, else `-1`. + * + * @private + * @param {Object} cache The cache object to inspect. + * @param {Mixed} value The value to search for. + * @returns {Number} Returns `0` if `value` is found, else `-1`. + */ + function cacheIndexOf(cache, value) { + var type = typeof value; + cache = cache.cache; + + if (type == 'boolean' || value == null) { + return cache[value]; + } + if (type != 'number' && type != 'string') { + type = 'object'; + } + var key = type == 'number' ? value : keyPrefix + value; + cache = cache[type] || (cache[type] = {}); + + return type == 'object' + ? (cache[key] && basicIndexOf(cache[key], value) > -1 ? 0 : -1) + : (cache[key] ? 0 : -1); + } + + /** + * Adds a given `value` to the corresponding cache object. + * + * @private + * @param {Mixed} value The value to add to the cache. + */ + function cachePush(value) { + var cache = this.cache, + type = typeof value; + + if (type == 'boolean' || value == null) { + cache[value] = true; + } else { + if (type != 'number' && type != 'string') { + type = 'object'; + } + var key = type == 'number' ? value : keyPrefix + value, + typeCache = cache[type] || (cache[type] = {}); + + if (type == 'object') { + if ((typeCache[key] || (typeCache[key] = [])).push(value) == this.array.length) { + cache[type] = false; + } + } else { + typeCache[key] = true; + } + } + } + + /** + * Used by `_.max` and `_.min` as the default `callback` when a given + * `collection` is a string value. + * + * @private + * @param {String} value The character to inspect. + * @returns {Number} Returns the code unit of given character. + */ + function charAtCallback(value) { + return value.charCodeAt(0); + } + + /** + * Used by `sortBy` to compare transformed `collection` values, stable sorting + * them in ascending order. + * + * @private + * @param {Object} a The object to compare to `b`. + * @param {Object} b The object to compare to `a`. + * @returns {Number} Returns the sort order indicator of `1` or `-1`. + */ + function compareAscending(a, b) { + var ai = a.index, + bi = b.index; + + a = a.criteria; + b = b.criteria; + + // ensure a stable sort in V8 and other engines + // http://code.google.com/p/v8/issues/detail?id=90 + if (a !== b) { + if (a > b || typeof a == 'undefined') { + return 1; + } + if (a < b || typeof b == 'undefined') { + return -1; + } + } + return ai < bi ? -1 : 1; + } + + /** + * Creates a cache object to optimize linear searches of large arrays. + * + * @private + * @param {Array} [array=[]] The array to search. + * @returns {Null|Object} Returns the cache object or `null` if caching should not be used. + */ + function createCache(array) { + var index = -1, + length = array.length; + + var cache = getObject(); + cache['false'] = cache['null'] = cache['true'] = cache['undefined'] = false; + + var result = getObject(); + result.array = array; + result.cache = cache; + result.push = cachePush; + + while (++index < length) { + result.push(array[index]); + } + return cache.object === false + ? (releaseObject(result), null) + : result; + } + + /** + * Used by `template` to escape characters for inclusion in compiled + * string literals. + * + * @private + * @param {String} match The matched character to escape. + * @returns {String} Returns the escaped character. + */ + function escapeStringChar(match) { + return '\\' + stringEscapes[match]; + } + + /** + * Gets an array from the array pool or creates a new one if the pool is empty. + * + * @private + * @returns {Array} The array from the pool. + */ + function getArray() { + return arrayPool.pop() || []; + } + + /** + * Gets an object from the object pool or creates a new one if the pool is empty. + * + * @private + * @returns {Object} The object from the pool. + */ + function getObject() { + return objectPool.pop() || { + 'array': null, + 'cache': null, + 'criteria': null, + 'false': false, + 'index': 0, + 'leading': false, + 'maxWait': 0, + 'null': false, + 'number': null, + 'object': null, + 'push': null, + 'string': null, + 'trailing': false, + 'true': false, + 'undefined': false, + 'value': null + }; + } + + /** + * A no-operation function. + * + * @private + */ + function noop() { + // no operation performed + } + + /** + * Releases the given `array` back to the array pool. + * + * @private + * @param {Array} [array] The array to release. + */ + function releaseArray(array) { + array.length = 0; + if (arrayPool.length < maxPoolSize) { + arrayPool.push(array); + } + } + + /** + * Releases the given `object` back to the object pool. + * + * @private + * @param {Object} [object] The object to release. + */ + function releaseObject(object) { + var cache = object.cache; + if (cache) { + releaseObject(cache); + } + object.array = object.cache = object.criteria = object.object = object.number = object.string = object.value = null; + if (objectPool.length < maxPoolSize) { + objectPool.push(object); + } + } + + /** + * Slices the `collection` from the `start` index up to, but not including, + * the `end` index. + * + * Note: This function is used, instead of `Array#slice`, to support node lists + * in IE < 9 and to ensure dense arrays are returned. + * + * @private + * @param {Array|Object|String} collection The collection to slice. + * @param {Number} start The start index. + * @param {Number} end The end index. + * @returns {Array} Returns the new array. + */ + function slice(array, start, end) { + start || (start = 0); + if (typeof end == 'undefined') { + end = array ? array.length : 0; + } + var index = -1, + length = end - start || 0, + result = Array(length < 0 ? 0 : length); + + while (++index < length) { + result[index] = array[start + index]; + } + return result; + } + + /*--------------------------------------------------------------------------*/ + + /** + * Create a new `lodash` function using the given `context` object. + * + * @static + * @memberOf _ + * @category Utilities + * @param {Object} [context=window] The context object. + * @returns {Function} Returns the `lodash` function. + */ + function runInContext(context) { + // Avoid issues with some ES3 environments that attempt to use values, named + // after built-in constructors like `Object`, for the creation of literals. + // ES5 clears this up by stating that literals must use built-in constructors. + // See http://es5.github.com/#x11.1.5. + context = context ? _.defaults(window.Object(), context, _.pick(window, contextProps)) : window; + + /** Native constructor references */ + var Array = context.Array, + Boolean = context.Boolean, + Date = context.Date, + Function = context.Function, + Math = context.Math, + Number = context.Number, + Object = context.Object, + RegExp = context.RegExp, + String = context.String, + TypeError = context.TypeError; + + /** + * Used for `Array` method references. + * + * Normally `Array.prototype` would suffice, however, using an array literal + * avoids issues in Narwhal. + */ + var arrayRef = []; + + /** Used for native method references */ + var objectProto = Object.prototype, + stringProto = String.prototype; + + /** Used to restore the original `_` reference in `noConflict` */ + var oldDash = context._; + + /** Used to detect if a method is native */ + var reNative = RegExp('^' + + String(objectProto.valueOf) + .replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + .replace(/valueOf|for [^\]]+/g, '.+?') + '$' + ); + + /** Native method shortcuts */ + var ceil = Math.ceil, + clearTimeout = context.clearTimeout, + concat = arrayRef.concat, + floor = Math.floor, + fnToString = Function.prototype.toString, + getPrototypeOf = reNative.test(getPrototypeOf = Object.getPrototypeOf) && getPrototypeOf, + hasOwnProperty = objectProto.hasOwnProperty, + push = arrayRef.push, + propertyIsEnumerable = objectProto.propertyIsEnumerable, + setImmediate = context.setImmediate, + setTimeout = context.setTimeout, + toString = objectProto.toString; + + /* Native method shortcuts for methods with the same name as other `lodash` methods */ + var nativeBind = reNative.test(nativeBind = toString.bind) && nativeBind, + nativeCreate = reNative.test(nativeCreate = Object.create) && nativeCreate, + nativeIsArray = reNative.test(nativeIsArray = Array.isArray) && nativeIsArray, + nativeIsFinite = context.isFinite, + nativeIsNaN = context.isNaN, + nativeKeys = reNative.test(nativeKeys = Object.keys) && nativeKeys, + nativeMax = Math.max, + nativeMin = Math.min, + nativeParseInt = context.parseInt, + nativeRandom = Math.random, + nativeSlice = arrayRef.slice; + + /** Detect various environments */ + var isIeOpera = reNative.test(context.attachEvent), + isV8 = nativeBind && !/\n|true/.test(nativeBind + isIeOpera); + + /** Used to lookup a built-in constructor by [[Class]] */ + var ctorByClass = {}; + ctorByClass[arrayClass] = Array; + ctorByClass[boolClass] = Boolean; + ctorByClass[dateClass] = Date; + ctorByClass[funcClass] = Function; + ctorByClass[objectClass] = Object; + ctorByClass[numberClass] = Number; + ctorByClass[regexpClass] = RegExp; + ctorByClass[stringClass] = String; + + /*--------------------------------------------------------------------------*/ + + /** + * Creates a `lodash` object, which wraps the given `value`, to enable method + * chaining. + * + * In addition to Lo-Dash methods, wrappers also have the following `Array` methods: + * `concat`, `join`, `pop`, `push`, `reverse`, `shift`, `slice`, `sort`, `splice`, + * and `unshift` + * + * Chaining is supported in custom builds as long as the `value` method is + * implicitly or explicitly included in the build. + * + * The chainable wrapper functions are: + * `after`, `assign`, `bind`, `bindAll`, `bindKey`, `chain`, `compact`, + * `compose`, `concat`, `countBy`, `createCallback`, `debounce`, `defaults`, + * `defer`, `delay`, `difference`, `filter`, `flatten`, `forEach`, `forIn`, + * `forOwn`, `functions`, `groupBy`, `initial`, `intersection`, `invert`, + * `invoke`, `keys`, `map`, `max`, `memoize`, `merge`, `min`, `object`, `omit`, + * `once`, `pairs`, `partial`, `partialRight`, `pick`, `pluck`, `push`, `range`, + * `reject`, `rest`, `reverse`, `shuffle`, `slice`, `sort`, `sortBy`, `splice`, + * `tap`, `throttle`, `times`, `toArray`, `transform`, `union`, `uniq`, `unshift`, + * `unzip`, `values`, `where`, `without`, `wrap`, and `zip` + * + * The non-chainable wrapper functions are: + * `clone`, `cloneDeep`, `contains`, `escape`, `every`, `find`, `has`, + * `identity`, `indexOf`, `isArguments`, `isArray`, `isBoolean`, `isDate`, + * `isElement`, `isEmpty`, `isEqual`, `isFinite`, `isFunction`, `isNaN`, + * `isNull`, `isNumber`, `isObject`, `isPlainObject`, `isRegExp`, `isString`, + * `isUndefined`, `join`, `lastIndexOf`, `mixin`, `noConflict`, `parseInt`, + * `pop`, `random`, `reduce`, `reduceRight`, `result`, `shift`, `size`, `some`, + * `sortedIndex`, `runInContext`, `template`, `unescape`, `uniqueId`, and `value` + * + * The wrapper functions `first` and `last` return wrapped values when `n` is + * passed, otherwise they return unwrapped values. + * + * @name _ + * @constructor + * @alias chain + * @category Chaining + * @param {Mixed} value The value to wrap in a `lodash` instance. + * @returns {Object} Returns a `lodash` instance. + * @example + * + * var wrapped = _([1, 2, 3]); + * + * // returns an unwrapped value + * wrapped.reduce(function(sum, num) { + * return sum + num; + * }); + * // => 6 + * + * // returns a wrapped value + * var squares = wrapped.map(function(num) { + * return num * num; + * }); + * + * _.isArray(squares); + * // => false + * + * _.isArray(squares.value()); + * // => true + */ + function lodash(value) { + // don't wrap if already wrapped, even if wrapped by a different `lodash` constructor + return (value && typeof value == 'object' && !isArray(value) && hasOwnProperty.call(value, '__wrapped__')) + ? value + : new lodashWrapper(value); + } + + /** + * A fast path for creating `lodash` wrapper objects. + * + * @private + * @param {Mixed} value The value to wrap in a `lodash` instance. + * @returns {Object} Returns a `lodash` instance. + */ + function lodashWrapper(value) { + this.__wrapped__ = value; + } + // ensure `new lodashWrapper` is an instance of `lodash` + lodashWrapper.prototype = lodash.prototype; + + /** + * An object used to flag environments features. + * + * @static + * @memberOf _ + * @type Object + */ + var support = lodash.support = {}; + + /** + * Detect if `Function#bind` exists and is inferred to be fast (all but V8). + * + * @memberOf _.support + * @type Boolean + */ + support.fastBind = nativeBind && !isV8; + + /** + * By default, the template delimiters used by Lo-Dash are similar to those in + * embedded Ruby (ERB). Change the following template settings to use alternative + * delimiters. + * + * @static + * @memberOf _ + * @type Object + */ + lodash.templateSettings = { + + /** + * Used to detect `data` property values to be HTML-escaped. + * + * @memberOf _.templateSettings + * @type RegExp + */ + 'escape': /<%-([\s\S]+?)%>/g, + + /** + * Used to detect code to be evaluated. + * + * @memberOf _.templateSettings + * @type RegExp + */ + 'evaluate': /<%([\s\S]+?)%>/g, + + /** + * Used to detect `data` property values to inject. + * + * @memberOf _.templateSettings + * @type RegExp + */ + 'interpolate': reInterpolate, + + /** + * Used to reference the data object in the template text. + * + * @memberOf _.templateSettings + * @type String + */ + 'variable': '', + + /** + * Used to import variables into the compiled template. + * + * @memberOf _.templateSettings + * @type Object + */ + 'imports': { + + /** + * A reference to the `lodash` function. + * + * @memberOf _.templateSettings.imports + * @type Function + */ + '_': lodash + } + }; + + /*--------------------------------------------------------------------------*/ + + /** + * Creates a function that, when called, invokes `func` with the `this` binding + * of `thisArg` and prepends any `partialArgs` to the arguments passed to the + * bound function. + * + * @private + * @param {Function|String} func The function to bind or the method name. + * @param {Mixed} [thisArg] The `this` binding of `func`. + * @param {Array} partialArgs An array of arguments to be partially applied. + * @param {Object} [idicator] Used to indicate binding by key or partially + * applying arguments from the right. + * @returns {Function} Returns the new bound function. + */ + function createBound(func, thisArg, partialArgs, indicator) { + var isFunc = isFunction(func), + isPartial = !partialArgs, + key = thisArg; + + // juggle arguments + if (isPartial) { + var rightIndicator = indicator; + partialArgs = thisArg; + } + else if (!isFunc) { + if (!indicator) { + throw new TypeError; + } + thisArg = func; + } + + function bound() { + // `Function#bind` spec + // http://es5.github.com/#x15.3.4.5 + var args = arguments, + thisBinding = isPartial ? this : thisArg; + + if (!isFunc) { + func = thisArg[key]; + } + if (partialArgs.length) { + args = args.length + ? (args = nativeSlice.call(args), rightIndicator ? args.concat(partialArgs) : partialArgs.concat(args)) + : partialArgs; + } + if (this instanceof bound) { + // ensure `new bound` is an instance of `func` + thisBinding = createObject(func.prototype); + + // mimic the constructor's `return` behavior + // http://es5.github.com/#x13.2.2 + var result = func.apply(thisBinding, args); + return isObject(result) ? result : thisBinding; + } + return func.apply(thisBinding, args); + } + return bound; + } + + /** + * Creates a new object with the specified `prototype`. + * + * @private + * @param {Object} prototype The prototype object. + * @returns {Object} Returns the new object. + */ + function createObject(prototype) { + return isObject(prototype) ? nativeCreate(prototype) : {}; + } + + /** + * Used by `escape` to convert characters to HTML entities. + * + * @private + * @param {String} match The matched character to escape. + * @returns {String} Returns the escaped character. + */ + function escapeHtmlChar(match) { + return htmlEscapes[match]; + } + + /** + * Gets the appropriate "indexOf" function. If the `_.indexOf` method is + * customized, this method returns the custom method, otherwise it returns + * the `basicIndexOf` function. + * + * @private + * @returns {Function} Returns the "indexOf" function. + */ + function getIndexOf(array, value, fromIndex) { + var result = (result = lodash.indexOf) === indexOf ? basicIndexOf : result; + return result; + } + + /** + * Creates a function that juggles arguments, allowing argument overloading + * for `_.flatten` and `_.uniq`, before passing them to the given `func`. + * + * @private + * @param {Function} func The function to wrap. + * @returns {Function} Returns the new function. + */ + function overloadWrapper(func) { + return function(array, flag, callback, thisArg) { + // juggle arguments + if (typeof flag != 'boolean' && flag != null) { + thisArg = callback; + callback = !(thisArg && thisArg[flag] === array) ? flag : undefined; + flag = false; + } + if (callback != null) { + callback = lodash.createCallback(callback, thisArg); + } + return func(array, flag, callback, thisArg); + }; + } + + /** + * A fallback implementation of `isPlainObject` which checks if a given `value` + * is an object created by the `Object` constructor, assuming objects created + * by the `Object` constructor have no inherited enumerable properties and that + * there are no `Object.prototype` extensions. + * + * @private + * @param {Mixed} value The value to check. + * @returns {Boolean} Returns `true`, if `value` is a plain object, else `false`. + */ + function shimIsPlainObject(value) { + var ctor, + result; + + // avoid non Object objects, `arguments` objects, and DOM elements + if (!(value && toString.call(value) == objectClass) || + (ctor = value.constructor, isFunction(ctor) && !(ctor instanceof ctor))) { + return false; + } + // In most environments an object's own properties are iterated before + // its inherited properties. If the last iterated property is an object's + // own property then there are no inherited enumerable properties. + forIn(value, function(value, key) { + result = key; + }); + return result === undefined || hasOwnProperty.call(value, result); + } + + /** + * Used by `unescape` to convert HTML entities to characters. + * + * @private + * @param {String} match The matched character to unescape. + * @returns {String} Returns the unescaped character. + */ + function unescapeHtmlChar(match) { + return htmlUnescapes[match]; + } + + /*--------------------------------------------------------------------------*/ + + /** + * Checks if `value` is an `arguments` object. + * + * @static + * @memberOf _ + * @category Objects + * @param {Mixed} value The value to check. + * @returns {Boolean} Returns `true`, if the `value` is an `arguments` object, else `false`. + * @example + * + * (function() { return _.isArguments(arguments); })(1, 2, 3); + * // => true + * + * _.isArguments([1, 2, 3]); + * // => false + */ + function isArguments(value) { + return toString.call(value) == argsClass; + } + + /** + * Checks if `value` is an array. + * + * @static + * @memberOf _ + * @category Objects + * @param {Mixed} value The value to check. + * @returns {Boolean} Returns `true`, if the `value` is an array, else `false`. + * @example + * + * (function() { return _.isArray(arguments); })(); + * // => false + * + * _.isArray([1, 2, 3]); + * // => true + */ + var isArray = nativeIsArray; + + /** + * A fallback implementation of `Object.keys` which produces an array of the + * given object's own enumerable property names. + * + * @private + * @type Function + * @param {Object} object The object to inspect. + * @returns {Array} Returns a new array of property names. + */ + var shimKeys = function (object) { + var index, iterable = object, result = []; + if (!iterable) return result; + if (!(objectTypes[typeof object])) return result; + for (index in iterable) { + if (hasOwnProperty.call(iterable, index)) { + result.push(index); + } + } + return result + }; + + /** + * Creates an array composed of the own enumerable property names of `object`. + * + * @static + * @memberOf _ + * @category Objects + * @param {Object} object The object to inspect. + * @returns {Array} Returns a new array of property names. + * @example + * + * _.keys({ 'one': 1, 'two': 2, 'three': 3 }); + * // => ['one', 'two', 'three'] (order is not guaranteed) + */ + var keys = !nativeKeys ? shimKeys : function(object) { + if (!isObject(object)) { + return []; + } + return nativeKeys(object); + }; + + /** + * Used to convert characters to HTML entities: + * + * Though the `>` character is escaped for symmetry, characters like `>` and `/` + * don't require escaping in HTML and have no special meaning unless they're part + * of a tag or an unquoted attribute value. + * http://mathiasbynens.be/notes/ambiguous-ampersands (under "semi-related fun fact") + */ + var htmlEscapes = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''' + }; + + /** Used to convert HTML entities to characters */ + var htmlUnescapes = invert(htmlEscapes); + + /*--------------------------------------------------------------------------*/ + + /** + * Assigns own enumerable properties of source object(s) to the destination + * object. Subsequent sources will overwrite property assignments of previous + * sources. If a `callback` function is passed, it will be executed to produce + * the assigned values. The `callback` is bound to `thisArg` and invoked with + * two arguments; (objectValue, sourceValue). + * + * @static + * @memberOf _ + * @type Function + * @alias extend + * @category Objects + * @param {Object} object The destination object. + * @param {Object} [source1, source2, ...] The source objects. + * @param {Function} [callback] The function to customize assigning values. + * @param {Mixed} [thisArg] The `this` binding of `callback`. + * @returns {Object} Returns the destination object. + * @example + * + * _.assign({ 'name': 'moe' }, { 'age': 40 }); + * // => { 'name': 'moe', 'age': 40 } + * + * var defaults = _.partialRight(_.assign, function(a, b) { + * return typeof a == 'undefined' ? b : a; + * }); + * + * var food = { 'name': 'apple' }; + * defaults(food, { 'name': 'banana', 'type': 'fruit' }); + * // => { 'name': 'apple', 'type': 'fruit' } + */ + var assign = function (object, source, guard) { + var index, iterable = object, result = iterable; + if (!iterable) return result; + var args = arguments, + argsIndex = 0, + argsLength = typeof guard == 'number' ? 2 : args.length; + if (argsLength > 3 && typeof args[argsLength - 2] == 'function') { + var callback = lodash.createCallback(args[--argsLength - 1], args[argsLength--], 2); + } else if (argsLength > 2 && typeof args[argsLength - 1] == 'function') { + callback = args[--argsLength]; + } + while (++argsIndex < argsLength) { + iterable = args[argsIndex]; + if (iterable && objectTypes[typeof iterable]) { + var ownIndex = -1, + ownProps = objectTypes[typeof iterable] && keys(iterable), + length = ownProps ? ownProps.length : 0; + + while (++ownIndex < length) { + index = ownProps[ownIndex]; + result[index] = callback ? callback(result[index], iterable[index]) : iterable[index]; + } + } + } + return result + }; + + /** + * Creates a clone of `value`. If `deep` is `true`, nested objects will also + * be cloned, otherwise they will be assigned by reference. If a `callback` + * function is passed, it will be executed to produce the cloned values. If + * `callback` returns `undefined`, cloning will be handled by the method instead. + * The `callback` is bound to `thisArg` and invoked with one argument; (value). + * + * @static + * @memberOf _ + * @category Objects + * @param {Mixed} value The value to clone. + * @param {Boolean} [deep=false] A flag to indicate a deep clone. + * @param {Function} [callback] The function to customize cloning values. + * @param {Mixed} [thisArg] The `this` binding of `callback`. + * @param- {Array} [stackA=[]] Tracks traversed source objects. + * @param- {Array} [stackB=[]] Associates clones with source counterparts. + * @returns {Mixed} Returns the cloned `value`. + * @example + * + * var stooges = [ + * { 'name': 'moe', 'age': 40 }, + * { 'name': 'larry', 'age': 50 } + * ]; + * + * var shallow = _.clone(stooges); + * shallow[0] === stooges[0]; + * // => true + * + * var deep = _.clone(stooges, true); + * deep[0] === stooges[0]; + * // => false + * + * _.mixin({ + * 'clone': _.partialRight(_.clone, function(value) { + * return _.isElement(value) ? value.cloneNode(false) : undefined; + * }) + * }); + * + * var clone = _.clone(document.body); + * clone.childNodes.length; + * // => 0 + */ + function clone(value, deep, callback, thisArg, stackA, stackB) { + var result = value; + + // allows working with "Collections" methods without using their `callback` + // argument, `index|key`, for this method's `callback` + if (typeof deep != 'boolean' && deep != null) { + thisArg = callback; + callback = deep; + deep = false; + } + if (typeof callback == 'function') { + callback = (typeof thisArg == 'undefined') + ? callback + : lodash.createCallback(callback, thisArg, 1); + + result = callback(result); + if (typeof result != 'undefined') { + return result; + } + result = value; + } + // inspect [[Class]] + var isObj = isObject(result); + if (isObj) { + var className = toString.call(result); + if (!cloneableClasses[className]) { + return result; + } + var isArr = isArray(result); + } + // shallow clone + if (!isObj || !deep) { + return isObj + ? (isArr ? slice(result) : assign({}, result)) + : result; + } + var ctor = ctorByClass[className]; + switch (className) { + case boolClass: + case dateClass: + return new ctor(+result); + + case numberClass: + case stringClass: + return new ctor(result); + + case regexpClass: + return ctor(result.source, reFlags.exec(result)); + } + // check for circular references and return corresponding clone + var initedStack = !stackA; + stackA || (stackA = getArray()); + stackB || (stackB = getArray()); + + var length = stackA.length; + while (length--) { + if (stackA[length] == value) { + return stackB[length]; + } + } + // init cloned object + result = isArr ? ctor(result.length) : {}; + + // add array properties assigned by `RegExp#exec` + if (isArr) { + if (hasOwnProperty.call(value, 'index')) { + result.index = value.index; + } + if (hasOwnProperty.call(value, 'input')) { + result.input = value.input; + } + } + // add the source value to the stack of traversed objects + // and associate it with its clone + stackA.push(value); + stackB.push(result); + + // recursively populate clone (susceptible to call stack limits) + (isArr ? forEach : forOwn)(value, function(objValue, key) { + result[key] = clone(objValue, deep, callback, undefined, stackA, stackB); + }); + + if (initedStack) { + releaseArray(stackA); + releaseArray(stackB); + } + return result; + } + + /** + * Creates a deep clone of `value`. If a `callback` function is passed, + * it will be executed to produce the cloned values. If `callback` returns + * `undefined`, cloning will be handled by the method instead. The `callback` + * is bound to `thisArg` and invoked with one argument; (value). + * + * Note: This method is loosely based on the structured clone algorithm. Functions + * and DOM nodes are **not** cloned. The enumerable properties of `arguments` objects and + * objects created by constructors other than `Object` are cloned to plain `Object` objects. + * See http://www.w3.org/TR/html5/infrastructure.html#internal-structured-cloning-algorithm. + * + * @static + * @memberOf _ + * @category Objects + * @param {Mixed} value The value to deep clone. + * @param {Function} [callback] The function to customize cloning values. + * @param {Mixed} [thisArg] The `this` binding of `callback`. + * @returns {Mixed} Returns the deep cloned `value`. + * @example + * + * var stooges = [ + * { 'name': 'moe', 'age': 40 }, + * { 'name': 'larry', 'age': 50 } + * ]; + * + * var deep = _.cloneDeep(stooges); + * deep[0] === stooges[0]; + * // => false + * + * var view = { + * 'label': 'docs', + * 'node': element + * }; + * + * var clone = _.cloneDeep(view, function(value) { + * return _.isElement(value) ? value.cloneNode(true) : undefined; + * }); + * + * clone.node == view.node; + * // => false + */ + function cloneDeep(value, callback, thisArg) { + return clone(value, true, callback, thisArg); + } + + /** + * Assigns own enumerable properties of source object(s) to the destination + * object for all destination properties that resolve to `undefined`. Once a + * property is set, additional defaults of the same property will be ignored. + * + * @static + * @memberOf _ + * @type Function + * @category Objects + * @param {Object} object The destination object. + * @param {Object} [source1, source2, ...] The source objects. + * @param- {Object} [guard] Allows working with `_.reduce` without using its + * callback's `key` and `object` arguments as sources. + * @returns {Object} Returns the destination object. + * @example + * + * var food = { 'name': 'apple' }; + * _.defaults(food, { 'name': 'banana', 'type': 'fruit' }); + * // => { 'name': 'apple', 'type': 'fruit' } + */ + var defaults = function (object, source, guard) { + var index, iterable = object, result = iterable; + if (!iterable) return result; + var args = arguments, + argsIndex = 0, + argsLength = typeof guard == 'number' ? 2 : args.length; + while (++argsIndex < argsLength) { + iterable = args[argsIndex]; + if (iterable && objectTypes[typeof iterable]) { + var ownIndex = -1, + ownProps = objectTypes[typeof iterable] && keys(iterable), + length = ownProps ? ownProps.length : 0; + + while (++ownIndex < length) { + index = ownProps[ownIndex]; + if (typeof result[index] == 'undefined') result[index] = iterable[index]; + } + } + } + return result + }; + + /** + * This method is similar to `_.find`, except that it returns the key of the + * element that passes the callback check, instead of the element itself. + * + * @static + * @memberOf _ + * @category Objects + * @param {Object} object The object to search. + * @param {Function|Object|String} [callback=identity] The function called per + * iteration. If a property name or object is passed, it will be used to create + * a "_.pluck" or "_.where" style callback, respectively. + * @param {Mixed} [thisArg] The `this` binding of `callback`. + * @returns {Mixed} Returns the key of the found element, else `undefined`. + * @example + * + * _.findKey({ 'a': 1, 'b': 2, 'c': 3, 'd': 4 }, function(num) { + * return num % 2 == 0; + * }); + * // => 'b' + */ + function findKey(object, callback, thisArg) { + var result; + callback = lodash.createCallback(callback, thisArg); + forOwn(object, function(value, key, object) { + if (callback(value, key, object)) { + result = key; + return false; + } + }); + return result; + } + + /** + * Iterates over `object`'s own and inherited enumerable properties, executing + * the `callback` for each property. The `callback` is bound to `thisArg` and + * invoked with three arguments; (value, key, object). Callbacks may exit iteration + * early by explicitly returning `false`. + * + * @static + * @memberOf _ + * @type Function + * @category Objects + * @param {Object} object The object to iterate over. + * @param {Function} [callback=identity] The function called per iteration. + * @param {Mixed} [thisArg] The `this` binding of `callback`. + * @returns {Object} Returns `object`. + * @example + * + * function Dog(name) { + * this.name = name; + * } + * + * Dog.prototype.bark = function() { + * alert('Woof, woof!'); + * }; + * + * _.forIn(new Dog('Dagny'), function(value, key) { + * alert(key); + * }); + * // => alerts 'name' and 'bark' (order is not guaranteed) + */ + var forIn = function (collection, callback, thisArg) { + var index, iterable = collection, result = iterable; + if (!iterable) return result; + if (!objectTypes[typeof iterable]) return result; + callback = callback && typeof thisArg == 'undefined' ? callback : lodash.createCallback(callback, thisArg); + for (index in iterable) { + if (callback(iterable[index], index, collection) === false) return result; + } + return result + }; + + /** + * Iterates over an object's own enumerable properties, executing the `callback` + * for each property. The `callback` is bound to `thisArg` and invoked with three + * arguments; (value, key, object). Callbacks may exit iteration early by explicitly + * returning `false`. + * + * @static + * @memberOf _ + * @type Function + * @category Objects + * @param {Object} object The object to iterate over. + * @param {Function} [callback=identity] The function called per iteration. + * @param {Mixed} [thisArg] The `this` binding of `callback`. + * @returns {Object} Returns `object`. + * @example + * + * _.forOwn({ '0': 'zero', '1': 'one', 'length': 2 }, function(num, key) { + * alert(key); + * }); + * // => alerts '0', '1', and 'length' (order is not guaranteed) + */ + var forOwn = function (collection, callback, thisArg) { + var index, iterable = collection, result = iterable; + if (!iterable) return result; + if (!objectTypes[typeof iterable]) return result; + callback = callback && typeof thisArg == 'undefined' ? callback : lodash.createCallback(callback, thisArg); + var ownIndex = -1, + ownProps = objectTypes[typeof iterable] && keys(iterable), + length = ownProps ? ownProps.length : 0; + + while (++ownIndex < length) { + index = ownProps[ownIndex]; + if (callback(iterable[index], index, collection) === false) return result; + } + return result + }; + + /** + * Creates a sorted array of all enumerable properties, own and inherited, + * of `object` that have function values. + * + * @static + * @memberOf _ + * @alias methods + * @category Objects + * @param {Object} object The object to inspect. + * @returns {Array} Returns a new array of property names that have function values. + * @example + * + * _.functions(_); + * // => ['all', 'any', 'bind', 'bindAll', 'clone', 'compact', 'compose', ...] + */ + function functions(object) { + var result = []; + forIn(object, function(value, key) { + if (isFunction(value)) { + result.push(key); + } + }); + return result.sort(); + } + + /** + * Checks if the specified object `property` exists and is a direct property, + * instead of an inherited property. + * + * @static + * @memberOf _ + * @category Objects + * @param {Object} object The object to check. + * @param {String} property The property to check for. + * @returns {Boolean} Returns `true` if key is a direct property, else `false`. + * @example + * + * _.has({ 'a': 1, 'b': 2, 'c': 3 }, 'b'); + * // => true + */ + function has(object, property) { + return object ? hasOwnProperty.call(object, property) : false; + } + + /** + * Creates an object composed of the inverted keys and values of the given `object`. + * + * @static + * @memberOf _ + * @category Objects + * @param {Object} object The object to invert. + * @returns {Object} Returns the created inverted object. + * @example + * + * _.invert({ 'first': 'moe', 'second': 'larry' }); + * // => { 'moe': 'first', 'larry': 'second' } + */ + function invert(object) { + var index = -1, + props = keys(object), + length = props.length, + result = {}; + + while (++index < length) { + var key = props[index]; + result[object[key]] = key; + } + return result; + } + + /** + * Checks if `value` is a boolean value. + * + * @static + * @memberOf _ + * @category Objects + * @param {Mixed} value The value to check. + * @returns {Boolean} Returns `true`, if the `value` is a boolean value, else `false`. + * @example + * + * _.isBoolean(null); + * // => false + */ + function isBoolean(value) { + return value === true || value === false || toString.call(value) == boolClass; + } + + /** + * Checks if `value` is a date. + * + * @static + * @memberOf _ + * @category Objects + * @param {Mixed} value The value to check. + * @returns {Boolean} Returns `true`, if the `value` is a date, else `false`. + * @example + * + * _.isDate(new Date); + * // => true + */ + function isDate(value) { + return value ? (typeof value == 'object' && toString.call(value) == dateClass) : false; + } + + /** + * Checks if `value` is a DOM element. + * + * @static + * @memberOf _ + * @category Objects + * @param {Mixed} value The value to check. + * @returns {Boolean} Returns `true`, if the `value` is a DOM element, else `false`. + * @example + * + * _.isElement(document.body); + * // => true + */ + function isElement(value) { + return value ? value.nodeType === 1 : false; + } + + /** + * Checks if `value` is empty. Arrays, strings, or `arguments` objects with a + * length of `0` and objects with no own enumerable properties are considered + * "empty". + * + * @static + * @memberOf _ + * @category Objects + * @param {Array|Object|String} value The value to inspect. + * @returns {Boolean} Returns `true`, if the `value` is empty, else `false`. + * @example + * + * _.isEmpty([1, 2, 3]); + * // => false + * + * _.isEmpty({}); + * // => true + * + * _.isEmpty(''); + * // => true + */ + function isEmpty(value) { + var result = true; + if (!value) { + return result; + } + var className = toString.call(value), + length = value.length; + + if ((className == arrayClass || className == stringClass || className == argsClass ) || + (className == objectClass && typeof length == 'number' && isFunction(value.splice))) { + return !length; + } + forOwn(value, function() { + return (result = false); + }); + return result; + } + + /** + * Performs a deep comparison between two values to determine if they are + * equivalent to each other. If `callback` is passed, it will be executed to + * compare values. If `callback` returns `undefined`, comparisons will be handled + * by the method instead. The `callback` is bound to `thisArg` and invoked with + * two arguments; (a, b). + * + * @static + * @memberOf _ + * @category Objects + * @param {Mixed} a The value to compare. + * @param {Mixed} b The other value to compare. + * @param {Function} [callback] The function to customize comparing values. + * @param {Mixed} [thisArg] The `this` binding of `callback`. + * @param- {Array} [stackA=[]] Tracks traversed `a` objects. + * @param- {Array} [stackB=[]] Tracks traversed `b` objects. + * @returns {Boolean} Returns `true`, if the values are equivalent, else `false`. + * @example + * + * var moe = { 'name': 'moe', 'age': 40 }; + * var copy = { 'name': 'moe', 'age': 40 }; + * + * moe == copy; + * // => false + * + * _.isEqual(moe, copy); + * // => true + * + * var words = ['hello', 'goodbye']; + * var otherWords = ['hi', 'goodbye']; + * + * _.isEqual(words, otherWords, function(a, b) { + * var reGreet = /^(?:hello|hi)$/i, + * aGreet = _.isString(a) && reGreet.test(a), + * bGreet = _.isString(b) && reGreet.test(b); + * + * return (aGreet || bGreet) ? (aGreet == bGreet) : undefined; + * }); + * // => true + */ + function isEqual(a, b, callback, thisArg, stackA, stackB) { + // used to indicate that when comparing objects, `a` has at least the properties of `b` + var whereIndicator = callback === indicatorObject; + if (typeof callback == 'function' && !whereIndicator) { + callback = lodash.createCallback(callback, thisArg, 2); + var result = callback(a, b); + if (typeof result != 'undefined') { + return !!result; + } + } + // exit early for identical values + if (a === b) { + // treat `+0` vs. `-0` as not equal + return a !== 0 || (1 / a == 1 / b); + } + var type = typeof a, + otherType = typeof b; + + // exit early for unlike primitive values + if (a === a && + (!a || (type != 'function' && type != 'object')) && + (!b || (otherType != 'function' && otherType != 'object'))) { + return false; + } + // exit early for `null` and `undefined`, avoiding ES3's Function#call behavior + // http://es5.github.com/#x15.3.4.4 + if (a == null || b == null) { + return a === b; + } + // compare [[Class]] names + var className = toString.call(a), + otherClass = toString.call(b); + + if (className == argsClass) { + className = objectClass; + } + if (otherClass == argsClass) { + otherClass = objectClass; + } + if (className != otherClass) { + return false; + } + switch (className) { + case boolClass: + case dateClass: + // coerce dates and booleans to numbers, dates to milliseconds and booleans + // to `1` or `0`, treating invalid dates coerced to `NaN` as not equal + return +a == +b; + + case numberClass: + // treat `NaN` vs. `NaN` as equal + return (a != +a) + ? b != +b + // but treat `+0` vs. `-0` as not equal + : (a == 0 ? (1 / a == 1 / b) : a == +b); + + case regexpClass: + case stringClass: + // coerce regexes to strings (http://es5.github.com/#x15.10.6.4) + // treat string primitives and their corresponding object instances as equal + return a == String(b); + } + var isArr = className == arrayClass; + if (!isArr) { + // unwrap any `lodash` wrapped values + if (hasOwnProperty.call(a, '__wrapped__ ') || hasOwnProperty.call(b, '__wrapped__')) { + return isEqual(a.__wrapped__ || a, b.__wrapped__ || b, callback, thisArg, stackA, stackB); + } + // exit for functions and DOM nodes + if (className != objectClass) { + return false; + } + // in older versions of Opera, `arguments` objects have `Array` constructors + var ctorA = a.constructor, + ctorB = b.constructor; + + // non `Object` object instances with different constructors are not equal + if (ctorA != ctorB && !( + isFunction(ctorA) && ctorA instanceof ctorA && + isFunction(ctorB) && ctorB instanceof ctorB + )) { + return false; + } + } + // assume cyclic structures are equal + // the algorithm for detecting cyclic structures is adapted from ES 5.1 + // section 15.12.3, abstract operation `JO` (http://es5.github.com/#x15.12.3) + var initedStack = !stackA; + stackA || (stackA = getArray()); + stackB || (stackB = getArray()); + + var length = stackA.length; + while (length--) { + if (stackA[length] == a) { + return stackB[length] == b; + } + } + var size = 0; + result = true; + + // add `a` and `b` to the stack of traversed objects + stackA.push(a); + stackB.push(b); + + // recursively compare objects and arrays (susceptible to call stack limits) + if (isArr) { + length = a.length; + size = b.length; + + // compare lengths to determine if a deep comparison is necessary + result = size == a.length; + if (!result && !whereIndicator) { + return result; + } + // deep compare the contents, ignoring non-numeric properties + while (size--) { + var index = length, + value = b[size]; + + if (whereIndicator) { + while (index--) { + if ((result = isEqual(a[index], value, callback, thisArg, stackA, stackB))) { + break; + } + } + } else if (!(result = isEqual(a[size], value, callback, thisArg, stackA, stackB))) { + break; + } + } + return result; + } + // deep compare objects using `forIn`, instead of `forOwn`, to avoid `Object.keys` + // which, in this case, is more costly + forIn(b, function(value, key, b) { + if (hasOwnProperty.call(b, key)) { + // count the number of properties. + size++; + // deep compare each property value. + return (result = hasOwnProperty.call(a, key) && isEqual(a[key], value, callback, thisArg, stackA, stackB)); + } + }); + + if (result && !whereIndicator) { + // ensure both objects have the same number of properties + forIn(a, function(value, key, a) { + if (hasOwnProperty.call(a, key)) { + // `size` will be `-1` if `a` has more properties than `b` + return (result = --size > -1); + } + }); + } + if (initedStack) { + releaseArray(stackA); + releaseArray(stackB); + } + return result; + } + + /** + * Checks if `value` is, or can be coerced to, a finite number. + * + * Note: This is not the same as native `isFinite`, which will return true for + * booleans and empty strings. See http://es5.github.com/#x15.1.2.5. + * + * @static + * @memberOf _ + * @category Objects + * @param {Mixed} value The value to check. + * @returns {Boolean} Returns `true`, if the `value` is finite, else `false`. + * @example + * + * _.isFinite(-101); + * // => true + * + * _.isFinite('10'); + * // => true + * + * _.isFinite(true); + * // => false + * + * _.isFinite(''); + * // => false + * + * _.isFinite(Infinity); + * // => false + */ + function isFinite(value) { + return nativeIsFinite(value) && !nativeIsNaN(parseFloat(value)); + } + + /** + * Checks if `value` is a function. + * + * @static + * @memberOf _ + * @category Objects + * @param {Mixed} value The value to check. + * @returns {Boolean} Returns `true`, if the `value` is a function, else `false`. + * @example + * + * _.isFunction(_); + * // => true + */ + function isFunction(value) { + return typeof value == 'function'; + } + + /** + * Checks if `value` is the language type of Object. + * (e.g. arrays, functions, objects, regexes, `new Number(0)`, and `new String('')`) + * + * @static + * @memberOf _ + * @category Objects + * @param {Mixed} value The value to check. + * @returns {Boolean} Returns `true`, if the `value` is an object, else `false`. + * @example + * + * _.isObject({}); + * // => true + * + * _.isObject([1, 2, 3]); + * // => true + * + * _.isObject(1); + * // => false + */ + function isObject(value) { + // check if the value is the ECMAScript language type of Object + // http://es5.github.com/#x8 + // and avoid a V8 bug + // http://code.google.com/p/v8/issues/detail?id=2291 + return !!(value && objectTypes[typeof value]); + } + + /** + * Checks if `value` is `NaN`. + * + * Note: This is not the same as native `isNaN`, which will return `true` for + * `undefined` and other values. See http://es5.github.com/#x15.1.2.4. + * + * @static + * @memberOf _ + * @category Objects + * @param {Mixed} value The value to check. + * @returns {Boolean} Returns `true`, if the `value` is `NaN`, else `false`. + * @example + * + * _.isNaN(NaN); + * // => true + * + * _.isNaN(new Number(NaN)); + * // => true + * + * isNaN(undefined); + * // => true + * + * _.isNaN(undefined); + * // => false + */ + function isNaN(value) { + // `NaN` as a primitive is the only value that is not equal to itself + // (perform the [[Class]] check first to avoid errors with some host objects in IE) + return isNumber(value) && value != +value + } + + /** + * Checks if `value` is `null`. + * + * @static + * @memberOf _ + * @category Objects + * @param {Mixed} value The value to check. + * @returns {Boolean} Returns `true`, if the `value` is `null`, else `false`. + * @example + * + * _.isNull(null); + * // => true + * + * _.isNull(undefined); + * // => false + */ + function isNull(value) { + return value === null; + } + + /** + * Checks if `value` is a number. + * + * @static + * @memberOf _ + * @category Objects + * @param {Mixed} value The value to check. + * @returns {Boolean} Returns `true`, if the `value` is a number, else `false`. + * @example + * + * _.isNumber(8.4 * 5); + * // => true + */ + function isNumber(value) { + return typeof value == 'number' || toString.call(value) == numberClass; + } + + /** + * Checks if a given `value` is an object created by the `Object` constructor. + * + * @static + * @memberOf _ + * @category Objects + * @param {Mixed} value The value to check. + * @returns {Boolean} Returns `true`, if `value` is a plain object, else `false`. + * @example + * + * function Stooge(name, age) { + * this.name = name; + * this.age = age; + * } + * + * _.isPlainObject(new Stooge('moe', 40)); + * // => false + * + * _.isPlainObject([1, 2, 3]); + * // => false + * + * _.isPlainObject({ 'name': 'moe', 'age': 40 }); + * // => true + */ + var isPlainObject = function(value) { + if (!(value && toString.call(value) == objectClass)) { + return false; + } + var valueOf = value.valueOf, + objProto = typeof valueOf == 'function' && (objProto = getPrototypeOf(valueOf)) && getPrototypeOf(objProto); + + return objProto + ? (value == objProto || getPrototypeOf(value) == objProto) + : shimIsPlainObject(value); + }; + + /** + * Checks if `value` is a regular expression. + * + * @static + * @memberOf _ + * @category Objects + * @param {Mixed} value The value to check. + * @returns {Boolean} Returns `true`, if the `value` is a regular expression, else `false`. + * @example + * + * _.isRegExp(/moe/); + * // => true + */ + function isRegExp(value) { + return value ? (typeof value == 'object' && toString.call(value) == regexpClass) : false; + } + + /** + * Checks if `value` is a string. + * + * @static + * @memberOf _ + * @category Objects + * @param {Mixed} value The value to check. + * @returns {Boolean} Returns `true`, if the `value` is a string, else `false`. + * @example + * + * _.isString('moe'); + * // => true + */ + function isString(value) { + return typeof value == 'string' || toString.call(value) == stringClass; + } + + /** + * Checks if `value` is `undefined`. + * + * @static + * @memberOf _ + * @category Objects + * @param {Mixed} value The value to check. + * @returns {Boolean} Returns `true`, if the `value` is `undefined`, else `false`. + * @example + * + * _.isUndefined(void 0); + * // => true + */ + function isUndefined(value) { + return typeof value == 'undefined'; + } + + /** + * Recursively merges own enumerable properties of the source object(s), that + * don't resolve to `undefined`, into the destination object. Subsequent sources + * will overwrite property assignments of previous sources. If a `callback` function + * is passed, it will be executed to produce the merged values of the destination + * and source properties. If `callback` returns `undefined`, merging will be + * handled by the method instead. The `callback` is bound to `thisArg` and + * invoked with two arguments; (objectValue, sourceValue). + * + * @static + * @memberOf _ + * @category Objects + * @param {Object} object The destination object. + * @param {Object} [source1, source2, ...] The source objects. + * @param {Function} [callback] The function to customize merging properties. + * @param {Mixed} [thisArg] The `this` binding of `callback`. + * @param- {Object} [deepIndicator] Indicates that `stackA` and `stackB` are + * arrays of traversed objects, instead of source objects. + * @param- {Array} [stackA=[]] Tracks traversed source objects. + * @param- {Array} [stackB=[]] Associates values with source counterparts. + * @returns {Object} Returns the destination object. + * @example + * + * var names = { + * 'stooges': [ + * { 'name': 'moe' }, + * { 'name': 'larry' } + * ] + * }; + * + * var ages = { + * 'stooges': [ + * { 'age': 40 }, + * { 'age': 50 } + * ] + * }; + * + * _.merge(names, ages); + * // => { 'stooges': [{ 'name': 'moe', 'age': 40 }, { 'name': 'larry', 'age': 50 }] } + * + * var food = { + * 'fruits': ['apple'], + * 'vegetables': ['beet'] + * }; + * + * var otherFood = { + * 'fruits': ['banana'], + * 'vegetables': ['carrot'] + * }; + * + * _.merge(food, otherFood, function(a, b) { + * return _.isArray(a) ? a.concat(b) : undefined; + * }); + * // => { 'fruits': ['apple', 'banana'], 'vegetables': ['beet', 'carrot] } + */ + function merge(object, source, deepIndicator) { + var args = arguments, + index = 0, + length = 2; + + if (!isObject(object)) { + return object; + } + if (deepIndicator === indicatorObject) { + var callback = args[3], + stackA = args[4], + stackB = args[5]; + } else { + var initedStack = true; + stackA = getArray(); + stackB = getArray(); + + // allows working with `_.reduce` and `_.reduceRight` without + // using their `callback` arguments, `index|key` and `collection` + if (typeof deepIndicator != 'number') { + length = args.length; + } + if (length > 3 && typeof args[length - 2] == 'function') { + callback = lodash.createCallback(args[--length - 1], args[length--], 2); + } else if (length > 2 && typeof args[length - 1] == 'function') { + callback = args[--length]; + } + } + while (++index < length) { + (isArray(args[index]) ? forEach : forOwn)(args[index], function(source, key) { + var found, + isArr, + result = source, + value = object[key]; + + if (source && ((isArr = isArray(source)) || isPlainObject(source))) { + // avoid merging previously merged cyclic sources + var stackLength = stackA.length; + while (stackLength--) { + if ((found = stackA[stackLength] == source)) { + value = stackB[stackLength]; + break; + } + } + if (!found) { + var isShallow; + if (callback) { + result = callback(value, source); + if ((isShallow = typeof result != 'undefined')) { + value = result; + } + } + if (!isShallow) { + value = isArr + ? (isArray(value) ? value : []) + : (isPlainObject(value) ? value : {}); + } + // add `source` and associated `value` to the stack of traversed objects + stackA.push(source); + stackB.push(value); + + // recursively merge objects and arrays (susceptible to call stack limits) + if (!isShallow) { + value = merge(value, source, indicatorObject, callback, stackA, stackB); + } + } + } + else { + if (callback) { + result = callback(value, source); + if (typeof result == 'undefined') { + result = source; + } + } + if (typeof result != 'undefined') { + value = result; + } + } + object[key] = value; + }); + } + + if (initedStack) { + releaseArray(stackA); + releaseArray(stackB); + } + return object; + } + + /** + * Creates a shallow clone of `object` excluding the specified properties. + * Property names may be specified as individual arguments or as arrays of + * property names. If a `callback` function is passed, it will be executed + * for each property in the `object`, omitting the properties `callback` + * returns truthy for. The `callback` is bound to `thisArg` and invoked + * with three arguments; (value, key, object). + * + * @static + * @memberOf _ + * @category Objects + * @param {Object} object The source object. + * @param {Function|String} callback|[prop1, prop2, ...] The properties to omit + * or the function called per iteration. + * @param {Mixed} [thisArg] The `this` binding of `callback`. + * @returns {Object} Returns an object without the omitted properties. + * @example + * + * _.omit({ 'name': 'moe', 'age': 40 }, 'age'); + * // => { 'name': 'moe' } + * + * _.omit({ 'name': 'moe', 'age': 40 }, function(value) { + * return typeof value == 'number'; + * }); + * // => { 'name': 'moe' } + */ + function omit(object, callback, thisArg) { + var indexOf = getIndexOf(), + isFunc = typeof callback == 'function', + result = {}; + + if (isFunc) { + callback = lodash.createCallback(callback, thisArg); + } else { + var props = concat.apply(arrayRef, nativeSlice.call(arguments, 1)); + } + forIn(object, function(value, key, object) { + if (isFunc + ? !callback(value, key, object) + : indexOf(props, key) < 0 + ) { + result[key] = value; + } + }); + return result; + } + + /** + * Creates a two dimensional array of the given object's key-value pairs, + * i.e. `[[key1, value1], [key2, value2]]`. + * + * @static + * @memberOf _ + * @category Objects + * @param {Object} object The object to inspect. + * @returns {Array} Returns new array of key-value pairs. + * @example + * + * _.pairs({ 'moe': 30, 'larry': 40 }); + * // => [['moe', 30], ['larry', 40]] (order is not guaranteed) + */ + function pairs(object) { + var index = -1, + props = keys(object), + length = props.length, + result = Array(length); + + while (++index < length) { + var key = props[index]; + result[index] = [key, object[key]]; + } + return result; + } + + /** + * Creates a shallow clone of `object` composed of the specified properties. + * Property names may be specified as individual arguments or as arrays of property + * names. If `callback` is passed, it will be executed for each property in the + * `object`, picking the properties `callback` returns truthy for. The `callback` + * is bound to `thisArg` and invoked with three arguments; (value, key, object). + * + * @static + * @memberOf _ + * @category Objects + * @param {Object} object The source object. + * @param {Array|Function|String} callback|[prop1, prop2, ...] The function called + * per iteration or properties to pick, either as individual arguments or arrays. + * @param {Mixed} [thisArg] The `this` binding of `callback`. + * @returns {Object} Returns an object composed of the picked properties. + * @example + * + * _.pick({ 'name': 'moe', '_userid': 'moe1' }, 'name'); + * // => { 'name': 'moe' } + * + * _.pick({ 'name': 'moe', '_userid': 'moe1' }, function(value, key) { + * return key.charAt(0) != '_'; + * }); + * // => { 'name': 'moe' } + */ + function pick(object, callback, thisArg) { + var result = {}; + if (typeof callback != 'function') { + var index = -1, + props = concat.apply(arrayRef, nativeSlice.call(arguments, 1)), + length = isObject(object) ? props.length : 0; + + while (++index < length) { + var key = props[index]; + if (key in object) { + result[key] = object[key]; + } + } + } else { + callback = lodash.createCallback(callback, thisArg); + forIn(object, function(value, key, object) { + if (callback(value, key, object)) { + result[key] = value; + } + }); + } + return result; + } + + /** + * An alternative to `_.reduce`, this method transforms an `object` to a new + * `accumulator` object which is the result of running each of its elements + * through the `callback`, with each `callback` execution potentially mutating + * the `accumulator` object. The `callback` is bound to `thisArg` and invoked + * with four arguments; (accumulator, value, key, object). Callbacks may exit + * iteration early by explicitly returning `false`. + * + * @static + * @memberOf _ + * @category Objects + * @param {Array|Object} collection The collection to iterate over. + * @param {Function} [callback=identity] The function called per iteration. + * @param {Mixed} [accumulator] The custom accumulator value. + * @param {Mixed} [thisArg] The `this` binding of `callback`. + * @returns {Mixed} Returns the accumulated value. + * @example + * + * var squares = _.transform([1, 2, 3, 4, 5, 6, 7, 8, 9, 10], function(result, num) { + * num *= num; + * if (num % 2) { + * return result.push(num) < 3; + * } + * }); + * // => [1, 9, 25] + * + * var mapped = _.transform({ 'a': 1, 'b': 2, 'c': 3 }, function(result, num, key) { + * result[key] = num * 3; + * }); + * // => { 'a': 3, 'b': 6, 'c': 9 } + */ + function transform(object, callback, accumulator, thisArg) { + var isArr = isArray(object); + callback = lodash.createCallback(callback, thisArg, 4); + + if (accumulator == null) { + if (isArr) { + accumulator = []; + } else { + var ctor = object && object.constructor, + proto = ctor && ctor.prototype; + + accumulator = createObject(proto); + } + } + (isArr ? forEach : forOwn)(object, function(value, index, object) { + return callback(accumulator, value, index, object); + }); + return accumulator; + } + + /** + * Creates an array composed of the own enumerable property values of `object`. + * + * @static + * @memberOf _ + * @category Objects + * @param {Object} object The object to inspect. + * @returns {Array} Returns a new array of property values. + * @example + * + * _.values({ 'one': 1, 'two': 2, 'three': 3 }); + * // => [1, 2, 3] (order is not guaranteed) + */ + function values(object) { + var index = -1, + props = keys(object), + length = props.length, + result = Array(length); + + while (++index < length) { + result[index] = object[props[index]]; + } + return result; + } + + /*--------------------------------------------------------------------------*/ + + /** + * Creates an array of elements from the specified indexes, or keys, of the + * `collection`. Indexes may be specified as individual arguments or as arrays + * of indexes. + * + * @static + * @memberOf _ + * @category Collections + * @param {Array|Object|String} collection The collection to iterate over. + * @param {Array|Number|String} [index1, index2, ...] The indexes of + * `collection` to retrieve, either as individual arguments or arrays. + * @returns {Array} Returns a new array of elements corresponding to the + * provided indexes. + * @example + * + * _.at(['a', 'b', 'c', 'd', 'e'], [0, 2, 4]); + * // => ['a', 'c', 'e'] + * + * _.at(['moe', 'larry', 'curly'], 0, 2); + * // => ['moe', 'curly'] + */ + function at(collection) { + var index = -1, + props = concat.apply(arrayRef, nativeSlice.call(arguments, 1)), + length = props.length, + result = Array(length); + + while(++index < length) { + result[index] = collection[props[index]]; + } + return result; + } + + /** + * Checks if a given `target` element is present in a `collection` using strict + * equality for comparisons, i.e. `===`. If `fromIndex` is negative, it is used + * as the offset from the end of the collection. + * + * @static + * @memberOf _ + * @alias include + * @category Collections + * @param {Array|Object|String} collection The collection to iterate over. + * @param {Mixed} target The value to check for. + * @param {Number} [fromIndex=0] The index to search from. + * @returns {Boolean} Returns `true` if the `target` element is found, else `false`. + * @example + * + * _.contains([1, 2, 3], 1); + * // => true + * + * _.contains([1, 2, 3], 1, 2); + * // => false + * + * _.contains({ 'name': 'moe', 'age': 40 }, 'moe'); + * // => true + * + * _.contains('curly', 'ur'); + * // => true + */ + function contains(collection, target, fromIndex) { + var index = -1, + indexOf = getIndexOf(), + length = collection ? collection.length : 0, + result = false; + + fromIndex = (fromIndex < 0 ? nativeMax(0, length + fromIndex) : fromIndex) || 0; + if (length && typeof length == 'number') { + result = (isString(collection) + ? collection.indexOf(target, fromIndex) + : indexOf(collection, target, fromIndex) + ) > -1; + } else { + forOwn(collection, function(value) { + if (++index >= fromIndex) { + return !(result = value === target); + } + }); + } + return result; + } + + /** + * Creates an object composed of keys returned from running each element of the + * `collection` through the given `callback`. The corresponding value of each key + * is the number of times the key was returned by the `callback`. The `callback` + * is bound to `thisArg` and invoked with three arguments; (value, index|key, collection). + * + * If a property name is passed for `callback`, the created "_.pluck" style + * callback will return the property value of the given element. + * + * If an object is passed for `callback`, the created "_.where" style callback + * will return `true` for elements that have the properties of the given object, + * else `false`. + * + * @static + * @memberOf _ + * @category Collections + * @param {Array|Object|String} collection The collection to iterate over. + * @param {Function|Object|String} [callback=identity] The function called per + * iteration. If a property name or object is passed, it will be used to create + * a "_.pluck" or "_.where" style callback, respectively. + * @param {Mixed} [thisArg] The `this` binding of `callback`. + * @returns {Object} Returns the composed aggregate object. + * @example + * + * _.countBy([4.3, 6.1, 6.4], function(num) { return Math.floor(num); }); + * // => { '4': 1, '6': 2 } + * + * _.countBy([4.3, 6.1, 6.4], function(num) { return this.floor(num); }, Math); + * // => { '4': 1, '6': 2 } + * + * _.countBy(['one', 'two', 'three'], 'length'); + * // => { '3': 2, '5': 1 } + */ + function countBy(collection, callback, thisArg) { + var result = {}; + callback = lodash.createCallback(callback, thisArg); + + forEach(collection, function(value, key, collection) { + key = String(callback(value, key, collection)); + (hasOwnProperty.call(result, key) ? result[key]++ : result[key] = 1); + }); + return result; + } + + /** + * Checks if the `callback` returns a truthy value for **all** elements of a + * `collection`. The `callback` is bound to `thisArg` and invoked with three + * arguments; (value, index|key, collection). + * + * If a property name is passed for `callback`, the created "_.pluck" style + * callback will return the property value of the given element. + * + * If an object is passed for `callback`, the created "_.where" style callback + * will return `true` for elements that have the properties of the given object, + * else `false`. + * + * @static + * @memberOf _ + * @alias all + * @category Collections + * @param {Array|Object|String} collection The collection to iterate over. + * @param {Function|Object|String} [callback=identity] The function called per + * iteration. If a property name or object is passed, it will be used to create + * a "_.pluck" or "_.where" style callback, respectively. + * @param {Mixed} [thisArg] The `this` binding of `callback`. + * @returns {Boolean} Returns `true` if all elements pass the callback check, + * else `false`. + * @example + * + * _.every([true, 1, null, 'yes'], Boolean); + * // => false + * + * var stooges = [ + * { 'name': 'moe', 'age': 40 }, + * { 'name': 'larry', 'age': 50 } + * ]; + * + * // using "_.pluck" callback shorthand + * _.every(stooges, 'age'); + * // => true + * + * // using "_.where" callback shorthand + * _.every(stooges, { 'age': 50 }); + * // => false + */ + function every(collection, callback, thisArg) { + var result = true; + callback = lodash.createCallback(callback, thisArg); + + var index = -1, + length = collection ? collection.length : 0; + + if (typeof length == 'number') { + while (++index < length) { + if (!(result = !!callback(collection[index], index, collection))) { + break; + } + } + } else { + forOwn(collection, function(value, index, collection) { + return (result = !!callback(value, index, collection)); + }); + } + return result; + } + + /** + * Examines each element in a `collection`, returning an array of all elements + * the `callback` returns truthy for. The `callback` is bound to `thisArg` and + * invoked with three arguments; (value, index|key, collection). + * + * If a property name is passed for `callback`, the created "_.pluck" style + * callback will return the property value of the given element. + * + * If an object is passed for `callback`, the created "_.where" style callback + * will return `true` for elements that have the properties of the given object, + * else `false`. + * + * @static + * @memberOf _ + * @alias select + * @category Collections + * @param {Array|Object|String} collection The collection to iterate over. + * @param {Function|Object|String} [callback=identity] The function called per + * iteration. If a property name or object is passed, it will be used to create + * a "_.pluck" or "_.where" style callback, respectively. + * @param {Mixed} [thisArg] The `this` binding of `callback`. + * @returns {Array} Returns a new array of elements that passed the callback check. + * @example + * + * var evens = _.filter([1, 2, 3, 4, 5, 6], function(num) { return num % 2 == 0; }); + * // => [2, 4, 6] + * + * var food = [ + * { 'name': 'apple', 'organic': false, 'type': 'fruit' }, + * { 'name': 'carrot', 'organic': true, 'type': 'vegetable' } + * ]; + * + * // using "_.pluck" callback shorthand + * _.filter(food, 'organic'); + * // => [{ 'name': 'carrot', 'organic': true, 'type': 'vegetable' }] + * + * // using "_.where" callback shorthand + * _.filter(food, { 'type': 'fruit' }); + * // => [{ 'name': 'apple', 'organic': false, 'type': 'fruit' }] + */ + function filter(collection, callback, thisArg) { + var result = []; + callback = lodash.createCallback(callback, thisArg); + + var index = -1, + length = collection ? collection.length : 0; + + if (typeof length == 'number') { + while (++index < length) { + var value = collection[index]; + if (callback(value, index, collection)) { + result.push(value); + } + } + } else { + forOwn(collection, function(value, index, collection) { + if (callback(value, index, collection)) { + result.push(value); + } + }); + } + return result; + } + + /** + * Examines each element in a `collection`, returning the first that the `callback` + * returns truthy for. The `callback` is bound to `thisArg` and invoked with three + * arguments; (value, index|key, collection). + * + * If a property name is passed for `callback`, the created "_.pluck" style + * callback will return the property value of the given element. + * + * If an object is passed for `callback`, the created "_.where" style callback + * will return `true` for elements that have the properties of the given object, + * else `false`. + * + * @static + * @memberOf _ + * @alias detect, findWhere + * @category Collections + * @param {Array|Object|String} collection The collection to iterate over. + * @param {Function|Object|String} [callback=identity] The function called per + * iteration. If a property name or object is passed, it will be used to create + * a "_.pluck" or "_.where" style callback, respectively. + * @param {Mixed} [thisArg] The `this` binding of `callback`. + * @returns {Mixed} Returns the found element, else `undefined`. + * @example + * + * _.find([1, 2, 3, 4], function(num) { + * return num % 2 == 0; + * }); + * // => 2 + * + * var food = [ + * { 'name': 'apple', 'organic': false, 'type': 'fruit' }, + * { 'name': 'banana', 'organic': true, 'type': 'fruit' }, + * { 'name': 'beet', 'organic': false, 'type': 'vegetable' } + * ]; + * + * // using "_.where" callback shorthand + * _.find(food, { 'type': 'vegetable' }); + * // => { 'name': 'beet', 'organic': false, 'type': 'vegetable' } + * + * // using "_.pluck" callback shorthand + * _.find(food, 'organic'); + * // => { 'name': 'banana', 'organic': true, 'type': 'fruit' } + */ + function find(collection, callback, thisArg) { + callback = lodash.createCallback(callback, thisArg); + + var index = -1, + length = collection ? collection.length : 0; + + if (typeof length == 'number') { + while (++index < length) { + var value = collection[index]; + if (callback(value, index, collection)) { + return value; + } + } + } else { + var result; + forOwn(collection, function(value, index, collection) { + if (callback(value, index, collection)) { + result = value; + return false; + } + }); + return result; + } + } + + /** + * Iterates over a `collection`, executing the `callback` for each element in + * the `collection`. The `callback` is bound to `thisArg` and invoked with three + * arguments; (value, index|key, collection). Callbacks may exit iteration early + * by explicitly returning `false`. + * + * @static + * @memberOf _ + * @alias each + * @category Collections + * @param {Array|Object|String} collection The collection to iterate over. + * @param {Function} [callback=identity] The function called per iteration. + * @param {Mixed} [thisArg] The `this` binding of `callback`. + * @returns {Array|Object|String} Returns `collection`. + * @example + * + * _([1, 2, 3]).forEach(alert).join(','); + * // => alerts each number and returns '1,2,3' + * + * _.forEach({ 'one': 1, 'two': 2, 'three': 3 }, alert); + * // => alerts each number value (order is not guaranteed) + */ + function forEach(collection, callback, thisArg) { + var index = -1, + length = collection ? collection.length : 0; + + callback = callback && typeof thisArg == 'undefined' ? callback : lodash.createCallback(callback, thisArg); + if (typeof length == 'number') { + while (++index < length) { + if (callback(collection[index], index, collection) === false) { + break; + } + } + } else { + forOwn(collection, callback); + } + return collection; + } + + /** + * Creates an object composed of keys returned from running each element of the + * `collection` through the `callback`. The corresponding value of each key is + * an array of elements passed to `callback` that returned the key. The `callback` + * is bound to `thisArg` and invoked with three arguments; (value, index|key, collection). + * + * If a property name is passed for `callback`, the created "_.pluck" style + * callback will return the property value of the given element. + * + * If an object is passed for `callback`, the created "_.where" style callback + * will return `true` for elements that have the properties of the given object, + * else `false` + * + * @static + * @memberOf _ + * @category Collections + * @param {Array|Object|String} collection The collection to iterate over. + * @param {Function|Object|String} [callback=identity] The function called per + * iteration. If a property name or object is passed, it will be used to create + * a "_.pluck" or "_.where" style callback, respectively. + * @param {Mixed} [thisArg] The `this` binding of `callback`. + * @returns {Object} Returns the composed aggregate object. + * @example + * + * _.groupBy([4.2, 6.1, 6.4], function(num) { return Math.floor(num); }); + * // => { '4': [4.2], '6': [6.1, 6.4] } + * + * _.groupBy([4.2, 6.1, 6.4], function(num) { return this.floor(num); }, Math); + * // => { '4': [4.2], '6': [6.1, 6.4] } + * + * // using "_.pluck" callback shorthand + * _.groupBy(['one', 'two', 'three'], 'length'); + * // => { '3': ['one', 'two'], '5': ['three'] } + */ + function groupBy(collection, callback, thisArg) { + var result = {}; + callback = lodash.createCallback(callback, thisArg); + + forEach(collection, function(value, key, collection) { + key = String(callback(value, key, collection)); + (hasOwnProperty.call(result, key) ? result[key] : result[key] = []).push(value); + }); + return result; + } + + /** + * Invokes the method named by `methodName` on each element in the `collection`, + * returning an array of the results of each invoked method. Additional arguments + * will be passed to each invoked method. If `methodName` is a function, it will + * be invoked for, and `this` bound to, each element in the `collection`. + * + * @static + * @memberOf _ + * @category Collections + * @param {Array|Object|String} collection The collection to iterate over. + * @param {Function|String} methodName The name of the method to invoke or + * the function invoked per iteration. + * @param {Mixed} [arg1, arg2, ...] Arguments to invoke the method with. + * @returns {Array} Returns a new array of the results of each invoked method. + * @example + * + * _.invoke([[5, 1, 7], [3, 2, 1]], 'sort'); + * // => [[1, 5, 7], [1, 2, 3]] + * + * _.invoke([123, 456], String.prototype.split, ''); + * // => [['1', '2', '3'], ['4', '5', '6']] + */ + function invoke(collection, methodName) { + var args = nativeSlice.call(arguments, 2), + index = -1, + isFunc = typeof methodName == 'function', + length = collection ? collection.length : 0, + result = Array(typeof length == 'number' ? length : 0); + + forEach(collection, function(value) { + result[++index] = (isFunc ? methodName : value[methodName]).apply(value, args); + }); + return result; + } + + /** + * Creates an array of values by running each element in the `collection` + * through the `callback`. The `callback` is bound to `thisArg` and invoked with + * three arguments; (value, index|key, collection). + * + * If a property name is passed for `callback`, the created "_.pluck" style + * callback will return the property value of the given element. + * + * If an object is passed for `callback`, the created "_.where" style callback + * will return `true` for elements that have the properties of the given object, + * else `false`. + * + * @static + * @memberOf _ + * @alias collect + * @category Collections + * @param {Array|Object|String} collection The collection to iterate over. + * @param {Function|Object|String} [callback=identity] The function called per + * iteration. If a property name or object is passed, it will be used to create + * a "_.pluck" or "_.where" style callback, respectively. + * @param {Mixed} [thisArg] The `this` binding of `callback`. + * @returns {Array} Returns a new array of the results of each `callback` execution. + * @example + * + * _.map([1, 2, 3], function(num) { return num * 3; }); + * // => [3, 6, 9] + * + * _.map({ 'one': 1, 'two': 2, 'three': 3 }, function(num) { return num * 3; }); + * // => [3, 6, 9] (order is not guaranteed) + * + * var stooges = [ + * { 'name': 'moe', 'age': 40 }, + * { 'name': 'larry', 'age': 50 } + * ]; + * + * // using "_.pluck" callback shorthand + * _.map(stooges, 'name'); + * // => ['moe', 'larry'] + */ + function map(collection, callback, thisArg) { + var index = -1, + length = collection ? collection.length : 0; + + callback = lodash.createCallback(callback, thisArg); + if (typeof length == 'number') { + var result = Array(length); + while (++index < length) { + result[index] = callback(collection[index], index, collection); + } + } else { + result = []; + forOwn(collection, function(value, key, collection) { + result[++index] = callback(value, key, collection); + }); + } + return result; + } + + /** + * Retrieves the maximum value of an `array`. If `callback` is passed, + * it will be executed for each value in the `array` to generate the + * criterion by which the value is ranked. The `callback` is bound to + * `thisArg` and invoked with three arguments; (value, index, collection). + * + * If a property name is passed for `callback`, the created "_.pluck" style + * callback will return the property value of the given element. + * + * If an object is passed for `callback`, the created "_.where" style callback + * will return `true` for elements that have the properties of the given object, + * else `false`. + * + * @static + * @memberOf _ + * @category Collections + * @param {Array|Object|String} collection The collection to iterate over. + * @param {Function|Object|String} [callback=identity] The function called per + * iteration. If a property name or object is passed, it will be used to create + * a "_.pluck" or "_.where" style callback, respectively. + * @param {Mixed} [thisArg] The `this` binding of `callback`. + * @returns {Mixed} Returns the maximum value. + * @example + * + * _.max([4, 2, 8, 6]); + * // => 8 + * + * var stooges = [ + * { 'name': 'moe', 'age': 40 }, + * { 'name': 'larry', 'age': 50 } + * ]; + * + * _.max(stooges, function(stooge) { return stooge.age; }); + * // => { 'name': 'larry', 'age': 50 }; + * + * // using "_.pluck" callback shorthand + * _.max(stooges, 'age'); + * // => { 'name': 'larry', 'age': 50 }; + */ + function max(collection, callback, thisArg) { + var computed = -Infinity, + result = computed; + + if (!callback && isArray(collection)) { + var index = -1, + length = collection.length; + + while (++index < length) { + var value = collection[index]; + if (value > result) { + result = value; + } + } + } else { + callback = (!callback && isString(collection)) + ? charAtCallback + : lodash.createCallback(callback, thisArg); + + forEach(collection, function(value, index, collection) { + var current = callback(value, index, collection); + if (current > computed) { + computed = current; + result = value; + } + }); + } + return result; + } + + /** + * Retrieves the minimum value of an `array`. If `callback` is passed, + * it will be executed for each value in the `array` to generate the + * criterion by which the value is ranked. The `callback` is bound to `thisArg` + * and invoked with three arguments; (value, index, collection). + * + * If a property name is passed for `callback`, the created "_.pluck" style + * callback will return the property value of the given element. + * + * If an object is passed for `callback`, the created "_.where" style callback + * will return `true` for elements that have the properties of the given object, + * else `false`. + * + * @static + * @memberOf _ + * @category Collections + * @param {Array|Object|String} collection The collection to iterate over. + * @param {Function|Object|String} [callback=identity] The function called per + * iteration. If a property name or object is passed, it will be used to create + * a "_.pluck" or "_.where" style callback, respectively. + * @param {Mixed} [thisArg] The `this` binding of `callback`. + * @returns {Mixed} Returns the minimum value. + * @example + * + * _.min([4, 2, 8, 6]); + * // => 2 + * + * var stooges = [ + * { 'name': 'moe', 'age': 40 }, + * { 'name': 'larry', 'age': 50 } + * ]; + * + * _.min(stooges, function(stooge) { return stooge.age; }); + * // => { 'name': 'moe', 'age': 40 }; + * + * // using "_.pluck" callback shorthand + * _.min(stooges, 'age'); + * // => { 'name': 'moe', 'age': 40 }; + */ + function min(collection, callback, thisArg) { + var computed = Infinity, + result = computed; + + if (!callback && isArray(collection)) { + var index = -1, + length = collection.length; + + while (++index < length) { + var value = collection[index]; + if (value < result) { + result = value; + } + } + } else { + callback = (!callback && isString(collection)) + ? charAtCallback + : lodash.createCallback(callback, thisArg); + + forEach(collection, function(value, index, collection) { + var current = callback(value, index, collection); + if (current < computed) { + computed = current; + result = value; + } + }); + } + return result; + } + + /** + * Retrieves the value of a specified property from all elements in the `collection`. + * + * @static + * @memberOf _ + * @type Function + * @category Collections + * @param {Array|Object|String} collection The collection to iterate over. + * @param {String} property The property to pluck. + * @returns {Array} Returns a new array of property values. + * @example + * + * var stooges = [ + * { 'name': 'moe', 'age': 40 }, + * { 'name': 'larry', 'age': 50 } + * ]; + * + * _.pluck(stooges, 'name'); + * // => ['moe', 'larry'] + */ + function pluck(collection, property) { + var index = -1, + length = collection ? collection.length : 0; + + if (typeof length == 'number') { + var result = Array(length); + while (++index < length) { + result[index] = collection[index][property]; + } + } + return result || map(collection, property); + } + + /** + * Reduces a `collection` to a value which is the accumulated result of running + * each element in the `collection` through the `callback`, where each successive + * `callback` execution consumes the return value of the previous execution. + * If `accumulator` is not passed, the first element of the `collection` will be + * used as the initial `accumulator` value. The `callback` is bound to `thisArg` + * and invoked with four arguments; (accumulator, value, index|key, collection). + * + * @static + * @memberOf _ + * @alias foldl, inject + * @category Collections + * @param {Array|Object|String} collection The collection to iterate over. + * @param {Function} [callback=identity] The function called per iteration. + * @param {Mixed} [accumulator] Initial value of the accumulator. + * @param {Mixed} [thisArg] The `this` binding of `callback`. + * @returns {Mixed} Returns the accumulated value. + * @example + * + * var sum = _.reduce([1, 2, 3], function(sum, num) { + * return sum + num; + * }); + * // => 6 + * + * var mapped = _.reduce({ 'a': 1, 'b': 2, 'c': 3 }, function(result, num, key) { + * result[key] = num * 3; + * return result; + * }, {}); + * // => { 'a': 3, 'b': 6, 'c': 9 } + */ + function reduce(collection, callback, accumulator, thisArg) { + if (!collection) return accumulator; + var noaccum = arguments.length < 3; + callback = lodash.createCallback(callback, thisArg, 4); + + var index = -1, + length = collection.length; + + if (typeof length == 'number') { + if (noaccum) { + accumulator = collection[++index]; + } + while (++index < length) { + accumulator = callback(accumulator, collection[index], index, collection); + } + } else { + forOwn(collection, function(value, index, collection) { + accumulator = noaccum + ? (noaccum = false, value) + : callback(accumulator, value, index, collection) + }); + } + return accumulator; + } + + /** + * This method is similar to `_.reduce`, except that it iterates over a + * `collection` from right to left. + * + * @static + * @memberOf _ + * @alias foldr + * @category Collections + * @param {Array|Object|String} collection The collection to iterate over. + * @param {Function} [callback=identity] The function called per iteration. + * @param {Mixed} [accumulator] Initial value of the accumulator. + * @param {Mixed} [thisArg] The `this` binding of `callback`. + * @returns {Mixed} Returns the accumulated value. + * @example + * + * var list = [[0, 1], [2, 3], [4, 5]]; + * var flat = _.reduceRight(list, function(a, b) { return a.concat(b); }, []); + * // => [4, 5, 2, 3, 0, 1] + */ + function reduceRight(collection, callback, accumulator, thisArg) { + var iterable = collection, + length = collection ? collection.length : 0, + noaccum = arguments.length < 3; + + if (typeof length != 'number') { + var props = keys(collection); + length = props.length; + } + callback = lodash.createCallback(callback, thisArg, 4); + forEach(collection, function(value, index, collection) { + index = props ? props[--length] : --length; + accumulator = noaccum + ? (noaccum = false, iterable[index]) + : callback(accumulator, iterable[index], index, collection); + }); + return accumulator; + } + + /** + * The opposite of `_.filter`, this method returns the elements of a + * `collection` that `callback` does **not** return truthy for. + * + * If a property name is passed for `callback`, the created "_.pluck" style + * callback will return the property value of the given element. + * + * If an object is passed for `callback`, the created "_.where" style callback + * will return `true` for elements that have the properties of the given object, + * else `false`. + * + * @static + * @memberOf _ + * @category Collections + * @param {Array|Object|String} collection The collection to iterate over. + * @param {Function|Object|String} [callback=identity] The function called per + * iteration. If a property name or object is passed, it will be used to create + * a "_.pluck" or "_.where" style callback, respectively. + * @param {Mixed} [thisArg] The `this` binding of `callback`. + * @returns {Array} Returns a new array of elements that did **not** pass the + * callback check. + * @example + * + * var odds = _.reject([1, 2, 3, 4, 5, 6], function(num) { return num % 2 == 0; }); + * // => [1, 3, 5] + * + * var food = [ + * { 'name': 'apple', 'organic': false, 'type': 'fruit' }, + * { 'name': 'carrot', 'organic': true, 'type': 'vegetable' } + * ]; + * + * // using "_.pluck" callback shorthand + * _.reject(food, 'organic'); + * // => [{ 'name': 'apple', 'organic': false, 'type': 'fruit' }] + * + * // using "_.where" callback shorthand + * _.reject(food, { 'type': 'fruit' }); + * // => [{ 'name': 'carrot', 'organic': true, 'type': 'vegetable' }] + */ + function reject(collection, callback, thisArg) { + callback = lodash.createCallback(callback, thisArg); + return filter(collection, function(value, index, collection) { + return !callback(value, index, collection); + }); + } + + /** + * Creates an array of shuffled `array` values, using a version of the + * Fisher-Yates shuffle. See http://en.wikipedia.org/wiki/Fisher-Yates_shuffle. + * + * @static + * @memberOf _ + * @category Collections + * @param {Array|Object|String} collection The collection to shuffle. + * @returns {Array} Returns a new shuffled collection. + * @example + * + * _.shuffle([1, 2, 3, 4, 5, 6]); + * // => [4, 1, 6, 3, 5, 2] + */ + function shuffle(collection) { + var index = -1, + length = collection ? collection.length : 0, + result = Array(typeof length == 'number' ? length : 0); + + forEach(collection, function(value) { + var rand = floor(nativeRandom() * (++index + 1)); + result[index] = result[rand]; + result[rand] = value; + }); + return result; + } + + /** + * Gets the size of the `collection` by returning `collection.length` for arrays + * and array-like objects or the number of own enumerable properties for objects. + * + * @static + * @memberOf _ + * @category Collections + * @param {Array|Object|String} collection The collection to inspect. + * @returns {Number} Returns `collection.length` or number of own enumerable properties. + * @example + * + * _.size([1, 2]); + * // => 2 + * + * _.size({ 'one': 1, 'two': 2, 'three': 3 }); + * // => 3 + * + * _.size('curly'); + * // => 5 + */ + function size(collection) { + var length = collection ? collection.length : 0; + return typeof length == 'number' ? length : keys(collection).length; + } + + /** + * Checks if the `callback` returns a truthy value for **any** element of a + * `collection`. The function returns as soon as it finds passing value, and + * does not iterate over the entire `collection`. The `callback` is bound to + * `thisArg` and invoked with three arguments; (value, index|key, collection). + * + * If a property name is passed for `callback`, the created "_.pluck" style + * callback will return the property value of the given element. + * + * If an object is passed for `callback`, the created "_.where" style callback + * will return `true` for elements that have the properties of the given object, + * else `false`. + * + * @static + * @memberOf _ + * @alias any + * @category Collections + * @param {Array|Object|String} collection The collection to iterate over. + * @param {Function|Object|String} [callback=identity] The function called per + * iteration. If a property name or object is passed, it will be used to create + * a "_.pluck" or "_.where" style callback, respectively. + * @param {Mixed} [thisArg] The `this` binding of `callback`. + * @returns {Boolean} Returns `true` if any element passes the callback check, + * else `false`. + * @example + * + * _.some([null, 0, 'yes', false], Boolean); + * // => true + * + * var food = [ + * { 'name': 'apple', 'organic': false, 'type': 'fruit' }, + * { 'name': 'carrot', 'organic': true, 'type': 'vegetable' } + * ]; + * + * // using "_.pluck" callback shorthand + * _.some(food, 'organic'); + * // => true + * + * // using "_.where" callback shorthand + * _.some(food, { 'type': 'meat' }); + * // => false + */ + function some(collection, callback, thisArg) { + var result; + callback = lodash.createCallback(callback, thisArg); + + var index = -1, + length = collection ? collection.length : 0; + + if (typeof length == 'number') { + while (++index < length) { + if ((result = callback(collection[index], index, collection))) { + break; + } + } + } else { + forOwn(collection, function(value, index, collection) { + return !(result = callback(value, index, collection)); + }); + } + return !!result; + } + + /** + * Creates an array of elements, sorted in ascending order by the results of + * running each element in the `collection` through the `callback`. This method + * performs a stable sort, that is, it will preserve the original sort order of + * equal elements. The `callback` is bound to `thisArg` and invoked with three + * arguments; (value, index|key, collection). + * + * If a property name is passed for `callback`, the created "_.pluck" style + * callback will return the property value of the given element. + * + * If an object is passed for `callback`, the created "_.where" style callback + * will return `true` for elements that have the properties of the given object, + * else `false`. + * + * @static + * @memberOf _ + * @category Collections + * @param {Array|Object|String} collection The collection to iterate over. + * @param {Function|Object|String} [callback=identity] The function called per + * iteration. If a property name or object is passed, it will be used to create + * a "_.pluck" or "_.where" style callback, respectively. + * @param {Mixed} [thisArg] The `this` binding of `callback`. + * @returns {Array} Returns a new array of sorted elements. + * @example + * + * _.sortBy([1, 2, 3], function(num) { return Math.sin(num); }); + * // => [3, 1, 2] + * + * _.sortBy([1, 2, 3], function(num) { return this.sin(num); }, Math); + * // => [3, 1, 2] + * + * // using "_.pluck" callback shorthand + * _.sortBy(['banana', 'strawberry', 'apple'], 'length'); + * // => ['apple', 'banana', 'strawberry'] + */ + function sortBy(collection, callback, thisArg) { + var index = -1, + length = collection ? collection.length : 0, + result = Array(typeof length == 'number' ? length : 0); + + callback = lodash.createCallback(callback, thisArg); + forEach(collection, function(value, key, collection) { + var object = result[++index] = getObject(); + object.criteria = callback(value, key, collection); + object.index = index; + object.value = value; + }); + + length = result.length; + result.sort(compareAscending); + while (length--) { + var object = result[length]; + result[length] = object.value; + releaseObject(object); + } + return result; + } + + /** + * Converts the `collection` to an array. + * + * @static + * @memberOf _ + * @category Collections + * @param {Array|Object|String} collection The collection to convert. + * @returns {Array} Returns the new converted array. + * @example + * + * (function() { return _.toArray(arguments).slice(1); })(1, 2, 3, 4); + * // => [2, 3, 4] + */ + function toArray(collection) { + if (collection && typeof collection.length == 'number') { + return slice(collection); + } + return values(collection); + } + + /** + * Examines each element in a `collection`, returning an array of all elements + * that have the given `properties`. When checking `properties`, this method + * performs a deep comparison between values to determine if they are equivalent + * to each other. + * + * @static + * @memberOf _ + * @type Function + * @category Collections + * @param {Array|Object|String} collection The collection to iterate over. + * @param {Object} properties The object of property values to filter by. + * @returns {Array} Returns a new array of elements that have the given `properties`. + * @example + * + * var stooges = [ + * { 'name': 'moe', 'age': 40 }, + * { 'name': 'larry', 'age': 50 } + * ]; + * + * _.where(stooges, { 'age': 40 }); + * // => [{ 'name': 'moe', 'age': 40 }] + */ + var where = filter; + + /*--------------------------------------------------------------------------*/ + + /** + * Creates an array with all falsey values of `array` removed. The values + * `false`, `null`, `0`, `""`, `undefined` and `NaN` are all falsey. + * + * @static + * @memberOf _ + * @category Arrays + * @param {Array} array The array to compact. + * @returns {Array} Returns a new filtered array. + * @example + * + * _.compact([0, 1, false, 2, '', 3]); + * // => [1, 2, 3] + */ + function compact(array) { + var index = -1, + length = array ? array.length : 0, + result = []; + + while (++index < length) { + var value = array[index]; + if (value) { + result.push(value); + } + } + return result; + } + + /** + * Creates an array of `array` elements not present in the other arrays + * using strict equality for comparisons, i.e. `===`. + * + * @static + * @memberOf _ + * @category Arrays + * @param {Array} array The array to process. + * @param {Array} [array1, array2, ...] Arrays to check. + * @returns {Array} Returns a new array of `array` elements not present in the + * other arrays. + * @example + * + * _.difference([1, 2, 3, 4, 5], [5, 2, 10]); + * // => [1, 3, 4] + */ + function difference(array) { + var index = -1, + indexOf = getIndexOf(), + length = array ? array.length : 0, + seen = concat.apply(arrayRef, nativeSlice.call(arguments, 1)), + result = []; + + var isLarge = length >= largeArraySize && indexOf === basicIndexOf; + + if (isLarge) { + var cache = createCache(seen); + if (cache) { + indexOf = cacheIndexOf; + seen = cache; + } else { + isLarge = false; + } + } + while (++index < length) { + var value = array[index]; + if (indexOf(seen, value) < 0) { + result.push(value); + } + } + if (isLarge) { + releaseObject(seen); + } + return result; + } + + /** + * This method is similar to `_.find`, except that it returns the index of + * the element that passes the callback check, instead of the element itself. + * + * @static + * @memberOf _ + * @category Arrays + * @param {Array} array The array to search. + * @param {Function|Object|String} [callback=identity] The function called per + * iteration. If a property name or object is passed, it will be used to create + * a "_.pluck" or "_.where" style callback, respectively. + * @param {Mixed} [thisArg] The `this` binding of `callback`. + * @returns {Mixed} Returns the index of the found element, else `-1`. + * @example + * + * _.findIndex(['apple', 'banana', 'beet'], function(food) { + * return /^b/.test(food); + * }); + * // => 1 + */ + function findIndex(array, callback, thisArg) { + var index = -1, + length = array ? array.length : 0; + + callback = lodash.createCallback(callback, thisArg); + while (++index < length) { + if (callback(array[index], index, array)) { + return index; + } + } + return -1; + } + + /** + * Gets the first element of the `array`. If a number `n` is passed, the first + * `n` elements of the `array` are returned. If a `callback` function is passed, + * elements at the beginning of the array are returned as long as the `callback` + * returns truthy. The `callback` is bound to `thisArg` and invoked with three + * arguments; (value, index, array). + * + * If a property name is passed for `callback`, the created "_.pluck" style + * callback will return the property value of the given element. + * + * If an object is passed for `callback`, the created "_.where" style callback + * will return `true` for elements that have the properties of the given object, + * else `false`. + * + * @static + * @memberOf _ + * @alias head, take + * @category Arrays + * @param {Array} array The array to query. + * @param {Function|Object|Number|String} [callback|n] The function called + * per element or the number of elements to return. If a property name or + * object is passed, it will be used to create a "_.pluck" or "_.where" + * style callback, respectively. + * @param {Mixed} [thisArg] The `this` binding of `callback`. + * @returns {Mixed} Returns the first element(s) of `array`. + * @example + * + * _.first([1, 2, 3]); + * // => 1 + * + * _.first([1, 2, 3], 2); + * // => [1, 2] + * + * _.first([1, 2, 3], function(num) { + * return num < 3; + * }); + * // => [1, 2] + * + * var food = [ + * { 'name': 'banana', 'organic': true }, + * { 'name': 'beet', 'organic': false }, + * ]; + * + * // using "_.pluck" callback shorthand + * _.first(food, 'organic'); + * // => [{ 'name': 'banana', 'organic': true }] + * + * var food = [ + * { 'name': 'apple', 'type': 'fruit' }, + * { 'name': 'banana', 'type': 'fruit' }, + * { 'name': 'beet', 'type': 'vegetable' } + * ]; + * + * // using "_.where" callback shorthand + * _.first(food, { 'type': 'fruit' }); + * // => [{ 'name': 'apple', 'type': 'fruit' }, { 'name': 'banana', 'type': 'fruit' }] + */ + function first(array, callback, thisArg) { + if (array) { + var n = 0, + length = array.length; + + if (typeof callback != 'number' && callback != null) { + var index = -1; + callback = lodash.createCallback(callback, thisArg); + while (++index < length && callback(array[index], index, array)) { + n++; + } + } else { + n = callback; + if (n == null || thisArg) { + return array[0]; + } + } + return slice(array, 0, nativeMin(nativeMax(0, n), length)); + } + } + + /** + * Flattens a nested array (the nesting can be to any depth). If `isShallow` + * is truthy, `array` will only be flattened a single level. If `callback` + * is passed, each element of `array` is passed through a `callback` before + * flattening. The `callback` is bound to `thisArg` and invoked with three + * arguments; (value, index, array). + * + * If a property name is passed for `callback`, the created "_.pluck" style + * callback will return the property value of the given element. + * + * If an object is passed for `callback`, the created "_.where" style callback + * will return `true` for elements that have the properties of the given object, + * else `false`. + * + * @static + * @memberOf _ + * @category Arrays + * @param {Array} array The array to flatten. + * @param {Boolean} [isShallow=false] A flag to indicate only flattening a single level. + * @param {Function|Object|String} [callback=identity] The function called per + * iteration. If a property name or object is passed, it will be used to create + * a "_.pluck" or "_.where" style callback, respectively. + * @param {Mixed} [thisArg] The `this` binding of `callback`. + * @returns {Array} Returns a new flattened array. + * @example + * + * _.flatten([1, [2], [3, [[4]]]]); + * // => [1, 2, 3, 4]; + * + * _.flatten([1, [2], [3, [[4]]]], true); + * // => [1, 2, 3, [[4]]]; + * + * var stooges = [ + * { 'name': 'curly', 'quotes': ['Oh, a wise guy, eh?', 'Poifect!'] }, + * { 'name': 'moe', 'quotes': ['Spread out!', 'You knucklehead!'] } + * ]; + * + * // using "_.pluck" callback shorthand + * _.flatten(stooges, 'quotes'); + * // => ['Oh, a wise guy, eh?', 'Poifect!', 'Spread out!', 'You knucklehead!'] + */ + var flatten = overloadWrapper(function flatten(array, isShallow, callback) { + var index = -1, + length = array ? array.length : 0, + result = []; + + while (++index < length) { + var value = array[index]; + if (callback) { + value = callback(value, index, array); + } + // recursively flatten arrays (susceptible to call stack limits) + if (isArray(value)) { + push.apply(result, isShallow ? value : flatten(value)); + } else { + result.push(value); + } + } + return result; + }); + + /** + * Gets the index at which the first occurrence of `value` is found using + * strict equality for comparisons, i.e. `===`. If the `array` is already + * sorted, passing `true` for `fromIndex` will run a faster binary search. + * + * @static + * @memberOf _ + * @category Arrays + * @param {Array} array The array to search. + * @param {Mixed} value The value to search for. + * @param {Boolean|Number} [fromIndex=0] The index to search from or `true` to + * perform a binary search on a sorted `array`. + * @returns {Number} Returns the index of the matched value or `-1`. + * @example + * + * _.indexOf([1, 2, 3, 1, 2, 3], 2); + * // => 1 + * + * _.indexOf([1, 2, 3, 1, 2, 3], 2, 3); + * // => 4 + * + * _.indexOf([1, 1, 2, 2, 3, 3], 2, true); + * // => 2 + */ + function indexOf(array, value, fromIndex) { + if (typeof fromIndex == 'number') { + var length = array ? array.length : 0; + fromIndex = (fromIndex < 0 ? nativeMax(0, length + fromIndex) : fromIndex || 0); + } else if (fromIndex) { + var index = sortedIndex(array, value); + return array[index] === value ? index : -1; + } + return array ? basicIndexOf(array, value, fromIndex) : -1; + } + + /** + * Gets all but the last element of `array`. If a number `n` is passed, the + * last `n` elements are excluded from the result. If a `callback` function + * is passed, elements at the end of the array are excluded from the result + * as long as the `callback` returns truthy. The `callback` is bound to + * `thisArg` and invoked with three arguments; (value, index, array). + * + * If a property name is passed for `callback`, the created "_.pluck" style + * callback will return the property value of the given element. + * + * If an object is passed for `callback`, the created "_.where" style callback + * will return `true` for elements that have the properties of the given object, + * else `false`. + * + * @static + * @memberOf _ + * @category Arrays + * @param {Array} array The array to query. + * @param {Function|Object|Number|String} [callback|n=1] The function called + * per element or the number of elements to exclude. If a property name or + * object is passed, it will be used to create a "_.pluck" or "_.where" + * style callback, respectively. + * @param {Mixed} [thisArg] The `this` binding of `callback`. + * @returns {Array} Returns a slice of `array`. + * @example + * + * _.initial([1, 2, 3]); + * // => [1, 2] + * + * _.initial([1, 2, 3], 2); + * // => [1] + * + * _.initial([1, 2, 3], function(num) { + * return num > 1; + * }); + * // => [1] + * + * var food = [ + * { 'name': 'beet', 'organic': false }, + * { 'name': 'carrot', 'organic': true } + * ]; + * + * // using "_.pluck" callback shorthand + * _.initial(food, 'organic'); + * // => [{ 'name': 'beet', 'organic': false }] + * + * var food = [ + * { 'name': 'banana', 'type': 'fruit' }, + * { 'name': 'beet', 'type': 'vegetable' }, + * { 'name': 'carrot', 'type': 'vegetable' } + * ]; + * + * // using "_.where" callback shorthand + * _.initial(food, { 'type': 'vegetable' }); + * // => [{ 'name': 'banana', 'type': 'fruit' }] + */ + function initial(array, callback, thisArg) { + if (!array) { + return []; + } + var n = 0, + length = array.length; + + if (typeof callback != 'number' && callback != null) { + var index = length; + callback = lodash.createCallback(callback, thisArg); + while (index-- && callback(array[index], index, array)) { + n++; + } + } else { + n = (callback == null || thisArg) ? 1 : callback || n; + } + return slice(array, 0, nativeMin(nativeMax(0, length - n), length)); + } + + /** + * Computes the intersection of all the passed-in arrays using strict equality + * for comparisons, i.e. `===`. + * + * @static + * @memberOf _ + * @category Arrays + * @param {Array} [array1, array2, ...] Arrays to process. + * @returns {Array} Returns a new array of unique elements that are present + * in **all** of the arrays. + * @example + * + * _.intersection([1, 2, 3], [101, 2, 1, 10], [2, 1]); + * // => [1, 2] + */ + function intersection(array) { + var args = arguments, + argsLength = args.length, + argsIndex = -1, + caches = getArray(), + index = -1, + indexOf = getIndexOf(), + length = array ? array.length : 0, + result = [], + seen = getArray(); + + while (++argsIndex < argsLength) { + var value = args[argsIndex]; + caches[argsIndex] = indexOf === basicIndexOf && + (value ? value.length : 0) >= largeArraySize && + createCache(argsIndex ? args[argsIndex] : seen); + } + outer: + while (++index < length) { + var cache = caches[0]; + value = array[index]; + + if ((cache ? cacheIndexOf(cache, value) : indexOf(seen, value)) < 0) { + argsIndex = argsLength; + (cache || seen).push(value); + while (--argsIndex) { + cache = caches[argsIndex]; + if ((cache ? cacheIndexOf(cache, value) : indexOf(args[argsIndex], value)) < 0) { + continue outer; + } + } + result.push(value); + } + } + while (argsLength--) { + cache = caches[argsLength]; + if (cache) { + releaseObject(cache); + } + } + releaseArray(caches); + releaseArray(seen); + return result; + } + + /** + * Gets the last element of the `array`. If a number `n` is passed, the + * last `n` elements of the `array` are returned. If a `callback` function + * is passed, elements at the end of the array are returned as long as the + * `callback` returns truthy. The `callback` is bound to `thisArg` and + * invoked with three arguments;(value, index, array). + * + * + * If a property name is passed for `callback`, the created "_.pluck" style + * callback will return the property value of the given element. + * + * If an object is passed for `callback`, the created "_.where" style callback + * will return `true` for elements that have the properties of the given object, + * else `false`. + * + * @static + * @memberOf _ + * @category Arrays + * @param {Array} array The array to query. + * @param {Function|Object|Number|String} [callback|n] The function called + * per element or the number of elements to return. If a property name or + * object is passed, it will be used to create a "_.pluck" or "_.where" + * style callback, respectively. + * @param {Mixed} [thisArg] The `this` binding of `callback`. + * @returns {Mixed} Returns the last element(s) of `array`. + * @example + * + * _.last([1, 2, 3]); + * // => 3 + * + * _.last([1, 2, 3], 2); + * // => [2, 3] + * + * _.last([1, 2, 3], function(num) { + * return num > 1; + * }); + * // => [2, 3] + * + * var food = [ + * { 'name': 'beet', 'organic': false }, + * { 'name': 'carrot', 'organic': true } + * ]; + * + * // using "_.pluck" callback shorthand + * _.last(food, 'organic'); + * // => [{ 'name': 'carrot', 'organic': true }] + * + * var food = [ + * { 'name': 'banana', 'type': 'fruit' }, + * { 'name': 'beet', 'type': 'vegetable' }, + * { 'name': 'carrot', 'type': 'vegetable' } + * ]; + * + * // using "_.where" callback shorthand + * _.last(food, { 'type': 'vegetable' }); + * // => [{ 'name': 'beet', 'type': 'vegetable' }, { 'name': 'carrot', 'type': 'vegetable' }] + */ + function last(array, callback, thisArg) { + if (array) { + var n = 0, + length = array.length; + + if (typeof callback != 'number' && callback != null) { + var index = length; + callback = lodash.createCallback(callback, thisArg); + while (index-- && callback(array[index], index, array)) { + n++; + } + } else { + n = callback; + if (n == null || thisArg) { + return array[length - 1]; + } + } + return slice(array, nativeMax(0, length - n)); + } + } + + /** + * Gets the index at which the last occurrence of `value` is found using strict + * equality for comparisons, i.e. `===`. If `fromIndex` is negative, it is used + * as the offset from the end of the collection. + * + * @static + * @memberOf _ + * @category Arrays + * @param {Array} array The array to search. + * @param {Mixed} value The value to search for. + * @param {Number} [fromIndex=array.length-1] The index to search from. + * @returns {Number} Returns the index of the matched value or `-1`. + * @example + * + * _.lastIndexOf([1, 2, 3, 1, 2, 3], 2); + * // => 4 + * + * _.lastIndexOf([1, 2, 3, 1, 2, 3], 2, 3); + * // => 1 + */ + function lastIndexOf(array, value, fromIndex) { + var index = array ? array.length : 0; + if (typeof fromIndex == 'number') { + index = (fromIndex < 0 ? nativeMax(0, index + fromIndex) : nativeMin(fromIndex, index - 1)) + 1; + } + while (index--) { + if (array[index] === value) { + return index; + } + } + return -1; + } + + /** + * Creates an array of numbers (positive and/or negative) progressing from + * `start` up to but not including `end`. + * + * @static + * @memberOf _ + * @category Arrays + * @param {Number} [start=0] The start of the range. + * @param {Number} end The end of the range. + * @param {Number} [step=1] The value to increment or decrement by. + * @returns {Array} Returns a new range array. + * @example + * + * _.range(10); + * // => [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] + * + * _.range(1, 11); + * // => [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] + * + * _.range(0, 30, 5); + * // => [0, 5, 10, 15, 20, 25] + * + * _.range(0, -10, -1); + * // => [0, -1, -2, -3, -4, -5, -6, -7, -8, -9] + * + * _.range(0); + * // => [] + */ + function range(start, end, step) { + start = +start || 0; + step = +step || 1; + + if (end == null) { + end = start; + start = 0; + } + // use `Array(length)` so V8 will avoid the slower "dictionary" mode + // http://youtu.be/XAqIpGU8ZZk#t=17m25s + var index = -1, + length = nativeMax(0, ceil((end - start) / step)), + result = Array(length); + + while (++index < length) { + result[index] = start; + start += step; + } + return result; + } + + /** + * The opposite of `_.initial`, this method gets all but the first value of + * `array`. If a number `n` is passed, the first `n` values are excluded from + * the result. If a `callback` function is passed, elements at the beginning + * of the array are excluded from the result as long as the `callback` returns + * truthy. The `callback` is bound to `thisArg` and invoked with three + * arguments; (value, index, array). + * + * If a property name is passed for `callback`, the created "_.pluck" style + * callback will return the property value of the given element. + * + * If an object is passed for `callback`, the created "_.where" style callback + * will return `true` for elements that have the properties of the given object, + * else `false`. + * + * @static + * @memberOf _ + * @alias drop, tail + * @category Arrays + * @param {Array} array The array to query. + * @param {Function|Object|Number|String} [callback|n=1] The function called + * per element or the number of elements to exclude. If a property name or + * object is passed, it will be used to create a "_.pluck" or "_.where" + * style callback, respectively. + * @param {Mixed} [thisArg] The `this` binding of `callback`. + * @returns {Array} Returns a slice of `array`. + * @example + * + * _.rest([1, 2, 3]); + * // => [2, 3] + * + * _.rest([1, 2, 3], 2); + * // => [3] + * + * _.rest([1, 2, 3], function(num) { + * return num < 3; + * }); + * // => [3] + * + * var food = [ + * { 'name': 'banana', 'organic': true }, + * { 'name': 'beet', 'organic': false }, + * ]; + * + * // using "_.pluck" callback shorthand + * _.rest(food, 'organic'); + * // => [{ 'name': 'beet', 'organic': false }] + * + * var food = [ + * { 'name': 'apple', 'type': 'fruit' }, + * { 'name': 'banana', 'type': 'fruit' }, + * { 'name': 'beet', 'type': 'vegetable' } + * ]; + * + * // using "_.where" callback shorthand + * _.rest(food, { 'type': 'fruit' }); + * // => [{ 'name': 'beet', 'type': 'vegetable' }] + */ + function rest(array, callback, thisArg) { + if (typeof callback != 'number' && callback != null) { + var n = 0, + index = -1, + length = array ? array.length : 0; + + callback = lodash.createCallback(callback, thisArg); + while (++index < length && callback(array[index], index, array)) { + n++; + } + } else { + n = (callback == null || thisArg) ? 1 : nativeMax(0, callback); + } + return slice(array, n); + } + + /** + * Uses a binary search to determine the smallest index at which the `value` + * should be inserted into `array` in order to maintain the sort order of the + * sorted `array`. If `callback` is passed, it will be executed for `value` and + * each element in `array` to compute their sort ranking. The `callback` is + * bound to `thisArg` and invoked with one argument; (value). + * + * If a property name is passed for `callback`, the created "_.pluck" style + * callback will return the property value of the given element. + * + * If an object is passed for `callback`, the created "_.where" style callback + * will return `true` for elements that have the properties of the given object, + * else `false`. + * + * @static + * @memberOf _ + * @category Arrays + * @param {Array} array The array to inspect. + * @param {Mixed} value The value to evaluate. + * @param {Function|Object|String} [callback=identity] The function called per + * iteration. If a property name or object is passed, it will be used to create + * a "_.pluck" or "_.where" style callback, respectively. + * @param {Mixed} [thisArg] The `this` binding of `callback`. + * @returns {Number} Returns the index at which the value should be inserted + * into `array`. + * @example + * + * _.sortedIndex([20, 30, 50], 40); + * // => 2 + * + * // using "_.pluck" callback shorthand + * _.sortedIndex([{ 'x': 20 }, { 'x': 30 }, { 'x': 50 }], { 'x': 40 }, 'x'); + * // => 2 + * + * var dict = { + * 'wordToNumber': { 'twenty': 20, 'thirty': 30, 'fourty': 40, 'fifty': 50 } + * }; + * + * _.sortedIndex(['twenty', 'thirty', 'fifty'], 'fourty', function(word) { + * return dict.wordToNumber[word]; + * }); + * // => 2 + * + * _.sortedIndex(['twenty', 'thirty', 'fifty'], 'fourty', function(word) { + * return this.wordToNumber[word]; + * }, dict); + * // => 2 + */ + function sortedIndex(array, value, callback, thisArg) { + var low = 0, + high = array ? array.length : low; + + // explicitly reference `identity` for better inlining in Firefox + callback = callback ? lodash.createCallback(callback, thisArg, 1) : identity; + value = callback(value); + + while (low < high) { + var mid = (low + high) >>> 1; + (callback(array[mid]) < value) + ? low = mid + 1 + : high = mid; + } + return low; + } + + /** + * Computes the union of the passed-in arrays using strict equality for + * comparisons, i.e. `===`. + * + * @static + * @memberOf _ + * @category Arrays + * @param {Array} [array1, array2, ...] Arrays to process. + * @returns {Array} Returns a new array of unique values, in order, that are + * present in one or more of the arrays. + * @example + * + * _.union([1, 2, 3], [101, 2, 1, 10], [2, 1]); + * // => [1, 2, 3, 101, 10] + */ + function union(array) { + if (!isArray(array)) { + arguments[0] = array ? nativeSlice.call(array) : arrayRef; + } + return uniq(concat.apply(arrayRef, arguments)); + } + + /** + * Creates a duplicate-value-free version of the `array` using strict equality + * for comparisons, i.e. `===`. If the `array` is already sorted, passing `true` + * for `isSorted` will run a faster algorithm. If `callback` is passed, each + * element of `array` is passed through the `callback` before uniqueness is computed. + * The `callback` is bound to `thisArg` and invoked with three arguments; (value, index, array). + * + * If a property name is passed for `callback`, the created "_.pluck" style + * callback will return the property value of the given element. + * + * If an object is passed for `callback`, the created "_.where" style callback + * will return `true` for elements that have the properties of the given object, + * else `false`. + * + * @static + * @memberOf _ + * @alias unique + * @category Arrays + * @param {Array} array The array to process. + * @param {Boolean} [isSorted=false] A flag to indicate that the `array` is already sorted. + * @param {Function|Object|String} [callback=identity] The function called per + * iteration. If a property name or object is passed, it will be used to create + * a "_.pluck" or "_.where" style callback, respectively. + * @param {Mixed} [thisArg] The `this` binding of `callback`. + * @returns {Array} Returns a duplicate-value-free array. + * @example + * + * _.uniq([1, 2, 1, 3, 1]); + * // => [1, 2, 3] + * + * _.uniq([1, 1, 2, 2, 3], true); + * // => [1, 2, 3] + * + * _.uniq(['A', 'b', 'C', 'a', 'B', 'c'], function(letter) { return letter.toLowerCase(); }); + * // => ['A', 'b', 'C'] + * + * _.uniq([1, 2.5, 3, 1.5, 2, 3.5], function(num) { return this.floor(num); }, Math); + * // => [1, 2.5, 3] + * + * // using "_.pluck" callback shorthand + * _.uniq([{ 'x': 1 }, { 'x': 2 }, { 'x': 1 }], 'x'); + * // => [{ 'x': 1 }, { 'x': 2 }] + */ + var uniq = overloadWrapper(function(array, isSorted, callback) { + var index = -1, + indexOf = getIndexOf(), + length = array ? array.length : 0, + result = []; + + var isLarge = !isSorted && length >= largeArraySize && indexOf === basicIndexOf, + seen = (callback || isLarge) ? getArray() : result; + + if (isLarge) { + var cache = createCache(seen); + if (cache) { + indexOf = cacheIndexOf; + seen = cache; + } else { + isLarge = false; + seen = callback ? seen : (releaseArray(seen), result); + } + } + while (++index < length) { + var value = array[index], + computed = callback ? callback(value, index, array) : value; + + if (isSorted + ? !index || seen[seen.length - 1] !== computed + : indexOf(seen, computed) < 0 + ) { + if (callback || isLarge) { + seen.push(computed); + } + result.push(value); + } + } + if (isLarge) { + releaseArray(seen.array); + releaseObject(seen); + } else if (callback) { + releaseArray(seen); + } + return result; + }); + + /** + * The inverse of `_.zip`, this method splits groups of elements into arrays + * composed of elements from each group at their corresponding indexes. + * + * @static + * @memberOf _ + * @category Arrays + * @param {Array} array The array to process. + * @returns {Array} Returns a new array of the composed arrays. + * @example + * + * _.unzip([['moe', 30, true], ['larry', 40, false]]); + * // => [['moe', 'larry'], [30, 40], [true, false]]; + */ + function unzip(array) { + var index = -1, + length = array ? max(pluck(array, 'length')) : 0, + result = Array(length < 0 ? 0 : length); + + while (++index < length) { + result[index] = pluck(array, index); + } + return result; + } + + /** + * Creates an array with all occurrences of the passed values removed using + * strict equality for comparisons, i.e. `===`. + * + * @static + * @memberOf _ + * @category Arrays + * @param {Array} array The array to filter. + * @param {Mixed} [value1, value2, ...] Values to remove. + * @returns {Array} Returns a new filtered array. + * @example + * + * _.without([1, 2, 1, 0, 3, 1, 4], 0, 1); + * // => [2, 3, 4] + */ + function without(array) { + return difference(array, nativeSlice.call(arguments, 1)); + } + + /** + * Groups the elements of each array at their corresponding indexes. Useful for + * separate data sources that are coordinated through matching array indexes. + * For a matrix of nested arrays, `_.zip.apply(...)` can transpose the matrix + * in a similar fashion. + * + * @static + * @memberOf _ + * @category Arrays + * @param {Array} [array1, array2, ...] Arrays to process. + * @returns {Array} Returns a new array of grouped elements. + * @example + * + * _.zip(['moe', 'larry'], [30, 40], [true, false]); + * // => [['moe', 30, true], ['larry', 40, false]] + */ + function zip(array) { + return array ? unzip(arguments) : []; + } + + /** + * Creates an object composed from arrays of `keys` and `values`. Pass either + * a single two dimensional array, i.e. `[[key1, value1], [key2, value2]]`, or + * two arrays, one of `keys` and one of corresponding `values`. + * + * @static + * @memberOf _ + * @alias object + * @category Arrays + * @param {Array} keys The array of keys. + * @param {Array} [values=[]] The array of values. + * @returns {Object} Returns an object composed of the given keys and + * corresponding values. + * @example + * + * _.zipObject(['moe', 'larry'], [30, 40]); + * // => { 'moe': 30, 'larry': 40 } + */ + function zipObject(keys, values) { + var index = -1, + length = keys ? keys.length : 0, + result = {}; + + while (++index < length) { + var key = keys[index]; + if (values) { + result[key] = values[index]; + } else { + result[key[0]] = key[1]; + } + } + return result; + } + + /*--------------------------------------------------------------------------*/ + + /** + * If `n` is greater than `0`, a function is created that is restricted to + * executing `func`, with the `this` binding and arguments of the created + * function, only after it is called `n` times. If `n` is less than `1`, + * `func` is executed immediately, without a `this` binding or additional + * arguments, and its result is returned. + * + * @static + * @memberOf _ + * @category Functions + * @param {Number} n The number of times the function must be called before + * it is executed. + * @param {Function} func The function to restrict. + * @returns {Function} Returns the new restricted function. + * @example + * + * var renderNotes = _.after(notes.length, render); + * _.forEach(notes, function(note) { + * note.asyncSave({ 'success': renderNotes }); + * }); + * // `renderNotes` is run once, after all notes have saved + */ + function after(n, func) { + if (n < 1) { + return func(); + } + return function() { + if (--n < 1) { + return func.apply(this, arguments); + } + }; + } + + /** + * Creates a function that, when called, invokes `func` with the `this` + * binding of `thisArg` and prepends any additional `bind` arguments to those + * passed to the bound function. + * + * @static + * @memberOf _ + * @category Functions + * @param {Function} func The function to bind. + * @param {Mixed} [thisArg] The `this` binding of `func`. + * @param {Mixed} [arg1, arg2, ...] Arguments to be partially applied. + * @returns {Function} Returns the new bound function. + * @example + * + * var func = function(greeting) { + * return greeting + ' ' + this.name; + * }; + * + * func = _.bind(func, { 'name': 'moe' }, 'hi'); + * func(); + * // => 'hi moe' + */ + function bind(func, thisArg) { + // use `Function#bind` if it exists and is fast + // (in V8 `Function#bind` is slower except when partially applied) + return support.fastBind || (nativeBind && arguments.length > 2) + ? nativeBind.call.apply(nativeBind, arguments) + : createBound(func, thisArg, nativeSlice.call(arguments, 2)); + } + + /** + * Binds methods on `object` to `object`, overwriting the existing method. + * Method names may be specified as individual arguments or as arrays of method + * names. If no method names are provided, all the function properties of `object` + * will be bound. + * + * @static + * @memberOf _ + * @category Functions + * @param {Object} object The object to bind and assign the bound methods to. + * @param {String} [methodName1, methodName2, ...] Method names on the object to bind. + * @returns {Object} Returns `object`. + * @example + * + * var view = { + * 'label': 'docs', + * 'onClick': function() { alert('clicked ' + this.label); } + * }; + * + * _.bindAll(view); + * jQuery('#docs').on('click', view.onClick); + * // => alerts 'clicked docs', when the button is clicked + */ + function bindAll(object) { + var funcs = arguments.length > 1 ? concat.apply(arrayRef, nativeSlice.call(arguments, 1)) : functions(object), + index = -1, + length = funcs.length; + + while (++index < length) { + var key = funcs[index]; + object[key] = bind(object[key], object); + } + return object; + } + + /** + * Creates a function that, when called, invokes the method at `object[key]` + * and prepends any additional `bindKey` arguments to those passed to the bound + * function. This method differs from `_.bind` by allowing bound functions to + * reference methods that will be redefined or don't yet exist. + * See http://michaux.ca/articles/lazy-function-definition-pattern. + * + * @static + * @memberOf _ + * @category Functions + * @param {Object} object The object the method belongs to. + * @param {String} key The key of the method. + * @param {Mixed} [arg1, arg2, ...] Arguments to be partially applied. + * @returns {Function} Returns the new bound function. + * @example + * + * var object = { + * 'name': 'moe', + * 'greet': function(greeting) { + * return greeting + ' ' + this.name; + * } + * }; + * + * var func = _.bindKey(object, 'greet', 'hi'); + * func(); + * // => 'hi moe' + * + * object.greet = function(greeting) { + * return greeting + ', ' + this.name + '!'; + * }; + * + * func(); + * // => 'hi, moe!' + */ + function bindKey(object, key) { + return createBound(object, key, nativeSlice.call(arguments, 2), indicatorObject); + } + + /** + * Creates a function that is the composition of the passed functions, + * where each function consumes the return value of the function that follows. + * For example, composing the functions `f()`, `g()`, and `h()` produces `f(g(h()))`. + * Each function is executed with the `this` binding of the composed function. + * + * @static + * @memberOf _ + * @category Functions + * @param {Function} [func1, func2, ...] Functions to compose. + * @returns {Function} Returns the new composed function. + * @example + * + * var greet = function(name) { return 'hi ' + name; }; + * var exclaim = function(statement) { return statement + '!'; }; + * var welcome = _.compose(exclaim, greet); + * welcome('moe'); + * // => 'hi moe!' + */ + function compose() { + var funcs = arguments; + return function() { + var args = arguments, + length = funcs.length; + + while (length--) { + args = [funcs[length].apply(this, args)]; + } + return args[0]; + }; + } + + /** + * Produces a callback bound to an optional `thisArg`. If `func` is a property + * name, the created callback will return the property value for a given element. + * If `func` is an object, the created callback will return `true` for elements + * that contain the equivalent object properties, otherwise it will return `false`. + * + * Note: All Lo-Dash methods, that accept a `callback` argument, use `_.createCallback`. + * + * @static + * @memberOf _ + * @category Functions + * @param {Mixed} [func=identity] The value to convert to a callback. + * @param {Mixed} [thisArg] The `this` binding of the created callback. + * @param {Number} [argCount=3] The number of arguments the callback accepts. + * @returns {Function} Returns a callback function. + * @example + * + * var stooges = [ + * { 'name': 'moe', 'age': 40 }, + * { 'name': 'larry', 'age': 50 } + * ]; + * + * // wrap to create custom callback shorthands + * _.createCallback = _.wrap(_.createCallback, function(func, callback, thisArg) { + * var match = /^(.+?)__([gl]t)(.+)$/.exec(callback); + * return !match ? func(callback, thisArg) : function(object) { + * return match[2] == 'gt' ? object[match[1]] > match[3] : object[match[1]] < match[3]; + * }; + * }); + * + * _.filter(stooges, 'age__gt45'); + * // => [{ 'name': 'larry', 'age': 50 }] + * + * // create mixins with support for "_.pluck" and "_.where" callback shorthands + * _.mixin({ + * 'toLookup': function(collection, callback, thisArg) { + * callback = _.createCallback(callback, thisArg); + * return _.reduce(collection, function(result, value, index, collection) { + * return (result[callback(value, index, collection)] = value, result); + * }, {}); + * } + * }); + * + * _.toLookup(stooges, 'name'); + * // => { 'moe': { 'name': 'moe', 'age': 40 }, 'larry': { 'name': 'larry', 'age': 50 } } + */ + function createCallback(func, thisArg, argCount) { + if (func == null) { + return identity; + } + var type = typeof func; + if (type != 'function') { + if (type != 'object') { + return function(object) { + return object[func]; + }; + } + var props = keys(func); + return function(object) { + var length = props.length, + result = false; + while (length--) { + if (!(result = isEqual(object[props[length]], func[props[length]], indicatorObject))) { + break; + } + } + return result; + }; + } + if (typeof thisArg == 'undefined' || (reThis && !reThis.test(fnToString.call(func)))) { + return func; + } + if (argCount === 1) { + return function(value) { + return func.call(thisArg, value); + }; + } + if (argCount === 2) { + return function(a, b) { + return func.call(thisArg, a, b); + }; + } + if (argCount === 4) { + return function(accumulator, value, index, collection) { + return func.call(thisArg, accumulator, value, index, collection); + }; + } + return function(value, index, collection) { + return func.call(thisArg, value, index, collection); + }; + } + + /** + * Creates a function that will delay the execution of `func` until after + * `wait` milliseconds have elapsed since the last time it was invoked. Pass + * an `options` object to indicate that `func` should be invoked on the leading + * and/or trailing edge of the `wait` timeout. Subsequent calls to the debounced + * function will return the result of the last `func` call. + * + * Note: If `leading` and `trailing` options are `true`, `func` will be called + * on the trailing edge of the timeout only if the the debounced function is + * invoked more than once during the `wait` timeout. + * + * @static + * @memberOf _ + * @category Functions + * @param {Function} func The function to debounce. + * @param {Number} wait The number of milliseconds to delay. + * @param {Object} options The options object. + * [leading=false] A boolean to specify execution on the leading edge of the timeout. + * [maxWait] The maximum time `func` is allowed to be delayed before it's called. + * [trailing=true] A boolean to specify execution on the trailing edge of the timeout. + * @returns {Function} Returns the new debounced function. + * @example + * + * var lazyLayout = _.debounce(calculateLayout, 300); + * jQuery(window).on('resize', lazyLayout); + * + * jQuery('#postbox').on('click', _.debounce(sendMail, 200, { + * 'leading': true, + * 'trailing': false + * }); + */ + function debounce(func, wait, options) { + var args, + result, + thisArg, + callCount = 0, + lastCalled = 0, + maxWait = false, + maxTimeoutId = null, + timeoutId = null, + trailing = true; + + function clear() { + clearTimeout(maxTimeoutId); + clearTimeout(timeoutId); + callCount = 0; + maxTimeoutId = timeoutId = null; + } + + function delayed() { + var isCalled = trailing && (!leading || callCount > 1); + clear(); + if (isCalled) { + if (maxWait !== false) { + lastCalled = new Date; + } + result = func.apply(thisArg, args); + } + } + + function maxDelayed() { + clear(); + if (trailing || (maxWait !== wait)) { + lastCalled = new Date; + result = func.apply(thisArg, args); + } + } + + wait = nativeMax(0, wait || 0); + if (options === true) { + var leading = true; + trailing = false; + } else if (isObject(options)) { + leading = options.leading; + maxWait = 'maxWait' in options && nativeMax(wait, options.maxWait || 0); + trailing = 'trailing' in options ? options.trailing : trailing; + } + return function() { + args = arguments; + thisArg = this; + callCount++; + + // avoid issues with Titanium and `undefined` timeout ids + // https://github.com/appcelerator/titanium_mobile/blob/3_1_0_GA/android/titanium/src/java/ti/modules/titanium/TitaniumModule.java#L185-L192 + clearTimeout(timeoutId); + + if (maxWait === false) { + if (leading && callCount < 2) { + result = func.apply(thisArg, args); + } + } else { + var now = new Date; + if (!maxTimeoutId && !leading) { + lastCalled = now; + } + var remaining = maxWait - (now - lastCalled); + if (remaining <= 0) { + clearTimeout(maxTimeoutId); + maxTimeoutId = null; + lastCalled = now; + result = func.apply(thisArg, args); + } + else if (!maxTimeoutId) { + maxTimeoutId = setTimeout(maxDelayed, remaining); + } + } + if (wait !== maxWait) { + timeoutId = setTimeout(delayed, wait); + } + return result; + }; + } + + /** + * Defers executing the `func` function until the current call stack has cleared. + * Additional arguments will be passed to `func` when it is invoked. + * + * @static + * @memberOf _ + * @category Functions + * @param {Function} func The function to defer. + * @param {Mixed} [arg1, arg2, ...] Arguments to invoke the function with. + * @returns {Number} Returns the timer id. + * @example + * + * _.defer(function() { alert('deferred'); }); + * // returns from the function before `alert` is called + */ + function defer(func) { + var args = nativeSlice.call(arguments, 1); + return setTimeout(function() { func.apply(undefined, args); }, 1); + } + // use `setImmediate` if it's available in Node.js + if (isV8 && freeModule && typeof setImmediate == 'function') { + defer = bind(setImmediate, context); + } + + /** + * Executes the `func` function after `wait` milliseconds. Additional arguments + * will be passed to `func` when it is invoked. + * + * @static + * @memberOf _ + * @category Functions + * @param {Function} func The function to delay. + * @param {Number} wait The number of milliseconds to delay execution. + * @param {Mixed} [arg1, arg2, ...] Arguments to invoke the function with. + * @returns {Number} Returns the timer id. + * @example + * + * var log = _.bind(console.log, console); + * _.delay(log, 1000, 'logged later'); + * // => 'logged later' (Appears after one second.) + */ + function delay(func, wait) { + var args = nativeSlice.call(arguments, 2); + return setTimeout(function() { func.apply(undefined, args); }, wait); + } + + /** + * Creates a function that memoizes the result of `func`. If `resolver` is + * passed, it will be used to determine the cache key for storing the result + * based on the arguments passed to the memoized function. By default, the first + * argument passed to the memoized function is used as the cache key. The `func` + * is executed with the `this` binding of the memoized function. The result + * cache is exposed as the `cache` property on the memoized function. + * + * @static + * @memberOf _ + * @category Functions + * @param {Function} func The function to have its output memoized. + * @param {Function} [resolver] A function used to resolve the cache key. + * @returns {Function} Returns the new memoizing function. + * @example + * + * var fibonacci = _.memoize(function(n) { + * return n < 2 ? n : fibonacci(n - 1) + fibonacci(n - 2); + * }); + */ + function memoize(func, resolver) { + function memoized() { + var cache = memoized.cache, + key = keyPrefix + (resolver ? resolver.apply(this, arguments) : arguments[0]); + + return hasOwnProperty.call(cache, key) + ? cache[key] + : (cache[key] = func.apply(this, arguments)); + } + memoized.cache = {}; + return memoized; + } + + /** + * Creates a function that is restricted to execute `func` once. Repeat calls to + * the function will return the value of the first call. The `func` is executed + * with the `this` binding of the created function. + * + * @static + * @memberOf _ + * @category Functions + * @param {Function} func The function to restrict. + * @returns {Function} Returns the new restricted function. + * @example + * + * var initialize = _.once(createApplication); + * initialize(); + * initialize(); + * // `initialize` executes `createApplication` once + */ + function once(func) { + var ran, + result; + + return function() { + if (ran) { + return result; + } + ran = true; + result = func.apply(this, arguments); + + // clear the `func` variable so the function may be garbage collected + func = null; + return result; + }; + } + + /** + * Creates a function that, when called, invokes `func` with any additional + * `partial` arguments prepended to those passed to the new function. This + * method is similar to `_.bind`, except it does **not** alter the `this` binding. + * + * @static + * @memberOf _ + * @category Functions + * @param {Function} func The function to partially apply arguments to. + * @param {Mixed} [arg1, arg2, ...] Arguments to be partially applied. + * @returns {Function} Returns the new partially applied function. + * @example + * + * var greet = function(greeting, name) { return greeting + ' ' + name; }; + * var hi = _.partial(greet, 'hi'); + * hi('moe'); + * // => 'hi moe' + */ + function partial(func) { + return createBound(func, nativeSlice.call(arguments, 1)); + } + + /** + * This method is similar to `_.partial`, except that `partial` arguments are + * appended to those passed to the new function. + * + * @static + * @memberOf _ + * @category Functions + * @param {Function} func The function to partially apply arguments to. + * @param {Mixed} [arg1, arg2, ...] Arguments to be partially applied. + * @returns {Function} Returns the new partially applied function. + * @example + * + * var defaultsDeep = _.partialRight(_.merge, _.defaults); + * + * var options = { + * 'variable': 'data', + * 'imports': { 'jq': $ } + * }; + * + * defaultsDeep(options, _.templateSettings); + * + * options.variable + * // => 'data' + * + * options.imports + * // => { '_': _, 'jq': $ } + */ + function partialRight(func) { + return createBound(func, nativeSlice.call(arguments, 1), null, indicatorObject); + } + + /** + * Creates a function that, when executed, will only call the `func` function + * at most once per every `wait` milliseconds. Pass an `options` object to + * indicate that `func` should be invoked on the leading and/or trailing edge + * of the `wait` timeout. Subsequent calls to the throttled function will + * return the result of the last `func` call. + * + * Note: If `leading` and `trailing` options are `true`, `func` will be called + * on the trailing edge of the timeout only if the the throttled function is + * invoked more than once during the `wait` timeout. + * + * @static + * @memberOf _ + * @category Functions + * @param {Function} func The function to throttle. + * @param {Number} wait The number of milliseconds to throttle executions to. + * @param {Object} options The options object. + * [leading=true] A boolean to specify execution on the leading edge of the timeout. + * [trailing=true] A boolean to specify execution on the trailing edge of the timeout. + * @returns {Function} Returns the new throttled function. + * @example + * + * var throttled = _.throttle(updatePosition, 100); + * jQuery(window).on('scroll', throttled); + * + * jQuery('.interactive').on('click', _.throttle(renewToken, 300000, { + * 'trailing': false + * })); + */ + function throttle(func, wait, options) { + var leading = true, + trailing = true; + + if (options === false) { + leading = false; + } else if (isObject(options)) { + leading = 'leading' in options ? options.leading : leading; + trailing = 'trailing' in options ? options.trailing : trailing; + } + options = getObject(); + options.leading = leading; + options.maxWait = wait; + options.trailing = trailing; + + var result = debounce(func, wait, options); + releaseObject(options); + return result; + } + + /** + * Creates a function that passes `value` to the `wrapper` function as its + * first argument. Additional arguments passed to the function are appended + * to those passed to the `wrapper` function. The `wrapper` is executed with + * the `this` binding of the created function. + * + * @static + * @memberOf _ + * @category Functions + * @param {Mixed} value The value to wrap. + * @param {Function} wrapper The wrapper function. + * @returns {Function} Returns the new function. + * @example + * + * var hello = function(name) { return 'hello ' + name; }; + * hello = _.wrap(hello, function(func) { + * return 'before, ' + func('moe') + ', after'; + * }); + * hello(); + * // => 'before, hello moe, after' + */ + function wrap(value, wrapper) { + return function() { + var args = [value]; + push.apply(args, arguments); + return wrapper.apply(this, args); + }; + } + + /*--------------------------------------------------------------------------*/ + + /** + * Converts the characters `&`, `<`, `>`, `"`, and `'` in `string` to their + * corresponding HTML entities. + * + * @static + * @memberOf _ + * @category Utilities + * @param {String} string The string to escape. + * @returns {String} Returns the escaped string. + * @example + * + * _.escape('Moe, Larry & Curly'); + * // => 'Moe, Larry & Curly' + */ + function escape(string) { + return string == null ? '' : String(string).replace(reUnescapedHtml, escapeHtmlChar); + } + + /** + * This method returns the first argument passed to it. + * + * @static + * @memberOf _ + * @category Utilities + * @param {Mixed} value Any value. + * @returns {Mixed} Returns `value`. + * @example + * + * var moe = { 'name': 'moe' }; + * moe === _.identity(moe); + * // => true + */ + function identity(value) { + return value; + } + + /** + * Adds functions properties of `object` to the `lodash` function and chainable + * wrapper. + * + * @static + * @memberOf _ + * @category Utilities + * @param {Object} object The object of function properties to add to `lodash`. + * @example + * + * _.mixin({ + * 'capitalize': function(string) { + * return string.charAt(0).toUpperCase() + string.slice(1).toLowerCase(); + * } + * }); + * + * _.capitalize('moe'); + * // => 'Moe' + * + * _('moe').capitalize(); + * // => 'Moe' + */ + function mixin(object) { + forEach(functions(object), function(methodName) { + var func = lodash[methodName] = object[methodName]; + + lodash.prototype[methodName] = function() { + var value = this.__wrapped__, + args = [value]; + + push.apply(args, arguments); + var result = func.apply(lodash, args); + return (value && typeof value == 'object' && value === result) + ? this + : new lodashWrapper(result); + }; + }); + } + + /** + * Reverts the '_' variable to its previous value and returns a reference to + * the `lodash` function. + * + * @static + * @memberOf _ + * @category Utilities + * @returns {Function} Returns the `lodash` function. + * @example + * + * var lodash = _.noConflict(); + */ + function noConflict() { + context._ = oldDash; + return this; + } + + /** + * Converts the given `value` into an integer of the specified `radix`. + * If `radix` is `undefined` or `0`, a `radix` of `10` is used unless the + * `value` is a hexadecimal, in which case a `radix` of `16` is used. + * + * Note: This method avoids differences in native ES3 and ES5 `parseInt` + * implementations. See http://es5.github.com/#E. + * + * @static + * @memberOf _ + * @category Utilities + * @param {String} value The value to parse. + * @param {Number} [radix] The radix used to interpret the value to parse. + * @returns {Number} Returns the new integer value. + * @example + * + * _.parseInt('08'); + * // => 8 + */ + var parseInt = nativeParseInt(whitespace + '08') == 8 ? nativeParseInt : function(value, radix) { + // Firefox and Opera still follow the ES3 specified implementation of `parseInt` + return nativeParseInt(isString(value) ? value.replace(reLeadingSpacesAndZeros, '') : value, radix || 0); + }; + + /** + * Produces a random number between `min` and `max` (inclusive). If only one + * argument is passed, a number between `0` and the given number will be returned. + * + * @static + * @memberOf _ + * @category Utilities + * @param {Number} [min=0] The minimum possible value. + * @param {Number} [max=1] The maximum possible value. + * @returns {Number} Returns a random number. + * @example + * + * _.random(0, 5); + * // => a number between 0 and 5 + * + * _.random(5); + * // => also a number between 0 and 5 + */ + function random(min, max) { + if (min == null && max == null) { + max = 1; + } + min = +min || 0; + if (max == null) { + max = min; + min = 0; + } else { + max = +max || 0; + } + var rand = nativeRandom(); + return (min % 1 || max % 1) + ? min + nativeMin(rand * (max - min + parseFloat('1e-' + ((rand +'').length - 1))), max) + : min + floor(rand * (max - min + 1)); + } + + /** + * Resolves the value of `property` on `object`. If `property` is a function, + * it will be invoked with the `this` binding of `object` and its result returned, + * else the property value is returned. If `object` is falsey, then `undefined` + * is returned. + * + * @static + * @memberOf _ + * @category Utilities + * @param {Object} object The object to inspect. + * @param {String} property The property to get the value of. + * @returns {Mixed} Returns the resolved value. + * @example + * + * var object = { + * 'cheese': 'crumpets', + * 'stuff': function() { + * return 'nonsense'; + * } + * }; + * + * _.result(object, 'cheese'); + * // => 'crumpets' + * + * _.result(object, 'stuff'); + * // => 'nonsense' + */ + function result(object, property) { + var value = object ? object[property] : undefined; + return isFunction(value) ? object[property]() : value; + } + + /** + * A micro-templating method that handles arbitrary delimiters, preserves + * whitespace, and correctly escapes quotes within interpolated code. + * + * Note: In the development build, `_.template` utilizes sourceURLs for easier + * debugging. See http://www.html5rocks.com/en/tutorials/developertools/sourcemaps/#toc-sourceurl + * + * For more information on precompiling templates see: + * http://lodash.com/#custom-builds + * + * For more information on Chrome extension sandboxes see: + * http://developer.chrome.com/stable/extensions/sandboxingEval.html + * + * @static + * @memberOf _ + * @category Utilities + * @param {String} text The template text. + * @param {Object} data The data object used to populate the text. + * @param {Object} options The options object. + * escape - The "escape" delimiter regexp. + * evaluate - The "evaluate" delimiter regexp. + * interpolate - The "interpolate" delimiter regexp. + * sourceURL - The sourceURL of the template's compiled source. + * variable - The data object variable name. + * @returns {Function|String} Returns a compiled function when no `data` object + * is given, else it returns the interpolated text. + * @example + * + * // using a compiled template + * var compiled = _.template('hello <%= name %>'); + * compiled({ 'name': 'moe' }); + * // => 'hello moe' + * + * var list = '<% _.forEach(people, function(name) { %>
  • <%= name %>
  • <% }); %>'; + * _.template(list, { 'people': ['moe', 'larry'] }); + * // => '
  • moe
  • larry
  • ' + * + * // using the "escape" delimiter to escape HTML in data property values + * _.template('<%- value %>', { 'value': '