From cb6cee5e14451dd9b852723a30a5fbdee404d959 Mon Sep 17 00:00:00 2001 From: richardgraham Date: Sun, 5 Aug 2012 23:59:54 +0000 Subject: [PATCH] Big update to custom expression feature. - supports range and index subexpressions and many new operators - switched to my patched version of exp4j to support all this. - expressions belong to rocket document. Accessed from analysis menu. - expression importing from file - datatypes section defined in file for storing datatypes other than internal ones - flightdatatype fix to forget outdated types - many GUI fixes to custom expressions - new unitgroups supported. Auto unit detection for SI units in custom expressions. Had to carefully merge loading/saving code with Kevins recent de-localization update. Hopefully changes to materials saving kept but switched datatype access to just using symbol as the key. Hopefully can get the changes to exp4j upstream so we don't need to keep using this patched version. git-svn-id: https://openrocket.svn.sourceforge.net/svnroot/openrocket/trunk@953 180e2498-e6e9-4542-8430-84ac67f01cd8 --- core/fileformat.txt | 6 +- core/lib/exp4j-rdg.jar | Bin 0 -> 57182 bytes core/resources/l10n/messages.properties | 31 +- .../document/OpenRocketDocument.java | 21 +- .../sf/openrocket/document/Simulation.java | 17 - .../file/openrocket/OpenRocketSaver.java | 54 +- .../openrocket/importt/OpenRocketLoader.java | 434 +++++++-------- .../file/openrocket/savers/RocketSaver.java | 3 - .../CustomExpressionDialog.java | 35 ++ .../CustomExpressionPanel.java | 111 +++- .../ExpressionBuilderDialog.java | 27 +- .../customexpression/OperatorSelector.java | 85 ++- .../customexpression/OperatorTableModel.java | 8 +- .../customexpression/VariableSelector.java | 111 ++-- .../customexpression/VariableTableModel.java | 18 +- .../sf/openrocket/gui/main/BasicFrame.java | 15 +- .../gui/main/SimulationEditDialog.java | 11 +- .../sf/openrocket/rocketcomponent/Rocket.java | 5 +- .../BasicEventSimulationEngine.java | 5 +- .../simulation/CustomExpression.java | 341 ------------ .../openrocket/simulation/FlightDataType.java | 209 +++++--- .../customexpression/CustomExpression.java | 496 ++++++++++++++++++ .../customexpression/Functions.java | 269 ++++++++++ .../customexpression/IndexExpression.java | 54 ++ .../customexpression/RangeExpression.java | 116 ++++ .../sf/openrocket/unit/FixedUnitGroup.java | 4 + .../src/net/sf/openrocket/unit/UnitGroup.java | 134 ++++- .../net/sf/openrocket/util/ArrayUtils.java | 127 +++++ .../sf/openrocket/util/ExpressionParser.java | 2 +- .../openrocket/util/LinearInterpolator.java | 120 +++-- core/src/net/sf/openrocket/util/MathUtil.java | 2 +- core/src/net/sf/openrocket/util/TextUtil.java | 13 + .../customexpression/TestExpressions.java | 23 + .../net/sf/openrocket/util/MathUtilTest.java | 17 + 34 files changed, 2074 insertions(+), 850 deletions(-) create mode 100644 core/lib/exp4j-rdg.jar create mode 100644 core/src/net/sf/openrocket/gui/customexpression/CustomExpressionDialog.java delete mode 100644 core/src/net/sf/openrocket/simulation/CustomExpression.java create mode 100644 core/src/net/sf/openrocket/simulation/customexpression/CustomExpression.java create mode 100644 core/src/net/sf/openrocket/simulation/customexpression/Functions.java create mode 100644 core/src/net/sf/openrocket/simulation/customexpression/IndexExpression.java create mode 100644 core/src/net/sf/openrocket/simulation/customexpression/RangeExpression.java create mode 100644 core/test/net/sf/openrocket/simulation/customexpression/TestExpressions.java diff --git a/core/fileformat.txt b/core/fileformat.txt index 3e58beee..7f8fb932 100644 --- a/core/fileformat.txt +++ b/core/fileformat.txt @@ -40,4 +40,8 @@ The following file format versions exist: elements to stage components (except sustainer). 1.5: Introduced with OpenRocket 12.xx. Added ComponentPresets. - Added lowerstageseparation as recovery device deployment event. \ No newline at end of file + Added lowerstageseparation as recovery device deployment event. + +1.6 (pre): + Added section for supporting datatypes other than + internal ones. Currently only supports datatypes from custom expressions. \ No newline at end of file diff --git a/core/lib/exp4j-rdg.jar b/core/lib/exp4j-rdg.jar new file mode 100644 index 0000000000000000000000000000000000000000..0de4e9d985fbc81e66c11325e800cd11b13fa7da GIT binary patch literal 57182 zcmbTdQ;?-=x-OcwZQFKcrERm)wr$(CZQHhOo0XZB+%>h(fB+!L-scDSr-K3j0w5!zEI=zED@rdTAS)p%qNGeGBl1#Sb=&skX^g1}D`f-u9-lk60@u^*fg(c*mz(I`#YxvH9m|o&DyBuw$;kwo3Fwp!Ch-7@3 z@RK?CavjC&x9#cqqL-;`X-omSH%=}lYB}Nep!fcgwK86i}#42*0p8L^?Fi3(VVU#}DPF0(5 zsj-ahgkKE#&+?#n zj&Ilb59B~u%@k-D!qAKeAX6jejLr%2xOlm>dsUrDRIq+0s8cg$8}?^xrbl1n5*+$= z!EJQ}*j&Fuxb9NY$wn3HpQ%ChCitc#^xcsKCG} zJ17&|yc9vt5)#ZUp0fyX#2n)QP>~)i^MCM)WpHW;Y`_x>s)o;y2EdoTxgdP*3C%8G z-0K`12~G>B8o-CJWxYUh(&c5(6fOJ93Mx&^%RGwY64RY)gwVk*dXS>N2IZi6iF}K} z&=duPR$j*L=37%efqv3&B;ywC^r2pj@@)({0>j_E-hF5S7B`-qpBHc=0E3s=HC z@NYrc=ntU#0JhqP9u+ZcG`&$ECZ-iA++1q_JL%#rlCTBR)XT*u7aC2;8*tnp8cxM3 zJnWELrd+{+=-Nkp@+Tb%?12veqLEf`{_q4v`DB_tM9g_z*5d>8?gjh{mmZ4cDH6bc za0&LGaEbq)aQR=Tj8@fA!Zt?n`I&GPQm2U}NLjp)q$u+WrK>7cC#+~T&tfek72Pyw zHGq^tmf6;sAAa~E_W25WQT)xH4H;N=7^TMLQT$^7+7{;)ceXQg4h-$6d+gnL$1%ro z#&PB()wlN(un&X}b$(#3yF$1@gtl~2gS0?dG7&qzJwlt}7!}bbm@?tmXa0f}m?V#JQ9Da#GSXnskB7X5g{+cCLHk!iWWB)tFsmr z-|W$tcJkN-KMaCFIO|BN!NxAiF*%_IL-er_8K&?ZpVlnX;ipb?Dr}>=9fNbiepX^G zEO4VSYj4Hb&nyG8Y7p>%t_6e0xQW(9s{>1%InEtrM&9GTy#^NG)kH!JNh(Ip7YsdFPbPCNA}%M;h~1 zPF3=BC}sx|Z-H;nnPn7=J~rr6fXxJ4OhrgQSD#4<^%7kZu`p-w0&Cs*80);dfm{nU z&DSdHO1TA`sPcpxk}TTw}LseTSGrms_UXY!-2I z@+c2xa}Tz(xa@N?UPI||CcA*!(I-EH)Fntl1CEt9VeqphKZE5JwZv8G7k)+1><~w; z01{?8B(B@ICT1D@qWBtQIM1Q)AvwR3oM-^5t(h)m_!gABb304SG*q&=-hu5D>Cjj1 z({{;Bbxu;bf!`+lVlCO{?UKvdG~s>qv)L~CP46X64SznhPgo6-@G&O%&d8U)F2CgNy=IA471l$7Y9DVH&WzE2jJl7!(& zqcNKomNS=EPO;)3vUk2uP5WK1eX;$IN#cIPFi;h!5h<=&kib>h(Q+6`rRdnyBso1< zcR`!xqw%LNz_KhV^e5>OXwV;trzAQgtL{jaIb?^%Q%sF_a|sINW+ZEF=@KPNCBH2n z1VMAPk0nbVv+Xj&7ccwaOISdT-v)lRjm)B=+S9>s4Yk4S0U@>c{Zhzvq$86~xIyCM z2P{Q!(eds7brmCNb6|6pMakmIdxmBfDGgvR24REfJ?wNX1>^E)UszU;E?Nu6Zs zD85*&jkGdeC8GMCMy%D?R|t1Xmf5-1AKxt>{i)j@{Y^;wb$@~v&M3L?X)~b{fjWF6 zqFGl>au;|2$XSmTAh3{%`}LHHo1>t?8ZUkX!MQcIXb(55ddsyRWs@P_pR-Zj%{M5y z)5Ji?1Uz{LF6QD2PzJxN`26}u+dkT__c44tGToxZJh>(M^6`aX(K+$>0q5^3)WKd0 zk-F`OR=wxegQqIZCNc;<^MlJu<+f)24s8sbG z%=Hcan0t8#b8CGE_y21ql5FL$1Q14YfB6CPBpXOYuKA)-=Ou4YnFBG(7gL^7j#QEq z)uuQ~#rEmLF&t|(zX~pvl+?cGXUzPpYX&1u-MZ%SWNz$TU+>iI?ac;u3y+F`3<(yB zXWMky-Y*f4oE5u_b_(W3t7R4OnyuH|IHN z`Z@n@5F12De2_K>l7ZbRoaD*(PIR^Y%A+Nz3S{i11&rRyr%Z8cIZtvw)j&27pU-^S z5`tiip-`)3?Yh}wz@6S5W58qcXd0T&l?eir@Zj91fh)*vDw9`WM3IP7)m&icUU`^E z#U8SK_dFscgp>pWXv^Mh*9!j^reZ2I^(xlDm5CH_iKIiMa%%BM{Qh#-r+AE5sh)@s z(kuoW9r?P&gDg2W)#Z0-h$cGA2Ja|}Els<>61r5C~zcBD+~P`gCc zIms4U9CQgMsKZ9dW9@LcZdL3@G%!*UkdYEE+P?QpA#l3JsPOvun&b19$7 zp~y1D&}yTnX3Ydas*JZTYc6w;l5HzoDQz_;*tq-)E12&^bM16RIW{O4cPjm|xThBF zig++7hF67o#jLfzWhhCG`}SNVG>rdPCSAcMNS*eCC10DpfB^Uh%#FBHCoT*xl#$ye z;*bbtHw^Ljw3+}u&k%LCruNVF-W`!@K6=iAJ`mFMmx0k75fWH$&B^BlfFF<1H!_ormXI2u`ecO^W^FdB9 zJ#e2~tpeKuT-yaU4@O1|LM$}qDl7}Qc;m~IyuFIzY?eHuoW9hCxyPyOJL;4XsEokF z(dAUsB81(&yO$G!$pfuCwKQm!pA3F7d$$E|GNH{p6&2_2!J$#6m2O?X8OpLOZ?rnX z?f+(oG~Qv#{we?PLD-Fi^dT5uV1iQ7Nmyeq*Y8b2Id0Kjg_*P`QOf2eWVD!*cn>%j zyjm(zSb^%s0be&!zv_mUFmFvKPMy}zO3zj%I6r4~20AMDcuYtxP|b%R(Sd;qa1-j^ao@8_imol^Qi_ zdZCB92@c!ILzX?7w8v;Jj_gt{`U2)Did(0l;wY!O{E*^2I9K(`CdJBJy1o2c*oh_M zoa$vdj|5hTIW^LvxM03$%q3DkEo53oh`fLHG$H51YhhSch|(oPc%!>W0<`28&*vt& z4*GRry^z!J2>cxGJE%YK)2Ii7onL;M6N%{-FN8IY-jS)I6QQ(auiqJ28=eC`KYFwx zz4u^Sm;x81rYvKK>yQou)ffXHZhb!Wkx+rRI=@(k9wOXw9ug^4b={|tOSm35gjbwR zDPfSEMqh|9Tu5f!ARCQuMjlNYh`&T8e|S^-R~`XFzXJJ|QdTIe&oZD_TAvui2h=c5 z$(UI9$QtEcKiR7uvR7OmDj;95C7YYJ5wKb0XT(ja3_>h|Xt2<*VjV7%hlq|V!;LL# zjuT6d|9MG$r8&=&QsjM7;k``Ptsl=qgxBD_TL~g_r-)*@k=WL&`WxVG&wy13`5yZe zzQK5I+2IW}&sf-_P|<}tdHA=G=Ea({y}ikbBhU6pPKu9k+bnWiNy64>+cAOSXv#OS z);o4MXc7PB3%PGh=ji2__-`&gne2t<%9N8yG&IhB!NY=lA(V@?LlUfsp75#$c#a%6Z4MKvTC$Dpw@fb6xmj+{e0C^pbR`B<( zYYeUd#o`Dw$?3b#)~S+Y%7+Q2V1&J5m~;b(p%?X!53Jz*JE9S zQmaU)rY#_3%DRTBb4U^sl^GHm>UguC5Ez0A1x6VpVUbSQgv5G!*g1R4V8sTajuF%Oi=7M*f%^z}su0CTmZAkiVvckQlF|~((^FW}40b#e6|7s_LpBaChmTfV ze)k59Uz~pL>#vhfm)llMI!nX>;ywCU`s1LJF9hW5OmJ}AJ(lJ`h*r_$vO0CoxAR3@ zKzWEZ`U^=1oSD7I&D(ObY{a1!`~?$^l?LFJh2cEt-Vu~}T8j1V%mkLxD;rxjoAU;? zU*4&wFKZRnXIYqlUe`Rg9X80OpYW}L-on|pLcgATg%>@Xc9vWqXBfn(a)yDa&vhN7 za%*yDo0hMR;;Bu^gm}sAmeS(Ns3|qsdONq(EnH*4A#`6L&glaD5^bV@>v~5@>yi-j zN8hdB`gWLdv*gg9`vcB8k=)}r#Ny9Uy#lb!W`IMfsIC=*rOp7==Ej7M0C=e~ReqDP zm{1hDCVjRcv!QsuLUiV|7wAdgr;Zgrh#tdP(t@aNef(4e_6J8(5B<`wsz5hJtpS(m zrxN?D%=B5jMQRZ4?jU5uNu%C!hy6;MNoBkgZE8hxPj%wi%xQ>P|89nEcRMBEIG?VSER{J&0k+XhJh;4SaI2%JHXSwPwuYF z&&1LsSvraTP97KnUIW8(=&mU}x`WUHITS>{7&Nu+bWaSEv2rvC(I!Cp>Q_gnuE6eM((UL*v>OmN)OK#CmJFaXbL^%V92#~z&A<_T$IqI z_>21?x2jlbi&D1EU^`pS@R9oFQIGe;CLmi%xrYQtsVcQl^YxF2~bK}t&y z7tZqRjPlvEA~^kmaF&%%X*+XJ({S`xkh67$^KeE#vrmjREe=T<`hQiLHZ6P)5PwFk z@J}T9A8$7fEcfHT8S&2p%m0s2`->^7l@%;E`C)uwe1q?#7HJw+8%k3I8rTd?Nd37q z&JT}iHe0z&Z*R2OlfGUgQk(2GC}B*(0%beoa`_};!~g-bm!?`(_ zx14oIc(%*fM6|Dgti6)8r%ukVh!vjIR67JM`}b~DF{xa-LFW+&DOZFWk#Lv5g!SXR zYD@;Q6=cs1U|ME$$YGeIORAVHwmZ~3coz#R-T{l!<`86+#1{~HWEV+rna5-rK6=z? z_f0&!d9&#gHJ~Cb+tjD%6O9|v5`V##JZinhPtlM16`>vXhmP2R3M~xS|=gT;D4&92}8vk_$l`o=D`qa zTn<2lBi6rtT=GC1AVzOMnDlJ(P)!M)dn%M)rE>*3B7Bn-IFkk+4aolNW7;}4UtL0v zL!GAhRgE~}kZ3|r5lZvACvV!4`of68BDxJNhGZv@a3UWGMI{M@7LBy*lc6?F>gAtE zZSX_y4Y)u=xy-xTiNzO-+bv5uBIQ^IN$#t}SB65`ir=H~r7op`B-g3PN{vcGv7%+- zW26cZ1Fw-!RB5wS{!INh*Gr^5e{uM6VBm@uFBz+gMUU1%oIZaH09-Z>Fr;<*DN{0YNn+RRk zRj?@H0-ROwckjfAZS&*(G;{klkKlc&*G zd7Xw!l}vMGoyVrwdXVx&#=kqdGn5Bs2%Bak#Jt~DwGjObvl^85=7^SCCR!$xX44@A zv?VyB6?_Kj^ zLDMy*+2vwxj!w<>{LGK;zRt?Aii6$wBsYo87-+pHzWmiLn9p>C$m7KmU_qJdWRs0F z6{vsC4?RefOk+xX5UDHvcTu|1@_d~X!B&;;NRN>z{U+*EB{&NVgmCwhqi7Go^{PQ7 z-UD%f0Bk4rno3Pj3^*d{91XYeOm2qBO8yYPpkSBS=-?W4D-^LK=V?U6Y*C0D!t1iC zHp<3*82_ZxSgbb(MdTZ!IYr4K0^FkOh7%P+ET&_^^rj%N9BZNw-j9OAL=H(b%=RpE zN|e#l4H!hc=IaUtNaxI zL7yz}Nv3^HLs?VknI=}DtU?p*L@rL&dSLd`iqo!$hQ&u-R5-j!dYfCw(@X4wesWAvA z6pN)p7}#oNVa^OLy~Bc#Dt=R77_1x<&s`gyw|F6e&_akjirJ30cu|Q_W6C9pq>t3) z-h#u&S0K+puMRUKsPLxj=}h}Vy&6U~^38jgQ?X+~h?@i_PgSJP$x2NlnFHmpjhIu= z=PM{O*v%QPe#}B6uxw0bbv-iyEgy{FJVcsZvVL>+&OCfVoTO$?fl;+(uQWy|!wTtZ zR8s#Q*6oRc1&(H8rDE98C%pRUCj(t3+m>CA<;2I(uiNfV+L4PGZqbJY3F1<)o{&`# z58e?(NRM>~6RcQhk}mb8iBPp&mR;Ng+uh}RvYt_tXk`nPYwF}IBe!XPy9hkEFn)&E)#cx568Z_(B3!?C6`F7@ zXLu}8X*@Wouy@Ra)i}$u!^i!9dAbMh#sP0pBkb^bl;3cnBXn7HUd}t({?3mRQ?sUt zmzn*7zKGq{v}oGUxDCG@T?KZjmYPEDxVK`=Wr@{l`0yu-1~;Z^3q@>Zs!?qkqtDm= zHCCIRWOLF1>htwDNw%suVk`KuHPD*G1qd=2c?Lya=xTttX+zX^oULus8;-j_CiB!= zTD3Jj3vdm*xqtYZ_Uym{{Fob2*>eJ|uw5-5MVU_naw{~}@z!U8?fy4FC|h@=@14v@ z2a*r|r%ot3f{i;kW)BG3-5jelt>Lcl?3V}*2JK`BS;z+8)~zLOPYl1V*7qPeaVI*% z)^M!Ytd2G6=cdtycz{$Beuh*Fm5l8L94T?;+YuI~-k< z&gj)15{!Q@LG_fDq9LT?k<6y^(EG;L_Euc&Y^xuSN(6M8FLb_f5Sj9jZV`J#@6scV z4rOt}q=?t??8AqOza_O);LX9WF<2E2$eTJUNeov4Nd)`>z$<~QS%ohBio{rDXZlbELjg5YgBHxwQ$3)G(1!gwx=AN}Z?Ujg*vY=M}lDaVbQk zd<}v?Y!nYqm#;BWy@(zuN#{ufVkvi`&@;id%Izm=nX!y_qnEGC8pmqnN}d|`m$cf4 zKhrOF)k9%qbg;e{R}%(|$K{e5a}P{yKI~T|LW|%jUmj1+ z=ou;^o$0tL=(z5r1a;Y7sY8BQl72>CAo}k?0s}%jkbvy z&$Hm+gz>1X@wm^)6rs=l^52-{T2!38g!6<=!mVa=p|UOv*k)OgER2CF)-? zRqH^}Ef3zH>ReuKPa*KdpRMsl|5rw`8&ky51qT3tMEqyf#;#lC|0C4@eg3{j|KF

