From cad69c8de5995b93af084210217afffb1fd3d6eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B8rgen=20Austvik?= Date: Fri, 2 Dec 2022 21:14:53 +0100 Subject: [PATCH] [Nanoleaf] New Channel: State (#13746) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [Nanoleaf] New Channel: State Shows an image of the state of the panels with color. Also makes the layout slightly prettier. This is less functional than the layout, and more eyecandy. Signed-off-by: Jørgen Austvik --- .../org.openhab.binding.nanoleaf/README.md | 14 +- .../doc/Layout.png | Bin 25495 -> 71541 bytes .../doc/NanoCanvas_rendered.png | Bin 0 -> 7422 bytes .../internal/NanoleafBindingConstants.java | 1 + .../internal/NanoleafHandlerFactory.java | 2 + .../handler/NanoleafControllerHandler.java | 61 +- .../handler/NanoleafPanelHandler.java | 21 +- .../internal/layout/DrawingSettings.java | 95 +++ .../internal/layout/ImagePoint2D.java | 45 + .../internal/layout/LayoutSettings.java | 52 ++ .../internal/layout/NanoleafLayout.java | 109 +-- .../nanoleaf/internal/layout/PanelState.java | 52 ++ .../nanoleaf/internal/layout/ShapeType.java | 46 +- .../shape/BarycentricTriangleGradient.java | 152 ++++ .../internal/layout/shape/Hexagon.java | 6 +- .../internal/layout/shape/HexagonCorners.java | 152 ++++ .../nanoleaf/internal/layout/shape/Panel.java | 79 ++ .../internal/layout/shape/PanelFactory.java | 93 +++ .../nanoleaf/internal/layout/shape/Point.java | 41 +- .../nanoleaf/internal/layout/shape/Shape.java | 81 +- .../internal/layout/shape/ShapeFactory.java | 44 - .../internal/layout/shape/Square.java | 11 +- .../internal/layout/shape/Triangle.java | 12 +- .../resources/OH-INF/i18n/nanoleaf.properties | 2 + .../resources/OH-INF/thing/lightpanels.xml | 7 + .../internal/layout/NanoleafLayoutTest.java | 92 ++ .../src/test/resources/lasvegas.json | 788 ++++++++++++++++++ .../src/test/resources/spaceinvader.json | 152 ++++ .../src/test/resources/squares.json | 172 ++++ .../src/test/resources/theduck.json | 129 +++ .../src/test/resources/wings.json | 143 ++++ 31 files changed, 2418 insertions(+), 236 deletions(-) create mode 100644 bundles/org.openhab.binding.nanoleaf/doc/NanoCanvas_rendered.png create mode 100644 bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/DrawingSettings.java create mode 100644 bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/ImagePoint2D.java create mode 100644 bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/LayoutSettings.java create mode 100644 bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/PanelState.java create mode 100644 bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/shape/BarycentricTriangleGradient.java create mode 100644 bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/shape/HexagonCorners.java create mode 100644 bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/shape/Panel.java create mode 100644 bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/shape/PanelFactory.java delete mode 100644 bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/shape/ShapeFactory.java create mode 100644 bundles/org.openhab.binding.nanoleaf/src/test/java/org/openhab/binding/nanoleaf/internal/layout/NanoleafLayoutTest.java create mode 100644 bundles/org.openhab.binding.nanoleaf/src/test/resources/lasvegas.json create mode 100644 bundles/org.openhab.binding.nanoleaf/src/test/resources/spaceinvader.json create mode 100644 bundles/org.openhab.binding.nanoleaf/src/test/resources/squares.json create mode 100644 bundles/org.openhab.binding.nanoleaf/src/test/resources/theduck.json create mode 100644 bundles/org.openhab.binding.nanoleaf/src/test/resources/wings.json diff --git a/bundles/org.openhab.binding.nanoleaf/README.md b/bundles/org.openhab.binding.nanoleaf/README.md index b8a635b936f..39816999e6d 100644 --- a/bundles/org.openhab.binding.nanoleaf/README.md +++ b/bundles/org.openhab.binding.nanoleaf/README.md @@ -104,7 +104,15 @@ Compare the following output with the right picture at the beginning of the arti 41451 ``` - + +## State + +The state channel shows an image of the panels on the wall. +You have to configure things for each panel to get the correct color. +Since the colors of the panels can make it difficult to see the panel ids, please use the layout channel where the background color is always white to identify them. + +![Image](doc/NanoCanvas_rendered.jpg) + ## Thing Configuration The controller thing has the following parameters: @@ -137,10 +145,12 @@ The controller bridge has the following channels: | colorTemperatureAbs | Number | Color temperature (in Kelvin, 1200 to 6500) of all light panels | No | | colorMode | String | Color mode of the light panels | Yes | | effect | String | Selected effect of the light panels | No | +| layout | Image | Shows the layout of your panels with IDs. | Yes | | rhythmState | Switch | Connection state of the rhythm module | Yes | | rhythmActive | Switch | Activity state of the rhythm module | Yes | | rhythmMode | Number | Sound source for the rhythm module. 0=Microphone, 1=Aux cable | No | -| swipe | Trigger | [Canvas / Shapes Only] Detects Swipes over the panel.LEFT, RIGHT, UP, DOWN events are supported. | YES | +| state | Image | Shows the current state of your panels with colors. | Yes | +| swipe | Trigger | [Canvas / Shapes Only] Detects Swipes over the panel.LEFT, RIGHT, UP, DOWN events are supported. | Yes | diff --git a/bundles/org.openhab.binding.nanoleaf/doc/Layout.png b/bundles/org.openhab.binding.nanoleaf/doc/Layout.png index a8d684a0ce019fbf7009b9d57351c70d473aa48b..d716ffae1e27f8fa7d02069f7e4048720427cf38 100644 GIT binary patch literal 71541 zcmc$`cQjmm*ab>NBqT(%gdk1t1kpv0-bQDlNAE@_LUa;D@12Pjy-q@OMjfLK(fcSf zdb^9s`+o2D-L>w&*I#EXoO6EddG_AVeomOGvg`wVN_;FVtOxJpq|~vnaFwvIZsFX$ zg@uI$*(>^jg%yDHPU@|uhtU>-KvToA5zogh>b*3wNKqP>l!Nhwpa{Qtv*v}DE42vF zMx_g;o55qZova52){xuOgi8{0p`wkKNR31%$-s zLkYN}2>_g&o{D-g=PKxX1W6` zNXf`__4Ej-_-s%1mw9=4+YBK|sECM&fq?Y5Uljgat8u1X(ZfJIXK!n=%0|BW7e!Q5)DEJlYwoEWbai=kRVA6*{FyWsmMUf^ zJ`*iLO&p-niW$Nu!@06z4##nJ)3tVpM!;}aBnaOvM&M!m_VQ({<9dQ`=D8DmmBuyh zGWN@tFMEq!1g4Ow5c6;Z0)d|MK0-D$Hp)m#AN*%+L%(q{tS&DPqzi%IWrm~`>FMbj z8XCcD`g(fbzkd&ph#>Rr_vo6_r0RxktgU&RAMZ_8SrRFTHa@Kvzno{Cc1kEHC`d?1 zh@jvofXGhhY`1fFFW=s@Upykk#qH*8TlS0AV5dgF){ImoBKyy9skRh?QLy2 z+1Vc*o{*AOTf>;`o347cgJOEFMc5%J1I2d#oPDxAv9j#5N~Q zMa+1FO|cmdE8thi&B1!6)uxycAHTG+(z%nf-QTZhpBj6GULPGD9X)9|UvfV?+NrCp zg$@iX$DKz$;i+|)V^`cK-C~*@Ww#a|U_8_~UR|A7JlhUAwcS6l&a5aG#lpIO4|9Bn z-&-`3tel^p`}A)6^i5V-YItcgmiKgZbO^k8qlyEw*-z!O`%zg*On=hgvMM7hOAvR? zv2a<2_@B4;^jM)*21wyzSLZG) zEG)^2orZClT&A7V4iL6xdZ2-UBB;S_Ght)N-5PI%XsA7eXg#sEw)Te*7S{WRfA1=3 zWJE*fWB=;>d8X3aw{N4Pqj#E7gmX=zZ{BzuZcSQPSdeb1qziT!#&NK+zUJliJw+Gq zW}ha{Af=r}1wcpxA0J_TT&#e%L73SjY)kXk*&m*)vh)Y+ocjfKU@h&laIFIic)(Cee;!ajm5 zJUpn0N>f`hEUY`Yn8WubK0f|7F7CnM;SV8KqKmJ_#>O*2S6En_-I#y>J>UKHB`z+m zEkGLvhbz2_Wl+he5W>O|@B4fEtnc5qetyKssZSd0gF>T2Lm$HD?qg-){r&YbX6B8~ zC@NdPsVAl3qAH*v!t(HaU;Ig(aScxx1^Lxp_VW;tvAxH%SUd(~9}cx8c^}V_{8U z!hP1LiK(fJv-3nr(QAJGKC>U;;rQ1x_yHR;cw|Xg*&zzGxv^ma3LPlbuL}$eME{|^ zg_U*o_W|%Q5{5E^hBmDZrsvO}i-@F`-ogqfz^wXO(bSY)vf}vUWDoLTVQnp+zyH;b zAKW(&AjLc&T3%7{{A}(Ds@1V|d2#04(43q5>3V!Af5#U=Bj(5YP5yx6k$>rLEo>}t zm%k5Ib9UYb&G~j|b(H7kO0p-;LHskX|B*5fGchidl$5U?^;B67PEdPgfB)V;HwP&# zjk>vaz~4Vn&J?}auABP-5Q)vm$oTT*rxvz&E;*~tdt+nc(xv(Nx8F`rPot>#qpA3# znUP2&2sx8&A})>v|IB0ZJG9Dju!PN-C^aR;+TQ+hfPe0;KgAj;X5|q3=yeSs5O~o# zgMh)vNJ(8c$JyNRus%e`#c8~I7gnJT07%nvsALFd+i`G|OGl93JBl3{QKzCxy$-+O z>cy_J&7SP+?A<4g_ir+{cwJsW0cG1gS83^BMS2^1dkYX&z~`jY)Vk{G3K;G3($d8A zG*1U6@YZCSNJvO%Xp9Ep65O9Rm2dt#X?K^6fWSFF<$;5qU;>J0S}~{=wm3;+Q&UuA zB)v+8t*L31Nk@2Fo5cKVZ*MQJ&G^<#gN9C?3JnVjcwu1y$ogV+^9^Q%)Ujp1wGK2@ zR8&5ESUcE+Pf}xgkmkH2i36Okr7BS!rx+Oi4~=Gip8_&Xo-p{oKVe z_8<=)8Xg`Fg+dPx4-riuApwDd!$bATqN31CsB!yeHehRWL4nMKC?n$sSP7s_9w@!! zLyZZfSuEF?(%1Jv35&O#U8&RyD_dJL5LPH&zPUVfa&nS1ug+mkSq+$dPTBYC*K>CE zvZf~Q!3+_H=Dp83JM()bIP1I-mq}{5GT#ys_P^EE*8cn{?(KaR9Zgs!;eU=^A5GqQ z0wX8uJ`p6sOuY7xr;D{Uvpx)@tE>C<>sMPcabd=O$l;XDjJu`f>e_Jb=EjDzi%X$$ zdM{aOYO0c=V$x1kQ2XK;t=yH3i1*=E$|DG$rM7nB*RN{6z88a3%#oOUYFC(-r+yB_@^$VR}k4cO`xbjQG(q1AN4eSZG3}$JE*85G6!MM^g*Av+Di`I-G5E zi=eWa8yzKi^oZN<+~cwd>lB8KjolWq0WK1U!aXr~QE-0R0V3ji`dXj&zDkDhSGiXu z6jaz!A7iT;8a$1Rj1&|iVqylnzrG~E$8T$IpYz&eYmEYfH&0NjWWM)fw1vK0YG>Ea z4h^Xq8D%6Sxa;f>FaN~d1NNs18>bZ)ulI34n(YyS_TvGOk&#b$EOc~qhUO|f_DoJw z8=5G{$<-BByk%;n37uP`C2z3oV=vce<+q8-qm$HkUh?zvv$87k&K9L-DsHD)x&o6G z`P?=%si;z;sQA_Co;?d_dTIGPl-P1GBVQ0VTfk){l3K|9^87@*%;02C8(SE1jf%Le zFsMlI)_R4o+XktN2+*UVm@sCwh%k>KDk_SXmsgXOkueL@+Yv#r&h{lK>G6{%PaZw` z;eU16;(g@h?cKM$9~v6^36F?oe^N>y)2}U0|M(Tr9o(B;^M1|u2m$Kp>9K|VR8{Zo z?>7rCLB$oPqaszIP9Tu~?w^kpTru2_^2^7^#|sMRNl4;s;ALuDf`TbDnRfV`?@M0J zqfn?q#bgPpIsdB`N1xWOUxydFVorpdBcr02>Jk%gLLSm%eI!3JGIH*Gg+DtxJAQ(x zZd6U_Nk**jvZwq({5I3P2DH#yZKW_bWtOnN!oPLSuV|2dhePBCqIW&Zxya)mz2osXksQ;L_{QxL1iU@UBhO&)=t099>7S!m)qn>p%xJyPOqHS z&4>H=v8dW;45UBAezplT2!ru5Gs|Z;-xV|MjQsgi9DkRJ&o+tQfkxUj9Qi0LNTLY> zxhesGJ0qXGjO$seI6|Wp<>lp7RIV0jua0|J%2l@tcjtzNc72%A&kpA|U|@8F{Y?EY zYhwEKN+fjoX6UHpllU z=Ecsos=`PZTG1m4RTcx`5fOa@10t?#L-9-+{R0DRtgOfLd+{5q1F5oP^Wt7nf5uCf z683ODVR8>=yr7VfIro$F=qjQ603V<82A9?2RLt_T{sS*#Db38xRLveKyFVl)ef5;d z_XueyB-HdSj-Jh+{>!>37N_G4#!=JLn~E?9g2$$%rS0zSW@ON~KfVq&7k z2ENZ`XkhT;$B*MwEQq|KqN0gO&aOD@?ZCi*HADzU4v`m(ZJ(bPf-th#Nyf9~uOtP1BoRtZDQ%>lFTAI6`YfYB^k->*?P zBt(nxM^zPrV$#n8f~?iKk&#EA=>MFI!Ql$|BV}dm?jgtq?<4jWf+XH^@fZwLx&Htk z9IwE_!ZJyHhl7K|VJ`XsjepYdWy##-_;}~oSbTD(?fOXm`1ttsb_fmqvjfo7TtXu4 z&CQorRysK}jdXP6K>nv_rnyMfKTeL0S+b5!PS>Hwk>cl1--`n{1&5ItxN!r$KC06^ z>WE>G_iJ$p$aQpdl2TJ+p73bJja8b~-I~svYN5c&$RIjTLO@%ILB`2>wfc z_lNoU`Qb1atiRuOZ=vIYm4g9aZ0US(wlGaatoql$yRxoRuY=K%k(K^r9`~(@6Vxhd z&H#OQ*lYHqSfg-iVq&O;v=>Q~k&!_{uUPTphhD9%uCA`GSj@eYk&%&w&CThlsb1s| zAU!?(@hy$cWcW1G&JJE z!g^|K;H15tV{3AMD80SCHSGO&|6uzcH8wWJh+Wp_MvXZnzbx$S{Z97H?cX`2vqm= z>(_51;bbiM1O&m}EG#VW@48hktIDl(%Ysx?RITQUiv1ff@X29qZ7u2NVnzen*?+RT zph3(B90R2C@kZQ0Du1~_12-F+-LF90;o)KY<O#)8O!xGJ>F<{o6?Kn{ zI8Rg<_a$J zB|BFdZg89rJS^za5K1x3wr37%)pMp1@si^B{q?1+U*)oRk?+CIY>SPhCBM~3UN8ZL z768B$uW;`%6_u=v%*fX-U%$Tfca)UGxq7y|yj)vb>sszdLt^w)!Y>NYN}Q!dim%S$GTvZHjb)5^w%;QsyUx;jg(pO0#V-M1#HtwtHK zY2G;g?Cksug+0J%!Q zVemO%ni$>T`{x-`r&skay8pa67I@Yft7=w{f_jG2@lt6a^F5Z z*aV<5rik}_=LzpwP~JkqVxpq*fxjp)fp)E@vZ`t^qRMg*IxqlWWZ3pz{-%BF)~#d1 z`>>bz8TubS6wwGLhlPceEQ*Rd{0&HV@SVAfcUq~bYMZZsw0_5{wY9bX;zz0MW(gew zgYT+Fbwu~>JrVO0uHj%|>4QQG!YlG6JUdb^72dVOVBa$PV_wjT&0T`aTTlHVfFMRG zGBUDC!1f>6DsVdfTRR1HLqieYQ}oo-RNb7PKJO|x`pXVM%Wr4@clEA)ttyxGn=V9+ zOg;ze`I(%?x8R>%GB9+FjKunxIyi9P`uY3&2mc@vGrx4F6Xf&p@d+X-I}Clq7qBW> zF6ZdTsZW+oODNXhGc;@S4@bX)1ED`Z1N)un>r-iHj3xs8JQ0m(|yk zc$HTIoS0cyP$-m*ot-%-;P`Lix+A(eI@n50^`&VnJSJvUFvmt8m^(?v97oT|wz#k` z>w{9VPwhDwi_LNS@PQFgS5dLs8AV0GVWg;{qN1!E9T}O|Xuy7iMxXEZaab*u`(N0| z$#p-^REkHfGblFVh6EwO#l_k0``wC$iB_TM5m8a*;AegqqVVbCP94PZtjWlKGD3N* zgp!Ji?$J9-ONJ&%$>U$UZf3U-FW~;XTwGlI{QNvTR07UR zMQXVTZ2II+pPs|Cfl5i7#s?d)u_Se01B0g>-o4k5y=QRns!QxrO;>kvXlQ73wEgPE zzaZ90ZcWrniNmn*w^m0aVRw57(P(NQ0{aDqDssxzr+FT1z`lI>Ld9=i(ez+em#((t z`S}qm&}Sby#-!2Q+V}Vx4^0!$Ffd3OeK@Z#s^mv`-orcM=bS@A#b@;^R3 zMIxHugM%D8Ov1ux?&*SV*6SnrG&D4>b0I{ujZIBFFPHFcg#?AZ=H(s!Iy5^AMLuje zJ@pEc$`Pbu45h@t?Dyc((tMK58-3`$j-@4o$TiQ{B7894rF1R^IWvCeNkS6GW!gF9 zFB?PCV%+|DQ0&TQB=23dow<39$KFC--Ya=T{B{Ttt$qIQ7;g$u`Z)*S_FYfoK46ff z#E~y*fPZ~`-4?)M(B!_oJKx?0rGMs*St!Y83=9my!cQ5-n`dTbj*mTRO9a|Zh=oOF z6%-V_cmKSVl>CJJ&Kx5tktpJQs0f~ue9$m4G2yZbtpq$|`-?V-|AIp6qQJnw-oC!c zk`+~D)WOMqgvw>LL)V?;F>QXl_Qfwlx``Y>Wkp3ZuvcD94Qz0*{QLJv1oi?(yW@PF zlA3DHJ=q>YG)euru&9Xf_3QXj>#46VWiJowAcjruO8k}gt`K=wS64qjQGHku9uduuM(J70f|J}@Rw}=P zjI?wmz&Ac6rDP64ejigL`6&*A!yRVooo4Et#>dAo!p{7Lm6estO8*lf_pS32RJlPz z8=1Gaw>kIZSh0qkt?inT0oQ+FR}76vPVf3?VO(siraonKW24~ds*ShOep1roN>&<~ z5GiM8ZvEhFm)$?V78XouOJW)u8v{RmBEZL=pnk@OSrr@r0Iizpa+tYQE)SRS@^BPb~@F}>lQIGlC#$M#7 zJGX9-{U6Vz8|jE8p5jC{hGxL6nw^P6ZR+g4^Ht@270(xUpoZ;D7W&eUzDmy19@JE6%&7($oX6B8R zfmB;Sxw*Nyw3HP3T1XWa#vl;Cntx@cxTXeA)XFy&?SlM#Hl2!Rf`WS41|4?DnTGh; zt{B>#t*x|_loC~UYG92u+10b?=;)J^6NA2kO?YZ*DvIp^9$k1|-mBUYwS$9$_+%j= zp-JjSHXa_-+HkHd*&S^F;2Q)2L8@$KIk|2P6L)L9#k_(zNNp`2eKx>9w@ZdULQYOT zE>A;4b3sbBfxKJ@4-aQ$V+(meRqJ!IpOQjZ=JCHsT57z|KWq+DEckTU{Q2|e-QC?A zSj4EU4M(jGy0FDO71)PEF5Rv!FANk~qx^CiLN0s0efzdW7$vW)e2Hw@nJ9tgGBCV& z@q&dV0)hRBk&A0@f4R>VhV`14_g((TjVc>@`zDO6F$(sj@jGNhL_}~F2?+>%i;usn zcc8234O03S`|PZ(Wxd~7TT4nzgpZDrkdiLlP`$tPhq}V;{{DWALS-)a%{H&IwYANI zBwxO7Z-~^=(UDV2D77XP0k^icqQfhMeg_D>DglEhsW}u_-D!f(Y){v%Ybd)? z8wX%iT0!RL&$Ek*t$qdKwq@Dd+m{I@=-a7nBQmMdH-Q?DGSE~v7>9$8&wYEU#^-CG zmzI{6pI>WwIyEruhA$$I=KMLHW;&Rep^x`Wjr#%v0~3LM0zyKmkX*mqFDh!8rPi9( z)+I880of8EQjU&}+S(LNIMo~?B8FplgAoeK%A(hL6~M$x0r&Q8N0E{@FJ&U%cprY3 zK;XZ`Umi;xEl{eet~O|Jrn}-zm;xyLX}C$C0m>wI4v}Z z&9r-VcE%F#d(711k{Hjli$ZNfPB*~#=0~>13WCynUwC26EG$PmbLFsxRy{qvJI1kM zb_$A$ud8*bT(S8j^FFEsPYe&g#lA!$niN~EX4TQga+#f_rKQ%|kBf_ovx(lFPUIb3 z9&S%FZLpjg)!fdY!fcAy^hMe?Kz)Si7>Q_FH~69uW*uR@a|u@c?C{|OBL|0yf&vQd zF}O%Xa%HopLuEstp`jtDV%Y5AWMo7Gk5vv{cTIR9n0}lg;PRP;oQ$lYsYzc~x1ph7 zFR`!;2obe}KIP%znd(Q&P{O6%+EBe`2QO6Yc9+=MOhI?k8#unhUwvNHCU zFLO^%y7i;$VJa}b&>MOTKc`9stivOgZK_x@(RqWK1p0OJ5>e1n$a;B(=h)8-< zv3oG_)SJJA4MuxI*a{jO^|njs&Z*}GZU{;MVKz!w{x}hbVrcMrrV<_zjjOZsRK3$n z5V7AXG@XP&`KgFk^|x=Z9O;PI*jQ&57p9HD43X)XnVIo%cSNIG{|mDc%N&kSs#3wb zB1KHOBVje7bO0kizt&og1ogBxp8K!#$pl11U%B?ScpphfN=}yiYI0hXk(29&fwj-p zZNJ6FN?>PXK9}Cce<@a@y>vh{l|gBuq5r|bmmwzE=%)>Co82fi1HJQQh$)1Snv%!jzArvgJPsY7{mdKtnK$^Eq#7q-0$%@tVnx{g zb7NzgE-o$ugrp+M^q!I`o`$kTEYTN8j1PVx`iwYodgll#Hxr+1c=N z-1N_XNQw(SKK_fBFDFa~qk;;J$SWdP*74*y2isV zT(!bVtu-AT9pAmXrwu!+Sjg^lM^S6*I~E#uLM91D&WS(M#ffWuwsPvHNP;t;THo9 zF00V__K?XE(UBNQiNg4VglKA^+V9`Dw(I6Boc+wrms5Cc+>tZIs_wu{ArSJqp8OBu zVzw>r637%q=BYDLdu$O7y}LoKSuDyK!ab#BtS=@fCy9xPKXKYr*Z0S# zFpJK}$f%&8z$Y+AO6p0EURa;@V1@_^jZWh<4*d2F_QSldzIf%#D1dPfu^$4dU8=0fgK?81Av7OWy;i^`LAF4cTXWVkTy0XD6p*0T=qrthBVWq>pGf zH#b`Ub0=wOTwM5H^|Gx6-+a)Tii(Qewn*8V{K*rmfQyL^Z@INl%fSr8CU?8Tt;t-O zsD)MqZazL59*h10rPQPZO$cb`I%r)3s5~KjhV>zM+SPX5TtQx51z^bdW{o(}jLc`J zr?qu;RYFpK%>kb(6vZ_(NS5{G+qMYPk5TZ>RO(1WLWUybJ&Uu1l*1?1e=6 z`gQhX6cjA+E<3>(m^5{Cc-UB9fBY8}p$Nq4(cG&pJT~{+q+Vsc{rjHk8X6sb&OYbI zdju5h6#%w+-!u33@3ZRfydfngzn1(TkrTI+8TXS8 zHsk#K{Ept<@IFIfQLV5K`FEm77N2Lo+~409MeHL}{HqFRr~Zrwt;US}tM=&V>9HF% zCjq%yi;B7ti1cU~etv!ndE%D-69EAM2IaK(;H>GJGxk2z>803NLp2Njz`y{h65i?$ zYiH;E_0ht~5{a(~34E5&r}AT{)xp(;kSXGwrxuPvJHxrMn%aiiThiy#kc;UI5pPm4 z+7rb=GU?HOi{1C|k`ogXr>CZ7Tf7s?zhq@)0l-@KSi)&dzpZIm57oC4-izL_>1{qe zI5?oK~G#;+^yO*6| zZnxid-R#Vt4|6MieOIDWud_$wunOMYRCaT9y?y&OCs%!OanJAHK}`kJ)YM8ny2D4I zAlR>M;ogxpH8cluqSBOJ)AEf5qmXnAC2vV}b$rviZn)$WKk^g%A?<=?y>MYOsq!hQM8BUojO!bPR@9lAUs}r;C)oKJy4q?~lOdtdU&6#BM^4*%h!rLon0uOXc(CE}Pw?FV(yL_j8+2cE zv@5#S=f+>cT*sb3b0TplC@H;mW<_f8|5^h+kAUWU6VuZAwWPtNr2}(wzzGo}Z+3(F zp9bjJf2)m*ZCp*H8#2v2@5&n+y-v~VH8nNX<&9T*q?t?Cra1CRBO3k3Dr)T}a2C(tqCQUx(PESu)FOF~e0_I1~K~&Tdn*J06 zNMqaE+g+TUk9!eKpzWJItDa9_OW`P~v3Hs*hpf&WEA(m+4X) zlLz92=C67ik9OuPK_7laQSmb-8?<;GfXiFGE)L))jS&$FT3QJ?JH$KTv~fn)`nbmw z9~W0;MFl56KP^UJ)haW1hn=w?c%d&WA_6(zSD3m>KoAigp5u|3k)bS*>36n0U8kU+ zu(`3ZMsbfeZKqB}tU5dD2^4LbeYe{GX&HawRDt(HTRlLL|W<*x?}qLa??1Sq;rFq_N#dDO@XgWOig(#`jh4^59|C-M>7wRh$i>#srmL0jSkJ8{(e?r zVPP>bNM7F3e0#{%`QTMrLPDo#PNV3opxmIA_0TiHSBKty5BGXpPbVQeI2IPSGdNc* z&CL3fxZ2y=0-etmIwDTql1ocVBauigEiHR{`}5vlG}O(}v7)xt{iY&)U;C*9nkXBR zwpsgUem+~3P;(Mvy)KgQ7ZZyRrQZG@74%Fuw6d;_pOuxBpFa^{EVnlCI4xgNTmiF3 z?~;;|O3BFl09=gxkH;-U=Hd%V%saF1B!gxr|HgXD_xkmH3GKja2`)wYIHe@cU;X`& z3uzLbxYMJfam?{@iER3vL?T7J4>@h{%k*ljSNf9;pxRUf9TCyk#XH&~h@I`D z^Pj+itB|0E22U}U*Z*j&!@lUX;Teg?($dl-49aGymj>?c?ov{}QBgX?O?`jztM*=+7W$%GsVv`sAQZUz=20K>h1Xoi3Q5(C8|dK zt9)`UE>&S+_xydinVGG(r)oCg<=WI4Kf5H3nD)-+!SRy%|tiz}tKg3vQ9^rW4rb;Y;A`6nPC-p;YwP+1(9Ud4 zRaNS1tLrd=7i&sIMRg*L|DbCil|MHpXM#EO?~yHujg9^D=Y4GnoRQ&u1-KwDFYob_ zCl@D!{snImzkYp7T$!EyaZcaJ$lk_A!6~}79piWNOT_rytQHU8KryF>QnIlDOCXXwy|mV!K%vkA3j4FeZPJ5=M(e)S@zGHp z>oG=qHY{a*ef{_EvszkwWo7X$E~2BOJv}|gSQ#0+Cnr6h#r0e>#MiImk!{E!ARZCT z5;+h{4TGEBl0A8%tE&1_>%dS;D`9p}=*=7DbU|}V%dYaVKPxK<$;mx-a5!AUWAV&X zLt(}1@*9ix0;V)rmg6M?d`uARMx6bW{i|H{#SxeqQGt=_oTp1nSJspN>yd!NV8wO< z;w9~X#>U3T$jD<|Dv!?2&LbouA}Y$T$(;@8>g4p~edhUMR}8S;@nh)*ll*Z!YLYqh zAtpf5gj-@`W8ZOeiX5Guruu7Z48UG=PB&n$tFB(UE?2LNi`iIPdo6#{URzrL`CeTd zwe6k6W6_HFHJkN3($LT-OMKG-;WF*CovIFl5QIboE2^=otE<=B6HMj0yW*I;v9Pcp z1uLewP=6aTrl+T-M!?0j%i_DED)txe5i2pGwl+4KYikpgrg$x_)kou{`ZGWYTHfc+ zpD%XB6dt0Hshq~QKs90v3?b1u*zVnhpJeId0GgT=LTEKj&9Q-0{{IY05WYHI!!14& ztObjR;(QoCdV}i{zLblemzJ`L#NnG4)Kpd3Xy*j1E-tpu*c@(6p8K4Yx1KTs7&o*p z)}r`lzw|9fMMYVgK1ZqMh#ODC;YTOzF+@^gxm1CHfk7<{nqB>)JQ$Tv)}N+D{iSU5 z(&<4_?fLp!z|iK%KUT|?^}UQPTKZPVMjfQ4hKrZi_hdN{UaH^q;B2tFJBiEmb8s;3 zMNI?+$BH)j{kwM^|C1Q{2Wc9Cu0ca$6<}}#A{$trOnraXTc-t6w)+&M{Nm#L{I#e^ zQs~mk%IDxmU;f-69DV%|xUPead zb5PLM9K?TTwqtwW z(>fLK)_H&F-KX#ZYqhcYt~l@`s9AxYt+h2b7uUR?l+;aW?q2FTzcG|3&i+Y|0fS(C zBi*BW{3;n1#nD09=RU5I^78n&9yN&z4O}QXI=bcM<)fXs!NI}8SG1Ir);2a&BqWky z>CQs=px#|SKR?LHfJ0zlAfugnTVa}tRVKF>^%XBKZ~yBAABeB~nUVjb(A^7;si~>+ zi;KSA-U;f{Kz)Pv4>R69eu|;ck2f|q^VO}cEAQ<;RkaFNv&(r~f_RdP$=3y*ig>*S z5!VzIeOUM%rl=s&c{ELQFu8<+iRKXlt_!tta__+$k^U=3zWZHe^z?lF`csxvU_hqMthi$XI|Bp5vu6SN&D0*QZ%)q6jt(w{BmQ`+cZ;Q& z3ytsRCmL~cab;&+4$IPRBbq>0mlr^vy^bQa+_eu&9BreWTa#4-0|Rz;b|+jYvwyvO zIsJnGMuEXp{=v33i8qax@P+ZxF&WuA03b^xV9&1*dTZN{ZhWB3|ga4IqYKS+a=I7f(xRWEFAm0Be147F3cYK3h(i19($}1?a zaBw($_%Kw=P9`8AFh4*4iSwg@tZaw@JH-D=v`KP_-LNtKnpVE;$0&W9>KYouFEvBC zndtz%U?1siMGM8Rfi1bYFPGVSdE&3q)6*Xug5z9Pp&eaaRRA2Bu1_0MpD6d*YF64@;3qdoeLF3vAqnLKyLs-LCwAP921t zxp((&cTdlJ0l$V&+!KvROn`^LJ38oCSRx=+s*K4&K|$}c9SErvjE#+L0WLW?IkB;^ zNuI~959)ebcAI#%->Im;J2mf0N_(fjeSa4-f$rBnh2r93Ti72pv;EVE)r{>O!L~-g1wi zo)-ovj2n6j+L_Hi>U-872mcQaeS?dKXH(2`_x^LtxL(+MF<2k;r2Sy$gDh)!*N5+7(?1z>&Nc#m%4( zOA89RO+cZy#B^~BOhP3?JHPfBX)B(;gl&TPxjkZLXd0>*x#H#|M9u=)-f_Nd zZ?S8#Wa6oSvy_7%eE=r=ehB^--l9%Vy~@@gAnIsc`<32vW{;} zP&UR+l9HI1C~Nt#FOj3rMby&L66Seq#{C4eE=o@?Q!m<_n``Rlcd4>{n{AzI6`I~; z125ZK>;n5LI?YSO|6-3UylxD+Q;!a)6@q1o`HRxg1($}ub#QQylKTDU=_9oM+O8?M zF=~?&pra#4`!>^O{~p}9xski|y`7-u*j23m?CJ9nQQuw-lU* zc6V1)RDAgG;m=XRzuMt{(I~hgv!a4dq@c2rD_tGzt9b3kyhT4U8sFI|Bj5bF_B23C z2nKR>4fPVgP5nr{xVo`11(9tv1Hzx2oZKQzm+cy?oAc`fC6$zy(~5Y#KB`euQycyr zMsmhnvK^FPOZU$~YSt)Jo}8TA!&yhn%y9GYG}sAJySlrtB-#`t^dI=Nwlj=!GckqH zgg7wyC=Rb9kqJF!ZfSouy-@o5^iC|Z}4oV-Br-EBG}0s;aXIwB&XU}q~uB>mqW zljUhKFLd|J%z5|Mmp;LlekDGr0sfhR%NVms=LzY{4@8ZbW4Z0zx z|7Qr4lFxzk2Z%b;c8bjNo^khPsTXRD-XSABz^0K{It3pepCoTJlhqKf*3%8JrrLBY zG~3X?fYFcfb_|p4vlrRf+0xR|wq!<%E-qDy^x&eR_n)G|YX!P?TlFpQKZt!uO zwdsnc*4Q>)T3XUnS8pTr%+Jr~m4wp-V2Un#}3^4m7^VJprKH0TO2n z29u8Qxw`Z<4q=jC!B7$r5s|lV-&RM6zK4;++}qTrV;yilVOphL%hc1KUmdNA)i*Tg z>go>rHUT{@`HQ|ubfgKm^!NAAAQ0>QHJJ&nt;%*mF-$Vc%ge4b{aSBH z>=BL9gWJzsVKjPENKKW&=SRw8IOgW&etv#~#Gn033kvA*2m9S@U0hIpom#fjwRW51 zrRM`hMMX6>@G>AIWAFscJlf8#Qz32#wM2?OH1G%i{{0(rc~GtxQamxCbGm)5L_v}G z{X1)obWM82SnZNS+aFAmQ55mYxvvo4kWwu!BkA7W-U78#))^yZeqmu1KqO~d`QSPy z5)hbzP27<)0`l3m00R5j<|mIHNpL&_#qlxXsALFFc6M5Hgp&ma2e);`UtydC4XH2~ zY_w3NytLH!Y`ZS_;*hKtdQ~Ej*>EPV+I^IGf;>9^c~&K+l)SLEyi7Nfnw~CV+;$sz zmY$aOR6#-E)O3uTY(voa0QTrJhIxOuKj+{-cnE5_{6W#z241lEytB1s1`7CGT~niW zeXTJvWMs655VM1>({EcRFsqsrq3Y6nTkM&&-w0rd;h>dGNA@GHZ~z4 zq3dWzM=%p#olNv)YaDZn*9hNqUApL>;K?*ihH>}T8N@n`O8-Ay`KqWHVK(q z;qRiGoFrKKej24yK3ng0s2iL-?0Bs?OTc;7wIWuF_x3|%+ka05NF z$ZBe8^wOCXBL#(o)&*{w2m=ayOMh#1s7$AC8|UnA-*^u1EOh4&+L-2ED(ZgQe0Xsi{fYafU{__MY(D&(!;#@gXY9%F2q1 z!@PY6=NvNqKHyxGqA@<74+QD!pF={3+QMpdu3O2k!QN0{bTnbO_>Dpwnx-WL)2Kin z9j%5WZe~tQ=olAzv1Om1C;EEV*`MxQ`L04lr>?tcsHmfa+_$ng%T<-(viD{Xh>NO0 z(OgC4p0s^WS6A2f@3R!?r|TT%zUx+9Ud&w~y1GL4h4a0751~+KJZgRP6So+yhgWg} zyP-7Qva+rWq}F*KAzQqU zMre0WRx@KLc{kREb9;Jv;;$D!HZv2buD-uJ-~RdYXSdDqgEhCU;x&xVp{cYTsG`IL8E$P z{pFRE*x1ZrP2T^v z50%e;rkm{lehfon8NEK*V#>EM0RYP5UqU5rq ztSmu3U+3f&`ti+1DIz8#t8w3+I@pA}2tZm}GXe!Ec*1X53yW0C?uvNsoAvIt33YaK zwEXS2Wk7(-%A&&+5?HfIHVX>qTN#@Z{cIc@_|qct1w#qPhK5wPqio?cUUPOBhi^ea z0XsYUjN%zKHulUd+kceBVcD2eO!t)|n~1P*y@|bj85j&sPM%!oh*-Z+*s;MexW&W5 z!t&xpAaIgUeDEf&VS)dZE*&5jd~*)|sFeX0?Xo=9w>{6VEvsqv*8golylxyOyH-ET zjRwTxOG~$FY~b}K78WbRxw0oO=45u%a^&RYO~9O?YwrsfSsFL%002O1Yb$BTI^>$Z z=14~*-T60B`ySM1N5#fInm6?F@_PF8>B7Q7V^h=7)+DNv%E6%3M>o@g<>{N#H#tOf zwfI>SH{9{_;|^077Z<6K9PT!EV}Pd6Eq z)3P2ILm}QrW$Eb%3|f)flgv)5@4NXTMy#KyjLhfYU};H7Nhc?+3zz->{n?VAWK2O% zg*|pZ5sCf@4+ItsuJuep{Qb?%>6&ovXlrSaQ-f^*_v@7p1Ox;Q4-fmx{`pFXYz&pe zj!?nNd%m$lAvkr2nHgIXlYN*r&}rv`tk{I^M#pyjTQrx!Ho)zzBQUfR6JMUAqP*?0Dps>=&HtjM_1Q8w6gy@Rn;$5 zRW9D%-s0#%yfcjg!VsW20%qHnwf6O&Z&5Y#WVj+qQ9^H|aU&-unl>@7=R!&z@Pc zX6?P^l+kPyl)Y^bKAGkP-i1K9oB z$MPRtaf~|J%F1L*80hHaGDn4cmgoc9P}JSEb#gSS(xU{t zCP65usM7g8l1gY1a;h){g1|h!1CA*W)cMn?77Uvh2*Tc(vuY#xtimS~e0H!SU1z-k$bq>NLJjjsJ4_ z{2*meNGD4BkT5SNy{9E|%IHc}TT=rG1+}oac&U6cDe&XrYJY!!e@V86&(hPCc$>pJ zTP!TA?yNvL{|*TaE?2KNe4FZ+Rpu&Y0@V8UL@STKedNm$KHSpBKy7X9=b_9D=k<{< zAmDEx0Mpgfj=R)CZEfw+Je3`6c-F=J%Uo9q}&!Y@AE;EofEmz1@pzNt!uC$W*nDvN8m4V3CoW)+VjLLOPIAKCzYnYN-=)9aP9CR^$%Y1Br4a~t z@>dE5xc!geXIE8eab(qm;n(S-i#>Pf0of=S8J^5i* z!ecRz-JUmnxjk>xv@16&Ryw*4uht&iW6wqwn{QZe_x!O8@n!5pj zTeqsmHg>Ns&n}lcv}z zuxJA+HPBiF+c<+d%Z|+<|8bw8@VqL*0ev7id5V3_6iZiUXC|!m(0r->3UKIy%t(=a!Si zsAhtYh<3g_%?O~w7_i_tL1No0xV!VV*d%1VFFMoGuqmCdn=w;7oBFGglCag>(9qD- z)KoN;JRz5@|H`%0vrdXgW*MXuNc9^a^}T%j8&7JJ9o4!0C$FLV z&F*{^HC|+QeK76gRI7W%8q~?ipaAxdYv(7MaLM;J^F_zVm|x{?kuoe$S6uwD)QhG` zWOC2_;cUGNJ#mhJ6bl#E6b20;bRm_;iMo7yYs)$_T+%?e6N9t>v_*r6*IDr1eXB20 z$LpD$;szK`{QLa$MBP8>Ws*#*vI{jh#+Y?u&sc+0ipg3nUR4i2`($9WHeU6d8=rsl z^Y+%wbB$4tN28{$j`3S%cWK z02wwqJ}wRu9bMnZh=|X1YGvi+`F1rQ{dV3cz2$br?eX?hz+$qS5CN`cFY`jYqUY}`G6;itpDZF0R8X=zqTX8z{B%+JXFUq%$P{&>1C_X68NLt z&dhuf5D?&Z*z>u&bH3`E_CW9N%+aW4r@XQBaGUz0h#JzpxVShtD9sSBT%XG4R-2fZ z=;sIicQYy~Dk&)`nZv3ubzTm=etVV1^x^Jq465#_DQh(rOszzn-EK#QVYf)jSiwTE z*|YEd(KGIH9utK0ZPJ2iZZC$Ln%a1c&p8O0gPu#k{Hq(an?mKpwiO>#X+=fkhAz-f z0=NH=TYpCMMrb!0O)DuYoBi!qPP99kE-xvG!tlB>E9Il8sAx7@s`bWTo@QYCQ+u}w z9*orTptM?+)3mw?P@_RafoU^ABABKxP!JF>ZW!U%%)WBk)3xLc4GpEGrG|z82%e|q zUNtqPVw%^OfNTuGQAVgStx6@IhCU*b$7er90cd`pa058PE$^cZy=3WXD~W$&csOE+ zC;)|f-ru%EUsfqKEFCc9IzWVktaItZ5 z69pP(W^_6zwMYB=HrCd`KYwOlHuJgNSns-v(#DP?bBzA|3(2K}H!v`Oh|ftJ`tv^J z=El*>>$SD<+EhVJP0iZ6Jh&B9v8bvl+IEWgqbdmQxlHop@_q(D=pZL&8f+`lqB(o# z2|!q!ap}A{kH9m3MTf0l1_zL)i~inMr>TDLyI*i5#x*oFxVH`O>=**mfGK0&zAGz7 zA7pi8pj^=|)!X=$9DIViw?E;5Q5Hd3aP?deu_^*)I^i{_#a2S80@euf)F@Ld7!Gh~kG(%9y>I7~V^I?EQK8v1`EjXEcL`=Qa% zT`SUEv#zcnq;1;ID?6y@vaN@DwG8-quk%PdY( z;-69w7+8XeqQfldzb=-t(%r9Gm_i`%ez!&@#y+W=SmJnVFg0 zZcmQ&?{DMc;#xf(h0Rj9?D8TbQB0hnprF8^K3F?AY|m^lu+23n3a;7u1t$om{w9VY zHD}YJqoIurrQ2jTgMx-;)NW4rVr^?HB`23>rMP?O3l5D4!{&?uAt)#)2#la+fP3Dp z2Te_>%oMK1#K#Znv)tK$g9B8*eEIYdl?)*^2M341z`z_F|DYg%m#{2k*#61sRt`0j zOad#@1m>yrKAkyRTnKzK4igK@NMFCYrpBjrx!#71+MQ`;YAWV+z!wtU-Q7Jn9>K=A z&G8tQm$9?6vk%2NOqiU6#OD!N6j{iRn@9+G3ukOdEDYA_ zTmE$#tVL<;LHOU%}C;JM?_+3b8y%E8}e zwc5(g&c0QG>(GkGX}^ao!8t=qJuxvMEF$98Uhs0UJ^1qS(y5+=$Y7VNuda_RFlhUr8y^uynY~oX}_|PrLn12bPwi2#yTG9gHmxP`??FsAVTFA2$YY1 zE>R$a8bb{oxh-6dsAX_@!L(}b3MO3_oGaVQmh6@b<-a^VXP4EzSV>C4ZmPq2)2@kq zH?`|bVfq0^ke|Tk_Nk;yEOm8RMMdRksi6uQGi#P6<4kDRZ@~!OUeMg!TvHQYZ)yiO z8>e?fBYL*)(68%$s1y|)U0+{+sz|Mj%Wh|>#VuZ=8)96u#dUgN!S(KJeKJqhPdW;F zo$|Y>>A=xQG7N!G8tg$G(cb>S{W4-m@y9cnv2Cb%u>X928bayAG4F2zRzLB~BFrM5 z)R|4l2hxl=A%FLp4ZV1Q#4fYvLizr!53@Cu85tl~J$}$g^^Z#R>~?l`RB|cC=H^F7 zN17@s-6etCZEjrdcV~9~hI=Hpr>knz=t(0j%ZSyE3TyWJj9_JEzmgckQ|lSeJPZ)|QkYT($Z6fjg0a4==;Ap>WRz6Kna` zdm9)UcCvr2;w43|Tp~96j7;+G*Yj(It0R|l%&g83hSR#kCVW|2nH=fq>FIHCjvL(} zdwY9hV+wmI9*dyn?UA$fFR^=14n_PzR5H)`m6b{bDsY4FEod1wP!JGT-d3H*i*@VG z)3dWxkOT^|*w>RAa}De~92XC3_K1oKk&%(s1xndIJ{>h?vwfe?MD6y*G6UeTCX&L# z!;@Z6mj3g(qi_v*X?o<+-4WYR1x5(I!QfN(Fuug2P-?qQQN7`b9b|$;%N-q{qCAQh z{r&wSL5RyWW@;)bQ#vmfSYxl2mX@OZA_QX}h%badyz_hq%HdO2vppX(ZrbK62vV7@ ziSF3F?zgdKX!XPKxjNab2qL4RvWBMv8hd!z!&47MCqopIk>scpe(#mNR_I^$IGoj% zl+Y6sfA5PRx$4ySHD+O9DK0MV8*(=^%qu7`F)-N9m5%S*wRPH+^$#az?2{LGM#sVF zSErBx=68}m_`+bltw7#KW(G88c4Yc~;bKlGpO=(WcdIYb#l^*5&T}-4uPX@Y_4n%? zQsI_bnfCF~kxXo}pI_JhWWLw)T)wwl0vMQjfN&=Ae9}-@SeUl9HedM`-k+tlweQ1E z0l&;-sBDg8hUI=W^K&+{v9V3f%yb$gCnVIXwURD-6^_Pl(nBZX;^6pLrZek7U01Fn zWcdtYo*?b)0L`fL^YiaMP2B*83lYC>`MDru3}uVU*uvttf}I5tRPzoS#V;7?Z4s%U zwhPiUmyQ}xjC0;~qPxncu4DH+jz5obG{0m;YdwO1;E=P-1@4Wk<3r54}XHjlXmZJx^mx}!Ad(PxKm~D=l z**Rgor@2riD|FpHej->L`S*f!DS5ODlOc;Zgs_WCgK*}bq9Q%`oZeVEHD6!fuXMgJME@4j;&@6hDBM~Z6n}BZ zs6spaM=_iKehZD83JKS3t1mJzFi<&9hRbf}!+Gg!O2=ZAk^JdOO9NMn&{olX-gO$d zedm;sAe*lkhMD=_^UdDh_*|Xp4(<1syWn78BdI*`si|73s@FToR8~*%(;ZKSbo3bL(E+?5G!bKq}C@6TmJvAzs0B3N#3K0>7J#dE*iWpftcuHM- z^}N`Uv`lGE^Oj3^kkQm!Dp1b8h%?&eA8Xw24pCqW2dR`v^TG3Ziey zP`lUDd2ck8d|+J=5)mPXCH}Y17mHSH4t92a2m%5EOfTDvUZ4R;VElA+KShVVU((sl zFjdwFYkx9j^1+b8EG#YxZFEw!V@kl*->A}Jn0-c8^agIv9M+6YhIo(sxd*Q6MI*C4 z=so{}bYAB(LCR=$Hnws|(yXj3u!WW9$J=>dOA0C~Dv1i(Wv_z=B=Osm<-xHr>vnI@ zvpTQWmvoPt`8?U=KmC=^#4qZb-u~g$~ml@-SHR9ks zpmw~=I$vL3Z)j*J#t;$(eX~$LGCDds7FPaiw8Nf%K@d45Li(2cHJ9_QmBTI2f3^y80X#gwR#;($Z3U zdwXr|r&_^ga&mHVDyph45GW{b-(QW2iRt+ABj+ z7H~LF(bU8@i-jV-5IXQ%GlEBk7`I~3YRpk85G8V*oSO0vF-N9(^DzIQ%1V|@#yT<@ zDw&pz^UdC|km8aO19-cL{N zr4@yRrq>74W5kP#i?d}qryCn!J+-y9k!0dxqN3HcwU&#ufF{{N_~#M>7Mw&2J13`f zeh=oKRis-Mm{rmxBH1kzP--?(bebMr?&j%UXM8(CczDP#J#bM&&4m5RVD;1C{ z-$EneCkS})*Zjt12Zp6v>FMa~Y;5M6U07W-6cvA1S?PTHc0Vpo#LWG=-W6nMXvkr? zw1$fU;ldy8(60X3M=F<=mR7d}hE(irwNHo&D8#}5kqqnysVkLC3s@2Y8=>aDHwaw1 zQ%G-A6solJV3Ar`r=zv?*~Z_WCXvz6Xy3*g-+GJW3*>^uyV*DoC*1fmk{rAt7Mr;oc(4DhdI?^B`ke`4ja>LW%`gvYz75`@#W>{{Y`|Wto@LcMH(yhR~<#P`E6_O!{+AZ z(!E|1Yv70~xPKgABqk;XArk18MD3n$_7bk1z+BlcAFrG87*;nm@hJc3^oM0zeL0%1 znkKj>lg#nZ(9jSRY>yxjStHJP9rQBj@P!;pGwEqy*f7`cQ8@NY#0#l^qqBY3&ExP(Gg_g|D} z-^(USN=o~N$6vobIEX(ShM-a5;^Fb|@I*g-rIJg*=5YkX#ARh=jfN6H#lMI5UoSFJ zQ!}x!xSp+boSG8DOc2Dx#nHwNJUqD83L3}6$9K`=T27gR`UbzJbFbq6xmSac6VdYc z*A5S*$?5uLLBXN^MTz3lQtejv`?U_g4#b7nKtH)uZj5TWg0>%BP!7GCb;-%ev9Ym< ziSs(F=9$fSnySsrYtv1HjFjk|Hv0wJ2WZc}` zEG#TMJQ{GMI?K#XC8|Bka4-qy+Kaidai3OBHXR{H zvzWz+I!sp3o2R-&?P*X(J7jfq`*Wr+T?gIVI0^`~`TByfva&|;g)4peva_*K>2hi4 zAP(ZS+UgJ}wy9btv7>QcP~b^xY$Qw*y3}w9&Z*3g?dl z3@ips?~`n)+>KIjov&YwO-&d6SCV2&OG_swCkvfhE;mlk%)~VzYD!9mF({VnmO@y(hcK?NuB+3f}IUI&uch@4G#~`iOy`Ar|8V@Ou<(cX~GM!LfK&^ z8bJuc5El`F9L1yj)qJVmW}(_dI-VhyJAJkx?@$bGRtsL}3K<9}5(F%()HG_2nLIxZ zkj8EyBONz;5Zv6{hBLDQ1EIXUy!K#NY%(n@ES?{($r`q&30$W;I!JhVQ#8c!t7~h0 zBT5Skb>SPqS3=lV6dbk5*4Ee8kB;0AXG&HP2ja!n@7JcGlEm($lEQ-O0L$g{5-XmJ zAx+ttdUSMDe|f3-;ll?&ZK0;Ry87yPG2TA@@ZbQD?5kjS1@v548)M!rhP$}1 zAodH}W_N@jPk7e^o!f^EHYw0s4bK>t9S_MWdX1e_0x3E+7Sf4#*ex~B>_ZtELK#Z% z@83a1?Nd{LmOKUqMt4F?3?N&ouwT~p@&LS@@chR}ujdzz*=(umt0%LZ@6O-3&tzr) z-Tz1+&a))S`2MtlpM_N>@?I&7SK2wQm8rP4YdGTBE4PK5%ax9oC}yYb_xAQC5eXFZ z*c0tODADL}E&v2QW{2 zZHsnncsRIUzkb!fyWBnl%tW*|m>M;@C@)%dLyrRYZE;9&u=DM*QwRzJUw!HqRn-nZ z*iXU!frIXI|pJ| zd5=+~x|wty`u88I_nWDho!9wyadUCu;NkfV7;t-?&2)GBC#s>~Yo?wa*Z!h*@h4$n zK~)Dcn+8pK+r(eYl$3@STm9AOwTa&b+-^^Vs0gR}0^qUMh`2l+*s5>d(3mlAZxAc1 zQ=ry$cD}p2^Y`~ZT4`}xY?ObM-?e(ePlh1Xy|}zwM>V#vps;e$(yO;W`DVA)?sL21 z_IrVzp1z;$WgwPry&@_m27=`=vD0hKBsapXRO7n8QXcE|bSLb6%;xNyb%AZw#985O zKCa7hji(x(&=u6%Qse)d(0-7mlF^=((k3fwqqy=E3&+PnK>=-Pw^*!YUT(U--2IKm zA-Bh!{%}lJ=W%|_u~?Cw4||orZR=uZ$Eq<#5Yf+#3oD=Tws=jlM1KL#Cr;|n=4M`n z3xVgW$+PvYw)-7Cg(G4p!4|*3)dsuWqO#UPr2@f4zfR~>BvT?bFGWR^YIn*oitEMI z)|W{u$M4aCqH_BBmPE- zpTZ9LGr|G`q3$&6dC}0&+SJc02GfVuYdoj(^~Xw@o^D*0Ymh|+gi?PK2aB4RnkrGF zv$M0C`*e@lcWzzazr@i$lUk3&Ts-Oe}1OS@jO|6mEBuZ#6^DS<-hK7cff1`Hw zp^IyHGNK)bUT+`qIEwx1mO^SmL*e#ftmevncwmtb9G2>Byk0KT8=}uvy&JwLOYCYOuZY)n24hL9@qWZKLvRLaFo2AD+PEBsx~Z(qN*tA6;MqnIt`6B<~wI6FJLvI6wv0I>EANcpHaTTxLFghYghfN*_#yV_t! zr8&!z_@UIyxgPMRutOc3)ZfIY5J1j_x*f0M84@0DSQ6!L^Sem z8D(F-eJkK9BQ-u*Zpv4nHXZ`YI>EjgU+xSWGYe|vxmsIwWctL)e&0p(^4-{&({LyO zgYJuDtgM#qZa;oxR#mZL3nfPQuQZ#W zr(u|*2BX*8Zo^TZ4Gj(XK_jVu`=hB_@7B)NyG;3C zB{RWaYou}~B7Zj``@C>hQ_n2ODx@Nnm3?tfiHU)*K%Mf{zTT;jB+h~^<-E08sx5h( zve?`$YcZsWADkxOf0~(@2@4B5b58wau@H!WCm7~O32lJ*fKLdz*%L;Vd6BUq)Ya8< zvfLyhEG)PP4FwfW$cOtY<6ci!cTET!@A&vQF)=YOFK_g!c?aQG2)V!qKwxNWZ;rlY zaiM_C!bWH8tM{8tUDf%Y;IgJ88S#*k>W*i9?k43=Eju~WqNRyxW5iQs)D06ay$IadOrqJ2s=HtUdI6fEZ(L31K=E3AktOXAER#bLwuEFj|>YnPL zU@B8pRaH@8A@^L90{)~;7|cXwxHWi2c$1O`IY z()toJzG2D|h!K_Vbqh54q|=GzKv|oD`Lpg&!Z<)jnCR&d8#_o#OVcD1%z7x(YTDV_ zf;jkzO(l~cczHaU#)tQj_uM)7)g*kCN{PLGHSO!>C94o#$SiqNaA#*MMg%Z z?~2E!Wa%ApEC|4!R)3=Z|Jnwk#}Lowi&j=vYP7L>J-;f-%fGg|b1@ql7;tfMb;JYj zN~Q8RO%aU4DR4WVeZP}m7ihHI4*jI3t4o@K+(S)MSXj6gbD-ic{`oV&%>Uv+K~Ims zfM9H54(ba-^ubRY*#ipLA*CM&Qw8AAh!FSO-`l+r@IG0GiPkHTfwB>*fzZcbOrChdHQVSI^`-VP$|+`IiG#Ex33mWVY0Ke z1@vAF2IELX0y#N3$tfv)&)a>#hjc-g6fP?(S+KC;hm$#!;maz@%g2U?j|E;Y1rn38 zsOahGDJcVM9Eq$84qWkt#08_zZf-2i&Gq4tA%u`cIo>RRT3uUvbG$fP?AKtoi;0e| zKnCLs4-an&K}$<}bbL%lM`uFLRYtj0LZfMFYKji`Sy~!i2CA) zR3{!_cPk?!qjzRTqk=G1B)S#}2}wtLJ24v@dbr(2H|*Wm!otGZ+S=crSd-jxa&r1N z*!H;Yu4URSJ41gjFHlxSF3SV$TcPfSeg>=^dkbwCs0^Sgf??#J#ef^{*2!&?6QJwqTvqma)`b*=NI+XGaeQ>EvtrSQl_q{rk`9%$ zq@={h$7gTD})mM zOC{to0*5IdEp1_GspY{`!Ta~_-Y+|?QY;P+4?jNw^2-P0Q5yvj_~Xz>gw>UmSv|05 zl==n+TK~aiR+Ex&)q}o&|89M}P?L{aR8*wnajkm7PooST~qd4+e?&@lbSW@ji-OiZlX3cq{N8x&kq zOY8FDqPV2Q#2^dS#mp9whFnQWX=8Kqo0(HK&Oqsj^;AB0I!Dw-yASwj%PnDvm5B+g zz@J))(EJPX<&3x1?AmowF2x>$Lr@#kQ!BYUuBT>_mDkPD{Pza?{fRIcuQtZqhclY=Er0Bw{-a0djiyBTvy^-5YgM)+fV3{^APcFvTO2()#zQlclZ|IIL zR8IU`VPt+eKvAm0U}kI#)W`KUWD@q&^b8CmeSIL3J?#H?^ZsPD4GFAgd>kbvFew|> z#f(Kq5ZU`cff_vtZFU*PikinKadh@q4#>8-t}a;T%X(K(T3VXN!xiwvLahXxg`l)I z90di1q@<)y$=%$x{Lb0KQ`t}84KG7OD6jo)H%rTt?hv%Fu&~tB)R0MNd|-6`4CRb* zD1q6pm~-y4o{CD0wa7q|oQ#Z&x;n1eG5?x4EE1AYNv9~OESL~7giFKQp&c4yU+3c| zFnL}eCL_zv6bWMZR_VWG+vNHD5JRIZ7;6)zS-*0Zp(1q0oD@N7-1hv=51A(&TcuPP)fJDUq;eqHSellOax3^bZTs$Z+ z5DEgq+0D&rwRO3xtE;z{ikw{k_GH;i^Hs|JrBO8<*`t()P{8v;y4&@EYOSDX1lt|A zEM=KeL4qLOI%xo<4>62yvpoQ+k{bRQ35u^2%GdB^FU8{ zzGmMYRnq2iXE-V<>TtGfcY8aP%Z`$XX>4NRqTL7F($W&RKdupR31gndGDYev7NHgh z(fRk>6&^G0g>V4zHk--}phxR;G`9jy2eZ=Zp;4eRGd*1asVg)qM5I=#nJL7vA$av} z(EWFVDztfQ*D|&~=TV8Ku2H{@J{p!RgYwoYc`u@ zV1aD|xK|o=of-;4lXRUI%z8`9%R)C;SXexqoW^3aPLM1Q z)+b9`cNtGl?zLZ`bMo?{kilyO@2YESJAbRHs{X~aCnCORFk=n{N8x}4LNpQ3&rNlZ*Muh;^OEX!uwpGwp{7C!921b;mX62QjA^;OWR(1lF&haN^!0!C_y zm;1?iC7xl?C=(JAzN)L+lF=9rCmC2;@_RlxpA1-}iYwVea<&R*?)enM5$~=EK$`So z!lXAh^QMZ8U}0bssL;;Px{EH613}jVz$CXQqxOILzoh=!NI}B#UE!DWWCek2V0fRpO+Rb662Nf)GpG5 z^Hd#@=+bPj4^)YWh@N?oM#Uv1L*%Fy2mjYbs~~QD^SC|O_w0z7nYlNdK(8b!CofMd zQ(@1Z<}@QCqrRS#5-Gja=Jn;7(C$h?LLxXcRJX*Jf`vuNX;btdoMLZca`IbIJ@U6` zL1ez1mKLo7m2e!^p3Y9)`-|=G1*N5dVI6+Z$;rt868wRg&eV?k+S0#GZYh z9|#dBkv%Z1BlRkv(n;b?S6v#D@#;H7EI_04G9Se`Etp?U|?|2fCXns z|NG+dvd6>Kes!>diV6hS%Xqf9g@px~c=)g2U}Fo5*@3&UsL?V}4Xr z)Trp_Z$;QBvVf5w{2zi*$o6=fDc|wfUpB`JHD)u#icT^+zj0Utu@4Uq5fBhEFVCj* zNdN782gs-j8XB6y?=1@@rHSqkG-w3etB1p~N&Ca*7l-rnb4N$Vyu7^2onf+*c@q

~H#KU5{xUf*j6L`%2e0Z|b0uLuZMh4Ed z+vsp8co4me@Zm#--te*(cXf3&HF{`L5^inABMdw|S_&OYuzSP<@!dQa0WuDb%SJcs z3@v_TW#yXA_mX9CTU%RUVK6fcc3~@2n5+|twu7A=Ljway?)y}y6&eNxO2-vrNA1n2 z0%d4KdJVmv^#xLzl* z8X;bx)i?c7p)EZ2^Ya7VANiS*lG3*va+?5?rS=vXvAr!d|D~oQWu>Jjt8D@y%K`b< z}wzT~kv5 z={ovfZiAqnUUpqwot#?EBqW;)hNh-f%QdE?c5aE$=%Hrze8}vO2PA=OgQlFYQLdk0 zGK8lS9<gww9e`vn9%m5MUdu9|-`L`$w^5@SV?_J>SFmWOe(dQer z|B}!Y5ZBk&P3d18zPq2c-2NG#n22%)Z9gFv03N+-d%juJcM1VX#TF0{NHs+AwUd*p zTqBkozqq`d2P05WQp!@HJv=;|2jdGQFC&%PVRON-8cmyR5G%<=lP75?lS}2M7)HMm zHb;fY0-6>Pkxsydl6}|J&B7@M=kRZA6@=6UrM9cn;)+37-4%qC{>cKj52*(8wGofQvi-Kdzu%|SFF)TX zo!>*kma~$_y{cGKJ5rwo$KsX?z^zM8$N3xuNObRTHtg<`6c?W}5tFgY-swSv7AY z^=f03`8;l-a9-0X>OJcAc*O#hF=_Aok7Zs2=7MeWBO{1u-JYJFV{RZM;!akUmfDs2 zu;$XcBnYJG`@9Z}L5KukkbUZqpVifIVVm{g8}aUfBO;LRID1Mhyk#c$wT9yjl}aei zREIeXZ;Mq_RXZZBR$EIFo27>L0o&NGgitkdSe%|!rPvLc8S<+r(Y>-()>kTJ>CxTP!{)NMu!7At&lpGZqISz0MdxwUH1B!;` z=CB$5WMz@+e*E~+zi#P2_rw28rfeoUZnvKLe2Bkcf7^p+)ovGmC7P+h^t~NKO-&6s z`b?-gFFTv;>X*y&p)mnSOwZnU_B4Td%n9PM?q0YA`99W_r+pF>(N)(-Dv#TD64z9) zU1-vZxBgQA(gXFumj4Ikr$r`EsjO2K9ltQ1eaHZhKA<;-itRp zVwPhyO~4BG{BXUwg0SNGXfX%>F{1|n>>jT6H+z5g_Vjcfb2BlG_4f7-4+BK?fG}Vr zoS69gVxxS>$ZEy8UEvmrC^rxm{R5o(Qp~ne3zW@5vZ&4&v?jTx_AMs`;w&Fs;U!@9 z+8Dl;gAlHdAq659#l~XngX$X^3S(jNs}1k!6N z?j2OqwfB#kU;`ROf0?U%eA>l@n+&l!?eoBx0j0%r|x8Q9)79F0=Lf*P3-n;-|E8$pm zk3M`D!5bdIUO%z9*pjR#(X6)yl@jWDy-a^~zn)f8rJXL-YH~bTYVde;G#(m^XM|Qo zI>y=JoAPOb!{60;eZab6HZ?PA=+`m9(tUi8Z=0`3ZQLdcwtu4^!Iynl!>t*fIu{#v zsCHx_lX=X8_uMn>HTeMh4Kgya?Ck75B_j9ct&ULDcS@V1lT-fZjF!{oCJxIb&U!K*O6b4* z#$&{LwD{ciF!nExr)`YU{TY(U2?=+1cY*~$LE@U4_)FQqt4S@_Ga6_3L7(LD_tURe zKA)(6`}WOR55<)Y$Cnox$V`8%d&Jm=HM2T2D_)^1oSwlD@w?vvvKYx6R-Ijhj$9rO z_U7gvc~3<|M4q3Y`@Q*iFES~QTHS6>((D`ECS=dgNf>%{DgK*7W`O*Ujw-CCHbFBy zJWLxKsvk^#h8SV;W~3?1JE^snb{^EB{(FqWu*AlGU2D2+Ev*})T0m>8}yJnQVd7?M=>Ig*UHj}=rF zUevSkb?TZZ{A9T)J~MOO8x(xa>h@_WyKdDJ zZ-ExUrtQ6aU5jJ4-y5Tlj$wT(17WD%BtIz7hvAZJdR&jvcbY#lM zH3;rZA*h%2iZT;zzD%dh<#NYo0;EAMdq>;jTGh$vw!EAv;+8ml4F?J7aWI|{3j;${ zRdsrL`uWBMKc7e8@nrSNe{%VFu`X7M;*wpoD_i`xJ`2v10emCu#>b3(laI!Nug)$m z*D@fi{gYCc+X;Q!Hm2IO91KlN>qL@@(qF4`_Zy5Ps7{tJ61f{ z$;pUpEG!$8Wee}Lkw)b1w48a+_Gg#H&)r zl1#&x;2>Nm|BM99YTt+ERJYJY)l}_@*KS&fk-GB65%;b+{R{MWxYLFPL7TJ4807qRFew(Ym&z&FHs*%r}sCJ*TEDG?E;l-QnlOd z$s|FyBQho?)G%QwOML_%FK?~YDx%j9ypP|5CQjR4J;M%lgm&LZ?hfmk;8Ch$gUkmo z3*?Py;W1UWXl!^$7F3$f8tA_EkyM)(7pFJQ?vgyH<_LVml$1Mj6?(-OLf!BO?hujO zOF!=T(n^Uz%q=Y98Fd(<`!kx_>~`e<%E95{;$l-n!+14^jg8G_Pne(QZneoITfX<6 zeROTESz0Wi#zYFax>4mzD9B!S7py*5PE+K zPp{2DhhJ{IHm+92#`SKuCj+r`tDcV*$j8>qK*Kwr!Ryr%4=>s4X_JU}aQy&}+kOv_ zj*2)WKg00!pj}uWpqOQeNKS?q_uh;k{(48?;?>>T_Hf(<|H=8VnOIR#5n$%1UbGk7 z+|AG{)OOAupYkl#I#Li6)CP30PewYTiun+a4y5t9p`xNDFiW+7Ei|O3uWk><19VcF zKbI(?2TUi9At51SX8tGqeXz9$(z(5HY-L47WP^THkiwMjatbw$L^d`y`EsecCAyga z=%<9_9Mk?6MmVXyH#srs>U4E=Y|Q#-t|FsDaJEFN>855@`*605Xzzw&#TlHtm>eST zuE(!$wbg?M<2d61xL{XmQkypF%3y6u#}%nwR$AO_8#KK%-LIjIQH9!8($~e7W9@Sm z#MIa)BTeD61tW~M52lNJe0+*kU#aQoj}y7TA;zKKe0<-!?sBeV^~CWYO=|+b@gs53 z)r|@-o%Mkr8jEy+6B!aAZ$!WT!zZSxmaAXluo9KmMf?;e=Q<^Ki@=G@AE|~XFiw%m zMYYch3F*Js9xUKvW=8oqqPlPmcQfUk&C5lVXCUCMQUXX^z;El z2G0-I3=9khh+g<2A|mz49si+E_#&eWaDF5;@MlSspKtSOlZ#IiLgoYJbtDLWCMj04(hX%N}$p4Z7OCd-#U9_my}p^2ctwrM#93vYPzYY zp!YdYwtCM>04()y5IK#Fjb5)W-{oUbIHqr0jUN}hAW7vI?LsY%gf2-k5}i2UyCI6C4ZyRL_|k#Zf&u- z-5d$|L2sRO24d-Uhm+%DVjyC>y1Kk{6u-lm;bTS;`z8-j zp<`mYy1CsB3Dd${R04k*B^JD2=4vZ>d30pH*8x)zt)->a;(mY8;RlU}gCm>9dp{6M zSN(@hXr#5Zb!mG{=ReSl%1pZtc!*6`AOaE*e@wsr_4W1DT2_89uT+kpu&{7CzlWxl z*5cx#d~Os+NIv-!g8jwmA>4`T698CfQO4KvF3n}uOWx)3Os|OUHzIcf+M4SU6ckrlz_)kCY?WZt_gc< zDEH5wzRu2#@oe#O3p2Apf*(G9Om^i;8NIwxqVo3k)v>V{u^Ygv?+hmWbo=aK)6>&{ z&eIBE_$Sk;KPf3GwY8sOr651^*=3JG3gZWTCJzv#x`Zt$DLLDgWI%#Ip#UO5V?_T~ zAF(gDb5T3QsLd@c@bK{SU?j+IEwiY^L=!zdk&jv*Y}16XlQ5)-*ItqGoE>! z&(`V+Co-t`q`KiGML^Wz5)*OQI3LczUEwrbr!Jd~ynLL^YHw#}R&!5@YsLl|t_T@_@Bn&AWaJcP#2+5;gqy(?#qIM1t%f9C- z(Quw#iT@^yr15y~Y}x&E#)x^i6&it1xX$RdT&mAJNqF8{tUC=xAp^vSMpB!sR$J@q zIV}zL$D#=-Ho!$&-zib8*HyD0=`+(yPR6QdW@eU@2NtCo#QyNw?Enw0S_b_3^?{DA zC@YI}Ze(bPjh!9Cm`ZzWvpg6b`AVOE`|17?J2W5|W9lA;R52+jDZ|YJcxXb9^J*w+ z$3G?}=K1lq>dP}FFRy#MH|RT{GP2y{)bP0(E)>?5@{da4{pIdt9UyZ!Je*PX$!g_; zsm|E(F68)*qA;dE5v$>c6rx6A-qX;Ncbtn@6nN*>_Hn!z?wvQjR1Bv}N=4P{)>|1- zcC>!o_Il?PO(p+{mDR6pNd30CB*yGtXSX4QTyU$1r&9VCJ{Rf=H#c{u%~^Osgo!M6iOs08i_!e7%Z^iD~tCjQXZ*k~N&&-PHx|IiOKz zsR(}>dg64t0vUkRh%R^WhJY1$4HpWE$uBK0n|y(c?3lh^u^e2Z*ATe~{*I4{!m*Js zM8)g*G}qfJj<1Ia(+Tu&BqC>KzA!Q}W*TLExt)sIfvC(=H!-2nn;kOmEjpaSl{}i# z8gp2}ch>S3CasLv{ zWpKRIsC}lVt(_JV1BW6jE$#N_>BfLfUMIB6H68|o$)<2y41H&@?P|p6vMk&6{*Mid z5}3i9dfix3BFk0mmqhjB)A)8aG?eH4dL!-#-&-2h9`0lXX*szgk{c1kJ&wN-e=1Zq zJa0H2pdX2%5G2CK@7Clo?dNf#i0diK%37;WuiXVAE`aYUY-+lP4a9kDF;6}f+wt6}#9eMRlph}-^%I3M%Cauj2}{)T z!uxls&BiE&J(FrbzATfJ{#OaMz|{chE`}XH+?>=`SNl7|e@F`qL~>aQ;4Su3zuPOw zq@ktNC=S4#=X*=T3E@vx_*q=6#FS8PvucU~f+PW-+5jAlp69nJ-~b36H2rLB=hXI2 zM-tAX-TSFQ$1Ud|2KYnW3pH=}_+G|8ULP%P*}I8|bl`k= z1d)-YrlqCD#Mtdk((_4KS+QK1WO+ZF`bAntc?RJuG&(Sj!nj~NJ0}aZ2TP3(C^(b> zdXWnQey0jL6GHzSFxnWdTn-4Wc*tw9%$qT~_X-T*OM zz*T|b87&Es05xN33KSf`T_$E`aoBKbJC;*UM@L6l12)S|NcSxYnZkYFC=h;8Qc_|s zGl0gjV0vCxGZcc(zDS1|85vX$80hF(!iO_gSzx?7*>?3C_&N~O^Q>0Eo6 z)>Y`X-|2#?BZ?P>4|qV3pFc1#u=5=^CSHDLstUL#J~5G*0A1EEk7$qYEe$E;eX;^n zi`TRh(gQSM#SmptJ2*IK6swQd<+{~hK;{~)v<~ih$DPm{8C&QD+Vk>|xZPxBhu1p& z*KO?WIe!w1iHU){Q{P$LA6AdGw6@+H&TnpPaN2G3Ocf}InQatgdNVOI`?tQ->4SAl zp-8#2-`0JF!hkI3_qB6I2UoA>Iq?*Xu8Xo=VawUK6%nsK2-DF8sLQAEaX26Th9aPx zo2q~Sn)2VCIy|wS&595{(2cP$v?SRbDCWEX2OtBXgSNJ|vZngq=DeN&V(*@Dvef>A zu5AVoq?Lrj*P<>IFjSHRJ3aSw6qHNb_^6b^X8FHN@o zM}#5bq5=X94@3d)GY=orr87zHX2+`CP+_wwEYXUu%rx_OcR@m{gzp|aJ3IR?N3ZGT z=Ef)ijg5^Jtv;Y;L)C7{avg|8oCFJOR)m0J){KR-b8{hu%CkTlH7X4v@bjnhrKG#0 z)hPZsFgAvRikgKnL^~$u;UO4um&$ANW#994Ree2Ro}K&o+&YSa_2IwblJA?pznGB` zZTZ)l-A=p}{|9GouDUG<_W<`i^^r5jZ9HlKaHFb-^Yu8CEa=z%d0@ViH$X`x2 za=2`b^FH(c?PCbv8cLiR>_``MSwnreKH4P3g=zc^iBU*Mh>0nz_M>lauLK(#ka4R* zk|KT)bCMVD*36>x^+}uc0ej`k{$r!V&i(zpJqx1s$p2R^mX?&jc%JLpvwSJpk!_s? zudc4f5m;GROfVq9#(f63*;HEtp75cF(=8P93kzSI*_#@6G5_1kz|zEK^89$W*&8XA z9@RnxynYza{4h)C{&?YlFs6Td6L^6Cj5jbaaJEwU7RK!1+KJp^;^++q0|aV+6dXRm zr&qFadU~rdw2~>QsbX`%y_FZFNI*gSW1eBDl#4BJR@CRg5 zPvFprW7{Hr`q-?90FmzC;Gkit6fc~Rsa^1jlqWgSgpVy}rv2gX%A};1_e88KZC($4 zs`V8WNrEocU@%+(tKjd_#x~6NpMgZu|FyuyrVtVmUSD4)BqRV0;{Od+GVe3)iTS|f zL^SQl_`t+op9d_83S>Nkxz`y!uP+9=-NJ-H>()sKR*E*oJwYBB$if4?%JgKHmV&dkzGeO!r)ww3-@jPTSGnLxY3b+S>1IR!Y26$SAXvnTX%2 zchuUt?Br-DjVr6F8Wyn!ZEu_MF)koSMO(wvQA$*9*ZAJ%jI9|9vn3}dd)yq43=R&C zj^&v!IQf-HY_pwKe^{w-(RJSqek| zG69!je0mzTx#{q9?^GwU_TK=rj@qA0o9!2g^~#@5RG8bpnw$HQP4`BUfpZ*+g`6fx zc6EH$ZqCm?yWF1oZwG&ql#`RYD}SqA@#5HnFFvB>?|-@`AUJqud%H>%P3(Jb;m@CI zr0dj2f2+*he8^nZuK%v*DKRxE&6dS);MX!^M`O1Y#PdEU#K%{tLU;*?iN%6nvhvo6 zQ~-h1(=jlRl8`)YzWS3zG=f<>79a7BnR#enVBq&}*WL({7LskGo;b`}YX#0}cTa)I z`&zJVP7pRllkosvzU4%MxEVSI#`<(;W6FEC^Z9Bs3lsSJBU&~@hW_FXj~{HM>U@K%twrKzb2s0_JeyTUI&74jfqX-oq0^DFey_nE}K z=9(R7-<-O4;KL5zWi-BX8Q-U-rk?(poLzS?V7|DpAhIC=frtbfFV7O(D_vzNgpXD0L6;Wcke=v*+oqrb}qB1gFBO}_cuI}#k zQ2^S2jMw_7pgX&x%SsB(=Kf4^TU(oFB?%xa zM30x7854&-MOcUm9nZ4XP=4hY@9*!Qo_6ewATgGDzP~z9%9kbKv!$BDvKh+|YW28b zcn%AQh={;vl#iegB>H0SvQs+X35N?JCMG5$8`8{!sHh}-E`-F)Uc`U@-j$3}*g=Gf zMIJ1EncUGK;!5y2ebp^!p@89sknLK>=$kW@`w>Uo!l?AZyk<3$=ITu$~Tj+BDTz?!RG`nv(d}?Xw@Z->;^sSrb9#gt{EFkg1^yiTw=l^(eX?Bh17#aVls+ebS zVbf3na^a^h%IU9gViQi1xnJ^JRoIjRr>>*@{k{cP|L*k7Ac&f>vax}|+3o~wFDupf zW|@|+aOQtV8FVi$9g`0*_N3R|A<;k@Vva&XLxX~XZ0{5?PNIvq*WTJJaKV#L_4EW3 z4}xHg)E3ty=?W9tub&GC%%doU28VYnx6|-JyAl4VsK6^?%BoKshchxZMhm$EhJ1eI z<>kFOS#2*W+Ajn<0@i|RQ>-}}2+$ZtK@mym!Y1|Q%RbJB#|}+;5lu}^Sy@a=arfwd zYPJWY7kv}QAXimW6A>OxOSD3*#-3cGY9N58^}8+9;#x8~;ueMiS|>;(rP0ec;XE(F^>a(jE5JLDU06?V+5S)W@aOd z=nN!MU32r-mNOnXIXNQhf_!|6NWTPOMcZU)TU~M<$bE<=d2$KVQfmKKvK-&Bv~1PA zUite6aqbF0n^v*CKAe{=^p%p50y&Pu(eV6zt9;IffT0>6Qb3}C5@oh4xebGfsNqo# z{4=wP?tmSc62b)0sT9{9(}cZJYH7v*Zzv%;Qnh(S!<*Ny*UESTTi*7w$E}@f`^m-* zZ2<+2Lca|P`z?yJywiLkK+>S5BwIGHgz<_Uanb)685v3Ev&*ThtgNo)n%mJ+7x6*! zuLBtX{<9v0OYGK`mcgN+8Befd1mMVXc6Oefodp~hsR4{Gj9|y{;<|szl!cH6M$NMO z2!G!jh(YXpv_Mbs2X*{=#IC3=Q4dsg%11p@T0tQyENp}6^}lyo(lk|L1{}HTE&{7q zT+`=qX=%fV;xr){fG4KsMQBNQXeb&61})JFm4L31uI>upT1ZEp=&T+Y9sPF66F5|; zRB%eYd<2n{1c>3P96-14(GkmKv52g(i3t)ClEpJkiuB<$g@3}wJUnYVA|j%pM7EvT zc84A8kRz{$>zMkMW=48?ximhz@hmXV>Um*;`(v{rf?WH+5#JIC8;vP+ILWZCVD;3q+i@*vV6e1tp z2T_YVB_t%4s03E}1@vc2G`6?5H~XS?ddV)3CkP1$IyeA?WieXJ@bGYVFiy2`_Z`m91T{BHYAjowU}I4Io{mtUL2Ba3e5Dr-y6>sD^Y>`r+Hcxg3~`XhO#U( zU8OmMPHU@{#%(eAokdH8Qjm{tTq-Us4Bf-S1N071s1tWB%?QBoTV5O8&!6#Le`#WO z-SNdPFDnyvJBR7sq1*dlA2)p9O3`-spXiXBgeN17O-+$eQ0f8y+-G`T-uiGgA-`Qh z;I?o&ewQPnpcZArQS~iYCXsbwKmbB45@ma`(%Mt0;puy=#FSv%$pyg)jfi;ME6Ci{ z8liLkZ{uqPmLJKqy_FTq^y8X8PDm=w%w0>f|JnXb@qaIvr0KIBx2NmN%`P@8Ex>f_ zl$4aDBv)8JF5kDPsHo=V=HTGq%*@P>c?+9+gEM!=dWaKdBD282bJaq4wMyGWG?83P zVCN#d)R^OkLZM6Qst{sjH8l*@KW@Zugu!wA_rpIVqvAhq9WBZ)Zf9!YF~2q2dM$qWdR z#H5Qtcz9S=PA*&X&-2r&&mS+v!}ay`)9{m}#*2gU_UC$^2VOR|a)4T{ z;B!B-L>ZN{hoKsAUYX(7>xQWh44|D#z%+}s)y6NyLhss7d=c9F(kIXO5~S5aVgLchmWY<^ayP^mL2g>Yko0>FMc(g?lY-7feh{QpOZN zwaa3>(;&WOXk~8CpMjxSKo2ZD%owGFaN@%nQIT@L^Vdgm-3loBD{xcz^z`&A?^|f? zN8g`6e_mcX&W2HS0rlNor2p23t!!-c+Pz!D!^4|s7ccr_XlZF^G_|zqtrqL_JK>%B zUT~SK#afL0vx4tqpE!apNJvO39suDsQ!My6f{gjH<0bvCs1MJskGFQSVY?aMzkkL8y}ya9=^imBVL-4htJa>QEKqU(QY|mP z{lAGnWw${=K@qTjS6OIeNJt1E*It6tF8`^u4k!XPHa1<=H`2!DaI)QlA5tEn%F4>G z3DCirJ`FEuX=yc>{is5EdQlcJ-e)6#}DaUqJpeYI7rP&u~H1GEpM%&$NF z07ex})SH-?u(GnI^4c))ta_XXX&PHtB&Vli^)@=}O!f;H<~l(X+})e_wEF^OG&MD) zq@?B&Urphgn@414uWQ+~ii;WL%cl$2u8CY=zx(>TqjUf(=j`f=-U+q5<$Q1;jCrKI4>`d1w{L=g1n@hj!FXoq?Pxl&!6G_9`|R8D=5gxTU^f!HDNFq zL?RL6?=U-$t9>4`(X?il6ThwANHPWn27Z42Q}y%9%fqPxocFPzxY$)LC%Rv9CGmN! ztgLi(b+@**zQRpbKc05u6c!eisI*gObIi}rL!nS^ZsdyE+E%xVd{a4wkRX{)pJD<7 zfBle*az$_eA%YPhj*gCohBTIhwBvIhrKI#rQE9jpcW7h996o<@d5%;Lk|dSd0xaUk zqq)try}7x$q4F%5IAjW18~*p&5kBcm)RsCl-WBJ&-qmSDtm3 zo|d+&{ElZ~aq-Vpxle+WF-6*|@w*a2H@BYN-riroDrfZ?9d@v_6CSRQlq~Ja_UIaP zrZ$5*gti?(e>tW-SdpJxN@%gvy#BYLas_ULiZtwF-p2hVr~%54>PO|bS&->1;J6F2 zB*YlE-0#h5FECY-0Hym0yow=8R~S4SZMSn0wrLXlt=?$A)#7#$8ylO&dDV169?d#4 zGm~X-J*bxZprG?-eRrkR1In3=&PYp(P_(tZEuY3W@%#7h`FQ|M`1XH^N?@JgBo10e z#$a;W*%FP4x!bc%F;UUo0^7*R&=L#`jG?hHv(o;dABBZzh%N8zFbrLn8thV19t%5< zQYd)|tojbovtZK53=G^slmC)o_Ck2vmR)MK_4Q`oos8lCekUkPFK&*PnP+R-#`pGn zwH$Zm<8dK#S)-D4C?~n(P?zV(V&GLlDG(!SYp+h%y9?w}C4!;ZK&jtmr3J12YI<6E z{I}#ca=7*N&CSi@)Aq_%RW6l zr2GSGEiGbrsxYvlfI*Ouj%5YbM9UQf@fVka4Mq)YA$4B|Q75^wMkRAGxZd;${>guC z9aZ}yyqwJ=?^a@`qFVc>r2+*#BqVzC zWD?glzgSup{&|13h;!+9^=OT9)-gx%Iiv(3@Owtas3xuu6+L}mGGpP-pT6N`RaNId zqJ#~(`O6(5Q`j2P8$|{AO?NtGcK#;TANGBSzAl^+wrmCUcGMyyva6@0q`b3U;&a%R zt-Yoq)d$Mp(yPCJqc*#`yMM;;qYAMd7#$6=j_MDZF4u0vBOvI})bky!enT5l22TI; zXI>Eh=g+dQCw(obOq_xsI|!KG?^B$k%QPKBFPqF6+GwVLwXb=x~QfCqRm)I0(1oxcr}40a@#mWhvMwjX0p5=N!I3< zAk_E#wgwD7 zG%`}B_6uPp7x3H?Dz<`gZ{jHNryDXeUumn-Id2zadedk*0lf?u7#C+}XQ!vWmfAEi z9VXbBlpxSrn+?mZr>iT4dciCeDIyq@jW9&`QoJ$r6B1U*hNqX8#sSxlG&vG5H}bwG zgwdD&0a`wH{Ak;?V8YhMH|u$Oy58H{OEnGNkE0iU#OMV+ZiFoKFP4@T^4{Cl)HY3m zHZ~`3l=6L-M@L!Tya`=+kkQm!nD`;7aUeoF+b<9+Tv}NfVg%%`_Jk1Yy24>&5PyY05c?nE7!_hmn=I$bI>}B}T8Gk@rfptPo+7r2f)K&(1uZS) z-xW4qvi!)&K}SbdKd6RVm34HisjcPVP3816$DFa_aM&$3)#AQJ!6HK&8zqa{vc+g%t-)^IzqC)ukI)PcO%dj##8?J`; zwXHgi*VENp)&Mx!U-*Ob;Y(E4$(`ntHV0?tAVxSiIDnsVWMyRq-r3pO>I_7%f%?eE z$k?p5$wdu3BNcu6b_|Y&ZdHcS#9Zu*@p5obFX{@q9K+hSLIW8a({Ul)xw*Nv_=)HQ zv7L$WG?cH`3_rM@uHguE;HRmAM*M*ts_&Z6@Z_~xr+|HUzoe5tppvo0F^8$&8cZReYeD19uFk^4iQ4smROApV9)3^zI-mTC}&|oj;dT z3Le*oB%H<}g@tdVXlZO3I*$hLuF0$$QqS&3=9bg+3btL*pSzekdOdS z1{CHHA`RXO9vj@NPQJO^+}vmyf+$KM!)BV0pX)@S-0$AycN%KDzrh>Oa>WC4kEIK` z7;|d0rxnzo$MWTumPT5+zEj+x^~VPGuwt;}_xs)BV{B~f(V-zDGGtN?@!7k(I|EoM zRhBeI?P?=3cp9Ar56vMiBV#33I!+>zwAOeac4NbEGCPqbrU+euravn>A%T#Tbm8Ws z*uo2}91d>oG(i{E#-mSqdVn#X!>BhYHI+@fAv)Fj13mq(-@lWZq6))0r9XbGs;mTt z+W&dFX>Dm4w|PYZQQWz~St)M1apku*rht;XArNZ9-hQx{%I_q{*~F-&MFO7xqXYS1atz~beiI)C1L6FXoD9{U%nKzwm$7n z{MZ>uy?L8gkxUaaGc$8^bW~be>Tz>iTUW>6UYJM|(-O#tYYale!5QoC*Duw?M9X=@ z#^&PS;Lk`GlAoV1K@$@_lNcU8lxou^EihzU)#$LZFh4&r;meC$P*+FP`#mj~o9pX~?8i!oi$kdxub+Y4lv8lIS7Fet z&oZ&v6^F{%&@ls-h^a04)!*6S`fwfwGqwg{(ev`UoWllSZra-9k0>g6{@FIcL)Wc0 zE401W*VnkT5*?@K&1I-LV8xveLM{IA_-qB_8U^n^cjO9NOafg46E4&U7!hmi->Oxq zK}1B~6B2%DqL5}w&UV()CJxQCQUfP!_Q4fnSyv+vDb_vtil}qs(9jS7r&+y){3OHh zq+!7;l$DeW&d8wj9M2ICzt?re1J^(;=gRJGZ#xoxy)$RFWx-#SCWVMKdOcj9Zws!P2OD|O-xMuovf*)t{!a#MkFBsA{_)Gos%)9aLMf-8}qr_O1wFo=W;$&*V0;y zk0-EJfRR97iPOZKZu3rhi+FTnA{Kxa*~d{is=nFpj#I0{P02*e6|2twmT8ErDq!`a z>I!Vu_1R$3b~80oy(Z(e1`61iZ}fLssmCxvJR1wRA$4y$W+vPhlmR5ArHP4&yu7@c z8u8C>I?c||E|kqykDD*v{%`OajR){l77RM1$QCP{RjB;p8OUrz< z85s!)(Au*^W;Dj9XJ%&B_v;r2n0sq$3xFjF-7Gbqv<(rQtd~y=NvwI{3l~p5 zr~c@jHJdCMT*X@x6BG8sj>8YuOARx{YH@@Z#K%lI4Gq7?$4>#c<^hyy0j#A(_O>oL zJfsWDWGk{{J-yrjjoV}}{`c?Sqobow4>wck4Q6TO>*g658MfdvE-19_#`A~`v?-D{wO$IlcIO_5}-)x7wKaZ1j;)51Wmd-k)4}I1HT{q&Q(ek z!x9hZtK`ofVm5BiHXXKyv*k{;L+oV)vne&c`e*Uk!=wN^5`{S;sYboifvP(B`1niR za!p-YV(b5G5WgUe@?uS3HNsZM=CWi9WU#>W%9 zdi9EkX#3>kWNT{+27~SIGbIj*iHRY=!(%t{`;7)mR(!=*{VfU^9!ce0US3X3NqO09 z|C%{aV)k@&&em(foRqHw3WYnfa@)U4L7eC~7kvHtXCf7;h@M`1O-+28u`q^kYht}# zlcUA{bYTf0m^-BIz|&Mw(Wy{r*ZnD!!}W>f{XV!yzz6G`-!sQR7YRMU}^ zJgDCn0r<0i!%|TMd8Z^!o~#ZeVhy!VpE%qvcLxVQ9;=d;tFe^FbZBMlznR5C^)p9n zL83^yYTCFdwmo)FUkPAiX7(3$c6R<;$PM_O1n$Gx+JHG2KV8O8vtWIZWc6lal;hXH z5RC36tdv*1c?%1grgoxF*9gnxI_(ajIvX}so{p|Ms-r3Qy0t-Qo zEyii^lkH$ON0B>I@ZxLaoHQ7jO-rT2j{L6^9aGoVFK&^DE9c5jI=obKen|!NK72z~h;q>cMus3+dJf9Rp+D6cHEKb|Nq!po2%0r6s#$ zFop-c?T!@tsuShaJqLgDI;&gbyMyiO0@4miE-*mHOyMvJ3Jpbj zG^j8%(6(vFblM%yrWOktzlp8)e>$pP4ccM1?s(bd#pY*D`Ttu=U6SLhwSU(J%N2@Z za*YDl&n;=^+w@#aol^C->!Ms-)%*_I?`4E^17yHpXcbDnZD@G-ln5cc%HIVn5;cnV zC_=Ay>$Q)qK&-E?FE1}wvt{KdQ)AKIuB)pnEiG-4um`DZ72Y>ki+~`DzRXV@bCxdN zeCupBk(4H;oSwI~2nhZbQ~6fEE1*jO!7|>k{uq1)+1tg{X9?@BljUaO*#q{h&bPzq z^Jv57FKFp7&BswB8gp~$zMKd;IXTgwz7~sKp{9=(o0F3h1?JWw|A3s16Gf3K+)YIa=`k-K-Ju|GZ=6n;J18{YJ|vNpVa&mG~(rp(88zM4t*9Q6b-3rA_KD&)3r-LclY*|n_Z$>au1x`@iQQ^omnrsp@D(oj*i?U1kXD@&nVx}Vhi>B z;m;ObR@<+eZPINO$Gyw++9}D&hc%I~(RKA9SLyCm!GS?6)B*yeM97+LA&h9MHY+Uy@2Eo5ebGo~ zFM7l%r@ci)Uc4RM8A;t)TYE8d#n%2f+0|v(6M{=C`7NN3M0F14z4-j}0L=I?HSJ9= z$3RCvJUWVuh#+Fs?r-?A;TcI)G%k>i47sD^T6N>7dy)B{e6|{r}5cYhQy(v!NK1_&>%7m zNdr?Qm23`Kg%w*eYyN?tAmpUcC2}HfTgoYi$MxCTp`F{NQCNvXDJ1NNThEBf#My94lNT?QC603ZCK#o;70|8G07;( zN-PAd$dVuRzm$(sn2AH_g$CqYSNa9a%*^od@vYs8o*r&)I6{JMkGc^u%xC(2srs`3 zl-x@(_2N2sF++fiA`bc_qT|ScM+rtoDxJDxM{j8KuD>hh(G-47{=r)t;drWFNl{4& zfir2EjS!f)5CPDI5wNtqSXTv(R-Z|*9Jcm7SI(6$86M-`D9x5=h}m>8OU|mOsxB-n ze9ZeS{%1%!mWq_cWl+2aY9AxjfG#U5D|P(x^DM5EoLp^v{VO5&%NDnbq?9-H*2{jh z?00!?k^R+EOG|HKq=0!D$X0o+t;>CVI}5dzUlII5;M2-+jX@Iv={OJw#5ZjA9GqQR zdbHN*&&O*?P32o%U9C~vu)Di^dUeIZ#AHZ@9IrQ}dUBtZTZ&x!3IuxL?)`Lsb;;0T z@5bbg{~YD>NUKJACNXQkFAW*`#EOU3p<=t)izUw{Ze#=uN&1!XKK|3EPvg+Vrmyo= z$y{cfu^DqA?*;!&Lp3d3kaKe4Nwcses>DL|qe4B$G*2HT9v@NXsI^y&DM;9`2bJ?MJ~2@ti5>b*90K9v<15jq z4r1Q>yAUfYtm93WBqSv3>+2)Q9400*lU#5T3%X|m=H6MB-$r4$0 zQc_aT(=Fr%1znD1Glg5r%D!pg_|1i2qBb@*{>49uzEH)j`0h)HwCD?vAQi%$_dKar zA|fLqdhi-F;X{Xwt!qP*%F4<*Iz(zs$$R<*AaSP>r4Er-S64u*E0<|RW8+;{Acpjj zEFXV*dO8aW%gFFB5u2{IhKBS?Kbh1Y0`{AW;^N}P#YHVGEdY>aWhHm*CiC%Q?||sfZG8+Ih$AsFHQlbUnCf@f61f-dYN-4m^&CR7+&@Qj8B)^gG?e6wl!pQO5+~E-rWMpJCipN8uq7tG8JNrY0OOH#bUj zEH8U)4JM$^Kra;#Yg!AMdxqR7-mjr4$yZfX;g!{vm6g@i)d9CbYZO}bw=68u)S*jD z+Db}Fx2Y$~&1x59r;Gzz=RrZpy0kmpF$IWio*zDZm}L@K^YpLIX=zEb5&SUcQvtXQEC)Y4cG~7)x30GIYX<{hia+mSH6Vf_9MAj#?t3A&TBN?qqn} zEW21*T873bs;HP47<~HtSycBNd9oP(a>gG8XAQNmu#i$-FTgq#8>bxM;QZpEQJ}q~ zTLBLf4&W^bfct2<9*{`K{B2B%3_V`F1_y3Xg%lkHK9{^!DhOtj~Y5OMKB9;Hx* z!jh89(^GQ`3j;C$>z4b$JbaY6b3|QHBdd0QfB!c!o{c!J#jz1MNuFsA`a*s|I2^l)=^b;ZTSRj%C# zuQqshu>%h4>+1^+47_`I@NjeE=jU&1Y8u=+AO8y>zwxsClXsEsGjn&i4r2LHS4R|3 zNi}$O9aB?Nla%ywHVpV&pTNwHZRXa!L_|bgMYwUh=9GGRdK=uuLx9o1*w~hZ$4g$b z*AnI>NPIgg8A%#=lOA0V1nFu&A7mc3_WXJ;cKfuZKr?Jq7a zE{)>oz5RW3baYW1Fd`TP(-=ojXUk8mw6nA88sVf;{2%$`6!jMcoByT#FjRWTo%g)W z>tME2tM02o1nQ)ui0DwR^&+oV(|UN+_0bbDOn~|<7SdC4)!S;Bm5kO-=0DxPz zG*iO;!-I#rI{={WX>V@!(AUo@Ej>!%u@r%I&wK1m=2cZy9UdNXnT@`8zYHOhT9xcS zM)2hJR5UQ4UW_ouXFryv!#msRq^Ii3>UGyCF~BiDDjiEOx>5absz3fhA) zLy2tuoS&}sybLF+Rw7$$Y^<%{adAD|?PNAeAnpm?-Q5Yk^QwDva&Y}>_sB;@Ma9hx zj55H+#+H|t*VWav^RQ&ZD#WPq8whu~j+@ymq^ zm@<+TPfA8Mj|{<5R#yJ2UMRMACw^F1SX`Q{#(h5{^>7pPJR17WvZ)Em)w6YauEWaA z&Cl=JyIJc*D78-*8yUeuLj#xQrFRt4BzzRtuBKvO2olVoMfIbSS|#yJXB$kdBq1R& zFfeE>aFIYpChP2?d5GzYnC zudA1Zvitce=NTB`SXo&G1ygJH-&fE)Ui(fF^+kQSJNa<3&`6=%<@bAa^)H{arMA{- zB1aq;NM3(R8XguVbt`mplxr~%OW%5kN+qTDv%EZUz}WA#;^)tw;ThjwnSpDdLy2sX z5J-h8M{H~?jdeW|K(nO4%pWCw8c|m#-FVk>-{AA7O_}LeLOti*-5u}^JUU%`m?uq6 zO?gR5nv#-oolz4p=ej>#NWx{Bl$2zdl)sy}i9=qiNJ^ zdmP|ZR_zAGT{PN;y^%GvnH|&|A#Pq?89HDbRUKH%!p7$2?5xrGh;-2x6B$UPRI8rZ z?~GC;%Q9?4_ydeuUoLY2JxS|cXGceUq-|(kem*h`F2woPWwp%<7!F+7+|=X;`LmU% zXFgU|R`y0Izo)m?+1dGcwLNn-%ztKbsnOx+@bKp-V;|j2wYIkQE(cPW;$$jy({Q81 z&R-^@vyzSu)(@1~&lAWrG&HPwZNr-4a;mDT=H{j4K&6y~>4M#|Jv zwneJXBGyRA$YFf=&>#y-ON1EPr~9j|p~RDwR;`K6!sqiLwv+j4fMi-%SNG=4n=s1K zA(m5X6BDPsN&5R(wVl;AuT)B<2GrNBBsr)U2|eK@a8j3=>^n3$NFl42kAMmr#3 z1#^ODjV57eE8-^7*WBFvMXA%yC`BFI1nTLNOy1}8E9EBVqm-y91rHCwe;c#i4CE-> zNWu(>8ZpbRt(_Sf!sww}^u3H59hLvgbqCNOmXhDQai)Fhi+YvGWrlr(Fvi8n>3zG7 zvhDq>@ZkxfcfK;VLm6j*hwdk16SPYzE-oI7^UAc;Z(}fl+1uNDmy_#t+IzRpZo9K@ z@bK{P@bNV)ab&|Z#K&aK3=Kt#ejYA0Dow<7{FQy|GJ;R<9GA(^G(TqZAC?c%ge2Wg`rj=_sAvkthz0zu^C!i8h?|Vmy>xin|)CQ zy&Bb~TU%Q?t?mhAndJoqE+;EO3&CnT`28Fj#Uda>cP>Z)Dkdhu@s)UJR21VGIE2B% z$|^yMP9Oulvb1z$aj`i!H=vTaAPK3YwA5y)p>r<};OuOVq!t$!H#axC9R9BKyt5Zq zfcQe8aKja+@0pm;SHuM_DDlA{L|nv_*}F^Aw~JY1xdjE10GlHh$G1OGdb!T#-f?9A zrru@|ovpd4>GA0aC@7B^w8CTH^3@Uu2|s4t8Swwy#eL*neHJ`v=@z0=+aV?-j1d@K zGO`!MKw8h|6>|&-3q${>0`)_phqiWe@(eh+xbX4t)QgYpF1F>i7Li-tm_|lMdJF_2 zt}@PJm6w;l;xI(CGxo1`?+QT6ML6LA)HOKEB3(~W5kyG%OVbHig+I;pX;+!hX@`YC zp~2~ZNOr%6advSrwP7DO!{_;KszAX(5CcOK|5>36S4q%-6X(pp$paBy&x5PrfD@Xqwj zE-q%Kqod>HO|0ZfA)h7uzjePL3%_?NDk>W4>WY7STsgfTZ$t4JS=B3i?V2F}38?6z z{67gTrzp$0GK%-EiYocM`;vWmH=<_&At9(JD8W3Iv#$>xOSS5)=c@ua&@uMw+kc&H zB8Ven6Ey? zlQ0Sj3U_yR7$h+KY(`D*>iRmr^j{A1GA})yg119)=I_R|l5h}c&&lbUn@g**tk8rmdhN>wLXn49$;o>cCfc!8~$o( zxmasCmm?ldAjE^Q7Mhw$cEdM5GC3K$bTl?H5*UawC-)ag9Riw28zok{&x2bZO&8=$ zj$l?L`5-WekMXOox3^5QHcb6x`s(tskk`HA8B4hVH69+GABH8mu3pGHgmW>bgm>KB z6*v^iB7h|%e^VfS!U3(rk4UPGwMh7`r+1^O9 zdiSd@>Q26A)J2j;w zyO)TJ2r34H!ESDD#B91P9yhF;l6~Vc*|*~1L?oo7v1nvXk>vc76B99xQu6Y3m6f<# z=cw^kKy7PAx+if36PILue*Wz2tV9J;Q&ZEyBlR(l3IVAUf)KZ0S>7tquDg}P)pBK` zJX0e5e=}3&c0|yw5k(nC;Y9>s)2VRhUTkh`=%@E-I#H_kGw%w<`!}nrj$M=Y|cuV}Z|k&iVcc z@2?C7tToqMbKUW|uKUV%=liQ6A`+6ZIb0xh_}Xwv_%+Y=_Cnvy(A4zmXt`tQ90!+% zhGubb5$tUq%GpfxnwSLwCB?;U)9jM``0;Dq0wGG!+S;1K=HSRjj^^j7uZ|`YGC3`F zc6UF7y%aIVOq8LMNo+lPPC7C&Vmmp)?+KI~L>00J^H>}xIf00)Py5rS=V%A&$1T<=A}s8Sw&Qgc6cK?XBqX$sT-2*HHahw`O7cf?vKbAr%6k*ErgNh@ zP7B{~67F`4=pR48K6jq$fjsSqgqrV?Fy#0j0-6!G@%+=!KGI&UhxoD6sI;brP+U_^ z4r{9!VEVL+{^Ywx+oYkn!mlcV`#D?B8F66t@&zoG-Hi9SIyyQ^xiX>Q;rn}g^DRC? z8o!Uz@S3_WE}SVS(k-S+8F&tUoc+!EaJD^wlZo{8nWLZu0V{Re8{KH6F4*a&daoaJucqoy?xBzG9VepcM7x%hz`|1z)zewtOp^i;KgwLci2* zVYrnRLNRKMG0{928H?a>&%{ViFR$g^c1(A3YwLJPYH)RRI3=3@8QJ-cXQEQhr|t9I zse(RjN5`|9!xniyb#-;$&UMaqgrKD%BK1x^T^|cVaC(hfWG>c09Q<7;N zU{-p1$r&A``7KFzcJaFJ9~r^w_wV@mQ@+ZtF}T97-eMdcA0IFI+zTi@Ue=&GdGPb&F` z7sIJ%#v#H8skzBCEDM(;G7oH*xv&k%562k(GR&EehLj{>EV#E%?J?@c7}l^|bg zjb-KJB64z!-oFpZqT@|3C@|B}A@lm?YieVY@Gm1q>t}#{>AN<$u3RQtwE98PeiNBz(=q&aTzu`j$bwe@-bg ztBI|w0&;YhD+E76au7-<)rQ3?7%86>!;4Ew?ysj>K7C`!;M*R+c{Msb3<8V4cpdEu zq+2qcl-<)PYf@{Qn-o{>`-9kY@N_kqVE!J}EX90!aJDu?LS=yX5#MVa+1uNTj*d3< z(T_?__Sl<#`&egc$s|mb=RXi0x3l5GOW>6&gESxJ%BB9PwheE-+6YBDf3JE{x9r z9dgRVllAd)(`HHdCg_I?ROy*8ng)Z_4z=D888zxOH z&Huu}VwZ&5+Lk$>5xdArq2=~=PRIc^sbQic>YkaOzjd_S@mTuj!Dt#B8XCF;m$Nv| zot0p%_JqBA%fVr&y#aVeIyjsr=(;m-aKu8tP4(_3(_xkBHWL#Pc59*%=bEww5x&kn z%K>(Wi=LM%1Oj+BTagHA+%@p>3*NgDz<>1HXeub^+1S_^8X^mEqbDop^U%14YrDu^ ziPvjmnP6EF(14Q{F>s$IB_*MwqZg}O*14V1hSkc-%4%m`DulytH>{&v^Pnp zm$Wxp&)3;)kLHAhg~i6k0@~PMYzmZ${gsuKogMe1zakLG5_1Fverk)?Sl*5x%^OBW z0e=2Y9dk*OvE*0&8I_gzr6R^_BO0S)WA!B2N9$YL+p<46%*OK-rQ?|xSXegBZVAHo zJDwu`Qha@I+|&Y;!eFopRiIV|6F@3EEm(ryK&zMPGkVCdGpf8iJ=Ked_1;mwaksv) zeTJ*#&cMoV(2b$4VrXRK*Ngr1DcEkM6Tthe=XsK3HrCdt<38Tr-gb0!;N#&9rU@j+ z#3(5%Bi{lF&DFJnCBgh#8*Co4zp;|i(tuXILnGgb98oY*F+qP!ZEk7&^MB(9Iq@K0ZFqcU)ZDBf{Y|Yzo1j>p`f68|B?8Z2Daoo8<65 zE;LdZn=hukIXO8GHmREoOiY2VF582i&3maQqMeB|WT&R; zsHzf{MM0kaw*~40K64J6gB6p0zQ28W zJg<&$sU*}jG)f>X@ zQK=q@ix?yJSQLWV%F3OZPUOQEOZ7aBhgyv`r#?2bR}MP^Y~-5pEms-C92D+8)c>-QBHb`>GWqm-zPWTQf7WVwG||cXsi1M}$6ubQ2PSTykcUNZ8ye_A9Z?-EM*#`9}; z3Y!)%upDs6#B4_%OMaz^wKWk%_2CnGUh$;w__(r%$IVFghoNNdNWv-Q1N8QD?tvS| zA)$Nt_E?^j$*Ysbj~u9-kWdtft4FYvVHa-H7eA1~8>59ig7Pe|xnx8J`Py++LC7ONt{(aTse?oSks4eG!xm7B5Ol&|v#}7`6{6TImE^BM+jf>xX zeaT!_#BPh@8UMjH^v4x!&sjrkz z`ei?+UG!$I`k92Jq{X=nNWjeV*Xrc@z;>8vaM}eJU>edg&7#M$%*-h>D{DHNzJFob z8Vy1OFnqG~eF&N#0U<6SDBORjvnuA18A$Onq37-U)|0fAlmHErh&M9Af;}0K#)6q&_9!+ zcd#KJ|8c_-B-DZ-rn=scloZkd9{u)FoI!zQ2`-@Y*XOnx&K=VG6HhKqlaYaeNXQ%B z8H)dcNnQ9rzHmqC==^;B01@r^`F_Wno?1rbz@Em21{UqQqXdX|CgCF6*V$Q3g)26m z4&O?z8|UxWRX#q;JMii0X_X??s7>uu`R)Joyt@DskiDB>Of*Dzn^-WG${pleqOUI~ z)5hr)nm;A6L^c>Qf*(glPClsV z>xJ-V{5qf3toPqcYdpQYZlkC^fWct&f|@lZCdSIr61i5OC*yJFJykQ_+}vz|pY#FB3O?uJk`k+rAD>p7 zeo<_?zq>i3BqStU`*M#DGe}HCL_|-IlIidC7r)$!3fw^f1ZK?N%slFA zseCRG#5ZwO#gVn=IBN``S9F)LUwXOf4{!U;mJSLF-;@R_9TjkXH;<2x|7j_1ZCzSW zz%a9_bRY%V-`U@ng*?S|Is_i^80|q2wnHEk3aDWnd7O4)LqmTDpkhDw)PF8?53j7K zu=Dm7gb!F-TZ;u_?F-bSr=ZxQ!t=lfe*SC@8OXutO|{9w316`Uv_;}k)i;|c}~^>(KP zDo3y7y$CXAXJ>mcMvO!oA{AM*KPPGmSJOK!`azcN=mBvjERMXi-W`S>gbgl zg91klw)E;;L6FBXg^;)BUDaJo)Yo`nwfYZ=C z=gj}?0a#B+a@z>(Yg&Qy2V@kcFreSD+ls&lQr|6C%^VpY|M`}#KQIt^9TLb5y**y- z85|spkB=`O)oF6IGcw94OnZ8c+#JCF670COba%mhz+9InsX3W2=Z-cCsRIjLai>Pw zDlQ2!;iH%R?W`RWoPTydnj@VtXEr9y+U$5R&r3m(e!1v@{o~z(Uj;C@)#u(@VJ~A& z1QA@;nGmDLI7)o=epQspisg+6yg z!T}}ot{X_^R#Q_8pkAyn=s8*MQ#n`8r`r->VHq49jb@qu{cvL!opbJIH2!B~JGB&B z8(SBbNcj@6ierLID&r8(>jcaQh~NBrcWnRp&zV?E*xAz(?Kb$Dz6cD#JH3?pC zQ5hH*B+1m643R4-jb&s|;=qo|qM{V#_q9aeE}z+FZES3~dw4)vyi=_vt;TDylhe~%48~0QAlwlCDZ_qK04_~VOzcnP zyG*u$+S%J9(qO;iV`8%G4kKb=VL7Aa=ZCFQ+@rOVp9w9Hu3=`o-t5=5os3Ge27UQ* zccXM8rlXVkgOn(icLd3TK(M5w1TR#aTN)RD3x)>uc04Kn4dwlcCM47X3~4WR`U`yK zBn+OC$%6HB^YTuuqmH_3kq!>=@xFLUNAH1)faD#9Ayki zomu+C#M?P}ySQ*OZ7qAK!tWB%PME$ov)dicUA#Eo@A#C89ce`(DD}VCW97Af?6IG= zmR7Ph3o9d|0SU64v9a+ZN{KA;9s0HxOrZ)A5fSk!o}uFT)7OVh2JX~Ejo6(kLq$cc zRQ1b#ODDr~sfYQLQL%fFWC_c{^x3m#-4p3^4#NJ66b_@R9z0&S4OU=&C~WRNmE9yF zDT(NkP}?Ua{;fvYa(^KAyB_iHCe+N#`&%Q~#VWzT3l&CSUAkm-XO3B-*K;w9m6esn z5u;cA>(}Vm*y%|22g?Ax?Y+ITqoe$i5~D|U$p+)sAnMNUZq6qLZl~|23N8a$IIt|o z*oj2^joX{c<#{TjX0)c`eOvq4+1YoO>xpUd818wvm0mJh`%oL^5tuIOBoGMV_`dUt z(I^Q>^HsPMv~+ajz@VJx_tZp_DtABKkgXBHY&9*8P(}*~x=sO_$2esAM^1tu#<+|O zN=(;FGdvt8a&q#W8+pIaXXoceC0W8Ta*07fK^ZbE+I7P5eSePk!^6YNpx1@J*!lOR ztlK;A#@gHcb4${A>{e=*y!&LeWT}jA5^H`uLawJ^a*?|6@$p{6cVYCZ`EC?7P$)Un z1k;e&%lYHSk9zQN-*1GTBy4nq1OyeB?VUqI^0I>fAQcjkK>(%*2@OSX>eLL95fw$0 z6r2RxiobtP@cenXYDLCdI+^dokJ*^?^YfLiZ}ri4gQv_GMXG<)dGg@KLqucC$uJPJf%AK9LMI)Hj``v4xv*I=K|z z<&bJ~#?a8vAEE5Ppg@|oLSK4jW=sqW!$)q9qU!x-ZB>=4tE(w3LNbc4(--mAiV*k8 zA3un{x22}0S_Xs+hv6H0dkfNguDojTx`lkfB-z;9{9w~wrde&?`~4NwhX{_nw%%T# zn3MqRTh!mI-?b*CZLos5PCO?Ct_9UUD%Kfj$h!x0d`09SvvcT;`jlH^ZoqWR~oYC(8C7y%AR`P-eGmsApOl;fUDd~!=h|0^$o0*x}P@n?b007Ji z#D;~19Ua+D7NYFz?EGc?9G!+1A{LAlPRe_7wA}GEjk)rCcPf&Iy?cJ%bGbc;V^!>b zx9mSG^juqu;~$+w=$a<2fde{%2}_|NMaR z@^WDC>Ko_rX6)pDRna%d4ULUrVq%;QA6xTUw|g{$Kq*4(CW9$&4Nyj4oSd9^p@EjW zb&m+@*R;KI6cm)7ly=I>vDMX1En$y4QwIzgiGj(`#-O{2e{A90oBrujO%)zKVjB_*ZJp>zrYf(Zzf zKP6h$Gt=xr)b@@J_IK~piwIAzjtxvqOr)iyk&ps}?v5j?Y<&xd>tbV{8yKvzn+z&E zvl;sNlbVEtL`g|0G4F7`er$4*cDT3SM7&Ne5iM)=KVjO}=T3(mC@3iL@$tV@GjS;> zu-PdsRFK@<+}hgO{#;_Tt{fZ)TFq7|W=oVnT68nP7`pXzWg~NQb15CCLl5)rmIO;E znvT0k_*@PG>9kWJy1G~vzME+t2MaDsB_$;^-O`2hnYl{!x>F3K7E*yWgG9*TUeDk&%mwi%tX)nD<@whe%IPPg?mj zj65QE^&{r2qoV_bLLZe$UflNB-+*xHCG!NVwc27zPu0T4#)PCQ@#@O;`$|$$(pavn zQa)BVQVCYO_Z=Ljym@*N79Q?(*nBC8dPSbmQM=?_RC#kO>ew!hBR;Zfg+(leR5}~* zt>IEXJ1a{(rr_tVU!xrzs|=mCKCdn@>P~6t7o}oo&!bm;zqFlRtZ~`{b%%InEp>~s z@7m@6vZ-UV;i)MKUi+Wf*;HfrdpS8dkIndTQxg-Qmf%9=^$-P;>`%ae88v{f&-&>P zBw?=P#KiBs(XLQMh&wxbW!JYCN)UI%{;}@v?*4e*vWiH4&nt!3uxd6ExD}&XDRsB5 z=xQAp(l*5o24CM^F6EK^y?+8uhmMOV^Xk93KYI#|37le&xuhndr;|YzL8J7Qe*iMY zO-%Cg^75`P^erqbL|%5}m1FwTFf(H+(_5$@JsRfZL3F&m@A_C_2M2Z;8lihQ`_wdFRZY=p&3&!S(3FabiY;Lqo+wKKHwVXa0)nV)Cnu-2 zdqlzK*PCl=ttl#sikVw8acrENB}GNMjZvq|z;Ti|VxGD*If?SM2w;Ow-3K(Zw3h1Z z0xVuZ&n6}&C}GZf(;}YR41Mv;$-EAUp03;G5`E-ueBm8UpVo$sFM{(Y`$MEK3VU+N z=d6qj;S#AqJUqO240x0$p5@GM>@g#dOPoQbBKA0q_a5lReYMK~$g*aw+B7ya9gDe+bY1 zx8`>5?SriiaaZVLBbu6+uv1z%BECTkE&lfHTL1(6AKD_@9NcZ^Z?{`o87*u^SW+AZD!JUofjSfrgAVbxy|69oeN z{W%oeaJ{J#Qym-}B1w5ao0&=NR~ire>FZl{gcIPQV7~zrY2#=DL#ccT@$ts``Uqn< zP0x_|Wdz--?f-3<>cmev9m)!z}^%C>^wZKmNT^U^_QzHrrx}HbFkPvH$B}8 z%lEkf^iHkP)(_Q9eon+;SUDmZI=bI>Qj_vJr<#t=!s6oM>}-R}p{|96h4pjpRq2+` zU0)4)A{e~WN8_ii#PvSLKSFLD2^6x`zx>hD=4Ssrd#Q1zV&)72Ld>&fFxC{hQrqQN z5m+i#fSsXVN_49cpVpsJ{%1)ECbfcw2GJa)t?>BBM@uBE{&)ZC!Z^>~zI{7?tS#F* z1*D9A))8%^vaC>31V}-a2`|D&@(rv+XOY^D=bq;~6AO(l9UM`KiLUUgV|094_&TeP zUkqUTB_Sa}xqW+kyUd*Kc_Q`cQ*?6jT;f{;7VSEaO>sei+s%G$Cf!Y&m+w3?NMQ{~ zppZg9j?k4_?OpNfixtifkAjw$PMO7JWt-Qd|9Bt%yy-e+4<3SqV~Ja>Ry2mH9_ zu}28oD?rI04-XGFJK_3-T$(V(&}y+prN6&F@Fqwnu%z<298^!~BF)^N^sxp6krp;J0U@2S zu`yevf`Oe~MP}x5M=dZnl+~L=*@r*v#_)@^xS^I ztce`+HfrVi`kJl4=MJ{%b3K;+H#9U99ql|*EEtPI5H56oDuj=R_xbbZ_V#uxGB9;Q zU(P3nVw4bR@_g~~;NW1N``gR^YG;)`80yH%VlsW}iZyl<&X7x-_nQ@f~DUQa6c z%O{2Q`}c1yF0Mb@x46h|gmP*81(Vkf#1bXz2MrAk<3G9~(x7z={36Xm#?9_m=o9C6Dexe{h5+{q{1iBQa1pXs17!0O6xGO0)s}n&nJ3amK$$qG{$fkv4H~aUD$Qej#jkY7eB_2}u;j z5%>C;mq)>CA6WhLsncPHe>-aGkY-okg9}bl2s;)KjE2zSh7I}@OIMqio10VPp`+RV zAA0^jeF6az5)w=d3>Mv{{4I3+>Y5tWv#T8Wq<1u2Ts4)I4)=GrG9`779WtdYFohfl zJITU#S8XdWlu{J=O97A?yss(3erc8RAo#Euf5*@5GXc-{0Ru&Tjpcs=g#yj%}fRz~|anDnfyyjHt0 zOW(uxsZStCLF?7wM@P1~U$B79Nh$C5-4QB>FiNJ{I#mVsw)SdR$nlN4LiiFsiod6Wk7Q<_D$l*sZ`}rvQE+f{?9YYDzEV%|O{JxOaPVxv;R1 z)S5q6YjX+Zotv2{$jd{$_W2(GL)H6cvP5h73cOq3xIfEDK|v9fv+z(J!VCaINc@5* z3zFblBUI`KGHLuo@p1o$-_MAQiz6teZER@p^zzbYn3*vqFmwkPGva+%_zj(ED zbTD5NcJDNMTxx1*#n?I?%<0z&I&5HUA~{6_1_pL^ig!!0Qza%PG3z!Z8cG~pTx<}^ zxZS}v<R>_J@v)-n|3&+|{x@kb z`M;Sbtt!)~%*QdQY zHJJqObr8OIF*iFKt-!z{KcBLGb~{nwGhMDvKtM2#XcHU|04A91?(s!hB1dy=2o|MG z!YwQ+T25G~FhKns*7^i0bycFJscCLuvCP;1`?p`(T&)difjszs-_N%U&L=fq_4ZQ{ zR!4ZMes_0w20al$o8`I)MA*{8?}w~j55r4JO3EWDs;a2_I_KxL)n~>hCK%kE5lo@C zE{krcOqAyq-UjP`;_U0SZMVzN_V@QY4dShYal`@NQA=;OC2joI8}MXx;iiRgaR4n` zou9w8neL-{W(hun@@i>lB*nxaE&P}AIG7WO1%_gsZH<6QQif;*1axK${4-LB5Rmz0 zBGxRzA|kx+Z!gVaf`YobzP<1m3VYCJzeaL`5{ftp@;9YSDY``}=q)fM^r71BM^J{u z13i9)UdrzL_x1nXI~gc1EI^&J2m@5WNuE*NWvc)QP*#wN5m@Yt*cMQ^{lM+!%4USmyf8ZsOadENFO+opdw2` zV_jWcLjyU#D~{nBVNayUW`{vf?$9`O*DV|#D^Tu86rCH@nT@-%yBo(&)mp~yb$u2? zC;v(Mp{F)*b-cPwLO=ftmmXt)JccY`DQ;23;`w|>X zM@^j~;K4;lmzST9CjK`76&nl&_u}R+3G)991z+gv+XlRfsNUf}ifJoD8hb4h;c~dR zJ`najB?Y4}?eY!|&@e_pz=Q$5tN6NOXDa>gen^lNxg)SYi0xi<06^W7e`2rlSKXwkas6>tLY~;7)!A3=Z}61z`K% z9kJ-z^*);kelq^*#IGY5`vn03AdP@pO&z8nEs!El>M6E!ZM`X%V5O(mD?#%QLBnxkSppv}-OWt-XjS?w zr4<&w`SiMTYz#k5O<7r8ODkZM&taoKn8MrC!eaOEkTvdOPZ-hZaSsLRlPA19Jiet! zG}s0~;o+F{@L+6;QBv2vU^Dz$>xJmPb+0AIVY2^8E*?pEOJn3M1#N9hQ&Th{j!PYz zmRl}I*X^7HQ8BU5p#OEbnwa!+fQNWt%=qM_yQ{0MgX1f-*Mh?1WYg!cL2SBKq;Y6j z*<-UCR(%1NA>HZe-lITZrvKwO<{buW!D7ldL-rk<2 znHjJBTJMyuN6D{W(H4P1-p4ClooImC)(245oo)X?Fc;`Y)KRJYJfs@WxY>k zTNfH#xNR1-wY8a|Bx|hZYCK`~=EbF@2L}g>6$YqpxgN9y+D)#CBb84qt*lB4>5OK6 z8L^uTK5rNdhAfF~5dXa<0Q}&TJLw)`3k=Hq(`tasv#~j>so|;-N$`Eb!sK5)zfUOg z-2_!RrBTLSZ))?%mk#`T2XLrJ_er z(B0Eh#QJaFV2{HPDgP-|ckoOGQJJ)-)G|1lV7xB47*WC*`H#;23hF4q&0m6c@`P%Boq+nav-B#%1N z){hkx`uY)xmMqH{e1S4CG4U>`7_c@SM))s$6>{_+&c1(gGQR#j1Ogf2^e?ad_3O{j z(BaO`$@zH@E%Beuv#>{x2{#tU)TRGAqdZ)@`x7UqUT<$N8aB2`$!Ap6sCv7)VHJ z1$F(?!9J8wDCH5Ipn8BsCMG7$Groo8ll4AaTwEaV6Yuryee@P{xV?@kP%6>Z)~+il zDJjXASeT!$>rP1_m6n$N#L33ZJvBaFRazQRyZK-i`0w+dVJE-8zyGP=46w?7j8}nw z_T9JohYW3_!zR`or7j{OqW2xl1X#`2h9{?|%~-|H7P!0y8V!Om+y zj;6ieUyX}~9kKjl)PY<4`IEXle@6o=kRKl(GcYg|tAvR^Bq~V& zpNK}fQ+o)t*c;ZFRSQt{so-?uFz8rF?Xqz;P+ayt=vqT5G^MuvjlPZ66%4{J;3J$Th}} zXC|Pe1|}1CiHeB@D8G3RQ3X0#FNc7!P?}DO`T6+(q4i)4A(<|c%BtMZ(BSXy&!XGZ z%N61uLnoic>+l7*v5=_2K{+2EADIuL`6VUiM@PPgf&Ug`C}Z#i+xhwVr-J8qMkL5` z78XU~ugxqi4S^R%{G+UFxD0KF9k!&TWN&}}V-wT=40RmVzOmBvO+-ZGwX`ZKBft9Q z19q(#6BUIE%$;)z>+Y5a6(60Q{lf)(&d^3F6gHowB_<|TRdGzdQ3vz!3GnlqTUg8h zn_qEkVS$X4G$kRShieNs6?olFs2`{;oS*3#gD?1*nVBiW7#SI7Q1n0j6Nus>i#USt z!kqx`R1t1KQv$yvAtruwN)Wk5-|>8_@F4o{ib`D^8Y;`m@&{I83+1uLxUjHb8G!XY z_4a7feLg5aX1sXLIo|zdlh$(-c8d$fV3rSfgd@zhR;yqeJCT-oF6{ zAJXAFczUx3A5;8g+;rhdjfu%lLj?IQMGQ`b`1>PbdU_};DaE9u6sIA!Do000fBN(( zE6qIhx8c`^aknb0pnt!-yu94m*;!cl=Gjn!1s z$jHdtywLqA2y}p+T1IANWi>J~Qqx#jQC~k}${{yUq=L{o`*<*LUNkBnurM$bl$Oc~ zJOP0&0|%#KqoeUi7N#{66#oAGi$g(SleD(J?&juJ-_YtuDJ)f01<>d-V+KtY#Pg|<0l-mWFm}EXdk`8Ao>FDXV z^3uHkElK8dW5B}N8hZQZ?cm^G zb8~ZHrrRD#52wXsF{CAJ5LNG6VNnr29^RMW;E89mqqDQbBqV$}Tnr%4kw^9^=hjNs zx8vjE=$@URU=;`iqO3grjTLIT4G#+oi(^#NOv@PC{?pgT_~uQT$3Z=|5d0{VF74>} zn1P=DdrnkT)b?1O^u33xD-#3?AI*{eBRxAi+tl1VH#fKBx{3*=@D2YnILPwm%_sS4 z8aleov+c2mWyE%ba;gjm@Av?48O8FC39|P>^`(fXowG`>7@u+ zSy=%b71-E@?PnVrz%Y8ei{|F$sHiAsXJ-`^6&MW0Zrm>^B}GI=Cg8l6`0bmtnpy&X zeNK*%si~=+-rwvFMrniF1Ed`W9_HTU&)@ z5D0XDlv*YWglvT^p0Ho;%W9aInB?T-US3`N z6O%lr&2Lpz9IUKmybc?C3yo@v=2}{FQB)rg5fPc>zZ*=XqRG(4G&eWH$KJ8CPnGHv zVQhPXxJqhjCQUj1m^hvL(>^C45NKtk2Z4k|HZ6cG922EuAenpOnE?Ut?b02ZFUrOIzFWrY3$mIyx$<4u0o3IJ&&Ya(fW$bdwP^`Zm5kJsqNxC1hc4PROho`{Rc>`Av@d-Q8V9MTLDzdOGU>>k<_g7gv0I{12I2ty=5- z=?bx@5Y;kB%i?v>M3EXvc=y49bzc%fYfbMaH;i9ZORGq?+%8Wp_0Pb7iaY|S)hqk7 zT4u-P{_fUfC{1Mm9}iDdR5W%K-K9PGu%YGF8w_!W78Mm06cm(~vw7Zp+4o6fj0{Qj zN#M=^?~mq4FDxv0d3j-z^5_xi*4xw2)AtSxFjG?g?N4Igs9@B5!K%BrvU0K(BVVdj zE9c~ca4LRC0kV`cFfg#RwDj?5ac?sQ497YPLX5AUAu@9)R|-rCBq zt*t%BOzAlcY`M9+-~#}!TF;)z;b8?86~MYnTRSBoAt5FvCOZ0;%@TA-@W%4;V8P>J z?=5k7^nsL=)IF9*U&HA{9ui1BYG`g|W@e{g0PJ!wAE=gs0P?;0uxYUbJKdzI^toCN zJ~=sQYionUJqz5|bI;zyryi|d)&_t1!o|rsF**6i0e*XX?(Su6Z7m`3<$0rH<9H)S zQ*4tbWr4ey2ZQIfXMvZQH-pc%&qUm5{O-m2E7W;QBG7@gt1Hi|SD6hBn$3F2c=-71 zMFGI6M2-xG+)!sywY%6XoF$YG*huVBB`|9dzIb7x5eL76ZKkg$mt=p4ba!_@n5~w^ zyB*FHJ4xy7?F|V*qo7Ei()EyypxLI`qdD~Q@}k+MxuUtDxod1_I4Bv7yE$UGzsb-= z0)>r@j@l=X6wiMD@#DwZn&J6#5Qv}B5qhDbtSq_Qa`(Z-4SpVe|EsoEUPh+s?p0H5 zZEbe;>U_OJt>w(SyWpdk7HbO7kq4jMO6PJ%@Pv{U6-1T8K_3wW!hQvY-26OUT7t)q z!VYgwZGwV=E>Fc=L9O>xF~4I|QalgldC&d#O|7lDh{N%%DUd)QY~<83uhG+y>)!GK zn>SPYF_$2{vDvw~bN6QEy9Na_kASv}v29@BuJycWGxhP|-d=HoXTLi=q^Z55!(=FJ ze`BM@a^_cew**mlR~JKVw4zKh*Z$S<>ZAz>q-UOWnuqM%j^rC38yoxc=g<5+>}COv zi>9+t=|<;$Vq)S?%t3*HOxkq{6R^pSh=>T6C9m3?93w}^v)juhpWE}Pl5_@QVq$)N z{_E>&Wd{%_ml8|?0+CRac>Hbk8y)?0-Y(W}_--JXo0XIE{N}Jljrq6jzz@#dsj}Jw zIC}6`r93%i^>Sg(`5LRa$r3FUF%T%17Wfl$4y)am&ALl7GX=V?>MF|0{5N}*z%ZrN zNTGw(RV4+5jQ+X*9QGwPH{j*rc0IOY-R4>M(Yd+ZjSW>bH4_13klvrHQv^`Cl#!7U z#Pc|4=Zo`Yd8AG?w;(I)P+wo)!~`b~kGi`S5g{Sr#~h)HX#+qFlf-VqRF>KVhkNqz zH8N|}^pXhv&*9#$R-zdf7e|9SDJ8V508qhW+bj^MrGSj2BpNz;MH&di7B>O|fu;+q zs;Xk>%;AVq>-2B?tmBZFzz~mg+`E>rk&72?>euXED<~g5D&a5)y#)pLu9%+wo82 z@sitS>fgVA+uPe)TZJbg#}4IQFTE^UqG)zo%DYeGl+K&l9WXEwG#O0s^Ya7t#@)xo z!V%NGgM&0%dinUf$gnVFA5f(m>|}j)l>+2j5H^SkYOV1)pHN%!x=c0aN%GoTt!Eq+0rluw)CMF`%cE5G+ z1{PExPq)6878dqRPELmNddJ*fm0V4B02^WC14Xk}B!Y{}`V^*igX8btzqb=0-ipmQ z|8ouJ=jTKiUy9VqG-+dIdc5 z+$&SWVe}x--%GJgB#_>zH~jbm!%?NlP<%pyi=(5AF~!Z5)lp_jm4Ksb?UJ|0@k-a|{_^s2y5~tKhRfb`)Q=x;+#t%z)TusqaM-sOtbd-Zi(Q_b zsrxAOIo+ImM}ifdzX5@+1crgF%)h-EdCv!5(zm(YZRUQ_pMHPbL!njn(L>q~^~sa{ z`Ff=v+JCgK`2_?71O*rM*u=%fudc75*E>Z~QBhJ-Qm9y@-)rRM;y;PC? zDZFDNBZ_y9wzkvd`eHIN{iqb4dNwvd32%O0dmGMfGN@f=n+b*9?MxKu=;-WDmQ-rj zwm5Eoa&d8C@Im+PgRQQtq)jZP@17-;^)c#Le}$_33LDg_EU}3M%6MWKfC>UV1r9C{2(%Ae jJqQFE#`^!`53V3{@o!Ck$8UuL_bByVPP|l9&;S1eD~ft> literal 25495 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≷L)AGS9S*ecu(5fyUJYQ~kZ2@5boY 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 literal 0 HcmV?d00001 diff --git a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/NanoleafBindingConstants.java b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/NanoleafBindingConstants.java index e7e8a2fb5a7..db83c1fd789 100644 --- a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/NanoleafBindingConstants.java +++ b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/NanoleafBindingConstants.java @@ -59,6 +59,7 @@ public class NanoleafBindingConstants { public static final String CHANNEL_SWIPE_EVENT_LEFT = "LEFT"; public static final String CHANNEL_SWIPE_EVENT_RIGHT = "RIGHT"; public static final String CHANNEL_LAYOUT = "layout"; + public static final String CHANNEL_STATE = "state"; // List of light panel channels public static final String CHANNEL_PANEL_COLOR = "color"; diff --git a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/NanoleafHandlerFactory.java b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/NanoleafHandlerFactory.java index 41538ee05b5..aede5dbb536 100644 --- a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/NanoleafHandlerFactory.java +++ b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/NanoleafHandlerFactory.java @@ -57,11 +57,13 @@ public class NanoleafHandlerFactory extends BaseThingHandlerFactory { this.httpClientFactory = httpClientFactory; } + @Override public boolean supportsThingType(ThingTypeUID thingTypeUID) { return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID); } @Nullable + @Override protected ThingHandler createHandler(Thing thing) { ThingTypeUID thingTypeUID = thing.getThingTypeUID(); if (NanoleafBindingConstants.THING_TYPE_CONTROLLER.equals(thingTypeUID)) { diff --git a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/handler/NanoleafControllerHandler.java b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/handler/NanoleafControllerHandler.java index bd2efd45546..d37a82f06c7 100644 --- a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/handler/NanoleafControllerHandler.java +++ b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/handler/NanoleafControllerHandler.java @@ -44,7 +44,9 @@ import org.openhab.binding.nanoleaf.internal.OpenAPIUtils; import org.openhab.binding.nanoleaf.internal.commanddescription.NanoleafCommandDescriptionProvider; import org.openhab.binding.nanoleaf.internal.config.NanoleafControllerConfig; import org.openhab.binding.nanoleaf.internal.discovery.NanoleafPanelsDiscoveryService; +import org.openhab.binding.nanoleaf.internal.layout.LayoutSettings; import org.openhab.binding.nanoleaf.internal.layout.NanoleafLayout; +import org.openhab.binding.nanoleaf.internal.layout.PanelState; import org.openhab.binding.nanoleaf.internal.model.AuthToken; import org.openhab.binding.nanoleaf.internal.model.BooleanState; import org.openhab.binding.nanoleaf.internal.model.Brightness; @@ -101,12 +103,12 @@ public class NanoleafControllerHandler extends BaseBridgeHandler { private static final int CONNECT_TIMEOUT = 10; private final Logger logger = LoggerFactory.getLogger(NanoleafControllerHandler.class); - private HttpClientFactory httpClientFactory; - private HttpClient httpClient; + private final HttpClientFactory httpClientFactory; + private final HttpClient httpClient; private @Nullable HttpClient httpClientSSETouchEvent; private @Nullable Request sseTouchjobRequest; - private List controllerListeners = new CopyOnWriteArrayList(); + private final List controllerListeners = new CopyOnWriteArrayList(); private PanelLayout previousPanelLayout = new PanelLayout(); private @NonNullByDefault({}) ScheduledFuture pairingJob; @@ -515,9 +517,8 @@ public class NanoleafControllerHandler extends BaseBridgeHandler { localhttpSSEClientTouchEvent.setIdleTimeout(CONNECT_TIMEOUT * 1000L); sseTouchjobRequest = localhttpSSEClientTouchEvent.newRequest(eventUri); final Request localSSETouchjobRequest = sseTouchjobRequest; - int requestHashCode = -1; if (localSSETouchjobRequest != null) { - requestHashCode = localSSETouchjobRequest.hashCode(); + int requestHashCode = localSSETouchjobRequest.hashCode(); logger.debug("tj: triggering new touch job request {} for {} with client {}", requestHashCode, thing.getUID(), eventHashcode); @@ -525,23 +526,21 @@ public class NanoleafControllerHandler extends BaseBridgeHandler { String s = StandardCharsets.UTF_8.decode(content).toString(); logger.debug("touch detected for controller {}", thing.getUID()); logger.trace("content {}", s); - Scanner eventContent = new Scanner(s); + try (Scanner eventContent = new Scanner(s)) { + while (eventContent.hasNextLine()) { + String line = eventContent.nextLine().trim(); + if (line.startsWith("data:")) { + String json = line.substring(5).trim(); - while (eventContent.hasNextLine()) { - String line = eventContent.nextLine().trim(); - if (line.startsWith("data:")) { - String json = line.substring(5).trim(); - - try { - TouchEvents touchEvents = gson.fromJson(json, TouchEvents.class); - handleTouchEvents(Objects.requireNonNull(touchEvents)); - } catch (JsonSyntaxException e) { - logger.error("Couldn't parse touch event json {}", json); + try { + TouchEvents touchEvents = gson.fromJson(json, TouchEvents.class); + handleTouchEvents(Objects.requireNonNull(touchEvents)); + } catch (JsonSyntaxException e) { + logger.error("Couldn't parse touch event json {}", json); + } } } } - - eventContent.close(); logger.debug("leaving touch onContent"); }).onResponseSuccess((response) -> { logger.trace("tj: r={} touch event SUCCESS: {}", response.getRequest(), response); @@ -670,6 +669,7 @@ public class NanoleafControllerHandler extends BaseBridgeHandler { updateProperties(); updateConfiguration(); updateLayout(controllerInfo.getPanelLayout()); + updateState(controllerInfo.getPanelLayout()); for (NanoleafControllerListener controllerListener : controllerListeners) { controllerListener.onControllerInfoFetched(getThing().getUID(), controllerInfo); @@ -711,6 +711,24 @@ public class NanoleafControllerHandler extends BaseBridgeHandler { } } + private void updateState(PanelLayout panelLayout) { + ChannelUID stateChannel = new ChannelUID(getThing().getUID(), CHANNEL_STATE); + + Bridge bridge = getThing(); + List things = bridge.getThings(); + try { + LayoutSettings settings = new LayoutSettings(false, true, true, true); + byte[] bytes = NanoleafLayout.render(panelLayout, new PanelState(things), settings); + if (bytes.length > 0) { + updateState(stateChannel, new RawType(bytes, "image/png")); + } + + previousPanelLayout = panelLayout; + } catch (IOException ioex) { + logger.warn("Failed to create state image", ioex); + } + } + private void updateLayout(PanelLayout panelLayout) { ChannelUID layoutChannel = new ChannelUID(getThing().getUID(), CHANNEL_LAYOUT); ThingHandlerCallback callback = getCallback(); @@ -726,10 +744,13 @@ public class NanoleafControllerHandler extends BaseBridgeHandler { return; } + Bridge bridge = getThing(); + List things = bridge.getThings(); try { - byte[] bytes = NanoleafLayout.render(panelLayout); + LayoutSettings settings = new LayoutSettings(true, false, true, false); + byte[] bytes = NanoleafLayout.render(panelLayout, new PanelState(things), settings); if (bytes.length > 0) { - updateState(CHANNEL_LAYOUT, new RawType(bytes, "image/png")); + updateState(layoutChannel, new RawType(bytes, "image/png")); } previousPanelLayout = panelLayout; diff --git a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/handler/NanoleafPanelHandler.java b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/handler/NanoleafPanelHandler.java index 2c165852065..995eb8a3479 100644 --- a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/handler/NanoleafPanelHandler.java +++ b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/handler/NanoleafPanelHandler.java @@ -72,12 +72,12 @@ public class NanoleafPanelHandler extends BaseThingHandler { private final Logger logger = LoggerFactory.getLogger(NanoleafPanelHandler.class); - private HttpClient httpClient; + private final HttpClient httpClient; // JSON parser for API responses private final Gson gson = new Gson(); // holds current color data per panel - private Map panelInfo = new HashMap<>(); + private final Map panelInfo = new HashMap<>(); private @NonNullByDefault({}) ScheduledFuture singleTapJob; private @NonNullByDefault({}) ScheduledFuture doubleTapJob; @@ -227,7 +227,7 @@ public class NanoleafPanelHandler extends BaseThingHandler { Write write = new Write(); write.setCommand("display"); write.setAnimType("static"); - String panelID = this.thing.getConfiguration().get(CONFIG_PANEL_ID).toString(); + Integer panelID = Integer.valueOf(this.thing.getConfiguration().get(CONFIG_PANEL_ID).toString()); @Nullable BridgeHandler handler = bridge.getHandler(); if (handler != null) { @@ -239,8 +239,8 @@ public class NanoleafPanelHandler extends BaseThingHandler { write.setAnimData(String.format("1 %s 1 %d %d %d 0 10", panelID, red, green, blue)); } else { // this is only used in special streaming situations with canvas which is not yet supported - int quotient = Integer.divideUnsigned(Integer.valueOf(panelID), 256); - int remainder = Integer.remainderUnsigned(Integer.valueOf(panelID), 256); + int quotient = Integer.divideUnsigned(panelID, 256); + int remainder = Integer.remainderUnsigned(panelID, 256); write.setAnimData( String.format("0 1 %d %d %d %d %d 0 0 10", quotient, remainder, red, green, blue)); } @@ -288,6 +288,11 @@ public class NanoleafPanelHandler extends BaseThingHandler { return panelID; } + public @Nullable HSBType getColor() { + String panelID = getPanelID(); + return panelInfo.get(panelID); + } + private @Nullable HSBType getPanelColor() { String panelID = getPanelID(); @@ -357,9 +362,9 @@ public class NanoleafPanelHandler extends BaseThingHandler { String[] panelDataPoints = Arrays.copyOfRange(tokenizedData, 2, tokenizedData.length); for (int i = 0; i < panelDataPoints.length; i++) { if (i % 8 == 0) { - String idQuotient = panelDataPoints[i]; - String idRemainder = panelDataPoints[i + 1]; - Integer idNum = Integer.valueOf(idQuotient) * 256 + Integer.valueOf(idRemainder); + Integer idQuotient = Integer.valueOf(panelDataPoints[i]); + Integer idRemainder = Integer.valueOf(panelDataPoints[i + 1]); + Integer idNum = idQuotient * 256 + idRemainder; if (String.valueOf(idNum).equals(panelID)) { // found panel data - store it panelInfo.put(panelID, diff --git a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/DrawingSettings.java b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/DrawingSettings.java new file mode 100644 index 00000000000..be16a3b22af --- /dev/null +++ b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/DrawingSettings.java @@ -0,0 +1,95 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.nanoleaf.internal.layout; + +import java.awt.Color; +import java.util.ArrayList; +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.nanoleaf.internal.NanoleafBindingConstants; + +/** + * Information to the drawing algorithm about which style to use and how to draw. + * + * @author Jørgen Austvik - Initial contribution + */ +@NonNullByDefault +public class DrawingSettings { + + private static final Color COLOR_SIDE = Color.GRAY; + private static final Color COLOR_TEXT = Color.BLACK; + + private final LayoutSettings layoutSettings; + private final int imageHeight; + private final ImagePoint2D min; + private final double rotationRadians; + + public DrawingSettings(LayoutSettings layoutSettings, int imageHeight, ImagePoint2D min, double rotationRadians) { + this.imageHeight = imageHeight; + this.min = min; + this.rotationRadians = rotationRadians; + this.layoutSettings = layoutSettings; + } + + public boolean shouldDrawLabels() { + return layoutSettings.shouldDrawLabels(); + } + + public boolean shouldDrawCorners() { + return layoutSettings.shouldDrawCorners(); + } + + public boolean shouldDrawOutline() { + return layoutSettings.shouldDrawOutline(); + } + + public boolean shouldFillWithColor() { + return layoutSettings.shouldFillWithColor(); + } + + public Color getOutlineColor() { + return COLOR_SIDE; + } + + public Color getLabelColor() { + return COLOR_TEXT; + } + + public ImagePoint2D generateImagePoint(Point2D point) { + return toPictureLayout(point, imageHeight, min, rotationRadians); + } + + public List generateImagePoints(List points) { + return toPictureLayout(points, imageHeight, min, rotationRadians); + } + + private static ImagePoint2D toPictureLayout(Point2D original, int imageHeight, ImagePoint2D min, + double rotationRadians) { + Point2D rotated = original.rotate(rotationRadians); + ImagePoint2D translated = new ImagePoint2D( + NanoleafBindingConstants.LAYOUT_BORDER_WIDTH + rotated.getX() - min.getX(), + imageHeight - NanoleafBindingConstants.LAYOUT_BORDER_WIDTH - rotated.getY() + min.getY()); + return translated; + } + + private static List toPictureLayout(List originals, int imageHeight, ImagePoint2D min, + double rotationRadians) { + List result = new ArrayList<>(originals.size()); + for (Point2D original : originals) { + result.add(toPictureLayout(original, imageHeight, min, rotationRadians)); + } + + return result; + } +} diff --git a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/ImagePoint2D.java b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/ImagePoint2D.java new file mode 100644 index 00000000000..4dcb0060d01 --- /dev/null +++ b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/ImagePoint2D.java @@ -0,0 +1,45 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + +package org.openhab.binding.nanoleaf.internal.layout; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Coordinate in the 2D space of the image. + * + * @author Jørgen Austvik - Initial contribution + */ +@NonNullByDefault +public class ImagePoint2D { + private final int x; + private final int y; + + public ImagePoint2D(int x, int y) { + this.x = x; + this.y = y; + } + + public int getX() { + return x; + } + + public int getY() { + return y; + } + + @Override + public String toString() { + return String.format("image coordinate x:%d, y:%d", x, y); + } +} diff --git a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/LayoutSettings.java b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/LayoutSettings.java new file mode 100644 index 00000000000..2364f5513ff --- /dev/null +++ b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/LayoutSettings.java @@ -0,0 +1,52 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.nanoleaf.internal.layout; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Settigns used for layout. + * + * @author Jørgen Austvik - Initial contribution + */ +@NonNullByDefault +public class LayoutSettings { + + private final boolean drawLabels; + private final boolean drawCorners; + private final boolean drawOutline; + private final boolean fillColor; + + public LayoutSettings(boolean drawLabels, boolean drawCorners, boolean drawOutline, boolean fillColor) { + this.drawLabels = drawLabels; + this.drawCorners = drawCorners; + this.drawOutline = drawOutline; + this.fillColor = fillColor; + } + + public boolean shouldDrawLabels() { + return drawLabels; + } + + public boolean shouldDrawCorners() { + return drawCorners; + } + + public boolean shouldDrawOutline() { + return drawOutline; + } + + public boolean shouldFillWithColor() { + return fillColor; + } +} diff --git a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/NanoleafLayout.java b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/NanoleafLayout.java index 70724333eca..61ceaa5497d 100644 --- a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/NanoleafLayout.java +++ b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/NanoleafLayout.java @@ -15,19 +15,18 @@ package org.openhab.binding.nanoleaf.internal.layout; import java.awt.Color; import java.awt.Graphics2D; +import java.awt.RenderingHints; import java.awt.image.BufferedImage; import java.io.ByteArrayOutputStream; import java.io.IOException; -import java.util.ArrayList; -import java.util.Collection; import java.util.List; import javax.imageio.ImageIO; import org.eclipse.jdt.annotation.NonNullByDefault; import org.openhab.binding.nanoleaf.internal.NanoleafBindingConstants; -import org.openhab.binding.nanoleaf.internal.layout.shape.Shape; -import org.openhab.binding.nanoleaf.internal.layout.shape.ShapeFactory; +import org.openhab.binding.nanoleaf.internal.layout.shape.Panel; +import org.openhab.binding.nanoleaf.internal.layout.shape.PanelFactory; import org.openhab.binding.nanoleaf.internal.model.GlobalOrientation; import org.openhab.binding.nanoleaf.internal.model.Layout; import org.openhab.binding.nanoleaf.internal.model.PanelLayout; @@ -42,11 +41,8 @@ import org.openhab.binding.nanoleaf.internal.model.PositionDatum; public class NanoleafLayout { private static final Color COLOR_BACKGROUND = Color.WHITE; - private static final Color COLOR_PANEL = Color.BLACK; - private static final Color COLOR_SIDE = Color.GRAY; - private static final Color COLOR_TEXT = Color.BLACK; - public static byte[] render(PanelLayout panelLayout) throws IOException { + public static byte[] render(PanelLayout panelLayout, PanelState state, LayoutSettings settings) throws IOException { double rotationRadians = 0; GlobalOrientation globalOrientation = panelLayout.getGlobalOrientation(); if (globalOrientation != null) { @@ -58,78 +54,31 @@ public class NanoleafLayout { return new byte[] {}; } - List panels = layout.getPositionData(); - if (panels == null) { + List positionDatums = layout.getPositionData(); + if (positionDatums == null) { return new byte[] {}; } - Point2D size[] = findSize(panels, rotationRadians); - final Point2D min = size[0]; - final Point2D max = size[1]; - Point2D prev = null; - Point2D first = null; + ImagePoint2D size[] = findSize(positionDatums, rotationRadians); + final ImagePoint2D min = size[0]; + final ImagePoint2D max = size[1]; - int sideCounter = 0; BufferedImage image = new BufferedImage( (max.getX() - min.getX()) + 2 * NanoleafBindingConstants.LAYOUT_BORDER_WIDTH, (max.getY() - min.getY()) + 2 * NanoleafBindingConstants.LAYOUT_BORDER_WIDTH, BufferedImage.TYPE_INT_RGB); Graphics2D g2 = image.createGraphics(); + g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + g2.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); + g2.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_PURE); g2.setBackground(COLOR_BACKGROUND); g2.clearRect(0, 0, image.getWidth(), image.getHeight()); - for (PositionDatum panel : panels) { - final ShapeType shapeType = ShapeType.valueOf(panel.getShapeType()); - - Shape shape = ShapeFactory.CreateShape(shapeType, panel); - List outline = toPictureLayout(shape.generateOutline(), image.getHeight(), min, rotationRadians); - for (int i = 0; i < outline.size(); i++) { - g2.setColor(COLOR_SIDE); - Point2D pos = outline.get(i); - Point2D nextPos = outline.get((i + 1) % outline.size()); - g2.drawLine(pos.getX(), pos.getY(), nextPos.getX(), nextPos.getY()); - } - - for (int i = 0; i < outline.size(); i++) { - Point2D pos = outline.get(i); - g2.setColor(COLOR_PANEL); - g2.fillOval(pos.getX() - NanoleafBindingConstants.LAYOUT_LIGHT_RADIUS / 2, - pos.getY() - NanoleafBindingConstants.LAYOUT_LIGHT_RADIUS / 2, - NanoleafBindingConstants.LAYOUT_LIGHT_RADIUS, NanoleafBindingConstants.LAYOUT_LIGHT_RADIUS); - } - - Point2D current = toPictureLayout(new Point2D(panel.getPosX(), panel.getPosY()), image.getHeight(), min, - rotationRadians); - if (sideCounter == 0) { - first = current; - } - - g2.setColor(COLOR_SIDE); - final int expectedSides = shapeType.getNumSides(); - if (shapeType.getDrawingAlgorithm() == DrawingAlgorithm.CORNER) { - // Special handling of Elements Hexagon Corners, where we get 6 corners instead of 1 shape. They seem to - // come after each other in the JSON, so this algorithm connects them based on the number of sides the - // shape is expected to have. - if (sideCounter > 0 && sideCounter != expectedSides && prev != null) { - g2.drawLine(prev.getX(), prev.getY(), current.getX(), current.getY()); - } - - sideCounter++; - - if (sideCounter == expectedSides && first != null) { - g2.drawLine(current.getX(), current.getY(), first.getX(), first.getY()); - sideCounter = 0; - } - } else { - sideCounter = 0; - } - - prev = current; - - g2.setColor(COLOR_TEXT); - Point2D textPos = shape.labelPosition(g2, outline); - g2.drawString(Integer.toString(panel.getPanelId()), textPos.getX(), textPos.getY()); + DrawingSettings dc = new DrawingSettings(settings, image.getHeight(), min, rotationRadians); + List panels = PanelFactory.createPanels(positionDatums); + for (Panel panel : panels) { + panel.draw(g2, dc, state); } ByteArrayOutputStream out = new ByteArrayOutputStream(); @@ -144,15 +93,14 @@ public class NanoleafLayout { return ((double) (maxValue - value)) * (Math.PI / 180); } - private static Point2D[] findSize(Collection panels, double rotationRadians) { + private static ImagePoint2D[] findSize(List positionDatums, double rotationRadians) { int maxX = 0; int maxY = 0; int minX = 0; int minY = 0; - for (PositionDatum panel : panels) { - ShapeType shapeType = ShapeType.valueOf(panel.getShapeType()); - Shape shape = ShapeFactory.CreateShape(shapeType, panel); + List panels = PanelFactory.createPanels(positionDatums); + for (Panel shape : panels) { for (Point2D point : shape.generateOutline()) { var rotated = point.rotate(rotationRadians); maxX = Math.max(rotated.getX(), maxX); @@ -162,23 +110,6 @@ public class NanoleafLayout { } } - return new Point2D[] { new Point2D(minX, minY), new Point2D(maxX, maxY) }; - } - - private static Point2D toPictureLayout(Point2D original, int imageHeight, Point2D min, double rotationRadians) { - Point2D rotated = original.rotate(rotationRadians); - Point2D translated = new Point2D(NanoleafBindingConstants.LAYOUT_BORDER_WIDTH + rotated.getX() - min.getX(), - imageHeight - NanoleafBindingConstants.LAYOUT_BORDER_WIDTH - rotated.getY() + min.getY()); - return translated; - } - - private static List toPictureLayout(List originals, int imageHeight, Point2D min, - double rotationRadians) { - List result = new ArrayList(originals.size()); - for (Point2D original : originals) { - result.add(toPictureLayout(original, imageHeight, min, rotationRadians)); - } - - return result; + return new ImagePoint2D[] { new ImagePoint2D(minX, minY), new ImagePoint2D(maxX, maxY) }; } } diff --git a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/PanelState.java b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/PanelState.java new file mode 100644 index 00000000000..fba1804fe85 --- /dev/null +++ b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/PanelState.java @@ -0,0 +1,52 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + +package org.openhab.binding.nanoleaf.internal.layout; + +import static org.openhab.binding.nanoleaf.internal.NanoleafBindingConstants.CONFIG_PANEL_ID; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.nanoleaf.internal.handler.NanoleafPanelHandler; +import org.openhab.core.library.types.HSBType; +import org.openhab.core.thing.Thing; + +/** + * Stores the state of the panels. + * + * @author Jørgen Austvik - Initial contribution + */ +@NonNullByDefault +public class PanelState { + + private final Map panelStates = new HashMap<>(); + + public PanelState(List panels) { + for (Thing panel : panels) { + Integer panelId = Integer.valueOf(panel.getConfiguration().get(CONFIG_PANEL_ID).toString()); + NanoleafPanelHandler panelHandler = (NanoleafPanelHandler) panel.getHandler(); + if (panelHandler != null) { + HSBType c = panelHandler.getColor(); + HSBType color = (c == null) ? HSBType.BLACK : c; + panelStates.put(panelId, color); + } + } + } + + public HSBType getHSBForPanel(Integer panelId) { + return panelStates.getOrDefault(panelId, HSBType.BLACK); + } +} diff --git a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/ShapeType.java b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/ShapeType.java index f90262e0e7e..471dafb3622 100644 --- a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/ShapeType.java +++ b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/ShapeType.java @@ -23,35 +23,37 @@ import org.eclipse.jdt.annotation.NonNullByDefault; @NonNullByDefault public enum ShapeType { // side lengths are taken from https://forum.nanoleaf.me/docs chapter 3.3 - UNKNOWN("Unknown", -1, 0, 0, DrawingAlgorithm.NONE), - TRIANGLE("Triangle", 0, 150, 3, DrawingAlgorithm.TRIANGLE), - RHYTHM("Rhythm", 1, 0, 1, DrawingAlgorithm.NONE), - SQUARE("Square", 2, 100, 0, DrawingAlgorithm.SQUARE), - CONTROL_SQUARE_MASTER("Control Square Master", 3, 100, 0, DrawingAlgorithm.SQUARE), - CONTROL_SQUARE_PASSIVE("Control Square Passive", 4, 100, 0, DrawingAlgorithm.SQUARE), - SHAPES_HEXAGON("Hexagon (Shapes)", 7, 67, 6, DrawingAlgorithm.HEXAGON), - SHAPES_TRIANGLE("Triangle (Shapes)", 8, 134, 3, DrawingAlgorithm.TRIANGLE), - SHAPES_MINI_TRIANGLE("Mini Triangle (Shapes)", 9, 67, 3, DrawingAlgorithm.TRIANGLE), - SHAPES_CONTROLLER("Controller (Shapes)", 12, 0, 0, DrawingAlgorithm.NONE), - ELEMENTS_HEXAGON("Elements Hexagon", 14, 134, 6, DrawingAlgorithm.HEXAGON), - ELEMENTS_HEXAGON_CORNER("Elements Hexagon - Corner", 15, 33.5 / 58, 6, DrawingAlgorithm.CORNER), - LINES_CONNECTOR("Lines Connector", 16, 11, 1, DrawingAlgorithm.LINE), - LIGHT_LINES("Light Lines", 17, 154, 1, DrawingAlgorithm.LINE), - LINES_LINES_SINGLE("Light Lines - Single Sone", 18, 77, 1, DrawingAlgorithm.LINE), - CONTROLLER_CAP("Controller Cap", 19, 11, 0, DrawingAlgorithm.NONE), - POWER_CONNECTOR("Power Connector", 20, 11, 0, DrawingAlgorithm.NONE); + UNKNOWN("Unknown", -1, 0, 0, 1, DrawingAlgorithm.NONE), + TRIANGLE("Triangle", 0, 150, 3, 1, DrawingAlgorithm.TRIANGLE), + RHYTHM("Rhythm", 1, 0, 1, 1, DrawingAlgorithm.NONE), + SQUARE("Square", 2, 100, 0, 1, DrawingAlgorithm.SQUARE), + CONTROL_SQUARE_MASTER("Control Square Master", 3, 100, 0, 1, DrawingAlgorithm.SQUARE), + CONTROL_SQUARE_PASSIVE("Control Square Passive", 4, 100, 0, 1, DrawingAlgorithm.SQUARE), + SHAPES_HEXAGON("Hexagon (Shapes)", 7, 67, 6, 1, DrawingAlgorithm.HEXAGON), + SHAPES_TRIANGLE("Triangle (Shapes)", 8, 134, 3, 1, DrawingAlgorithm.TRIANGLE), + SHAPES_MINI_TRIANGLE("Mini Triangle (Shapes)", 9, 67, 3, 1, DrawingAlgorithm.TRIANGLE), + SHAPES_CONTROLLER("Controller (Shapes)", 12, 0, 0, 1, DrawingAlgorithm.NONE), + ELEMENTS_HEXAGON("Elements Hexagon", 14, 134, 6, 1, DrawingAlgorithm.HEXAGON), + ELEMENTS_HEXAGON_CORNER("Elements Hexagon - Corner", 15, 58, 6, 6, DrawingAlgorithm.CORNER), + LINES_CONNECTOR("Lines Connector", 16, 11, 1, 1, DrawingAlgorithm.LINE), + LIGHT_LINES("Light Lines", 17, 154, 1, 1, DrawingAlgorithm.LINE), + LINES_LINES_SINGLE("Light Lines - Single Sone", 18, 77, 1, 1, DrawingAlgorithm.LINE), + CONTROLLER_CAP("Controller Cap", 19, 11, 0, 1, DrawingAlgorithm.NONE), + POWER_CONNECTOR("Power Connector", 20, 11, 0, 1, DrawingAlgorithm.NONE); private final String name; private final int id; - private final double sideLength; + private final int sideLength; private final int numSides; + private final int numLights; private final DrawingAlgorithm drawingAlgorithm; - ShapeType(String name, int id, double sideLenght, int numSides, DrawingAlgorithm drawingAlgorithm) { + ShapeType(String name, int id, int sideLenght, int numSides, int numLights, DrawingAlgorithm drawingAlgorithm) { this.name = name; this.id = id; this.sideLength = sideLenght; this.numSides = numSides; + this.numLights = numLights; this.drawingAlgorithm = drawingAlgorithm; } @@ -63,7 +65,7 @@ public enum ShapeType { return id; } - public double getSideLength() { + public int getSideLength() { return sideLength; } @@ -71,6 +73,10 @@ public enum ShapeType { return numSides; } + public int getNumLightsPerShape() { + return numLights; + } + public DrawingAlgorithm getDrawingAlgorithm() { return drawingAlgorithm; } diff --git a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/shape/BarycentricTriangleGradient.java b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/shape/BarycentricTriangleGradient.java new file mode 100644 index 00000000000..e04e43fa49f --- /dev/null +++ b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/shape/BarycentricTriangleGradient.java @@ -0,0 +1,152 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.nanoleaf.internal.layout.shape; + +import java.awt.Color; +import java.awt.Paint; +import java.awt.PaintContext; +import java.awt.Rectangle; +import java.awt.RenderingHints; +import java.awt.geom.AffineTransform; +import java.awt.geom.Rectangle2D; +import java.awt.image.ColorModel; +import java.awt.image.DataBufferInt; +import java.awt.image.PackedColorModel; +import java.awt.image.Raster; +import java.awt.image.WritableRaster; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.nanoleaf.internal.layout.ImagePoint2D; + +/** + * Paint for triangles with one color in each corner. Used to make gradients between the colors when + * dividing a hexagon into 6 triangles. + * + * https://codeplea.com/triangular-interpolation is instructive for the math. + * + * Inspired by + * https://github.com/hageldave/JPlotter/blob/9c92731f3b29a2cdb14f3dfdeeed6fffde37eee4/jplotter/src/main/java/hageldave/jplotter/util/BarycentricGradientPaint.java, + * for how to integrate it into Java AWT but kept so simple that I could understand it. It was however far too big to + * use as a dependency. + * + * @author Jørgen Austvik - Initial contribution + */ +@NonNullByDefault +public class BarycentricTriangleGradient implements Paint { + + private final Color color1; + private final Color color2; + private final Color color3; + + private final ImagePoint2D corner1; + private final ImagePoint2D corner2; + private final ImagePoint2D corner3; + + public BarycentricTriangleGradient(ImagePoint2D corner1, Color color1, ImagePoint2D corner2, Color color2, + ImagePoint2D corner3, Color color3) { + this.corner1 = corner1; + this.corner2 = corner2; + this.corner3 = corner3; + this.color1 = color1; + this.color2 = color2; + this.color3 = color3; + } + + @Override + public @Nullable PaintContext createContext(@Nullable ColorModel cm, @Nullable Rectangle deviceBounds, + @Nullable Rectangle2D userBounds, @Nullable AffineTransform xform, @Nullable RenderingHints hints) { + return new BarycentricTriangleGradientContext(corner1, color1, corner2, color2, corner3, color3); + } + + @Override + public int getTransparency() { + return OPAQUE; + } + + private class BarycentricTriangleGradientContext implements PaintContext { + + private final Color color1; + private final Color color2; + private final Color color3; + + private final ImagePoint2D corner1; + private final ImagePoint2D corner2; + private final ImagePoint2D corner3; + + private final PackedColorModel colorModel = (PackedColorModel) ColorModel.getRGBdefault(); + + public BarycentricTriangleGradientContext(ImagePoint2D corner1, Color color1, ImagePoint2D corner2, + Color color2, ImagePoint2D corner3, Color color3) { + this.corner1 = corner1; + this.corner2 = corner2; + this.corner3 = corner3; + this.color1 = color1; + this.color2 = color2; + this.color3 = color3; + } + + @Override + public void dispose() { + } + + @Override + public @Nullable ColorModel getColorModel() { + return colorModel; + } + + @Override + public Raster getRaster(int x, int y, int w, int h) { + int[] data = new int[h * w]; + DataBufferInt buffer = new DataBufferInt(data, w * h); + WritableRaster raster = Raster.createPackedRaster(buffer, w, h, w, colorModel.getMasks(), null); + + float denominator = 1f / (((corner2.getY() - corner3.getY()) * (corner1.getX() - corner3.getX())) + + ((corner3.getX() - corner2.getX()) * (corner1.getY() - corner3.getY()))); + + for (int yPos = 0; yPos < h; yPos++) { + int imageY = y + yPos; + for (int xPos = 0; xPos < w; xPos++) { + int imageX = xPos + x; + + float weight1 = (((corner2.getY() - corner3.getY()) * (imageX - corner3.getX())) + + ((corner3.getX() - corner2.getX()) * (imageY - corner3.getY()))) * denominator; + float weight2 = (((corner3.getY() - corner1.getY()) * (imageX - corner3.getX())) + + ((corner1.getX() - corner3.getX()) * (imageY - corner3.getY()))) * denominator; + float weight3 = 1 - weight1 - weight2; + + if (weight1 < 0 || weight2 < 0 || weight3 < 0) { + // Outside of triangle + data[yPos * w + xPos] = 0; + } else { + Color c = mergeColors(weight1, color1, weight2, color2, weight3, color3); + data[yPos * w + xPos] = c.getRGB(); + } + } + } + + return raster; + } + + private Color mergeColors(float weight1, Color color1, float weight2, Color color2, float weight3, + Color color3) { + float normalize = 1f / (weight1 + weight2 + weight3); + float r = (color1.getRed() * weight1 + color2.getRed() * weight2 + color3.getRed() * weight3) * normalize; + float g = (color1.getGreen() * weight1 + color2.getGreen() * weight2 + color3.getGreen() * weight3) + * normalize; + float b = (color1.getBlue() * weight1 + color2.getBlue() * weight2 + color3.getBlue() * weight3) + * normalize; + return new Color((int) r, (int) g, (int) b); + } + } +} diff --git a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/shape/Hexagon.java b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/shape/Hexagon.java index a292335342d..762acb5c852 100644 --- a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/shape/Hexagon.java +++ b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/shape/Hexagon.java @@ -18,6 +18,7 @@ import java.util.Arrays; import java.util.List; import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.nanoleaf.internal.layout.ImagePoint2D; import org.openhab.binding.nanoleaf.internal.layout.Point2D; import org.openhab.binding.nanoleaf.internal.layout.ShapeType; @@ -28,6 +29,7 @@ import org.openhab.binding.nanoleaf.internal.layout.ShapeType; */ @NonNullByDefault public class Hexagon extends Shape { + public Hexagon(ShapeType shapeType, int panelId, Point2D position, int orientation) { super(shapeType, panelId, position, orientation); } @@ -45,12 +47,12 @@ public class Hexagon extends Shape { } @Override - public Point2D labelPosition(Graphics2D graphics, List outline) { + protected ImagePoint2D labelPosition(Graphics2D graphics, List outline) { Point2D[] bounds = findBounds(outline); int midX = bounds[0].getX() + (bounds[1].getX() - bounds[0].getX()) / 2; int midY = bounds[0].getY() + (bounds[1].getY() - bounds[0].getY()) / 2; Rectangle2D rect = graphics.getFontMetrics().getStringBounds(Integer.toString(getPanelId()), graphics); - return new Point2D(midX - (int) (rect.getWidth() / 2), midY - (int) (rect.getHeight() / 2)); + return new ImagePoint2D(midX - (int) (rect.getWidth() / 2), midY + (int) (rect.getHeight() / 2)); } } diff --git a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/shape/HexagonCorners.java b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/shape/HexagonCorners.java new file mode 100644 index 00000000000..873333a76b7 --- /dev/null +++ b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/shape/HexagonCorners.java @@ -0,0 +1,152 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.nanoleaf.internal.layout.shape; + +import java.awt.Color; +import java.awt.Graphics2D; +import java.awt.Polygon; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.nanoleaf.internal.layout.DrawingSettings; +import org.openhab.binding.nanoleaf.internal.layout.ImagePoint2D; +import org.openhab.binding.nanoleaf.internal.layout.PanelState; +import org.openhab.binding.nanoleaf.internal.layout.Point2D; +import org.openhab.binding.nanoleaf.internal.layout.ShapeType; +import org.openhab.binding.nanoleaf.internal.model.PositionDatum; +import org.openhab.core.library.types.HSBType; + +/** + * A hexagon shape. + * + * @author Jørgen Austvik - Initial contribution + */ +@NonNullByDefault +public class HexagonCorners extends Panel { + + private static final int CORNER_DIAMETER = 4; + + private final List corners; + + public HexagonCorners(ShapeType shapeType, List corners) { + super(shapeType); + + this.corners = Collections.unmodifiableList(new ArrayList<>(corners)); + } + + @Override + public List generateOutline() { + List result = new ArrayList<>(corners.size()); + for (PositionDatum corner : corners) { + result.add(new Point2D(corner.getPosX(), corner.getPosY())); + } + + return result; + } + + @Override + public void draw(Graphics2D graphics, DrawingSettings settings, PanelState state) { + List outline = settings.generateImagePoints(generateOutline()); + Polygon p = new Polygon(); + for (int i = 0; i < outline.size(); i++) { + ImagePoint2D pos = outline.get(i); + p.addPoint(pos.getX(), pos.getY()); + } + + if (settings.shouldFillWithColor()) { + Color averageColor = getAverageColor(state); + graphics.setColor(averageColor); + graphics.fillPolygon(p); + + // Draw color cradient + ImagePoint2D center = findCenter(outline); + for (int i = 0; i < outline.size(); i++) { + ImagePoint2D corner1Pos = outline.get(i); + ImagePoint2D corner2Pos = outline.get((i + 1) % outline.size()); + + PositionDatum corner1 = corners.get(i); + PositionDatum corner2 = corners.get((i + 1) % outline.size()); + + Color corner1Color = getColor(corner1.getPanelId(), state); + Color corner2Color = getColor(corner2.getPanelId(), state); + graphics.setPaint(new BarycentricTriangleGradient( + new ImagePoint2D(corner1Pos.getX(), corner1Pos.getY()), corner1Color, + new ImagePoint2D(corner2Pos.getX(), corner2Pos.getY()), corner2Color, center, averageColor)); + + Polygon wedge = new Polygon(); + wedge.addPoint(corner1Pos.getX(), corner1Pos.getY()); + wedge.addPoint(corner2Pos.getX(), corner2Pos.getY()); + wedge.addPoint(center.getX(), center.getY()); + graphics.fillPolygon(p); + } + } + + if (settings.shouldDrawOutline()) { + graphics.setColor(settings.getOutlineColor()); + graphics.drawPolygon(p); + } + + if (settings.shouldDrawCorners()) { + for (PositionDatum corner : corners) { + ImagePoint2D position = settings.generateImagePoint(new Point2D(corner.getPosX(), corner.getPosY())); + graphics.setColor(getColor(corner.getPanelId(), state)); + graphics.fillOval(position.getX() - CORNER_DIAMETER / 2, position.getY() - CORNER_DIAMETER / 2, + CORNER_DIAMETER, CORNER_DIAMETER); + + if (settings.shouldDrawOutline()) { + graphics.setColor(settings.getOutlineColor()); + graphics.drawOval(position.getX() - CORNER_DIAMETER / 2, position.getY() - CORNER_DIAMETER / 2, + CORNER_DIAMETER, CORNER_DIAMETER); + } + } + } + + if (settings.shouldDrawLabels()) { + graphics.setColor(settings.getLabelColor()); + + for (PositionDatum corner : corners) { + ImagePoint2D position = settings.generateImagePoint(new Point2D(corner.getPosX(), corner.getPosY())); + graphics.drawString(Integer.toString(corner.getPanelId()), position.getX(), position.getY()); + } + } + } + + private ImagePoint2D findCenter(List outline) { + Point2D[] bounds = findBounds(outline); + int midX = bounds[0].getX() + (bounds[1].getX() - bounds[0].getX()) / 2; + int midY = bounds[0].getY() + (bounds[1].getY() - bounds[0].getY()) / 2; + return new ImagePoint2D(midX, midY); + } + + private static Color getColor(int panelId, PanelState state) { + HSBType color = state.getHSBForPanel(panelId); + return new Color(color.getRGB()); + } + + private Color getAverageColor(PanelState state) { + float r = 0; + float g = 0; + float b = 0; + for (PositionDatum corner : corners) { + Color c = getColor(corner.getPanelId(), state); + r += c.getRed() * c.getRed(); + g += c.getGreen() * c.getGreen(); + b += c.getBlue() * c.getBlue(); + } + + return new Color((int) Math.sqrt((double) r / corners.size()), (int) Math.sqrt((double) g / corners.size()), + (int) Math.sqrt((double) b / corners.size())); + } +} diff --git a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/shape/Panel.java b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/shape/Panel.java new file mode 100644 index 00000000000..ef831425ad4 --- /dev/null +++ b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/shape/Panel.java @@ -0,0 +1,79 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.nanoleaf.internal.layout.shape; + +import java.awt.Graphics2D; +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.nanoleaf.internal.layout.DrawingSettings; +import org.openhab.binding.nanoleaf.internal.layout.ImagePoint2D; +import org.openhab.binding.nanoleaf.internal.layout.PanelState; +import org.openhab.binding.nanoleaf.internal.layout.Point2D; +import org.openhab.binding.nanoleaf.internal.layout.ShapeType; + +/** + * Panel is a physical piece of plastic you place on the wall and connect to other panels. + * + * @author Jørgen Austvik - Initial contribution + */ +@NonNullByDefault +public abstract class Panel { + private final ShapeType shapeType; + + public Panel(ShapeType shapeType) { + this.shapeType = shapeType; + } + + public ShapeType getShapeType() { + return shapeType; + } + + /** + * Calculates the minimal bounding rectangle around an outline. + * + * @param outline The outline to find the minimal bounding rectangle around + * @return The opposite points of the minimum bounding rectangle around this shape. + */ + public Point2D[] findBounds(List outline) { + int minX = Integer.MAX_VALUE; + int minY = Integer.MAX_VALUE; + int maxX = Integer.MIN_VALUE; + int maxY = Integer.MIN_VALUE; + + for (ImagePoint2D point : outline) { + maxX = Math.max(point.getX(), maxX); + maxY = Math.max(point.getY(), maxY); + minX = Math.min(point.getX(), minX); + minY = Math.min(point.getY(), minY); + } + + return new Point2D[] { new Point2D(minX, minY), new Point2D(maxX, maxY) }; + } + + /** + * Generate the outline of the shape. + * + * @return The points that make up this shape. + */ + public abstract List generateOutline(); + + /** + * Draws the shape on the the supplied graphics. + * + * @param graphics The picture to draw on + * @param settings Information on how to draw + * @param state The state of the panels to draw + */ + public abstract void draw(Graphics2D graphics, DrawingSettings settings, PanelState state); +} diff --git a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/shape/PanelFactory.java b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/shape/PanelFactory.java new file mode 100644 index 00000000000..ec15a0f6743 --- /dev/null +++ b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/shape/PanelFactory.java @@ -0,0 +1,93 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.nanoleaf.internal.layout.shape; + +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Deque; +import java.util.List; +import java.util.Queue; + +import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.nanoleaf.internal.layout.Point2D; +import org.openhab.binding.nanoleaf.internal.layout.ShapeType; +import org.openhab.binding.nanoleaf.internal.model.PositionDatum; + +/** + * Create the correct chape for a given shape type. + * + * @author Jørgen Austvik - Initial contribution + */ +@NonNullByDefault +public class PanelFactory { + + public static List createPanels(List panels) { + List result = new ArrayList<>(panels.size()); + Deque panelStack = new ArrayDeque<>(panels); + while (!panelStack.isEmpty()) { + PositionDatum panel = panelStack.peek(); + final ShapeType shapeType = ShapeType.valueOf(panel.getShapeType()); + Panel shape = createPanel(shapeType, takeFirst(shapeType.getNumLightsPerShape(), panelStack)); + result.add(shape); + } + + return result; + } + + /** + * Return the first n elements from the stack. + * + * @param n The number of elements to return + * @param stack The stack top get elements from + * @return The first n elements of the stack. + */ + private static <@NonNull T> List<@NonNull T> takeFirst(int n, Queue queue) { + List result = new ArrayList<>(n); + for (int i = 0; i < n; i++) { + var res = queue.poll(); + if (res != null) { + result.add(res); + } + } + + return result; + } + + private static Panel createPanel(ShapeType shapeType, List positionDatum) { + switch (shapeType.getDrawingAlgorithm()) { + case SQUARE: + PositionDatum squareShape = positionDatum.get(0); + Point2D pos1 = new Point2D(squareShape.getPosX(), squareShape.getPosY()); + return new Square(shapeType, squareShape.getPanelId(), pos1, squareShape.getOrientation()); + + case TRIANGLE: + PositionDatum triangleShape = positionDatum.get(0); + Point2D pos2 = new Point2D(triangleShape.getPosX(), triangleShape.getPosY()); + return new Triangle(shapeType, triangleShape.getPanelId(), pos2, triangleShape.getOrientation()); + + case HEXAGON: + PositionDatum hexShape = positionDatum.get(0); + Point2D pos3 = new Point2D(hexShape.getPosX(), hexShape.getPosY()); + return new Hexagon(shapeType, hexShape.getPanelId(), pos3, hexShape.getOrientation()); + + case CORNER: + return new HexagonCorners(shapeType, positionDatum); + + default: + PositionDatum shape = positionDatum.get(0); + Point2D pos4 = new Point2D(shape.getPosX(), shape.getPosY()); + return new Point(shapeType, shape.getPanelId(), pos4); + } + } +} diff --git a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/shape/Point.java b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/shape/Point.java index 0ae05dc2b55..e6600bcd908 100644 --- a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/shape/Point.java +++ b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/shape/Point.java @@ -12,13 +12,18 @@ */ package org.openhab.binding.nanoleaf.internal.layout.shape; +import java.awt.Color; import java.awt.Graphics2D; import java.util.Arrays; import java.util.List; import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.nanoleaf.internal.layout.DrawingSettings; +import org.openhab.binding.nanoleaf.internal.layout.ImagePoint2D; +import org.openhab.binding.nanoleaf.internal.layout.PanelState; import org.openhab.binding.nanoleaf.internal.layout.Point2D; import org.openhab.binding.nanoleaf.internal.layout.ShapeType; +import org.openhab.core.library.types.HSBType; /** * A shape without any area. @@ -26,18 +31,42 @@ import org.openhab.binding.nanoleaf.internal.layout.ShapeType; * @author Jørgen Austvik - Initial contribution */ @NonNullByDefault -public class Point extends Shape { - public Point(ShapeType shapeType, int panelId, Point2D position, int orientation) { - super(shapeType, panelId, position, orientation); +public class Point extends Panel { + + private static final int POINT_DIAMETER = 4; + + private final Point2D position; + private final int panelId; + + public Point(ShapeType shapeType, int panelId, Point2D position) { + super(shapeType); + this.position = position; + this.panelId = panelId; } @Override public List generateOutline() { - return Arrays.asList(getPosition()); + return Arrays.asList(position); } @Override - public Point2D labelPosition(Graphics2D graphics, List outline) { - return outline.get(0); + public void draw(Graphics2D graphics, DrawingSettings settings, PanelState state) { + ImagePoint2D pos = settings.generateImagePoint(position); + + if (settings.shouldFillWithColor()) { + HSBType color = state.getHSBForPanel(panelId); + graphics.setColor(new Color(color.getRGB())); + graphics.fillOval(pos.getX(), pos.getY(), POINT_DIAMETER, POINT_DIAMETER); + } + + if (settings.shouldDrawOutline()) { + graphics.setColor(settings.getOutlineColor()); + graphics.drawOval(pos.getX(), pos.getY(), POINT_DIAMETER, POINT_DIAMETER); + } + + if (settings.shouldDrawLabels()) { + graphics.setColor(settings.getLabelColor()); + graphics.drawString(Integer.toString(panelId), pos.getX(), pos.getY()); + } } } diff --git a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/shape/Shape.java b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/shape/Shape.java index 99412ba2ea6..5e12c7bee26 100644 --- a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/shape/Shape.java +++ b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/shape/Shape.java @@ -12,36 +12,38 @@ */ package org.openhab.binding.nanoleaf.internal.layout.shape; +import java.awt.Color; import java.awt.Graphics2D; +import java.awt.Polygon; import java.util.List; import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.nanoleaf.internal.layout.DrawingSettings; +import org.openhab.binding.nanoleaf.internal.layout.ImagePoint2D; +import org.openhab.binding.nanoleaf.internal.layout.PanelState; import org.openhab.binding.nanoleaf.internal.layout.Point2D; import org.openhab.binding.nanoleaf.internal.layout.ShapeType; +import org.openhab.core.library.types.HSBType; /** - * Shape that can be drawn. + * Draws shapes, which are panels with a single LED. * * @author Jørgen Austvik - Initial contribution */ @NonNullByDefault -public abstract class Shape { - private final ShapeType shapeType; - private final int panelId; +public abstract class Shape extends Panel { + private final Point2D position; private final int orientation; + private final int panelId; public Shape(ShapeType shapeType, int panelId, Point2D position, int orientation) { - this.shapeType = shapeType; - this.panelId = panelId; + super(shapeType); this.position = position; this.orientation = orientation; + this.panelId = panelId; } - public int getPanelId() { - return panelId; - }; - public Point2D getPosition() { return position; } @@ -50,36 +52,45 @@ public abstract class Shape { return orientation; }; - public ShapeType getShapeType() { - return shapeType; + protected int getPanelId() { + return panelId; } - /** - * @return The opposite points of the minimum bounding rectangle around this shape. - */ - public Point2D[] findBounds(List outline) { - int minX = Integer.MAX_VALUE; - int minY = Integer.MAX_VALUE; - int maxX = Integer.MIN_VALUE; - int maxY = Integer.MIN_VALUE; - - for (Point2D point : outline) { - maxX = Math.max(point.getX(), maxX); - maxY = Math.max(point.getY(), maxY); - minX = Math.min(point.getX(), minX); - minY = Math.min(point.getY(), minY); - } - - return new Point2D[] { new Point2D(minX, minY), new Point2D(maxX, maxY) }; - } - - /** - * @return The points that make up this shape. - */ + @Override public abstract List generateOutline(); /** + * @param graphics The picture to draw on + * @param outline Outline of the shape to draw inside * @return The position where the label of the shape should be placed */ - public abstract Point2D labelPosition(Graphics2D graphics, List outline); + protected abstract ImagePoint2D labelPosition(Graphics2D graphics, List outline); + + @Override + public void draw(Graphics2D graphics, DrawingSettings settings, PanelState state) { + List outline = settings.generateImagePoints(generateOutline()); + + Polygon p = new Polygon(); + for (int i = 0; i < outline.size(); i++) { + ImagePoint2D pos = outline.get(i); + p.addPoint(pos.getX(), pos.getY()); + } + + HSBType color = state.getHSBForPanel(getPanelId()); + graphics.setColor(new Color(color.getRGB())); + if (settings.shouldFillWithColor()) { + graphics.fillPolygon(p); + } + + if (settings.shouldDrawOutline()) { + graphics.setColor(settings.getOutlineColor()); + graphics.drawPolygon(p); + } + + if (settings.shouldDrawLabels()) { + graphics.setColor(settings.getLabelColor()); + ImagePoint2D textPos = labelPosition(graphics, outline); + graphics.drawString(Integer.toString(getPanelId()), textPos.getX(), textPos.getY()); + } + } } diff --git a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/shape/ShapeFactory.java b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/shape/ShapeFactory.java deleted file mode 100644 index 78e9ec08828..00000000000 --- a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/shape/ShapeFactory.java +++ /dev/null @@ -1,44 +0,0 @@ -/** - * Copyright (c) 2010-2022 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.binding.nanoleaf.internal.layout.shape; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.binding.nanoleaf.internal.layout.Point2D; -import org.openhab.binding.nanoleaf.internal.layout.ShapeType; -import org.openhab.binding.nanoleaf.internal.model.PositionDatum; - -/** - * Create the correct chape for a given shape type. - * - * @author Jørgen Austvik - Initial contribution - */ -@NonNullByDefault -public class ShapeFactory { - - public static Shape CreateShape(ShapeType shapeType, PositionDatum positionDatum) { - Point2D pos = new Point2D(positionDatum.getPosX(), positionDatum.getPosY()); - switch (shapeType.getDrawingAlgorithm()) { - case SQUARE: - return new Square(shapeType, positionDatum.getPanelId(), pos, positionDatum.getOrientation()); - - case TRIANGLE: - return new Triangle(shapeType, positionDatum.getPanelId(), pos, positionDatum.getOrientation()); - - case HEXAGON: - return new Hexagon(shapeType, positionDatum.getPanelId(), pos, positionDatum.getOrientation()); - - default: - return new Point(shapeType, positionDatum.getPanelId(), pos, positionDatum.getOrientation()); - } - } -} diff --git a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/shape/Square.java b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/shape/Square.java index a9cf762843d..50c3cd8a9f7 100644 --- a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/shape/Square.java +++ b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/shape/Square.java @@ -18,6 +18,7 @@ import java.util.Arrays; import java.util.List; import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.nanoleaf.internal.layout.ImagePoint2D; import org.openhab.binding.nanoleaf.internal.layout.Point2D; import org.openhab.binding.nanoleaf.internal.layout.ShapeType; @@ -44,14 +45,14 @@ public class Square extends Shape { } @Override - public Point2D labelPosition(Graphics2D graphics, List outline) { + protected ImagePoint2D labelPosition(Graphics2D graphics, List outline) { // Center of square is average of oposite corners - Point2D p0 = outline.get(0); - Point2D p2 = outline.get(2); + ImagePoint2D p0 = outline.get(0); + ImagePoint2D p2 = outline.get(2); Rectangle2D rect = graphics.getFontMetrics().getStringBounds(Integer.toString(getPanelId()), graphics); - return new Point2D((p0.getX() + p2.getX()) / 2 - (int) (rect.getWidth() / 2), - (p0.getY() + p2.getY()) / 2 - (int) (rect.getHeight() / 2)); + return new ImagePoint2D((p0.getX() + p2.getX()) / 2 - (int) (rect.getWidth() / 2), + (p0.getY() + p2.getY()) / 2 + (int) (rect.getHeight() / 2)); } } diff --git a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/shape/Triangle.java b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/shape/Triangle.java index 586e89fc6e7..4bf64e0de8d 100644 --- a/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/shape/Triangle.java +++ b/bundles/org.openhab.binding.nanoleaf/src/main/java/org/openhab/binding/nanoleaf/internal/layout/shape/Triangle.java @@ -18,6 +18,7 @@ import java.util.Arrays; import java.util.List; import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.nanoleaf.internal.layout.ImagePoint2D; import org.openhab.binding.nanoleaf.internal.layout.Point2D; import org.openhab.binding.nanoleaf.internal.layout.ShapeType; @@ -28,6 +29,7 @@ import org.openhab.binding.nanoleaf.internal.layout.ShapeType; */ @NonNullByDefault public class Triangle extends Shape { + public Triangle(ShapeType shapeType, int panelId, Point2D position, int orientation) { super(shapeType, panelId, position, orientation); } @@ -48,13 +50,13 @@ public class Triangle extends Shape { } @Override - public Point2D labelPosition(Graphics2D graphics, List outline) { - Point2D[] bounds = findBounds(outline); - int midX = bounds[0].getX() + (bounds[1].getX() - bounds[0].getX()) / 2; - int midY = bounds[0].getY() + (bounds[1].getY() - bounds[0].getY()) / 2; + protected ImagePoint2D labelPosition(Graphics2D graphics, List outline) { + Point2D centroid = new Point2D((outline.get(0).getX() + outline.get(1).getX() + outline.get(2).getX()) / 3, + (outline.get(0).getY() + outline.get(1).getY() + outline.get(2).getY()) / 3); Rectangle2D rect = graphics.getFontMetrics().getStringBounds(Integer.toString(getPanelId()), graphics); - return new Point2D(midX - (int) (rect.getWidth() / 2), midY - (int) (rect.getHeight() / 2)); + return new ImagePoint2D(centroid.getX() - (int) (rect.getWidth() / 2), + centroid.getY() + (int) (rect.getHeight() / 2)); } private boolean pointsUp() { diff --git a/bundles/org.openhab.binding.nanoleaf/src/main/resources/OH-INF/i18n/nanoleaf.properties b/bundles/org.openhab.binding.nanoleaf/src/main/resources/OH-INF/i18n/nanoleaf.properties index e3ee3da9dbd..a730c5089d6 100644 --- a/bundles/org.openhab.binding.nanoleaf/src/main/resources/OH-INF/i18n/nanoleaf.properties +++ b/bundles/org.openhab.binding.nanoleaf/src/main/resources/OH-INF/i18n/nanoleaf.properties @@ -40,6 +40,8 @@ channel-type.nanoleaf.swipe.label = Swipe channel-type.nanoleaf.swipe.description = Swipe over the panels channel-type.nanoleaf.layout.label = Layout channel-type.nanoleaf.layout.description = Layout of the panels +channel-type.nanoleaf.state.label = State +channel-type.nanoleaf.state.description = Current state of the panels # error messages error.nanoleaf.controller.noIp = IP/host address and/or port are not configured for the controller. diff --git a/bundles/org.openhab.binding.nanoleaf/src/main/resources/OH-INF/thing/lightpanels.xml b/bundles/org.openhab.binding.nanoleaf/src/main/resources/OH-INF/thing/lightpanels.xml index afc9142ad9e..d5ac6ea23e5 100644 --- a/bundles/org.openhab.binding.nanoleaf/src/main/resources/OH-INF/thing/lightpanels.xml +++ b/bundles/org.openhab.binding.nanoleaf/src/main/resources/OH-INF/thing/lightpanels.xml @@ -19,6 +19,7 @@ + @@ -114,4 +115,10 @@ @text/channel-type.nanoleaf.layout.description + + Image + + @text/channel-type.nanoleaf.state.description + + diff --git a/bundles/org.openhab.binding.nanoleaf/src/test/java/org/openhab/binding/nanoleaf/internal/layout/NanoleafLayoutTest.java b/bundles/org.openhab.binding.nanoleaf/src/test/java/org/openhab/binding/nanoleaf/internal/layout/NanoleafLayoutTest.java new file mode 100644 index 00000000000..c6344abd54d --- /dev/null +++ b/bundles/org.openhab.binding.nanoleaf/src/test/java/org/openhab/binding/nanoleaf/internal/layout/NanoleafLayoutTest.java @@ -0,0 +1,92 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.nanoleaf.internal.layout; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.nio.charset.Charset; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.attribute.FileAttribute; +import java.nio.file.attribute.PosixFilePermission; +import java.nio.file.attribute.PosixFilePermissions; +import java.util.Collections; +import java.util.Set; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.junit.jupiter.api.io.TempDir; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.openhab.binding.nanoleaf.internal.model.ControllerInfo; +import org.openhab.binding.nanoleaf.internal.model.PanelLayout; +import org.openhab.core.library.types.HSBType; + +import com.google.gson.Gson; + +/** + * Test for layout + * + * @author Jørgen Austvik - Initial contribution + */ +@NonNullByDefault +public class NanoleafLayoutTest { + + @TempDir + static @Nullable Path temporaryDirectory; + + @ParameterizedTest + @ValueSource(strings = { "lasvegas.json", "theduck.json", "squares.json", "wings.json", "spaceinvader.json" }) + public void testFile(String fileName) throws Exception { + Path file = Path.of("src/test/resources/", fileName); + assertTrue(Files.exists(file), "File should exist: " + file); + + Gson gson = new Gson(); + ControllerInfo controllerInfo = gson.fromJson(Files.readString(file, Charset.defaultCharset()), + ControllerInfo.class); + assertNotNull(controllerInfo, "File should contain controller info: " + file); + + PanelLayout panelLayout = controllerInfo.getPanelLayout(); + assertNotNull(panelLayout, "The controller info should contain panel layout"); + + LayoutSettings settings = new LayoutSettings(true, true, true, true); + byte[] result = NanoleafLayout.render(panelLayout, new TestPanelState(), settings); + assertNotNull(result, "Should be able to render the layout: " + fileName); + assertTrue(result.length > 0, "Should get content back, but got " + result.length + "bytes"); + + Set permissions = PosixFilePermissions.fromString("rw-r--r--"); + FileAttribute> attributes = PosixFilePermissions.asFileAttribute(permissions); + Path outFile = Files.createTempFile(temporaryDirectory, fileName.replace(".json", ""), ".png", attributes); + Files.write(outFile, result); + + // For inspecting images on own computer + // Path permanentOutFile = Files.createFile(Path.of("/tmp", fileName.replace(".json", "") + ".png"), + // attributes); + // Files.write(permanentOutFile, result); + } + + private class TestPanelState extends PanelState { + private final HSBType testColors[] = { HSBType.fromRGB(160, 120, 40), HSBType.fromRGB(80, 60, 20), + HSBType.fromRGB(120, 90, 30), HSBType.fromRGB(200, 150, 60) }; + + public TestPanelState() { + super(Collections.emptyList()); + } + + @Override + public HSBType getHSBForPanel(Integer panelId) { + return testColors[panelId % testColors.length]; + } + } +} diff --git a/bundles/org.openhab.binding.nanoleaf/src/test/resources/lasvegas.json b/bundles/org.openhab.binding.nanoleaf/src/test/resources/lasvegas.json new file mode 100644 index 00000000000..3d8da0de15c --- /dev/null +++ b/bundles/org.openhab.binding.nanoleaf/src/test/resources/lasvegas.json @@ -0,0 +1,788 @@ +{ + "name": "Elements AB01", + "serialNo": "12345", + "manufacturer": "Nanoleaf", + "firmwareVersion": "6.5.1", + "hardwareVersion": "1.1-0", + "model": "NL52", + "discovery": {}, + "effects": { + "effectsList": [ + "Bloom", + "Calming Waterfall", + "Clouds", + "Ember", + "Fireflies", + "Glimmer", + "Sahara Night", + "Slow Glimmer", + "Splash", + "Sunbeam", + "Warm Waves" + ], + "select": "Slow Glimmer" + }, + "firmwareUpgrade": {}, + "panelLayout": { + "globalOrientation": { + "value": 235, + "max": 360, + "min": 0 + }, + "layout": { + "numPanels": 103, + "sideLength": 67, + "positionData": [ + { + "panelId": 26651, + "x": 159, + "y": 224, + "o": 360, + "shapeType": 15 + }, + { + "panelId": 63706, + "x": 134, + "y": 181, + "o": 300, + "shapeType": 15 + }, + { + "panelId": 51864, + "x": 84, + "y": 181, + "o": 240, + "shapeType": 15 + }, + { + "panelId": 23129, + "x": 59, + "y": 224, + "o": 180, + "shapeType": 15 + }, + { + "panelId": 43801, + "x": 84, + "y": 268, + "o": 120, + "shapeType": 15 + }, + { + "panelId": 15320, + "x": 134, + "y": 268, + "o": 60, + "shapeType": 15 + }, + { + "panelId": 62298, + "x": 185, + "y": 210, + "o": 480, + "shapeType": 15 + }, + { + "panelId": 25499, + "x": 235, + "y": 210, + "o": 420, + "shapeType": 15 + }, + { + "panelId": 37595, + "x": 260, + "y": 166, + "o": 360, + "shapeType": 15 + }, + { + "panelId": 538, + "x": 235, + "y": 123, + "o": 300, + "shapeType": 15 + }, + { + "panelId": 12376, + "x": 185, + "y": 123, + "o": 240, + "shapeType": 15 + }, + { + "panelId": 41113, + "x": 159, + "y": 166, + "o": 180, + "shapeType": 15 + }, + { + "panelId": 41368, + "x": 285, + "y": 181, + "o": 600, + "shapeType": 15 + }, + { + "panelId": 37850, + "x": 260, + "y": 224, + "o": 540, + "shapeType": 15 + }, + { + "panelId": 795, + "x": 285, + "y": 268, + "o": 480, + "shapeType": 15 + }, + { + "panelId": 62043, + "x": 335, + "y": 268, + "o": 420, + "shapeType": 15 + }, + { + "panelId": 25242, + "x": 360, + "y": 224, + "o": 360, + "shapeType": 15 + }, + { + "panelId": 38623, + "x": 335, + "y": 181, + "o": 300, + "shapeType": 15 + }, + { + "panelId": 45271, + "x": 360, + "y": 282, + "o": 540, + "shapeType": 15 + }, + { + "panelId": 8214, + "x": 386, + "y": 326, + "o": 480, + "shapeType": 15 + }, + { + "panelId": 53590, + "x": 436, + "y": 326, + "o": 420, + "shapeType": 15 + }, + { + "panelId": 16791, + "x": 461, + "y": 282, + "o": 360, + "shapeType": 15 + }, + { + "panelId": 31199, + "x": 436, + "y": 239, + "o": 300, + "shapeType": 15 + }, + { + "panelId": 59678, + "x": 386, + "y": 239, + "o": 240, + "shapeType": 15 + }, + { + "panelId": 778, + "x": 386, + "y": 355, + "o": 600, + "shapeType": 15 + }, + { + "panelId": 37835, + "x": 360, + "y": 398, + "o": 540, + "shapeType": 15 + }, + { + "panelId": 25227, + "x": 386, + "y": 442, + "o": 480, + "shapeType": 15 + }, + { + "panelId": 62026, + "x": 436, + "y": 442, + "o": 420, + "shapeType": 15 + }, + { + "panelId": 1551, + "x": 461, + "y": 398, + "o": 360, + "shapeType": 15 + }, + { + "panelId": 38606, + "x": 436, + "y": 355, + "o": 300, + "shapeType": 15 + }, + { + "panelId": 11722, + "x": 335, + "y": 413, + "o": 660, + "shapeType": 15 + }, + { + "panelId": 48395, + "x": 285, + "y": 413, + "o": 600, + "shapeType": 15 + }, + { + "panelId": 19531, + "x": 260, + "y": 456, + "o": 540, + "shapeType": 15 + }, + { + "panelId": 56458, + "x": 285, + "y": 500, + "o": 480, + "shapeType": 15 + }, + { + "panelId": 61128, + "x": 335, + "y": 500, + "o": 420, + "shapeType": 15 + }, + { + "panelId": 32265, + "x": 360, + "y": 456, + "o": 360, + "shapeType": 15 + }, + { + "panelId": 63667, + "x": 185, + "y": 558, + "o": 480, + "shapeType": 15 + }, + { + "panelId": 26738, + "x": 235, + "y": 558, + "o": 420, + "shapeType": 15 + }, + { + "panelId": 39218, + "x": 260, + "y": 514, + "o": 360, + "shapeType": 15 + }, + { + "panelId": 2547, + "x": 235, + "y": 471, + "o": 300, + "shapeType": 15 + }, + { + "panelId": 15281, + "x": 185, + "y": 471, + "o": 240, + "shapeType": 15 + }, + { + "panelId": 43888, + "x": 159, + "y": 514, + "o": 180, + "shapeType": 15 + }, + { + "panelId": 2795, + "x": 185, + "y": 587, + "o": 600, + "shapeType": 15 + }, + { + "panelId": 64427, + "x": 159, + "y": 630, + "o": 540, + "shapeType": 15 + }, + { + "panelId": 27498, + "x": 185, + "y": 674, + "o": 480, + "shapeType": 15 + }, + { + "panelId": 22824, + "x": 235, + "y": 674, + "o": 420, + "shapeType": 15 + }, + { + "panelId": 51689, + "x": 260, + "y": 630, + "o": 360, + "shapeType": 15 + }, + { + "panelId": 14505, + "x": 235, + "y": 587, + "o": 300, + "shapeType": 15 + }, + { + "panelId": 32057, + "x": 360, + "y": 514, + "o": 540, + "shapeType": 15 + }, + { + "panelId": 35961, + "x": 386, + "y": 558, + "o": 480, + "shapeType": 15 + }, + { + "panelId": 7352, + "x": 436, + "y": 558, + "o": 420, + "shapeType": 15 + }, + { + "panelId": 12026, + "x": 461, + "y": 514, + "o": 360, + "shapeType": 15 + }, + { + "panelId": 48699, + "x": 436, + "y": 471, + "o": 300, + "shapeType": 15 + }, + { + "panelId": 20347, + "x": 386, + "y": 471, + "o": 240, + "shapeType": 15 + }, + { + "panelId": 17188, + "x": 436, + "y": 674, + "o": 420, + "shapeType": 15 + }, + { + "panelId": 31596, + "x": 461, + "y": 630, + "o": 360, + "shapeType": 15 + }, + { + "panelId": 60333, + "x": 436, + "y": 587, + "o": 300, + "shapeType": 15 + }, + { + "panelId": 6893, + "x": 386, + "y": 587, + "o": 240, + "shapeType": 15 + }, + { + "panelId": 35372, + "x": 360, + "y": 630, + "o": 180, + "shapeType": 15 + }, + { + "panelId": 47214, + "x": 386, + "y": 674, + "o": 120, + "shapeType": 15 + }, + { + "panelId": 4006, + "x": 285, + "y": 732, + "o": 480, + "shapeType": 15 + }, + { + "panelId": 40807, + "x": 335, + "y": 732, + "o": 420, + "shapeType": 15 + }, + { + "panelId": 44325, + "x": 360, + "y": 688, + "o": 360, + "shapeType": 15 + }, + { + "panelId": 15844, + "x": 335, + "y": 645, + "o": 300, + "shapeType": 15 + }, + { + "panelId": 52388, + "x": 285, + "y": 645, + "o": 240, + "shapeType": 15 + }, + { + "panelId": 23653, + "x": 260, + "y": 688, + "o": 180, + "shapeType": 15 + }, + { + "panelId": 31275, + "x": 461, + "y": 688, + "o": 540, + "shapeType": 15 + }, + { + "panelId": 18537, + "x": 486, + "y": 732, + "o": 480, + "shapeType": 15 + }, + { + "panelId": 55464, + "x": 536, + "y": 732, + "o": 420, + "shapeType": 15 + }, + { + "panelId": 10728, + "x": 561, + "y": 688, + "o": 360, + "shapeType": 15 + }, + { + "panelId": 47401, + "x": 536, + "y": 645, + "o": 300, + "shapeType": 15 + }, + { + "panelId": 22904, + "x": 486, + "y": 645, + "o": 240, + "shapeType": 15 + }, + { + "panelId": 48871, + "x": 461, + "y": 805, + "o": 540, + "shapeType": 15 + }, + { + "panelId": 19106, + "x": 486, + "y": 848, + "o": 480, + "shapeType": 15 + }, + { + "panelId": 55907, + "x": 536, + "y": 848, + "o": 420, + "shapeType": 15 + }, + { + "panelId": 11043, + "x": 561, + "y": 805, + "o": 360, + "shapeType": 15 + }, + { + "panelId": 48098, + "x": 536, + "y": 761, + "o": 300, + "shapeType": 15 + }, + { + "panelId": 35232, + "x": 486, + "y": 761, + "o": 240, + "shapeType": 15 + }, + { + "panelId": 57198, + "x": 561, + "y": 921, + "o": 360, + "shapeType": 15 + }, + { + "panelId": 20399, + "x": 536, + "y": 877, + "o": 300, + "shapeType": 15 + }, + { + "panelId": 32237, + "x": 486, + "y": 877, + "o": 240, + "shapeType": 15 + }, + { + "panelId": 60716, + "x": 461, + "y": 921, + "o": 180, + "shapeType": 15 + }, + { + "panelId": 7276, + "x": 486, + "y": 964, + "o": 120, + "shapeType": 15 + }, + { + "panelId": 36013, + "x": 536, + "y": 964, + "o": 60, + "shapeType": 15 + }, + { + "panelId": 7941, + "x": 486, + "y": 1080, + "o": 480, + "shapeType": 15 + }, + { + "panelId": 36804, + "x": 536, + "y": 1080, + "o": 420, + "shapeType": 15 + }, + { + "panelId": 32388, + "x": 561, + "y": 1037, + "o": 360, + "shapeType": 15 + }, + { + "panelId": 60997, + "x": 536, + "y": 993, + "o": 300, + "shapeType": 15 + }, + { + "panelId": 56327, + "x": 486, + "y": 993, + "o": 240, + "shapeType": 15 + }, + { + "panelId": 19654, + "x": 461, + "y": 1037, + "o": 180, + "shapeType": 15 + }, + { + "panelId": 23647, + "x": 587, + "y": 1051, + "o": 600, + "shapeType": 15 + }, + { + "panelId": 28189, + "x": 561, + "y": 1095, + "o": 540, + "shapeType": 15 + }, + { + "panelId": 65244, + "x": 587, + "y": 1138, + "o": 480, + "shapeType": 15 + }, + { + "panelId": 3996, + "x": 637, + "y": 1138, + "o": 420, + "shapeType": 15 + }, + { + "panelId": 40797, + "x": 662, + "y": 1095, + "o": 360, + "shapeType": 15 + }, + { + "panelId": 27416, + "x": 637, + "y": 1051, + "o": 300, + "shapeType": 15 + }, + { + "panelId": 9035, + "x": 260, + "y": 50, + "o": 360, + "shapeType": 15 + }, + { + "panelId": 53771, + "x": 235, + "y": 7, + "o": 300, + "shapeType": 15 + }, + { + "panelId": 17098, + "x": 185, + "y": 7, + "o": 240, + "shapeType": 15 + }, + { + "panelId": 28808, + "x": 159, + "y": 50, + "o": 180, + "shapeType": 15 + }, + { + "panelId": 57417, + "x": 185, + "y": 94, + "o": 120, + "shapeType": 15 + }, + { + "panelId": 4361, + "x": 235, + "y": 94, + "o": 60, + "shapeType": 15 + }, + { + "panelId": 0, + "x": 50, + "y": 190, + "o": 120, + "shapeType": 12 + } + ] + } + }, + "qkihnokomhartlnp": {}, + "schedules": {}, + "state": { + "brightness": { + "value": 36, + "max": 100, + "min": 0 + }, + "colorMode": "effect", + "ct": { + "value": 3803, + "max": 4000, + "min": 1500 + }, + "hue": { + "value": 0, + "max": 360, + "min": 0 + }, + "on": { + "value": true + }, + "sat": { + "value": 0, + "max": 100, + "min": 0 + } + } +} diff --git a/bundles/org.openhab.binding.nanoleaf/src/test/resources/spaceinvader.json b/bundles/org.openhab.binding.nanoleaf/src/test/resources/spaceinvader.json new file mode 100644 index 00000000000..2376436d193 --- /dev/null +++ b/bundles/org.openhab.binding.nanoleaf/src/test/resources/spaceinvader.json @@ -0,0 +1,152 @@ +{ + "name": "Nanoleaf Light Panels", + "serialNo": "S007", + "manufacturer": "Nanoleaf", + "firmwareVersion": "5.1.0", + "hardwareVersion": "1.6-2", + "model": "NL22", + "cloudHash": {}, + "discovery": {}, + "effects": { + "effectsList": [ + "20 Minute Sunset", + "Color Burst", + "Fireworks", + "Flames", + "Forest", + "Inner Peace", + "Jungle", + "Meteor Shower", + "Nemo", + "Northern Lights", + "Paint Splatter", + "Pulse Pop Beats", + "Rhythmic Northern Lights", + "Ripple", + "Romantic", + "Snowfall", + "Sound Bar", + "Streaking Notes", + "Falling Whites" + ], + "select": "Forest" + }, + "firmwareUpgrade": {}, + "panelLayout": { + "globalOrientation": { + "value": 0, + "max": 360, + "min": 0 + }, + "layout": { + "numPanels": 9, + "sideLength": 150, + "positionData": [ + { + "panelId": 145, + "x": 374, + "y": 43, + "o": 60, + "shapeType": 0 + }, + { + "panelId": 106, + "x": 374, + "y": 129, + "o": 120, + "shapeType": 0 + }, + { + "panelId": 175, + "x": 299, + "y": 173, + "o": 180, + "shapeType": 0 + }, + { + "panelId": 215, + "x": 224, + "y": 129, + "o": 0, + "shapeType": 0 + }, + { + "panelId": 231, + "x": 149, + "y": 173, + "o": 60, + "shapeType": 0 + }, + { + "panelId": 59, + "x": 74, + "y": 129, + "o": 0, + "shapeType": 0 + }, + { + "panelId": 186, + "x": 74, + "y": 43, + "o": 180, + "shapeType": 0 + }, + { + "panelId": 61, + "x": 149, + "y": 259, + "o": 240, + "shapeType": 0 + }, + { + "panelId": 94, + "x": 299, + "y": 259, + "o": 240, + "shapeType": 0 + } + ] + } + }, + "rhythm": { + "auxAvailable": false, + "firmwareVersion": "2.4.3", + "hardwareVersion": "2.0", + "rhythmActive": false, + "rhythmConnected": true, + "rhythmId": 123, + "rhythmMode": 0, + "rhythmPos": { + "x": 0.0, + "y": 0.0, + "o": 240.0 + } + }, + "schedules": {}, + "state": { + "brightness": { + "value": 100, + "max": 100, + "min": 0 + }, + "colorMode": "effect", + "ct": { + "value": 6500, + "max": 6500, + "min": 1200 + }, + "hue": { + "value": 0, + "max": 360, + "min": 0 + }, + "on": { + "value": false + }, + "sat": { + "value": 0, + "max": 100, + "min": 0 + } + } +} diff --git a/bundles/org.openhab.binding.nanoleaf/src/test/resources/squares.json b/bundles/org.openhab.binding.nanoleaf/src/test/resources/squares.json new file mode 100644 index 00000000000..4eec9a489d0 --- /dev/null +++ b/bundles/org.openhab.binding.nanoleaf/src/test/resources/squares.json @@ -0,0 +1,172 @@ +{ + "name": "Canvas Squares", + "serialNo": "S987654321", + "manufacturer": "Nanoleaf", + "firmwareVersion": "6.5.1", + "hardwareVersion": "2.2-4", + "model": "NL29", + "discovery": {}, + "effects": { + "effectsList": [ + "Bedtime", + "Color Burst", + "Falling Whites", + "Fireworks", + "Fireworks and Firecrackers", + "Flames", + "Forest", + "Inner Peace", + "Meteor Shower", + "Nemo", + "Northern Lights", + "Paint Splatter", + "Pulse Pop Beats", + "Radial Sound Bar", + "Rhythmic Northern Lights", + "Romantic", + "Sound Bar", + "Streaking Notes" + ], + "select": "*Solid*" + }, + "firmwareUpgrade": {}, + "panelLayout": { + "globalOrientation": { + "value": 0, + "max": 360, + "min": 0 + }, + "layout": { + "numPanels": 14, + "sideLength": 100, + "positionData": [ + { + "panelId": 12250, + "x": 300, + "y": 0, + "o": 0, + "shapeType": 3 + }, + { + "panelId": 8134, + "x": 300, + "y": 100, + "o": 0, + "shapeType": 2 + }, + { + "panelId": 58086, + "x": 200, + "y": 100, + "o": 270, + "shapeType": 2 + }, + { + "panelId": 38724, + "x": 300, + "y": 200, + "o": 0, + "shapeType": 2 + }, + { + "panelId": 48111, + "x": 200, + "y": 200, + "o": 270, + "shapeType": 2 + }, + { + "panelId": 56093, + "x": 100, + "y": 200, + "o": 0, + "shapeType": 2 + }, + { + "panelId": 55836, + "x": 0, + "y": 200, + "o": 0, + "shapeType": 2 + }, + { + "panelId": 31413, + "x": 100, + "y": 300, + "o": 90, + "shapeType": 2 + }, + { + "panelId": 9162, + "x": 300, + "y": 300, + "o": 90, + "shapeType": 2 + }, + { + "panelId": 13276, + "x": 400, + "y": 300, + "o": 90, + "shapeType": 2 + }, + { + "panelId": 17870, + "x": 400, + "y": 200, + "o": 0, + "shapeType": 2 + }, + { + "panelId": 5164, + "x": 500, + "y": 200, + "o": 0, + "shapeType": 2 + }, + { + "panelId": 64279, + "x": 600, + "y": 200, + "o": 0, + "shapeType": 2 + }, + { + "panelId": 39755, + "x": 500, + "y": 100, + "o": 90, + "shapeType": 2 + } + ] + } + }, + "qkihnokomhartlnp": {}, + "schedules": {}, + "state": { + "brightness": { + "value": 77, + "max": 100, + "min": 0 + }, + "colorMode": "ct", + "ct": { + "value": 2700, + "max": 6500, + "min": 1200 + }, + "hue": { + "value": 28, + "max": 360, + "min": 0 + }, + "on": { + "value": false + }, + "sat": { + "value": 66, + "max": 100, + "min": 0 + } + } +} diff --git a/bundles/org.openhab.binding.nanoleaf/src/test/resources/theduck.json b/bundles/org.openhab.binding.nanoleaf/src/test/resources/theduck.json new file mode 100644 index 00000000000..0da9bf3ddcd --- /dev/null +++ b/bundles/org.openhab.binding.nanoleaf/src/test/resources/theduck.json @@ -0,0 +1,129 @@ +{ + "name": "The Duck", + "serialNo": "S123", + "manufacturer": "Nanoleaf", + "firmwareVersion": "6.5.1", + "hardwareVersion": "1.2-0", + "model": "NL42", + "discovery": {}, + "effects": { + "effectsList": [ + "20 Minute Sunset", + "Beatdrop", + "Blaze", + "Cocoa Beach", + "Cotton Candy", + "Date Night", + "Hip Hop", + "Hot Sauce", + "Jungle", + "Lightscape", + "Morning Sky", + "Northern Lights", + "Pop Rocks", + "Prism", + "Starlight", + "Sundown", + "Waterfall" + ], + "select": "*Solid*" + }, + "firmwareUpgrade": {}, + "panelLayout": { + "globalOrientation": { + "value": 59, + "max": 360, + "min": 0 + }, + "layout": { + "numPanels": 8, + "sideLength": 0, + "positionData": [ + { + "panelId": 49632, + "x": 59, + "y": 56, + "o": 0, + "shapeType": 8 + }, + { + "panelId": 34671, + "x": 126, + "y": 56, + "o": 60, + "shapeType": 9 + }, + { + "panelId": 36406, + "x": 126, + "y": 95, + "o": 120, + "shapeType": 9 + }, + { + "panelId": 39807, + "x": 159, + "y": 114, + "o": 180, + "shapeType": 9 + }, + { + "panelId": 42632, + "x": 159, + "y": 153, + "o": 120, + "shapeType": 9 + }, + { + "panelId": 15767, + "x": 126, + "y": 172, + "o": 180, + "shapeType": 9 + }, + { + "panelId": 32797, + "x": 126, + "y": 250, + "o": 120, + "shapeType": 7 + }, + { + "panelId": 0, + "x": 0, + "y": 52, + "o": 60, + "shapeType": 12 + } + ] + } + }, + "qkihnokomhartlnp": {}, + "schedules": {}, + "state": { + "brightness": { + "value": 100, + "max": 100, + "min": 0 + }, + "colorMode": "hs", + "ct": { + "value": 5000, + "max": 6500, + "min": 1200 + }, + "hue": { + "value": 40, + "max": 360, + "min": 0 + }, + "on": { + "value": false + }, + "sat": { + "value": 60, + "max": 100, + "min": 0 + } + } +} diff --git a/bundles/org.openhab.binding.nanoleaf/src/test/resources/wings.json b/bundles/org.openhab.binding.nanoleaf/src/test/resources/wings.json new file mode 100644 index 00000000000..24d854b6780 --- /dev/null +++ b/bundles/org.openhab.binding.nanoleaf/src/test/resources/wings.json @@ -0,0 +1,143 @@ +{ + "name": "Winds", + "serialNo": "S123456789", + "manufacturer": "Nanoleaf", + "firmwareVersion": "6.5.1", + "hardwareVersion": "1.6-0", + "model": "NL42", + "discovery": {}, + "effects": { + "effectsList": [ + "Beatdrop", + "Blaze", + "Cocoa Beach", + "Cotton Candy", + "Date Night", + "Hip Hop", + "Hot Sauce", + "Jungle", + "Lightscape", + "Morning Sky", + "Northern Lights", + "Pop Rocks", + "Prism", + "Starlight", + "Sundown", + "Waterfall", + "Falling Whites" + ], + "select": "*Solid*" + }, + "firmwareUpgrade": {}, + "panelLayout": { + "globalOrientation": { + "value": 299, + "max": 360, + "min": 0 + }, + "layout": { + "numPanels": 10, + "sideLength": 134, + "positionData": [ + { + "panelId": 1837, + "x": 268, + "y": 437, + "o": 0, + "shapeType": 8 + }, + { + "panelId": 37923, + "x": 234, + "y": 534, + "o": 180, + "shapeType": 8 + }, + { + "panelId": 59975, + "x": 167, + "y": 611, + "o": 240, + "shapeType": 8 + }, + { + "panelId": 20510, + "x": 100, + "y": 650, + "o": 300, + "shapeType": 8 + }, + { + "panelId": 31270, + "x": 0, + "y": 669, + "o": 120, + "shapeType": 8 + }, + { + "panelId": 25862, + "x": 335, + "y": 359, + "o": 60, + "shapeType": 8 + }, + { + "panelId": 24968, + "x": 368, + "y": 263, + "o": 0, + "shapeType": 8 + }, + { + "panelId": 923, + "x": 368, + "y": 185, + "o": 60, + "shapeType": 8 + }, + { + "panelId": 34168, + "x": 335, + "y": 89, + "o": 120, + "shapeType": 8 + }, + { + "panelId": 0, + "x": 234, + "y": 388, + "o": 180, + "shapeType": 12 + } + ] + } + }, + "qkihnokomhartlnp": {}, + "schedules": {}, + "state": { + "brightness": { + "value": 100, + "max": 100, + "min": 0 + }, + "colorMode": "hs", + "ct": { + "value": 2700, + "max": 6500, + "min": 1200 + }, + "hue": { + "value": 45, + "max": 360, + "min": 0 + }, + "on": { + "value": false + }, + "sat": { + "value": 80, + "max": 100, + "min": 0 + } + } +}