M?GYHzPG`zS?BQyr7w55?2v8=(fKilIzjHGU*CZ^KiaK+)ac4O
zj|}i<%$su*H$Cq_>=-6=jv}uuroER2ALD$XFchzxmZ#peH@xLZSBY|k
z&{GAnCCJcK)y>wdi%%8i8$Y2K0kxQ~`4p=r#-oq2p_J@+-TO~tG*S~gl{DaEN3hIrA&v=BZ9GpO)WBC>EBC&_V{ikY+DjxB``?of4u}5dfIcE$=p?#
zNfh5pBGC1u&qAE9be9q}(7oeP)~qYc)k=2<7RHwzR6?I6y;RQ|lq9k$i6K^=1#;5vh515
zRC?O6yX^*Gt}m5EmLf{Zg~%#(uMvFHx{+blB<)rP_+fnSCNY9KP(Rj&l3kE6&FY>S(S-YcXVnXj@Ccv}hb--5Y)({>ytaL>f-`NmyL
zpbq#H5|Yubv_4J(_v2<9W(e>j%r;6NS>QLwdL?lq6dRR{F5E3u_$DhbjgMMVd@xca
z1?`xij~1lJZcBIt?eUVYds13vXjd%FCH)+xSAuz`1!(zeE
zbeGJq&83(FO}PfV>}^INW?_cSbA6Q+ly^tZV(E``9)tQPhbW^rRV*WbzY|>7?nV}3
zE=QhMT&j@moc0iW4Nv1Bdh41Rr1KQ!7HN%$B?H?n})
z0_^TNSS@_k4eYZ}#AH8M|qCBbc8?l^{QGIqvn
z-|c4<;yQ-yAe}p!k#*9qrohtNMKaivxtiz!v78gKo|I!!<*L6&1K&h8?1~c+-HSA&
zFCv3tyO1QlHe_7mu=l+NBx2kT39pazjth`w3*+qJkwwP8mwEdBuNaOj#jP{L%W=Gd
zBnCFRyx`tKHPZO6!_HQ#agSCcO=t!^#0~FhE+>e){656}oQ^c|wi?t^h$r=^cRWGN
z^+wX7Cl-jNFS0h_Xzz0j($Ee-deQ?pooI0wdFtD|&s~VSx&ukj*hc>Pjuk3Ys8FH8
b8N>eo8=(qLl)QVr00000NkvXXu0mjfYrXcJR;2{X)$ovejy5oJrZvWMbPAxo%G
zs3?-Cq!d}AsQBHZ^49x!uIu-EfA62qxvsh9+~<7P&-tA1IrqKxWNi#U9566403cwK
zfye;B+BlG*A3$^k03#z5zzhHY4B&u30aLK20RDDB7ytu2nUjVh4DC&urHM$>8APT`U$kYlSxC?QJ?B++mIvLvb~-NbhrdITz-LiDDiBTTo6b0+)JG?2(2hP~a%
zG&0ql?7dz-Enk|KqZb&Iua^c$H5`eE7sq*dtI>z4;J(2VG~CE!H&4RGC?1@~5#2}{
zbi}}A0n-D4#_Hi{I1?g?NOIeNbdzQ9Bja(-Xgw1nSu=}6`;GP)S?TM^>f7n+n^{@v
z>saXS*EcmZGSxRS)zh~#GBrdBgB!^OkHu36IB>&ju5TAhg3tGkCsEUqMsNiSfDxFN
zI-1UVaO`_$T?+lHF4%YZ*Dm>PLdTQ#(-xfTztSl_79DE24c6JUyp6y(GqC#T3kwQojap}EiaVaG!5v05hMoCpoQ&Uq+
zPR~eJeg9q!O*Og^2n#DK2RnxV0wJI#iIi0PKex3ufQuQj1a*NykN}hm0^@?LwF5iB
zgAoS6=%?e)24`S|Kw(VaBtHb41b@FYC=3o^SQ`e|Kouwgh5!%Nq3~bk|LOj(5rF&e
zX43u}Y3AA`^StSpA+h(;wU8sDdij;_sw5uvi@hA=2;Wo%o6CbZ(pX)uq17CHZ1J1)
zW7Wcr#y|rHJD~vV+==wJi$ba^a(^5>b$L^50fEJ+y)|8&2Rop-N*R3v75$S+0rfty
z1MMF$0^dX@&-K5szt3}|VzScDv-1z9O>DqoYX|$i4hfdS$&M)6(ty$Id&qJ7fCqS8
zo2!O7{VzrkSFSPz>sRe`JiUSzL$SU7L!E-%#13FSIArr-9@sY*U|@H99e0#HyMdhkDiD|Cqts?g8Y=5F*
zpyAGtKLhTaz>cx+3ON>hD&WnTio5Sne^xl2x-k0kspcARG{&JxZBI;P{uw7~-d}7n
z<;cx<+*W@daz9(K>OMRFHvcrA`fgnE6U)m$|72(Jwc?~WY?p>>sFGiLX6e4sgbTYK
zE-4Rf?Ymc^+V>Z009R2?mq}CMpAFa`&V$sU<)t&&0`sYr`NM+ArXDxdGpaoc)__Zf
zoqOC%8qdTL@+T?7<+(#2eeVA8*U;$JeGa>SQ1H!OOb_WTe%Rg&$ovlCWPyAN`B?ow{Y?|=0YK~`TK?cQl4WXqjn$m&OMX&giw
z+&+J<{z~US%%kQ@&D2MSzL?(cy|41jKG*Y{-rkjOxFP(pTT?bV(uk7^wc89ZnBoD2
ze49PR_#I9a=Eu8txtz_1>-Lm&X7S!YJ2w*&%Q?$)B7&?{+Bx6A4@B<%dOaCYV3(;{
za}j^JQOwf|>O(g-i8`MD`LbqWn4&*%;USPO6zdT_&4w626X2d?H-hel#k9ZB~s1~%nc?K
zEuXiAKQzEWJADajn;c(yai3G@#=;2WfCK;*XG!7|iX^H5t|JHhEyEkJi>?u8Jul&PUvSqQEF4YTR>g@?i31D`UMO)5Hvkgu>yTnL?ZPM;M#k
zF|j*E2ARY3z2hDiU2YPiwXll2l0SX|Nh{6;K!4*bNLc{Jh1iNw(AI%-izp&-;b!ZM
z1+NfVkk!`U0Wm-Fjx(`G&-Q*sPx064h9h$l-r{E-6s9}}(X)+nH1@tXD|+b(qbZVb
zWU}eX;{H#HU8Z7pFikWEu~4iX(H5ir
z){z_Mf<#>m3CS>0Z?DK;RBNli60#l3f?3B49wuY7ce&M>Ex=G8W9@RCDiiEmKHOp4
zaW+`%`uoiHt7Apld0|d#zz;mYEevDWxG>T2KyYovXmg7wAa!)j!-c=&0j1;dEwXl~
z@KSa}1_F{iYm-_T+k=aWw=~&PCLj1&H0(uj+P?CU_E57N|KyVnBae#gWtHt-V)&wp
zMvK!+6J*MQo!znzwb=EZb|_BMf8=F;B`{v^%PkY1*u>%oo^E&KUZQzuapu*Sp8UFm
z(z=9YhRiSH&X+~^WS9op)v;F)Oe*m5oS9RhU$lW?4Q&zC*Nm1E+j!=(o=)wEnZn3U
zD_be*Xc{oU+>c2nn&k%U*0Cbv3$X4PYX$O={k)d-YDJ
z@8}ebbv_xszu}O4)RSYJydTsf<$OvN@Qb|fKJD!{BXCRGmBAMJvE3Su`8Ue3I^
zWyJSo8-Y9U+_gT2*sq1B$6QReWKHP6ldKnz^E04N|CqIJAg*zyD0JbMbr
zavd?4$#Hd4e*S@}m-+@rI;`$K&1b}PkI?Lfx?If~$M?9J@3rEM4TKvhi|Zu3Haj^w
zQ}IHz-zsZXEl|Lqwj^`imCaSPkp>mKW^B{zqC3h3Mdjagd&`?iykv=>o-W=T@!r~a
z*SBTw!#EElSKH+rGp??+N>BV08+zl`(zxHee?<=%&avu0%LNRB(j&JU1ZR#pm0B-f
zsIu;Iw{_$5BQGf)ACVh1UCca#bZYDK@J*7HExQG&o0HyV$jkpimA~v3n`QZU>~>px
zbSYxHkc-~lDC@2U>Dsu4K(&FxCmbR|46S6Yd3+&Rizsr&SqH}_C#>!awUNwxHa$3}
zd*=heg85r(=z-<40ow1cwxZsz0oF&^S6TXQ&surz4Lxh8MXAvBV^tfO?q#|geQd(+
z0^$n_qVxEsv4l-k3_@^VoV#w&HKmz7vYX*KA)8ghwmr~q@UTe4%J$ieP~LvGq0sH2
z>G_JE%Q@fE>zMR+CWAEodH6yAE`$iiTnCq5BW&gqkuvMl+Ap6sKKJwZ)e>3lOTAhZ
zDQG8)y!}}|zi0T8+taXSXnNWfZ&fK{kqe!S7Ue=KcTTfV4lu-@2n|UHTvfI!o12mr
zL>$sS7ty*4I=RG!3S}!)NhU5#BdZu=wI>L}vR6n4P$|eKV{%+$&Jl2^WZ+CxjGoBF
zX{Zi!i&a5$jab$js4*;UN>5&$-NFaFYtE$aYl&7v9d
z!gXl(LYxS^KNE_zX~59r>U6Og^rTslL&l@g`cS
zsledX&NNC+lCj|KVBHI&ZM)ju8Q|&i31e+NcT#NSJ@3v~s*Q|2Gr7Vm@xb-q%2wh;?NrqWT#`hxcdFUZ|@41FvPIw
z%-<^r&8Vv{RxTWkVMz0vYzs?+`EALmHcWwuq68CQ{CC;p&R!h%E#;EIR_!`vS79oV
z-K*r}!~ae3*kS`f4%Pvlxg8
za|=-m&m+c91Au;wtUWj@NB-VReEC9xCv+BB=lq
zkxrslxkWfU8xdyOd9d8mT
zC&!{;X76kx^qS8rE)Vedp(L?%{{MKB2fqOT0QfThPx}uL_;CPkqDj#PSpRO8IWiIs
zN49LD1+GLMP_%c6d6QQ@0ANJ0$V;&?8*^-`DGz*P(LIjZYz`w<3ur)7S_3wl1b{Sv
zQ*SUoe3Qw25I`)P+e62FlR1FxUO!u*hh0|QW|JVTv_<&-;nk+t{O5zmFlWGaqfuB;lPwUCvB+%X#|D*$N^=QQhmRM?he5(_`dlJrJ`eF+
zT6U84!5EBw)2pOJeY>soT;(BfRypCGQea&9fX@}L*}&E1){|Q`u}G9MU_K#Cz1{cH
z<@wk`*4cMdv3U(cFXQq^ps-s<%&NnOGx6Z)+Q@Kndd0WV;1@z
zTWt`Qs;BirqETKq_q8Kb3NdF7D#-jh!x*j9A|NJlys
zdOBtz_xR2)ydS^rIk@y9DXD^|Rj|pm$iVU53vlpSETWqGXnlqFA?!_4wB~)T+FPSf
z@cvwcsp!YNUl$5ry_nUn5i)0QCz{l3cc?JGt0a0HcO(w;{7dYIC}7eF9(={NI53N9
z=y1}27f57`X^Tu0(`%RFs|Q5zJGXyZ~@?LM8`Z|80V8xGiw&n>eN5}
zuWtcbh&7-@yCg+GEJr!E`Lp=$n<5;^tBhl9rsAPT8{V46k|$Qwv?30*EhmqrYsuel
zntHH3T+JuJgxg+BxWT-~I9IBRU#L5!NtGZQR2vDldak)wY2WMPQdVW<
zr)Gl_W%Rq@24|B)#y;&hSDJcxaB8xJ+0IPS-_zbTdbe@!`H`>QUlLWesYBD8*pCs{
zfZERr$>M?D)iT
zl}C2#_4%t0-d1^4-h64wvmFCZ7~vc}AtKIos=hK_npP_BpVWKr%h6Hx-t^paCc$1_
zpLTdl<{fCULG?$meQ52jtnx1iF&rZ`W7G`{7W+zb-)K#peW5$y5xF9Hl=)huYmnow
z9vC<^E_HO5zpO0V%9qn1{c#OwMhh_-koR3WK8l_??R;3wbEM2FzUdqL4IIj7H2A*F
zOi0DY8y-(R$k!e`7T=XuSbi=mpxxeTk}zbGbJF8Z-isGh>bdUyvFU|InCR45l8t2A
z2}#y?e@&a9Ka{8I>PLvS-k09`+!-j1dJ*X6I8=0cA*&jJAX=ByN+y{IG=9CgaO?Wv
z>G^ut^4S_*%l*878Qtdi=~*UsOS
z?18CsVN>}YgS;jtZ=_P+%Bfx#i1#s1b5cHfN{j0jFU4K`YZ=>Yd$(TC8`AKJI0+D-#8TM{Z66UCZ21d*0ARpCVAPRU(d~@;5
zXrSV|6FeMlK~vYiHbC)$-6a!dv)PvzTF`YcHNJ6?`Caa)wsK+k>{R-KD?hDlF*r^W8}&nFi`Rug#n})F|EJ
zFt0eZlm~^v(3x<^R<8S5QXh0=AEGZ0usS#8b_)6ELju{Szx3xp52F?-I+P_Fta23clqxY;1yUIqzphq0|3ze4Zz<&fHzw1F4ndl9=0}&
z@_vlA@|J##J}^&vMiAW6&Q{&h4rcAh%jhr2FUbFzpD`dnUC8q98bASng@K8MiGhWM
ziG_`gg@gAB4-Xd?4@gLa|B3=gNl5`DC#R-kW1yyCp(Q70e?-u*v9a-R@JR9SNI9v=sX70@;cpLs7zgkhEfF1!8SsJ_4V@V6Z$E$*0C<6p
z_74F67cjA2pkrX80dW3d!LI;l=;$xd|0g^S1}5e|P5eUwFp064NCf3bnYAt5uz^A$
zEb@?~!g{hdR-rT7_H@*=TL
z4jbiewK7Z5juXJA^&P~Qn(!{d@!2KSf7rkJ_i63kmgB8i{v;iZB}|nwE_LLk@t(79
zeb~MsDlx-yp3CvSO5sFE1Ot8WG!622gd!mzri=#c)|VW>H439p!?S6SPU9kC8Bqw(
zH~KUzFmX9)&K_G`iqg*iyyAv3z#wRx*5!MPbHDa6M0N0Eji`p3RywKQTqWOD2F(sV
zm}d6}(v*yIurpFGM*|;If#*-
zl1Dq{9DesX_*X?ghi?$#6@f8q>xacaeOqro=l8-964-btIc|iJx$j%XgpQ*zYKPzd
zmcC9|s5|*&JZqquUHaW6J89S`Q06;qris$XsYn}5y*?svKB>$MCwoO;%TEfcWbV&W
zLZsl;=mQ0&$!*4-b*tad>(^pwIr?*kg2>L%?xO!Hl~<~cx!gUiqj^(61BSXW$$Iw?
zd(I}hdUIlg-q!anY=3)P)|;zR8iBhi+J`7FM(^mqfFN!YsiQwB=Sl7(u*ne^7o<&t
zTap4p|Id1eiczAl({V*?@{y`M^^O!bq~k+#YpH>~+xtxHWLFspmwKik9gHs&B>sB;
z;c54g=mr2L-W0~tYUDOVeb&FOQaX|3>Q@bs_I(lg^HG5w`E@l|@cd2w5Qc5Af0Y9V
z!+VhaX)e5ezf1jeulX!A@;9cnw@H-dtW>k#hp_K|0U>!~=9lCS18~F}Zf!$4vEb**
zdDX`L-wJ3BbiN+HrU4T3DKYddPu3=^e*qrDeq+DT@n0uOxKR)9e|OAZ#UNUtEbOeB
zR(+9-V&dM3O}}0jcXQOQnDE8I^NgO
zPUszXA&-KG^TamUs-XGq1rQL>EXj6?GBl6KM{1(6%)8eFq%`|OlMkJQo?cH_snhxW
z#632kkE}gwL}0+wFAtf+JE}>zLeGOW33x$f?1k#GSqE*`2XcChXJNBc=O
zcvTW;LWFVe`|``x)WE_OQlZ5kKHZ2%)x#w9y0qjsM$R*9Nd&UHVioosy31ws(`!6X
zxq%$!PdSJxAsb^!71|!NLpFu-0g4%WKXR+}R4$gd%Lsh9T!wd$rfI31k)*+qYigMo
z7sjE(^`|1G!k_qwT=E>`^e)6AmTzVpK!J>U0zR
zwWB|#3)u}`IH+_8!P^7Y?Tasz%x|<1KAy}s2jbgFE1rg%R
ziV@`w+Ej#Bb-D$+F
z(8*G3pj9<^NrgivdR0^};9Vi~ds1?k-yJ;)@aS0+aMuQxvqN{EG$8aa3)7Ss?3qi_
z7p1Wzdi&i7@){`RwyPQ~VilV<#;JI-SejEFoCuN%u8fd^iIX4wY~s7_GR?Ge8ofj%>M?v{hIn0!0xelyEhh|#sY0${|i?Dn5l4VvV|RJA-EEDpmCS_)zrKAek`mM6;96l
z_2^X@>9@XBdB9~cG!3au`YkR#p0G_`O_l#m4k4`i=b<@~>(GH;*l3mVM}smlC!tL6
z^=Mk(4wfvXSV^GP%>5vpRG;9(lH$1-4YwDFk|r-Ts(Qnzqc0hiR(JCqWq%m2Xhfsn
z9vhrgUc(uW=~NPW6BZ8eBP>lJ%_G(%J)5y(19Rf|{sq|Qd8R+TjCqQaY)Eu78{0)<
zL)03lE{x&Hb{_smL`Gz8?Jq*n3Sv^-ChJra^>R;N6gjLXaC{v~QJY=ZoOqRZAy)_?
zOVNyBv3?g3@IEH@7UU&;P6oCo&Y-33xuo}e`~F?V7a`bHCjKy@I9N9b&v84#9pQu$
zAy}>iQ_E+xi?3%5*@p?u@NwHz4?^ln=;bpDk0@3bV!pJy8Ku!k
z!f6*RiDaPLf38^mWN@&8|Idu7bA*xoKZojM6{I`ma~GJc@Kfcgu-3$OSVo;~;nUBB
zaOy->-<{+b#iUB&V|;4$%k0qIHutD$ov-mq2V()co
zla2vC8~=O7G!+hR)yMqNsxLcb^|8zkB8&kJHIbzRs?%EwsXs3d`Z;z~r~{SoXgg2!
zlC}m$iawF~XigTstM$ow3M45`0LSx!lK5F?j5IXv&K2@@O2Ve+qElE2Kn$;j{wu#G
z+hPyzczJYw!4mT#F+SP_{h~&AbctG?R*nf?{wiR=>xt&i&4
zQ~Ti$KlS5tRP`TqJW)$d1^ky*&8thSp~^#jxlXIvhpvzr^!^x{(*1^2t@=K&zrIS;
z$5@f3g4%jB!Guc3e-DT;M;7aFa^fR;fgx=@!4@vjKdOdJ;~#Qy{d$^nA9S8!pNIJr
z!?!JQP-}~W#HJ*qFUV)-T8^3P-a0rn7^VoAa&fWcVeTBj(pP`bvj2A^
z2v#y+M;PSoAWH1EL`c)I9URAn;`L#UmL-vsz~XTzkJG(kYT*!GBC7QmYBp$%R5+;&
zt1F^cek-MLQ&cKiSRn5f>IuSJB9mY``I>mJTy-*fs6=hTsA;UygE6t1~{>wIh*~`zygnYe6tay&PKw_hl8eq@bsvBM1@k
zaIy>M=tl=VqL|*gD+`s*YDjyoj!w^Es%e2q-m;!?k>9-5$U7aG!e%|}(QopNo5VAx
zl)4{Q`B3WiM>+X&NTUCAg~Pw#UFuDB<1*|Bx9|G20gkFC764^C-)MEo_A7ZG<
z>ffzs$~P|NvrO<+af;Qr5t#6Ysk?n=1I*cUt0>jCQ^=QC>`IGOJ4@+afDY^xToT0>
zCZvH~$C*86DJf0s3(-9Z-w#B~L7JpU`34hPZzhFvLMF8rM~8XKC2X$D!s1aL57hgf
z+vnVFNv^il&w%ph3G~?+qt5|x<8kC8BiJ=>2mBerHFAN*e*qk%`fOca3;bC+o6^`a
zfcdYIl@w|O!?sPq_n#81ljwztlOy7i;{ZuH?uGuNGxUl;Q%gon4m@+571%q6XD_~=
zUpST5cX{e;^OeD$sfbkl1ZV40%c!8-u?MMXP8Qv({gI?sz&PMzjPtyJ#LrYAMzNAS
zj9N~~%e(4Tl7la~%@1>$4lS3s&}Wde2t~)Z_+j{)6)EumbWhAIsZ!HyzNYrR8X%i-
z$d3K9aaz_P-ULaWQ%F>(=UjyerQtNTm0ac;}bFGvFPv0g#
zs}nk6B$V#0?
zi94rQE1GIr=!$Ub{xd$36)Yz~+{T*mUi%m*R?ZiM%fj&$XINai#7d8
zau6=zdo%e_AW%Fki!VskIB5@|DJuO)@Jl<5!EbIv{D+FirHF%x@ZeZ9J3cm9yqlL?LrLpFDE=pfIKytX?aR*K
zPu4EmhD~$}qNiU!T(=n{zQ8>qQqXwp74k3HlSLd^q=tVWs$4
zyHrT^qJkTJo?+YYcQGT|LoDrlGUs4y-B&wj`TYvIsy13K@g6GVl)X-Nl81w&B2S$L
zP$l{p+=8rdrhB)SF8
zvYA_`(o#uJ1QtX*P(R<}>7c4zIp@a3UmC+Ja3Lm)_;#PG8FGJ%n=&K^X1mbUMkOKG
zXoWp-L7ah8gv@dkYX=`2pTQ$?E3#TYQ+KtEZTHjKu=G-L81NqW>pZ}GL*kNnT{GWk
z+p>~J01+*Px`LRICg)fn8U)K|I*lLvu+dPna6lbGYv#MIP5+^&*ZdL0Pr7B9?+z!Z
zAmHS(b3(|6iJoK|K!Ea*zsNFTx?UEwu}9#&L|b&>2FrSNv+NKM&Cm9KQK$stY%^Xs
zGHQ{XRg1$?^8rM=I8f@43s&ZZXo8z
zp-9I^Rhg}uS`oUAk=i}B)$_@6{+z4|$AJ(0KJAN~XPyqm-Co9OKd6gi-#PBlQmY!`
zsX%{~8mREGr%{@|F{Vhz6p5g@maT3m!+{ajvN2xMP8jW#W-@ly0Iz`p+9E%kI5wa_
z()Ql(*=sLtOn-&}Ig1<#oBUWp7!SwD&c3uBlPB*`dX>)R#)e`1*w!@=o^j*Oc9jQc
z_7KS!<({kBHsl0L!A7~pa2(;hjY$qs(DMtB0b^K5KHiW^dYg`6^73f57E}tBze@I{
zKCyps$g8)f_(1#v=*ZjIm1@pRTFLg)Y^DB&nK1eoo4{(pf1$Jh*$viGT2%bOzq`#;{>&f
z2hblImv^sjC>Eqi&1G(P>1hXVp~f80T+O4^kiS95G+R&7ya?b7AGRo~>$-`1V$mF13i->53Nr
zLR@#vy!*H%=U~s)l>tlxg>2AO*OL9N-TLHX@J;39v!C76i+aiG@8V}xqWM2Kv$Qmx
zPe5X;MMg$TJ?Dk@&3Y3(zR)%6pE}Quq)V__5{2aNj5o9fWZ^Dm&V)*8)Ry##uJ7sU
zm4$zu>+o}bZFWjJ;e0EtS2OtRFc>giRO)!vHX?Q@q?>qHD68k<}9U8b24Nvjf^w7cs#G!T5GTUQpNMnF^X@M
zy_P{9B4Zz-9%o4y(#_Liqd_E>l~@Efuw{sbmmR1(SoI?qX_Ipv(B!=biN^@whHR8+
z28BOsuF8FMOo=p=aL0!L+bUiK{#8T!@=l8_qicxSYt8;zwD~M=to1eZ76cnxeIH=L
zPvs<^|12fk@VJ{pwQ0V5H)AEEi8Uv8!k;Wpul#*@+@%V~V%Kl()IT*6m=@Q65R~M8J-I1f{Y^a@n{(0$3F?KGH44i`@!Wn{>v?|)Y(0CUzwF%iM?Z{tO`E^LJ
z#h#gz)R?TS5<+%55_SB%z6#drxfEJwJ0CgE08QN(o}(2pJ&o3g9j#q3&4?
z9Rp!C{MS9Db+4e$)T3H>f&MK?uaPIhg!@nhU5Ap=obTwaQW*^-Tsj`CBEd$Yi5G&M
zCRjKUU#sZsVcUGS7r+JCCqH!X4c_b&$vc7O9DGJK^woC>4-!ZRLlaf6$f(n?ktx~6
zEavh4!9-*lB!`8&HW0IvEvt(p
zNJ6Vp}+)PM;;OHH+no15rQ{NO%D)`VOdMjPJq}
zc_&V12<{f+*81nR^rFJ`q1P{DAp&pEZ@cxns3R7R;&9COI8rx4iLO}k1B
z=r&z)fuv?#?IT(>K~a2V>&c^dWI1-ZE^TYuxZM7V{SCA~FwPbHHA73z6B@7lhC8_p
zC+!PG$-Bih!~z}FFU@-*c&0zJ1gY+bz8+{t
zrx*J}hUrkj@-Eez_?kV`J&+0B{7meC-<-GSXh8m*NuZt48Co`IlixC7NcID
zrZn{jBP%q9%*jEUz-}TV8(=;am8lR6wLwUio0M{!0{%@H#ANpIE~0P)Hbj&ujD&!O
zGK){2Y{uSq8`F@M^PPVx+RWF&lryU6c|ES2w4I{HGFp<-EMTI-Ct@$rrT%UzyeUEP
z@TU5-%f#f6&3f)dgf-3ltcQY?DG9fuDT{g6i0`&Lo%W4kY3^H$khL~kj&ys1Wa2bN
zeE9g1=3F94k*OQ@#%{3cP=8HmqfEI}oachr-rU2x7w$is$XiwJDNVPdiGuZ>xQ?U)
zP9|Bpz7+HeP;9FZ|-(O)VA@vW=K#LLZtsH2y)N#`m97PG{GK
zG*t4VIV^IH-?b+wy>rY^6@cQN>8Rm1T_VAcr=|{fXl8C{N
z*p&_pGm>v6Qky99TcX@yR(}CDNtQ-tma}KJw3ybI96gJtI@?VhstH
zxN?o?Mp-XOep340G2s3ILV)^IiElc9uYqr|+MQGUl?KY7#p_Pthy8O-FXyve+|)*;
zN2}2HzG@hjHVP^iYwmF^C{-f=WL8yE8yL6ABgm#dr1aJlk{G*}wz-QfbaS#dH#S++
zczUuCy$$Uq4lel%puQED`zg^hQ{#vwIk`bm1*0;z<0{a{5@pXaQ60d|neMF~Dt@vtg$?bLeq@UZR*wRMp#
zk28A&;+Apybc7pHFJ8kI=iET{`=&fm#x?xHqC~-uzl4Tx%M<_JkEtKa
z8Mv?!*mKjZR2X~6HM|p<->W=p{>=gw9*sIZc%49Z17#j^b_x_3lG#uv+~N2vasgWo
zvZkrOXUwuP$datMcZ0G$Nh%y|ktHpB2*n&3NktJ!^IH*%^qTDI>*yej
znkqf0V7a1c3_(;3ceurv(M}kCr}gV?<&{X#TGDYdq`d(u`J*k!chM@1HL(B^K;YPF
zj5HzFAuj6lkP7}a_m#4`-YMpoDrUYoxhPQm)t9nOUVLe3QL>&|a0fKEUD>{^R4nL&%nT*<5~4T3fz?SRgTs#8#oV!&pGBP>
z>G#XYy)ji*hJK8w!u}%Sz-2tJ~J&%@;7}_j=W|g5JMun1|BqD
zF|o
zEa)P<6YSmI{ZLcu8U6gNcz^1$hBM8=PuE}47Vei}f!Zezn?%LTxLFW-fqe#qjx%W;
z39Efn1SBN76zx$X8Mj6-2VQLj>7*85`fTd{f@cj1PsmE~z30~Mjx+~K;Akw!mQKf%
zl6$4|!FOhvBygLs>a05C*i7ItY*y8srM
z2>y+vUXe}dJDTX*#XYnITFKuF`Sq@!XIe8$Kj`htXr~b$xap~Pr8Bl&uc~riG~rH>
zbOvIoe{sZbC}XSMW{Fm$MFZcP@r1<~D}=KGn-IZ4)L(a={TJ5L#gvTCD%)EA;PJ)W
zf1XS$ujmm=)r=MiTV*X{F*3A80+ENW8wiE4!4pNS6+XvjUsF4XFY_{GCO^BhL^@9h
z%}2aib9T$|UJ{#XdP1xJfQnKCo-R
zy^Fity%*lH)yw9`@+ox*PdVTAicqK!TW%W{V3TiFb_nT)wdr$?ovpw>1AJ&@2mgFx
z?$9_{pG$T1XdM!_*O{aK9IM{0y^h$8<>AI;MbmCSQPT9|h9ys82@d4U&Y@6IIkqzq
zg-YD?SkbUnrlPZ%w(!jUSH6R?Vijj|3#@yKYOhi%o)`s>7G#)DS2^i{C3|!9Rf%il
zRDp$bx0kH+B}asNt@iKO}6*L%0O}YaNih?PaRF?+n>|#p*6-)#TSMY`-*>2ghE$P%OonJY>mk(UA!6Xb2zq~GmU9UA5u6gs|
zLR9eNmDeA$CP#JuB=qOU)}8K;{pKOeZxiuprLpPft>y^lV_`v0x)w2PW&7oxIDYsw
z%zAuae$na)9u7hW+~Lu^nb*DbSAdcMIgaJik`P6;^{amY3WI^1uH8s_#zeunG^<%n
ziS?B$Hc=xtwewK2!^y1gRVv5lXsVj2p>E@^ZWbB
zw(5oWZS=2&RkH^;Np}+3aMiopq-3c9`);C-4N9qRjX4_3A0f-9`noH4_-J4OFH#M~{tltzDK3rh7{A>G-Vs
z>nPOKHK^BbO=QH&_S@!4tsB
zc;%297dIc3MxG=plrUXS9@`ayqy>U4><)NBRAKL>PkAu7@VLaUMV13I-rsleui5Nt
zFB<>yV952@a6{klavGe?VH?2KX;li6S`ttbFii<#u6VbmCAvTcVE{wkWF7l^uaY+j
zi^A%F+t+_QyGr2$Eujw}H+;+>Bj5&4A0vB+w+JKV3ucTGVzEFhu0xj1RCju*5!$uccyGW
zd-=3ll@%rnFidr)=L}^!Z4VyEZlS?u^g4YP>GksoO6rllpTFcVLJ4~wvRFYvX+wF|
zQFq`DNk;wAwUbr+BoeQz;{ey_fRYr3#dqkJJ|s{P@aL-S4SXuN#?!uC{0kU~?=Qt}
zVxP}D`5@%c`ftPPvp~RynYWEo3|7_FL%`T`ydcZ+f{IW{gOQig?UOP@gLx>A;*Il)
zz%)$J9oLe>?RRo0%zB5{^`FV}NX3eP$~a;yt^qvFhOAUj--IeDH8^X&6a?d($nE{W
zf4RxKX+FAoark#+(|pzJD=^zA6U~>LXljxK?O7T6HXLNl*AgQfaX3C}Jof~Kf|+tu
zTxw3(X#=Bl=O#Kf|)PL*lx=||nO
zR_HY2B?em3c9vy_>9!He{yk~p>-1#z;_GrRQJyTVJsk(lup4m=biL;4OM@E}z_x6rEF
zzS0Cy%=M;;oWssm&0{pvGJ2%m!1~8MM@Ua0!%`CLT-aiq4^mS0`BDEF4*kdql*CaV~K!r
zy%FzBCuXJGgA^`g1U`vxl(v@K0gd1a^(mI|dNgzc+?u>|sj5Y_T-KN6OvormeXzXI
zNfMytmoOO4eg2_SX!DpgJy!)0^g=?{rS*urnVa^7et%+o2pY>FXkY}1P@MBsj&~#d
z4nG^Sck>Pye&Rrx7M4(vM^SpX&c^s$vi!Nl8>rADl=-6W)J2O=^h!!<0Qe`>xaf6B
zPfT5Lx0t+g!0~L21AQlESsdD`4VRCx<(_ZMI~8sXt!*}r%+*Y0b%PFEHOUG;{i$4w
zdF^n=su9%-&a1NNg(F@mAtQR$-Jb+l2w3*hPnbA}C$2WW^AF2psD58bj_x9rZOJ0#
zZmodRx(;!svzy4bRi61<;SvH_-HNouD#W~~ptgbHto`>?U*3N
z4u3*?y$N!yxO*VDw63-s(Ecq*dun
z!4p&aTY8hD+K$NS9rS2!M*Og_sl`}F)fs1sHg}fJxi%-{Bqn+sUE}={fB1-Be+m9x
z%+cegwLWUgPE4L1Y4nO3ij*9C=lNd}c>?n1?FNquggLgv^U;X1qiK0#&FHINI&70B
zuvhb`5^21zWWR@qm2C4hH>x9m|5|8B9LGgGe3sAbigk?QApBUUxI>$3+9y`(aRPM!
zN{+?dyAl#&W0~R+%gj>PvttbQO;s)78Q52EYv(|BVxj{a@pxT0kc81S6OME(7T%>j
zV|sj9zXSOq(M-@SLyYq0zSDU8RQI;)EbXTDUwsdYKS#rF?K|%R8=OP|uz!&0{%gx{
zX^UXTXKhk_Sl1_J;8r}#mC81}1yyCovY8Zj1io^dh9b(J=|8@u8X4)HvKjW)jbcb_
z*sY%Z0!f~0OPYkJAJR_fTc<^R%q9U@$h
zET&h#=`ZoH>@}FS6G8k-8{_yi`d}5ia=<88MC^t);d@zo?@tP^pbz@|OB6bNd&k_G
zQTC1u#E_U&5XWc9nG#s^=N}Tozp|PoEYBOclCFivdp$+WA;&pg>WO~3$`BtEyj*M*
ziiWGT#$4e=lUs%1qtCQKS8jWJ+W#>QTg=Sx$apiSnY%rv3d>&)}9xEpLdXQGIC+@XBfnRUW=>UfbAeBJdXUdU2FNkdAxj
ze76KSrHS`Ac252qnWL5gtLj+oMeDg%aR|SI=nCfNVm50!@AN=$Cf|g0eQ57_D~=N?
zJ}f16C5C!AJALZR6BbLL^nDx@_ZM)*#m%>-ar~tk8**!x-VjCF##q8|TLVl6`y8s=
zk>1llb0t1mfp?mDrAA~{&g9UC&($gkHIy=eX=^C9n-Z
z8x9EH7~(|{9C0!vuo0-?uca<_=)o3cZWFswXDu|GhPZPb~2Af`Na{Qw{StivBpg%)L
zX}T!~Q!_)F<|9a^%tpplm$0}k=z#UpR?SHY*IDCn(2A9{>d{#Kb`=H{x40T1!$$Sy
zEKSm1K;~02ceB|A_g_Gk`Dl!jSyfPQaLc$h?{XuS^d(rrR
zJM&PTRmPNVB~h?iQcBI}Uw~8%-e5-d!XW8MN9OF0gcuX;r-ek6Y@)9BO>JmGylQIq
zK=;(wXVJ3vySr`5^pR!1=@HUA*)mK9+#qn;@P|l-<&DLl;q`2Mw5#>GYcjzP%^bM{
zDl;Nc_1Z_$kGI10v;U5~k@mKOBt!i{#aEbpcwUS(j}^tSN`
zC9Y)nzX<5%hp_$vXwq^MnzUU((aB4rn6&Xln#q$^wfX#C?`Zm9fnFCMF7G`Lt`F5pD41!%`
z?`sWTc+fA*{Ug&?y~DEV*VAVwhQ!DODbAD1$3j&14>BLl0
zD1$>uriOwygK2u_0Sx)`tIq>Ad
zJTl<#hnf|nL1Z?n>lYw&F8In$e7NWE!0{%VhjB@3`Z4Ug^~cnV*H6}b<-aYkzRl~K
z%T+K_`IQ*aL7t0-HieA@b*)HF43V8d9_gEzL5n}1~AfwsL0$i
zpGcw}nH4dU@YAG
zFLBW=nfF+d>y#r5HVe{IY+S=BOjq01-3h#LL3nm88Z;R;(jyZ0{JoVk8Nx{L7&f}t
z%Rjo*EASMz?2GzYu|M@v$x_FkzaAS`7-hTl1s7rYD!ZL(&ER>(D=}642Vro-W>UZw
zpAa`>BVQxQNi~MQ88|YO!)WD86QJ7e1+tU6^R^Bq@nPcO^>yM1dp}MrNYpmBBA0N$
z&_u%J{V;=Or;*uj%wDYYHbsSvPphGqa=)3P9%h9de-x1isn|VIT
z$BwF|z@uJY6y;5PQiLk9mT$9&SiRZB^p$=+sx|yLB;vbfso%;Vb*&j(Mnm{naZO_J
za?9^q`n&|1_L`?f!$1fhLG=YGlr4gIT)SN~qo}*o9Q0CiJzxOThCz_>pc7b8T{z0l
zL-JCe{JOBOdz-$=f6YPXC0~K90~C8x--u61BMO)j{zJnRH43M{F3Dp%OAsJ6nfO$%
zgUy246?PtTCv<*mF-s*ktl6)kr%hwp?9%zN{9WQ}R1DPntmIuoS1P*i^2c
zCphiaUbW}&{VGJ}PrzKuMxEs#(JKmHIX1uMFlJyaz@|OuwX!P>UQd-(?XBdjHI~@iDj7$BlOw?bb3n~n!`V}(
z*d$CGJaV8JTyC>mCDL#R6r00AGf5BWQ{nskXU45;^nDn{N1(jjvv%f0vyk
zt<$=#6rMk-HYQnbrB;a6UNN~+pL<|YGTLCF971_SD6Qw-SHPTy#YST7SDjnFK1Om6
zH*Al%7QGoZ3Z5hp#{-{{<)G!vmF73IV=(pO_>FVf++LTinK&i{#JkCe8Pg4}6mv=0
zkA7krn@lefh_GHn47$F0TV0|~s3u2P%;Jxf_ilbh_lV1)b6s;j+-J=eRUw}~x>(ds9`$Z-_m@Nf54R2e0>B4J
znlmveF6|cyb*l+4E4G;{V^a%QZ)m8qPP)h^;dw9vbpe`!=Z_U1Z>TS}J&
zB+7BAuSqy%enyq;F}L8>w^8fukj?fDZNi3HU&7=^@{BC#Rt1~1BDCTaUxRR;t$jm88}y&dDB$lO@e6Ki3Vxh-jBu1E8yIj^TNLc
z(>c6lt$mGt<9${9SE-U#mdGVJoJM8H$4={8X(^;QMEDqhq!Qd
zC-Z8#h|wD9%b4h~t!_p1UOyD@jyok9?ttdr<4i%U0iLV7D#?z0`J*}xW$IW|!{AT=
zkiuR55HjAJ{Mbv2SXi9y6Glz7xO-(~IIebxZ?|$OCMRWC&(7dc`4wRe{TDbt@nUdn
zqN)kp7INg^v=&Zv7#_&L_sso#hX?B4m$>G1c&U9qeIN?)`K7Hcg8!eR90OiOjaS)s
zLree}!B8%E!NJ6{eK5ynUO+qRvafOr+-(%~)h+xPL}pv1ft_G~?Yb1TT1GXE-T(pY
z*BAX4`YJfJ1b~Gpa*`FJ>Rt^^o^zw>(d0`GQ6t%^Q8@AHP2=uBUyP!4t*~Xymq3nj
zhl2XNHy!jgD4(ODFIrcOVn)GQFT!{8;?Pgx7C&0!Nn^ZUM7-9FAdW*t*l91}3U$&|Em<-Rc%w+>x>-<=
z)&CBSj6ASMb9=zC5qM{t0D;0I#|1{}u7lZYRU`M52dD-%%ULz*YxBJST;faO)U2ks
z`>4q(5S}K9DoY`m1EF|4V*WL&uU@k+k;IjCc#;+eRT24!fG6?%IaE|kdBM!*do_^q
z3wHPA`vp;DTtLk9$mDNKqAnly*IQD9xW5+D^>4Ywk7cQN;Q?xsdxvRGY6Sod9(=9D
zBt$ccR;p)$H-aSb#1zpKo<<@Hl-Xe2y(9b!2%mvoVyV*T4)j4tB;PLeh`Wq7(UWg(
z=s$5etQSWs;<0oSp0&w%E=81ibWq$=i}K%~{dSWsDit6VeC+*0Y(xl_KK{)JP>U8v
zvv|}i=)4Yg)8PCx!;W~_UpS#E)5i1Ou``FXZ{*HNi~1ttIp`KEJh|qT;Gyz1YUhQ*
z=RWwyRk|%r7$1&m(v@4DX
zm4E%_j@vfqBr*?Ni<7~-FX_Up!#YW@2HTg6X1K@eE^S-aem=k=$vQ2398J5p7=!d>
zi`BVG=h8$|5oID{Ej#Osf8t!v3zCwWX<~6WuxZ<&!e%IV9+`S%nlDAlvwgK%xc>A@
z4W|BwLfM4A3S*A|@~Rk>k@$VH$UY#UIJDu$k_wo`sNDUvqHGMj?#ztw??Z%YNq7b4
zt~P+U8;U(z#T3nsf8-%V?6^~|HTk7ujI
zN>}beIAfZ&$x
ze-_1Bzy?zV96DIPFP~MJmKrPmNlx|)>Qj_Kx&e4T8St*&i_uouf<{lyL#WnJgqA?%
zx1KKhCJj5HCFjtR1zc&e*M4)eBuA@r`Cf+2R@m^$c>yG`MU9S{BmdCvJx`H65PWzT
z)HfuYXVnzejMJgCt?%5+X>eH@%zw#-X-1~uVSw_Hy`LUt;&;a$Z0?X0k`vZ>sW|PC
z0==y?-iLZl2qf~$wce&sArmS)V)3v@8n=($
zDIeGkJ6fHV*NYx|wq|CEoApX;|Z%PbA7mzRfPR7H=Un32Qql2^bQ#p??DYW}Hr^;DK=VN)NU0b~RKt5@d+(j^l~j)`~)p
z?(Y-2T`6xiw1*mma$998_z)02
zHJpr-o%v$7-=eNvqDyUKjn-6w>P9Quj5fWZdpc%4zDgraYk=^qXR>`9(6P^+N8?dPyDbABp);j`!6?+=)*j4^4tNDe}nQ!TWV5X;JG#H5|U0E3N&$HJmS
zsT$q5Ns`lXFECRv+kDOmanp_$I6?0Kt*}8#uzYKv>1p<-w3G7f_Qb-1lJ994AtQ9;
zD`BzDDMry?W-BgsDLwPVqEu8=d`UZ+H>8jo&=*vFmCF3hAaHY|sPxt8sKZU^VFFTL
zZNVTk(l}BMPa~dG!MsV
zt1jCdZEb}mNGT&6!4!m7dxs}QOOE_$FG;XSSZfa`t#MX<9`W3u^51ZA<6AHC*|=LG
zvgk{VIW9U>g>DoJs01G^>e%cq^XzfxvQ$?gxNbAXd6l)0lk1d&NVZ#wrNgyxABtR4
z{{ZzxE=ADYtu(t(ZgC}A%xfK?3_JQtTS*8hY;ceh`$q%=(|QSR)!J$r6fF8h*>V2>
z>YwcUM}KR+N{`PJPyQezI;&lx+Swtga*CY;>jWntA1vVfsCJdG2Vw%DovC0W6~5o6
zHN~arwpixZa$!XU#*i9$hXBqL2Fe_^IRsWMFMfVZRk1&XGgw-fd=6Y`I5_e+&(62#
z2tT8Bb>)V}5bRc5Q1c2{&)rGObFkN1iL`7C#Va15QWx@c9T=`HGkZQVq}Mj~YazJy
z_FQ#oLK4^|N;m*xt#n-mbEK~X{{Ymalw%ezKOUL$${{Yh+2VQ;@tqMiH>#19~S?%mVwnZo1yC#Ac
zAe@fTN0?3y!{%!qt<`5*PZsN8E$P_m;Rzdi+LC#Z(Xow(UUb!`Y^A8-TsYg6ECCnW
zA)`>nnRwI^Tx!7?OG|OmvV=I4WGTl8+>C>P%LA3w)b0?DuC9ODaGOEm-Hk<7Whpct3SvPrunOl5DpL3y`LQTw+1k
zy3(_PdT{~>KYv;duhCOoZJyaoOl?7ELW-Dq3LQW=P@;B=A{&-o3USBQ0!xlKrKpb#
zl4@9GF|Q2#Ul<>{wjkh&AucR=QeTm6XvSenB@8KCTm!>BY7wFZDQI3pfMgzqp+mYV
z3frAXE1gV3coH~A-(MK+r*ebxr=}#&QgSw{5_dVOf>a60NU2g{$)yV06&KUQl?s9n
zE$Yney(9r{d}7;$N!o}&nq;sWbmi1_@fE6L0cbmKK^B!pd@Vf<38P*4PQ
zt!V`ErO=Z4AfO5q4^d0#a3P?D91PPD+LmVwIN<%NO@W%2P)B?cbJWo&YbBhotp<(?
z(v*X+qG(qNKory?TNzWsz)=B{Sm1zANZi)6>^7$$q!qBwR4Iuh;R^1n!j4>EB@Q<_
z5Wqr$ibvJ{MwHNY+*3}UxbdJev))L+&(q2<>u!nk&R)MypZ<|Ny~!}-#ksOL
zno#WCPl%&Pip;AUjIf}l9Z4GF4fS~!+dOTnp3>ko{ex_!p|;6zH*0o*y_h0_mXlI
z^AuN7Jt4aUmtm>k;&8k&tSqD`6$GCWoPku$u`N&`UbJJ+Tv}Q(^AE~W%ji}JQc8{j
z#yM@wisxR^crMr0LD99YthNxDv0&XAPc>k$;rp_a_X;y@xaq%Wh1fIg%ZYYnJbVrS
zhmbRlHr!7!p9AVBQf9nRQ!2@KZPE38$!h#k2tR#n(Kac{)oH#$vM{sXS5
zSji~|x{wE5$?3I2l2qA-*D_V6=Q_!9d$xKafJ4M!y4r?Pqvz21ieu8GD?#0qITs}U
zX1V>&?A`NUU1va795fV0ZN&0(gFHldjFLW7{p|H3ZjZUbLV$EgeU$^yoyXvyXaGR=
zb&t`^{{V1TLeit&eVomX-|G|aEfMF$s^;2ho35RY*lBpDMd&pKUY$jI#!*AdA=kT<|Qh|bh?>xJg|9Kf^NrZT5T0_I9&wA(2>)HtwI
zuK))=v_ccp@1j4mDbkj3(QZ?~pR!c4KMLg9OeisD2mY=v{{Si}>)MslEt^V3(e|bB
zOOVo8QbEaSf$=F`8O8^WPlYa8;TY{(Nf^Y~-~7m+A)DKRLZ)f4{{X7@`9&Vm2m%6j
z=|MfV49(Mc{{YQ<{GyI&Q-}%lq$2AG#<{rT{)xZys=1`ok7yc${{VJ>%9`m`5B~VL
zf9#w806MFh%}GAcH37!n_J7KnK=?XGvqd2~ZsMo0*@=&pXTv+vb)VZcwnnaM*3=Jn
z)5`h=rZ2nS&Js#~st+K2DX%~bX$!dj0PAi3t1i9_<)?BQAKyaA4laF
zSwjFGWaQV(uZn8GJb!YDHW^v*httxggW99*rDB9V^0=
zsBVycRaSPdQlbH&fg(YM$tOeg;sSvg-_o!pWSX9GDYJ@_!xGFMQQs$M
z3nkXu7HnAD4M}Y-B}o7s#%r8vuVb#Hsq1Wv=YAE}W%d&-cVA}RjI1a`d2F_N@a&=a
z(b;UaTLhpUmCU{~)4kQc4yKw=)UCEhlku$kB)c2xX5hk_SmoeHtzuH}4StUHonVPR
z$t$%Oainpf%x*E#zc#Ws-T?9yXH9puJoZ)9Y1IoXo%CkAT
zxHExUBQS&{5INPfR6J(j@FOkZG#wVpDYQ>~Xf5_Z7JPm!ywZx`USwR~8`Z;ZSsf&!4O
zl#|Gc-cxttpTjdw7;SwC?Kc6nDYG$XP&x0mX!W%*kLu^JeR3#nr?)j8Qu{J?!3W6n
zp%9i*anA;ssi$vUGR08bvy-e^8YM%seTDiix+b4Wlv07nZb+^_r2E-f<}0e&6k3MY
z4>t!9Kj~cC>-E>W?6F#Vw&ZguTZkiofzp-Vx7@6iq#8bAmMNdr17PQyd^*!^C~#yMI=}oxm!yzn+r(6-(n9!4@2insD{Z_cz3OBG?b))%NU`s
zTf`dPv77?*E(j-8U3tcR2b_kjfb^HfdLn#yVmU!)j}m26cSC`!IXMOde&;vqnj
z`BoDRpsqviqw5Noi(=CX5|<(JItrrEwd2I%kJ2WgP0!a9WodUMlfk*B<@dAPg6{;GTM10Ryhmc9ndOhQii!7|JguD
B=uiLv
From ff7e3c34b8bf215b916c1b94cfdc91bb27c4f9a6 Mon Sep 17 00:00:00 2001
From: pawelsa
Date: Sun, 26 Apr 2026 13:28:28 +0200
Subject: [PATCH 049/157] feat: Implement SearchBarComponent
---
.../elements/SearchBarComponent.java | 49 +++++++++++++++++++
.../ntnu/idi/idatt/view/primary/MainView.java | 6 ++-
2 files changed, 54 insertions(+), 1 deletion(-)
create mode 100644 src/main/java/edu/ntnu/idi/idatt/view/components/elements/SearchBarComponent.java
diff --git a/src/main/java/edu/ntnu/idi/idatt/view/components/elements/SearchBarComponent.java b/src/main/java/edu/ntnu/idi/idatt/view/components/elements/SearchBarComponent.java
new file mode 100644
index 0000000..0493b21
--- /dev/null
+++ b/src/main/java/edu/ntnu/idi/idatt/view/components/elements/SearchBarComponent.java
@@ -0,0 +1,49 @@
+package edu.ntnu.idi.idatt.view.components.elements;
+
+import javafx.beans.property.SimpleStringProperty;
+import javafx.beans.property.StringProperty;
+import javafx.scene.control.Button;
+import javafx.scene.control.TextField;
+import javafx.scene.image.Image;
+import javafx.scene.image.ImageView;
+import javafx.scene.layout.HBox;
+
+public class SearchBarComponent extends HBox {
+
+ private final StringProperty query = new SimpleStringProperty();
+ private final Button searchButton;
+
+ public SearchBarComponent(String placeholder) {
+ TextField searchBar = new TextField();
+ searchBar.setPromptText(placeholder);
+ searchBar.getStyleClass().add("button");
+ searchBar.textProperty().bindBidirectional(query);
+ searchBar.setFocusTraversable(false);
+
+ Image image = new Image(this.getClass().getResource("/icons/search.png/").toExternalForm());
+ ImageView iv = new ImageView();
+ iv.setImage(image);
+
+ iv.setFitHeight(40);
+ iv.setFitWidth(40);
+
+ searchButton = new Button("", iv);
+ searchButton.getStyleClass().clear(); // TODO: Make icon component or fix
+
+ this.getChildren().addAll(searchBar, searchButton);
+ }
+
+ public String getQuery() {
+ return query.get();
+ }
+
+ public void onSearchQuery(ActionEventHandler handler) {
+ this.searchButton.setOnAction(e -> handler.handle());
+ }
+
+ @FunctionalInterface
+ public interface ActionEventHandler {
+ void handle();
+ }
+
+}
diff --git a/src/main/java/edu/ntnu/idi/idatt/view/primary/MainView.java b/src/main/java/edu/ntnu/idi/idatt/view/primary/MainView.java
index cb0869b..09e1e17 100644
--- a/src/main/java/edu/ntnu/idi/idatt/view/primary/MainView.java
+++ b/src/main/java/edu/ntnu/idi/idatt/view/primary/MainView.java
@@ -3,6 +3,7 @@
import edu.ntnu.idi.idatt.session.UserSession;
import edu.ntnu.idi.idatt.view.components.AbstractView;
import edu.ntnu.idi.idatt.view.components.AbstractViewUI;
+import edu.ntnu.idi.idatt.view.components.elements.SearchBarComponent;
import javafx.geometry.Pos;
import javafx.scene.Parent;
import javafx.scene.control.Label;
@@ -52,7 +53,10 @@ public Parent createNavigation() {
@Override
public Parent createHeader() {
- return new Label("Header");
+ this.getHeader().setAlignment(Pos.BASELINE_CENTER);
+ SearchBarComponent bar = new SearchBarComponent("Search after stocks..");
+ bar.onSearchQuery(() -> System.out.println(bar.getQuery()));
+ return bar;
}
@Override
From a3d28017af83b272c31240454eb036db795fea06 Mon Sep 17 00:00:00 2001
From: pawelsa
Date: Sun, 26 Apr 2026 23:02:41 +0200
Subject: [PATCH 050/157] feat(AbstractViewUI): Implement togglable menu
solution
---
.../idatt/view/components/AbstractViewUI.java | 30 ++++++++++++++++++-
1 file changed, 29 insertions(+), 1 deletion(-)
diff --git a/src/main/java/edu/ntnu/idi/idatt/view/components/AbstractViewUI.java b/src/main/java/edu/ntnu/idi/idatt/view/components/AbstractViewUI.java
index 76a5375..1b7b889 100644
--- a/src/main/java/edu/ntnu/idi/idatt/view/components/AbstractViewUI.java
+++ b/src/main/java/edu/ntnu/idi/idatt/view/components/AbstractViewUI.java
@@ -1,9 +1,12 @@
package edu.ntnu.idi.idatt.view.components;
+import javafx.beans.property.SimpleBooleanProperty;
+import javafx.geometry.Pos;
import javafx.scene.Parent;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
+import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
@@ -12,6 +15,7 @@ public abstract class AbstractViewUI extends AbstractView {
private VBox navigation;
private HBox header;
private HBox toolbar;
+ private SimpleBooleanProperty isMenuVisible = new SimpleBooleanProperty(false);
public AbstractViewUI() {
HBox wrapper = new HBox();
@@ -30,7 +34,25 @@ public AbstractViewUI() {
wrapper.getChildren().addAll(navigation, layout);
this.setInstance(new StackPane());
- this.getInstance().getChildren().add(wrapper);
+
+ Parent menu = createMenu();
+ StackPane.setAlignment(menu, Pos.CENTER_RIGHT);
+ menu.setVisible(false);
+
+ Region disableMenu = new Region();
+ disableMenu.setVisible(false);
+
+ disableMenu.setOnMouseClicked(e -> {
+ menu.setVisible(false);
+ disableMenu.setVisible(false);
+ });
+
+ isMenuVisible.addListener((observer, oldVal, newVal) -> {
+ menu.setVisible(true);
+ disableMenu.setVisible(true);
+ });
+
+ this.getInstance().getChildren().addAll(wrapper, disableMenu, menu);
}
public void createUIComponents() {
@@ -57,6 +79,8 @@ public void createUIComponents() {
public abstract Parent createToolbar();
+ public abstract Parent createMenu();
+
public VBox getNavigation() {
return navigation;
}
@@ -69,4 +93,8 @@ public HBox getToolbar() {
return toolbar;
}
+ public void toggleMenu() {
+ isMenuVisible.set(!isMenuVisible.get());
+ }
+
}
From e86dd10fd18f5951e3b748732c51a4e061b51327 Mon Sep 17 00:00:00 2001
From: pawelsa
Date: Sun, 26 Apr 2026 23:02:58 +0200
Subject: [PATCH 051/157] chore: Update .css stylesheet
---
src/main/resources/themes/default.css | 24 +++++++++++++++++++++++-
1 file changed, 23 insertions(+), 1 deletion(-)
diff --git a/src/main/resources/themes/default.css b/src/main/resources/themes/default.css
index 2e79159..a372c2b 100644
--- a/src/main/resources/themes/default.css
+++ b/src/main/resources/themes/default.css
@@ -30,7 +30,6 @@
-fx-font-size: 16px;
-fx-font-weight: 700;
-fx-text-fill: #EEEEEE;
- -fx-padding: 30px;
}
/* =======================
@@ -51,3 +50,26 @@
-fx-effect: dropshadow(gaussian, rgba(0,0,0,0.4), 8, 0.4, 0, 4);
-fx-translate-y: 1;
}
+
+.icon {
+ -fx-border-radius: 20;
+ -fx-padding: 5;
+ -fx-cursor: hand;
+}
+
+.searchbar {
+ -fx-background-color: #FFFFFF;
+ -fx-background-radius: 20;
+ -fx-border-radius: 20;
+ -fx-effect: dropshadow(gaussian, rgba(0,0,0,0.3), 6, 0.3, 0, 2);
+}
+
+.searchbar-field {
+ -fx-font-size: 16px;
+ -fx-text-fill: black;
+ -fx-background-color: #FFFFFF;
+ -fx-background-radius: 20;
+ -fx-border-radius: 20;
+ -fx-cursor: hand;
+}
+
From 3c60893d1de4fd5df59a37ff5b30493e92faef20 Mon Sep 17 00:00:00 2001
From: pawelsa
Date: Sun, 26 Apr 2026 23:03:31 +0200
Subject: [PATCH 052/157] feat: Add functional interface for event handeling
---
.../view/components/primitives/ActionEventHandler.java | 6 ++++++
1 file changed, 6 insertions(+)
create mode 100644 src/main/java/edu/ntnu/idi/idatt/view/components/primitives/ActionEventHandler.java
diff --git a/src/main/java/edu/ntnu/idi/idatt/view/components/primitives/ActionEventHandler.java b/src/main/java/edu/ntnu/idi/idatt/view/components/primitives/ActionEventHandler.java
new file mode 100644
index 0000000..a42993d
--- /dev/null
+++ b/src/main/java/edu/ntnu/idi/idatt/view/components/primitives/ActionEventHandler.java
@@ -0,0 +1,6 @@
+package edu.ntnu.idi.idatt.view.components.primitives;
+
+@FunctionalInterface
+public interface ActionEventHandler {
+ void handle();
+}
From 3f8b74e95e569d8b2e8c1ea20a9ca26d1a7504f6 Mon Sep 17 00:00:00 2001
From: pawelsa
Date: Sun, 26 Apr 2026 23:04:05 +0200
Subject: [PATCH 053/157] feat: Implement IconComponent class
---
.../components/elements/IconComponent.java | 32 +++++++++++++++++++
1 file changed, 32 insertions(+)
create mode 100644 src/main/java/edu/ntnu/idi/idatt/view/components/elements/IconComponent.java
diff --git a/src/main/java/edu/ntnu/idi/idatt/view/components/elements/IconComponent.java b/src/main/java/edu/ntnu/idi/idatt/view/components/elements/IconComponent.java
new file mode 100644
index 0000000..a3ef67a
--- /dev/null
+++ b/src/main/java/edu/ntnu/idi/idatt/view/components/elements/IconComponent.java
@@ -0,0 +1,32 @@
+package edu.ntnu.idi.idatt.view.components.elements;
+
+import edu.ntnu.idi.idatt.view.components.primitives.ActionEventHandler;
+import javafx.scene.control.Button;
+import javafx.scene.image.Image;
+import javafx.scene.image.ImageView;
+import javafx.scene.layout.StackPane;
+
+public class IconComponent extends StackPane {
+
+ private Button icon;
+
+ public IconComponent(Image image, String description, int size) {
+
+ ImageView iv = new ImageView();
+ iv.setImage(image);
+ iv.setFitHeight(size); // TODO: Fix?
+ iv.setFitWidth(size);
+
+ icon = new Button(description, iv);
+ icon.getStyleClass().clear(); // Remove parent buffers in case.
+ icon.getStyleClass().add("icon");
+ icon.setMaxHeight(Double.MAX_VALUE);
+
+ this.getChildren().add(icon);
+ }
+
+ public void onIconClick(ActionEventHandler handler) {
+ this.icon.setOnAction(e -> handler.handle());
+ }
+
+}
From ff5e1bf12b5d8df0315e80441f6c08d9fa24de4c Mon Sep 17 00:00:00 2001
From: pawelsa
Date: Sun, 26 Apr 2026 23:04:52 +0200
Subject: [PATCH 054/157] refactor: Update SearchBarComponent class
---
.../elements/SearchBarComponent.java | 32 ++++++++-----------
1 file changed, 14 insertions(+), 18 deletions(-)
diff --git a/src/main/java/edu/ntnu/idi/idatt/view/components/elements/SearchBarComponent.java b/src/main/java/edu/ntnu/idi/idatt/view/components/elements/SearchBarComponent.java
index 0493b21..0e56051 100644
--- a/src/main/java/edu/ntnu/idi/idatt/view/components/elements/SearchBarComponent.java
+++ b/src/main/java/edu/ntnu/idi/idatt/view/components/elements/SearchBarComponent.java
@@ -1,36 +1,37 @@
package edu.ntnu.idi.idatt.view.components.elements;
+import edu.ntnu.idi.idatt.view.components.primitives.ActionEventHandler;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
-import javafx.scene.control.Button;
+import javafx.geometry.Pos;
import javafx.scene.control.TextField;
import javafx.scene.image.Image;
-import javafx.scene.image.ImageView;
import javafx.scene.layout.HBox;
public class SearchBarComponent extends HBox {
private final StringProperty query = new SimpleStringProperty();
- private final Button searchButton;
+ private final IconComponent searchIcon;
public SearchBarComponent(String placeholder) {
+
+ HBox wrapper = new HBox();
+ wrapper.getStyleClass().add("searchbar");
+ wrapper.setAlignment(Pos.CENTER);
+
TextField searchBar = new TextField();
+ searchBar.getStyleClass().add("searchbar-field");
searchBar.setPromptText(placeholder);
- searchBar.getStyleClass().add("button");
+ searchBar.setMaxHeight(Double.MAX_VALUE);
searchBar.textProperty().bindBidirectional(query);
searchBar.setFocusTraversable(false);
Image image = new Image(this.getClass().getResource("/icons/search.png/").toExternalForm());
- ImageView iv = new ImageView();
- iv.setImage(image);
-
- iv.setFitHeight(40);
- iv.setFitWidth(40);
+ searchIcon = new IconComponent(image, null, 32);
- searchButton = new Button("", iv);
- searchButton.getStyleClass().clear(); // TODO: Make icon component or fix
+ wrapper.getChildren().addAll(searchBar, searchIcon);
- this.getChildren().addAll(searchBar, searchButton);
+ this.getChildren().addAll(wrapper);
}
public String getQuery() {
@@ -38,12 +39,7 @@ public String getQuery() {
}
public void onSearchQuery(ActionEventHandler handler) {
- this.searchButton.setOnAction(e -> handler.handle());
- }
-
- @FunctionalInterface
- public interface ActionEventHandler {
- void handle();
+ this.searchIcon.onIconClick(() -> handler.handle());
}
}
From 78971a36a11c2c0b0e9b8b8ff5391de536addee9 Mon Sep 17 00:00:00 2001
From: pawelsa
Date: Sun, 26 Apr 2026 23:05:16 +0200
Subject: [PATCH 055/157] feat: Implement initial MainView
---
.../ntnu/idi/idatt/view/primary/MainView.java | 117 ++++++++++++------
1 file changed, 82 insertions(+), 35 deletions(-)
diff --git a/src/main/java/edu/ntnu/idi/idatt/view/primary/MainView.java b/src/main/java/edu/ntnu/idi/idatt/view/primary/MainView.java
index 09e1e17..d3a151d 100644
--- a/src/main/java/edu/ntnu/idi/idatt/view/primary/MainView.java
+++ b/src/main/java/edu/ntnu/idi/idatt/view/primary/MainView.java
@@ -1,67 +1,114 @@
package edu.ntnu.idi.idatt.view.primary;
import edu.ntnu.idi.idatt.session.UserSession;
-import edu.ntnu.idi.idatt.view.components.AbstractView;
import edu.ntnu.idi.idatt.view.components.AbstractViewUI;
+import edu.ntnu.idi.idatt.view.components.elements.IconComponent;
import edu.ntnu.idi.idatt.view.components.elements.SearchBarComponent;
+import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Parent;
import javafx.scene.control.Label;
+import javafx.scene.image.Image;
+import javafx.scene.layout.HBox;
+import javafx.scene.layout.Priority;
+import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
-import javafx.scene.transform.Scale;
-
-/*
-public class MainView extends AbstractView {
-
- public MainView() {
- super(new StackPane());
- this.getInstance().getStyleClass().add("light");
- }
-
- @Override
- public Parent createContent() {
- StackPane menu = new StackPane();
- menu.getStyleClass().add("dark");
- menu.setScaleX(menu.getScaleX()*0.75);
- Scale scale = new Scale(-2.0, 1.0, 0, 0);
- menu.getTransforms().add(scale);
-
- StackPane background = new StackPane();
- background.getStyleClass().add("primary");
- StackPane.setAlignment(background, Pos.CENTER);
- background.setScaleY(background.getScaleY()*0.80);
- StackPane main = new StackPane(background, menu);
-
-
- return main;
- }
-}
-*/
+import javafx.scene.layout.VBox;
public class MainView extends AbstractViewUI {
@Override
public Parent createContent() {
- return new Label("Hello " + UserSession.getInstance().getPlayer().getName() + " "
- + UserSession.getInstance().getPlayer().getNetWorth());
+ StackPane root = new StackPane();
+ root.getStyleClass().add("primary");
+ return root;
}
@Override
public Parent createNavigation() {
- return new Label("Hello from nav");
+ VBox navigation = new VBox();
+ Label title = new Label("Title");
+ title.getStyleClass().add("big-text-32");
+
+ Label newspaper = new Label(" • Newspaper");
+ newspaper.getStyleClass().add("med-text-16");
+
+ navigation.getChildren().addAll(title, newspaper);
+ return navigation;
}
@Override
public Parent createHeader() {
- this.getHeader().setAlignment(Pos.BASELINE_CENTER);
+ this.getHeader().setAlignment(Pos.CENTER);
SearchBarComponent bar = new SearchBarComponent("Search after stocks..");
bar.onSearchQuery(() -> System.out.println(bar.getQuery()));
+ HBox.setMargin(bar, new Insets(20));
return bar;
}
@Override
public Parent createToolbar() {
- return new Label("Toolbar");
+ HBox bar = new HBox();
+ bar.setAlignment(Pos.CENTER);
+ bar.setMaxWidth(Double.MAX_VALUE);
+ HBox.setHgrow(bar, Priority.ALWAYS);
+
+ Label balance = new Label(" Money: " + UserSession.getInstance().getPlayer().getMoney().toString() + " USD");
+ balance.getStyleClass().add("med-text-16");
+
+ Region filler = new Region();
+ HBox.setHgrow(filler, Priority.ALWAYS);
+
+ VBox infoWrapper = new VBox();
+ infoWrapper.setAlignment(Pos.CENTER);
+
+ Label playerStatus = new Label();
+ playerStatus.setText("Status:" + UserSession.getInstance().getPlayer().getStatus());
+ playerStatus.getStyleClass().add("med-text-16");
+
+ Label playerNetWorth = new Label();
+ playerNetWorth.setText("Net Worth: " + UserSession.getInstance().getPlayer().getNetWorth().toString() + " USD");
+ playerNetWorth.getStyleClass().add("med-text-16");
+
+ infoWrapper.getChildren().addAll(playerStatus, playerNetWorth);
+
+ HBox iconWrapper = new HBox();
+ iconWrapper.setAlignment(Pos.CENTER);
+
+ Image userImg = new Image(this.getClass().getResource("/icons/user.png").toExternalForm());
+ Image quitImg = new Image(this.getClass().getResource("/icons/quit.png").toExternalForm());
+
+ IconComponent userIcon = new IconComponent(userImg, null, 44);
+ IconComponent quitIcon = new IconComponent(quitImg, null, 44);
+
+ userIcon.onIconClick(() -> toggleMenu());
+
+ iconWrapper.getChildren().addAll(userIcon, quitIcon);
+
+ bar.getChildren().addAll(balance, filler, infoWrapper, iconWrapper);
+ return bar;
+ }
+
+ @Override
+ public Parent createMenu() {
+ VBox menu = new VBox();
+ menu.setMaxSize(300, Double.MAX_VALUE);
+ menu.getStyleClass().add("dark");
+ menu.setAlignment(Pos.TOP_CENTER);
+
+ Image menuImg = new Image(this.getClass().getResource("/icons/user.png").toExternalForm());
+ IconComponent menuIcon = new IconComponent(menuImg, "Account", 60);
+ menuIcon.getStyleClass().add("big-text-32");
+
+ Label portfolio = new Label(" • Portfolio");
+ portfolio.getStyleClass().add("med-text-16");
+
+ Label transactions = new Label(" • Transactions");
+ transactions.getStyleClass().add("med-text-16");
+
+ menu.getChildren().addAll(menuIcon, portfolio, transactions);
+
+ return menu;
}
}
From b54748ee8f73fb71a8e37847a86b8bcbb478b819 Mon Sep 17 00:00:00 2001
From: pawelsa
Date: Sat, 9 May 2026 23:37:40 +0200
Subject: [PATCH 056/157] chore: remove unused class.
---
.../edu/ntnu/idi/idatt/storage/CSVtoJSON.java | 33 -------------------
1 file changed, 33 deletions(-)
delete mode 100644 src/main/java/edu/ntnu/idi/idatt/storage/CSVtoJSON.java
diff --git a/src/main/java/edu/ntnu/idi/idatt/storage/CSVtoJSON.java b/src/main/java/edu/ntnu/idi/idatt/storage/CSVtoJSON.java
deleted file mode 100644
index cf91bec..0000000
--- a/src/main/java/edu/ntnu/idi/idatt/storage/CSVtoJSON.java
+++ /dev/null
@@ -1,33 +0,0 @@
-package edu.ntnu.idi.idatt.storage;
-
-import com.google.gson.Gson;
-import com.google.gson.GsonBuilder;
-import com.google.gson.JsonElement;
-import com.google.gson.JsonParser;
-
-import java.io.*;
-import java.nio.charset.StandardCharsets;
-import java.util.stream.Collectors;
-
-
-public class CSVtoJSON {
-
- public void csvFile(){
- String csvData;
- try (InputStream rawData = CSVtoJSON.class.getResourceAsStream("/stocks.csv")){
- csvData = new BufferedReader(new InputStreamReader(rawData, StandardCharsets.UTF_8))
- .lines()
- .collect(Collectors.joining(" "));
- } catch (IOException e) {
- throw new RuntimeException(e);
- }
-
- Gson gson = new GsonBuilder().setPrettyPrinting().create();
- try (Writer writer = new FileWriter("src/main/resources/save.json")) {
- gson.toJson(csvData, writer);
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
-}
-
From 8e55e996ba68bffbdbef06241e5eddf96af70ca6 Mon Sep 17 00:00:00 2001
From: pawelsa
Date: Sat, 9 May 2026 23:41:08 +0200
Subject: [PATCH 057/157] refactor: ExchangeLoader -> StockParser. Uncoupling
and small fixes.
---
.../edu/ntnu/idi/idatt/model/Exchange.java | 31 ++----
.../idi/idatt/storage/ExchangeLoader.java | 100 -----------------
.../ntnu/idi/idatt/storage/StockParser.java | 76 +++++++++++++
.../ntnu/idi/idatt/model/ExchangeTest.java | 28 ++---
.../idi/idatt/storage/ExchangeLoaderTest.java | 103 ------------------
.../idi/idatt/storage/StockParserTest.java | 68 ++++++++++++
6 files changed, 159 insertions(+), 247 deletions(-)
delete mode 100644 src/main/java/edu/ntnu/idi/idatt/storage/ExchangeLoader.java
create mode 100644 src/main/java/edu/ntnu/idi/idatt/storage/StockParser.java
delete mode 100644 src/test/java/edu/ntnu/idi/idatt/storage/ExchangeLoaderTest.java
create mode 100644 src/test/java/edu/ntnu/idi/idatt/storage/StockParserTest.java
diff --git a/src/main/java/edu/ntnu/idi/idatt/model/Exchange.java b/src/main/java/edu/ntnu/idi/idatt/model/Exchange.java
index 84d2d9d..d009ed1 100644
--- a/src/main/java/edu/ntnu/idi/idatt/model/Exchange.java
+++ b/src/main/java/edu/ntnu/idi/idatt/model/Exchange.java
@@ -1,6 +1,5 @@
package edu.ntnu.idi.idatt.model;
-import java.io.IOException;
import java.math.BigDecimal;
import java.util.*;
@@ -10,7 +9,6 @@
import edu.ntnu.idi.idatt.model.transaction.Purchase;
import edu.ntnu.idi.idatt.model.transaction.Sale;
import edu.ntnu.idi.idatt.model.transaction.Transaction;
-import edu.ntnu.idi.idatt.storage.ExchangeLoader;
/**
* Exchange class
@@ -22,29 +20,23 @@
*
*
*/
-public class Exchange extends ExchangeLoader {
+public class Exchange {
private final String name;
private int week;
private HashMap stockMap = new HashMap<>();
- private Random random = new Random();
/**
* Constructor for Exchange class
*
- * @param name - Name of the current stock Exchange
- * @param path - Path to .csv file.
+ * @param name - Name of the current stock Exchange
+ * @param stocks - List of stocks for this exchange
*/
- public Exchange(String name, String path) {
- super(path);
+ public Exchange(String name, List stocks) {
this.name = name;
this.week = 1;
- try {
- this.load().forEach(stock -> stockMap.put(stock.getSymbol(), stock));
- } catch (IOException e) {
- throw new IllegalArgumentException("Problem loading [" + name + "] exchange : " + e);
- }
+ stocks.forEach(stock -> stockMap.put(stock.getSymbol(), stock));
}
@@ -164,7 +156,8 @@ public List getLosers(int limit) {
* @see Transaction
*/
public Transaction buy(String symbol, BigDecimal quantity, Player player) {
- Share share = new Share(getStock(symbol), quantity, BigDecimal.valueOf(random.nextDouble()));
+ Stock stock = getStock(symbol);
+ Share share = new Share(stock, quantity, stock.getSalesPrice());
Purchase purchase = new Purchase(share, this.week);
purchase.commit(player);
return player.getTransactionArchive().getPurchases(this.week).getLast();
@@ -201,16 +194,6 @@ public Transaction sell(Share share, Player player) {
* @see Stock
*/
public void advance() {
- for (Stock stocks : stockMap.values()) {
- stocks.addNewSalesPrice(BigDecimal.valueOf(random.nextDouble()));
-
- // TODO: Move this to JavaFx on Window close?
- try {
- this.save(stockMap.values().stream().toList());
- } catch (IOException e) {
- throw new IllegalArgumentException("Problem loading [" + name + "] exchange : " + e);
- }
- }
}
}
diff --git a/src/main/java/edu/ntnu/idi/idatt/storage/ExchangeLoader.java b/src/main/java/edu/ntnu/idi/idatt/storage/ExchangeLoader.java
deleted file mode 100644
index 71b4e18..0000000
--- a/src/main/java/edu/ntnu/idi/idatt/storage/ExchangeLoader.java
+++ /dev/null
@@ -1,100 +0,0 @@
-package edu.ntnu.idi.idatt.storage;
-
-import java.io.BufferedReader;
-import java.io.BufferedWriter;
-import java.io.File;
-import java.io.FileReader;
-import java.io.FileWriter;
-import java.io.IOException;
-import java.math.BigDecimal;
-import java.net.URL;
-import java.util.ArrayList;
-import java.util.List;
-
-import edu.ntnu.idi.idatt.model.market.Stock;
-
-public class ExchangeLoader {
-
- private final File file;
-
- /**
- * Constructors for ExchangeLoader
- *
- *
- * Utilizes method overloading for different types
- * of path formatting.
- *
- *
- * @throws IllegalArgumentException if specified path doesn't exist.
- */
- protected ExchangeLoader(String path) {
- file = new File(path);
- if (!file.exists()) {
- throw new IllegalArgumentException("File at this path doesn't exist!");
- }
- }
-
- protected ExchangeLoader(URL path) {
- file = new File(path.toString());
- if (!file.exists()) {
- throw new IllegalArgumentException("File at this path doesn't exist!");
- }
- }
-
- /**
- * Method for loading from stocks from file
- *
- * @return a list of loaded stocks.
- * @throws IOException on BufferedReader error
- */
- protected List load() throws IOException {
-
- ArrayList stocks = new ArrayList<>();
-
- try (BufferedReader reader = new BufferedReader(new FileReader(file))) {
- List stockStringList = new ArrayList<>(reader.readAllLines());
-
- // Remove comments
- stockStringList.removeIf(s -> s.isBlank());
- stockStringList.removeIf(s -> s.startsWith("#"));
-
- for (String stockString : stockStringList) {
- String[] stockValues = stockString.split(",");
-
- // TODO: Loading all historical prices not the recent, saved one.
- Stock stock = new Stock(stockValues[0], stockValues[1], List.of(new BigDecimal(stockValues[2])));
- stocks.add(stock);
- }
-
- } catch (IOException e) {
- throw new IOException("File loading failed!");
- }
-
- return stocks;
-
- }
-
- /**
- * Method for saving stocks to file.
- *
- * @param stocks The destined list to be saved.
- * @throws IOException on BufferedWriter error.
- */
- protected void save(List stocks) throws IOException {
-
- try (BufferedWriter writer = new BufferedWriter(new FileWriter(file))) {
-
- for (Stock stock : stocks) {
- String data = stock.getSymbol() + "," + stock.getCompany() + ","
- + stock.getHistoricalPrices().getLast().toString();
- writer.write(data);
- writer.newLine();
- }
-
- } catch (IOException e) {
- throw new IOException("File saving failed!");
- }
-
- }
-
-}
diff --git a/src/main/java/edu/ntnu/idi/idatt/storage/StockParser.java b/src/main/java/edu/ntnu/idi/idatt/storage/StockParser.java
new file mode 100644
index 0000000..a4936dc
--- /dev/null
+++ b/src/main/java/edu/ntnu/idi/idatt/storage/StockParser.java
@@ -0,0 +1,76 @@
+package edu.ntnu.idi.idatt.storage;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileReader;
+import java.io.IOException;
+import java.math.BigDecimal;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.List;
+
+import edu.ntnu.idi.idatt.model.market.Stock;
+
+/**
+ * Utility class for parsing stocks.
+ *
+ *
+ * Decomponents files into the .csv (comma separated valuus)
+ * format, and creats stocks out of them.
+ *
+ */
+public class StockParser {
+
+ // Disable initialization.
+ private StockParser() {
+
+ }
+
+ /**
+ * Method for loading from stocks from file
+ *
+ * @param path - The path to the .csv file.
+ *
+ * @return a list of loaded stocks.
+ * @throws IOException on BufferedReader error
+ */
+ public static List load(String path) throws IOException {
+ File file = new File(path.toString());
+ if (!file.exists()) {
+ throw new IOException("File at this path doesn't exist!");
+ }
+
+ if (!Files.probeContentType(Paths.get(file.getPath())).equals("text/csv")) {
+ throw new IOException("Please choose a .csv file!");
+ }
+
+ ArrayList stocks = new ArrayList<>();
+
+ try (BufferedReader reader = new BufferedReader(new FileReader(file))) {
+ List stockStringList = new ArrayList<>(reader.readAllLines());
+
+ // Remove comments and inproper syntax
+ stockStringList.removeIf(s -> s.isBlank());
+ stockStringList.removeIf(s -> s.startsWith("#"));
+ stockStringList.removeIf(s -> !s.contains(","));
+
+ for (String stockString : stockStringList) {
+ String[] stockValues = stockString.split(",");
+ if (stockValues.length != 3) {
+ throw new IOException("Invalid CSV format!");
+ }
+
+ Stock stock = new Stock(stockValues[0], stockValues[1], List.of(new BigDecimal(stockValues[2])));
+ stocks.add(stock);
+ }
+
+ } catch (IOException e) {
+ throw new IOException("File loading failed!");
+ }
+
+ return stocks;
+
+ }
+
+}
diff --git a/src/test/java/edu/ntnu/idi/idatt/model/ExchangeTest.java b/src/test/java/edu/ntnu/idi/idatt/model/ExchangeTest.java
index 3c9f878..af14b62 100644
--- a/src/test/java/edu/ntnu/idi/idatt/model/ExchangeTest.java
+++ b/src/test/java/edu/ntnu/idi/idatt/model/ExchangeTest.java
@@ -45,14 +45,13 @@ class ExchangeTest {
@BeforeEach
public void PT_setup() throws IOException {
- InputStream is = getClass()
- .getClassLoader()
- .getResourceAsStream("stocks.csv");
+ Stock AAPL = new Stock("AAPL", "Apple Inc", List.of(new BigDecimal("30")));
+ Stock NVDA = new Stock("NVDA", "NVIDIA", List.of(new BigDecimal("182.81")));
+ Stock TSLA = new Stock("TSLA", "Tesla", List.of(new BigDecimal("417.44")));
+ Stock AMD = new Stock("AMD", "Advanced Micro Devices", List.of(new BigDecimal("207.32")));
- Path tempFile = Files.createTempFile("stocks", ".csv");
- Files.copy(is, tempFile, StandardCopyOption.REPLACE_EXISTING);
-
- exchange = new Exchange("TestExchange", tempFile.toFile().toPath().toString());
+ List stockss = List.of(AAPL, NVDA, TSLA, AMD);
+ exchange = new Exchange("TestExchange", stockss);
stocks = exchange.getStocks();
player = new Player("TestPlayer", new BigDecimal("500"));
}
@@ -114,7 +113,7 @@ void PTBuy() {
void PTSell() {
// Player has to have a share to sell it.
exchange.buy("AAPL", new BigDecimal("1"), player);
- stocks.get(0).addNewSalesPrice(new BigDecimal("40")); // Simulate increase of AAPL stock price
+ exchange.getStock("AAPL").addNewSalesPrice(new BigDecimal("40")); // Simulate increase of AAPL stock price
Transaction transaction = exchange.sell(player.getPortfolio().getShares().getLast(), player);
assertEquals(transaction, player.getTransactionArchive().getTransactions(1).getLast());
@@ -125,18 +124,7 @@ void PTSell() {
@Test
void PTAdvance() {
- List stockPricesBefore = new ArrayList<>();
- for (Stock stock : stocks) {
- stockPricesBefore.add(stocks.indexOf(stock), stock.getSalesPrice());
- }
-
- exchange.advance();
-
- for (Stock stock : stocks) {
- assertTrue(stockPricesBefore.get(stocks.indexOf(stock)).compareTo(stock.getSalesPrice()) != 0);
- // If compareTo returns 0 then its equal.
- }
-
+ // TODO: do
}
/**
diff --git a/src/test/java/edu/ntnu/idi/idatt/storage/ExchangeLoaderTest.java b/src/test/java/edu/ntnu/idi/idatt/storage/ExchangeLoaderTest.java
deleted file mode 100644
index d3d82d7..0000000
--- a/src/test/java/edu/ntnu/idi/idatt/storage/ExchangeLoaderTest.java
+++ /dev/null
@@ -1,103 +0,0 @@
-package edu.ntnu.idi.idatt.storage;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertThrows;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.math.BigDecimal;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.StandardCopyOption;
-import java.util.List;
-
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Test;
-
-import edu.ntnu.idi.idatt.model.market.Stock;
-
-/**
- * Test class for ExchangeLoader
- *
- *
- * Tests the loading and saving of stock data.
- *
- */
-class ExchangeLoaderTest {
-
- private ExchangeLoader loader;
- private List exampleStocks;
-
- @BeforeEach
- public void PT_setup() throws IOException {
-
- InputStream is = getClass()
- .getClassLoader()
- .getResourceAsStream("stocks.csv");
-
- Path tempFile = Files.createTempFile("stocks", ".csv");
- Files.copy(is, tempFile, StandardCopyOption.REPLACE_EXISTING);
-
- loader = new ExchangeLoader(tempFile.toFile().toPath().toString());
-
- Stock AAPL = new Stock("AAPL", "Apple Inc.", List.of(new BigDecimal("32")));
- Stock NVDA = new Stock("NVDA", "NVIDIA", List.of(new BigDecimal("182.81")));
- Stock TSLA = new Stock("TSLA", "Tesla", List.of(new BigDecimal("417.44")));
- Stock AMD = new Stock("AMD", "Advanced Micro Devices", List.of(new BigDecimal("207.32")));
-
- exampleStocks = List.of(AAPL, NVDA, TSLA, AMD);
- }
-
- /**
- * Positive test for loading/reading stocks
- */
- @Test
- void PT_load() {
- List stocks = null;
- try {
- stocks = loader.load();
- } catch (IOException e) {
- e.printStackTrace();
- }
-
- assertEquals(4, stocks.size());
-
- }
-
- /**
- * Positive test for saving stocks.
- */
- @Test
- void PT_save() {
- exampleStocks.get(3).addNewSalesPrice(new BigDecimal("99999"));
-
- // Save
-
- try {
- loader.save(exampleStocks);
- } catch (IOException e) {
- e.printStackTrace();
- }
-
- // Try to read again
- List stocks = null;
- try {
- stocks = loader.load();
- } catch (IOException e) {
- e.printStackTrace();
- }
-
- assertEquals(new BigDecimal("99999"), stocks.get(3).getSalesPrice());
-
- }
-
- /**
- * Negative tests for constructor
- */
- @Test
- void NT_IllegalArgumentException_Constructor() {
- assertThrows(IllegalArgumentException.class,
- () -> new ExchangeLoader("resources/notexistantfile.csv"));
- }
-
-}
diff --git a/src/test/java/edu/ntnu/idi/idatt/storage/StockParserTest.java b/src/test/java/edu/ntnu/idi/idatt/storage/StockParserTest.java
new file mode 100644
index 0000000..102cf7b
--- /dev/null
+++ b/src/test/java/edu/ntnu/idi/idatt/storage/StockParserTest.java
@@ -0,0 +1,68 @@
+package edu.ntnu.idi.idatt.storage;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardCopyOption;
+import java.util.List;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import edu.ntnu.idi.idatt.model.market.Stock;
+
+/**
+ * Test class for ExchangeLoader
+ *
+ *
+ * Tests the loading and saving of stock data.
+ *
+ */
+class StockParserTest {
+
+ String file;
+
+ @BeforeEach
+ public void PT_setup() throws IOException {
+
+ InputStream is = getClass()
+ .getClassLoader()
+ .getResourceAsStream("stocks.csv");
+
+ Path tempFile = Files.createTempFile("stocks", ".csv");
+ Files.copy(is, tempFile, StandardCopyOption.REPLACE_EXISTING);
+
+ file = tempFile.toFile().toPath().toString();
+
+ }
+
+ /**
+ * Positive test for loading/reading stocks
+ */
+ @Test
+ void PT_load() {
+ List stocks = null;
+ try {
+ stocks = StockParser.load(file);
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+
+ assertEquals(4, stocks.size());
+
+ }
+
+ /**
+ * Negative tests reading stocks.
+ */
+ @Test
+ void NT_IllegalArgumentException_Constructor() {
+ assertThrows(IOException.class,
+ () -> StockParser.load("resources/notexistantfile.csv"));
+ }
+
+}
From d294f512a14ed5452a8ec64a6e3134e6506a7994 Mon Sep 17 00:00:00 2001
From: pawelsa
Date: Sat, 9 May 2026 23:41:37 +0200
Subject: [PATCH 058/157] chore(Launcher): remove old code.
---
src/main/java/edu/ntnu/idi/idatt/Launcher.java | 3 ---
1 file changed, 3 deletions(-)
diff --git a/src/main/java/edu/ntnu/idi/idatt/Launcher.java b/src/main/java/edu/ntnu/idi/idatt/Launcher.java
index f13f7fd..9fc9145 100644
--- a/src/main/java/edu/ntnu/idi/idatt/Launcher.java
+++ b/src/main/java/edu/ntnu/idi/idatt/Launcher.java
@@ -1,6 +1,5 @@
package edu.ntnu.idi.idatt;
-import edu.ntnu.idi.idatt.storage.CSVtoJSON;
import edu.ntnu.idi.idatt.view.SceneManager;
import edu.ntnu.idi.idatt.view.entry.StartController;
import edu.ntnu.idi.idatt.view.entry.StartModel;
@@ -34,8 +33,6 @@ public void start(Stage stage) {
SceneManager.init(stage, view.getInstance());
stage.show();
- CSVtoJSON ja = new CSVtoJSON();
- ja.csvFile();
}
}
From 666b6647beef3c0a2a7b4f9bd917f3ce3093a3f5 Mon Sep 17 00:00:00 2001
From: pawelsa
Date: Sat, 9 May 2026 23:42:06 +0200
Subject: [PATCH 059/157] chore: JavaDocs cleanup.
---
src/main/java/edu/ntnu/idi/idatt/model/market/Stock.java | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/main/java/edu/ntnu/idi/idatt/model/market/Stock.java b/src/main/java/edu/ntnu/idi/idatt/model/market/Stock.java
index 88bdfeb..923df9f 100644
--- a/src/main/java/edu/ntnu/idi/idatt/model/market/Stock.java
+++ b/src/main/java/edu/ntnu/idi/idatt/model/market/Stock.java
@@ -91,7 +91,7 @@ public BigDecimal getLatestPriceChange() {
}
/**
- * Getter for sale price
+ * Getter for current sale price
*
* @return - BigDecimal with current (newest in array) stock price.
*/
From dfd25d7d62dd1fa93b4a63cc42d2e9a1b2a7a8cd Mon Sep 17 00:00:00 2001
From: pawelsa
Date: Sat, 9 May 2026 23:42:51 +0200
Subject: [PATCH 060/157] refactor(UserSession): Add Exchange as session
information.
---
.../ntnu/idi/idatt/session/UserSession.java | 34 +++++++++++++++++++
1 file changed, 34 insertions(+)
diff --git a/src/main/java/edu/ntnu/idi/idatt/session/UserSession.java b/src/main/java/edu/ntnu/idi/idatt/session/UserSession.java
index c8a2863..841a22a 100644
--- a/src/main/java/edu/ntnu/idi/idatt/session/UserSession.java
+++ b/src/main/java/edu/ntnu/idi/idatt/session/UserSession.java
@@ -1,5 +1,6 @@
package edu.ntnu.idi.idatt.session;
+import edu.ntnu.idi.idatt.model.Exchange;
import edu.ntnu.idi.idatt.model.player.Player;
public class UserSession {
@@ -19,6 +20,7 @@ public static UserSession getInstance() {
}
private Player player;
+ private Exchange exchange;
public Player getPlayer() {
return player;
@@ -28,4 +30,36 @@ public void setPlayer(Player player) {
this.player = player;
}
+ public Exchange getExchange() {
+ return exchange;
+ }
+
+ public void setExchange(Exchange exchange) {
+ this.exchange = exchange;
+ }
+
+ public SessionBundle getSession() {
+ return new SessionBundle(player, exchange);
+ }
+
+ public class SessionBundle {
+
+ private Player player;
+ private Exchange exchange;
+
+ public SessionBundle(Player player, Exchange exchange) {
+ this.player = player;
+ this.exchange = exchange;
+ }
+
+ public Player getPlayer() {
+ return player;
+ }
+
+ public Exchange getExchange() {
+ return exchange;
+ }
+
+ }
+
}
From 625e9363961d217a4c6add4738d1a483e666f847 Mon Sep 17 00:00:00 2001
From: pawelsa
Date: Sat, 9 May 2026 23:43:23 +0200
Subject: [PATCH 061/157] feat(StorageFile): Class for persistent storage file
placement.
---
.../ntnu/idi/idatt/storage/StorageFile.java | 75 +++++++++++++++++++
1 file changed, 75 insertions(+)
create mode 100644 src/main/java/edu/ntnu/idi/idatt/storage/StorageFile.java
diff --git a/src/main/java/edu/ntnu/idi/idatt/storage/StorageFile.java b/src/main/java/edu/ntnu/idi/idatt/storage/StorageFile.java
new file mode 100644
index 0000000..9fa36e7
--- /dev/null
+++ b/src/main/java/edu/ntnu/idi/idatt/storage/StorageFile.java
@@ -0,0 +1,75 @@
+package edu.ntnu.idi.idatt.storage;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+
+/**
+ * Utility class for system-specific path obtaining.
+ */
+public class StorageFile {
+ private static final String STORAGE_FOLDER = "Millions";
+
+ /**
+ * Method for obtaining storage file path.
+ *
+ * @return Path object of storage file.
+ */
+ public static Path getStorageFile() {
+ return getAppDataDirectory().resolve("storage.json");
+ }
+
+ /**
+ * Method for ensuring that storage directory exists.
+ *
+ *
+ * Attempts to create the application data directory if
+ * one does not exist.
+ *
+ */
+ public static void ensureAppDataDirectoryExists() {
+ Path dir = getAppDataDirectory();
+ try {
+ Files.createDirectories(dir);
+
+ Path storageFile = dir.resolve("storage.json");
+ if (!Files.exists(storageFile)) {
+ Files.createFile(storageFile);
+ }
+ } catch (IOException e) {
+ throw new RuntimeException("Could not create app data directory: " + dir, e);
+ }
+ }
+
+ /**
+ * Method for obtaining system-specific directory for application data.
+ *
+ * @return Path object of the found directory.
+ */
+ private static Path getAppDataDirectory() {
+ String userHome = System.getProperty("user.home");
+ String os = System.getProperty("os.name").toLowerCase();
+
+ // AppData folder for windows
+ if (os.contains("win")) {
+ String appData = System.getenv("APPDATA");
+
+ if (appData != null) {
+ return Paths.get(appData, STORAGE_FOLDER);
+ }
+ // If AppData not a environmental variable, sets to user directory.
+ return Paths.get(userHome, STORAGE_FOLDER);
+
+ }
+ // Library folder in MacOS
+ if (os.contains("mac")) {
+ return Paths.get(userHome, "Library", "Application Support", STORAGE_FOLDER);
+ }
+
+ // All linux and unix-like systems.
+ return Paths.get(userHome, ".local", "share", STORAGE_FOLDER);
+
+ }
+
+}
From 302f33c5773c2e863b4238ab87670d52c0960441 Mon Sep 17 00:00:00 2001
From: pawelsa
Date: Sat, 9 May 2026 23:44:15 +0200
Subject: [PATCH 062/157] feat(SessionManager): Class for managing current
session data + persistance.
---
.../idi/idatt/storage/SessionManager.java | 129 ++++++++++++++++++
1 file changed, 129 insertions(+)
create mode 100644 src/main/java/edu/ntnu/idi/idatt/storage/SessionManager.java
diff --git a/src/main/java/edu/ntnu/idi/idatt/storage/SessionManager.java b/src/main/java/edu/ntnu/idi/idatt/storage/SessionManager.java
new file mode 100644
index 0000000..d2f1e55
--- /dev/null
+++ b/src/main/java/edu/ntnu/idi/idatt/storage/SessionManager.java
@@ -0,0 +1,129 @@
+package edu.ntnu.idi.idatt.storage;
+
+import java.io.FileNotFoundException;
+import java.io.FileReader;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.Writer;
+import java.lang.reflect.Type;
+import java.nio.file.Files;
+import java.util.ArrayList;
+import java.util.List;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonIOException;
+import com.google.gson.JsonSyntaxException;
+import com.google.gson.reflect.TypeToken;
+
+import edu.ntnu.idi.idatt.model.Exchange;
+import edu.ntnu.idi.idatt.model.player.Player;
+import edu.ntnu.idi.idatt.session.UserSession;
+import edu.ntnu.idi.idatt.session.UserSession.SessionBundle;
+
+/**
+ * Class for managing user sessions.
+ *
+ *
+ * Utilizes gson serialization and deserialization to manage
+ * player and game state.
+ */
+public class SessionManager {
+
+ // Gson type to list format. Instead of using wrapper class.
+ private static Type SESSION_BUNDLE_TYPE = new TypeToken>() {
+ }.getType();
+
+ private static Gson gson = new GsonBuilder()
+ .setPrettyPrinting()
+ .create();
+
+ // Static initiator to ensure persistent storage file.
+ static {
+ StorageFile.ensureAppDataDirectoryExists();
+ }
+
+ /**
+ * Method for setting new session.
+ *
+ * @see UserSession
+ */
+ public static void newSession(Player player, Exchange exchange) {
+ UserSession.getInstance().setPlayer(player);
+ UserSession.getInstance().setExchange(exchange);
+ }
+
+ /**
+ * Method for saving current session.
+ */
+ public static void saveSession() {
+ // don't save if current session is null accidentally
+ if (UserSession.getInstance().getPlayer() == null || UserSession.getInstance().getExchange() == null) {
+ return;
+ }
+
+ // Load all sessions
+ List bundles = loadAllSessions();
+
+ try (Writer writer = new FileWriter(StorageFile.getStorageFile().toFile())) {
+
+ // Append current session
+ SessionBundle existing = bundles.stream()
+ .filter(s -> s.getPlayer().getName().equals(UserSession.getInstance().getPlayer().getName()))
+ .findFirst().orElse(null);
+
+ if (existing != null) {
+ bundles.set(bundles.indexOf(existing), UserSession.getInstance().getSession());
+ } else {
+ bundles.add(UserSession.getInstance().getSession());
+ }
+
+ gson.toJson(bundles, writer);
+
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to save current session!", e);
+ }
+ }
+
+ /**
+ * Method for loading a user session by player name.
+ *
+ * @return if session was found or not.
+ */
+ public static boolean loadSession(String playerName) {
+
+ List bundles = loadAllSessions();
+
+ for (SessionBundle session : bundles) {
+ if (session.getPlayer().getName().equals(playerName)) {
+ UserSession.getInstance().setPlayer(session.getPlayer());
+ UserSession.getInstance().setExchange(session.getExchange());
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Method for serialization of all sessions.
+ *
+ * @return List or empty list if none entires were made.
+ */
+ private static List loadAllSessions() {
+ try {
+ if (!Files.exists(StorageFile.getStorageFile()) || Files.size(StorageFile.getStorageFile()) == 0) {
+ return new ArrayList<>();
+ }
+ } catch (IOException e) {
+ return new ArrayList<>();
+ }
+
+ try {
+ return gson.fromJson(new FileReader(StorageFile.getStorageFile().toString()), SESSION_BUNDLE_TYPE);
+ } catch (JsonSyntaxException | JsonIOException | FileNotFoundException e) {
+ return new ArrayList<>();
+ }
+
+ }
+
+}
From 03a918ecd937a6930aa3c138e6a83a8e790238c3 Mon Sep 17 00:00:00 2001
From: pawelsa
Date: Sat, 9 May 2026 23:45:03 +0200
Subject: [PATCH 063/157] feat(Start-MVC): Added persistent storage to start
view.
---
.../idi/idatt/view/entry/StartController.java | 48 +++++++++++++++----
.../ntnu/idi/idatt/view/entry/StartModel.java | 8 ++++
.../ntnu/idi/idatt/view/entry/StartView.java | 6 ++-
3 files changed, 53 insertions(+), 9 deletions(-)
diff --git a/src/main/java/edu/ntnu/idi/idatt/view/entry/StartController.java b/src/main/java/edu/ntnu/idi/idatt/view/entry/StartController.java
index 52f7eeb..04eea82 100644
--- a/src/main/java/edu/ntnu/idi/idatt/view/entry/StartController.java
+++ b/src/main/java/edu/ntnu/idi/idatt/view/entry/StartController.java
@@ -1,10 +1,15 @@
package edu.ntnu.idi.idatt.view.entry;
import java.io.File;
+import java.io.IOException;
import java.math.BigDecimal;
+import java.util.List;
+import edu.ntnu.idi.idatt.model.Exchange;
+import edu.ntnu.idi.idatt.model.market.Stock;
import edu.ntnu.idi.idatt.model.player.Player;
-import edu.ntnu.idi.idatt.session.UserSession;
+import edu.ntnu.idi.idatt.storage.SessionManager;
+import edu.ntnu.idi.idatt.storage.StockParser;
import edu.ntnu.idi.idatt.view.SceneManager;
import edu.ntnu.idi.idatt.view.components.AbstractController;
import edu.ntnu.idi.idatt.view.primary.MainView;
@@ -28,14 +33,31 @@ public void obtainCSVFile() {
public void initializeGame() {
model.getError().set(" "); // Empty buffers
- if (model.getName().get() == null || model.getBalance().get() == null) {
- model.getError().set("Name and/or balance fields can't be empty");
+ if (model.getName().get() == null) {
+ model.getError().set("Name field can't be empty");
return;
}
- if (model.getName().get().isBlank() ||
- model.getBalance().get().isBlank()) {
- model.getError().set("Name and/or balance fields can't be empty");
+ if (model.getName().get().isBlank()) {
+ model.getError().set("Name field can't be empty!");
+ return;
+ }
+
+ boolean loadResult = SessionManager.loadSession(model.getName().get());
+ if (loadResult) {
+ SceneManager.switchTo(new MainView().getInstance());
+ return;
+ } else {
+ model.isNewGame().set(true);
+ }
+
+ if (model.getBalance().get() == null) {
+ model.getError().set("Balance field can't be empty");
+ return;
+ }
+
+ if (model.getBalance().get().isBlank()) {
+ model.getError().set("Balance field can't be empty!");
return;
}
@@ -52,9 +74,19 @@ public void initializeGame() {
return;
}
- UserSession.getInstance().setPlayer(new Player(model.getName().get(), balance));
- SceneManager.switchTo(new MainView().getInstance());
+ List stocks;
+ try {
+ stocks = StockParser.load(csv.getPath());
+ } catch (IOException e) {
+ model.getError().set(e.getMessage());
+ return;
+ }
+ Player player = new Player(model.getName().get(), balance);
+ Exchange exchange = new Exchange(player.getName(), stocks);
+ SessionManager.newSession(player, exchange);
+ SessionManager.saveSession();
+ SceneManager.switchTo(new MainView().getInstance());
}
}
diff --git a/src/main/java/edu/ntnu/idi/idatt/view/entry/StartModel.java b/src/main/java/edu/ntnu/idi/idatt/view/entry/StartModel.java
index e1b0f92..3e4c4ed 100644
--- a/src/main/java/edu/ntnu/idi/idatt/view/entry/StartModel.java
+++ b/src/main/java/edu/ntnu/idi/idatt/view/entry/StartModel.java
@@ -1,6 +1,8 @@
package edu.ntnu.idi.idatt.view.entry;
import edu.ntnu.idi.idatt.view.components.Model;
+import javafx.beans.property.BooleanProperty;
+import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
@@ -11,6 +13,8 @@ public class StartModel implements Model {
private final StringProperty error = new SimpleStringProperty();
private final StringProperty fileName = new SimpleStringProperty();
+ private final BooleanProperty newGame = new SimpleBooleanProperty();
+
public StringProperty getName() {
return name;
}
@@ -27,4 +31,8 @@ public StringProperty getFileName() {
return fileName;
}
+ public BooleanProperty isNewGame() {
+ return newGame;
+ }
+
}
diff --git a/src/main/java/edu/ntnu/idi/idatt/view/entry/StartView.java b/src/main/java/edu/ntnu/idi/idatt/view/entry/StartView.java
index 60180b1..f32cc87 100644
--- a/src/main/java/edu/ntnu/idi/idatt/view/entry/StartView.java
+++ b/src/main/java/edu/ntnu/idi/idatt/view/entry/StartView.java
@@ -24,6 +24,7 @@ public class StartView extends AbstractView {
private Label errorLabel;
// Global buttons for controller implementation
+ private VBox newGameWrapper;
private Button csvButton;
private Button startButton;
@@ -44,6 +45,7 @@ public Parent createContent() {
// Create and style wrappers
VBox wrapper = new VBox();
+ newGameWrapper = new VBox();
HBox playerWrapper = new HBox();
HBox balanceWrapper = new HBox();
playerWrapper.setAlignment(Pos.CENTER_LEFT);
@@ -98,7 +100,8 @@ public Parent createContent() {
VBox.setVgrow(filler, Priority.ALWAYS);
// Wrap components together and adjust positioning
- wrapper.getChildren().addAll(List.of(playerWrapper, balanceWrapper, csvWrapper));
+ newGameWrapper.getChildren().addAll(balanceWrapper, csvWrapper);
+ wrapper.getChildren().addAll(playerWrapper, newGameWrapper);
wrapper.setAlignment(Pos.BASELINE_LEFT);
root.getChildren().addAll(List.of(title, wrapper, filler, errorLabel, startButton));
@@ -111,6 +114,7 @@ public void setModel(StartModel model) {
this.balanceField.textProperty().bindBidirectional(model.getBalance());
this.errorLabel.textProperty().bind(model.getError());
this.fileLabel.textProperty().bind(model.getFileName());
+ this.newGameWrapper.visibleProperty().bind(model.isNewGame());
}
public void setController(StartController controller) {
From a2d57143f6f4acf61315c19cdd946303a9c4a80e Mon Sep 17 00:00:00 2001
From: Pawel Sapula
Date: Sat, 9 May 2026 23:56:06 +0200
Subject: [PATCH 064/157] Update README.md
---
README.md | 70 +++++++++++++++++++++++++++++++++++++++++++++++++++----
1 file changed, 66 insertions(+), 4 deletions(-)
diff --git a/README.md b/README.md
index ab3bf21..01d477e 100644
--- a/README.md
+++ b/README.md
@@ -19,10 +19,72 @@ What you can expect from the game:
- Something 3
# Packages
-- Main package: `/edu/ntnu/idi/idatt/`
-- Base classes: `model/`
-- Game classes: `game/`
-- GUI elements: `view/`
+```
+.
+├── java
+│ └── edu
+│ └── ntnu
+│ └── idi
+│ └── idatt
+│ ├── common
+│ │ └── Observer.java
+│ ├── Launcher.java
+│ ├── model
+│ │ ├── Exchange.java
+│ │ ├── market
+│ │ │ └── Stock.java
+│ │ ├── player
+│ │ │ └── Player.java
+│ │ ├── portfolio
+│ │ │ ├── Portfolio.java
+│ │ │ └── Share.java
+│ │ └── transaction
+│ │ ├── Purchase.java
+│ │ ├── Sale.java
+│ │ ├── TransactionArchive.java
+│ │ └── Transaction.java
+│ ├── service
+│ │ └── transaction
+│ │ ├── PurchaseCalculator.java
+│ │ ├── SaleCalculator.java
+│ │ └── TransactionCalculator.java
+│ ├── session
+│ │ └── UserSession.java
+│ ├── storage
+│ │ ├── SessionManager.java
+│ │ ├── StockParser.java
+│ │ └── StorageFile.java
+│ └── view
+│ ├── components
+│ │ ├── AbstractController.java
+│ │ ├── AbstractView.java
+│ │ ├── AbstractViewUI.java
+│ │ ├── elements
+│ │ │ ├── IconComponent.java
+│ │ │ └── SearchBarComponent.java
+│ │ ├── Model.java
+│ │ ├── primitives
+│ │ │ └── ActionEventHandler.java
+│ │ └── ui
+│ ├── entry
+│ │ ├── StartController.java
+│ │ ├── StartModel.java
+│ │ └── StartView.java
+│ ├── primary
+│ │ └── MainView.java
+│ └── SceneManager.java
+└── resources
+ ├── icons
+ │ ├── portfolio.png
+ │ ├── quit.png
+ │ ├── search.png
+ │ └── user.png
+ ├── save.json
+ ├── stocks.csv
+ ├── themes
+ │ └── default.css
+ └── user.png
+```
# How to run
- Running program: `mvn clean javafx:run`
From 00765f5e3600d8185af171798d8b5f3dec657e19 Mon Sep 17 00:00:00 2001
From: pawelsa
Date: Sun, 10 May 2026 13:29:37 +0200
Subject: [PATCH 065/157] feat: add UIElementCompositor class for UI building.
---
.../components/ui/UIElementCompositor.java | 85 +++++++++++++++++++
1 file changed, 85 insertions(+)
create mode 100644 src/main/java/edu/ntnu/idi/idatt/view/components/ui/UIElementCompositor.java
diff --git a/src/main/java/edu/ntnu/idi/idatt/view/components/ui/UIElementCompositor.java b/src/main/java/edu/ntnu/idi/idatt/view/components/ui/UIElementCompositor.java
new file mode 100644
index 0000000..0fed8e6
--- /dev/null
+++ b/src/main/java/edu/ntnu/idi/idatt/view/components/ui/UIElementCompositor.java
@@ -0,0 +1,85 @@
+package edu.ntnu.idi.idatt.view.components.ui;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import javafx.geometry.Pos;
+import javafx.scene.Parent;
+import javafx.scene.layout.HBox;
+import javafx.scene.layout.Pane;
+import javafx.scene.layout.Priority;
+import javafx.scene.layout.Region;
+import javafx.scene.layout.VBox;
+
+public class UIElementCompositor {
+
+ private final Pane parent;
+ private final List elements;
+
+ public UIElementCompositor(Builder builder) {
+ this.parent = builder.parent;
+ this.elements = builder.elements;
+ }
+
+ public Parent makeUI() {
+ parent.getChildren().addAll(elements);
+ return parent;
+ }
+
+ public static class Builder {
+ private Pane parent;
+ private ArrayList elements = new ArrayList<>();
+
+ public Builder parent(Pane parent) {
+ this.parent = parent;
+ return this;
+ }
+
+ public Builder growWithAlignment(Pos position) {
+ if (parent instanceof HBox) {
+ ((HBox) parent).setAlignment(position);
+ parent.setMaxWidth(Double.MAX_VALUE);
+ HBox.setHgrow(parent, Priority.ALWAYS);
+ }
+ if (parent instanceof VBox) {
+ ((VBox) parent).setAlignment(position);
+ parent.setMaxHeight(Double.MAX_VALUE);
+ VBox.setVgrow(parent, Priority.ALWAYS);
+ }
+ return this;
+ }
+
+ public Builder setPrefSize(double width, double height) {
+ parent.setPrefSize(width, height);
+ return this;
+ }
+
+ public Builder addContent(Parent parent) {
+ elements.add(parent);
+ return this;
+ }
+
+ public Builder addAllContent(Parent... parents) {
+ elements.addAll(List.of(parents));
+ return this;
+ }
+
+ public Builder filler() {
+ Region filler = new Region();
+ if (parent instanceof HBox) {
+ HBox.setHgrow(filler, Priority.ALWAYS);
+ }
+ if (parent instanceof VBox) {
+ VBox.setVgrow(filler, Priority.ALWAYS);
+ }
+ elements.add(filler);
+ return this;
+ }
+
+ public UIElementCompositor build() {
+ return new UIElementCompositor(this);
+ }
+
+ }
+
+}
From 6bf236f8280071cd822293109f79d468df2b3f88 Mon Sep 17 00:00:00 2001
From: pawelsa
Date: Sun, 10 May 2026 13:30:04 +0200
Subject: [PATCH 066/157] refactor: MainView to utilize the new mechanism.
---
.../idatt/view/components/AbstractViewUI.java | 16 ++--
.../ntnu/idi/idatt/view/primary/MainView.java | 95 ++++++++++---------
2 files changed, 62 insertions(+), 49 deletions(-)
diff --git a/src/main/java/edu/ntnu/idi/idatt/view/components/AbstractViewUI.java b/src/main/java/edu/ntnu/idi/idatt/view/components/AbstractViewUI.java
index 1b7b889..1933e3b 100644
--- a/src/main/java/edu/ntnu/idi/idatt/view/components/AbstractViewUI.java
+++ b/src/main/java/edu/ntnu/idi/idatt/view/components/AbstractViewUI.java
@@ -15,6 +15,7 @@ public abstract class AbstractViewUI extends AbstractView {
private VBox navigation;
private HBox header;
private HBox toolbar;
+ private VBox menu;
private SimpleBooleanProperty isMenuVisible = new SimpleBooleanProperty(false);
public AbstractViewUI() {
@@ -35,8 +36,7 @@ public AbstractViewUI() {
this.setInstance(new StackPane());
- Parent menu = createMenu();
- StackPane.setAlignment(menu, Pos.CENTER_RIGHT);
+ menu.getChildren().add(createMenu());
menu.setVisible(false);
Region disableMenu = new Region();
@@ -59,16 +59,20 @@ public void createUIComponents() {
navigation = new VBox();
navigation.setMaxHeight(Double.MAX_VALUE);
navigation.getStyleClass().add("dark");
- navigation.setPrefWidth(150);
+ navigation.setMaxWidth(150);
header = new HBox();
header.getStyleClass().add("light");
- header.setPrefHeight(80);
+ header.setMaxHeight(80);
toolbar = new HBox();
toolbar.getStyleClass().add("light");
- HBox.setHgrow(toolbar, Priority.ALWAYS);
- toolbar.setPrefHeight(80);
+ toolbar.setMaxHeight(80);
+
+ menu = new VBox();
+ StackPane.setAlignment(menu, Pos.CENTER_RIGHT);
+ menu.getStyleClass().add("dark");
+ menu.setMaxWidth(300);
}
public abstract Parent createContent();
diff --git a/src/main/java/edu/ntnu/idi/idatt/view/primary/MainView.java b/src/main/java/edu/ntnu/idi/idatt/view/primary/MainView.java
index d3a151d..b6187ec 100644
--- a/src/main/java/edu/ntnu/idi/idatt/view/primary/MainView.java
+++ b/src/main/java/edu/ntnu/idi/idatt/view/primary/MainView.java
@@ -4,14 +4,13 @@
import edu.ntnu.idi.idatt.view.components.AbstractViewUI;
import edu.ntnu.idi.idatt.view.components.elements.IconComponent;
import edu.ntnu.idi.idatt.view.components.elements.SearchBarComponent;
+import edu.ntnu.idi.idatt.view.components.ui.UIElementCompositor;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Parent;
import javafx.scene.control.Label;
import javafx.scene.image.Image;
import javafx.scene.layout.HBox;
-import javafx.scene.layout.Priority;
-import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
@@ -26,89 +25,99 @@ public Parent createContent() {
@Override
public Parent createNavigation() {
- VBox navigation = new VBox();
Label title = new Label("Title");
- title.getStyleClass().add("big-text-32");
-
Label newspaper = new Label(" • Newspaper");
+
+ title.getStyleClass().add("big-text-32");
newspaper.getStyleClass().add("med-text-16");
- navigation.getChildren().addAll(title, newspaper);
- return navigation;
+ UIElementCompositor navigation = new UIElementCompositor.Builder()
+ .parent(new VBox())
+ .addAllContent(title, newspaper)
+ .build();
+
+ return navigation.makeUI();
}
@Override
public Parent createHeader() {
- this.getHeader().setAlignment(Pos.CENTER);
SearchBarComponent bar = new SearchBarComponent("Search after stocks..");
- bar.onSearchQuery(() -> System.out.println(bar.getQuery()));
+
HBox.setMargin(bar, new Insets(20));
- return bar;
+
+ UIElementCompositor header = new UIElementCompositor.Builder()
+ .parent(new HBox())
+ .growWithAlignment(Pos.CENTER)
+ .filler()
+ .addContent(bar)
+ .filler()
+ .build();
+
+ bar.onSearchQuery(() -> System.out.println(bar.getQuery()));
+
+ return header.makeUI();
}
@Override
public Parent createToolbar() {
- HBox bar = new HBox();
- bar.setAlignment(Pos.CENTER);
- bar.setMaxWidth(Double.MAX_VALUE);
- HBox.setHgrow(bar, Priority.ALWAYS);
-
Label balance = new Label(" Money: " + UserSession.getInstance().getPlayer().getMoney().toString() + " USD");
- balance.getStyleClass().add("med-text-16");
+ Label playerStatus = new Label("Status:" + UserSession.getInstance().getPlayer().getStatus());
+ Label playerNetWorth = new Label(
+ "Net Worth: " + UserSession.getInstance().getPlayer().getNetWorth().toString() + " USD");
- Region filler = new Region();
- HBox.setHgrow(filler, Priority.ALWAYS);
-
- VBox infoWrapper = new VBox();
- infoWrapper.setAlignment(Pos.CENTER);
-
- Label playerStatus = new Label();
- playerStatus.setText("Status:" + UserSession.getInstance().getPlayer().getStatus());
+ balance.getStyleClass().add("med-text-16");
playerStatus.getStyleClass().add("med-text-16");
-
- Label playerNetWorth = new Label();
- playerNetWorth.setText("Net Worth: " + UserSession.getInstance().getPlayer().getNetWorth().toString() + " USD");
playerNetWorth.getStyleClass().add("med-text-16");
+ VBox infoWrapper = new VBox();
+ infoWrapper.setAlignment(Pos.CENTER);
infoWrapper.getChildren().addAll(playerStatus, playerNetWorth);
- HBox iconWrapper = new HBox();
- iconWrapper.setAlignment(Pos.CENTER);
-
Image userImg = new Image(this.getClass().getResource("/icons/user.png").toExternalForm());
Image quitImg = new Image(this.getClass().getResource("/icons/quit.png").toExternalForm());
IconComponent userIcon = new IconComponent(userImg, null, 44);
IconComponent quitIcon = new IconComponent(quitImg, null, 44);
+ HBox iconWrapper = new HBox();
+ iconWrapper.setAlignment(Pos.CENTER);
+ iconWrapper.getChildren().addAll(userIcon, quitIcon);
+
+ UIElementCompositor toolbar = new UIElementCompositor.Builder()
+ .parent(new HBox())
+ .growWithAlignment(Pos.CENTER)
+ .addContent(balance)
+ .filler()
+ .addAllContent(infoWrapper, iconWrapper)
+ .build();
+
userIcon.onIconClick(() -> toggleMenu());
- iconWrapper.getChildren().addAll(userIcon, quitIcon);
+ return toolbar.makeUI();
- bar.getChildren().addAll(balance, filler, infoWrapper, iconWrapper);
- return bar;
}
@Override
public Parent createMenu() {
- VBox menu = new VBox();
- menu.setMaxSize(300, Double.MAX_VALUE);
- menu.getStyleClass().add("dark");
- menu.setAlignment(Pos.TOP_CENTER);
Image menuImg = new Image(this.getClass().getResource("/icons/user.png").toExternalForm());
- IconComponent menuIcon = new IconComponent(menuImg, "Account", 60);
- menuIcon.getStyleClass().add("big-text-32");
+ IconComponent menuIcon = new IconComponent(menuImg, "Account", 60);
Label portfolio = new Label(" • Portfolio");
- portfolio.getStyleClass().add("med-text-16");
-
Label transactions = new Label(" • Transactions");
+
+ menuIcon.getStyleClass().add("big-text-32");
+ portfolio.getStyleClass().add("med-text-16");
transactions.getStyleClass().add("med-text-16");
- menu.getChildren().addAll(menuIcon, portfolio, transactions);
+ UIElementCompositor menu = new UIElementCompositor.Builder()
+ .parent(new VBox())
+ .growWithAlignment(Pos.TOP_CENTER)
+ .setPrefSize(300, Double.MAX_VALUE)
+ .addAllContent(menuIcon, portfolio, transactions)
+ .build();
- return menu;
+ return menu.makeUI();
}
}
From 8d113105caacdf4daf83bcb2eb1860969b3e2e60 Mon Sep 17 00:00:00 2001
From: pawelsa