4I{lT4I5=WIR7nI-^}H zJqtTn^eUZqHb^jL#;mUF9Qp9ak`)P6-ysZ0=aKLJ_`Gx2Q-Jl7%bEfncS8L@D!>ks8Iway@} zT_%kJ2QK&E8@D4-wt;wG#C{BQBkmistQR5(xWv3@1Br?Xf}S1m{2Xp&e1gT_CTDu) zV-&O*4HQLt<%>%U%aA&bS{BDMcnUpQkw&nPN_9svBg^%P^6=$=e(WyQkJKh@On~Ck@2PEQG3|^KXY6sj6u1OT7C`*7JGfGq0O8$LYz3deS-b$4#>qq zGt|Ff3-*R2mZVC8uc_ICPH`XP@?VFvB(J-Wf=*~mLNdw-w#N~BG$*xmShIH<@Bmgt z(Jq!bG}GY!!bIVsw?jPE(!XGiZLd#XYrP*%%6@@b9SEEG?NEj-S-6DmWy>sltYm>N zz@Kp`=pb>|GkxSXY`txVXE*D{0EAT8G|HC}CtUcZ@51>w0sk~#IV4JVoS@aC&g^YD zXrkiK++a!a!P*sWkm)&k_uDj$2NY^s0L3Jl$4>ChuI9MVc&x-kei5B zEILLqU)c{wsuK*C%foA#8HzLj;Dib%m_{!C6X`i^9NfmG^6U$9rIT|S*UdsWgEOUTFRJe4@d#!| z+&3bv%1k)ooRJ)+b_fuA6HR@mJ>l)W&O?tW`Is4Nr_XgNyj$d|uan&+zzx1BG*B5X~ zfBj>V!}qbLy!)pDRfYd&(){F_f*`F zQyBtKL3J9@LJ-0t)$`z-a=&6dU{yZ9^Dl_nasIUQnb6RjFVY8ir@@lUrz|=Tqs?M& z*H7$@C%2-P?x=dBJYEyolkN7~o!!sv+8?@~U*LY1>QV$F3_YZh&YKo(U38KLv+3(} z+ov^kA0>Cvw#z$+sz6+W?Pb{=Mq#rVDd%b!A&$$|UumhDTNX(cJ(il$4nENWxvj0j zP*^h`Y`AatrxZIjiS#F^pVsc2vUba zop>aPT0S{ns~;YAjaM zGuG?c@Y{sXY=&H)gKV5M<$u$wYwRcZ+iT128*0n#VMPnTH~C|3<+X78uNtNClq)GD z2fZ)#nZ9OK9IeUPu6CGUg(#+pkowb`WytAU>`84EbU21i^!zGu85Mn7Vo6LZ%rPP@ z<_7CYNOBSj%LeH&X+x>T{%GE?E#HG zLwh^n3f=k`20B!zo?ILy52#j#4Kc6CH=8J26L7~eiVqf7_vrX7lfJATN+OkPOW<6z zqkql|#BScH_rh>k;*efpQIQI~kgk%lpAot8b6%#n_Bk}v*4dgUd;|1Yw5xUXp6{Zqk@xHA%5C1HXPbCHTNpf8z(sy;A4UGD%Z z|C!ho-~R4%LF`}EZYtWFN1!W-FH>xV2J@4RunWpXICQJjxzF%bjcq@$a)VTZ%*@#2?dS(LmM z!ioW|<7*;~yar8wB@%2K>_8PN=;t{x@@v3|Wc8lR8x#^))Z3hy*91!I#-Qw0zye&C z$Uz($)jGkRjDIz7dEQ)SOS~W|gy%WieohXlaf~f*=ud6|I{`>qUKJq%2y~K1(?jj| z7pIQhTP4J!l8vRs65x&VQY)XHv02@OqqqkRRx?_RkNx6&gVk07O(f+o)^V#Lye2>e z?4qXE1Kxrk(%P4Knmpol?cjaFQ{J6+aqK!BWULg1;?kRF53oNnY58OU%C(%}cbY<|3DOg|!;Q_f zVqW|EmWvM}yemRLnhha_w<&g%Yw(_=kT+jvS^DC>Zgo zq?+eZz1hPhiB$@FOlpMTvV}-qIqRugFv@yXbgKj#)dpI=<1QwG;nPY{hGQG}lSqrU z^6F->wVmM4+}%77Wx`e}*TAHbns`CqkLof8-I-WEvT!FDa#{+2&c8@11pgcE z==j8M?Ye}31}>=>aQWaHB19LVEvNjUbYE^wSd%+^|hP@XFMQv)K0SfU*UCxUJvynBQvN-pHVp zcvFmh*r!7lqhC>Hz|6#u)5Q~fQ91q>XW{`_0O>3<^7 ze>IlgiZZr~{0QEeZ8C=)Bt^RC=pYo;*Cnh-IC1>qpv_1WRH35nPe*N)dn`I?&n?rl zK;aNW{`h^df>Sjz=Gdu`t}Z9t>8-XOmoKro06B#kg8urSX`TWk9`*QkvGB7$pSrhg zi2f|yLrfXv5*Uz3F<_Q^eD^(pb)ULoxB1{n0>^6!V47Ec^yNnh&3g_rP!=N7A%z?l zuDlktP$5+~?2qQ};@%~R-K`gP*`#p%xPEZ^5h3S|r7Opn0AK8CS6y(@11CbU%h1(4 zioH!sYFe?V&#)shx+RSGLOYc=BHxr96V6GK#4uYIM?W|~bOE!-OSc;-{6@5Q6nMQ= z+yO(Sl@?N5paPAYraTV}O)6bprk@X!_+b$oP`cI9Ad`*{`jwZso<<7$o05c{-64m& z$B@Z7Bg9nPF`6F?<^5#j{J8KVqWqVu~%?`6pE9{@MlQHJ8mN}O}iFx2fLWMW2>0kYw!dp zPr+@2%@&DZV1{*J1n$&e;RiEMNknGL0CE*Dm!e`?Oj24vOi~t+ajpR|^8S3eJo1fN zl~)f|s-o`2iYnBYCbLyRj?m8znVo4uV74rrhYjn8(5>+rQR~ltTx-{ak_CJP1OPz$ zm-{`!N&gz9|GohKn|b<67rd1fY&ZTae#m+U4$4Jnnig|HsQM|E3seYDJ1R1s_dt+q zU{kMd1S6=v-n}I9ApJ#&_Gfx#bAN2EUUC31L?l)t?N9WrR$f}FQzg~5WK+#nRb+R;FSv7(~dZXBff*Jn@-abh65 z8m+Px2_?4O`67BaZuJpNc`AlyEUXz=;9D-ARn-eBohI+cw;06`s16tsv1Vc*#i29+ zf$1VJDwUW|1l@?RL9t(2QnH$ZXR70@H!Lsc$pLJ;5`sUN&VH#GM{fJ&_{I@hx<5E* z$MiD7oW|E@#?}7oWXEZsk{d%dXFrv_{TirV6DM>B$(UI$V}ME`Qo99HA$ov`j^aEB zs2&Bdlxz@1D1c!nE+Gq%q-aHOGs1$bt&SmH9XgO~*W&27As{xWIEAk3-vyd8smDn{^=U?9b3;r^{Q2vmt#&xJ_quK5xUJg>>dzwCtI~( z2n?2*ibO2UUPYrLmjBZ**8M~BVOIAB@9QF@9YF6LEV)ib&58xLa>Bog&c=d`o#ar$ z`bu2q4u^}jQaw@$zaB=kI14|Z2xbR7elOm(qXQjo*2%LZKLHgAMZ!PRlbUIvyP&rdV%@f~DOvA7lFhGYm;D_Xbgn0ZG{ z83j75;v2l>7SEQn)E0Zq=Ql^{hpnZ}k~(yGEAA($df%Wixap8}m56DbJ9BHv>d3+6 z0BWXku0qL#H!s$k%p4)*BT5Gvxm6@?5o+|_Bl+tcW@$d>Zvxd!L^u_lRmh1AUZ(_u zZfvt}bdBrHK*Y!1j+uRV{h5Jv&hLL<4XcNY9Q6-(r~F6m{(A=|VPj(MrtF|^ z<7lPtWc&YCO)8s8*eV#`xDDfi9Z8mELjDU2QUv@P0qT$iv}lns`LrlS^DR;h!NGOd zu5y}Y%{vvUrZ}cvclpdK=aEw!htj_Y8C*&Z8^1&0zo27?eNL??TJvNR*Lh86UBA6k zA2)Y6MchaKy3eldNU9Jto31q1SKL*W#Q9wJ{u>Ca2G~T$Cwbk{jP-cvI%9 z&)3G5s^Sk|{P`A{+GiLdw6k~nkEAbb$sbLH(5Xo_n|+S$ovjD^}O4@icK8r4^)&1D(yHh?**M%W&~2kd8stj6haXcfsPZpL;c z;;=K&ElpjJ0p&=t5KN=uTlG?u)>7D&!j^ z>F>JGHaJXg7EIFZH<)2(V1*KY6F=D#-j{?(XvdSDp6j2{tUQbp0g5j!z1bofVLL?{ zKoW&F5Yz~$*=Lgt!VO@R@Pn?f2&MJDZInnXzfsto@v~cLv@uJE=8FnlcKtr1Ho!` zl%2p}38rq=vgwfRk2SHN#2=E&54}--Q#D5~Ob$8v5$(WfjwwmBeK}rce^+hnd=ehmSVDrpL5~YNFG&P!6@h*>g5=-{yCDQHqUdv)kc&L6Cj;u!cAFwG4x)0lJ=_q68NK#eg>-b|L0ri^|-k~!JISiZZFu!d{c`XotE>QNV_*0GYnQY>N|RT1}_ z(NxErfJ8}Mrdi0-)G;cDD;l!hSfAvbHvx!8NN{FS7}J8(4p6!vOi9O_UQCk-2?h$(*Kn6Gi;iJ@1P< zBqmXp;DN&8FzNU{D&agejm5v;(c`?fv*FG7rrM>g(W2_Zu#+eZ-ww&c(v90@G&Gz; z0-3AV$?Q^$0?mzSq6?QHMBxCAK?Duz57+J~zj#7puyH&-)|BwVKmyQ4t6{tR4mXVE z9Ol_bUzj_6e<5?&qk!w4)hB`=2E>)c9wcV6uLTys1eCd;F-~R8AI>wMrmsO30L9{6 zC*kES=?M#g3pC=gU!+XRr=9ScRgE5sj^s_Fc*3PJb~w}UO!q@6y+1pN9gcs?0=vb; z;IrX;vUvE}Pa&%wyNJE|Z8eM?80hbq4mbox2Nu@e!a?w!Rkl|*U0L<0@Wtwo`Sc&; z%QE#XcuqU!yamv6py^*pZnat1pz~$(Ry5d+Hs#qIU|A?*Rzig)Dl<7)&vmztN?jpT z-C3V{MIM!C;#;8hFgJJU+iL3j+H7cF-CO8%>bL#W7T7M)oa}C%Zqmf&=_^w}JjM-m zbnUny1ip-t5-Z=9fJN#uY!CD7YgMd17NPyHWvE9~^XX?(j$;gDvTrD`42pB6nV1Lx zVZw#$sGhE%u5~UgI=AR1h~4*GlEYitW2%9C!K>wUMUGR+c{lyh-*P(83K#5~$kM%7 zon4vUhiA;7``DH#Mo|Lq?A>g}ZS&yS4`uV*ZVkhqAYyv?hIxkQ9k1 z)vliA1NwxdY!e9(iGB<8Lmq(WyiklT%1Pup)B(NIR}<;XSXZE`o}f)Q6lpVW^#fRh zp*(f3eak8XpMhiW`q5@d_ley?n)cTZib6bU9(nw(f~QJ1*IqktRH*tV%ZtizY!u(U z<|ArvZ`%B7>)!oBPvilU*l4p`(y_!cTX%iB6S{jvG~-p^#KS}Gx{VfesFsE1V^ zz(zXUi^2j%mkkq%M?zr1TEG^V(bMRMLyy~ZbP6=Rtn9pFXPuY&R5PPyF&)0Pe;4V# z!E3%}$@ulrjiseuaeB^py6>EN?>au$et&tv?V6$?W-cRfPEV07)L5sj=d=Jo_W+oYb8)23L(}QvfNFsv z`7~#m{DJAYhlrzwRKJVATiimF`8IoKgvm&CQt2o(SG5s~S}uxNn*xgtacwMCi~#s& ziFtcQsAygfQ3Wcl0UW$~q6Z3)r}9$4&pkx|z~bZr%Oj+O`ss&yUgNa4Wdk3FwkkB&YL%0 zQG0+;6daT?tv?dZ5CcIG!dX76lSwpOZ=|J2)m45#VE{8qIZ;tUgN!Uiwi$3eTI|Nn zj0~Mn*3SqyPY8-kC7Y*2aDd_vLT$%V76Kf|5!{NBg~oA4K6n|5sKuO}b)wl4L6=2{ zUUO_n*=F*8ID5w++oG&nFm2nmZQC|)+O}=mwr$+BZJRgkyvfe0?{&Yg>eYMQ(I;X@ zoE>rg>^OVQHP)D8j0N=ZcY*={ky~L{li_^1C)A?d2gpos_KgCXp^54$mS%ieNk@~5 zO3IW%S-C#DxoX{5i6kPZE{N>iYEor=2GmE&b*WOxS3!g7RJl$u21xsyk(s#~bwcgB zS8gt=>O&DU4dnS2Z=*Q45-#NHg#fhldVNvn2u2JNBqTulc83%WVbN|RVI+DL_Mq8JLd zdT68i+LH#s+Q=H%6LqU-)5T8id>%QPD9QqrdF)PU9Ni=0EccXfvKGOlWK_wj{;B(* zHYJ1!PB|n52$EjI`#M$?4`h|B^eI}pytS|QDQ0|7jtgaNf3UCB#`b~iDyYPKU{f>-`d9Cdf!OK;Pn%G~UcP*_ao z<|6!X0fbAljPQ&2Lw&sUz^q^ZK1NFfaRjYb)k$}lZAtLU)t&kSX0JW!8+i-Wk(Msk z;N>P;)xb_u?7FwVOe;|U_}L-3_2PWi&g*yC_SwT7d`T5&Yy`SFR_tm{miVN`uF>O& zuC%ULMu^-+bvzh zl_9`qc1Xo~#KThfuU{ER6&?Xif^lef^odZJ#(3_(l{yj&kl2Os@bw*7ry59idc&iP za7WSNt@FeyjR02N!zrby02i;MSWW5G{_kLvEKG5IZNf8-`0DxUMWTG+#VT{Th@t%Qq+jmaRS+!sH zk%o%}(io=JrvjusU#C&liZ4rJ)u|Dnj3KhORt|T32 z#(HwIDIdFY{E<$aL=n3qbt3-rW^$N%)#8{uBzget-rUL?Y!)h3K9CBl@Yg^Wb;kGJ zl>hsmPI|;qBD>C?1z{TYzb^>MP5+AS|2K2@|7Ah=2fn{j-?ZHnL-0xMHQWmec2u2! zd`pfp5N%Z?48YNl3d}znjKq<`R_?mglj#ch?qQZzDkR}%^aO@d@wav`bA6u5Xlk15 zH$Vpv>uhIrY;eB)I9q2Pi&ju4s2&zgo4zp)oG^saD>3h*MGOs*IMcT|9~5U%vjm+z zBo1HO&u7DZ)N>5izA_-S6|?}~AsUWHW6B`jlMM_3t{|ANB1#h?g(z{Jte3+jJ;BJk z!0@~V>445um{VSH1=$BO{-VjD)`I~v(HtS_i)qn>xu2Ma+ldi3OFCbV1L2hUoA20+ z2J>}NQeb@AMgRqr9M0K4B?`IWuYp05wFuMBvLM-V7POo}9U=&q^fQ6W_G%ke2aapB zhHIQIy?vL#*`afX8@KLnwB;=Ztcw$5+#kHDU@T9N+HK(nGvxS8fa*$L5Tb>yq{Z4S0vC6~d{Nw>7J}sqEZil*Ly5$b_ zh<81Sfh%gaF-t!Ur7g}vOOp(unoI~=JQX(sU|`uy>_@xqDT}zB{@c%VayTJn9!}j^ zG8dJBD$(w`j+of8rMmL7tYEKOjDt7$liB;rrKb&=rifke~H_tnoEs79zn_lw8M zg1#Mei{|6T^?BL0PyK`4#MfQ=8@`;3>t$EfSU2duW4r=FH{MRzDplY27oE)Nzyv2`VOBduQe>wX?rxRIo;v!wW|QzG?>L7a)C{5=7 zW}9;5F&rae-!51^h+Vm88F?5?V2{Dw)uz`)*R}nlwMc5qUA?<*F^-Gm^%ryJEP2zxqEIF)fV6ezH+1S_L1Ix zT^Qrd=QHy;Jb{jc#@^+K-M)^r`lVHBYde8%+S|E1+tF(P6 z=hW#GTAI;)8*W11;Vr*{WkGND7GGZ|!n4HswRmaD*FkNPvqL(T)+vndj^FFrEP_>f zYYRTV3|*@d4CA0rp+FmcnhK=Jh8;y6=l^ zU;<`aEILFC_F**xN?oC$pMNakgXI|2lAf5X|GoEe+U^VdkSYJqhyLf=|EhVAw>NS82i5*h!4t`U-ThA+UGlEu zrU=T&?;30A)|4d-tOYhwn?DNLkPe&8pqA`0R$T;Cn}m(Bs_6zOqOef0*$G!L`x)&3 zX)@dl&h00-?x>+nRtd$J%zM!1mv$~+S8ZKyhjsdTz#7BKn5P(yo3=aWZ`=V_?H(^( zpWI!4M|!c>K5geNeJ|cJ5IuDD#exkySNZB6a5s0y)j#|3f|M#%9!q!1nQ1V;xzz&* zEn4wd*`<#aFP&>lSe{^o>C?oJr9|y6N-;((W27d{! zdrX)}#F`Bt*5GwUw_SZkO@46={*8U_*8lU=87oeUbGvSqT%cvQ%ygAFU#T>ApN32; zNXdme`|@T5qr|pq8lK#>SsQ2$1cR=sGhp(k7o&5^GzR%@%6IQCH>j>(s@3TDdt@Yq zA(Szky6PAX3f!tH1m>bac^iGrs+rvpJjKxA=c^Nvw%pH9U{cH zU3Xrsmw;`|b{38C6S4%>0P*rM3g%+DWuD8YZC+GW9@Fqm!C&}X+n>TxQ(`^>0$dys?MiyafVU8~g5(qAXT`(>l9IBlzwTPH}e z3fyIRL$z{P(&BsuuHXTOe9uE8zxO)^vPc4C?MB<)gZy#uvMstf1Y20ON-E!bu^}|K za$zL9%eQjx{Ei{4HrB8*ABj zK;`p5n7G{X3>1w5$)iXWLSy{>#Z1`1ia%EnXpD{kCCuATH zDrYxaPO}}S-)~=W5Au0$yTXG2sfW?gD3GTu6(za;zj@D|%S$ZXR);sGFFKdlW5?Y8D#0BbL)^9NcJ|ZUsr0iEvqV? zm(SQKKNuDN#j@~C;y!a}dt3T=%Wqr6-J&HlY0@1iF)PXvDF!Lgg(as!M^{0WlQuF^ z&6kxOlrbJlCoQ-oUZhHoQGq;x%9_4zTwR93b3&Oy%{reMkx~~HRkilZ4Rjj%n#vRl zo7B6hS63b4Goc=ve_gwjbuH`d5D=_&?e#SB(Y0}@nZ)D&`7w{Ih>?w`@Z<`XmrarK zrGEaNP(z-UvYD0hl4Sbv?c4H(gjp4qGvpAdF&7YC8~q8OkMg~}ie&meOcyV=<(BSS z?=W^Grd)jZA2MhX6zY0{93?fc&mH@YU{=uBtLvO=rM%5uy+1JEJzvj_7JK?@#sPrR zD9OJnf~!fUpt2Sy+&X}g>Sr*nZ$nA4{n!wlt1Zel(dVmk`;z)PyE?tQkPk|$e>oxL zGFDF{4y$|=0hG%0Tk6Oy{hhwDi}ER|t(yI@jMfD{p+Xr~Bg#>1S-0gQv0*xE%p4g* zRW_v38Cwo5S}XReunjFF0IeGIz<4c4F@7}%!rDv31NXDl?OrAQqM%q}sa918^BjCq}b2Cv#pGF_pz~@i#ClkUu-|p+#5iG9`s2H zoL&=B__&t4&yl_$YuCi-73){ine;CiRQ1aWPBdm@GmuWxH5H?tG|*Fk74_aXl=aR6 zQk7r}1Dj(NDlCRiVQq}Aud?=}&CAXxXC8VN39!OG~~i}UPesXRf)p1Edm z`%2;S%3y;;od34FSy!wrI%gn=okC&|Yd`V{Z7V#>WJ|;^j!OFw&W+-g#vl0l=Ph+V6@j3!ii>U&yMfS-{#o&b<#T$9IFx56r95&=WEn?G* zt0gW!i+ig8&H)$_8lLjCb*yZCCti3Ptrj8#2#TjPQ{TbhqD4JcKTS%co=`!uXg6O-3$-&dRWzqvy)kjRUE;R9$wj-m}u4 zhM(IH5lI}?8x{`wS~M@($oY%|6Y5&{H=3pHVO0PgMBpTF;iV2)I}8QJ5JFQOHPRo1 zWkyLFGH5bo35lXOvOWOP-=Vc8-C>6IfL@*ATa;o?S|JwcywWY1Q*JOh8_z zY(?=hFd_pr=^qE_#)CptCoMrBzn9X*xF&SYd}*aS*IWeI8RQ+rx@F*;e{-yT|~{Jr?{xsl+tr9!vTJwFDa{GBNSQYwjbqd zn>je`JX!nB{FvUr0gOl>o=c7Th0AC>J(}fRTj2JMdXOrg8wH6Y3vI!7l1tROPE+Kq zdJ14h;V}pk4UmhC95S=%-8fnLDN1c9r*yWq({;e><^-XW&e;6a9b@b?jwlW`M>UQr z4k?bhW3uCtgU;ao(w6LAF=9ksdca-98eluP7c?g>55EH9zG3Ze@DHO}h6(c?_QWNp|1LE;-URQQ9+C-|bqV}o`rrYx|AhBLrhnMr`! z8RrK^0Qr4_)+Y<1?m=o9&CZzV|?QD})(~RL$8V`#>-=-nfjNd6a znWR=gSkIG6so5r6Johp>DrQQG>lHx|j}gKd{er>bKKfCd-nPU9xF93a6g?V1_<|S3 z-`>#`0G4wh7YGA*!3}AWuEehUYR*Mr54u}DXlb-OaHJjhf;` zc@>5iKyS$m+xBA}biuVTLfe^f?Dl)VGi}_dx_;=F;d`FoA41mtWy}4rVyO{l|XRW=sdsr5ai$FjWZ=Z+MjJ5Ua4luWtm;R^Af(wSS`fw zN6#OxAT#0e6ih{ip(bUxDD+x5k^HA*oP^~(3O&(W5le2*C^E}~z;>J|XN@L2Y|hCU z=Q9rZY?`Zw#PbgF5$gj}9Z&S(5?4?+ zUV$6TQ9=y<-LwQ;QwuvVUd$#91^yy)Kt&IY{KL5X!OFJ^^2=V@52OQB z=X)!HyTZwsiOBjm_G2bDj&;Y0dSIH~=}UVi;~|g=Bv1KfTXO;$=|3QRv3PGa7L?=D zKS;4IKMkx+4eNI>jG)=sCt&F|Z^b3~7$*vv(f3&#DU}J$h-%DqeLSY~5s5dkh|0f) z#IKIKdj(519{r}pD{CP9%Fnc5WGxBl%zS^llarZtb73{B7aL~Hnhn9x&Ylz%v$S^M zQ$JZFii&zolc75Zr9%;MK>J3acG_BUdrK!omhDNGowQhKhjKM5^Eh5wpe>v4!@I?r z(e}`O=Wts%_bLrOQku#IK^oPt7kg<2$NUgCdeHB#L)}Jcg+SudV9rBVA?4Fa`tb2ZrOC|NI5y# zxrLyk?D~{;|4$oZqE} zVol70+$ao?OalF3sOADkKoV{cB}oe*BiG1cDCLL(TUBZwIn|`fjlEl#)?^I*jn?7^ z77JlSG%Qn=^HP$}yY%e3wnH_lu$g)pNip;`TvY zz?4}sUFX93ompU$*qZ3nn(f7CsWNOak^@yBt%6;n<^(!LYfDS>1#I5*e4Z~eim^DLMuqVg+-+* z1VYKCW@Ow}h$!)GF;j=Y9?d#-zmPjOC;DGV$w<6$0u8RT}NJmKKQ*QGa^iovi>m(fD#>|)qxLh`}^x<(cGK=X?I0`S7%UL zVd3Gvgpa2*o6i8Ux)3s!kdg!lNz7P|Am(M&EP5CO13xJQ2cn+{YK0%LK6uLKK)nRi zrmJhiBbSQO08sptRTd_{65S@;SXAA|PSS#*~Kc z?uV|kQz*ul@^ePAtfn@cieP%b?65p0C)bvPOvzV_P?aWvb`ztXQZ()+%^wh>KH40G z5H@%{K1*|(kbhqDHac@(pG_F09YfNEzBPbupEv;8wd&_~L?7GLidT(9fAwqLlOCe) ztJ_>!fh@6iYV6abnXD&?Iz$Sx7Qk_+ux}|ug6V3*s;2XFxgMfaTo$1c%62N%H>63% zHfPc#`3Zzt|7upk3>E619D}BLl5B_BrOb7n+^npwvDubKnszx0N(r=s)KQ$v6UqZ= zwQ30@OQKCWml^wGrCaMmx*0sV?(l!J%< zcn`~6#b;lkW>^tB>Bh}=B3uA9(}9}zm=O$3m0KAdX>Q8s=tsu)zj!X@#2X_w$_Pz@ zZyyZZ=rjzO22^5+0-oV4A3iI!#}I#+H|Oa6$#dj3Pvw<*lvzJNp&g)x>N?h(e&hLI zWA|%6%=BTfKS_&8ol;S<0aBsN`ku>fXIaaRqwf(xJdg3ESU#rA#QE3Z*IkZco$go^Us)d9e6#)7|Y#nH&ZpUjSX+GQ!h zD$OoMAH6Cp)&y2pg~cL6NM3??WC)bTCAZu&=nBIg(X#a8vtMO5ci#ms4~ z7pz`@RpW-1I9PbaERA+$ZvlmrmOSdv9#rIkK!;S=;x1SR@_4r_+=VZI|W& z)9k&`g{@EPcSqT3X*T8HJovAFj$!v>TB6T>*g%zkK@R_}%VGPE$U)N9)xg@qSlGqM z+0I7H#n$K_d3ygItfwF=Kk#Fs3YtrViX=#PXV&|Ws8+PYpre2+y$f{IpD3lH>?+lR8l1{%W43#~dLO;O_Lsiwpg^1{TY!&=MM9L)uSHX5_A5^# zQd{&jRc6LF=8{R7Z>v(EbKEPGF`ph%dTp3X`1TPr*T|qij5GNe-QnyYoW&~KVB07c zEH;EQYxch(^mnLfmGBC^Q@kSLlte5jzPQU!yiBWjuJx(G;;}()1}xn3Wyu@g6Oc8K zumB_9OZ7)}d=X8aCp88~?fpLK1aYIQK~*IDyif$OGj1eF22Q!*{LsFl$r}X>$E`QG zECLE^npA~U6-(Yc&@d_;)rn|wa(oMXbDD3Oo~PDGGpjOZ_*30gA7px$0H?J^+q^L> zbBg>AEQhx2@=Et(Jv#Wg{`ZDd*8e~M$UkjJGyKp43<#jNoPSZ+S4s%lQOUpk_sBZ) zR-3X*EyhSo2tKdH6vW#O@7#YwOTb@ZXw_QW>WUm>meqH%uY`KL2noQc?(c~^FW5Gg z7VMhIOM?B*3Lkt2$VGZr+fU=Efm9Tefyrb@$;one-irj9P@L1{u(Zg9*VY)k*ARIi z{5D#q6s4r*txDZRSD-24w6E?}G@7EJ50;C5_-q?)CIoM-ho(U1Uf&WDI zc^psUTWZm&ccHy-TK)4!%jaDGEYJL$j~ucMBp^&?#^&bJ=iPPY=KJ#vsgKjUil7gH z{!V6|X);#r2($NBUf#5^&uY_&n0M3Jd1?j0(A@T3vMGv>FtYeajrf)^goRwkSf3+I zg5dU24`HYV5r5UR6{axrWJciWnW;y_8T|Lm{ga8(O z!7TRL*G|QQ*m@_SVeK;m1(F(VW*FWJ)a}gNV^O<&!m&g1oglr?B!}%7tI)xg1;ed< zI=2%K%G!A??@}p#TT3S$iDH#(q0H{`bvAbCfhgEttF)?s(nVK_BJ)#oiL0`FC$q9n zW;$7HW1ux2RpppTnMF|^E{sRKOO#sMutF`2HeeV73y?yJxMUNW_p*+sfM(`Y?pk;A z<)+dkP;`vNt_h4VVOXzZ?9lMNa<1mw%eYRxWga?jhC_}kEg+#z7qa8Xz;%jhd%2g;Zhwkf z1>0-nKvyv~R+8bkV_PZ8#T`&@qE;MokSGOv;|{>?p=_1jYzFcTQH)>&Hyhd0Tw20W zwoEx1SyO%5qZ~j%Lr-D=6&L{zaF?V=6Hq$9o+SIOgDQO@vHCOZSD0YvoCZ|y0PvME zxV>|^3-z0qPa!A`UNwfhFpzloRSOIvtXD^xf%YV71-NWc&H*uNGoIxMPns&A<$+1gZ)pTY` z{JG$kvRo$;5&JEtnq=Z6{yN$%maNw&2t4Pi&{;^f%J zvu?c0h>*P6m$w*H5Qp$C9TQYG39+s*Lx9}l0(%6F&x6Dv&D99$lH%h$GpwUCo*if!G7H{+()CQtrA4=yHrq3>5BH*}He(dV5u~K@j#$gJN7BRZVIzHTUSZ-k* zF%tAsOvwUwA-Z9M0?mi9L-H8hv|mh;hkm_r`nqH_q#l#G}Z#)Z6hMvxOWh#%{q( zyM;x1dHA#}ln%xNmnezn&)7(M3!v&N4BS9F3`R{!mC-qr zLNYbkI>As8kWGBh!{H>W04taOHpgG6UT2wm+Npo2;qSvJPwa88;>G~H@7|QAW<#9T zLmB2?D>tNlRbhDz=mYmB`;jW}xv^Y(sZtO{`j zEE9W9V-mNJnsl~V6w+g-Bi)!b728C8pVA16DXDZ5Kaqdc<4QyrMgtG1kx*Q(BNUE% z++DiLLCZ98N%Wve2KQOp(L|~y)%DSzk(xDZ^XGL$BRCw^B`;M7r=}cw5w{l{z63c> zY!aAIcm=I}=XlZ$qBMLCr63E9ZHs(j%yU&0_!!%)te1k6!a6u4^CpCS|GO5u_Ag(# z=g3||$?ZaZYwmR)WW+CSF$F0}(x@Ua>Zq8|@Z6=N8O!LJ!3f$GPZoRa!!DYULiSM| z77?CNMPr?yWjHM-?=n+~8H}&FgsG@>5>gl2$@@dnhM;9za9oGvM3|;+t%oiM1*5nDUq3%9qf8zyW!`yF6=B zY;l+Ck3S7DgVa`lZeWMIEe+0}3tO#@E#FV>T^(Ygykpg{PVV>`ebL;J(LztZK^6Lr z{?-6sa&ZkCSpr~MO7VM-4i8oZ!EZc^GH0!Ja$d$?8&N(uTF)m>sDfG#RHqgG>i=1s z#2b5zEWgIr>{_K!j=0>MYW5-&T$X@dC~twu>9?7+eSmM_U2lz>Pb_MpSe z>A;-iP$tch7r}U09ih%6fAW;IJ*nZ{u+~jeG<q#3tOE+5Qr+&em8C9^{^{wPlBe)(D!M*$t0}!30<=vi0By zbLe^ZvAa(kxYDZL>nzrqAe(@>>}awIDIhHEi~ARJnQx337WuB)+ue1g_RncMS8h=` zt)iTkDnDIkUBCF9mN3q z!Y(la*kug140Ri1z1Z3|#)u*3x`ktRSSK)i)f@b!E$9=m;($YLZqzh6*+hMXzLhAz zVy`sHAcg^_7|>KZ$vz2At$r9GrKe&qC9cB&elOj28kr_KWq~W1ZNMSJ=usw^JWW8F zI~K+e+k~YA!~0F6z*Kj^5yqmp%S;;uahT%Gr-&Au#5H3SDM&mZsz+YRhou;G0`eF* z_a5i$wxJUpS&S{NHu{q^FTAX|snaRmom?sK>AX*b3&l3Rp@V8I2qYPkgl34FCi|!Q zL0ojO*5d_JBY+PZW`2e+x)$gVrdKMLj7HTvr*6Ee+#ps((GkmbS%YDC8OF~8a8xwt zhZb#xb=x_(xG0pV>EM0hFyH$D_OgCJ&MRXR{o-~6_FNq?wftlEgd)3y=P?M50K4%n zj{u;!VIdRue0!$95v7jSR8149Gx~Ot^)r;(7?2=?mmd$- z^f}&s&T5#GUYXN-R0kI~{bqZEiA&i+>o%szQ6#R6#_$Q4Kcl8SjC&0s zZwP8IzO;L+3tto&`W=HWr9H+vKg3TmRd5E!gz1NEocETg6M6e`(&|w0cpoufINo#m z{+mR9!Xv^%CkYo$IgWT~m!jeW#lj2(-9Ge)IaHI2mLMdHBZ`Ug1#aP%A!4+6xh#Ev z+4z=w-_>4m#tBo`kSXu9LmI}3g&m*pB%y4VLH3);fvUME&mqJcDo5sAnR1k%wou^R zLkgL{z7}!T1LVbmF%ztGpByRq6%fp4po*;r#yze%^lOOw@pVVB9A31e??NMak~|=3 zFO5!=@1OfUx15)Ze*~@9{7bGs2-v?$5y}7KZufsF{C^;U997*PmnVu(s_$?JJ`|^F zN%N1(6U08rnHgiqn?%>yM9{WGQOMVWy=YE{DTC{ zUwYjZwJr^8C#Tm38hUt$n4zX2_Uay%UeUZd9~qU3K{{A*QtAzonsTdXK?lk`El0=@ z7|g(efu{*Ud%Zu>MoSRxeB=eY$hu7j@{gkl>ie?2 z(C}3I7cC;ydbT;6HLwz373xA}!cPFI!=PwRU@CNz0TivwaEqe%P6lz0>tp#JUjWJx z5EwDg=qNyM9MnUi7J|IdrrlCE=2xE19$Z<|!waC&Hhaz-ofxyj^WdMjcoq-;-O;2f zl=!7Wp4TNRZNT{>HOcC1YtRG4kdVh@=O|MB+e9g3mr=`Lty)FMN(e0#>RKCdR>W!= znL+g}hQm_5rIds~at|3Qeyx0{CZ$NN1|(*~r^&V^4l*ev%-4|>nhO}a#snqns#!fc zl+a)1;8{ru)hr5ZN22Y2#L>@WMdl(x7IfE%&nZw7?|LJ;3p1F=OPB(ZxS zzQQP{xz@h_8Vw<91L!@ygpXmKK|q;I6uzeBh(2aIWwsyqM3S0Mlwm z+9)lWrmCK zNL|3#tnkEXOGxvsg5U00mzuNYB<;pcmW4N~c)})odBU{PtYsll=@izBD(ATHLte^C zE~SZJsN(5j%CN;zX7-aa@z#m!XCbZJcW1E_Ta9y;&A$}9hnLte2mb!e^+7Xb_F}=` zTc);ekjwCjN`zsiht|Pi@;WM?J<4m<3_OzlK2;?U1Cr+2OlH5r7LvFd>H0GUo*Y`& zscJ5NDb01%>#yvG(T$)+v|kK(*ALxL1a$CpA<#p<>Jfht_4;S*OMAGs)D7RR7FbV( zlU->P6A)@kAbsrtW-ozVSFBDnseb9=qy{G#JG76t3ZR{MuS0fhahw4ZZ6I%(o$Xoe z3m*4d`z@f*C%!c5NYrdG7x4>Uog*Ih?>O49lB?|T&JroFDut3ZBzrN*M%YxY1J{0X z9qPCsg8UMup3qH-6@1#uz#(zye1r4C6-g|Im`GM1V}a5y^3 zy7jfFB9(+-8j}gPFOY9SeJ)_Ruo;Hi6#V7$?G(eyHMlExlA4&%fyY zpdaZ1(fc~W7%ro>p51gXcT+vBHe2wnY$0veJgBZf#4zaZx~Uzi@HE$1u{9qUWLR?b z77d9aFh)_OHgD1}*CVNsLWQBiC9{zF{AEg+lvK~=YO~y8KK>r#IIzp0 z4TVl2E}>wIz78#uejzVCjLZYJZGpSLacY{oIMH}YOMU4prE|2!=E1d za_b9(?v)Jj;yksd8`|E~u{}vFC9?1iGg`@4$FF;6>pXCTW@jVWLdle+o$?Bbt9gV` z>IRheE(BW5p{H(uiB^~d(rbY^QN$up1k74Bpl*U&mnoa|9w5Gs_e4m87oec^=Jx?o z%QOY^BK0d^|IuLG7QA#BK@SO^@r*jRDXBr#gc<9Ekx)PH>=lK*JbKF|Wcyb-_L81Y zDMWkn5RpYzM?!)NokZU(>7Hz8)B5th!NOf@tjRKJFS4yA#R0SjsTzhH>@UTcWtOe} zZ4kAJQTX$!Lp-XbMccUz!*-r>ba0nxb7?Td@5;Hy*a zlWM;>b~m%Cp5;%PT;eusg1~aH=kf=OQ)@gTGOucgHW$2)mbLBGV%+&$a_&gr;n zZ`*I&mlgII$#1v)>CPV!1)J;!lLV`{0@Tr|OP4$vt;}^37{(OjM=VYjhvI`0)QTSG z#z-eQYOWV-qkBgmLVV2s4nUvz5qxITFv>`FMSjH>HnJ2saY%m4g&;oi!j>;%${^z< z0aFcZ6XIiXD!+nXvdf@;foW&i99rk*vN3tx@ovOa;aEh^dBtkz2EYZ))mHLkAQU8- zf#L5V?O!~0$+TkJC1PB{U|JN4R-B@u5@422+_=ComtL--w&T4y_nAcqL_fbAv_&AN9Z+wRmYUD*&h3ed*UCn^?HAFJ2A*YsuFb%*8-z* zb4F?WkL-4ts{JuSRO}BC)e#CuB@DD_%%@Cs8?7Q9D#7afHNB zI6(afl;jONUUCv|y;APOR{uc_R~x<1EYxl% z3$u|!9`G08a?#25%D{Wu&5mGIMmxAEjBACP-ythh5G==hK3JD%IH&J|G*}EDY9oniYb`ehNzjU4~ zU@TvbI!SBO#0;XrmaT-DyxjgveOxg|O$*3;=C&PYrN4}=;AM)!EZQ3~?XrE;>WF!j zc;WIR83B_#KuuT!W*;i6OYx4xJH8sguW1cLoewdT7>8E+?w3GU$H= z*B#$GLP;$D0EGNzbpe)+Iuf=5Yt#c~sG8{H=z+|(ZZeBPr)uG|BfJ&@(R_uQ_B&iw zk@`CZ-d%@DYlfb=6lKuapC}RUn*gjSD@v|u%c+QEd^dCpspR5T`_X3_x73%v`ZBuR z&1$I(6x|$U<2+%UAGuH2r4F<=iaek{yNk`ac8|92>GURTrnaf6uknq&J-UU?GD$o( zP!y_U(RmDw`d(L`ITYOAi!6$nHyI^N4*j0y#ShzYb=oW3B(@RcX$X8JMS7hZ3k*;^ zI32N$ydmwk?8-7txR<;V4AFS{6MG!7O%MpqV2~aHHRhHg=m1eHn8#y<a>XzsH_9K#7TGoC7%XK%WRcx06rT*Q**cxVLf8)8&I5UF8 zDyJNzr5RuIxZ3OMO-#GuVXt7jLw$-w%KXzfyvDKDIJsakMxM0(%u|^3KcCVrZtdda z^}Vv{CIl4-N!LW~nwqD@jioSePQ}eLGG*ylb#TuN96d$sTtIpg3`QKGd~F{zeT#Ra z(E4dChS=pcz}LKjB#wk@R#Z_C!flw1dii~oG)=6R6ZkIjR-GC5n9t6CLFtLA$)E53 zqblh$f9s3=RHeti0Qi63xfA}!s{H46-NN(#J|bpY-NRdD8TISh*mUP-@&*lq0K`Nk z{Y>yH5>k}05G)D6gqdJDZCv{P7Zb7>8I4w#q*axn%@tZFNTrQVc_C^j2CNE5%F2P3 z&GO22tEF~Jlospfbrw5)k}=WS`%_&azTHgMY3?WY>5kjuVAmVq6sjf^4iMwu}+~l3J0hon|W4qWImc@jmAc^Ap8)>i1h+d=nBdbcC#xl z6@#tr_FgG!LpY*k%}H(Qi~@mau<%Olfo3x8o32H{995(HH9!wR z9KoM*t3)@;H=}rtOoe_0LV@DJlPT(1MVcBgl~Pdr8WZ+bWJDz&qn(}x>K*9ZkEo{8 zx(LWyX%XP~(=VAAiA4xsDos_IkSR$uhZ5nTKtZ_;;i({#QYuiP10}x6Lve~^^z^IM zKT71C5^sv|Cj~l;7;lQ08^iV~KQY+CfKOCXp;<}^T^bsCj3tJNRu0LAX^C0IxSu%6 zEch{W=9!GQQAtgYppFh_F0hK0L?dcOBdfnqMphXVTUF*edVnPy6AWGx{pNrcuxGHk zInW7I4hy9c7VJrlww+>OW4$)a@L4QiBAj&6v;{m<;Eu*m0Q#@+e)@0O z&Ej_>n@;`-zUQzb;ixxOzs8=DasNou3{ZKRY=x6yP7UgOd!@1GG3x!w31Pf_;5qT%gS}J(HH9_$;hY(m73{f?axE1O8W83U#U_jv%e&U z<7&Y4hkYBdw&sZp9`9VJULyp?$?XSZ1RI$(SlANR^&$wV6msd#&mYMR{E7rbxz^mw z6Uv(#+@-}ueopIZsM|X+Zjd3Mj}&RHaS=-<-NcjPTMRVW+IA1M&+rZT|02pNaW%X zqo1VaND3UTyd6)S>b0b=Iid5iKWWZY!4kP$e%M9nL_#Zr@c90~Ug$&+Rev-k@hv4X z2EubbD9m{4_dcbL#$X>R&qo4)UO^Lrqm)Ku+&BQv#6_OX0*CA_iVX4njeA!ryLU3)Cyf1&?qC{_KuyUr(EbKj~$!^ zz#F8>cy!NL28)tm2HJkWZs`x@oKl5e&jwvL#=TYrWCVUkRX~O5r*?1+ws@ms)I?eJ-$A)5vb0WUbk;5yIHa}b@ zpfC;=vD0?(w&{uKRWk`Nth+8@BFW9U)-=&oU}w8YX-9P--p!}h7}yY(6jVKzK9;zw z9o{3xA$s@!qU;^pGwrr?;fihBww+XL+qUhbV%xTD+qP}nm8A0KS*!c)e%4;y``G!A zA8?O3&uh#vPC)72W!)Z0UkY~vSxR|mKzbWMdOJY!5Cvr?37{3tl2ACQLu$tf?fG`^8kIEKQ8W!VX1`)RhXs zn~|81=~zj)IKIebQm+RoNq_Uj@kHWTS9l11E{R#O`I%H5DPamHGvWH4>4fZx*!u+hSY`C4yjycwfe?!RooJRrs*~?mT%i&$E4I~;aS!MCv_5`c(>5YBW zrk~WMKe<7`ys{-jf=moL`TR&SEJ~d?P*!P6T~=!`N)jJiMj|E2nBpH}Y|Kq+vX#v5 zKP)@MfPevVLkb7PT+O|$rXdEHw1<{c9^P#qgo0DyO(3^OIo;c$7WD9wD875t2?W9P zpieVCGU5oWdMbLiGauvi>V~SG=h6BT-y7|R_CC904He^=iv}OgOQuLLu&VQ zO7}LL>hSde*VU#qS61D|^E2=o!|bb-0UgVNVAhj$-pJJBwAh_T_tlfvgyk1}jrZs% zwjPg2>(oW-kqgic_Qw>>YRnZ^ta7a4Hcgf*>~BMrtawvGg2}1oEv;|O65$TLYlKyO zO@re65F1C;#LK}(lydbS3Dehn<^iOz8z{(823-j867lirVVFdep2pT zI<3(m1+!0>U`Xz^?bf0m^?U~msg$(7Z6utcl%$+K)C28Y3`k?ww=6PsbYL-3YV+*x zG3Hq66m6VL-`xauh;AxgI*!zpKwTV>I}F=;+5!rZN>EN4dP}o^bk%nV=L5q!+V64#eM$Qe z6nqPj{_`2{8!+aZ3x#Ce-R7XgSaT{UTQGCwICOUb6j|&PTnoLzofPnGDENVV9Y##p z)yHwz0mR)m(JAHu>RA0TRP`=$jSTFJ0>LVW<7N-fB*$$~^Ofw6aAORD?d_&}$fpy> zS5L&tE;0uz%SQ3JHKm!N>iVL7hDRpSa$zh{_Mk6%9Ms!BuVZ;zT7mKF54`#sw7Y1g zpVc96rzh-*mTY$es;QQ?4Zg;rrt4&RXLWXJSvdZ-zw*?xs&-L?Q`zQU;%In(>Gn~^ z@!V-2#(e%jnoJ3QVMyRAk#+}DcUO#X0ClURESrbaDY0%p#3)Nr3E~dJ?k7P^F$SY@ zS0gu-Lc~?+^&Bqz`o}C@{nfwJ>sv)Y{x>dQ*T3XP;s0M+`G2hA)#PmtSP^|(e1^1e zF+C5)GxoF3w}y(>kXbN^L!9e{>%d_frNpg_#7WBB95+5*G6;mz8j0B=20+9N%!3DP zyD_Grc|4tgf1nvZ5euiAzwSM)VGzfo!1X5XpCN?R2u(r_t;nHS((;>PPg9{D^oTG9 z$!bx`quvTWVh$JDZH7jZIWpcUE!XPH$|Yi249x}0#fD}AeIY(b1d!v?vMJN5Avmr= zW~F&!QX#CpcGbyOGYdKgLs5E3cnRyR|18dtx;P(Mv+CFqne2^{NSk)3C^b=yh?uxI zI(h+Lqi&;es74n1V@gBJ+2H}GLXewU1#dFlEGMB?c1XTJ0P8->)?TeI(g)F;mx5gGd^wywWVisbF1I>1dlIUcZZfX#qI%t z)~1?|+6Ikn<@J#%Q}+wDhqjte%8y9174Azx{QieNq(OWQjJ4!65V*KGL0R|+Ap%3Y zN+F9RN8u4s3Wx_VN(xpUc|IgS^_Ubbs{oU=j`PPiZCSN}$mq=vy2>3nMVL~3l1S)j zhVNKZ6uc}E^N(H(b$Qchfg{D*0!lo?&A5*~rRF{FSTA6~rH*TJj8An$YVJ-lT&G!% z-Owcp?k`olIKyU56EVS$5V2MINBA#2(8Fl0174G135VyO66oDo;OF6$VSo>YiYFRM zsT`JZ#9H?P11m3Wy#dBQxvAz0cU~%&F|IS|pSpv(j)P}cciWp%WAAH33naoti#ezn z!onXPc(@rU$c1axf-Bx+2oON_MGUspmRw_*eXyk6pg?v61{xPIWIbrK79~=!MmD%Y z157uc%mQac`alshYeq(_ib{bWC-(1u$%PrHm9Ev&>xlDE9}?oB;`RpATgin|*oifB zU?q~etB>=W>l6pn3x4EUp;!KX_`w$vDxB8zJMzvZ!GSxk5xcBCf2Prij*EQZoZEJJ zE(}Y5-LTvcr_wMgC=<~><&e`5bHYxm__1;wIK)3Ub?A8JJzTsQ;$a886R%u;&x3OF z(Z$uGiXm&vp+Yb2gxmP7P3i(Bdrfj7D>MF!2oZQYeL9s<#T@D~licCsP3|T136&PtOe*5L zv89pHT$6q28(ant>zIgAKGvg=pVIEgtV)5oRFHTig=j(mCL0xzwoZ~JEnVvV!Pp=T zHU^lq0_+<*@1H|LUkB(CnnXo*UO#kCK>3_H(G7Q$u=X_lnKf`(;fIqo`z4;AS^k#O z@l=bXwXqI3!_%EER&#%>FR#Nx@;6as7$URw-00vSm=L$dKn79`IR$`o)O3i_5M<=3 zRSg>|h*TW3B`qy+e_y`h_8VVax_41dWD>0i1nCLJ^suTg?JBbF{r0ufzkrW2ZVT6_Nyd9MA@~W|oEFN&xDr?akzID-iLk}?Jpbbc zAc!Gm9=ux$6zD2kmyNnX-PiICvb%m{0BOB!Z=>QXZ`^i&V-PdRWyqVaUH^>-{@~c; zTVjSLOxXPz(Bn%2?>1C*Ns^oO85N}Wr^OKV336-l%GiXP*yCpBN`^fDww&(6W7lAH`~@sX^z~7-;Z;#<+QOU;k@mT6t^; zG|%)E_%b%(i*;O1!|MT76nb-@K@@!@j>ATEw}8fWf0}ocF@|)EH|j19qT@M*?~C>< zavZd-S5Jj_*ZWIVt!Kp5N$v88`Cku)Ay(osw}pd9^NLi3n1_ z;)RxE&r%25^fNSLmq%7hbRp}Zx6LIDCr{UlEjUrl8X|wTpZ4alH4rlzM2#{S61lKB z8t(hJk*uIZ24~i${j=3`YaRDwlF5aO+l5sZ!wyRZGQ|9T5PJXn6hE{lTL3QNCHdDB zJ1DKDHvP6vhe}~KYXxHn%`?8vps}`|g!~1JZ4TJ`Bzv|$xJ_);&9j)yR$QTunU*zH z?mjc|se%l_KpIgW#Ko9-vrP9>nNB;(@|28wD?@@Lk;wPUOm*s|0G-+#vkGU+7Ey~8 z2F1**)BXTKeM?j?3peM|Vt(-bW9{AR#{18?w+}Cd|L>ZuMV4QzzZOvYQ-nV@fNb`y zPe320v?5&hud8i*8aUe^yGqz>e@nz8%<4^hJ4#b z$C4N?YxnWFke6gD5E*EBfnoxiAaf{fwjRj9y=JGR+KPE=M=cIs{86WdWz1vN7~va6`Te&+dB5`6$EG*lUX^UFRX2 z6C^Q0Yka@H-U-Z6{X_`i3KHk2+CNzOvgH%<&?1poslCJm*Bn`d1e&KocD8u)flA`$ z?#FC&MV_>@)?w!)4UoX1E=s7psNKJ7Il|eY7j@5MMzbDM{=pQ{NVvIre>|YcF zPcz8?&p?eELNr;$y81;Q(RO>{I2w`|7zE$&`VW)Wt~WmguW#8x)OU3Ff06gcfMof; zi~0Zm{d-@j|FN=v^^?wuy0RPe@V+*@LuInF&Ja?bfYKIV!V$p=VTlB|HPa+_>Fa4% z@RWnDJ8RPFb3%p9;odLrt}8}}0KG{zt|BT;0d%G(CvuodSU)Yx!ats>kPE3%X3PAc zThsNn^LKOhOdxwk)yh!3!e({{(IP`C?#_xrR?lFYTY-vjoRZN6HOP$xf6n-8fGPOR zZZyZ00`t>p-tj$s&$YATC2(KSoh zc(jN+ikPA1Xxyv_t^^VE^LUekkH|Dh86I&DtZ$~G^qEZuiL|712g zfIQDhw36kSvciws!ArY4DA(tfEtnp&_s%W*PzL5MrplN6Lgkjavo+PA+&-%B4yHPB z$*Em55xHZdm(9~Z!`zV#XsOXV-Y z7bkxSuED5j{wYKQMa?Zm)H0t3CHnRKfb0`Tv>(TUQ<9@@EQf{+S6^D$Ns&*Ka$6#; z1wVMM*mqup-gfJku}-{3*>sWsj$TE3pP8)xGjJziD;A`9sWtV3WPR=1^W5`WI8Xwb zzXx+%2D%H>%je64=~z{cKVtVwiZ zIT)@6GSh?ki9s$}S2TOusLy?Kc!+So!Lr(jvJ2~HEzM7&x z$KncX!tpK(0&C6m7U5ldnrgHWtdmBmnwe5)K%fG= zKWGUEh#fk_ac2at=E15uW95aRY9Yg2tz70>vOO1H zu{`Gdwb1#4WPjiOeOZzIra!t3`1_jm-!F9j+b{9gwY83$vI(Gv51hGEykwXn(DJw- zu~;HAOX@I0wL4fh>k^40-0q5CAdnAujaxoQi?OenQf~ogxt6}AF%R(Ue5~QKl~C@; z^2hOVW%b|OB*=7X1O+#AfF3?WCt!9bvGu>`Q}+#?k)%sE3-Fa)UJ^SnV&(zMYJ~z$ zoa7i{tWwI6k%_D_EfhiD7350DYg=oyB8nr@uD1N~7C(;R7V!tq#Ktwo+zK>a7v(B*d;m zqqX6xsMC$J*-a51C87;5<5@-M4g(e}Myg`NkfHAArcy_ky&XB!||eXrRUxan{K z-B9FftZkIK8AXzkPwj4O$`XqEn{Kn~DfHbrhKhT@D?*W4duFcI0Bp>&?m}6(u;oPNcWRwCN9~zOB^w!^hEKTQyq(A zh#LX2&d?xQzgRJyj2p3g6VbV)T3Ny%d)kzSz13nLa^R;Y3D$}nzR$k#WS7T=SQGLX zkLf9#Kpq%>#VGyioUsJrAY&xSfZ7Ta!{hjIXpHj}^}y+zxi=23jMX3C;{Y+sNg~|l zXw4)1Xi9@6<>|pYrtVxyJxpdIIbAsQE_VJ3@$L9$07urWj~^FZppOd{b2YUWNxufyLO1;jTx$~fNtznvvIfNS0 z7teJ2kh;W{>WXZzda3l?`lmk*jAn;%yc3fCfh~l*y2D}L2mHsk^5cJQ5NQ5i4*5TD zMov<8fF8R0Mb(W*DvbGP(E8UbYy!JqV_1iH7p-P;;2=ek+!vSiFgm_h?cvBvEzmhp zGcXLrl{S`qLO$;%cCxauOrK~lkV`2eRs}1!O-w+nZq-~aO)>DrB{SA{BqTlId+U>^ z!%v`{)o4%OL1TAw~y@9z_m!v3ba{n1@#;->^-hh(M51a0rC=z%kg7MvJ zv=T@mQ34Y56!a|8o&eLxLpRy6LIHAp-Vk+wisV-iYf=J~F5^b=#r))Z3;pm0h9H8- z8)V-{FZ4Q9On3E)z5BL{m*8^Da@oHC9cU$74f7jWZvF=7|B5XCUTUMF?_jR~{bBoW z#QG$q4VjIf@H|KY(A5M#DH2h~o0tLo&GS}rxsf576oiyP=1zvzKm+QCOruUs?*xi? zAAx?$E00{jgZs%Lj$z(RXF8gkRDOMYK4JB8vceRCFDRRH`RPIh5m~!y?65b!2%Uy| z%U*+u&a=gmPCAoXL(GJ6=WX_`xCc%aUv!iUxg}EJg(K;E_SdYTptSo`rS1?Fw)mGb z&jt8Eb&bN;8?U5r&E0iFyBZ4gi*eWmt{}!z7ccp3;7=ys#A*;fW`onsv*SR5BaOr% zN~k2-MK=fBN`M7E6(5VWWmhqUYoAgooJIepvYA4ogiO1)1nW0J3p6;h@j~)SMjfV? zIjsfcx1rF##V-#g|^adV4mO{L#tfb8EDPf)=F?9l%meLy7Tf+UM>*|Hw< zJ80|Db2{#}a!W1e^XB@@hWukVr~{I%_2Rd_Z=_ZYrQlVC?AKAz;A4@E9H~_&bFBz^ z56M_RRprAG%2WGeMT)|PwH=lqo(@td4L$s#F}V`FN<4;%zt|GdP|EePM*p~XJ8^0f zJ|s@t)j6H;#*%K7AN>GYG1wiQ&plc24otL!*d5*|HO-gM4u&X2>Id`FIkxHa0f4BX zI4U)?St2xOoH4W>_&_IHJ{6wDWNd*`XG6WyceEV$wmNO*!CDK`6m9{W1q$7440gdG z%YcHyf&qFzU^W{DDhJ#58BslMWu2)|akTZLIxxeaqQL>F8rUO09%FT`jJCQx2#+ir z0}QCPfM62$kQ6|*O-HPEi*U{K7RSEnItjhyS492#$Fn2IErcWb8&!DzMnU=4x%Izy zzxn$C{|ii#l%!?ci=T2)d@OYc zPQymEV>*M#8J)rtqJ=49CxV69P6g-J8<}!hoy0IDjND|6#Yo4NSRQ^3TUzjL85CPAhKh2vtqc-=Uc}jm46mQZGrM9ODeQB^-;k9KuXJJ(Dlv^|&?!HsprU||8uQl#2anv#H zq7JUm1+>?!LN=tI5)r?5Jqt2{;>Wb()m~Q_2ZD*z z11zSRxa6e)p+prQSlA|Ci|D!qBmQciyQAIS*w}B z)C^3*r$f%An%F@zcu85xIJU&vlDp^>3!H!lNn)@{*}EdI=WUseSE?hF8?C#}SYoQX ztq*X+7#qkTW13V>TVEZy2F2*@V#0-n6wZgOZqAB9Sb5@z3US3e>MjIRG?Q{P3HR99 zwTXG<;Yl8_aw#E0M}v!E!1%cepCXZ&=F$xDo}P&m-i@ zUdxmNDH8QO7uec^tZ&KkN*kH+0(m*$IIT3)+$ev2fFY}xd5DcKt3_@GY*Z53$9W`Zen@bx?pX9#F%DM@HeG2RJ z@CNFaSfG7?{DteT+XU)G-^c#pZ%>x{e{pgC-#f;C;5zYt_K*M3F(z88B8ei8d>M+d zu0fB%XD=p^Lky7MQP%k^e@bIVt!%MC&gvj?4>h`#-xkkZ+;MPD^D@!@dyT*spAN=0L- zU9+6eWiyV=Bv%r@*%WWREdA=bxZcK77e7|L7C8P5ZiN5iAA(cbfmd;{&zHz$ZTM*aG2~J z21@d?AgIWLt?QHpOZoM+m5mkEw?1WFiq#26!qe!Y-- zwS`R+vpP`a}yNLo)kBr%~Gxbp2)=x)Fv!8yUkOYjV)JH ze3!N(FyfoXP_^}16lF&?GeWQM`sH$gMZ)vTY;_Y(dn!5(*U(v-TObdRGJx}?Qgk|v zsqQ|51t3DlTx|^HQvi|0<$%^Ib{-`NG|d8pwF z@fbnO4?yN^3{Vmr=lu-zR3jsYBN2!R*$4~_=?K(;d#NgMyXDH7$5>@f7n)V&ge@tqGvOR?6!gq+LNnLCxEY|Ud4zlf%IM=QChjRuIQpE*&Fga+sFzBac% z^K0!5hDOuZ5bL;_S=ahnA=6ywPd9io$t1%?gyG{{v`eOf)J@^y4e$cX1E zXW7O5bB^xJ}B@Zzf(SeVOe{YN@CU|l7nsR~h1*haHi>P@TqBlCmn zNJ3_V&b*3Js$Afwn8YOyVAs1?{4n?)ljIKW zhm=LKWe=a|PekHc=D?xljzIZhqFWSEjTGB-xCV2hQbxFCH&!j&PCNhTF%uT0^kPVI zOuMaNQa^^rp_v=yir0GvK&9+EK&8x)44gBsA*|nt-wBp);i6_!F;_GD)Nug@-%>^6Ekpk497Y4?3iYH6bVwQA3a(#60}l+m7mlLa(@_xcE5)V?JCsOk)irDx+{UQ? z%<{t};K{wqjk|$zg7_0YtQy6~PG@qIJ8Wno zyb1Hpl1&dixHT=g6ZCdq-4BgHtN$R%18t}?^u{w6igTCRu7(C4)p~$Jd60$@<>q}9 zY_nAyq#NFY#BjawkdRlPF5}DoK}>mwXyPx9g=C z{vzT-HWPeQ>UlihG2`*Q=$cM8RZsjou{PwVEwfL=j(yIy8}V_S4ek_8T;GuI>?@Iu zX=z1zM#cJ}vwb4eSLIzWwbOFz%3Tm;VUO8|vRJ}0vx@S{atD^BMF!1BRLW-URs|5v zT=X?(&B%&679@y3BY)*vhti5uS@jpP<#ybUXp6yAxK^(Z%>!+Acj|JLM({r|!|(nJ z<>gz!x4KbqhJne!OT^@6MFxS(l!9VQlvD8Ks?05rePu}~M2oDP8}IR4v{KvCej7((0wE3ECY7% zs?^BiQdK4pLu*=1?ZaEdg=-pFda3DK7b+{@MRFBLQK_x&4~>+J?Fd*~5{;Bm2+PBh znz_np>If~qc`7Kuey!QEG08uf7x&5@(nGEYs_7D9=lx)O`0!qMxv-NbCGlvdRk+pV zdT)#CJxU0LaaVsPOERuRVlbu;;~{0Pd`J-ak`~`y_*BOw8Iu$NjZ#uviB983yCXBX z5;d6fF>Z*_x|JK@nef784Z=Bg0jYaAPamIiXz?Yy1E3VDjB6sdv4e~lGCqUu42 z%1JgL$qZ^at|aQ?hLcmIaH19jey8=NM=S;)#~FO1r*iumhT`ZD=R`JV1(sRfs-qs? z__|P&4>h*X(GS!k>g$mavw^eE(&@4TSHAOazvuA6rB5>&&)Lh4flbF&4tpA3cB=sw34|}z{yTv4v)>$GW!mi!0h)w;Z&0r}*%O?h*oD!ld#GMn z!vhUjXC!_;#_(vv`Hpq=A58WwC-qrV%hSt4$A(-nRVh~j%@eEDMjK-;I2+Pd9jn(S zNF1K4&viT|QXt>Di%`ku+6{s&<#}zQEz`mdnf4iR*9NB+an}$hR|(gdX`TSB9_8*H z>Mg-}Z|U}sq7AaGUrsSMV?zm^@dR8EJ)M4zbJA2Ev*NC{#Y`Rc2+g|=8crx5Ue2Hx z)$lgO2j7hFj?=?;I{;k5ZySWZg2qStZj+pT6t`q5o!x;aV+|UDER*<$BqK3hhCM~n zpKqg{cw6QJzObSn0SU5uXKLtj)AE~FvFML!=wZh)f}LxZ`G(MA+TGE%u@@Kv@0AGq zVJRV(3KTFqUYOTDor^IyR&MNPkSX%cXcc`pL^AF!3TCX`vQJ5GzX5#2uJK4(lH2ey zYI1f<#;SB#G;I%4#Ofs^MK*3}H5^_-VX6*;QGHN%X8ULEA_Q3z+jIqtizeWPS_^DW zJkq$l>2xeTVa>gmfvaKpfmqRz|EZEmuVjDki$^09E-4c2+fUIM7n|L9|49raQ zLY-}CXYSxBc#N#YYij}6#<0>km+zf3% zuzVq?KIn2Aicf75iJgA{_%Q*%BC6TvDuB-;_43Ci?JA!6$44drlW>qhjf>@4@mB+n zqcBtW6+@+G{v3fEm(D!^F(#Gs!_WHZ2rMn0+vZ=7SgI!H0yu@1nDFz+znE0;-UW|p zN{v80@~HG!P4K09uQmCsIqZPrBkA&Bbzs2@U<67%toqmu#^#%JWIM#unsVS>Dd5S$ z`z;yY4R&+T*-Oy)RM(D*)Ykzyid&-y?mBKhQ>X>)dB@-*>~KYo0m{P)50 zzc#u5enj;5CikxqQI@K<<2pOC_i)cX>^mTYW&AX6QZbUiJWT_H6Oeh!!dXCVkh~$W zYO(-h*2fD^ymc*5)-|YcsiNY@pbWmy_&H*cRQ3NLbpRbjXV>&PI8B?T2*~X<# z^RiG%+da53)uas-JAKUH*GA(Hnap1{LBl|3vV)Abp{szG3e5yXVI=8>Gvp(V1wjam zXY$p_CIifH260$4sLRaAN_G8`7e7iv#Vz1WnX{YD4A`+|Jo-xRK0Y69WO9OAS_a0v zyjTj(XrhV}Chokrv-F;$gA-;RSKJ)dh+v;~5&xu`kKh0YDdNNo=ncCc+!s-f9%94{ zDhRp7Nl{C(lbrimhK0y|i`x#T-8f2z_$inB=?_jkXCU(S3w(YwZF5mDz!8xkO%dYy z&*1HV^8a9z!1_Imt7aA@HjoB@iS=1Pgk4PV$u0|$X<2>U=Jkx7q9XD~1k7&7(PV1e zy7t4%Q*d(P$_Kv4PTE5i_aQzPe!w_$A+#t?dd=oQbk@PO#cq|SsNJf6nR^713Av9H z>e`FRh1H~fo>uQqCLX22y2uObPW4LwYKlvj@xiqv44nxNW^}PlW)vCviC|$t}e&nU_LDwTMs9m&jwK)c+kw$&nsZ}H1uuH z-ZSx84_JSd;vpa=FVRvDhRE6_;6})r#yEav@$Rz7dAT#kt1FBJD``0DrcJs*#pV5F3|`guDc%d!v9qiN_o z(3)qVj>m|Jt}=%R_)}ys&{V9?!xhbqj+tf)2p+3;>fJnfR}9z~wP`MG-&c%oqkI(S zVe@gMWLRf|6|MBGB)y{|PYazVK10<5V9CbZ)amGX4DpQOFU4iYDFm%y`6HzAnVcZ(si>v-0t5qAmXHrnm#Is@uvFe;Daib# z0nQC$dYZ;xzMs5PlBSao;)o%)&TDLpZiPv-{honJmu&Q!>x-tjsdQHHy^i=rP6Rjh zrkW~w8LsV4Qdi-wCoRtm)o`lRx=~7zTL&&JqF4NVq@o0l)w}o*YrCmyfPLnxCyF*z$9{518N~F&A%}WxH18~_%rNcng`Fd3I zq~1Ftw7J^cX6$!L=eFXM9f;W8p8Qde7Aw1qE+1!81|=I<(ltCD$-p_>l*E9ndO< zl?&9VJL~fF0B@oUmREnRFSo1v(?$l)Z@I)dm2TRz*0?;edcf;$JHQ;dx)tDI$Qv(& zRjP_>t6+o%amgDlxxA2elI=&x&k{1{RRX4-+WHH6Sba=RXP8mmQ_qF*kjG4H)VY0$t(T@}oi1A`!RfdRJD>l6+?eDybphy1m&7mlWLWQr){xUyB{ z06|fUsSu>Ds4YTnN%4;3i3g6H{JUkqE~^Xvu+(MLDb^RBd#)nOF3XXcuCn+U%|wPt z&kZZ)0nKvCs{~setqXm969m9(SCv&Ms-x<0x#0G%uz5Awp#yB*G&Z+4n+qC>M!rdM z9;#84qKE$Q#ZNoAWqbm(M$BHEF8=|?Te`odB(`V4<~QG4OaK2hzxv+~TLo>ct@Uk; ztjukU#ciC79qb&8zyJSp*czq0^VhKT4=IFIJ?#*wKMG|fe!IGu0${U5r3L~u3nk01 zre-)n#=#*Y*BKMVyPC5zq?zCF98cI^zy8ScbTL7IPOTHcy-Z78$K3e5Y#-(Le))ju zLG)OLGN`f0mwMRn>?+H{gZ;Vp(O7UPZf&CUq3&3N2Rp4*+z-RTVY}b( zkoKF(MD)@6(qJwBmAo(@V%FTUt*ZQxLu z(R&A(fy`5g?awBO`05U%m`cih17(t0pI?vHAYp?>U!fnY79MJ`OL&aVtjo*7OIo#| zA-ryE#G(cr)TgvjJrj^DwOr$Tl}q^{hhSE5{Jxdh`P)mf{ah^;z;W@k^w*5`O;E?S zOU2%p8nYqIPz(0=W0-!>E8p-D0vNwS9N|dx;cYr3i0grFThAS!^+-XyYGE6!6 z49U~zIf9w3%%*;!=qTOKd6T5S?FOo4I5r~lx9lM$NgcmnyGw6|k6>l0_$E>R6Y(|D z$^6LV)kCk+;n9(KajGtLdh^=txr#l7Bw55Yha(49LBfV$a2T2&QgA3BflaTdz%V5VG>WFlOyLEo$ zO0N2pggLyzPN$OAK+tr-K$mZ*sWn^Ys`;;?@;f`_nGjla5 zu>=tiYN>=+&F==;}YGZ#w z?(BEqZbJ6O78zZ-hVjL5_Adz*UVi5!lc>9+$!i!-AOV4tyTx5glBtdEdT!!-3nO4K z1&^8@@ubaoK-}BfKa630*v-i7T<^;K7Nzis&!-*&LX6QhT)57i?>peVI8ed?w~POF zNK&5M$8m|_%Nwc*m8Gi7(^WJkSFLJSuCFou%fIC^>Og+Q-t54v z9Zc_2Y&(-yH-#`iQjDAC7&X@w{Aan@MXNr{Oc|g9WU9CZSpc1QNg$GK%)T15-gM@GGDy(*{YvFVE9z zWztCWqU$!JA5B6!pgHB{D|?)R_j8VasHue4<69U zyNKE-MGyZP#%o$kytD$!Clm-vMVQj6%v;1~j52Btd5KR_Mkc2>(q7m`I2QfL~c zLlqR@bP=I+qOkPh+1V(Hf9x6m_(?~hmKv$!TfHhK5$&PhCM-cuY_(Kr*H#*ZjT=9W_|ypKy7ab zpX>ycUimXi2DPm=q#i6wp_G{}_Qf7DPu+s)@NLkHy9+b^+Jhvkr98qG8!MbPckN7B zutf$w(81wdtM@qAu2Q0$|D$MbMR|iaBS+VlQL@2bK0&C^cw##@jX}JP9wGd5AFLX- z?Vf7hRdncvy#YvLpY=>8a}I$ zp&y#Qu0ls`%iX{)dea$MHn*1;PsFR&WRw+Jd7GJ)GSjYSA(u8^VB5iA$3Ma#{eG)# zQPwoP;Os!*zNp&Ox*qP5mU@zHUI`ZreA#r-qhFdHMT!oTIHLt;Y1TmW<~Kg$d%?lb~G+WhMub=0A&i>OJ-v#{YGw%DY!~gBB|0{Eb=-<+n{*zds zXzcVKtk6VlMJ#3XkuNejYVj0A0Wm++GMb{XZ@^TG`EE4>C@97omZAeLnn!^lUjU13tETiwG(c-o|B`)8?>Ge&|Okq z26%+jwfG{D4QmeAIieDM}XB zVl%Gm*C$DEAcH)9aZAk?V9i)ot5wmFUvFEJF=;g^yeQ}z4q9Yt2%*RKSb%j)&&g7R z;SHqm-@9B5XD#)MOB;zBCKOG4ZEH4kd?SdUI=ziM5R(d^vI%HITWfhbwx4yGzOh26 z382#uQeQpUVH8qO=+YRAm)ZhG4sYeO;u9_zMIO5xHVv79y&yov_nfI=9gBPuX`|b`cTgHcB2|Cxd*P|l24WS0pgRKfE+6K_~RP>!EW7P}Swb4XR zy*6G=HaBPY<*~_OfZ<9GfM;v`8%U$LMHOVZh~pEcb}%b2iK}IQG?CbDK{t-ZMf=YD zgEqyf_i2D^>$OU#4EL?Gn!Q%*G`k@mtHU($JL;_8S*{WsAAf?(;)uVXrC@4rT-*fJ zl&5~(FTs5D4pOp@Gwi&HoTH=owR)Pd{(&Sy-Q24&(34*Ct!$4)5L@2BsUcXwW@#ux zN^qBUhZkGSDz*f}iCJMQ_nxsdmjhBA$A+xGgK@M%7=d>U&_#fLkSu)}Tq-Oa)V7P! zoT0x@P{BCjbRriDufWJ2iCu;b!^7k&Y=&esM)62J-TUykqa&^~fNRiI3o>o}NKBEx zqK`sfK_IqEz97#kYCIKe)`sI7nL&RfEf7?wOgT24Qq z4{1og54-GiL+usVA+<*t@{4vLjl{LWDb zo|&pqvjrKxKO@^83z%ltL2qNnO?K%s6SIt05efdfCYAfi?PgXycHMpRVB>cNHVwfj zF|RcPQ=B+kkKPFv6dOUv2D@F?U+APj@?y3#eAud#s7RtHhN1z5#Ee)H-U{64RLBIi zv7FKF0!2lT@6td)Xy(tiPDmM9Ig6kc61`-iPyjfDt`|147#Nw<=Ht^EfRG96v9)bn zvcre>SVQ}6y0tyDL{uQ8nQWd-B>Mjh%qi=O}@qyt`G^aP=FSVaWse615>n8dH zW_%fWNMhCv+@o%@i;sla3K~=EcbEIoJR*DAAY^<%-$PRE-a4lUI{AdVV9M3M@w2lN z_h60vJCmyxl&WV9NrdRss?tXiG$r6?GKzc3qcRqDXlHPjC0M*B=YVg5?PDR@N7JuZ zxn_-lg4ph@lE^`gk8l1c7Hh(tibLoPS%;|F*OCR`_Q~3Dgka zKHF;=I-JE%BA!#}0e+-!iZoE*yTWq2T#LyTrcW{{=6|G=w3B%JlwNqAy3A;?HF-IB zxWefrVAV20SNYxlK!jp;4J8|S9Sy2Oh6#Dpub+x{3w9l(1+uMdi|;iM>$$5>jgG3x zXK7tf1R0G{EK?X?+$UEiL%Y%rJ*~coCTzr@?H_?C*sO7Z+Doq~0ILY?NK-aw?6rZ^ zKH!ZTPEx$OYL_m)UsU zeJ*(ssk=cqU}9cQ2_(2G?reQc|x0{(PZv7+Fuo{ zdkc?r{jT6&tl!h*#Aof|GJFEiMD^~gNAj*yp%T8Ldux?pntB@hxT>wyOqsCcZq>51Nmxddw)CwEofJ?b<}y2~>8XX?W`k8A z28%*_SUJUPKjU!UB2LF!-$>VijEWJI`O`e`SbxQ{H{Fj>i^(L2h8#xCVn0`Y*yI>I=0-^{*QcIgn3-2{H~f8Ts>=Dm6E z`@WW^Jx;B`;;i+r4)jPa2P@X+w&ySWVv_4{YQWLBy(ed?_O?v>8uv$0e`k{8knSES2~_b+dmm!}*Lj|niVRWx}U?|g0;%1gQ`jht(k*89mrG*{vqJn9l< zRWj-4kvNlYYT()Q%-(z9#nl4|ZcR5IHJ0gg?8A8$Ys>H*@LWiNAD>c+rx` z$@%;N6aLpv-aP-(_`0v=A9^f~GzJ!pUA*gRxGAn+gUzQ89E!XQ7Y8zfQh(*Go3ycb znxn6s;aeR#)vM*1X?;l%t2=14|IA%|-so?`Lt5W|(tReK9_mP-_}>qQva)ywd*dg*o@zD;6BKmK$HVm798JwmZwtDWg0h zX8PLvV)De>$e|_GQtjj5%q2^2riD}YHxU6HE8L8)a$PixCQ7$C5{fOXzr}khkp|RY!d;Sf{0JmFmw{*!q611 zxHD_H7cBWXh}%lAe4%Iro3(Pq%@0eHCW+Is*a{KVIjFNH3y58yheuGKz}B8>Dx21p zdr$E2UXc~jD1@;fjn`tjDvfr)vSz<|J%FKn*l!Th#O5^EZW)C8$kFTh0hzF;D3`O# zZ{apYld3TFSwaKM`z3u74#$z`;Rd)qy9Y+P z@Q^eyc4c1_7j6w1NcM~uU;MQ%`bhEMD?!_vggozm$$2i6V-!K90|!B@7}Fmakh zmL4yWOXb+*1Fm&x<*MrHNCm18s1Ae)+MG3_(xiT15UTYY7D{t4f)fEf3Zc?$ss8)r z>1pDabjH4H58iQlL}2xey@MHuRe)G>yl%Q{$fe0``?)c?3!E$p^2v}k@PT4-Y`G%Y z0cmDLnph@>N)E~YZjR%IEhrulY?%>$oCVMtI8*Hj8$|eM0;LHx>9tL61Loi>%E!N1 zNMnX2%hN&XYGyqRHxzNTrp@+%^)goWEcYPIu*zb+;l*yVJ%u`DFjxm+uuvE^(=LY4 zs5?_6Ogtce`5R0UY)?RNyom4?h76zT!_~s+NW8irpFkejs~)Lr0W^K%)$veOLZ-D! zXhcnF6yk1%&R@I+9XSub6Zb#*pdAKY{)qHsQ9P{+rIJT6iDJ1NmyHT1;Wbpq@pF@D zz@*8~-cJYJm~r|-_t;8=`0ODu|V#{;p+aTm{w z!3PU%-{Sj# simulations = new ArrayList(); - + private ArrayList customExpressions = new ArrayList(); + /* * The undo/redo variables and mechanism are documented in doc/undo-redo-flow.* @@ -103,7 +105,22 @@ public class OpenRocketDocument implements ComponentChangeListener { } - + public void addCustomExpression(CustomExpression expression){ + if (customExpressions.contains(expression)){ + log.user("Could not add custom expression "+expression.getName()+" to document as document alerady has a matching expression."); + } else { + customExpressions.add(expression); + } + } + + public void removeCustomExpression(CustomExpression expression){ + customExpressions.remove(expression); + } + + public ArrayList getCustomExpressions(){ + return customExpressions; + } + public Rocket getRocket() { return rocket; diff --git a/core/src/net/sf/openrocket/document/Simulation.java b/core/src/net/sf/openrocket/document/Simulation.java index c947e23d..162ee20d 100644 --- a/core/src/net/sf/openrocket/document/Simulation.java +++ b/core/src/net/sf/openrocket/document/Simulation.java @@ -13,7 +13,6 @@ import net.sf.openrocket.masscalc.MassCalculator; import net.sf.openrocket.rocketcomponent.Configuration; import net.sf.openrocket.rocketcomponent.Rocket; import net.sf.openrocket.simulation.BasicEventSimulationEngine; -import net.sf.openrocket.simulation.CustomExpression; import net.sf.openrocket.simulation.FlightData; import net.sf.openrocket.simulation.RK4SimulationStepper; import net.sf.openrocket.simulation.SimulationConditions; @@ -72,7 +71,6 @@ public class Simulation implements ChangeSource, Cloneable { private SimulationOptions options; private ArrayList simulationListeners = new ArrayList(); - private ArrayList customExpressions = new ArrayList(); private final Class simulationEngineClass = BasicEventSimulationEngine.class; private Class simulationStepperClass = RK4SimulationStepper.class; @@ -161,21 +159,6 @@ public class Simulation implements ChangeSource, Cloneable { return document; } - public void addCustomExpression(CustomExpression expression){ - this.status = Simulation.Status.OUTDATED; - log.debug("Simulation must be run again to update custom expression."); - customExpressions.add(expression); - } - - public void removeCustomExpression(CustomExpression expression){ - customExpressions.remove(expression); - } - - public ArrayList getCustomExpressions(){ - return customExpressions; - } - - /** * Return the rocket associated with this simulation. * diff --git a/core/src/net/sf/openrocket/file/openrocket/OpenRocketSaver.java b/core/src/net/sf/openrocket/file/openrocket/OpenRocketSaver.java index 9378a455..fa1c372f 100644 --- a/core/src/net/sf/openrocket/file/openrocket/OpenRocketSaver.java +++ b/core/src/net/sf/openrocket/file/openrocket/OpenRocketSaver.java @@ -24,7 +24,7 @@ import net.sf.openrocket.rocketcomponent.RecoveryDevice.DeployEvent; import net.sf.openrocket.rocketcomponent.Rocket; import net.sf.openrocket.rocketcomponent.RocketComponent; import net.sf.openrocket.rocketcomponent.TubeCoupler; -import net.sf.openrocket.simulation.CustomExpression; +import net.sf.openrocket.simulation.customexpression.CustomExpression; import net.sf.openrocket.simulation.FlightData; import net.sf.openrocket.simulation.FlightDataBranch; import net.sf.openrocket.simulation.FlightDataType; @@ -101,6 +101,9 @@ public class OpenRocketSaver extends RocketSaver { writeln(""); + // Save custom expressions; + saveCustomDatatypes(document); + // Save all simulations writeln(""); indent++; @@ -124,7 +127,37 @@ public class OpenRocketSaver extends RocketSaver { } } + /* + * Save all the custom expressions + */ + private void saveCustomDatatypes(OpenRocketDocument doc) throws IOException { + + if (doc.getCustomExpressions().isEmpty()) + return; + + writeln(""); indent++; + + for (CustomExpression exp : doc.getCustomExpressions()){ + saveCustomExpressionDatatype(exp); + } + + indent--; writeln(""); + writeln(""); + } + /* + * Save one custom expression datatype + */ + private void saveCustomExpressionDatatype(CustomExpression exp) throws IOException { + // Write out custom expression + + writeln(""); indent++; + writeln("" + exp.getName() + ""); + writeln("" + exp.getSymbol() + ""); + writeln("" + exp.getUnit() + ""); // auto unit type means it will be determined from string + writeln("" + exp.getExpressionString() + ""); + indent--; writeln(""); + } @Override public long estimateFileSize(OpenRocketDocument doc, StorageOptions options) { @@ -327,20 +360,6 @@ public class OpenRocketSaver extends RocketSaver { writeln("RK4Simulator"); writeln("BarrowmanCalculator"); - // Write out custom expressions - if (!simulation.getCustomExpressions().isEmpty()){ - writeln(""); indent++; - for (CustomExpression expression : simulation.getCustomExpressions()){ - writeln(""); indent++; - writeElement("name", expression.getName()); - writeElement("symbol", expression.getSymbol()); - writeElement("unit", expression.getUnit()); - writeElement("expressionstring", expression.getExpressionString()); - indent--; writeln(""); - } - indent--; writeln(""); - } - writeln(""); indent++; writeElement("configid", cond.getMotorConfigurationID()); @@ -453,13 +472,16 @@ public class OpenRocketSaver extends RocketSaver { sb.append(" 0) sb.append(","); sb.append(escapeXML(types[i].getKey())); } + */ + sb.append("\" types=\""); for (int i = 0; i < types.length; i++) { if (i > 0) diff --git a/core/src/net/sf/openrocket/file/openrocket/importt/OpenRocketLoader.java b/core/src/net/sf/openrocket/file/openrocket/importt/OpenRocketLoader.java index 7ef136b8..b8b34b3b 100644 --- a/core/src/net/sf/openrocket/file/openrocket/importt/OpenRocketLoader.java +++ b/core/src/net/sf/openrocket/file/openrocket/importt/OpenRocketLoader.java @@ -67,7 +67,7 @@ import net.sf.openrocket.rocketcomponent.ThicknessRingComponent; import net.sf.openrocket.rocketcomponent.Transition; import net.sf.openrocket.rocketcomponent.TrapezoidFinSet; import net.sf.openrocket.rocketcomponent.TubeCoupler; -import net.sf.openrocket.simulation.CustomExpression; +import net.sf.openrocket.simulation.customexpression.CustomExpression; import net.sf.openrocket.simulation.FlightData; import net.sf.openrocket.simulation.FlightDataBranch; import net.sf.openrocket.simulation.FlightDataType; @@ -649,6 +649,7 @@ class OpenRocketContentHandler extends AbstractElementHandler { private boolean rocketDefined = false; private boolean simulationsDefined = false; + private boolean datatypesDefined = false; public OpenRocketContentHandler(DocumentLoadingContext context) { this.context = context; @@ -656,7 +657,6 @@ class OpenRocketContentHandler extends AbstractElementHandler { this.doc = new OpenRocketDocument(rocket); } - public OpenRocketDocument getDocument() { if (!rocketDefined) return null; @@ -677,6 +677,15 @@ class OpenRocketContentHandler extends AbstractElementHandler { rocketDefined = true; return new ComponentParameterHandler(rocket, context); } + + if (element.equals("datatypes")){ + if (datatypesDefined) { + warnings.add(Warning.fromString("Multiple datatype blocks. Ignoring later ones.")); + return null; + } + datatypesDefined = true; + return new DatatypeHandler(this, context); + } if (element.equals("simulations")) { if (simulationsDefined) { @@ -697,6 +706,90 @@ class OpenRocketContentHandler extends AbstractElementHandler { +class DatatypeHandler extends AbstractElementHandler { + private final DocumentLoadingContext context; + private final OpenRocketContentHandler contentHandler; + private CustomExpressionHandler customExpressionHandler = null; + + public DatatypeHandler(OpenRocketContentHandler contentHandler, DocumentLoadingContext context) { + this.context = context; + this.contentHandler = contentHandler; + } + + @Override + public ElementHandler openElement(String element, + HashMap attributes, WarningSet warnings) + throws SAXException { + + if (element.equals("type") && attributes.get("source").equals("customexpression") ){ + customExpressionHandler = new CustomExpressionHandler(contentHandler, context); + return customExpressionHandler; + } + else { + warnings.add(Warning.fromString("Unknown datatype " + element + " defined, ignoring")); + } + + return this; + } + + @Override + public void closeElement(String element, HashMap attributes, String content, WarningSet warnings) throws SAXException { + attributes.remove("source"); + super.closeElement(element, attributes, content, warnings); + + if (customExpressionHandler != null){ + contentHandler.getDocument().addCustomExpression(customExpressionHandler.currentExpression); + } + + } + +} + +class CustomExpressionHandler extends AbstractElementHandler{ + private final DocumentLoadingContext context; + private final OpenRocketContentHandler contentHandler; + public CustomExpression currentExpression; + + public CustomExpressionHandler(OpenRocketContentHandler contentHandler, DocumentLoadingContext context) { + this.context = context; + this.contentHandler = contentHandler; + currentExpression = new CustomExpression(contentHandler.getDocument()); + + } + + @Override + public ElementHandler openElement(String element, + HashMap attributes, WarningSet warnings) + throws SAXException { + + return this; + } + + @Override + public void closeElement(String element, HashMap attributes, + String content, WarningSet warnings) throws SAXException { + + if (element.equals("type")) { + contentHandler.getDocument().addCustomExpression(currentExpression); + } + + if (element.equals("name")) { + currentExpression.setName(content); + } + + if (element.equals("symbol")) { + currentExpression.setSymbol(content); + } + + if (element.equals("unit") && attributes.get("unittype").equals("auto")) { + currentExpression.setUnit(content); + } + + if (element.equals("expression")){ + currentExpression.setExpression(content); + } + } +} /** * A handler that creates components from the corresponding elements. The control of the @@ -1211,9 +1304,7 @@ class SingleSimulationHandler extends AbstractElementHandler { private SimulationConditionsHandler conditionHandler; private FlightDataHandler dataHandler; - private CustomExpressionsHandler customExpressionsHandler; - - private ArrayList customExpressions = new ArrayList(); + private final List listeners = new ArrayList(); public SingleSimulationHandler(OpenRocketDocument doc, DocumentLoadingContext context) { @@ -1221,14 +1312,10 @@ class SingleSimulationHandler extends AbstractElementHandler { this.context = context; } - public void setCustomExpressions(ArrayList expressions){ - this.customExpressions = expressions; - } - - public ArrayList getCustomExpressions(){ - return customExpressions; + public OpenRocketDocument getDocument(){ + return doc; } - + @Override public ElementHandler openElement(String element, HashMap attributes, WarningSet warnings) { @@ -1236,9 +1323,6 @@ class SingleSimulationHandler extends AbstractElementHandler { if (element.equals("name") || element.equals("simulator") || element.equals("calculator") || element.equals("listener")) { return PlainTextHandler.INSTANCE; - } else if (element.equals("customexpressions")) { - customExpressionsHandler = new CustomExpressionsHandler(this, context); - return customExpressionsHandler; } else if (element.equals("conditions")) { conditionHandler = new SimulationConditionsHandler(doc.getRocket(), context); return conditionHandler; @@ -1301,70 +1385,11 @@ class SingleSimulationHandler extends AbstractElementHandler { Simulation simulation = new Simulation(doc, doc.getRocket(), status, name, conditions, listeners, data); - - // Note : arraylist implementation in simulation different from standard one - for (CustomExpression exp : customExpressions){ - exp.setSimulation(simulation); - if (exp.checkAll()) - simulation.addCustomExpression(exp); - } - + doc.addSimulation(simulation); } } - -class CustomExpressionsHandler extends AbstractElementHandler { - private final DocumentLoadingContext context; - private final SingleSimulationHandler simHandler; - public CustomExpression currentExpression = new CustomExpression(); - private final ArrayList customExpressions = new ArrayList(); - - - public CustomExpressionsHandler(SingleSimulationHandler simHandler, DocumentLoadingContext context) { - this.context = context; - this.simHandler = simHandler; - } - - @Override - public ElementHandler openElement(String element, - HashMap attributes, WarningSet warnings) - throws SAXException { - - if (element.equals("expression")){ - currentExpression = new CustomExpression(); - } - - return this; - } - - @Override - public void closeElement(String element, HashMap attributes, - String content, WarningSet warnings) { - - if (element.equals("expression")) - customExpressions.add(currentExpression); - - if (element.equals("name")) - currentExpression.setName(content); - - else if (element.equals("symbol")) - currentExpression.setSymbol(content); - - else if (element.equals("unit")) - currentExpression.setUnit(content); - - else if (element.equals("expressionstring")) - currentExpression.setExpression(content); - - } - - @Override - public void endHandler(String element, HashMap attributes, - String content, WarningSet warnings) { - simHandler.setCustomExpressions(customExpressions); - } -} - + class SimulationConditionsHandler extends AbstractElementHandler { private final DocumentLoadingContext context; private SimulationOptions conditions; @@ -1548,7 +1573,7 @@ class FlightDataHandler extends AbstractElementHandler { private FlightDataBranchHandler dataHandler; private WarningSet warningSet = new WarningSet(); private List branches = new ArrayList(); - + private SingleSimulationHandler simHandler; private FlightData data; @@ -1575,9 +1600,8 @@ class FlightDataHandler extends AbstractElementHandler { return null; } dataHandler = new FlightDataBranchHandler( attributes.get("name"), - attributes.get("typekeys"), - attributes.get("types"), - simHandler, context); + attributes.get("types"), + simHandler, context); return dataHandler; } @@ -1673,23 +1697,18 @@ class FlightDataBranchHandler extends AbstractElementHandler { private final DocumentLoadingContext context; private final FlightDataType[] types; private final FlightDataBranch branch; - + private static final LogHelper log = Application.getLogger(); private final SingleSimulationHandler simHandler; - - public FlightDataBranchHandler(String name, String typeKeyList, String typeList, SingleSimulationHandler simHandler, DocumentLoadingContext context) { + + public FlightDataBranchHandler(String name, String typeList, SingleSimulationHandler simHandler, DocumentLoadingContext context) { this.simHandler = simHandler; this.context = context; - String[] typeNames = typeList.split(","); - String[] typeKeys = null; - if ( typeKeyList != null ) { - typeKeys = typeKeyList.split(","); - } - types = new FlightDataType[typeNames.length]; - for (int i = 0; i < typeNames.length; i++) { - String typeName = typeNames[i]; - String typeKey = (typeKeys != null ) ? typeKeys[i] : null ; - FlightDataType matching = findFlightDataType(typeKey, typeName); + String[] split = typeList.split(","); + types = new FlightDataType[split.length]; + for (int i = 0; i < split.length; i++) { + String typeName = split[i]; + FlightDataType matching = findFlightDataType(typeName); types[i] = matching; //types[i] = FlightDataType.getType(typeName, matching.getSymbol(), matching.getUnitGroup()); } @@ -1697,13 +1716,14 @@ class FlightDataBranchHandler extends AbstractElementHandler { // TODO: LOW: May throw an IllegalArgumentException branch = new FlightDataBranch(name, types); } - + // Find the full flight data type given name only // Note: this way of doing it requires that custom expressions always come before flight data in the file, // not the nicest but this is always the case anyway. - private FlightDataType findFlightDataType(String key, String name){ - - // Look in built in types by key. + private FlightDataType findFlightDataType(String name){ + + // Kevins version with lookup by key. Not using right now + /* if ( key != null ) { for (FlightDataType t : FlightDataType.ALL_TYPES){ if (t.getKey().equals(key) ){ @@ -1711,33 +1731,22 @@ class FlightDataBranchHandler extends AbstractElementHandler { } } } - // Look in built in types by name. + */ + + // Look in built in types for (FlightDataType t : FlightDataType.ALL_TYPES){ if (t.getName().equals(name) ){ return t; } } - + // Look in custom expressions - for (CustomExpression exp : simHandler.getCustomExpressions()){ - if (exp.getName().equals(name) ){ - return exp.getType(); - } - } - - // Look in custom expressions, meanwhile set priority based on order in file - /* - int totalExpressions = simHandler.getCustomExpressions().size(); - for (int i=0; i attributes, - WarningSet warnings) { - // FIXME - probably need more data in the warning messages - like what component preset... - String manufacturerName = attributes.get("manufacturer"); - if ( manufacturerName == null ) { - warnings.add(Warning.fromString("Invalid ComponentPreset, no manufacturer specified. Ignored")); - return; - } +@Override +public void set(RocketComponent c, String name, HashMap attributes, + WarningSet warnings) { + // FIXME - probably need more data in the warning messages - like what component preset... + String manufacturerName = attributes.get("manufacturer"); + if ( manufacturerName == null ) { + warnings.add(Warning.fromString("Invalid ComponentPreset, no manufacturer specified. Ignored")); + return; + } - String productNo = attributes.get("partno"); - if ( productNo == null ) { - warnings.add(Warning.fromString("Invalid ComponentPreset, no partno specified. Ignored")); - return; - } + String productNo = attributes.get("partno"); + if ( productNo == null ) { + warnings.add(Warning.fromString("Invalid ComponentPreset, no partno specified. Ignored")); + return; + } - String digest = attributes.get("digest"); - if ( digest == null ) { - warnings.add(Warning.fromString("Invalid ComponentPreset, no digest specified.")); - } + String digest = attributes.get("digest"); + if ( digest == null ) { + warnings.add(Warning.fromString("Invalid ComponentPreset, no digest specified.")); + } - String type = attributes.get("type"); - if ( type == null ) { - warnings.add(Warning.fromString("Invalid ComponentPreset, no type specified.")); - } + String type = attributes.get("type"); + if ( type == null ) { + warnings.add(Warning.fromString("Invalid ComponentPreset, no type specified.")); + } - List presets = Application.getComponentPresetDao().find( manufacturerName, productNo ); + List presets = Application.getComponentPresetDao().find( manufacturerName, productNo ); - ComponentPreset matchingPreset = null; + ComponentPreset matchingPreset = null; - for( ComponentPreset preset: presets ) { - if ( digest != null && preset.getDigest().equals(digest) ) { - // Found one with matching digest. Take it. - matchingPreset = preset; - break; - } - if ( type != null && preset.getType().name().equals(type) && matchingPreset != null) { - // Found the first one with matching type. - matchingPreset = preset; - } + for( ComponentPreset preset: presets ) { + if ( digest != null && preset.getDigest().equals(digest) ) { + // Found one with matching digest. Take it. + matchingPreset = preset; + break; } - - // Was any found? - if ( matchingPreset == null ) { - warnings.add(Warning.fromString("No matching ComponentPreset found " + manufacturerName + " " + productNo)); - return; + if ( type != null && preset.getType().name().equals(type) && matchingPreset != null) { + // Found the first one with matching type. + matchingPreset = preset; } + } - if ( digest != null && !matchingPreset.getDigest().equals(digest) ) { - warnings.add(Warning.fromString("ComponentPreset has wrong digest")); - } + // Was any found? + if ( matchingPreset == null ) { + warnings.add(Warning.fromString("No matching ComponentPreset found " + manufacturerName + " " + productNo)); + return; + } - setMethod.invoke(c, matchingPreset); + if ( digest != null && !matchingPreset.getDigest().equals(digest) ) { + warnings.add(Warning.fromString("ComponentPreset has wrong digest")); } + + setMethod.invoke(c, matchingPreset); +} } ////MaterialSetter - sets a Material value class MaterialSetter implements Setter { - private final Reflection.Method setMethod; - private final Material.Type type; +private final Reflection.Method setMethod; +private final Material.Type type; - public MaterialSetter(Reflection.Method set, Material.Type type) { - this.setMethod = set; - this.type = type; - } +public MaterialSetter(Reflection.Method set, Material.Type type) { + this.setMethod = set; + this.type = type; +} - @Override - public void set(RocketComponent c, String name, HashMap attributes, - WarningSet warnings) { +@Override +public void set(RocketComponent c, String name, HashMap attributes, + WarningSet warnings) { - Material mat; + Material mat; - // Check name != "" - name = name.trim(); - if (name.equals("")) { - warnings.add(Warning.fromString("Illegal material specification, ignoring.")); - return; - } + // Check name != "" + name = name.trim(); + if (name.equals("")) { + warnings.add(Warning.fromString("Illegal material specification, ignoring.")); + return; + } - // Parse density - double density; - String str; - str = attributes.remove("density"); - if (str == null) { - warnings.add(Warning.fromString("Illegal material specification, ignoring.")); - return; - } - try { - density = Double.parseDouble(str); - } catch (NumberFormatException e) { - warnings.add(Warning.fromString("Illegal material specification, ignoring.")); - return; - } + // Parse density + double density; + String str; + str = attributes.remove("density"); + if (str == null) { + warnings.add(Warning.fromString("Illegal material specification, ignoring.")); + return; + } + try { + density = Double.parseDouble(str); + } catch (NumberFormatException e) { + warnings.add(Warning.fromString("Illegal material specification, ignoring.")); + return; + } - // Parse thickness - // double thickness = 0; - // str = attributes.remove("thickness"); - // try { - // if (str != null) - // thickness = Double.parseDouble(str); - // } catch (NumberFormatException e){ - // warnings.add(Warning.fromString("Illegal material specification, ignoring.")); - // return; - // } - - // Check type if specified - str = attributes.remove("type"); - if (str != null && !type.name().toLowerCase(Locale.ENGLISH).equals(str)) { - warnings.add(Warning.fromString("Illegal material type specified, ignoring.")); - return; - } + // Parse thickness + // double thickness = 0; + // str = attributes.remove("thickness"); + // try { + // if (str != null) + // thickness = Double.parseDouble(str); + // } catch (NumberFormatException e){ + // warnings.add(Warning.fromString("Illegal material specification, ignoring.")); + // return; + // } - String key = attributes.remove("key"); + // Check type if specified + str = attributes.remove("type"); + if (str != null && !type.name().toLowerCase(Locale.ENGLISH).equals(str)) { + warnings.add(Warning.fromString("Illegal material type specified, ignoring.")); + return; + } - mat = Databases.findMaterial(type, key, name, density); + String key = attributes.remove("key"); - setMethod.invoke(c, mat); - } + mat = Databases.findMaterial(type, key, name, density); + + setMethod.invoke(c, mat); } +} + diff --git a/core/src/net/sf/openrocket/file/openrocket/savers/RocketSaver.java b/core/src/net/sf/openrocket/file/openrocket/savers/RocketSaver.java index 44d5599d..be7c52af 100644 --- a/core/src/net/sf/openrocket/file/openrocket/savers/RocketSaver.java +++ b/core/src/net/sf/openrocket/file/openrocket/savers/RocketSaver.java @@ -7,7 +7,6 @@ import java.util.Locale; import net.sf.openrocket.rocketcomponent.ReferenceType; import net.sf.openrocket.rocketcomponent.Rocket; - public class RocketSaver extends RocketComponentSaver { private static final RocketSaver instance = new RocketSaver(); @@ -22,8 +21,6 @@ public class RocketSaver extends RocketComponentSaver { return list; } - - @Override protected void addParams(net.sf.openrocket.rocketcomponent.RocketComponent c, List elements) { super.addParams(c, elements); diff --git a/core/src/net/sf/openrocket/gui/customexpression/CustomExpressionDialog.java b/core/src/net/sf/openrocket/gui/customexpression/CustomExpressionDialog.java new file mode 100644 index 00000000..b8dee9fb --- /dev/null +++ b/core/src/net/sf/openrocket/gui/customexpression/CustomExpressionDialog.java @@ -0,0 +1,35 @@ +package net.sf.openrocket.gui.customexpression; + +import java.awt.Window; + +import javax.swing.BorderFactory; +import javax.swing.JDialog; +import javax.swing.JPanel; + +import net.sf.openrocket.document.OpenRocketDocument; +import net.sf.openrocket.document.Simulation; +import net.sf.openrocket.gui.util.GUIUtil; +import net.sf.openrocket.l10n.Translator; +import net.sf.openrocket.logging.LogHelper; +import net.sf.openrocket.rocketcomponent.Rocket; +import net.sf.openrocket.startup.Application; + +public class CustomExpressionDialog extends JDialog { + private static final Translator trans = Application.getTranslator(); + private static final LogHelper log = Application.getLogger(); + + private final Window parentWindow; + private final OpenRocketDocument doc; + + public CustomExpressionDialog(OpenRocketDocument doc, Window parent){ + super(parent, trans.get("customExpressionPanel.lbl.CustomExpressions")); + + this.doc = doc; + this.parentWindow = parent; + + JPanel panel = new CustomExpressionPanel(doc, this); + this.add( panel ); + + GUIUtil.setDisposableDialogOptions(this, null); + } +} diff --git a/core/src/net/sf/openrocket/gui/customexpression/CustomExpressionPanel.java b/core/src/net/sf/openrocket/gui/customexpression/CustomExpressionPanel.java index cf35cbb4..ec60f250 100644 --- a/core/src/net/sf/openrocket/gui/customexpression/CustomExpressionPanel.java +++ b/core/src/net/sf/openrocket/gui/customexpression/CustomExpressionPanel.java @@ -4,17 +4,28 @@ import java.awt.Color; import java.awt.Window; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; +import java.io.File; import java.util.ArrayList; import java.util.Collections; import javax.swing.BorderFactory; import javax.swing.JButton; +import javax.swing.JDialog; +import javax.swing.JFileChooser; import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.SwingUtilities; +import javax.swing.border.Border; +import javax.swing.filechooser.FileFilter; +import javax.swing.filechooser.FileNameExtensionFilter; import net.miginfocom.swing.MigLayout; +import net.sf.openrocket.document.OpenRocketDocument; +import net.sf.openrocket.file.DatabaseMotorFinder; +import net.sf.openrocket.file.GeneralRocketLoader; +import net.sf.openrocket.file.MotorFinder; +import net.sf.openrocket.file.RocketLoadException; import net.sf.openrocket.document.Simulation; import net.sf.openrocket.gui.components.DescriptionArea; import net.sf.openrocket.gui.components.UnitSelector; @@ -22,7 +33,8 @@ import net.sf.openrocket.gui.customexpression.ExpressionBuilderDialog; import net.sf.openrocket.gui.util.Icons; import net.sf.openrocket.l10n.Translator; import net.sf.openrocket.logging.LogHelper; -import net.sf.openrocket.simulation.CustomExpression; +import net.sf.openrocket.rocketcomponent.Rocket; +import net.sf.openrocket.simulation.customexpression.CustomExpression; import net.sf.openrocket.startup.Application; public class CustomExpressionPanel extends JPanel { @@ -31,34 +43,91 @@ public class CustomExpressionPanel extends JPanel { private static final Translator trans = Application.getTranslator(); private JPanel expressionSelectorPanel; - private Simulation simulation; + private OpenRocketDocument doc; - public CustomExpressionPanel(final Simulation simulation) { + public CustomExpressionPanel(final OpenRocketDocument doc, final JDialog parentDialog) { super(new MigLayout("fill")); - this.simulation = simulation; + this.doc = doc; expressionSelectorPanel = new JPanel(new MigLayout("gapy rel")); - JScrollPane scroll = new JScrollPane(expressionSelectorPanel); - this.add(scroll, "spany 2, height 10px, wmin 400lp, grow 100, gapright para"); + expressionSelectorPanel.setToolTipText(trans.get("customExpressionPanel.lbl.CalcNote")); - DescriptionArea desc = new DescriptionArea(trans.get("customExpressionPanel.lbl.UpdateNote")+"\n\n"+trans.get("customExpressionPanel.lbl.CalcNote"), 8, -2f); - desc.setViewportBorder(BorderFactory.createEmptyBorder()); - this.add(desc, "width 1px, growx 1, wrap unrel"); + JScrollPane scroll = new JScrollPane(); + Border bdr = BorderFactory.createTitledBorder(trans.get("customExpressionPanel.lbl.CustomExpressions")); + + expressionSelectorPanel.setBorder(bdr); + expressionSelectorPanel.add(scroll); + + //this.add(expressionSelectorPanel, "spany 1, height 10px, wmin 600lp, grow 100, gapright para"); + this.add(expressionSelectorPanel, "hmin 200lp, wmin 700lp, grow 100, wrap"); + + //DescriptionArea desc = new DescriptionArea(trans.get("customExpressionPanel.lbl.UpdateNote")+"\n\n"+trans.get("customExpressionPanel.lbl.CalcNote"), 8, -2f); + //desc.setViewportBorder(BorderFactory.createEmptyBorder()); + //this.add(desc, "width 1px, growx 1, wrap unrel, wrap"); //// New expression JButton button = new JButton(trans.get("customExpressionPanel.but.NewExpression")); + button.setToolTipText(trans.get("customExpressionPanel.but.ttip.NewExpression")); button.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { // Open window to configure expression - log.debug("Opening window to configure new expression"); + log.info("Opening window to configure new expression"); Window parent = SwingUtilities.getWindowAncestor(CustomExpressionPanel.this); - new ExpressionBuilderDialog(parent, simulation).setVisible(true); + new ExpressionBuilderDialog(parent, doc).setVisible(true); updateExpressions(); } }); + this.add(button, "split 4, width :100:200"); + + //// Import + final JButton importButton = new JButton(trans.get("customExpressionPanel.but.Import")); + importButton.setToolTipText(trans.get("customExpressionPanel.but.ttip.Import")); + importButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + + //Create a file chooser + final JFileChooser fc = new JFileChooser(); + if (doc.getFile() != null){ + fc.setCurrentDirectory(doc.getFile().getParentFile()); + } + fc.setFileFilter(new FileNameExtensionFilter("Openrocket file", "ork")); + fc.setAcceptAllFileFilterUsed(false); - this.add(button, "left"); + int returnVal = fc.showOpenDialog(CustomExpressionPanel.this); + if (returnVal == JFileChooser.APPROVE_OPTION){ + File importFile = fc.getSelectedFile(); + log.info("User selected a file to import expressions from "+fc.getSelectedFile().toString()); + + //TODO: This should probably be somewhere else and ideally we would use an alternative minimal rocket loader. Still, it doesn't seem particularly slow this way. + + // Load expressions from selected document + GeneralRocketLoader loader = new GeneralRocketLoader(); + try { + OpenRocketDocument importedDocument = loader.load(importFile, new DatabaseMotorFinder()); + for (CustomExpression exp : importedDocument.getCustomExpressions()){ + doc.addCustomExpression(exp); + } + } catch (RocketLoadException e1) { + log.user("Error opening document to import expressions from."); + } + updateExpressions(); + } + } + }); + this.add(importButton, "width :100:200"); + + //// Close button + final JButton closeButton = new JButton(trans.get("dlg.but.close")); + closeButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + parentDialog.dispose(); + } + }); + this.add(new JPanel(), "growx"); + this.add(closeButton, "width :100:200"); updateExpressions(); } @@ -69,19 +138,18 @@ public class CustomExpressionPanel extends JPanel { private void updateExpressions(){ expressionSelectorPanel.removeAll(); - int totalExpressions = simulation.getCustomExpressions().size(); + int totalExpressions = doc.getCustomExpressions().size(); for (int i=0; i expressions = simulation.getCustomExpressions(); + ArrayList expressions = doc.getCustomExpressions(); int i = expressions.indexOf(expression); if (i+move == expressions.size() || i+move < 0) return; @@ -128,6 +196,9 @@ public class CustomExpressionPanel extends JPanel { JLabel unitLabel = new JLabel( trans.get("customExpression.Units")+ " :"); UnitSelector unitSelector = new UnitSelector(expression.getType().getUnitGroup()); + //JLabel unitSelector = new JLabel ( expression.getUnit() ); + //unitSelector = setLabelStyle(unitSelector); + //unitSelector.setBackground(Color.WHITE); JButton editButton = new JButton(Icons.EDIT); editButton.setToolTipText(trans.get("customExpression.Units.but.ttip.Edit")); @@ -136,7 +207,7 @@ public class CustomExpressionPanel extends JPanel { @Override public void actionPerformed(ActionEvent e){ Window parent = SwingUtilities.getWindowAncestor(CustomExpressionPanel.this); - new ExpressionBuilderDialog(parent, expression.getSimulation(), expression).setVisible(true); + new ExpressionBuilderDialog(parent, doc, expression).setVisible(true); updateExpressions(); } }); diff --git a/core/src/net/sf/openrocket/gui/customexpression/ExpressionBuilderDialog.java b/core/src/net/sf/openrocket/gui/customexpression/ExpressionBuilderDialog.java index 77fa3a4b..615fdc14 100644 --- a/core/src/net/sf/openrocket/gui/customexpression/ExpressionBuilderDialog.java +++ b/core/src/net/sf/openrocket/gui/customexpression/ExpressionBuilderDialog.java @@ -19,11 +19,13 @@ import javax.swing.SwingConstants; import javax.swing.SwingUtilities; import net.miginfocom.swing.MigLayout; +import net.sf.openrocket.document.OpenRocketDocument; import net.sf.openrocket.document.Simulation; import net.sf.openrocket.gui.util.Icons; import net.sf.openrocket.l10n.Translator; import net.sf.openrocket.logging.LogHelper; -import net.sf.openrocket.simulation.CustomExpression; +import net.sf.openrocket.rocketcomponent.Rocket; +import net.sf.openrocket.simulation.customexpression.CustomExpression; import net.sf.openrocket.startup.Application; /** @@ -44,7 +46,7 @@ public class ExpressionBuilderDialog extends JDialog { private CustomExpression previousExpressionCopy; private final Window parentWindow; - private final Simulation simulation; + private final OpenRocketDocument doc; // Define these check indicators to show if fields are OK private final JLabel nameCheck = new JLabel(RedIcon); @@ -53,16 +55,16 @@ public class ExpressionBuilderDialog extends JDialog { private final JButton okButton = new JButton(trans.get("dlg.but.ok")); private final JTextField expressionField = new JTextField(20); - public ExpressionBuilderDialog(Window parent, Simulation simulation){ - this(parent, simulation, new CustomExpression(simulation)); + public ExpressionBuilderDialog(Window parent, OpenRocketDocument doc){ + this(parent, doc, new CustomExpression(doc)); } - public ExpressionBuilderDialog(Window parent, final Simulation simulation, final CustomExpression previousExpression){ + public ExpressionBuilderDialog(Window parent, final OpenRocketDocument doc, final CustomExpression previousExpression){ super(parent, trans.get("ExpressionBuilderDialog.title"), JDialog.ModalityType.DOCUMENT_MODAL); + this.doc = doc; this.parentWindow = parent; - this.simulation = simulation; this.previousExpressionCopy = (CustomExpression) previousExpression.clone(); this.expression = previousExpression; @@ -159,7 +161,7 @@ public class ExpressionBuilderDialog extends JDialog { public void actionPerformed(ActionEvent e) { log.debug("Opening insert variable window"); Window parentWindow = SwingUtilities.getWindowAncestor(ExpressionBuilderDialog.this); - new VariableSelector(parentWindow, ExpressionBuilderDialog.this, simulation).setVisible(true); + new VariableSelector(parentWindow, ExpressionBuilderDialog.this, doc).setVisible(true); } }); @@ -174,21 +176,13 @@ public class ExpressionBuilderDialog extends JDialog { } }); - //// Copy expression check box - final JCheckBox copyCheckBox = new JCheckBox(trans.get("ExpressionBuilderDialog.CopyToOtherSimulations")); - copyCheckBox.setHorizontalTextPosition(SwingConstants.LEFT); - copyCheckBox.setToolTipText(trans.get("ExpressionBuilderDialog.CopyToOtherSimulations.ttip")); - //// OK Button okButton.setEnabled(false); okButton.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { // add to this simulation - expression.addToSimulation(); - if (copyCheckBox.isSelected()){ - expression.copyToOtherSimulations(); - } + expression.addToDocument(); // close window ExpressionBuilderDialog.this.dispose(); @@ -226,7 +220,6 @@ public class ExpressionBuilderDialog extends JDialog { mainPanel.add(expressionCheck, "wrap, center"); mainPanel.add(insertOperatorButton, "span 2, right, split 2"); mainPanel.add(insertVariableButton, "right, wrap"); - mainPanel.add(copyCheckBox, "span 2, right, wrap"); mainPanel.add(cancelButton, "span 2, right, width :50:100"); mainPanel.add(okButton, "right, width :50:100, wrap"); diff --git a/core/src/net/sf/openrocket/gui/customexpression/OperatorSelector.java b/core/src/net/sf/openrocket/gui/customexpression/OperatorSelector.java index bc1f8066..49f48657 100644 --- a/core/src/net/sf/openrocket/gui/customexpression/OperatorSelector.java +++ b/core/src/net/sf/openrocket/gui/customexpression/OperatorSelector.java @@ -1,14 +1,24 @@ package net.sf.openrocket.gui.customexpression; +import java.awt.Point; import java.awt.Window; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; +import java.awt.event.KeyEvent; +import java.awt.event.MouseEvent; +import java.awt.event.MouseListener; +import java.awt.event.MouseMotionAdapter; +import javax.swing.AbstractAction; +import javax.swing.ActionMap; +import javax.swing.InputMap; import javax.swing.JButton; +import javax.swing.JComponent; import javax.swing.JDialog; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.JTable; +import javax.swing.KeyStroke; import javax.swing.event.ListSelectionEvent; import javax.swing.event.ListSelectionListener; @@ -16,6 +26,7 @@ import net.miginfocom.swing.MigLayout; import net.sf.openrocket.l10n.Translator; import net.sf.openrocket.logging.LogHelper; import net.sf.openrocket.startup.Application; +import net.sf.openrocket.util.TextUtil; public class OperatorSelector extends JDialog { @@ -24,24 +35,76 @@ public class OperatorSelector extends JDialog { private final Window parentWindow; + private final JTable table; + private final OperatorTableModel tableModel; + private final ExpressionBuilderDialog parentBuilder; + public OperatorSelector(Window parent, final ExpressionBuilderDialog parentBuilder){ super(parent, trans.get("CustomOperatorSelector.title"), JDialog.ModalityType.DOCUMENT_MODAL); this.parentWindow = parent; + this.parentBuilder = parentBuilder; final JButton insertButton = new JButton(trans.get("ExpressionBuilderDialog.InsertOperator")); JPanel mainPanel = new JPanel(new MigLayout()); //// Table of variables and model - final OperatorTableModel tableModel = new OperatorTableModel(); - final JTable table = new JTable(tableModel); + tableModel = new OperatorTableModel(); + table = new JTable(tableModel); table.setSelectionMode(javax.swing.ListSelectionModel.SINGLE_SELECTION); int width = table.getColumnModel().getTotalColumnWidth(); - table.getColumnModel().getColumn(0).setPreferredWidth( (int) (.2 * width)); - table.getColumnModel().getColumn(1).setPreferredWidth( (int) (.8 * width)); + table.getColumnModel().getColumn(0).setPreferredWidth( (int) (.1 * width)); + table.getColumnModel().getColumn(1).setPreferredWidth( (int) (.9 * width)); + table.setAutoCreateRowSorter(true); + + table.addMouseMotionListener(new MouseMotionAdapter(){ + @Override + public void mouseMoved(MouseEvent e){ + Point p = e.getPoint(); + int row = table.rowAtPoint(p); + int col = table.columnAtPoint(p); + if (col == 1){ + String description = String.valueOf(table.getValueAt(row, 1)); + description = TextUtil.wrap(description, 60); + table.setToolTipText(description); + } else { + table.setToolTipText(null); + } + } + }); + + table.addMouseListener(new MouseListener(){ + @Override + public void mouseClicked(MouseEvent e){ + if (e.getClickCount() == 2){ + log.debug("Selected operator by double clicking."); + selectOperator(); + } + } + @Override + public void mouseEntered(MouseEvent e) {} + @Override + public void mouseExited(MouseEvent e) {} + @Override + public void mousePressed(MouseEvent e) {} + @Override + public void mouseReleased(MouseEvent e) {} + } ); + + InputMap inputMap = table.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT); + ActionMap actionMap = table.getActionMap(); + KeyStroke enter = KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0); + inputMap.put(enter, "select"); + actionMap.put("select", new AbstractAction(){ + @Override + public void actionPerformed(ActionEvent arg0) { + log.debug("Selected operator by enter key"); + selectOperator(); + } + }); JScrollPane scrollPane = new JScrollPane(table); table.setFillsViewportHeight(true); @@ -57,7 +120,7 @@ public class OperatorSelector extends JDialog { } }); - mainPanel.add(scrollPane, "wrap"); + mainPanel.add(scrollPane, "wrap, push, grow"); //// Cancel button final JButton cancelButton = new JButton(trans.get("dlg.but.cancel")); @@ -73,10 +136,7 @@ public class OperatorSelector extends JDialog { insertButton.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { - int row = table.getSelectedRow(); - String str = tableModel.getOperatorAt(row); - parentBuilder.pasteIntoExpression(str); - OperatorSelector.this.dispose(); + selectOperator(); } }); insertButton.setEnabled(false); // disabled by default, only enable when a variable selected @@ -87,4 +147,11 @@ public class OperatorSelector extends JDialog { this.pack(); this.setLocationByPlatform(true); } + + private void selectOperator(){ + int row = table.getSelectedRow(); + String str = tableModel.getOperatorAt(row); + parentBuilder.pasteIntoExpression(str); + OperatorSelector.this.dispose(); + } } diff --git a/core/src/net/sf/openrocket/gui/customexpression/OperatorTableModel.java b/core/src/net/sf/openrocket/gui/customexpression/OperatorTableModel.java index 76a1a8f2..3b084da5 100644 --- a/core/src/net/sf/openrocket/gui/customexpression/OperatorTableModel.java +++ b/core/src/net/sf/openrocket/gui/customexpression/OperatorTableModel.java @@ -3,7 +3,7 @@ package net.sf.openrocket.gui.customexpression; import javax.swing.table.AbstractTableModel; import net.sf.openrocket.l10n.Translator; -import net.sf.openrocket.simulation.CustomExpression; +import net.sf.openrocket.simulation.customexpression.Functions; import net.sf.openrocket.startup.Application; public class OperatorTableModel extends AbstractTableModel { @@ -12,8 +12,8 @@ public class OperatorTableModel extends AbstractTableModel { private static final String[] columnNames = {trans.get("customExpression.Operator"), trans.get("customExpression.Description")}; - private final Object[] operators = CustomExpression.AVAILABLE_OPERATORS.keySet().toArray(); - private final Object[] descriptions = CustomExpression.AVAILABLE_OPERATORS.values().toArray(); + private final Object[] operators = Functions.AVAILABLE_OPERATORS.keySet().toArray(); + private final Object[] descriptions = Functions.AVAILABLE_OPERATORS.values().toArray(); public OperatorTableModel(){ @@ -26,7 +26,7 @@ public class OperatorTableModel extends AbstractTableModel { @Override public int getRowCount() { - return CustomExpression.AVAILABLE_OPERATORS.size(); + return Functions.AVAILABLE_OPERATORS.size(); } @Override diff --git a/core/src/net/sf/openrocket/gui/customexpression/VariableSelector.java b/core/src/net/sf/openrocket/gui/customexpression/VariableSelector.java index d432a18c..5e0cfe9c 100644 --- a/core/src/net/sf/openrocket/gui/customexpression/VariableSelector.java +++ b/core/src/net/sf/openrocket/gui/customexpression/VariableSelector.java @@ -3,19 +3,31 @@ package net.sf.openrocket.gui.customexpression; import java.awt.Window; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; +import java.awt.event.KeyEvent; +import java.awt.event.KeyListener; +import java.awt.event.MouseEvent; +import java.awt.event.MouseListener; +import javax.swing.AbstractAction; +import javax.swing.ActionMap; +import javax.swing.InputMap; import javax.swing.JButton; +import javax.swing.JComponent; import javax.swing.JDialog; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.JTable; +import javax.swing.KeyStroke; import javax.swing.event.ListSelectionEvent; import javax.swing.event.ListSelectionListener; +import javax.swing.table.JTableHeader; import net.miginfocom.swing.MigLayout; +import net.sf.openrocket.document.OpenRocketDocument; import net.sf.openrocket.document.Simulation; import net.sf.openrocket.l10n.Translator; import net.sf.openrocket.logging.LogHelper; +import net.sf.openrocket.rocketcomponent.Rocket; import net.sf.openrocket.startup.Application; /** @@ -25,50 +37,82 @@ import net.sf.openrocket.startup.Application; */ public class VariableSelector extends JDialog { - + private static final Translator trans = Application.getTranslator(); private static final LogHelper log = Application.getLogger(); - - private final Window parentWindow; - private final Simulation simulation; - public VariableSelector(Window parent, final ExpressionBuilderDialog parentBuilder, final Simulation simulation){ - + private final JTable table; + private final VariableTableModel tableModel; + private final ExpressionBuilderDialog parentBuilder; + + public VariableSelector(Window parent, final ExpressionBuilderDialog parentBuilder, final OpenRocketDocument doc){ + super(parent, trans.get("CustomVariableSelector.title"), JDialog.ModalityType.DOCUMENT_MODAL); - - this.parentWindow = parent; - this.simulation = simulation; - + + this.parentBuilder = parentBuilder; final JButton insertButton = new JButton(trans.get("ExpressionBuilderDialog.InsertVariable")); - + JPanel mainPanel = new JPanel(new MigLayout()); - + //// Table of variables and model - final VariableTableModel tableModel = new VariableTableModel(simulation); - final JTable table = new JTable(tableModel); + tableModel = new VariableTableModel(doc); + table = new JTable(tableModel); + table.setAutoResizeMode(JTable.AUTO_RESIZE_ALL_COLUMNS); table.setSelectionMode(javax.swing.ListSelectionModel.SINGLE_SELECTION); int width = table.getColumnModel().getTotalColumnWidth(); table.getColumnModel().getColumn(0).setPreferredWidth( (int) (.7 * width)); table.getColumnModel().getColumn(1).setPreferredWidth( (int) (.15 * width)); table.getColumnModel().getColumn(2).setPreferredWidth( (int) (.15 * width)); + table.setAutoCreateRowSorter(true); + + table.addMouseListener(new MouseListener(){ + @Override + public void mouseClicked(MouseEvent e){ + if (e.getClickCount() == 2){ + log.debug("Selected variable by double clicking."); + selectVariable(); + } + } + @Override + public void mouseEntered(MouseEvent e) {} + @Override + public void mouseExited(MouseEvent e) {} + @Override + public void mousePressed(MouseEvent e) {} + @Override + public void mouseReleased(MouseEvent e) {} + } ); + + InputMap inputMap = table.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT); + ActionMap actionMap = table.getActionMap(); + KeyStroke enter = KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0); + inputMap.put(enter, "select"); + actionMap.put("select", new AbstractAction(){ + @Override + public void actionPerformed(ActionEvent arg0) { + log.debug("Selected variable by enter key"); + selectVariable(); + } + }); + JScrollPane scrollPane = new JScrollPane(table); table.setFillsViewportHeight(true); table.getSelectionModel().addListSelectionListener(new ListSelectionListener() { - @Override - public void valueChanged(ListSelectionEvent e){ - if (table.getSelectedRowCount() == 1){ - insertButton.setEnabled(true); - } - else { - insertButton.setEnabled(false); - } + @Override + public void valueChanged(ListSelectionEvent e){ + if (table.getSelectedRowCount() == 1){ + insertButton.setEnabled(true); } - }); - - mainPanel.add(scrollPane, "wrap"); - + else { + insertButton.setEnabled(false); + } + } + }); + + mainPanel.add(scrollPane, "wrap, push, grow"); + //// Cancel button final JButton cancelButton = new JButton(trans.get("dlg.but.cancel")); cancelButton.addActionListener(new ActionListener() { @@ -78,15 +122,12 @@ public class VariableSelector extends JDialog { } }); mainPanel.add(cancelButton, "right, width :100:200, split 2"); - + //// Insert button insertButton.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { - int row = table.getSelectedRow(); - String str = tableModel.getSymbolAt(row); - parentBuilder.pasteIntoExpression(str); - VariableSelector.this.dispose(); + selectVariable(); } }); insertButton.setEnabled(false); // disabled by default, only enable when a variable selected @@ -97,4 +138,12 @@ public class VariableSelector extends JDialog { this.pack(); this.setLocationByPlatform(true); } + + private void selectVariable(){ + int row = table.getSelectedRow(); + String str = tableModel.getSymbolAt(row); + parentBuilder.pasteIntoExpression(str); + VariableSelector.this.dispose(); + } + } diff --git a/core/src/net/sf/openrocket/gui/customexpression/VariableTableModel.java b/core/src/net/sf/openrocket/gui/customexpression/VariableTableModel.java index b81a6b05..950828ad 100644 --- a/core/src/net/sf/openrocket/gui/customexpression/VariableTableModel.java +++ b/core/src/net/sf/openrocket/gui/customexpression/VariableTableModel.java @@ -3,15 +3,20 @@ */ package net.sf.openrocket.gui.customexpression; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; +import java.util.Vector; +import javax.swing.JTable; import javax.swing.table.AbstractTableModel; +import javax.swing.table.TableColumn; +import javax.swing.table.TableColumnModel; -import net.sf.openrocket.document.Simulation; +import net.sf.openrocket.document.OpenRocketDocument; import net.sf.openrocket.l10n.Translator; -import net.sf.openrocket.simulation.CustomExpression; +import net.sf.openrocket.simulation.customexpression.CustomExpression; import net.sf.openrocket.simulation.FlightDataType; import net.sf.openrocket.startup.Application; @@ -29,14 +34,13 @@ public class VariableTableModel extends AbstractTableModel { /* * Table model will be constructed with all the built in variables and any custom variables defined */ - public VariableTableModel(Simulation sim){ + public VariableTableModel(OpenRocketDocument doc){ Collections.addAll(types, FlightDataType.ALL_TYPES); - for (CustomExpression expression : sim.getCustomExpressions()){ + for (CustomExpression expression : doc.getCustomExpressions()){ types.add(expression.getType()); } - } @Override @@ -56,7 +60,7 @@ public class VariableTableModel extends AbstractTableModel { else if (col == 1) return types.get(row).getSymbol(); else if (col == 2) - return types.get(row).getUnitGroup().getDefaultUnit().toString(); + return types.get(row).getUnitGroup().getSIUnit().toString(); return null; } diff --git a/core/src/net/sf/openrocket/gui/main/BasicFrame.java b/core/src/net/sf/openrocket/gui/main/BasicFrame.java index 9f758c34..ab617482 100644 --- a/core/src/net/sf/openrocket/gui/main/BasicFrame.java +++ b/core/src/net/sf/openrocket/gui/main/BasicFrame.java @@ -11,6 +11,7 @@ import net.sf.openrocket.file.openrocket.OpenRocketSaver; import net.sf.openrocket.file.rocksim.export.RocksimSaver; import net.sf.openrocket.gui.StorageOptionChooser; import net.sf.openrocket.gui.configdialog.ComponentConfigDialog; +import net.sf.openrocket.gui.customexpression.CustomExpressionDialog; import net.sf.openrocket.gui.dialogs.AboutDialog; import net.sf.openrocket.gui.dialogs.BugReportDialog; import net.sf.openrocket.gui.dialogs.ComponentAnalysisDialog; @@ -637,7 +638,7 @@ public class BasicFrame extends JFrame { }); menu.add(item); - + //// Optimize item = new JMenuItem(trans.get("main.menu.analyze.optimization"), KeyEvent.VK_O); item.getAccessibleContext().setAccessibleDescription(trans.get("main.menu.analyze.optimization.desc")); item.addActionListener(new ActionListener() { @@ -649,7 +650,17 @@ public class BasicFrame extends JFrame { }); menu.add(item); - + //// Custom expressions + item = new JMenuItem(trans.get("main.menu.analyze.customExpressions"), KeyEvent.VK_E); + item.getAccessibleContext().setAccessibleDescription(trans.get("main.menu.analyze.customExpressions.desc")); + item.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + log.debug("Custom expressions selected"); + new CustomExpressionDialog(document, BasicFrame.this).setVisible(true); + } + }); + menu.add(item); //// Debug // (shown if openrocket.debug.menu is defined) diff --git a/core/src/net/sf/openrocket/gui/main/SimulationEditDialog.java b/core/src/net/sf/openrocket/gui/main/SimulationEditDialog.java index 0f9f6656..b8a59ea7 100644 --- a/core/src/net/sf/openrocket/gui/main/SimulationEditDialog.java +++ b/core/src/net/sf/openrocket/gui/main/SimulationEditDialog.java @@ -78,7 +78,7 @@ public class SimulationEditDialog extends JDialog { public static final int DEFAULT = -1; public static final int EDIT = 1; - public static final int PLOT = 3; + public static final int PLOT = 2; private final Window parentWindow; @@ -139,8 +139,6 @@ public class SimulationEditDialog extends JDialog { tabbedPane.addTab(trans.get("simedtdlg.tab.Launchcond"), flightConditionsTab()); //// Simulation options tabbedPane.addTab(trans.get("simedtdlg.tab.Simopt"), simulationOptionsTab()); - //// Custom expressions tab - tabbedPane.addTab(trans.get("simedtdlg.tab.CustomExpressions"), customExpressionsTab()); //// Plot data tabbedPane.addTab(trans.get("simedtdlg.tab.Plotdata"), plotTab()); //// Export data @@ -150,7 +148,7 @@ public class SimulationEditDialog extends JDialog { if (tab == EDIT) { tabbedPane.setSelectedIndex(0); } else if (tab == PLOT) { - tabbedPane.setSelectedIndex(3); + tabbedPane.setSelectedIndex(2); } else { FlightData data = s.getSimulatedData(); if (data == null || data.getBranchCount() == 0) @@ -837,11 +835,6 @@ public class SimulationEditDialog extends JDialog { return new SimulationExportPanel(simulation); } - - private JPanel customExpressionsTab() { - return new CustomExpressionPanel(simulation); - } - /** * Return a panel stating that there is no data available, and that the user diff --git a/core/src/net/sf/openrocket/rocketcomponent/Rocket.java b/core/src/net/sf/openrocket/rocketcomponent/Rocket.java index 226f9527..d493ca49 100644 --- a/core/src/net/sf/openrocket/rocketcomponent/Rocket.java +++ b/core/src/net/sf/openrocket/rocketcomponent/Rocket.java @@ -42,7 +42,7 @@ public class Rocket extends RocketComponent { * List of component change listeners. */ private List listenerList = new ArrayList(); - + /** * When freezeList != null, events are not dispatched but stored in the list. * When the structure is thawed, a single combined event will be fired. @@ -121,9 +121,6 @@ public class Rocket extends RocketComponent { fireComponentChangeEvent(ComponentChangeEvent.NONFUNCTIONAL_CHANGE); } - - - /** * Return the number of stages in this rocket. * diff --git a/core/src/net/sf/openrocket/simulation/BasicEventSimulationEngine.java b/core/src/net/sf/openrocket/simulation/BasicEventSimulationEngine.java index b0139fbb..d8f44ba0 100644 --- a/core/src/net/sf/openrocket/simulation/BasicEventSimulationEngine.java +++ b/core/src/net/sf/openrocket/simulation/BasicEventSimulationEngine.java @@ -18,6 +18,7 @@ import net.sf.openrocket.rocketcomponent.MotorMount; import net.sf.openrocket.rocketcomponent.RecoveryDevice; import net.sf.openrocket.rocketcomponent.RocketComponent; import net.sf.openrocket.rocketcomponent.Stage; +import net.sf.openrocket.simulation.customexpression.CustomExpression; import net.sf.openrocket.simulation.exception.MotorIgnitionException; import net.sf.openrocket.simulation.exception.SimulationException; import net.sf.openrocket.simulation.exception.SimulationLaunchException; @@ -91,9 +92,9 @@ public class BasicEventSimulationEngine implements SimulationEngine { // Calculate values for custom expressions FlightDataBranch data = status.getFlightData(); - ArrayList allExpressions = status.getSimulationConditions().getSimulation().getCustomExpressions(); + ArrayList allExpressions = status.getSimulationConditions().getSimulation().getDocument().getCustomExpressions(); for (CustomExpression expression : allExpressions ) { - data.setValue(expression.getType(), expression.evaluate(status)); + data.setValue(expression.getType(), expression.evaluateDouble(status)); } // Check for NaN values in the simulation status diff --git a/core/src/net/sf/openrocket/simulation/CustomExpression.java b/core/src/net/sf/openrocket/simulation/CustomExpression.java deleted file mode 100644 index a383dd87..00000000 --- a/core/src/net/sf/openrocket/simulation/CustomExpression.java +++ /dev/null @@ -1,341 +0,0 @@ -package net.sf.openrocket.simulation; - -import java.util.SortedMap; -import java.util.TreeMap; - -import net.sf.openrocket.document.Simulation; -import net.sf.openrocket.l10n.Translator; -import net.sf.openrocket.logging.LogHelper; -import net.sf.openrocket.startup.Application; -import net.sf.openrocket.unit.FixedUnitGroup; -import net.sf.openrocket.unit.UnitGroup; -import net.sf.openrocket.util.ArrayList; -import de.congrace.exp4j.Calculable; -import de.congrace.exp4j.ExpressionBuilder; - - -/** - * Represents a single custom expression - * @author Richard Graham - * - */ -public class CustomExpression implements Cloneable{ - - private static final LogHelper log = Application.getLogger(); - private static final Translator trans = Application.getTranslator(); - - private String name, symbol, unit, expression; - private ExpressionBuilder builder; - private Simulation sim = null; - - // A map of available operator strings (keys) and description of function (value) - public static final SortedMap AVAILABLE_OPERATORS = new TreeMap() {{ - put("+" , trans.get("Operator.plus")); - put("-" , trans.get("Operator.minus")); - put("*" , trans.get("Operator.star")); - put("/" , trans.get("Operator.div")); - put("%" , trans.get("Operator.mod")); - put("^" , trans.get("Operator.pow")); - put("abs()" , trans.get("Operator.abs")); - put("ceil()" , trans.get("Operator.ceil")); - put("floor()" , trans.get("Operator.floor")); - put("sqrt()" , trans.get("Operator.sqrt")); - put("cbrt()" , trans.get("Operator.cbrt")); - put("exp()" , trans.get("Operator.exp")); - put("log()" , trans.get("Operator.ln")); - put("sin()" , trans.get("Operator.sin")); - put("cos()" , trans.get("Operator.cos")); - put("tan()" , trans.get("Operator.tan")); - put("asin()" , trans.get("Operator.asin")); - put("acos()" , trans.get("Operator.acos")); - put("atan()" , trans.get("Operator.atan")); - put("sinh()" , trans.get("Operator.hsin")); - put("cosh()" , trans.get("Operator.hcos")); - put("tanh()" , trans.get("Operator.htan")); - }}; - - public CustomExpression(){ - setName(""); - setSymbol(""); - setUnit(""); - setExpression(""); - } - - public CustomExpression(Simulation sim){ - this(); - setSimulation(sim); - } - - public CustomExpression(Simulation sim, String name, String symbol, String unit, String expression) { - - setName(name); - setSymbol(symbol); - setUnit(unit); - setExpression(expression); - setSimulation(sim); - } - - /* - * Use this to update the simulation this is associated with - */ - public void setSimulation(Simulation sim){ - this.sim = sim; - } - - public Simulation getSimulation() { - return this.sim; - } - - /* - * Returns the flight data branch 0 for this simulation, or an empty branch - * if no simulated data exists - */ - private FlightDataBranch getBranch() { - if ( sim == null || sim.getSimulatedData() == null || sim.getSimulatedData().getBranchCount() == 0){ - return new FlightDataBranch(); - } - else { - return sim.getSimulatedData().getBranch(0); - } - } - - - public void setName(String name){ - this.name = name; - } - - public void setUnit(String unit){ - this.unit = unit; - } - - public void setSymbol(String symbol){ - this.symbol = symbol; - } - - public void setExpression(String expression){ - this.expression = expression; - builder = new ExpressionBuilder(expression); - } - - // get a list of all the names of all the available variables - private ArrayList getAllNames(){ - ArrayList names = new ArrayList(); - for (FlightDataType type : FlightDataType.ALL_TYPES) - names.add(type.getName()); - for (CustomExpression exp : sim.getCustomExpressions() ){ - if (exp != this) - names.add(exp.getName()); - } - return names; - } - - // get a list of all the symbols of the available variables ignoring this one - private ArrayList getAllSymbols(){ - ArrayList symbols = new ArrayList(); - for (FlightDataType type : FlightDataType.ALL_TYPES) - symbols.add(type.getSymbol()); - for (CustomExpression exp : sim.getCustomExpressions() ){ - if (exp != this) - symbols.add(exp.getSymbol()); - } - return symbols; - } - - public boolean checkSymbol(){ - if (symbol.trim().isEmpty()) - return false; - - // No bad characters - for (char c : "0123456789.,()[]{}<> ".toCharArray()) - if (symbol.indexOf(c) != -1 ) - return false; - - // No operators (ignoring brackets) - for (String s : CustomExpression.AVAILABLE_OPERATORS.keySet()){ - if (symbol.contains(s.replaceAll("\\(|\\)", ""))) - return false; - } - - // No already defined symbols - ArrayList symbols = getAllSymbols().clone(); - if (symbols.contains(symbol.trim())){ - int index = symbols.indexOf(symbol.trim()); - log.user("Symbol "+symbol+" already exists, found "+symbols.get(index)); - return false; - } - - return true; - } - - public boolean checkName(){ - if (name.trim().isEmpty()) - return false; - - // No characters that could mess things up saving etc - for (char c : ",()[]{}<>".toCharArray()) - if (name.indexOf(c) != -1 ) - return false; - - ArrayList names = getAllNames().clone(); - if (names.contains(name.trim())){ - int index = names.indexOf(name.trim()); - log.user("Name "+name+" already exists, found "+names.get(index)); - return false; - } - - return true; - } - - // Currently no restrictions on unit - public boolean checkUnit(){ - return true; - } - - public boolean checkAll(){ - return checkUnit() && checkSymbol() && checkName(); - } - - public String getName(){ - return name; - } - - public String getSymbol(){ - return symbol; - } - - public String getUnit(){ - return unit; - } - - public String getExpressionString(){ - return expression; - } - - - /* - * Check if the current expression is valid - */ - public boolean checkExpression(){ - - if (expression.trim().isEmpty()){ - return false; - } - - // Define the available variables as 0 - for (FlightDataType type : getBranch().getTypes()){ - builder.withVariable(type.getSymbol(), 0.0); - } - - for (String symb : getAllSymbols()){ - builder.withVariable(symb, 0.0); - } - - // Try to build - try { - builder.build(); - } catch (Exception e) { - log.user("Custom expression invalid : " + e.toString()); - return false; - } - - // Otherwise, all OK - return true; - } - - /* - * Evaluate the expression using the last variable values from the simulation status. - * Returns NaN on any error. - */ - public Double evaluate(SimulationStatus status){ - - for (FlightDataType type : status.getFlightData().getTypes()){ - builder.withVariable(type.getSymbol(), status.getFlightData().getLast(type) ); - } - - Calculable calc; - try { - calc = builder.build(); - return new Double(calc.calculate()); - } catch (Exception e) { - log.user("Could not calculate custom expression "+name); - return Double.NaN; - } - } - - /* - * Returns the new flight data type corresponding to this calculated data - */ - public FlightDataType getType(){ - - UnitGroup ug = new FixedUnitGroup(unit); - FlightDataType type = FlightDataType.getType(name, symbol, ug); - - // If in a simulation, figure out priority from order in array so that customs expressions are always at the top - //if (sim != null && sim.getCustomExpressions().contains(this)){ - // int totalExpressions = sim.getCustomExpressions().size(); - // int p = -1*(totalExpressions-sim.getCustomExpressions().indexOf(this)); - // type.setPriority(p); - //} - - return type; - } - - /* - * Add this expression to the simulation if valid and not already added - */ - public void addToSimulation(){ - // Abort if exact expression already in - if ( !sim.getCustomExpressions().contains(this) && this.checkAll() ) - sim.addCustomExpression( this ); - } - - /* - * Removes this expression from the simulation, replacing it with a given new expression - */ - public void overwrite(CustomExpression newExpression){ - if (!sim.getCustomExpressions().contains(this)) - return; - else { - int index = sim.getCustomExpressions().indexOf(this); - sim.getCustomExpressions().set(index, newExpression); - } - } - - /* - * Add a copy to other simulations in this document if possible - * Will not overwrite existing expressions - */ - public void copyToOtherSimulations(){ - for (Simulation s : this.getSimulation().getDocument().getSimulations()){ - CustomExpression newExpression = (CustomExpression) this.clone(); - newExpression.setSimulation(s); - newExpression.addToSimulation(); - } - } - - @Override - public String toString(){ - return "Custom expression : "+this.name.toString()+ " " + this.expression.toString(); - } - - @Override - /* - * Clone method makes a deep copy of everything except the simulation. - * If you want to apply this to another simulation, set simulation manually after cloning. - * @see java.lang.Object#clone() - */ - public Object clone() { - try { - return super.clone(); - } - catch( CloneNotSupportedException e ) - { - return new CustomExpression( sim , - new String(this.getName()), - new String(this.getSymbol()), - new String(this.getUnit()), - new String(this.getExpressionString())); - } - } - -} diff --git a/core/src/net/sf/openrocket/simulation/FlightDataType.java b/core/src/net/sf/openrocket/simulation/FlightDataType.java index 8d124261..6a95c304 100644 --- a/core/src/net/sf/openrocket/simulation/FlightDataType.java +++ b/core/src/net/sf/openrocket/simulation/FlightDataType.java @@ -5,6 +5,7 @@ import java.util.Locale; import java.util.Map; import net.sf.openrocket.l10n.Translator; +import net.sf.openrocket.logging.LogHelper; import net.sf.openrocket.startup.Application; import net.sf.openrocket.unit.UnitGroup; @@ -23,159 +24,160 @@ import net.sf.openrocket.unit.UnitGroup; */ public class FlightDataType implements Comparable { private static final Translator trans = Application.getTranslator(); + private static final LogHelper log = Application.getLogger(); /** Priority of custom-created variables */ private static final int DEFAULT_PRIORITY = 999; /** List of existing types. MUST BE DEFINED BEFORE ANY TYPES!! */ + /** NOTE: The String key here is now the symbol */ private static final Map EXISTING_TYPES = new HashMap(); - //// Time - public static final FlightDataType TYPE_TIME = newType("TYPE_TIME", "t", UnitGroup.UNITS_FLIGHT_TIME, 1); + public static final FlightDataType TYPE_TIME = newType(trans.get("FlightDataType.TYPE_TIME"), "t", UnitGroup.UNITS_FLIGHT_TIME, 1); //// Vertical position and motion //// Altitude - public static final FlightDataType TYPE_ALTITUDE = newType("TYPE_ALTITUDE", "h", UnitGroup.UNITS_DISTANCE, 10); + public static final FlightDataType TYPE_ALTITUDE = newType(trans.get("FlightDataType.TYPE_ALTITUDE"), "h", UnitGroup.UNITS_DISTANCE, 10); //// Vertical velocity - public static final FlightDataType TYPE_VELOCITY_Z = newType("TYPE_VELOCITY_Z", "Vz", UnitGroup.UNITS_VELOCITY, 11); + public static final FlightDataType TYPE_VELOCITY_Z = newType(trans.get("FlightDataType.TYPE_VELOCITY_Z"), "Vz", UnitGroup.UNITS_VELOCITY, 11); //// Vertical acceleration - public static final FlightDataType TYPE_ACCELERATION_Z = newType("TYPE_ACCELERATION_Z", "Az", UnitGroup.UNITS_ACCELERATION, 12); + public static final FlightDataType TYPE_ACCELERATION_Z = newType(trans.get("FlightDataType.TYPE_ACCELERATION_Z"), "Az", UnitGroup.UNITS_ACCELERATION, 12); //// Total motion //// Total velocity - public static final FlightDataType TYPE_VELOCITY_TOTAL = newType("TYPE_VELOCITY_TOTAL", "Vt", UnitGroup.UNITS_VELOCITY, 20); + public static final FlightDataType TYPE_VELOCITY_TOTAL = newType(trans.get("FlightDataType.TYPE_VELOCITY_TOTAL"), "Vt", UnitGroup.UNITS_VELOCITY, 20); //// Total acceleration - public static final FlightDataType TYPE_ACCELERATION_TOTAL = newType("TYPE_ACCELERATION_TOTAL", "At", UnitGroup.UNITS_ACCELERATION, 21); + public static final FlightDataType TYPE_ACCELERATION_TOTAL = newType(trans.get("FlightDataType.TYPE_ACCELERATION_TOTAL"), "At", UnitGroup.UNITS_ACCELERATION, 21); //// Lateral position and motion //// Position upwind - public static final FlightDataType TYPE_POSITION_X = newType("TYPE_POSITION_X", "Px", UnitGroup.UNITS_DISTANCE, 30); + public static final FlightDataType TYPE_POSITION_X = newType(trans.get("FlightDataType.TYPE_POSITION_X"), "Px", UnitGroup.UNITS_DISTANCE, 30); //// Position parallel to wind - public static final FlightDataType TYPE_POSITION_Y = newType("TYPE_POSITION_Y", "Py", UnitGroup.UNITS_DISTANCE, 31); + public static final FlightDataType TYPE_POSITION_Y = newType(trans.get("FlightDataType.TYPE_POSITION_Y"), "Py", UnitGroup.UNITS_DISTANCE, 31); //// Lateral distance - public static final FlightDataType TYPE_POSITION_XY = newType("TYPE_POSITION_XY", "Pl", UnitGroup.UNITS_DISTANCE, 32); + public static final FlightDataType TYPE_POSITION_XY = newType(trans.get("FlightDataType.TYPE_POSITION_XY"), "Pl", UnitGroup.UNITS_DISTANCE, 32); //// Lateral direction - public static final FlightDataType TYPE_POSITION_DIRECTION = newType("TYPE_POSITION_DIRECTION", "\u03b8l", UnitGroup.UNITS_ANGLE, 33); + public static final FlightDataType TYPE_POSITION_DIRECTION = newType(trans.get("FlightDataType.TYPE_POSITION_DIRECTION"), "\u03b8l", UnitGroup.UNITS_ANGLE, 33); //// Lateral velocity - public static final FlightDataType TYPE_VELOCITY_XY = newType("TYPE_VELOCITY_XY", "Vl", UnitGroup.UNITS_VELOCITY, 34); + public static final FlightDataType TYPE_VELOCITY_XY = newType(trans.get("FlightDataType.TYPE_VELOCITY_XY"), "Vl", UnitGroup.UNITS_VELOCITY, 34); //// Lateral acceleration - public static final FlightDataType TYPE_ACCELERATION_XY = newType("TYPE_ACCELERATION_XY", "Al", UnitGroup.UNITS_ACCELERATION, 35); + public static final FlightDataType TYPE_ACCELERATION_XY = newType(trans.get("FlightDataType.TYPE_ACCELERATION_XY"), "Al", UnitGroup.UNITS_ACCELERATION, 35); //// Latitude - public static final FlightDataType TYPE_LATITUDE = newType("TYPE_LATITUDE", "\u03c6", UnitGroup.UNITS_ANGLE, 36); + public static final FlightDataType TYPE_LATITUDE = newType(trans.get("FlightDataType.TYPE_LATITUDE"), "\u03c6", UnitGroup.UNITS_ANGLE, 36); //// Longitude - public static final FlightDataType TYPE_LONGITUDE = newType("TYPE_LONGITUDE", "\u03bb", UnitGroup.UNITS_ANGLE, 37); + public static final FlightDataType TYPE_LONGITUDE = newType(trans.get("FlightDataType.TYPE_LONGITUDE"), "\u03bb", UnitGroup.UNITS_ANGLE, 37); //// Angular motion //// Angle of attack - public static final FlightDataType TYPE_AOA = newType("TYPE_AOA", "\u03b1", UnitGroup.UNITS_ANGLE, 40); + public static final FlightDataType TYPE_AOA = newType(trans.get("FlightDataType.TYPE_AOA"), "\u03b1", UnitGroup.UNITS_ANGLE, 40); //// Roll rate - public static final FlightDataType TYPE_ROLL_RATE = newType("TYPE_ROLL_RATE", "d\u03a6", UnitGroup.UNITS_ROLL, 41); + public static final FlightDataType TYPE_ROLL_RATE = newType(trans.get("FlightDataType.TYPE_ROLL_RATE"), "d\u03a6", UnitGroup.UNITS_ROLL, 41); //// Pitch rate - public static final FlightDataType TYPE_PITCH_RATE = newType("TYPE_PITCH_RATE", "d\u03b8", UnitGroup.UNITS_ROLL, 42); + public static final FlightDataType TYPE_PITCH_RATE = newType(trans.get("FlightDataType.TYPE_PITCH_RATE"), "d\u03b8", UnitGroup.UNITS_ROLL, 42); //// Yaw rate - public static final FlightDataType TYPE_YAW_RATE = newType("TYPE_YAW_RATE", "d\u03a8", UnitGroup.UNITS_ROLL, 43); + public static final FlightDataType TYPE_YAW_RATE = newType(trans.get("FlightDataType.TYPE_YAW_RATE"), "d\u03a8", UnitGroup.UNITS_ROLL, 43); //// Stability information //// Mass - public static final FlightDataType TYPE_MASS = newType("TYPE_MASS", "m", UnitGroup.UNITS_MASS, 50); + public static final FlightDataType TYPE_MASS = newType(trans.get("FlightDataType.TYPE_MASS"), "m", UnitGroup.UNITS_MASS, 50); //// Longitudinal moment of inertia - public static final FlightDataType TYPE_LONGITUDINAL_INERTIA = newType("TYPE_LONGITUDINAL_INERTIA", "Il", UnitGroup.UNITS_INERTIA, 51); + public static final FlightDataType TYPE_LONGITUDINAL_INERTIA = newType(trans.get("FlightDataType.TYPE_LONGITUDINAL_INERTIA"), "Il", UnitGroup.UNITS_INERTIA, 51); //// Rotational moment of inertia - public static final FlightDataType TYPE_ROTATIONAL_INERTIA = newType("TYPE_ROTATIONAL_INERTIA", "Ir", UnitGroup.UNITS_INERTIA, 52); + public static final FlightDataType TYPE_ROTATIONAL_INERTIA = newType(trans.get("FlightDataType.TYPE_ROTATIONAL_INERTIA"), "Ir", UnitGroup.UNITS_INERTIA, 52); //// CP location - public static final FlightDataType TYPE_CP_LOCATION = newType("TYPE_CP_LOCATION", "Cp", UnitGroup.UNITS_LENGTH, 53); + public static final FlightDataType TYPE_CP_LOCATION = newType(trans.get("FlightDataType.TYPE_CP_LOCATION"), "Cp", UnitGroup.UNITS_LENGTH, 53); //// CG location - public static final FlightDataType TYPE_CG_LOCATION = newType("TYPE_CG_LOCATION", "Cg", UnitGroup.UNITS_LENGTH, 54); + public static final FlightDataType TYPE_CG_LOCATION = newType(trans.get("FlightDataType.TYPE_CG_LOCATION"), "Cg", UnitGroup.UNITS_LENGTH, 54); //// Stability margin calibers - public static final FlightDataType TYPE_STABILITY = newType("TYPE_STABILITY", "S", UnitGroup.UNITS_COEFFICIENT, 55); + public static final FlightDataType TYPE_STABILITY = newType(trans.get("FlightDataType.TYPE_STABILITY"), "S", UnitGroup.UNITS_COEFFICIENT, 55); //// Characteristic numbers //// Mach number - public static final FlightDataType TYPE_MACH_NUMBER = newType("TYPE_MACH_NUMBER", "M", UnitGroup.UNITS_COEFFICIENT, 60); + public static final FlightDataType TYPE_MACH_NUMBER = newType(trans.get("FlightDataType.TYPE_MACH_NUMBER"), "M", UnitGroup.UNITS_COEFFICIENT, 60); //// Reynolds number - public static final FlightDataType TYPE_REYNOLDS_NUMBER = newType("TYPE_REYNOLDS_NUMBER", "R", UnitGroup.UNITS_COEFFICIENT, 61); + public static final FlightDataType TYPE_REYNOLDS_NUMBER = newType(trans.get("FlightDataType.TYPE_REYNOLDS_NUMBER"), "R", UnitGroup.UNITS_COEFFICIENT, 61); //// Thrust and drag //// Thrust - public static final FlightDataType TYPE_THRUST_FORCE = newType("TYPE_THRUST_FORCE", "Ft", UnitGroup.UNITS_FORCE, 70); + public static final FlightDataType TYPE_THRUST_FORCE = newType(trans.get("FlightDataType.TYPE_THRUST_FORCE"), "Ft", UnitGroup.UNITS_FORCE, 70); //// Drag force - public static final FlightDataType TYPE_DRAG_FORCE = newType("TYPE_DRAG_FORCE", "Fd", UnitGroup.UNITS_FORCE, 71); + public static final FlightDataType TYPE_DRAG_FORCE = newType(trans.get("FlightDataType.TYPE_DRAG_FORCE"), "Fd", UnitGroup.UNITS_FORCE, 71); //// Drag coefficient - public static final FlightDataType TYPE_DRAG_COEFF = newType("TYPE_DRAG_COEFF", "Cd", UnitGroup.UNITS_COEFFICIENT, 72); + public static final FlightDataType TYPE_DRAG_COEFF = newType(trans.get("FlightDataType.TYPE_DRAG_COEFF"), "Cd", UnitGroup.UNITS_COEFFICIENT, 72); //// Axial drag coefficient - public static final FlightDataType TYPE_AXIAL_DRAG_COEFF = newType("TYPE_AXIAL_DRAG_COEFF", "Cda", UnitGroup.UNITS_COEFFICIENT, 73); + public static final FlightDataType TYPE_AXIAL_DRAG_COEFF = newType(trans.get("FlightDataType.TYPE_AXIAL_DRAG_COEFF"), "Cda", UnitGroup.UNITS_COEFFICIENT, 73); //// Component drag coefficients //// Friction drag coefficient - public static final FlightDataType TYPE_FRICTION_DRAG_COEFF = newType("TYPE_FRICTION_DRAG_COEFF", "Cdf", UnitGroup.UNITS_COEFFICIENT, 80); + public static final FlightDataType TYPE_FRICTION_DRAG_COEFF = newType(trans.get("FlightDataType.TYPE_FRICTION_DRAG_COEFF"), "Cdf", UnitGroup.UNITS_COEFFICIENT, 80); //// Pressure drag coefficient - public static final FlightDataType TYPE_PRESSURE_DRAG_COEFF = newType("TYPE_PRESSURE_DRAG_COEFF", "Cdp", UnitGroup.UNITS_COEFFICIENT, 81); + public static final FlightDataType TYPE_PRESSURE_DRAG_COEFF = newType(trans.get("FlightDataType.TYPE_PRESSURE_DRAG_COEFF"), "Cdp", UnitGroup.UNITS_COEFFICIENT, 81); //// Base drag coefficient - public static final FlightDataType TYPE_BASE_DRAG_COEFF = newType("TYPE_BASE_DRAG_COEFF", "Cdb", UnitGroup.UNITS_COEFFICIENT, 82); + public static final FlightDataType TYPE_BASE_DRAG_COEFF = newType(trans.get("FlightDataType.TYPE_BASE_DRAG_COEFF"), "Cdb", UnitGroup.UNITS_COEFFICIENT, 82); //// Other coefficients //// Normal force coefficient - public static final FlightDataType TYPE_NORMAL_FORCE_COEFF = newType("TYPE_NORMAL_FORCE_COEFF", "Cn", UnitGroup.UNITS_COEFFICIENT, 90); + public static final FlightDataType TYPE_NORMAL_FORCE_COEFF = newType(trans.get("FlightDataType.TYPE_NORMAL_FORCE_COEFF"), "Cn", UnitGroup.UNITS_COEFFICIENT, 90); //// Pitch moment coefficient - public static final FlightDataType TYPE_PITCH_MOMENT_COEFF = newType("TYPE_PITCH_MOMENT_COEFF", "C\u03b8", UnitGroup.UNITS_COEFFICIENT, 91); + public static final FlightDataType TYPE_PITCH_MOMENT_COEFF = newType(trans.get("FlightDataType.TYPE_PITCH_MOMENT_COEFF"), "C\u03b8", UnitGroup.UNITS_COEFFICIENT, 91); //// Yaw moment coefficient - public static final FlightDataType TYPE_YAW_MOMENT_COEFF = newType("TYPE_YAW_MOMENT_COEFF", "C\u03c4\u03a8", UnitGroup.UNITS_COEFFICIENT, 92); + public static final FlightDataType TYPE_YAW_MOMENT_COEFF = newType(trans.get("FlightDataType.TYPE_YAW_MOMENT_COEFF"), "C\u03c4\u03a8", UnitGroup.UNITS_COEFFICIENT, 92); //// Side force coefficient - public static final FlightDataType TYPE_SIDE_FORCE_COEFF = newType("TYPE_SIDE_FORCE_COEFF", "C\u03c4s", UnitGroup.UNITS_COEFFICIENT, 93); + public static final FlightDataType TYPE_SIDE_FORCE_COEFF = newType(trans.get("FlightDataType.TYPE_SIDE_FORCE_COEFF"), "C\u03c4s", UnitGroup.UNITS_COEFFICIENT, 93); //// Roll moment coefficient - public static final FlightDataType TYPE_ROLL_MOMENT_COEFF = newType("TYPE_ROLL_MOMENT_COEFF", "C\u03c4\u03a6", UnitGroup.UNITS_COEFFICIENT, 94); + public static final FlightDataType TYPE_ROLL_MOMENT_COEFF = newType(trans.get("FlightDataType.TYPE_ROLL_MOMENT_COEFF"), "C\u03c4\u03a6", UnitGroup.UNITS_COEFFICIENT, 94); //// Roll forcing coefficient - public static final FlightDataType TYPE_ROLL_FORCING_COEFF = newType("TYPE_ROLL_FORCING_COEFF", "Cf\u03a6", UnitGroup.UNITS_COEFFICIENT, 95); + public static final FlightDataType TYPE_ROLL_FORCING_COEFF = newType(trans.get("FlightDataType.TYPE_ROLL_FORCING_COEFF"), "Cf\u03a6", UnitGroup.UNITS_COEFFICIENT, 95); //// Roll damping coefficient - public static final FlightDataType TYPE_ROLL_DAMPING_COEFF = newType("TYPE_ROLL_DAMPING_COEFF", "C\u03b6\u03a6", UnitGroup.UNITS_COEFFICIENT, 96); + public static final FlightDataType TYPE_ROLL_DAMPING_COEFF = newType(trans.get("FlightDataType.TYPE_ROLL_DAMPING_COEFF"), "C\u03b6\u03a6", UnitGroup.UNITS_COEFFICIENT, 96); //// Pitch damping coefficient - public static final FlightDataType TYPE_PITCH_DAMPING_MOMENT_COEFF = newType("TYPE_PITCH_DAMPING_MOMENT_COEFF", "C\u03b6\u03b8", UnitGroup.UNITS_COEFFICIENT, 97); + public static final FlightDataType TYPE_PITCH_DAMPING_MOMENT_COEFF = newType(trans.get("FlightDataType.TYPE_PITCH_DAMPING_MOMENT_COEFF"), "C\u03b6\u03b8", UnitGroup.UNITS_COEFFICIENT, 97); //// Yaw damping coefficient - public static final FlightDataType TYPE_YAW_DAMPING_MOMENT_COEFF = newType("TYPE_YAW_DAMPING_MOMENT_COEFF", "C\u03b6\u03a8", UnitGroup.UNITS_COEFFICIENT, 98); + public static final FlightDataType TYPE_YAW_DAMPING_MOMENT_COEFF = newType(trans.get("FlightDataType.TYPE_YAW_DAMPING_MOMENT_COEFF"), "C\u03b6\u03a8", UnitGroup.UNITS_COEFFICIENT, 98); //// Coriolis acceleration - public static final FlightDataType TYPE_CORIOLIS_ACCELERATION = newType("TYPE_CORIOLIS_ACCELERATION", "Ac", UnitGroup.UNITS_ACCELERATION, 99); + public static final FlightDataType TYPE_CORIOLIS_ACCELERATION = newType(trans.get("FlightDataType.TYPE_CORIOLIS_ACCELERATION"), "Ac", UnitGroup.UNITS_ACCELERATION, 99); //// Reference length + area //// Reference length - public static final FlightDataType TYPE_REFERENCE_LENGTH = newType("TYPE_REFERENCE_LENGTH", "Lr", UnitGroup.UNITS_LENGTH, 100); + public static final FlightDataType TYPE_REFERENCE_LENGTH = newType(trans.get("FlightDataType.TYPE_REFERENCE_LENGTH"), "Lr", UnitGroup.UNITS_LENGTH, 100); //// Reference area - public static final FlightDataType TYPE_REFERENCE_AREA = newType("TYPE_REFERENCE_AREA", "Ar", UnitGroup.UNITS_AREA, 101); + public static final FlightDataType TYPE_REFERENCE_AREA = newType(trans.get("FlightDataType.TYPE_REFERENCE_AREA"), "Ar", UnitGroup.UNITS_AREA, 101); //// Orientation //// Vertical orientation (zenith) - public static final FlightDataType TYPE_ORIENTATION_THETA = newType("TYPE_ORIENTATION_THETA", "\u0398", UnitGroup.UNITS_ANGLE, 106); + public static final FlightDataType TYPE_ORIENTATION_THETA = newType(trans.get("FlightDataType.TYPE_ORIENTATION_THETA"), "\u0398", UnitGroup.UNITS_ANGLE, 106); //// Lateral orientation (azimuth) - public static final FlightDataType TYPE_ORIENTATION_PHI = newType("TYPE_ORIENTATION_PHI", "\u03a6", UnitGroup.UNITS_ANGLE, 107); + public static final FlightDataType TYPE_ORIENTATION_PHI = newType(trans.get("FlightDataType.TYPE_ORIENTATION_PHI"), "\u03a6", UnitGroup.UNITS_ANGLE, 107); //// Atmospheric conditions //// Wind velocity - public static final FlightDataType TYPE_WIND_VELOCITY = newType("TYPE_WIND_VELOCITY", "Vw", UnitGroup.UNITS_VELOCITY, 110); + public static final FlightDataType TYPE_WIND_VELOCITY = newType(trans.get("FlightDataType.TYPE_WIND_VELOCITY"), "Vw", UnitGroup.UNITS_VELOCITY, 110); //// Air temperature - public static final FlightDataType TYPE_AIR_TEMPERATURE = newType("TYPE_AIR_TEMPERATURE", "T", UnitGroup.UNITS_TEMPERATURE, 111); + public static final FlightDataType TYPE_AIR_TEMPERATURE = newType(trans.get("FlightDataType.TYPE_AIR_TEMPERATURE"), "T", UnitGroup.UNITS_TEMPERATURE, 111); //// Air pressure - public static final FlightDataType TYPE_AIR_PRESSURE = newType("TYPE_AIR_PRESSURE", "p", UnitGroup.UNITS_PRESSURE, 112); + public static final FlightDataType TYPE_AIR_PRESSURE = newType(trans.get("FlightDataType.TYPE_AIR_PRESSURE"), "p", UnitGroup.UNITS_PRESSURE, 112); //// Speed of sound - public static final FlightDataType TYPE_SPEED_OF_SOUND = newType("TYPE_SPEED_OF_SOUND", "Vs", UnitGroup.UNITS_VELOCITY, 113); + public static final FlightDataType TYPE_SPEED_OF_SOUND = newType(trans.get("FlightDataType.TYPE_SPEED_OF_SOUND"), "Vs", UnitGroup.UNITS_VELOCITY, 113); //// Simulation information //// Simulation time step - public static final FlightDataType TYPE_TIME_STEP = newType("TYPE_TIME_STEP", "dt", UnitGroup.UNITS_TIME_STEP, 200); + public static final FlightDataType TYPE_TIME_STEP = newType(trans.get("FlightDataType.TYPE_TIME_STEP"), "dt", UnitGroup.UNITS_TIME_STEP, 200); //// Computation time - public static final FlightDataType TYPE_COMPUTATION_TIME = newType("TYPE_COMPUTATION_TIME", "tc", UnitGroup.UNITS_SHORT_TIME, 201); + public static final FlightDataType TYPE_COMPUTATION_TIME = newType(trans.get("FlightDataType.TYPE_COMPUTATION_TIME"), "tc", UnitGroup.UNITS_SHORT_TIME, 201); // An array of all the built in types public static final FlightDataType[] ALL_TYPES = { @@ -235,46 +237,90 @@ public class FlightDataType implements Comparable { }; /** - * Return a {@link FlightDataType} based on a string description. This returns known data types - * if possible, or a new type otherwise. + * Return a {@link FlightDataType} with a given string description, symbol and unitgroup. + * This returns an existing data type if the symbol matches that of an existing type. + * + * If the symbol matches but the unit and description information differ, then the old stored datatype + * is erased and the updated version based on the given parametes is returned. + * The only exception is if the description or unitgroup are undefined (null or empty string). In this case + * we just get these parameters from the existing type when making the new one. * * @param s the string description of the type. * @param u the unit group the new type should belong to if a new group is created. * @return a data type. */ + @SuppressWarnings("null") public static synchronized FlightDataType getType(String s, String symbol, UnitGroup u) { - // modified to include the unit - FlightDataType type = EXISTING_TYPES.get(s.toLowerCase(Locale.ENGLISH)); + + // if symbol is null : try finding by name + // if unit is null : don't do anything to the unit if found, just return datatype if found and generate an error and an empty unit otherwise + int oldPriority = DEFAULT_PRIORITY; - // added this for backward compatibility. Will update type if symbol undefined - //if (type != null && type.getSymbol() != symbol){ - // EXISTING_TYPES.remove(type); - // type = null; - //} + //FlightDataType type = findFromSymbol(symbol); + FlightDataType type = EXISTING_TYPES.get(symbol); if (type != null) { - return type; + // found it from symbol + + // if name was not give (empty string), can use the one we found name + if ( s.equals("") || s == null ){ + s = type.getName(); + } + if ( u == null ){ + u = type.getUnitGroup(); + } + + // if something has changed, then we need to remove the old one + // otherwise, just return what we found + if ( !u.equals(type.getUnitGroup()) || + !s.equals(type.getName()) + ) + { + oldPriority = type.priority; + + EXISTING_TYPES.remove(type); + log.info("Something changed with the type "+type.getName()+", removed old version."); + } + else{ + return type; + } } - type = newType("UserDefined." + s, s, symbol, u, DEFAULT_PRIORITY); - return type; + + if (u == null){ + u = UnitGroup.UNITS_NONE; + log.error("Made a new flightdatatype, but did not know what units to use."); + } + + // make a new one + type = newType(s, symbol, u, oldPriority); + return type; } - /** - * Used while initializing the class. + /* + * Get the flightdatatype from existing types based on the symbol. */ - - private static FlightDataType newType( String key , String symbol, UnitGroup u, int priority ) { - String name = trans.get("FlightDataType." + key ); - return newType( key, name, symbol, u, priority ); + /* + private static FlightDataType findFromSymbol(String symbol){ + for (FlightDataType t : EXISTING_TYPES.values()){ + if (t.getSymbol().equals(symbol)){ + return t; + } + } + return null; } + */ - private static synchronized FlightDataType newType(String key, String s, String symbol, UnitGroup u, int priority) { - FlightDataType type = new FlightDataType(key, s, symbol, u, priority); - EXISTING_TYPES.put(s.toLowerCase(Locale.ENGLISH), type); + /** + * Used while initializing the class. + */ + private static synchronized FlightDataType newType(String s, String symbol, UnitGroup u, int priority) { + FlightDataType type = new FlightDataType(s, symbol, u, priority); + //EXISTING_TYPES.put(s.toLowerCase(Locale.ENGLISH), type); + EXISTING_TYPES.put(symbol, type); return type; } - private final String key; + private final String name; private final String symbol; private final UnitGroup units; @@ -282,8 +328,7 @@ public class FlightDataType implements Comparable { private final int hashCode; - private FlightDataType(String key, String typeName, String symbol, UnitGroup units, int priority) { - this.key = key; + private FlightDataType(String typeName, String symbol, UnitGroup units, int priority) { if (typeName == null) throw new IllegalArgumentException("typeName is null"); if (units == null) @@ -292,7 +337,7 @@ public class FlightDataType implements Comparable { this.symbol = symbol; this.units = units; this.priority = priority; - this.hashCode = this.key.hashCode(); + this.hashCode = this.name.toLowerCase(Locale.ENGLISH).hashCode(); } /* @@ -301,10 +346,6 @@ public class FlightDataType implements Comparable { } */ - public String getKey() { - return key; - } - public String getName() { return name; } @@ -326,7 +367,7 @@ public class FlightDataType implements Comparable { public boolean equals(Object other) { if (!(other instanceof FlightDataType)) return false; - return this.hashCode == other.hashCode(); + return this.name.equalsIgnoreCase(((FlightDataType) other).name); } @Override diff --git a/core/src/net/sf/openrocket/simulation/customexpression/CustomExpression.java b/core/src/net/sf/openrocket/simulation/customexpression/CustomExpression.java new file mode 100644 index 00000000..2ac41ffc --- /dev/null +++ b/core/src/net/sf/openrocket/simulation/customexpression/CustomExpression.java @@ -0,0 +1,496 @@ +package net.sf.openrocket.simulation.customexpression; + +import java.util.List; +import java.util.regex.*; + +import net.sf.openrocket.document.OpenRocketDocument; +import net.sf.openrocket.logging.LogHelper; +import net.sf.openrocket.simulation.FlightDataType; +import net.sf.openrocket.simulation.SimulationStatus; +import net.sf.openrocket.startup.Application; +import net.sf.openrocket.unit.FixedUnitGroup; +import net.sf.openrocket.unit.UnitGroup; +import net.sf.openrocket.util.ArrayList; +import de.congrace.exp4j.Calculable; +import de.congrace.exp4j.ExpressionBuilder; +import de.congrace.exp4j.UnknownFunctionException; +import de.congrace.exp4j.UnparsableExpressionException; +import de.congrace.exp4j.Variable; + +/** + * Represents a single custom expression + * @author Richard Graham + * + */ +public class CustomExpression implements Cloneable{ + + private static final LogHelper log = Application.getLogger(); + + private OpenRocketDocument doc; + private String name, symbol, unit; + + protected String expression; + private ExpressionBuilder builder; + private List subExpressions = new ArrayList(); + + public CustomExpression(OpenRocketDocument doc){ + setName(""); + setSymbol(""); + setUnit(""); + setExpression(""); + this.doc = doc; + } + + public CustomExpression(OpenRocketDocument doc, + String name, + String symbol, + String unit, + String expression) { + this.doc = doc; + + setName(name); + setSymbol(symbol); + setUnit(unit); + setExpression(expression); + } + + /* + * Sets the long name of this expression, e.g. 'Kinetic energy' + */ + public void setName(String name){ + this.name = name; + } + + /* + * Sets the string for the units of the result of this expression. + */ + public void setUnit(String unit){ + this.unit = unit; + } + + /* + * Sets the symbol string. This is the short, locale independent symbol for this whole expression + */ + public void setSymbol(String symbol){ + this.symbol = symbol; + } + + /* + * Sets the actual expression string for this expression + */ + public void setExpression(String expression){ + + // This is the expression as supplied + this.expression = expression; + + // Replace any indexed variables + expression = subTimeIndexes(expression); + expression = subTimeRanges(expression); + + builder = new ExpressionBuilder(expression); + for (String n : getAllSymbols()){ + builder.withVariable(new Variable(n)); + } + for (CustomExpression exp : this.subExpressions){ + builder.withVariable(new Variable(exp.hash())); + } + + builder.withCustomFunctions(Functions.getInstance().getAllFunction()); + log.info("Built expression "+expression); + } + + /* + * Replaces expressions of the form: + * a[x:y] with a hash and creates an associated RangeExpression from x to y + */ + private String subTimeRanges(String str){ + + Pattern p = Pattern.compile(variableRegex()+"\\[[^\\]]*:.*?\\]"); + Matcher m = p.matcher(str); + + // for each match, make a new custom expression (in subExpressions) with a hashed name + // and replace the expression and variable in the original expression string with [hash]. + while (m.find()){ + String match = m.group(); + + int start = match.indexOf("["); + int end = match.indexOf("]"); + int colon = match.indexOf(":"); + + String startTime = match.substring(start+1, colon); + String endTime = match.substring(colon+1, end); + String variableType = match.substring(0, start); + + RangeExpression exp = new RangeExpression(doc, startTime, endTime, variableType); + subExpressions.add( exp ); + str = str.replace(match, exp.hash()); + } + return str; + } + + /* + * Replaces expressions of the form + * a[x] with a hash and creates an associated IndexExpression with x + */ + private String subTimeIndexes(String str){ + + // find any matches of the time-indexed variable notation, e.g. m[1.2] for mass at 1.2 sec + Pattern p = Pattern.compile(variableRegex()+"\\[[^:]*?\\]"); + Matcher m = p.matcher(str); + + // for each match, make a new custom expression (in subExpressions) with a hashed name + // and replace the expression and variable in the original expression string with [hash]. + while (m.find()){ + String match = m.group(); + // just the index part (in the square brackets) : + String indexText = match.substring(match.indexOf("[")+1, match.length()-1); + // just the flight data type + String typeText = match.substring(0, match.indexOf("[")); + + // Do the replacement and add a corresponding new IndexExpression to the list + IndexExpression exp = new IndexExpression(doc, indexText, typeText); + subExpressions.add( exp ); + str = str.replace(match, exp.hash()); + } + return str; + } + + /* + * Returns a string of the form (t|a| ... ) with all variable symbols available + * This is useful for regex evaluation + */ + protected String variableRegex(){ + String regex = "("; + for (String s : getAllSymbols()){ + regex = regex + s + "|"; + } + regex = regex.substring(0, regex.length()-1) + ")"; + return regex; + } + + // get a list of all the names of all the available variables + protected ArrayList getAllNames(){ + ArrayList names = new ArrayList(); + for (FlightDataType type : FlightDataType.ALL_TYPES) + names.add(type.getName()); + + if (doc != null){ + ArrayList expressions = doc.getCustomExpressions(); + for (CustomExpression exp : expressions ){ + if (exp != this) + names.add(exp.getName()); + } + } + return names; + } + + // get a list of all the symbols of the available variables ignoring this one + protected ArrayList getAllSymbols(){ + ArrayList symbols = new ArrayList(); + for (FlightDataType type : FlightDataType.ALL_TYPES) + symbols.add(type.getSymbol()); + + if (doc != null){ + for (CustomExpression exp : doc.getCustomExpressions() ){ + if (exp != this) + symbols.add(exp.getSymbol()); + } + } + return symbols; + } + + public boolean checkSymbol(){ + if (symbol.trim().isEmpty()) + return false; + + // No bad characters + for (char c : "0123456789.,()[]{}<>:#@%^&* ".toCharArray()) + if (symbol.indexOf(c) != -1 ) + return false; + + // No operators (ignoring brackets) + for (String s : Functions.AVAILABLE_OPERATORS.keySet()){ + if (symbol.equals(s.trim().replaceAll("\\(|\\)|\\]|\\[|:", ""))) + return false; + } + + // No already defined symbols + ArrayList symbols = getAllSymbols().clone(); + if (symbols.contains(symbol.trim())){ + int index = symbols.indexOf(symbol.trim()); + log.user("Symbol "+symbol+" already exists, found "+symbols.get(index)); + return false; + } + + return true; + } + + public boolean checkName(){ + if (name.trim().isEmpty()) + return false; + + // No characters that could mess things up saving etc + for (char c : ",()[]{}<>#".toCharArray()) + if (name.indexOf(c) != -1 ) + return false; + + ArrayList names = getAllNames().clone(); + if (names.contains(name.trim())){ + int index = names.indexOf(name.trim()); + log.user("Name "+name+" already exists, found "+names.get(index)); + return false; + } + + return true; + } + + // Currently no restrictions on unit + public boolean checkUnit(){ + return true; + } + + public boolean checkAll(){ + return checkUnit() && checkSymbol() && checkName() && checkExpression(); + } + + public String getName(){ + return name; + } + + public String getSymbol(){ + return symbol; + } + + public String getUnit(){ + return unit; + } + + public String getExpressionString(){ + return expression; + } + + /** + * Performs a basic check to see if the current expression string is valid + * This includes checking for bad characters and balanced brackets and test + * building the expression. + */ + public boolean checkExpression(){ + if (expression.trim().isEmpty()){ + return false; + } + + int round = 0, square = 0; // count of bracket openings + for (char c : expression.toCharArray()){ + switch (c) { + case '(' : round++; break; + case ')' : round--; break; + case '[' : square++; break; + case ']' : square--; break; + case ':' : + if (square <= 0){ + log.user(": found outside range expression"); + return false; + } + else break; + case '#' : return false; + case '=' : return false; + } + } + if (round != 0 || square != 0) { + log.user("Expression has unballanced brackets"); + return false; + } + + + //// Define the available variables as empty + // The built in data types + for (FlightDataType type : FlightDataType.ALL_TYPES){ + builder.withVariable(new Variable(type.getSymbol())); + } + + for (String symb : getAllSymbols()){ + builder.withVariable(new Variable(symb)); + } + + // Try to build + try { + builder.build(); + } catch (Exception e) { + log.user("Custom expression invalid : " + e.toString()); + return false; + } + + + // Otherwise, all OK + return true; + } + + public Double evaluateDouble(SimulationStatus status){ + return evaluate(status).getDoubleValue(); + } + + /* + * Builds the expression, done automatically during evaluation. Logs any errors. Returns null in case of error. + */ + protected Calculable buildExpression(){ + return buildExpression(builder); + } + + /* + * Builds a specified expression, log any errors and returns null in case of error. + */ + protected Calculable buildExpression(ExpressionBuilder b){ + Calculable calc; + try { + calc = b.build(); + } catch (UnknownFunctionException e1) { + log.user("Unknown function. Could not build custom expression "+name); + return null; + } catch (UnparsableExpressionException e1) { + log.user("Unparsable expression. Could not build custom expression "+name+". "+e1.getMessage()); + return null; + } + + return calc; + } + + /* + * Evaluate the expression using the last variable values from the simulation status. + * Returns NaN on any error. + */ + public Variable evaluate(SimulationStatus status){ + + Calculable calc = buildExpression(builder); + if (calc == null){ + return new Variable("Unknown"); + } + + // Evaluate any sub expressions and set associated variables in the calculable + for (CustomExpression expr : this.subExpressions){ + calc.setVariable( expr.evaluate(status) ); + } + + // Set all the built-in variables. Strictly we surely won't need all of them + // Going through and checking them to include only the ones used *might* give a speedup + for (FlightDataType type : status.getFlightData().getTypes()){ + double value = status.getFlightData().getLast(type); + calc.setVariable( new Variable(type.getSymbol(), value ) ); + } + + double result = Double.NaN; + try{ + result = calc.calculate().getDoubleValue(); + } + catch (java.util.EmptyStackException e){ + log.user("Unable to calculate expression "+this.expression+" due to empty stack exception"); + } + + return new Variable(name, result); + } + + /* + * Returns the new flight data type corresponding to this calculated data + * If the unit matches a SI unit string then the datatype will have the corresponding unitgroup. + * Otherwise, a fixed unit group will be created + */ + public FlightDataType getType(){ + + UnitGroup ug = UnitGroup.SIUNITS.get(unit); + if ( ug == null ){ + ug = new FixedUnitGroup(unit); + } + + FlightDataType type = FlightDataType.getType(name, symbol, ug); + + //log.debug(this.getClass().getSimpleName()+" returned type "+type.getName()+" (" + type.getSymbol() + ")" ); + + return type; + } + + /* + * Add this expression to the document if valid and not in document already + */ + public void addToDocument(){ + // Abort if exact expression already in + ArrayList expressions = doc.getCustomExpressions(); + if ( !expressions.isEmpty() ) { + // check if expression already exists + if ( expressions.contains(this) ){ + log.user("Expression already in document. This unit : "+this.getUnit()+", existing unit : "+expressions.get(0).getUnit()); + return; + } + } + + if (this.checkAll()){ + log.user("Custom expression added to rocket document"); + doc.addCustomExpression( this ); + } + } + + /* + * Removes this expression from the document, replacing it with a given new expression + */ + public void overwrite(CustomExpression newExpression){ + if (!doc.getCustomExpressions().contains(this)) + return; + else { + int index = doc.getCustomExpressions().indexOf(this); + doc.getCustomExpressions().set(index, newExpression); + } + } + + @Override + public String toString(){ + return "Custom expression : "+this.name.toString()+ " " + this.expression.toString(); + } + + @Override + /* + * Clone method makes a deep copy of everything except the reference to the document. + * If you want to apply this to another simulation, set simulation manually after cloning. + * @see java.lang.Object#clone() + */ + public Object clone() { + try { + return super.clone(); + } + catch( CloneNotSupportedException e ) + { + return new CustomExpression( doc , + new String(this.getName()), + new String(this.getSymbol()), + new String(this.getUnit()), + new String(this.getExpressionString())); + } + } + + /* + * Returns a simple all upper case string hash code with a proceeding # mark. + * Used for temporary substitution when evaluating index and range expressions. + */ + public String hash(){ + Integer hashint = new Integer(this.getExpressionString().hashCode()); + String hash = "#"; + for (char c : hashint.toString().toCharArray()){ + char newc = (char) (c + 17); + hash = hash + newc; + } + return hash; + } + + @Override + public boolean equals(Object obj){ + CustomExpression other = (CustomExpression) obj; + + return ( this.getName().equals( other.getName() ) && + this.getSymbol().equals( other.getSymbol() ) && + this.getExpressionString().equals( other.getExpressionString() ) && + this.getUnit().equals( other.getUnit() ) + ); + } + + @Override + public int hashCode() { + return hash().hashCode(); + } +} diff --git a/core/src/net/sf/openrocket/simulation/customexpression/Functions.java b/core/src/net/sf/openrocket/simulation/customexpression/Functions.java new file mode 100644 index 00000000..6e638d91 --- /dev/null +++ b/core/src/net/sf/openrocket/simulation/customexpression/Functions.java @@ -0,0 +1,269 @@ +package net.sf.openrocket.simulation.customexpression; + +import java.util.ArrayList; +import java.util.List; +import java.util.SortedMap; +import java.util.TreeMap; + +import net.sf.openrocket.l10n.Translator; +import net.sf.openrocket.logging.LogHelper; +import net.sf.openrocket.startup.Application; +import net.sf.openrocket.util.ArrayUtils; + +import de.congrace.exp4j.CustomFunction; +import de.congrace.exp4j.InvalidCustomFunctionException; +import de.congrace.exp4j.Variable; + +/* + * This is a singleton class which contains all the functions for custom expressions not provided by exp4j + */ +public class Functions { + private static Functions instance = null; + + private static final LogHelper log = Application.getLogger(); + private static final Translator trans = Application.getTranslator(); + + private List allFunctions = new ArrayList(); + + public static Functions getInstance() { + if(instance == null) { + try { + instance = new Functions(); + } catch (InvalidCustomFunctionException e) { + log.error("Invalid custom function."); + } + } + return instance; + } + + public List getAllFunction(){ + return allFunctions; + } + + // A map of available operator strings (keys) and description of function (value) + public static final SortedMap AVAILABLE_OPERATORS = new TreeMap() {{ + put("+" , trans.get("Operator.plus")); + put("-" , trans.get("Operator.minus")); + put("*" , trans.get("Operator.star")); + put("/" , trans.get("Operator.div")); + put("%" , trans.get("Operator.mod")); + put("^" , trans.get("Operator.pow")); + put("abs()" , trans.get("Operator.abs")); + put("ceil()" , trans.get("Operator.ceil")); + put("floor()" , trans.get("Operator.floor")); + put("sqrt()" , trans.get("Operator.sqrt")); + put("cbrt()" , trans.get("Operator.cbrt")); + put("exp()" , trans.get("Operator.exp")); + put("log()" , trans.get("Operator.ln")); + put("sin()" , trans.get("Operator.sin")); + put("cos()" , trans.get("Operator.cos")); + put("tan()" , trans.get("Operator.tan")); + put("asin()" , trans.get("Operator.asin")); + put("acos()" , trans.get("Operator.acos")); + put("atan()" , trans.get("Operator.atan")); + put("sinh()" , trans.get("Operator.hsin")); + put("cosh()" , trans.get("Operator.hcos")); + put("tanh()" , trans.get("Operator.htan")); + put("log10()" , trans.get("Operator.log10")); + put("round()" , trans.get("Operator.round")); + put("random()" , trans.get("Operator.random")); + put("expm1()" , trans.get("Operator.expm1")); + put("mean([:])" , trans.get("Operator.mean")); + put("min([:])" , trans.get("Operator.min")); + put("max([:])" , trans.get("Operator.max")); + put("var([:])" , trans.get("Operator.var")); + put("rms([:])" , trans.get("Operator.rms")); + put("stdev([:])", trans.get("Operator.stdev")); + put("lclip(,)" , trans.get("Operator.lclip")); + put("uclip(,)" , trans.get("Operator.uclip")); + put("binf([:],,)" , trans.get("Operator.binf")); + put("trapz([:])" , trans.get("Operator.trapz")); + put("tnear([:],)" , trans.get("Operator.tnear")); + }}; + + + protected Functions() throws InvalidCustomFunctionException { + + CustomFunction meanFn = new CustomFunction("mean") { + @Override + public Variable applyFunction(List vars) { + double[] vals; + try{ + vals = vars.get(0).getArrayValue(); + } catch (Exception e) { + return new Variable("Invalid"); + } + return new Variable("double MEAN result, ", ArrayUtils.mean(vals)); + } + }; + allFunctions.add(meanFn); + + CustomFunction minFn = new CustomFunction("min") { + @Override + public Variable applyFunction(List vars) { + double[] vals; + try{ + vals = vars.get(0).getArrayValue(); + } catch (Exception e) { + return new Variable("Invalid"); + } + return new Variable("double MIN result, ", ArrayUtils.min(vals)); + } + }; + allFunctions.add(minFn); + + CustomFunction maxFn = new CustomFunction("max") { + @Override + public Variable applyFunction(List vars) { + double[] vals; + try{ + vals = vars.get(0).getArrayValue(); + } catch (Exception e) { + return new Variable("Invalid"); + } + return new Variable("double MAX result, ", ArrayUtils.max(vals)); + } + }; + allFunctions.add(maxFn); + + CustomFunction varFn = new CustomFunction("var") { + @Override + public Variable applyFunction(List vars) { + double[] vals; + try{ + vals = vars.get(0).getArrayValue(); + } catch (Exception e) { + return new Variable("Invalid"); + } + return new Variable("double VAR result, ", ArrayUtils.variance(vals)); + } + }; + allFunctions.add(varFn); + + CustomFunction stdevFn = new CustomFunction("stdev") { + @Override + public Variable applyFunction(List vars) { + double[] vals; + try{ + vals = vars.get(0).getArrayValue(); + } catch (Exception e) { + return new Variable("Invalid"); + } + return new Variable("double STDEV result, ", ArrayUtils.stdev(vals)); + } + }; + allFunctions.add(stdevFn); + + CustomFunction rmsFn = new CustomFunction("rms") { + @Override + public Variable applyFunction(List vars) { + double[] vals; + try{ + vals = vars.get(0).getArrayValue(); + } catch (Exception e) { + return new Variable("Invalid"); + } + return new Variable("double RMS result, ", ArrayUtils.rms(vals)); + } + }; + allFunctions.add(rmsFn); + + CustomFunction lclipFn = new CustomFunction("lclip",2) { + @Override + public Variable applyFunction(List vars) { + double val, clip; + try{ + val = vars.get(0).getDoubleValue(); + clip = vars.get(1).getDoubleValue(); + } catch (Exception e) { + return new Variable("Invalid"); + } + if (val < clip){ + val = clip; + } + return new Variable("double LCLIP result, ", val); + } + }; + allFunctions.add(lclipFn); + + CustomFunction uclipFn = new CustomFunction("uclip",2) { + @Override + public Variable applyFunction(List vars) { + double val, clip; + try{ + val = vars.get(0).getDoubleValue(); + clip = vars.get(1).getDoubleValue(); + } catch (Exception e) { + return new Variable("Invalid"); + } + if (val > clip){ + val = clip; + } + return new Variable("double UCLIP result, ", val); + } + }; + allFunctions.add(uclipFn); + + CustomFunction binfFn = new CustomFunction("binf", 3) { + @Override + public Variable applyFunction(List vars) { + double[] range; + double min, max; + try{ + range = vars.get(0).getArrayValue(); + min = vars.get(1).getDoubleValue(); + max = vars.get(2).getDoubleValue(); + } catch (Exception e) { + return new Variable("Invalid"); + } + + int ins = 0; + for (double x: range){ + if (x < max && x > min){ + ins++; + } + } + return new Variable("double BINF result", (double) ins/ (double) range.length); + } + }; + allFunctions.add(binfFn); + + CustomFunction rombintFn = new CustomFunction("trapz") { + @Override + public Variable applyFunction(List vars) { + double[] range; + double dt = 0; + try{ + range = vars.get(0).getArrayValue(); + dt = vars.get(0).getStep(); + } catch (Exception e) { + return new Variable("Invalid"); + } + + return new Variable("double TRAPZ result", ArrayUtils.trapz(range, dt) ); + } + }; + allFunctions.add(rombintFn); + + CustomFunction tnearFn = new CustomFunction("tnear", 2) { + @Override + public Variable applyFunction(List vars) { + double[] range; + double dt = 0; + double start = 0; + double near = 0; + try{ + range = vars.get(0).getArrayValue(); + dt = vars.get(0).getStep(); + start = vars.get(0).getStart(); + near = vars.get(1).getDoubleValue(); + } catch (Exception e) { + return new Variable("Invalid"); + } + + return new Variable("double TNEAR result", ArrayUtils.tnear(range, near, start, dt) ); + } + }; + allFunctions.add(tnearFn); + } +} diff --git a/core/src/net/sf/openrocket/simulation/customexpression/IndexExpression.java b/core/src/net/sf/openrocket/simulation/customexpression/IndexExpression.java new file mode 100644 index 00000000..ab7e1572 --- /dev/null +++ b/core/src/net/sf/openrocket/simulation/customexpression/IndexExpression.java @@ -0,0 +1,54 @@ +package net.sf.openrocket.simulation.customexpression; + +import java.util.List; + +import de.congrace.exp4j.Calculable; +import de.congrace.exp4j.Variable; +import net.sf.openrocket.document.OpenRocketDocument; +import net.sf.openrocket.logging.LogHelper; +import net.sf.openrocket.simulation.customexpression.CustomExpression; +import net.sf.openrocket.simulation.FlightDataType; +import net.sf.openrocket.simulation.SimulationStatus; +import net.sf.openrocket.startup.Application; +import net.sf.openrocket.util.LinearInterpolator; + +public class IndexExpression extends CustomExpression { + + FlightDataType type; + private static final LogHelper log = Application.getLogger(); + + public IndexExpression(OpenRocketDocument doc, String indexText, String typeText){ + super(doc); + + setExpression(indexText); + this.setName(""); + this.setSymbol(typeText); + + } + + @Override + public Variable evaluate(SimulationStatus status){ + + Calculable calc = buildExpression(); + if (calc == null){ + return new Variable("Unknown"); + } + + // From the given datatype, get the time and function values and make an interpolator + FlightDataType type = getType(); + List data = status.getFlightData().get(type); + List time = status.getFlightData().get(FlightDataType.TYPE_TIME); + LinearInterpolator interp = new LinearInterpolator(time, data); + + // Evaluate this expression to get the t value + try{ + double tvalue = calc.calculate().getDoubleValue(); + return new Variable(hash(), interp.getValue( tvalue ) ); + } + catch (java.util.EmptyStackException e){ + log.user("Unable to calculate time index for indexed expression "+getExpressionString()+" due to empty stack exception"); + return new Variable("Unknown"); + } + + } +} diff --git a/core/src/net/sf/openrocket/simulation/customexpression/RangeExpression.java b/core/src/net/sf/openrocket/simulation/customexpression/RangeExpression.java new file mode 100644 index 00000000..c479b310 --- /dev/null +++ b/core/src/net/sf/openrocket/simulation/customexpression/RangeExpression.java @@ -0,0 +1,116 @@ +/* + * A range expression contains two indexExpressions for the beginning and end time index of a range + */ + +package net.sf.openrocket.simulation.customexpression; + +import java.util.List; + +import de.congrace.exp4j.Calculable; +import de.congrace.exp4j.ExpressionBuilder; +import de.congrace.exp4j.Variable; +import net.sf.openrocket.document.OpenRocketDocument; +import net.sf.openrocket.logging.LogHelper; +import net.sf.openrocket.simulation.customexpression.CustomExpression; +import net.sf.openrocket.simulation.FlightDataType; +import net.sf.openrocket.simulation.SimulationStatus; +import net.sf.openrocket.startup.Application; +import net.sf.openrocket.util.ArrayUtils; +import net.sf.openrocket.util.LinearInterpolator; +import net.sf.openrocket.util.MathUtil; + +public class RangeExpression extends CustomExpression { + private static final LogHelper log = Application.getLogger(); + + private ExpressionBuilder startBuilder, endBuilder; + + public RangeExpression(OpenRocketDocument doc, String startTime, String endTime, String variableType) { + super(doc); + + if (startTime.isEmpty()){ + startTime = "0"; + } + if (endTime.isEmpty()){ + endTime = "t"; + } + + this.setName(""); + this.setSymbol(variableType); + this.setExpressions(startTime, endTime); + this.expression = variableType+startTime+endTime; // this is used just for generating the hash + + log.info("New range expression, "+startTime + " to "+endTime); + + } + + /* + * Sets the actual expression string for this expression + */ + private void setExpressions(String start, String end){ + + startBuilder = new ExpressionBuilder(start); + endBuilder = new ExpressionBuilder(end); + for (String n : getAllSymbols()){ + startBuilder.withVariable(new Variable(n)); + endBuilder.withVariable(new Variable(n)); + } + } + + @Override + public Variable evaluate(SimulationStatus status){ + + Calculable startCalc = buildExpression(startBuilder); + Calculable endCalc = buildExpression(endBuilder); + if (startCalc == null || endCalc == null){ + return new Variable("Unknown"); + } + + // Set the variables in the start and end calculators + for (FlightDataType type : status.getFlightData().getTypes()){ + double value = status.getFlightData().getLast(type); + startCalc.setVariable( new Variable(type.getSymbol(), value ) ); + endCalc.setVariable( new Variable(type.getSymbol(), value ) ); + } + + // From the given datatype, get the time and function values and make an interpolator + FlightDataType type = getType(); + + List data = status.getFlightData().get(type); + List time = status.getFlightData().get(FlightDataType.TYPE_TIME); + LinearInterpolator interp = new LinearInterpolator(time, data); + + // Evaluate the expression to get the start and end of the range + double startTime, endTime; + try{ + startTime = startCalc.calculate().getDoubleValue(); + startTime = MathUtil.clamp(startTime, 0, Double.MAX_VALUE); + + endTime = endCalc.calculate().getDoubleValue(); + endTime = MathUtil.clamp(endTime, 0, time.get(time.size()-1)); + } + catch (java.util.EmptyStackException e){ + log.user("Unable to calculate time index for range expression "+getSymbol()+" due to empty stack exception"); + return new Variable("Unknown"); + } + + // generate an array representing the range + double step = status.getSimulationConditions().getSimulation().getOptions().getTimeStep(); + double[] t = ArrayUtils.range(startTime, endTime, step); + double[] y = new double[t.length]; + int i = 0; + for (double tval : t){ + y[i] = interp.getValue( tval ); + i++; + } + + Variable result; + if (y.length == 0){ + result = new Variable("Unknown"); + } + else { + result = new Variable(hash(), y, startTime, step); + } + + return result; + } +} diff --git a/core/src/net/sf/openrocket/unit/FixedUnitGroup.java b/core/src/net/sf/openrocket/unit/FixedUnitGroup.java index a0984961..8cb6fde4 100644 --- a/core/src/net/sf/openrocket/unit/FixedUnitGroup.java +++ b/core/src/net/sf/openrocket/unit/FixedUnitGroup.java @@ -24,6 +24,10 @@ public class FixedUnitGroup extends UnitGroup { return new GeneralUnit(1, unitString); } + public Unit getSIUnit(){ + return new GeneralUnit(1, unitString); + } + public boolean contains(Unit u){ return true; } diff --git a/core/src/net/sf/openrocket/unit/UnitGroup.java b/core/src/net/sf/openrocket/unit/UnitGroup.java index 88ab4a77..48ae67a9 100644 --- a/core/src/net/sf/openrocket/unit/UnitGroup.java +++ b/core/src/net/sf/openrocket/unit/UnitGroup.java @@ -27,6 +27,7 @@ public class UnitGroup { public static final UnitGroup UNITS_MOTOR_DIMENSIONS; public static final UnitGroup UNITS_LENGTH; + public static final UnitGroup UNITS_ALL_LENGTHS; public static final UnitGroup UNITS_DISTANCE; public static final UnitGroup UNITS_AREA; @@ -63,11 +64,17 @@ public class UnitGroup { public static final UnitGroup UNITS_ROUGHNESS; public static final UnitGroup UNITS_COEFFICIENT; + public static final UnitGroup UNITS_FREQUENCY; - // public static final UnitGroup UNITS_FREQUENCY; + public static final UnitGroup UNITS_ENERGY; + public static final UnitGroup UNITS_POWER; + public static final UnitGroup UNITS_MOMENTUM; + public static final UnitGroup UNITS_VOLTAGE; + public static final UnitGroup UNITS_CURRENT; - public static final Map UNITS; + public static final Map UNITS; // keys such as "LENGTH", "VELOCITY" + public static final Map SIUNITS; // keys such a "m", "m/s" /* @@ -80,6 +87,36 @@ public class UnitGroup { UNITS_NONE = new UnitGroup(); UNITS_NONE.addUnit(Unit.NOUNIT2); + UNITS_ENERGY = new UnitGroup(); + UNITS_ENERGY.addUnit(new GeneralUnit(1, "J")); + UNITS_ENERGY.addUnit(new GeneralUnit(1e-7, "erg")); + UNITS_ENERGY.addUnit(new GeneralUnit(1.055, "BTU")); + UNITS_ENERGY.addUnit(new GeneralUnit(4.184, "cal")); + UNITS_ENERGY.addUnit(new GeneralUnit(1.3558179483314, "ft"+DOT+"lbf")); + UNITS_ENERGY.setDefaultUnit(0); + + UNITS_POWER = new UnitGroup(); + UNITS_POWER.addUnit(new GeneralUnit(1e-3, "mW")); + UNITS_POWER.addUnit(new GeneralUnit(1, "W")); + UNITS_POWER.addUnit(new GeneralUnit(1e3, "kW")); + UNITS_POWER.addUnit(new GeneralUnit(1e-7, "ergs")); + UNITS_POWER.addUnit(new GeneralUnit(745.699872, "hp")); + UNITS_POWER.setDefaultUnit(1); + + UNITS_MOMENTUM = new UnitGroup(); + UNITS_MOMENTUM.addUnit(new GeneralUnit(1, "kg"+DOT+"m/s")); + UNITS_MOMENTUM.setDefaultUnit(0); + + UNITS_VOLTAGE = new UnitGroup(); + UNITS_VOLTAGE.addUnit(new GeneralUnit(1e-3, "mV")); + UNITS_VOLTAGE.addUnit(new GeneralUnit(1, "V")); + UNITS_VOLTAGE.setDefaultUnit(1); + + UNITS_CURRENT = new UnitGroup(); + UNITS_CURRENT.addUnit(new GeneralUnit(1e-3, "mA")); + UNITS_CURRENT.addUnit(new GeneralUnit(1, "A")); + UNITS_CURRENT.setDefaultUnit(1); + UNITS_LENGTH = new UnitGroup(); UNITS_LENGTH.addUnit(new GeneralUnit(0.001, "mm")); UNITS_LENGTH.addUnit(new GeneralUnit(0.01, "cm")); @@ -90,6 +127,7 @@ public class UnitGroup { UNITS_LENGTH.setDefaultUnit(1); UNITS_MOTOR_DIMENSIONS = new UnitGroup(); + UNITS_MOTOR_DIMENSIONS.addUnit(new GeneralUnit(1, "m")); // just added UNITS_MOTOR_DIMENSIONS.addUnit(new GeneralUnit(0.001, "mm")); UNITS_MOTOR_DIMENSIONS.addUnit(new GeneralUnit(0.01, "cm")); UNITS_MOTOR_DIMENSIONS.addUnit(new GeneralUnit(0.0254, "in")); @@ -103,6 +141,19 @@ public class UnitGroup { UNITS_DISTANCE.addUnit(new GeneralUnit(1609.344, "mi")); UNITS_DISTANCE.addUnit(new GeneralUnit(1852, "nmi")); + UNITS_ALL_LENGTHS = new UnitGroup(); + UNITS_ALL_LENGTHS.addUnit(new GeneralUnit(0.001, "mm")); + UNITS_ALL_LENGTHS.addUnit(new GeneralUnit(0.01, "cm")); + UNITS_ALL_LENGTHS.addUnit(new GeneralUnit(1, "m")); + UNITS_ALL_LENGTHS.addUnit(new GeneralUnit(1000, "km")); + UNITS_ALL_LENGTHS.addUnit(new GeneralUnit(0.0254, "in")); + UNITS_ALL_LENGTHS.addUnit(new FractionalUnit(0.0254, "in/64", "in", 64, 1d / 16d, 0.5d / 64d)); + UNITS_ALL_LENGTHS.addUnit(new GeneralUnit(0.3048, "ft")); + UNITS_ALL_LENGTHS.addUnit(new GeneralUnit(0.9144, "yd")); + UNITS_ALL_LENGTHS.addUnit(new GeneralUnit(1609.344, "mi")); + UNITS_ALL_LENGTHS.addUnit(new GeneralUnit(1852, "nmi")); + UNITS_ALL_LENGTHS.setDefaultUnit(2); + UNITS_AREA = new UnitGroup(); UNITS_AREA.addUnit(new GeneralUnit(pow2(0.001), "mm" + SQUARED)); UNITS_AREA.addUnit(new GeneralUnit(pow2(0.01), "cm" + SQUARED)); @@ -113,6 +164,7 @@ public class UnitGroup { UNITS_STABILITY = new UnitGroup(); + UNITS_STABILITY.addUnit(new GeneralUnit(1, "m")); UNITS_STABILITY.addUnit(new GeneralUnit(0.001, "mm")); UNITS_STABILITY.addUnit(new GeneralUnit(0.01, "cm")); UNITS_STABILITY.addUnit(new GeneralUnit(0.0254, "in")); @@ -234,6 +286,7 @@ public class UnitGroup { UNITS_ROUGHNESS = new UnitGroup(); + UNITS_ROUGHNESS.addUnit(new GeneralUnit(1, "m")); // just added UNITS_ROUGHNESS.addUnit(new GeneralUnit(0.000001, MICRO + "m")); UNITS_ROUGHNESS.addUnit(new GeneralUnit(0.0000254, "mil")); @@ -243,18 +296,20 @@ public class UnitGroup { // This is not used by OpenRocket, and not extensively tested: - // UNITS_FREQUENCY = new UnitGroup(); + UNITS_FREQUENCY = new UnitGroup(); // UNITS_FREQUENCY.addUnit(new GeneralUnit(1, "s")); // UNITS_FREQUENCY.addUnit(new GeneralUnit(0.001, "ms")); // UNITS_FREQUENCY.addUnit(new GeneralUnit(0.000001, MICRO + "s")); - // UNITS_FREQUENCY.addUnit(new FrequencyUnit(1, "Hz")); - // UNITS_FREQUENCY.addUnit(new FrequencyUnit(1000, "kHz")); - // UNITS_FREQUENCY.setDefaultUnit(3); + UNITS_FREQUENCY.addUnit(new FrequencyUnit(.001, "mHz")); + UNITS_FREQUENCY.addUnit(new FrequencyUnit(1, "Hz")); + UNITS_FREQUENCY.addUnit(new FrequencyUnit(1000, "kHz")); + UNITS_FREQUENCY.setDefaultUnit(1); HashMap map = new HashMap(); map.put("NONE", UNITS_NONE); map.put("LENGTH", UNITS_LENGTH); + map.put("ALL_LENGTHS", UNITS_ALL_LENGTHS); map.put("MOTOR_DIMENSIONS", UNITS_MOTOR_DIMENSIONS); map.put("DISTANCE", UNITS_DISTANCE); map.put("VELOCITY", UNITS_VELOCITY); @@ -278,8 +333,36 @@ public class UnitGroup { map.put("RELATIVE", UNITS_RELATIVE); map.put("ROUGHNESS", UNITS_ROUGHNESS); map.put("COEFFICIENT", UNITS_COEFFICIENT); + map.put("VOLTAGE", UNITS_VOLTAGE); + map.put("CURRENT", UNITS_CURRENT); + map.put("ENERGY", UNITS_ENERGY); + map.put("POWER", UNITS_POWER); + map.put("MOMENTUM", UNITS_MOMENTUM); + map.put("FREQUENCY", UNITS_FREQUENCY); UNITS = Collections.unmodifiableMap(map); + + HashMap simap = new HashMap(); + simap.put("m", UNITS_ALL_LENGTHS); + simap.put("m^2", UNITS_AREA); + simap.put("m/s", UNITS_VELOCITY); + simap.put("m/s^2", UNITS_ACCELERATION); + simap.put("kg", UNITS_MASS); + simap.put("kg m^2", UNITS_INERTIA); + simap.put("kg/m^3", UNITS_DENSITY_BULK); + simap.put("N", UNITS_FORCE); + simap.put("Ns", UNITS_IMPULSE); + simap.put("s", UNITS_FLIGHT_TIME); + simap.put("Pa", UNITS_PRESSURE); + simap.put("V", UNITS_VOLTAGE); + simap.put("A", UNITS_CURRENT); + simap.put("J", UNITS_ENERGY); + simap.put("W", UNITS_POWER); + simap.put("kg m/s", UNITS_MOMENTUM); + simap.put("Hz", UNITS_FREQUENCY); + simap.put("K", UNITS_TEMPERATURE); + + SIUNITS = Collections.unmodifiableMap(simap); } public static void setDefaultMetricUnits() { @@ -393,7 +476,14 @@ public class UnitGroup { defaultUnit = n; } - + public Unit getSIUnit(){ + for (Unit u : units){ + if (u.multiplier == 1){ + return u; + } + } + return UNITS_NONE.getDefaultUnit(); + } /** * Find a unit by approximate unit name. Only letters and (ordinary) numbers are @@ -510,7 +600,28 @@ public class UnitGroup { return this.getDefaultUnit().toValue(value); } + @Override + public String toString(){ + return this.getClass().getSimpleName()+":"+this.getSIUnit().toString(); + } + @Override + public boolean equals(Object o){ + UnitGroup u = (UnitGroup) o; + int size = units.size(); + if (size != u.units.size()){ + return false; + } + + for (int i=0; iLinearInterpolator with the given points. * @@ -27,8 +29,11 @@ public class LinearInterpolator implements Cloneable { public LinearInterpolator(double[] x, double[] y) { addPoints(x,y); } - - + + public LinearInterpolator(List x, List y) { + addPoints(x,y); + } + /** * Add the point to the linear interpolation. * @@ -38,7 +43,7 @@ public class LinearInterpolator implements Cloneable { public void addPoint(double x, double y) { sortMap.put(x, y); } - + /** * Add the points to the linear interpolation. * @@ -56,66 +61,50 @@ public class LinearInterpolator implements Cloneable { sortMap.put(x[i],y[i]); } } - - - + + public void addPoints(List x, List y){ + if (x.size() != y.size()) { + throw new IllegalArgumentException("Array lengths do not match, x="+x.size() + + " y="+y.size()); + } + for (int i=0; i < x.size(); i++) { + sortMap.put( (Double) x.toArray()[i], (Double) y.toArray()[i]); + } + } + + public double getValue(double x) { + Map.Entry e1, e2; double x1, x2; - Double y1, y2; - // Froyo does not support floorEntry, firstEntry or higherEntry. We instead have to - // resort to using other more awkward methods. - - y1 = sortMap.get(x); - - if ( y1 != null ) { - // Wow, x was a key in the map. Such luck. - return y1.doubleValue(); - } - - // we now know that x is not in the map, so we need to find the lower and higher keys. + double y1, y2; - // let's just make certain that our map is not empty. - if ( sortMap.isEmpty() ) { - throw new IllegalStateException("No points added yet to the interpolator."); - } + e1 = sortMap.floorEntry(x); - // firstKey in the map - cannot be null since the map is not empty. - Double firstKey = sortMap.firstKey(); - - // x is smaller than the first entry in the map. - if ( x < firstKey.doubleValue() ) { - y1 = sortMap.get(firstKey); - return y1.doubleValue(); + if (e1 == null) { + // x smaller than any value in the set + e1 = sortMap.firstEntry(); + if (e1 == null) { + throw new IllegalStateException("No points added yet to the interpolator."); + } + return e1.getValue(); } - // floor key is the largest key smaller than x - since we have at least one key, - // and x>=firstKey, we know that floorKey != null. - Double floorKey = sortMap.subMap(firstKey, x).lastKey(); - - x1 = floorKey.doubleValue(); - y1 = sortMap.get(floorKey); - - // Now we need to find the key that is greater or equal to x - SortedMap tailMap = sortMap.tailMap(x); + x1 = e1.getKey(); + e2 = sortMap.higherEntry(x1); - // Check if x is bigger than all the entries. - if ( tailMap.isEmpty() ) { - return y1.doubleValue(); + if (e2 == null) { + // x larger than any value in the set + return e1.getValue(); } - Double ceilKey = tailMap.firstKey(); - // Check if x is bigger than all the entries. - if ( ceilKey == null ) { - return y1.doubleValue(); - } + x2 = e2.getKey(); + y1 = e1.getValue(); + y2 = e2.getValue(); - x2 = ceilKey.doubleValue(); - y2 = sortMap.get(ceilKey); - return (x - x1)/(x2-x1) * (y2-y1) + y1; } - - + + public double[] getXPoints() { double[] x = new double[sortMap.size()]; Iterator iter = sortMap.keySet().iterator(); @@ -124,8 +113,8 @@ public class LinearInterpolator implements Cloneable { } return x; } - - + + @SuppressWarnings("unchecked") @Override public LinearInterpolator clone() { @@ -138,4 +127,25 @@ public class LinearInterpolator implements Cloneable { } } + + public static void main(String[] args) { + LinearInterpolator interpolator = new LinearInterpolator( + new double[] {1, 1.5, 2, 4, 5}, + new double[] {0, 1, 0, 2, 2} + ); + + for (double x=0; x < 6; x+=0.1) { + System.out.printf("%.1f: %.2f\n", x, interpolator.getValue(x)); + } + + // Should be the same + + ArrayList time = new ArrayList( Arrays.asList( new Double[] {1.0, 1.5, 2.0, 4.0, 5.0} )); + ArrayList y = new ArrayList( Arrays.asList( new Double[] {0.0, 1.0, 0.0, 2.0, 2.0} )); + + LinearInterpolator interpolator2 = new LinearInterpolator(time,y); + for (double x=0; x < 6; x+=0.1) { + System.out.printf("%.1f: %.2f\n", x, interpolator2.getValue(x)); + } + } } diff --git a/core/src/net/sf/openrocket/util/MathUtil.java b/core/src/net/sf/openrocket/util/MathUtil.java index c88e8585..09a31689 100644 --- a/core/src/net/sf/openrocket/util/MathUtil.java +++ b/core/src/net/sf/openrocket/util/MathUtil.java @@ -310,7 +310,7 @@ public class MathUtil { return sorted.get(n / 2).doubleValue(); } } - + /** * Use interpolation to determine the value of the function at point t. * Current implementation uses simple linear interpolation. The domain diff --git a/core/src/net/sf/openrocket/util/TextUtil.java b/core/src/net/sf/openrocket/util/TextUtil.java index 0189545e..0b86876d 100644 --- a/core/src/net/sf/openrocket/util/TextUtil.java +++ b/core/src/net/sf/openrocket/util/TextUtil.java @@ -184,4 +184,17 @@ public class TextUtil { s = s.replace(">", ">"); return s; } + + /* + * Returns a word-wrapped version of given input string using HTML syntax, wrapped to len characters. + */ + public static String wrap(String in,int len) { + in=in.trim(); + if(in.length()"+in.substring(0,place).trim()+"
"+wrap(in.substring(place),len); + } + } diff --git a/core/test/net/sf/openrocket/simulation/customexpression/TestExpressions.java b/core/test/net/sf/openrocket/simulation/customexpression/TestExpressions.java new file mode 100644 index 00000000..d67bca0a --- /dev/null +++ b/core/test/net/sf/openrocket/simulation/customexpression/TestExpressions.java @@ -0,0 +1,23 @@ +package net.sf.openrocket.simulation.customexpression; + +import org.junit.Test; + +import static org.junit.Assert.*; +import net.sf.openrocket.document.OpenRocketDocument; +import net.sf.openrocket.rocketcomponent.Rocket; + +public class TestExpressions { + + @Test + public void testExpressions() { + // TODO Auto-generated constructor stub + + OpenRocketDocument doc = new OpenRocketDocument(new Rocket()); + + //CustomExpression exp = new CustomExpression(doc, "Kinetic energy", "Ek", "J", ".5*m*Vt^2"); + + CustomExpression exp = new CustomExpression(doc, "Average mass", "Mavg", "kg", "mean(m[0:t])"); + System.out.println( exp.getExpressionString() ); + + } +} diff --git a/core/test/net/sf/openrocket/util/MathUtilTest.java b/core/test/net/sf/openrocket/util/MathUtilTest.java index f11ba89c..6b9c1e1a 100644 --- a/core/test/net/sf/openrocket/util/MathUtilTest.java +++ b/core/test/net/sf/openrocket/util/MathUtilTest.java @@ -13,6 +13,23 @@ public class MathUtilTest { public static final double EPS = 0.00000000001; + /* + @Test + public void rangeTest() { + double[] a; + + a = MathUtil.range(0, 10, 2); + assertEquals(0, a[0], 0); + assertEquals(10, a[5], 0); + assertEquals(6, a.length, 0); + + a = MathUtil.range(1, 2, 2); + assertEquals(1, a[0], 0); + assertEquals(1, a.length, 0); + + } + */ + @Test public void miscMathTest() { -- 2.30.2