From eed46f0a42443fa55396796ca843c553da5d90b5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 6 Oct 2023 00:28:45 +0000 Subject: [PATCH 01/30] Chore(deps): bump postcss from 8.4.26 to 8.4.31 Bumps [postcss](https://github.com/postcss/postcss) from 8.4.26 to 8.4.31. - [Release notes](https://github.com/postcss/postcss/releases) - [Changelog](https://github.com/postcss/postcss/blob/main/CHANGELOG.md) - [Commits](https://github.com/postcss/postcss/compare/8.4.26...8.4.31) --- updated-dependencies: - dependency-name: postcss dependency-type: indirect ... Signed-off-by: dependabot[bot] --- package-lock.json | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index 95780b5787..06a92aa368 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "mapcomplete", - "version": "0.33.5", + "version": "0.33.7", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "mapcomplete", - "version": "0.33.5", + "version": "0.33.7", "license": "GPL-3.0-or-later", "dependencies": { "@rgossiaux/svelte-headlessui": "^1.0.2", @@ -9612,9 +9612,9 @@ } }, "node_modules/postcss": { - "version": "8.4.26", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.26.tgz", - "integrity": "sha512-jrXHFF8iTloAenySjM/ob3gSj7pCu0Ji49hnjqzsgSRa50hkWCKD0HQ+gMNJkW38jBI68MpAAg7ZWwHwX8NMMw==", + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", "funding": [ { "type": "opencollective", @@ -20529,9 +20529,9 @@ } }, "postcss": { - "version": "8.4.26", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.26.tgz", - "integrity": "sha512-jrXHFF8iTloAenySjM/ob3gSj7pCu0Ji49hnjqzsgSRa50hkWCKD0HQ+gMNJkW38jBI68MpAAg7ZWwHwX8NMMw==", + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", "requires": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", From 929093d36f13ab1861d533f4d93152a7ddd1e6dc Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Sat, 30 Sep 2023 16:35:02 +0200 Subject: [PATCH 02/30] Fix: typo --- src/UI/LanguagePicker.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/UI/LanguagePicker.ts b/src/UI/LanguagePicker.ts index 497e0024b5..350822a1c8 100644 --- a/src/UI/LanguagePicker.ts +++ b/src/UI/LanguagePicker.ts @@ -12,7 +12,7 @@ import { QueryParameters } from "../Logic/Web/QueryParameters" export default class LanguagePicker extends Toggle { constructor(languages: string[], assignTo: UIEventSource) { - console.log("Constructing a language pîcker for languages", languages) + console.log("Constructing a language picker for languages", languages) if ( languages === undefined || languages.length <= 1 || From da39dde7b699e576e79eb5781933146fa9747ace Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Sat, 7 Oct 2023 02:59:17 +0200 Subject: [PATCH 03/30] Themes: add guidepost layer --- assets/layers/guidepost/guidepost.json | 53 +++++++++++++++ assets/layers/guidepost/guidepost.svg | 61 ++++++++++++++++++ assets/layers/guidepost/guidepost.svg.license | 2 + assets/layers/guidepost/license_info.json | 22 +++++++ assets/layers/guidepost/signpost_example.jpg | Bin 0 -> 79813 bytes .../guidepost/signpost_example.jpg.license | 2 + 6 files changed, 140 insertions(+) create mode 100644 assets/layers/guidepost/guidepost.json create mode 100644 assets/layers/guidepost/guidepost.svg create mode 100644 assets/layers/guidepost/guidepost.svg.license create mode 100644 assets/layers/guidepost/license_info.json create mode 100644 assets/layers/guidepost/signpost_example.jpg create mode 100644 assets/layers/guidepost/signpost_example.jpg.license diff --git a/assets/layers/guidepost/guidepost.json b/assets/layers/guidepost/guidepost.json new file mode 100644 index 0000000000..28e2d80b6d --- /dev/null +++ b/assets/layers/guidepost/guidepost.json @@ -0,0 +1,53 @@ +{ + "id": "guidepost", + "name": { + "en": "Guideposts" + }, + "description": { + "en": "Guideposts (also known as fingerposts or finger posts) are often found along official hiking/cycling/riding/skiing routes to indicate the directions to different destinations" + }, + "source": { + "osmTags": "information=guidepost" + }, + "minzoom": 14, + "presets": [ + { + "title": { + "en": "a guidepost" + }, + "tags": [ + "information=guidepost", + "tourism=information" + ], + "description": { + "en": "A guidepost (also known as fingerpost) is often found along official hiking/cycling/riding/skiing routes to indicate the directions to different destinations" + }, + "exampleImages": [ + "./assets/layers/guidepost/guidepost_example.jpg" + ] + } + ], + "deletion": true, + "allowMove": { + "enableImproveAccuracy": "true", + "enableRelocation": "false" + }, + "title": {}, + "pointRendering": [ + { + "location": [ + "centroid", + "point" + ], + "marker": [ + { + "icon": "./assets/layers/guidepost/guidepost.svg" + } + ], + "anchor": "bottom" + } + ], + "tagRenderings": [ + "images" + ] +} diff --git a/assets/layers/guidepost/guidepost.svg b/assets/layers/guidepost/guidepost.svg new file mode 100644 index 0000000000..61d75072db --- /dev/null +++ b/assets/layers/guidepost/guidepost.svg @@ -0,0 +1,61 @@ + + + + + + + + + image/svg+xml + + + + + + + + diff --git a/assets/layers/guidepost/guidepost.svg.license b/assets/layers/guidepost/guidepost.svg.license new file mode 100644 index 0000000000..bb226dbab0 --- /dev/null +++ b/assets/layers/guidepost/guidepost.svg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: OSM Carto +SPDX-License-Identifier: CC0-1.0 \ No newline at end of file diff --git a/assets/layers/guidepost/license_info.json b/assets/layers/guidepost/license_info.json new file mode 100644 index 0000000000..851b7188ad --- /dev/null +++ b/assets/layers/guidepost/license_info.json @@ -0,0 +1,22 @@ +[ + { + "path": "guidepost.svg", + "license": "CC0-1.0", + "authors": [ + "OSM Carto" + ], + "sources": [ + "https://wiki.openstreetmap.org/wiki/File:Guidepost-14.svg" + ] + }, + { + "path": "signpost_example.jpg", + "license": "CC0-1.0", + "authors": [ + "Mschaeuble" + ], + "sources": [ + "https://wiki.openstreetmap.org/wiki/File:Signpost.jpg" + ] + } +] \ No newline at end of file diff --git a/assets/layers/guidepost/signpost_example.jpg b/assets/layers/guidepost/signpost_example.jpg new file mode 100644 index 0000000000000000000000000000000000000000..8e06fecf44b49b4e93a397d6f3febc34910df9cc GIT binary patch literal 79813 zcmbTdbzECZ7d9H)p-3ROTc8xz;2PY5(^8xuh2YW_mo&xQ-KDs;#kIH=D^Ag3Ev0(X zbKY~__kMrf@6P^B_LIq)HS3wZGTHl?Uw*Ir-U8gyRD-GkFfcFx8t4P?dz;=;HNeFY z0MO9^fB^siK7a^=41kG7AaqT}xc3((M&oJ>3IG=RjDbD?7<2%fzc2vMhC%-?euKgD z9~-m>457dJ5~1yeF@*oZRA@YhN%>ztF)je?zx`lP4VD4$&{zOn4*+NrWkn;X9*eS` zjy}v-)esE?__d8y1OTdL_bns@1zFGqZTg4BpNRfGt3!44S%d{d1;haWVL?GjX+d#m zVG$M~A!$KDX%R_)KElPr2dV=Q6cH3e_rXE;q(&c@e;i=^O#?kOXnZde{R&_T&;ExS zW_WnszZe@L&76!hd2A5)>@N_>T?RYa!-8^NR&2 z#QFzgd@scL2V?G({M{Gh&&4hMr|%z(`QLF%vHk~R|2yt#Db9b|xzP5w|6}t{{D1uX zPyD6-#Q&$A@W1xxw|@#cVk{Q)nGanF|G9obQY=E^(t;w=qGJDE)BkP5@JCx~q3hq} z4-E!%Mb9gy8~SYjcQg7G6Yc*`W@8&lN&b2FC)=y z^Bc?LfAbriH2=w6tcRHY&NI#Dzr6h)dHMT)zk7dpqM80)11JOVadGi*aq#i*@CXR- z35lslh>3`ZX(=els2J&(nHcF97+5$2xLMfv*%=skBzXA+g+xU~nYpE8C50gZBBH{7 zoL~?T5D*g)gGfj~!mJFe!vELhcQ1e(9}or@#KK?$V3K2Ckz@QG1TdrH#6hnVe-?}X z6mrh4mNr!FrNNNZ)|cL3RWRSTuOt-cx+x&!eL28 z`0PrJeL%z63l0%mL^uHWOf9Y>BbqFZo-k03tQ{V4ciw4 zRTbNQB)W1nUZp;OB)*Fj@{U+7%#>61ud5j`M&(NaHHY5Ja~)BFj!g11IP0U#l$*9N zpS3g&Keio7D06$q5-vO#W|l!2=fF#&yEp&hfO00C{?$+y1}qi{eL*=5NAjy#X>=X} z4Ef(WC0^+2Z45nn@Yt%;onr8eKTP2yWf+VMK)!DsaI5f^PeWYyRB076?OKJbazCZpTlRK7it=ohtuj)6z z#pQ%g!#|$(sjDWG?cRmR2>cjn2rc0{9i1E)^IVIWSfjdQH=um-vh$O^%JhLpoQ!vN zQ_7?aXa$FOmh`@7{_9CSo^y*p>`XN)Smt(=0>m-dY&#&# zBcPgbm1CYJZ70wdS#@5QaB{3 z*_{#GUG3422wtS|4}2TTtf+MeF&~u@<-rqju8=>$0p>1qFd6^yG1`0l&eb}GhmbOA zFKn|sTAg|;daSxh8%kKG$<5<6q|-u_xNfCA7iH7;TsqH5e3T4eOhcn4OceSwO8rX_ zTa26CD~3gjkEYzIdI6wxo`}MV)zwn3{mXM5HO@peztEabuzbW1kwd2FyqpB$;7?Vm<;|fH52W95(oc$W)T0`2DI)h9GStPjU#d=SU_Kzvr1ep&< zwttB&(KymNPVbsqOyxdspD4n$XCV7Ty3=_{Wwl-YaV}3w%P4p5lU!c_f!o^KM`eC* zyY$Pfq+dyIk16i%_P1&$_AgOyj?MIJ6X>p|+LcAzqL(W0ZFQ zUAn$J(OKr8o$B+1cPG0TgJl1FVd0>zo0s{v6 zEcSd&LAy|8Z$9^g6 zK~Gf!zgCANB0hSXe>spz>ZJF?jyP4f7{1s) zoz?!*mv=V2yHDYX=$Mj8#IbDY5MP!c=V$)7V}r#D;8Hj{hvy6U9DP@-*DRscN^^!Y zBDPYk=tugKSX*!!$Ft+mFFmi3i%+d9Ss+0WEpwoMhe z`pLSJk4CF(j_(V+pr`68{3yL!rw3MxB~(+CJ#K9)a$*l@xK1Ri2(dfKedg=p043}K zI=I;!N=<87ESf5H_xJ9{L(8tltltfWtkl`2%3%*A}_szbo)hASzbCgCEh_-!#V zG!%NI_lyQ$;Mr*8d{bPu<)tbphKcey1d3|V?j>k{^U{3O=eM-=MkLtSF`DOKb*`F0G+2?CtO00Z*)5@;Vlj++VTSwpLxOI= zPx$27_^WWU_XDilE_Yr@>kLjmzAgH^=}w!~DV8qSeVof6Ann9(-jMl{cbG3*x*_Sf zlk3LTP6woTpoY6-CB8lUp#dRpWXdF|+Bq+G4RJFKqUiWMtxEuWcvKXPpd0gxz3#>0 z_QPYZ02K7+i_v={>ihM_BLNlcU!l9pNq5F$no!Y#4^P#yErj<(lz_!1`j6?&5aL@* zQs3w*MAkXx@aesyIE|sp6uHDtC8xIyt&aK+y{0LqNGs3ajtdcI_kH;%%Ym7<6OCNK0Qeq8|G`1JQ4=`#-nd}r*CU&FVcRly<774_ZltqlukL7 zeM@&$tt6T4krSk`nY+$W`f^Ld<|!Xe@tgLA^7QwJWbLwHj61~hK~qJ@bEhY{+hiLY zztSeHAYY#NiU!Y&Axed-6UV(joW!?Y6Gmx@acWxra;lrA9fIPto|pL&$}Rbhki0_l zOaN>(Y8tJd@K5Sg(g8Wnj268O-DD?~!B|*4mt%VBj)mX+YA4f^w(tdIX&0et}^8 z24L2SGqWYY$8~tzT7M8(CQ+tj_rL9mSMPaQthXU8D9lTXVfl7ZW4gpZyvWzxm*-k5 zbjVO#x8&VttnWR~HD_L>Jvr?e!}b_V3n7jt!>ls$d3syf+1a9|Jn6QYqt009ow+*v>?|7NmHSBF_ zPDOt5S%~p2Ht34G8msK`^lNYGkZO^Uu3Na_s5rqRZauw+5EiD<2i*&$?;)D+vR2<8 zW}jyaQu~WLuQ@OwI{HyJClT`<^-9wQI_$P zFSBy`;IP@<=e-SImzvhXg?o2u*UWSf-`NsS-d4WC>s0OGKVI&})wdhb05pNG}SxMMEja@>7bmaHC_EsD}H zv_zI3!@j;Bj@@??zz05C)9A_c(8yt`K%SXuw^#`ehh@wdzoQV{t%IL@u!V^tG98{+rd#B;nWkTS9JbP+idX*h+b`G$ zO--n{mIEXFL~rKiOII*DFVshY2({+}V($iaO1qOvk#ke{W2yvTQYk67P>~T(eSy`S zd&B+mY`YlPls9TOQU2${*072#3h&#&W=2Bao?syF>2xVBj{BT^K6kKww)VF{9 zeJGot$n?0^k1CzWhY7y{&*^`-EJ=86uoe0ShSK-*Vo6S^1;g45t99pZfF5ZyUrg8I z4zrKny}w_(o6WN)(7?+o!sq>M{x_f*_H0)=!4aJ_`yqOj8*9hZ6h+Yc16izYq$QP7 zyYYCx0S~)L#2>Y+D6%AKXv588ssKyicqW|+oLjb^Wh!sDZm&7R@~aLu5+o=ZynVSv04lq^e2lTahhO7hfk`&z(CWO+!=WR5M@td(bc~gh6d&SsGvvDQbwst1eY(?OboA%@ zddpuF4J6iqOBTNGuGg)!4%jI0jXuyqh#h0c9(lT~EG$f~wFDE8-KTY4yZIH~q?*J* zx)`+hVcgjsRZUGwz;Zp`_!}TuLr5O%*p6B?Q448bOl50ixq|nvRR0*Rqc!#cQciTf zj)60jj@ZZT&hNZ`J~^ z=xa55?&%60eOp?N2{CE2$iiK3&o&VnlVsN2yF?xes4Sab#1K8 zP^y=jZ@ky>sI#9H)tPt1b?J(1_(JQa#K8((37DSwA~tQY@@NgZUt>9fzilB(ANnaJ zVgE#LS&Y7$4}VpveVTUS6d)oX#Tq#M_NK-QF5#SS{{^yZz9{#>n<7oY4ndU0n$V50 z@GbxI=2t#_b3323cgbzK>iC7eM8{D1#0H5aU-H0b8J6sL>|U@{=AXr~u9X+Q(^-{} ze(}_(zN8Ck&R@67@qF22AMT$9h16Yzy#AIw{%g4zCmkBnW&(zFU1*%$)I9VU1U#?# z4VZcUZBchuqp6cKpI?MRidn!qjAEy6?3`ZDorfaL zZ9NtDzWfL_!7Z}0t$pFZlecFBgpEx6l65VTmJI`m@BaEGziXi5>QbSRVX&rgZpG^W zQR~1ecg>H|^hzt-oDMcy@^XiJ@%a)$+ox7GBX_!f66r@CRnwiz_Qf738iyzsnP<-J zv+RYujFy%0ue_0E>kkf0EaA@yTLcxF7Ju4$3GK(mGiJ(M4p1SCrA&uBbG-k%TFO~0?Umavy z-5jY#XE`9$f(&5(1)mJG(h@T7x^_%hV;G08=f%j=CF2OOC_^2H7BHq{!JJupiPh@g z-}#@yRiNxIQoVR(5-$_H&#b__&VyR_+QWtTCJq{{aQb?`+*a;(3N{BacasXrMhE{g zK4{^LAW+_NcqRGh$+OgUTzRl#Kf{XrV2; z)Z5h^xKPD!$PZVlA=y?OR>VuPRD!xucdl8w@+|YYdU@u&59f^w_?FJGQ{#HEe%xi> zY}SxDqX8=-8v}ecl%}hydgO8eII^GV`KolFDvK}g!{GfH7~xWHWu^|cwC>TKdPh~r zqduPx--rEpU}!8|l8wlBNpwiX9C^->wAw9?eH0h$#W|RaMP)CM4BBe9RrRJra-P0* zezXDQeOHNlufX8Q)4$rkp=98`;QyV!P_j)Zb}g0PH-W3j7W5 zF*b>o+|_!_b;qXIamaoAtknsy9IR)- znEBEm-qX{w;J|j}XGtKZ=AZ^9ICL2fq8!!%+UYLsQ*quuhs`>%{o=j9^HU988K@>&7l?5Gh%ETk^&4O@yU?X|!~6|MWt#N>$q77rzB~D=IDPr2 z&xg-5zX9y3R7?EmmZtB~@q@P~uYHw}eAUbZ)Az@vmw!Z9{_0Zw4JfL!{~Ga2 zwiJC$XH^|ccLbOTf(L&CuK0C+eXJ+a_zg&qRsZ#R?QT}p&T>~(-L^jKH-MEs;Wp-T zgW?7EkFff;C);lmi;>w+38kUWzJ4kB4jaHJ;Ix;p64q7@V{v-9{kr5FQ5R;QpP>_?6lT%VN&`?s*0m;chtROl@MrLMa zN*Xo}HYN@RCT6C;1b%c^JX}0de0)+SDsn2O|LgLXz>o1Ay}|Ln0zczF0{=hqKKf(K zKl(lZp8&0s8~v}mkB5Uri1)9&k1Ir}_+NP+_($FsF|?hf=0JoeZ(ram7dKtn_0Q38 zimDoUM?|L1iOboHzeA#>b--Vn3g{1|(8Bv)I|^91*tlpjv=Ihc z(Z;4=#Sv1ZG{9whEUe^(-cO(c_RX>@8#ZoV;8BYhUE2Qse~I6x;`&V*2uV3#Sv!{b zIidUoo3vmX6)B9XN*WKCh8W`Va<%5c7Ot>M@nX2zBg3o+Cy1fhCM#%)gy~?io1D{Q#3it7b_#jsQr*OnW@ZS|co&@k6FKO!g}{<_iwin~i3@=^GqJ9AKp z-fOaPVKBoBUPuJe0ei^3c==5I$>yhr;algQVCkp7e8D2qZpL03aVce~cCSq**(sj# zG%+Ai-($ttLWlWq-CL|k@hjsAliv?fOKEX|d(FYzw7Wn~P}4V<6{fPd_mFuxp?g_! zQQ{V!_gLkvGhso-WpOkuTZ|}Dm;`72Wg45vPP|kNzI7^wb_mU#x~Pi&Mqh;+fq~qr z&L|iPK*j*r0faK6FnD?=(^ncBZir*)S|~1wsq9X4?8AP_z?pKUn|p_B^^-QnGRAeO0#t%UfkOp5;8dz;`pSyWPU@;A5Aq3+ zG()k~hMl+AUa*_JR4n*JDQjF8a zd+9OZxH0&Rr#ep!^%hR@;z{i!JP8laO8yz^ADlH{hZHL=dIW~VR&XWbr?vrhm&z_w zIterJP_{XI?zjc==ewXGlMmKZN{WSPP0$(M1HTk3Nyd*{+JcaRpt|5YIwg_NfGY81 zYJC~xTL-EtOHNvQzEH_Rt#By&b=6}KstN?83jmFhKC`;wVa;aWQMD@E6*afx5`g#f zER^Ge8lZ%yEOa@Yq?^S3Di+&qDN$!y5EY*LQ}B(tMJS860ZLeG)KQ*G5XYj_WCwSQ zLlV?`TjekKBQ)4xJnCvlH9bTk1ZmmW&GN{JwBwv-DG?_XAbIu>8V!JbGP<9Z!{BYG zn?ymxU@l3u9~+LrmB3(6x?rq}=&$PHFXRUI%&G0i1Bx{zq(D2b-J&dW^`Ay4hqrqh zJh&&dmfa$waZr+K^1KXtbbQ6_YK3;l+~Pf$I4r{O{;ocx@_VW$ei5%YRWjQo#aSOM z(2}z-SwV7DszxggV1#TyKG{qTHhAHK3ysE683t4d4Fk?5)y&ai-4K(MaP9?<456Jz z>`N`Wl1>-LjC}}fa-amW&LBh-D@+GZ!<>&*ogSm*%H`S6u7DLof@UL<#VZOZ3=)(f zjQ7zPtU-RTJ?pD7P=bOOj2oX^?|mtW?PcsJMbNw8k6g_-T3wLnKGVfZ!&pAxAS8%N z+O`fatztYGFTAaY?W^n!P-AWtI_;Cf0t#;m;%E>+9~alIr5T+-8QY}zCM`1i8UmEj zxlb3{sHC1o?}!66Moatlp%_|zV=7X?TMI61kS|qky(#evOSD(zx?!qTm$`OSqmK!3 zRV_e@dq{>PjD5WhdJWRdFAZwXJe=o4dAu4J<^uq=>-O_RC_^n;Da=TA&p_HL6$#By zhS~7T)g&)H1{0+jfj2Pf9ob!ge-%pIZRSEm6;Mb0IB-Oi*IiMzdaHqoOYgC|Hwjhq zDv$D27;J|PWzr%+elAHx?wY{Ah_vlUt8!1JElpn|^lXJU-t$7LCq%3wGkK<4z$FKu z25ity*~0Aht_IH;YnA=iADcJfnDWvL#H)g{eC^e;Wd znaL-^ej)D!c?(x_nY~>mU%0J11zS&|b7+U`*YW`e^pa^*)XM2O(8^z$I3|uuwFzZB zI)i(UPU?iOv+lMs&V=O}dhEfsD!+>ltYrtF>Lsf@3iDlZpw}gSxMJd!hqRTq{)De9 zlS2yku3;9Dk&@h!<;XTZJM*y&FLt1VhjAFP+(bB$hUxQThH%hN9Y|uMLlY3Iqi9+f zM^X$|X66uYJPY9;G0ikwW8f!}_$8Uwl4X_>p4$L4x>iEyJr82*nG+mVs943}6X;PT z-@2_#Cg;v09x7zoN_Q=~Em;lH_v}XpK;jfv7dLdHPm+{?^cObblIA$3Y%<- z{NOwJEPD-GWvY-%s2-?PK;?4Geti)%T)DHGLP#XVpfmfA6=F)Ymk7;YUQI?2b+ww=cMq?nZxrcbQd@Ea!rmka0Z z&AqBX+%&ZM5(q_rf92fysALT+PH08n!%m-6$RdT$0kJNy75_F*FWUJlAw2mf%I&ER zXI3JQ+hRO*ZJk!~7bnp7g+xu7PxpRJLU>4Z3@jP89c(z^ z&vn&8%OOmv2MTgZiO=SeuM?WAQu%Sk$L^~Q9n__?uqAMObXw~@MA|2( zOMcR630t)iS2b66ED8W>m71m4KWIJ0@T2gG=Rs_oGRvvKC*7%>_AA?$l9x zvemW!y;$vuM>LVNRiiDiuDM|vS8T*gH?61S=X>E`9xI_M;I6!EJJBX5__nz1z*BwSz=8(lameJO!7UR5 ze@XsSOG^QAj3D2Kd5-%VHw(h1;N_i+(p+33=v_=>WS#56qn)z&L$BRz>spyO!?3Z+ zJ$J}*z74JpNx8`mG6CBS{7SYmcH#ie;@K%mD*&Qwu_purnif5P0bHdV)e2Iz3+-eZ z@i^3cyDbPfM8!yy%ZJHl)~jR7uR7qo-H&5-*Aaa$qEpngJd6=ieSfI;lte9QYLi4Y zFtD!*ggaKnGDfA!E@qKza|khx%*4o@7sX-o+mcB_V$bnxkx#1S?;;~a->`?4ep}Ed z-O)aT#JkR51~w?vszVl{6iVi@l9*Nmd9iKi=qFk$9k+sh8S^GJKM6zHwc!PMGY~Im zRVmf$IBE`igjreP$R4qD;=D3@=Brb=PJ>wI&nCQQOx6|>fhaVy@<6nmA2;h3@iwfB?*+y{$aLZO}c^Mo&5i#(`Vwt8s;2tk& zfs=x*Ci^g}4-xV2J-{n#P)=Q=mO3n$5TPKmQ#)vL3;cw|WZWI0>J=Hb8U^0613F%A z1sX$*z0Xu-uvCply>k~r5`!1MCt@B{Rdl@P5*9L5Pwtdvl&M-L7^|N6kQZ3ikt@Rd z2#$L1(8modCNFMhWq(gTDpkeXWrKeT#?Ja)4WH0NRi8r+$5G$S*l69U!u9Vt(~!3q zBv&Mv*uspEIJZ-o`lRrViF|b*gRc#Ip=w^6^=@h6fR*a>m&FM6I%w!9zO6U1ySc6V=ZBmNew$BjbRLa_XpW2hL*0xJ6jaSGy0jy$*;4DE(ryo7sNb zaAYOW{lf8G%|MAqcyPN){Q5mF12L;u*vLhPsQYgK_mEmh|Lgjz&CF( z*6v68v}RU~H}}NOqx`bB_fWFR{DHSf^-%CG z?M|8SnGLQ~S(<`gP#qMt+o$6d)oYNV$lDlj4tJZa3`xg~H207C6u$^}a0enrc)j9r ztlkTs0v0S6^s_!!>Td{Z0A#!8<+CuqH0xm2pY>M^PqZ~RTDRBZQ0OQ#4|*9x+!}S0 zp0cAtTB&X724MCUv7gJQpILiWWD^qS^jn8QVU;zmR}Q1amIRp*oU}`bka zT5J*4oK2)5lE$#$TJvfSND}YdrosH-M9;XB`s_lxd4oL(1Ba1_8IwMHn6`o5q3{WS>H2>cE+Mi%0`cT-8U)f7`vmQhJ`g1!hImgR#2`6@*Vfy}**GyUQ3CrHkdg8M z$}89KXOYIVEpOsfxsrE*rszlN+SV3Tq9JVqWE^~3pK=rB0GiaFwPoh*->I&zxG3$b zGFk^zEY*}h0wqH$!f&x`Tc^KvRJ^Daq^%^Q=OYhS+(~f93QJb3VK7uO=dG6@lfDt= zlM5(px;4;2%DyZZ&v0I{QrQmVN_P!wM>*CEqG;a2T}PplT(j+D>9%)6_;O@seq1`mZgp>M(2Erm7Ukuc1Ff3wt(AhVvij$WY zHd;ez!(}e8UxS|kFs4`Am3^sWvQv}cHBU^+3l#m`L~o$TI25y7A}s2n8H@Ej(%Y1zY5T})1VRXxov193BEW}qkX;feER6BNFT=_o(mV@f3I#U{+M9)dU_|LB= zTM@D!{YA1YLoO)swDa`=ON*`jc;|LOk{I}$N)xF+1=!v$DAC2L=Vi;3 zCW-@cZIT^Qd(Yml!O9Y^fYo)bD)vI?M0Yj0$gt>EBy@K?=S!{tO^N{r;tCwEV{SQ^ z&*V2#8s5o5D|qLHn$O>&x&jq%fNPjUl3?u`y@_O0V`gV|h~EM{$L+X}Mt?b7)xVG}l#&G1R+yW$bg4{L6<|abM0-A0HkYn{@CYhd z{}Zj)P4d5nRAmCUp7OS{`*w#-8SkV8Z~b)Xz5U!EQJzr~$0!9((s?Rs*kZ#tWUDG~ zz4EKYTKelX=@qT!_EYt^2(4-b)8HBaLj!2QmYCmt35l)lx``HUE-__=a5VG`!&H}7&TAvwo+q$Gwmp_~8o_0XX&SIbF(yS7)Msj3oqyQpaBtL5x z7y3G5q%9z7Vp;zi&=UxBt>^;*?h$+z#cjD5wUIGi^Tomga`K&OfVofg3&{d$E>_gU=oIpFTl3p{)F+fl^k!~mSU?NbXF{R-e}(~ ztmtDZe_^bCBc=3afg32dE+YUP;vb+64!nF-xMlqH>|K~yzDpxGYKt(5DpOL-xKvs_ zdF26jHr2M8_g8~Col}!2FPhQ7zQVjlQ)t(T9Z&=ao!#d|G1_nd^VJfmh6c?%CVO1j$Cwe;Kb;uZ{Gg#DUr5CEJN~hKImRGU@-x4w(Ror~;HYZJ&Md#!~ zhP@N%7%(M+Y%wEO15|%sSpD7@W_Vv1YDmv9J}#2`-RO3-kXYa5#o*&Jlf-^ZfSQ+~ zaU9U$`6nU@j2|@pAxdTbR)yWU#~l5$YQ_~|C6!!-KJm zY~}d+)oQ*1FvF?5VjhwKA9ajwrHph|i61EZxM=lTRjP2y!lq~bL;egsIt&~}?Z4Q@8X)UoAAFQ_BlRQAR7w||`=i;twcXq7gG{Z71V$H@# zCb0~A-l;?%R1qg@5{Bi&PXkWtjYxchb6?rP=C$Vp*($j5ry|>NOJs96qV(YNjFna8 z!c?2Rfr~?BZR^xl*^YVeTp@8`hI&@d{eDl$eikk0r<#17 zcCu<8kTei?{h+{OZziYe{Vq+| z!(>Mbls>~!d@L7;62g;KvvyjPZGle|2MM$%{iTb@Fw++QlcU{A_)r>``O zh0ht7mEInHGg+sL^Ds(?5WNI@IhmWSR2^cSKtn5*3VmLm9Gf_bKi*Xl+}s^q9Q5K7oL2D*vF$$z}iC*XUj=6yz|;0VzRVX*;u2J9Ixs-GqEm&3EJW@pk{X- z$_Mb2I}8KLsYzE1dGEi<_d)rIeiXp;LSE?JojBGR<&5l0zNIl|~=u9jo}AwBg0kPx|-kH53ST;=aU; zVL*X-L+|}gn2BDc<}a9Y;Xv&S{NAl)UVuf1mOoLd!)-Q{&-3UThfJpw69LZkQ4xq> zn4QQu$DvpV8;nZ_snFzqP&VWn#a5Ns^>cSKpNqv#bcz}W%<*u_Kb>S>!7>Z*{euOO zl-*cqxq;G(mq>wh25>N3cHp&QyIP&SFLCXjP+zljk4qM#H3SE zN3OG_}qG)Hj$=0%vksp(!QF3S_}% zqi>5aL3YlAEBqE`na_fXaJ`cwCnCA$8I^Fkt#@_7jX3iBEO&l&U-R1nwBlM>JH^H= zZTGyu8ORs3+fl9B!Fo$MM4>g%9u9_h3>n?e3cZ?C&1PjiC>jau0O*NcB1ATeBb1*g zZpYYNu9gK<)_f2nIwNY2{-m7#cuh>aZOpY#bUj>T-$0<;5t}>|ETb)Qa#KX>FJ7|tjBE5Mia&2&W|9!I~ zh8mCCb{KH44{Iv5@SG2$I96n+Px2cKvNytu=)sDxFx4))t?r)C@4ZGX&LlR)T1(bc zhAb8if3PTP)x*7C9Sp0>EkOVJLyL;#bWO7xUaLR8dLU-MuR}bPGy*60QK#oAjn=Q= zSacXmrJzr-sCn@`HNm1jMUTl&%!doPPd$#E;T`%Ox1RsKCLpl#+UzU@=hHXi9A{0z zJ5L;QDXu7_OG=U=gf0+37^2n773J9nk3VfpDt{LE$-u%8fprg3mGP3*`xRw2W1?ef zG{fRJPxT$h#2mDh8@yPa<6NZZaL|*4sQ|jI;d5@=x-H8+30rWs8;Cm7_h2Pn;RbsC z2KW{AJkIg@bOakh^{%S6ssp7lq5*S*HSM4>{+cOuD*9K+Km}T=iD>;S+!CFDN};Zr zda-*V*TlvsOYHK*?rTnXOnbkUe72#E5b z$nqhYk~ffFS@nMP#AV7@Nx4p+8#oslgDie-a>Tc#Ynt(`ggx&$GI>|`GHQ%@o0j7l zOkhbRnUkFm%JN_)=_i=vx@!%j`5R#0BxO5*4U}IhUY*?fK>H%O5@Ez-?&19#U_NP3 zzTVhT|FN0f@{R*8{+>x#1GBjX6VenYKbJhQpGRwjOVQN!K`8gRW}aJ>7?O{;ib;L5 z5d4(}5?;6C@#Jy*Z$KHO_;OO7?B{59st&WRD_O;HqE(cP#$&d_u5ihp-d_n%F0_c# zFDHrc;uUB?ek{wFF3+ZYqT(Wmf z@!^5BiS%7kSBLZaz=N9dRZ!GU9#1O6b;P7{(u<)+2Vx%snWvJY7&?2|H#{%%P~K zqVCNk=2*YJ5yaRjS8~c(WgnmMSjad_h} z+V4f2m|RSW6VmcKU(hLkZ=N)Lx_b<`&$r+Wc=AS3&9KtUT<8}y{0q)%yvB?!0 zRHOuc($Q#64i1F-472ryP;$r2Vj^{A)xc< z1tP|J<8erf0DNh937QS$6uRJV`}*Njiy@7%R|pZ)eg_W@U;^I3PColIi)razcwp)M zcA4M;i)Whsy_@t0)HTWMX(8>6gVOS(Zvc%4;8&7$y5LscSfEdi$YBjZLnX)@0L&%HW5)ND640Bi4Yzc0^aI(`JlZ#k5z1J2o5*z(F{G zZB+k*$co$$lf#WJS-g7h3NEtl&QV$Pv?$kqNN%amCr*y7*)Hr9N8D-)0Omz_Xi@X*wiau(l46F+`nrMPuwD zcH`MvLVcsfiIlVt-dVETTfY}<+o=b0MtroWO#k3*F|MUFGOK6JXqqDK1+R=b zL(y4+{NUBl)Im!O`Y=mLfw{!Bi$1g`SYwS-o5pJoR?P|Efjtlxlw zZA;01VUKko+6&dMVnwc>qw~7RLD3 z+fFZ!Gz@ajAWvOV6pHaj59RAX){s@ zcT{UR$yRo4;0${H4b>ihxH<=t-8yU?IbAloDXhcM?w*n$&6Jk3_u$QYW`wHR+a6fL zel>TOOcM$5yDR^(Nr>lnh71!W!wYHVquT4SA z+*%KGQz}T#%y%8U*b_V9;ni0?;_r0;S2Izod>WPcBy04WQT^#1e@XWJdMbpP%ltad znbs3eLt~S{a;f2SuhlyKVwchhV%y@u%a)vVl0}a7YKDl-(lz@8oJeG2(a1hbLKuhN z-{z;w@C77uL;nd771cd6!W77nSA?DOLF%B^oy!&gv?R3ITS!YYwjtN!v@O?@z@wUG z_S(HHRPje83uzL1^hpkxwghwaq3mZ-g%2`-l%BAyg(nPO*PuHY?$I9w%Eb42J)VLq zd2ws=sqB)y4OV6Al1%#MitKBzEGfEkZ!16mR=g(`LArXNv9iloo2hSw@M?MK&p|3-_mGM=yO7;bAj2+KmV))WG`jHL0KsAn zqfnUyaBRvwzwAWJv#py}6bGK&GAsV>O zgX`x$Z@%WRf`Km;T1J{Ktj#B8gYv3*Wc;^wq**#oX<;qh{RpTX1 z0c7F9{{y8!TE7Wnd;&?Sakxau?@sH%7t}hZ7|jXc`HwtPe$yUL&HB_ki{c*wf{S2s zNR*$uJzHp3^u3VDkXKU#{?xH-wA&k0^txIVQKMIpI0rZiCOooN0q4l*jv7}hn&?NpY# zz~I#8m9~Hznn}mN@R25KwT~mYsL6U{lLS<86HOsF#Y(icQ2Ala4XGj~+Z>4_Hg_Bo zQpLI2JpAa9q63!2K#D*>}Jk;*4eP28f^>oSf01j5QuOFbB(%=S-3I5};s? z^v&M{N{3278+Qs~f)~!3pvdDqnwb^0S92Vi4bmc3L7DUIOA3vro=r-g+_I5@^{7u9 zBjYs_wMdT9C1OAcCZ1zfQZvOzk2vJ^qbq*wfO(+PK}WnO(+7%wh@5dzSU;J~HzW@I zsMU*fOL-Jy9ln&<_A$*wXE?@t(yJ0MDD5Sb9ebjtt7<2=PxZ(b<;Qs!xnIM${A&SW zD!Q9lc{n)8{VT0FZO1-!gX_%{+s*q@&=DMs`_Q~>Z(pJQe_G-7T@y>!hv)bm4JM-h z0K<`&xQ=LL5wXfg3Bk`8=l*}0tBWjsopJB{>M{ksfmjfduMNljcd3w|hAphz43d8u z<6%|G>}{C$iaBEe&>VV-W#Y0~KZU^qfIgJ?!?TAw<9 zwMK+&^Grx1KP?QoJW)~xVa0k#b^r$^n}7^LJm}3J>?$}?PB3aj5V2sSRA`FqJ-lrq zjOP?dqYAj)x0K?kd!&6^eip?NY5` zJHf^UM%q;5(V&(5+ksMDxC-HyaW3Kr9jMZ6Ocvs+puQ2VIH2BM7vLP$ZH=-m6~=lBbFslWwGlG_8*HbYVqqRz5c)&W9|8nx@5WfDHLi=8i4I_M)1yQ4qQO zX|BvidL)Xau)V2_xToMzig4MfU`WV5UOtr`B;3Gx)%B`IkfB*8z3iSn&J8+TwG*yQt5V`$&LD4UZx zq~1D+7K|()k&3N-t0qD7s|(AeRVV;8Umn+DPBBdc(&Sk*dN-Bdpxej>ysDs_jMT8u$f7JaXEh~281<(|vu;#@Lw8kf4K3KoP?DT?98^=w zoKfAgkD8dFGlRu4;8)0zFh^P2DwXD8&n zBu9|AQUI#^q;jQ+Ja(&=j1k_W%A1=Q`D#W~2OBCavo8)g6qCdI!|7ILHB<78dem8? z%V#33jJXlUh>)tQVx`1^o7#XBbK5lKm%|)+(>_3*6Mk6v0+J>U+487OI}w6E3Xusr zdt#BqWbBedH!5k$WKqeajQqIZX1qfa^3n_>@+u}<6;m@QkdP`}>~hG#RB=p$Pi*d& z@~dG;Nn2%mI6P+_v|DsnU=C=va3aZw*Xv1atdGi(R$l@o&n1LtfiML}l&<^`D5~Yx zflQhu!DiyBPm$Ui6RvPPsEHo$n{FsJac&tEFKJPVl4MJPG2x7nnuUD8oNeUxt1L4n zF;SY>l34TUQY{6=vK_PzPDM_LxES)KiCH%Mn8isEZ(M>#YD!i}szgI41wiJ7BqmOK zVx&!I0W27TPH8ezf@)oMArxD2z@+;mfbrg^D+*r}4)k|0QX-pRsq=TJGk_TF;-%f) z68IgeCetf-r-#&6-+_uZ5xdTu0)xS%c-c5VjWt6OM`2DSo_5Kh&2JsdGF#6Wk|`9c zsq+;!7!z+inq;U^usEf3)dg_uCrq>~e2<~_HO?);N0P$rO7c{O<7xD-C$7@VO*a+xW&1O?gDT4#@`!gg zA$2IyCJQVmHu^E=Bk6)a=vDod!cPiJnF`&(?hm1?#-nQ_Q|!;ogD61mwQJMTm$s0w z!3~xJ*BqbHs?$k&dpx3KO-#DT-W!aL#;P@_QLj;uAG^Kz^YW>XUPUVeM$2P%J+q2~ zsXxzhbMtMC55$l2rOh5=aP#!v&FUqfB4o2uNl$mi;??RJTyJQm0m=@=*&t1B%! z{{T@8mR564M}G|qWAdpF(mg;TGB%TV0QHF1U(|~3%)XqO>`{%Ulirb}K2&?F8@pnD z(QPbv{_>GQNWsA;@~2WvN@!toj8ZD^I0B>#Vn#tUiyp?o??-Ya4QRrEa7nKs5J17n zsnMt^K3^(EMOOqTG@HXmt>OMGe6;D}LNYKaRAm}GcBq#us@$4H+ha9ABZw%-!Oxkg zQO3J*#%eQ6p+*O4AKF-QJJVBQ*thCCjw%rJqD>)@xjazvLG4z>a3qD;2|1_R#g-%t zfk_Z%(#JhRm)L*)BJF?}Es~f}wQ&0V|qGAQ5%}Oj_c^aggTjM~ABN|gaaSm!ro=2juX0CzD2o>vBP3EEYG4gRwC5P9 z9y@LCK<5~z5KkB)gGedzHc1%pO)wW`;2M&a{07H9ba>-IocBHbs5vFrJ6I@-Bzui9 zu1Mf#nsvrUp98f7CV4axUP%6e;z?V8apzK?hBW)#>5=cp%D5Hj7=w{QPWU3=B*heA zc%TUnYHP^OKopF-2X1O8lI@bCA1DBGOr9ssaZQGNDJ1r%z1DCPWSU-63SiRQ?kvZO z47l+&f++HM?71p&K}gH@IHy36pKyB9lm;7^;}pgLSaX_LH!z|^B#+4h73kp%Ic|Qm z#Z?@!r-=fCfDIOazN1}2H8Fwie+*NAE_>6K7Tu33H$e~qw5xYK(^z8+ql#Bj7>`cW zL4tsJ3IsKGG7NE5H$ZZ6o@n#V3s9wC%aO>%Vv3^aazha+4aAxofWoD^5}Xd+wAG7f zC52w3D@AtGoDtfdfyXLMNtG3I+co4WtJ|7=+zV8~+7}~s>{Z3HmRhN{JcY*A)8 z9)`2zqUZ;WS)Av!Ig3*tO)0bk=SH{5G>zF3rVXH6#BIL^im^ydpd+^0DJFn8!6v5&*q=d9NC$cw zf}&Q#7Ty$N6(u0^H6f!4e(gU14slV=!jJ-p4>as1DP&yG*%)@9hlss4XT1P+p@bR(MbamMlFaG%;=x!H13 zifge?J4pmPpg3_UUvIpb6(a80FYJ=(a;2gSN3R+Fbstbt*77Lh_r!jL{{TuZj^%tb zOoT1S!R@ol@dC5qmBr(0$kJ5zBDJ6Qs@&u{ZYTJ-#(li&qx>wsx6?1J@8`8k=%bK$ z(Kzm;09Wce{#At6t@g{9l>lwN-*n{um1)rTGOnhGqjXZ1+z((!ApW1NUkr{vFO(#e zyZ1in(k8OG(=F~a;UC#$K=F8L0+l_m2Wo-UI~L5y$jm~L6b?=+jeHgAjaO6A^(_V~ z)U=WYGC?66N0S)I&yYKgJAg1v&XhE(nWUFvq~L`ihXsCdo^U+-*6efL z6Eu|^x2WctU*8p$Cg+2}U#=@D9;%UF#|_kqJWTCkamX2|T|ugK%x>pV>ANRYJpG<# zR{sFr70kNhP;F8ax6|UD+fv+Pds)C%DEu9*_ecjGeY0AjIYW|}Le93=FZWPW;*Ljb zu>7huuK41N%7erxH*=Akdim9bw1*iy3hOMeX*jbc#-AuWj$ zO-;GvP)H)j8QYL3a(u1NYA|iRj~-Mx12>SU zxnzBa8(=Z})vD@S-9F2$P9jT&1z?LS4#FO7NBMKv8 z`^_7veBe2yv80dWB$K zJ8@PBfyZpt88(@sD#%Ki?kG1(!E!3tLObz5xj=qXkWB?XLgT?g-+YG6OSHE_xyF99 z=`DV8R1;F{&<+U2TPg4)W5`QeR7@SUIyKu#7zz$)l1!+ln$FvkE$lktVie+_yZDWh zM*h)BrmS28ie#t{ka5AejUltkXUeBR7#toA86wU>pym}4HMO{3x-n8jc*aM1lN>+} zYs>*PTa}GN1Y8UXMu!5VCkst;lS-Ib8Kb7D3iM_p2AkpT!KSfAEDbOWnkvKxmTB8D zQo}$h$B9<{m8U86KNb; zAm}qH0M-C#{#6uw{CeA7FhQ-PEVk# zQ?j(nu@j3vjxSSE#5_tVZOtg?ljcYgFQSy>G zAM+IjF>QAcl1%L4<{OZf$&Bm?OT~A>TQtH#qZDh?NLnE;nUc(+kvB#kND-F{Uv|U4N z!H91k0QzJ%^#ZkhGVcbolR$PzuB@aFKk}}wo(fP*#gbIcwbh^7NVyIfvz6>mwR0}B z((La;Ey8ZvcO!oiA5s4RT-M%QOy9*Il~y^BZXk9e)K+7u1Y{DqIbPwcChQQ&Y?Kj zWV=WukP=66nrlTF?<21n_4WS%Dq=`gy+Q9v-#49ktbKo|VRW)HqduRfn&v3+w_=-; zQ?z&aJAPEL!yXAPr*C|me>%o|W{z9PdRIvd!q#TGk>gXuk_~_p>OeRjS~p7lw@I|> zuA7?SzSpK{em$XV7L2?bhy?j7cKX+-)BHiyJs9NPn|>?B(ag!ywENoIOYt_=;RbjI z9-P#dF_h14D>2hLjFRDv>m#dUCe zYo(gG!K&_LpGC@@MW#&P^O_vWr19RI#AN4sW8XjJUQrovJY^%?V!F6~De1XB{XeQQ zPo?T(_kTfPgMQcV->`^JwR(7DYHR^B*a$GvNYSjq6wON(8|*1S1a1o~9CV~iZ*nvCFsyRq`G zB+L#+HEZXvtAa(+Hb&7+nTLiK%uw1{4H6(ZElb!oKyc37uM|YLn;BK9sIl)S98rv_ ztH2a@fk)h^k|#s+SEG+Cd8s$8ykrh&5uKzA9My^17P%kcfH6^|jK|O6+N8+G7$X!H zC6{h1j+X#z;yunk8kH&$jCoUWft92#bIHi| zqr6u1{hJrfGpPVjPX8p0RKH!-@oFeho{Bn;~(>ts~%gf@pa8MIx>{ zik^#+%_GDGF5$L-i9q6{A`aq_R0BjD3RcJ|;82g=^{11bGf9EEfdm=HG=v^#gn&7q z%OD-GMItezQ6i|U>|2T=$VVsBG}J0&qUr`u6c&@-fb!>$Dnx%?)i#B)Ks-=c$!vL3 z!5e$mkS=+m1Hp+DH)ajnwI3=yD0y7>%|ZyQO8QgKW3@LR91LQd90A-^Am#(i(h#&c zBp5g}HIR1AM1f%(4r!Ztq~3E+RtM`umNX#)&XZ<)iXd^E8h4dTq96t#ntH^(l>Y#& zC}1jCI3vvwJOXK=5Jw`8>@!VgIKZNt01~Q$0)aY~0~H~?=u`CwH?9<sCrcOvCK~M+nD&{{TbB}a)Hmc|F|N@tEJotO^f(XL#xk?Go@S=xhh;_kwZ-t|5xURRZeU^C5gbm?t0 z14)Z_i+Khm`KdaOEEP(;@% z-VBWU*%6qoIUE2_Ij**oh!`_{qiK07+e#)-2_9aeqxBh%OK|ZImDeMWDw%cEJsEeQ zTL;yC&2hBuc`ckLfKg4MI&z zPq>iAi+n>c10XW4f0;D8w@Y4hyptih-Vm!s;1Q9Y2su8~ewO)qi&}|K21sA)l|SWO zGpwzx^)72S2AlJurc)cvHEcN)Mx+$68iPCHxB`^v5y+TxG z^HNY_^&g}LGB!x?*+TC=T#s7pXmuF96>{Y!zk4%f$2C6OG9SWwRnm1n zo7E;VJ+7@gTF#q}9}%N2euUziW7b-r>#0tusYxVKYE5q?sZZUcdzb=*3@+R+9)5LO zq&Br-HjSpwI<=mYa??c_j0X;Q!9PGMMzw8ur)ziijb{n_P50Ub$zzp_lFXrTgOYpa zj8{@m4oOROHza0CX)55_Tyn&2nbn@1}wx&~a;sfC`O|?$aF^sZ<&0EK9VSn*9{&sloVQ8UsBO@%aM5jLB=CfT< zsA*Q0opq;ADHpTqh}J+xPVy&5UuvO48ka<;PL#nwL*p zoeE7z-Caki3#+MPU+*fAj(GV`h`4R~r44eL>dxjQ0~o0?DJn zbu^8_jj`d6w_%bw?M<6Qi!37N?E8K|hFL|mA9Gi%>SD{{CEb6kEG3$VTDuvV<+}FJs(5fyj z?c#Xm7be~Iypkt|0~3*)4&3pRS!L6`Z%x8f=_mOw_$*o6F458!X;g6SkEy2%hsh1K zhtPjY#A^LRtxNFtrmu9%v2A^Ebv3oqvsOsGdv$kVG&+onCHP4xF8Y5YtVK6qw0Eoqx0Y2!B%dcrmfpe zY7in4l_4{L%%=qXaf$`}obc>x{{#b47?e>W!b|aQJ055WL z4>8PC%k*KZHS`YFXqR+IV|XVNpLN}1gr+Zx(u8wi}NziL7qIgk@n~7PguViBE)mZG-Om&BOi>~$1XEL*#bQ_L&(M<%43XU=;H zByffA%}wCiMvV+3)Ok|PmKi*HQSM5wJ*qKs-z`PYA!vL<^TlK|2dVx+<>%y&Ofm zuQcGwam`2v(t~sqk?50X`cdLhRT%6kPBBu*MwGFk6*;Lc0OXw0SOZL$2!KeX3=eO%lE5=7Oibt8E2A~uP zA|&LRDvSc zXl=nHh$wbS)eK z)Fhtm8U8@mI@Cxq6Oqg>gYUJgf22yMsi7OQy${#!)w(zGm1bWt)AW`}^e+Q@6C0Ib zp1|@yujN=Cv+2z>%Y#~d4(r0acsnf1wEcUJ`uC_^N(06~#%o(e zF_Fr;R_nm(tBYH!jq}__z8ND1O`EcK;~*OB^C_R9E~452lEu^yGlf6pT+c?ewmQ6* z1{5+!ksA@pgTcq)#dS`fCT$IE$OIVOe^W@K@(tQY64TmiXNL6@g+~NFILPw+tEedi zZc~*!ipzXH2_}wEj%YxZWNqrq*qcY;adnBg6B{3LqdcNMy` zRBMRVR98adaUNII`*I z8Ap{|{vzgbgws!hb0kqn%I_L6Qv8di@ zTEy1!-P_yv;h-B&XTq@O1QiG9!mIitttILmQp(QrNg8xlhirEuZR@{jVhF&;3g4Ay zmrm5h%DLP9M_J&~`kHUCLH6aljvyxq;exzmamGIirq=pK((>`H)fugkoCa;k zrvQE<9;BK$uxhcVC3cnmD3YExNszVvkF~zGw+e^-p_>HUWVuX5f-_c* zdllObl+!#_XS<7B`&7kO5vmy5x1i(+>ggKaV4$1 zw#{uUF|=>VK%fr()c9!06;*=&(?-rf31zeyZ-=tByenL*(0?& z3ux};TWR9)qL6^-%aFpE3{%M|L75o+#6{!}*C*bGJhx)jbg{d8S*47s%$VGn?m_Mk zxaPT$O-XY_@3Dx|dScSY;vTzc0tn#KueSuo4K8 zu4|d!$)siOn`f%&SJwAd=_{qI&jDfgAH!@gsvQRE;(vmgtH&cO=SGn?XXXk!kHidC zw?|cbqZ^&5@w%}*4p5Ie#QZvx&G6Fx(r0FpOG}suC+{&*0sKvNG*fE|Z^Y4?GWq`i z6h`T7GA^A>2J$xo9c#vo(0$NVRipfPfB3xW+VfD<2gZFzOrGJ}Pq`4q1SIf|(5!<4 z-(;0W`lYPB5z!!CLf3F=c#;7i@dhJsa=p|JJv-HhT6mV7c`l~_EiJyAWvKz*CK*U! z{{R4C_}5owsg%l!6=ePF<^Pl?1DV2nrTk@j-f0b$U$e)SGeg9am1nmmAutmj*V^wR$+fCp7yx z(s%=QBZKv$6$ih6TKeVEe*C*vVUaP?<<1(Mt2hIhP1(#Ki@_1Bw%~Hx&2hxTX+u z$7*DN+Zv|;;NqI>PZ{E#!=BtzG0y^-0>P2(#dz`kD46FJ?S~_ZU{Q4nl5%@dB>)N) zyrTrsN|`+C1MXhAN2BK=l*TeWY0;+7tvij`rDGssGg9s27|5pOeQDxF1A|K|V^SVQ z+IqT^p7jx~0q>gfx6Qa53KUBvG0SJQ79rY#j0mSRs)|YMD>kQ5k57%{o1}}e!Olu6 zyAE9!?;Lt$_ooL#$RqQrS*}cy0^Db?9$wTL?iwT|ahmWselg0jwT=sKgSj4&9jkNX z(C=Q^6v-b0gUw(%&#dkGX5P_cic72Ouw;(i8*UIBWt#^XAmfiJA4+x2r%?%@jc2jF zi8r;p$0V+P@|OyxvL4{#4*{DwQr}@B{db+t#D% zdek~x3pSyAw(z{Z6inGWn;FLgjCqrQDq{G`GnU^=_YC?&ioM2<#2ChDk2nDHRVLIGj7ewuEUMf#P?23W7M$&E6PK20=TWwws>R*5NpnIDcbgHj_T9tCSzF+S2v zs;0Z5aFv?`=9XWYG6hEQRd^)ky!QYp1399cZ-xdS8(NiE9D;BtVpTo=07`Hhn>A&M z5kR7e06oSi83=hIqt9^|I0R8qvTmT)S8T7fC~RI)%Md-PERzN#`PYQ_;2er&3PNt= zGwxC{*`;>eI5ZZNHbCz}GVL_VHVY-i!u-VMg>b+&6!So9hQjSp-+@8Wk?e(ZX z8-5nGrKR6qXiTxjxNGY$xY|zz*UJYWk>p7~OB-Z6!E=ln-F!mnTh6TMdrMngVXTUh zW=7mnXvi7J7|sY%Gw)oErycDtq0rJxZJ#DI$#EEv0uL&{j>Pd+((12kHJ#?>1>s!m zBO@UH0FzXIh#fV3(>mSOn{x{zkDeSp<&rb^NI2*G>p!DhUg~pOCCpoV0#QH%=3g($ zxcWxnaPn%;8|3T#Evy|!O-saLWr^@x7|8^B{cB091(w-nj|51hueD|LNFEJ6ViB0p zbA>)x9x-k9!9XDu~;lGJ*NL8fZ9 z_ZQ5pm{oNW4Dtr$0lRs54l_y9a^%FPD8|?JbU1N{p z(ObunCu_++sO&$}4!O3TX#^zbP|XvAj|8g9axvQsGgCUMOTBTr9BJCM#4RdZD$Q#t zk93|QD=;GmkV*PgP3^Z(TIl+Iqo!Zn+e;0z+Q@+cD;sVZ!vY3D?l~Fahr3v?>E4{w zG|1vDqfIWsC@iEgQOE~4$o(mnY;D2Ct~g%v)dec<6(0&%gEH0MTcGh0bOQ(o4 zBx;SoQV0Z<88|g=-BUb&4_8>U7J7y3TBx#>&h3&R4Tb;_f_`3pyjEV_bt{g7xQ<9t zY2npH^w>K=-MkzRF6NI6(9O{L(+HN#okGrAdp3RU6=dY(<8i@ac=M*7JTrFb{J?~z z9>&k5^-HZ%%5S&96}{!Wiju_{C2+{DtTWFl-=$jJhMP;ivq zCxYHp$ATGwQ7H$Ug(v05e@edSnPr=$?7UK1NMjMRf3wH(#Y6b9q{43cUD7zCy1em} zf-ud3FxVbko_(tKO|@M<#Vq4Hl@pdD89mAT&2&11u>SxH2ME)bEYE>U)s?#>UCDg9 zZ0n_1MR%yVQ3;rz4l$pZ*a8c2RD-FZhvBWSRLN>CZ!Tqy-aA!tiX@-*za97@=`|Z7v8F%dIdN<)LkK(+Qw@ui+S=odR;W&WV@?K!}8oSTVAf!VbL|+ z4#39{w7IrP@8>yZiC%9FN`P_S98)x_>s?kkF0Jj|7Af5nNsYh`&D)HOA8gc?r%{g@ z$vE);0Jvk05|u}}ThV&6Y8J9vJRS(ONTWt$%!6kn{{XxX;flXK%}0jH)(OP$+oW;? z+m#sOImf4dYC-j`HhhuEk;*Ep)d;x8m|IC+8FS9xF;{W0DhNIMR((nRF;ld;@WK~7 ze-2OSUeDn^wdl|IT(5`Q@i={3MQhc8W)5?j8=x!*&w4OX-x#Kl+2X#Inda)x0kq&( zn+MA^<^VT(@NY^a1XO35YYgO33epfgaZH#9?17Io(;iQ?NeZOWl^%eKZN%C{5qb`5 z-T}=^MH!`N$T<}^l89m+az!gc=sBr)0Op(*Cj-3)FsO1z(}^rrtz$q3H5Sgy#&c5^ zCdYcCB$H!BQU@ch?>ZX+A;xfOK!^jDr-&7gVM?70kq>Jr#_lOXmONCsAsET>p~sE< zb5n6EkRBU@>qIIQdm1|~dx~R3Nb}>9RpiSYadMO44H!1*AV}<3;NUObK9tF)F4zMA z3;|7f7k7l8baVChsOZ_aan3L+D z+zN~p037CmHzVawAmXHyD`dcb77sP&6OZ0HXFT|yVL83F*AgRgklXkMH6Jj+$oxCh zJucEIF7)1{(e4`QQ4BH!OfnLFWn~=so_&ThT289g#g|j*`b3dHblaJ!SKB52uz9#JVA1ph+rDuG3fB7bc(n2;xe{CX--M0tr_3u`#4iup% zs(fGOg!SgrQg{CV>223~)Rx^@($M&_y~T~TCC4aTiEIK1&&qyd?^_%0tLeQbQMl7@ z^+5&1YBy#X3W7#)kfeEv!u606UUZRQ3mX}-*!$R#fc-y}SbR}}-pbO$O@a4n+LT|v zAx07q04_(9J`dr~n5jKB49^^9xUcFJl5kRK`8HAO8d1?#TK%B{>C!MoottX0=hp}L zPvuUw>Kz7MKIP%oEEynRC1E<@GDb3U=Unqxvv~d#O>c}_4NBQjaqGAr>G;(itJBu; z>l#EqW>B{_Z74y;;v_0FS+Ml2G}DulT)O-a^VDQgq?7D+{W^5IXs3o$o;gSEjN|`1=t++S^}i0CR#5BkS#4-=gm3(KODs(rt;2?9%V-ti9vHfI#&B06f-jrlOyx z?6kdp&7?tfcNMM5=OSfgF2MWn2tHL)=;`nIyLS5VWB&jW_~Y?AgcBDVwwyEaaoUzC zcSg;e7Jk*uejv}Nbr-~2Z5rQFyMo5j+SFR5yoCl`Ll@)txhFIu);fdyB}=P$pZ3AJ z7WWqlo1QcUvf~2)oD831ilOw0R2r8qujze;TCQIQOc!JcdHJ)E?@zRwbR_}F^UZTV zhnlvhCEwXEuiH?yo;~*SNZXxXl2<**Bi6bdm;K-+hZWD%>E)%#>9=N#a#o zVUGNohBJ}QJAGbnH;;Lb~^#j0I2OlTni@P zccH_<+&IAU6?`??m_L?p^(%NWwaif^B~~f&uVdxjp0^|t0t55y?t4^gQr<3dcm{^7 zR!!&EC*HKft?Qv*;=k0b=&(P!iPC`JlB_<4qpi9zI1act?hQy^nL~Pt13t~C{6{si z9}@K}ceg7jeK$=m@CL9&h9#Si@Q?GRp9m4Ok)KsHf$I%Js5Fm=x?Z7qs7%%u?J$nj z9QY{^7{FfH%M;I-tw&qyQuN>lNCN{twc7sx*t)8o zIL2x3UdBh$w7X>7TB74ICO~!lf69cAGqL4Pe(o55TIc;G)V)MCn@b*`k{i2yL7qE% zm5E|2A}DR7C<7QH%gj~x#IB>g>0K8~z0>S%uB5uTmN`6N#~W2j;{ggYUl6ppx^~}HlG@_l;#<>c_B3Me5{~jQz~ivSJN+wOFN?MB8=JzK zPP>&$qhL}=Ow&4*KQJnN#W*nS!8IetlW`_SZ(dF*&pS##cz1n3sw4jZkm8+B5AGR! zJpSze0AMi~!1;ma-@Q(SqE+O9Kr$}fp%do+0086kp%A2FBvzRJ0OSoE{?r;PeFrpq zNfM-*HT$^lN9tdW-9OV7BGEKGM*8rs2Z@Lgiur;;IPc|Ez6!fw6If4+y*&l)pKqvc zVR3ABDbEF2(QrrOTCw~|q3TkOJZ-5y){37^&!>x&>Wrse{7C9PzP?Wu${QO(*n%Vl zf?)D@AIzLqehn)2H_Km7pFOR#8rcmd5vN(t3}IFYnlL=ZKp_4kAFVRx5gX?ocq5AH z_^%=~1T96G)ur3d9QI>UxB5WN>KRk4*AMayQ*0cQwaSNb|E9 z9^V9!f^oO{3fn7S4|?W(8#38z5HzKjK4UCSPdpA!y>!Bi4%Si+rF`?Qe~GDiD~5VK z65EMI_#9=KZLXt);pVrJ5PKh)O7LrsXxev$WpSmHSao|hNg&GMiX(`qw(Rmqyn5V^G$$9bV5)*EFS-fLQ`OvZ+26#m-Jf-r3J0 zo21TZN(-~E)S8&a(`KJv>P;U+x6@_RWVlAuEltEr6eYmH!BdYve$_nQ*{+5)x4MQc zIf4;!BW**tWk4s)`gf{bZ@|~Je}~gbtDDHCjn}c<9wJxk_p2|j+|GU(TUaW`KiE2! zXwo+9`SCyp=t0F@4w;KAVEZ=t{r>>4lGI8zbc-@CB3bPiPa7`fk~6pw>JMyET74ftxznvK5h9ybOvWRFD}eg|VmoDz9#lWrdRoCeQQCxAzVO^m zG20pKiqCvvNpE_H+dxl=r}Xqrqs>@I)BbotT04zu%FCl;>PyCzt=qs1B2CSiJozJv z+m}l?;>9d&+`IlIP|BR7rP&ExH!sF2-+^jDkCS z+yY4LSm&hFJdut_zDpyQ8jEpeAFH}T>hoNiRkoU2iJA9_8e`(F4n`Q9;CXYJ)##F3 zzL@~LM;8&U$v5{Tal79<8XrkrO{w&?hM?-~UM1eg``xqY&OWtd6!I{Bly!Ns$$C@! zuYh8VZ**U9wlj-+nB_c$JmbDA3YOQm`fpNO#RA#fNvNC1;N6gnS(_R4?~f|mPN84r z?SN=tDBQq&-LvLuy*{c~6N>Xs^cr~7nrz1ConNTx9c!RnUg>t$G3ndkk)kBD=2D@F z2etx?4*vC3rfQa&R+pkrcMS4FrR%e^go`X;V5A%zbHL-;x(C>7g}}O7XobLR_?vnE z0N385-03zq0_VktKWPKS=eZr5*!$N?9Zwp3#+M)W_b7U2-pUSv*0k>lBHUn>{{D8h zfUnG|FyA&icdM&JjC@_{D|w}8qPSamS(K2$Oi_Y==TPD4TRU>HTg>sz9E$NsP!fc9 z?TqL8RfWEgKhjw>SmgQcBiSzI&Pc{NKEo8bwK!!t${%@t_L$MfG?n-=2THPoqqVJi z+(x$=UZWC1xyg*J%8p6F%M5*Td)AY#VUGU*O6j?r0}E;oZzfn|B=9gIsJfe?((hIm z_7SG%ZO<8O0N%BE)R5n|Ojuakg_rD?5YHh1mdXqbwV`TpyqM;t<^EiD6J(N-uVWjm z>jzu4mT5Qm`c9EyWpD`p0KJOv2mJUyjc7WfRld`IYqpXjj__-V8E1)IOfp-U5CD5& zyvKejw_DR(Uv*c9VC(xqQdCrvAYxe9ws`a>9M`oQ^p@uKV3Mm(HlJ{TqwZpiBe&BT z0QuxsPI<8AuFWqWp`AuiHQF7`&Bc=~>c%f1nn*4LV+tJ@jN{w@e;S>}J9AmfU0%*U zv(>DkFDSm$q%klADvf~T=hWnW6{Xn4O98;|UT0Z{W?PEk@^8Z{h}uOYFu*w-sZ{*K z%x0Y2NX!VpIjpdgi%oY$tzUBL!sJaBABP+oD=-I>R1sX5W(7ek-ztDRnz=>XGTrO! zKMi#mYqXy_<~$xVscDvx>uJ7FR9X~^{dXw%fQVBOP;Hbqg zK4*}Q2HG5KRY>CohQI^o&%Jo*9h8NQJV(YrH7Za=sZ-)EM`k~S$ss4FWw!p2*LI2e;(#MTFhE~@#Zyk z^fGV7ET3M+L#iw!zW9NzC9TEMW>FY;SdS0|!5kh(9`!cgMYy}^NwoWAJ{%ImDQunJ zChp$7>whMd0o9_^<0d&*JHq1+!5sQ`s|@qzf2}5Nu_;D7d{ASgNjo{;Q0bSqntp|= ztcoJCjp8b!1On`N{OSi-+3M4EZSJxsk9E?iw=yAMk%97nc=7{3O6o*#bLUNXqHq=C zM*v^~pw)Q#&PY4%8EGLS2YEI2sqo|WbHy86+g?K?uO0$yQpY&&?fTHQ2;KFLoW0oV z3^rmSNEj!@EOA>qyR9!z)$H__jbpjgA!!gSV`~K`hCaL#Qe?BWlHDL{c+63q$hhH8 zwMlgeVylfO&*aJIsXiZL3#Qs#P1M~}Zm_Z)MXr1>{2VuI`c`&n^>L+X{W(6H6cE_n z3yCf_<)mP{LgUMd>x*dv-Tk;lx^Sg;u1-gx^8D&9p`+>dw;nyjAH4Hm#BLt+(CW?& z?vD%)N}p%g#ocN*8V6e0btJl6*Ou~Fz!hU~hjK@b=gXR{=?k09Bc$&_>B)5~NR2dZ z3ELTFCm26_k%R4BI`d41{=uMlkAn~dGV$U19QG9(O404K^qFG@PaEZZ{N#FidegqG zc#@QPbg)09Z4};ek1Ckg9@l=ue(`?OkTjfMm(z z6_<~ttmD61g_cc<+k|9(ne_ZCOKpQL(lQue^Z3>+cB&ezs!{w4@WJDYvC=R|njDrO z=eKGUYGjdDwrWhVqKN=bPbx5eTpZ+n6bO#Vl(zs@8RKebwl2`JBBXnP``)9PNo3!& z0oiG02ViD!Njxv0rUC+aWSnEphDfyNDcDvUZ9pTEYs((k=E?1}aqe^dE7PVna(P_Q zScbKgxf`TkIPnHu3D z#7kD&27MH9QHvH(r_`L%q=YM}=OYx=p_eA^#7M+^XWS?02?loWyHUp0t!~8qLsWl@sP{|Lf+S*W=b7Yea03McpTN}bh*nn`uRz>b+aPIo#c-qDGoR;O zycuC1;hTM~%yWxtAxGDdtQ{lKcTi$Cj`hJKgSn6#{c3&oiuC2CFd1O(y2vdmFQHb2N-La0pdTUj6?7O40mHkUzuC5Pj)CXn0xbE4_mFYP;fG>Mkwu68p-Vh@}ZJmCHq z^R334JXiJ>_mDC0t|D1bp7>#!=h`*2H@$PH#OMf@MU9kyi|mXu`c>D$yTxvju(pzT z#NHn0<1ROpz~m12&TG&0O%5fCb|~_y{{S>?)8%`rOxKkwfPCoy=DZ0#>F6@U8Lv2( zJe!L*?f3tlviUt`x*id(YM&HLa=f%6RUfyZywx()Ty9ZoRYSy=|sI}?xAr|UMOP|$8ISOQ)$ zQ~jaH{Y83ga;9>n$@4w`07vN|8+1;=anFcHZ^pBluTEKH_##9j(Z9~Rexn{)$pdNa zn#<}+12+JJT2i!CXE(glZsWSPiqbWmoS4GAfeAibo3L};ohN+3NVs&&+c6gqovk8COD*N#NhG9{95 za9#`a$@~}!%bTQiMHwfK@7bd0YV@wEL|=yJ(Da?GYy!z1QSs_APm`t~U2!!_M@S?R zc!f-P$@AwvbojM9t5qu}OKR|fOE88Nk#c|8HRIJ6g7Qy0n^&j;UymD&VJx6lWmkoF6LmS}m;FeyOKgY6{kutg>xM zcQ9achVDowAC+jHY?T;F4Jgy?>{l4Z+fTieTCRy~+QzvUP&V|^T*vO;XJyaVkF8wI zXAf2NFGm~15(~W^K_EQt@Z6kZ{p@};N6|GFzG?o~^W15_VVS%5U`A909%GZwerl^! z>CIf|I%8VhU0Ysh%PdH+ONQ9&azRt(JBoPfV<#Jz)Aph~Z6`>k)ul6hH0ZdlB72)y zt}Yf>5rUR|iH6DZ05I?8SF`IHjNb^f`K<3QEUw|z(itQV`^|(nP{;VV+271m7&J{* zK8&@sw;N{C?;3dGnmmWf0oeB#BxBE-Bs1Ji@b^=K<~N@9eM$?5l~)K4h+Vk;5IG!r zih1S5IL4{ezumv?1sN{>#)qV5y;Z%^bvu{1ys(*7q<_4~@uZ8Np=KX|tK5E7pRu`H zkBB_+jfVMqbNW*+&c8QPYWA9K_MdNUZqS(R=CfxT9yMTh zhX*;`-!){X<^WYERQ2|y@b9io)uxeUs3xhU+cb%CtT;xHWMGdkGn(&o7{^hmaLTtT z@?qCGLlwihC684?36o0FuLu6Z@#1gE(2v%H`t8M{IMjM`RDnq4p=hTA{uj^bQf$BN z57YMmCbRJ7CG~r2kbgpe{VD5!$s&g0;yAWFMNPLYsv$ zbsfbj&#?diRD7VSOnWXH7&L`fIW*Rs6&z-ST(e+MNrupABMiK8NgQ3WYsgO`i5Mpt zpwReHIy{oYa*$l$ zn754X$8bIVwTQLrZ}fwqH6bO_!+RPxhzML2JQ0p~{41NEHC#|{!Bb<`H@1CqA{ivR zwwZFt5XfPU;d_pJ>#Fqrv1_c@i;HPdh~h<4osH>VFj!tqaj9GB@SwS3;6kzm#^5;a zagWCp*Z%<13m%w;4L$9x;kaO?RDnvS^1OgK+vVD`=<@7`&+qz=RF0#!CnlrbU8TI9 z2_+?U-4Q*T2j;=`92|T58iUiivuc`jcZM$rIN%zWs@lb=SsQsA#~>l%m45V2KI;$i z^8}97e!m7x`guberqOO0#u*f)*nmz?e{)bKn{X&c8RESS)P^~2?jsT1!gkDn@DGy@ zRU?u9V2T0Hkgttr!96}nLVeF)icy?Yqu6%fJ9AB9*d<0zI2rFridv$}PYm#TRJo)E z5T`kD!Kj!NcPdo|cmtXhXqI#DjZ#Htt8~4~31%N>n3Rn{U~)zWHJ*KD+gR&8Ds3*p zZ8PU6)SYhjeMftzX?iKWwbR+7xSi+N?8-9bMlb*XpL|uP zQ1vyImC_bE-LR%}Y#uORBn}WxSpXXl`YeU9y!y8Oh1P z$349BQ}qC3=-!&QjhU^}Qe}X!BPwF|_x^QGT?U;?9Y_9n8X59!cKJ0OAJxaHI+=7y zG0kicAf4o;ZWAEi62v|GDAt-RHA`6JXWC6oPT1(bM*{lLiO zTfRMi3Z@!NITSJFU6ZfWe^zj}4|3n)rm<;j(Hc8gUrqkCewPX$Q{nh3JFy3ncMJ@9 z8o9mKw9PL~)9&?)$n2W_KN1uw%v@&$u%O@rk(23I{d_?$!mT=3BS*GvQs3;-U;-mM z$y4croQj98++FG2SEESUMzM!jNA&x^LNCL`BZ9$)QY<*8Sr)ak+Z=zWZN_cHq(`o)>E#2$x4w~~ zQQ0xH5I>!57?7lc$Q8lW>DHT3v2wf{F=X{I?j&5vD$0Ii_i#D#B7>G7hUX`fT>Gze zb8n~W8l1tWNN*C()&nb~c?udwI~hwH<#{+8YbAHC+*{~&`jnbrUmdWhP1>e8Jz|5ZJA8z>2N=M|YDGq2&UrWk$k!CrHS4`MO4DxTv9@!l z#L}!$-d$x>91Ncq&(gX_Oxiu3ttGUPMlK_~^+FRp(~P!BlZQ<1U3?Odfcr0dD2Zvru3`6T*NSmfum zD;&2SsSnbrk{nEb)@epb03P-4Iz5CBb$K1UV=p5G0DGDdg|v__$w$;WhLZI@k5((bWD6>Q}{Z;M>U9;|wGUYesz> zWtSR+np>;tEOIpoTIP!Z)7HA8SUsG}JdvuZOArs^DD=VhJ&i}X=^IPQ?w%G_wzyJR z7v>xC4oMxxMtP`T3-uGCbf;3=NiO4|TiiS>dzFz1jQ%CZ=UA?Xw$vfC*7O@|!EYVi zm9@>pnG3Z-c-YC|ameKJ*wxYLxufcQ^|jY-=vSy$AD6l}+J8&SX@3>p+A$-<@Z2&v z-L$JH+DP;uVxU;`t=5;R+G;kavdL)P6iOceMswSqGr7K*tv^go^JrF{Go9%c-SQvp z0*r6T?hZ4btyz#k!Q(Z^$*R<2ZZhrL@ij*rCCU_0A|E#tkf!oMIKa&o)pB_4T;~T? zi_mLXXL(B)(#a7H-CJn~IpE^KAEDBu+ix#dX5 z*0e?NQu~;#NWm2Jv^OEy=z?N{mKp^@0RW4bho^tNNm5WiY~*~hQrS`_IXqhpWRR~g5c1NoX? z^e3u$#+4>LOtFvs>fh_b{YHOEUdC;r*fOUy<;foPBKFGa%0V5}O%s*e0rMFnxZ}vs zP%@r56=ITV(2Y}|v6jwiN;Z4>P+~&ICZ8h&U5niJG_58P9H}_PGhjUD^{*f_Ko6j) z*dUC2kZDwGA1+58R6xFU<^W`$(xDTg70rc>*Ed!+vw!!ntA8<7S3N1y6P(GSS!etU zv;P1tD(*+me$>N~coZHOOYI?+S+2~^H%I8H#jENzgVGz>G5Ikx@AO*gH6LW^eO58) zFphEhfm%4!p7?g4(PvWgZ zcsjNO zwfUN1*7HZYF>!T9fhhrXOL30j@}K#!Q{dDrZa9MW+Ee}sLw_pKT)_pv#l$hpJtR=y z&Z=(uYo~5=<7l=mpYFmJ`C_@begwrY{ydNLG{>&Ly%2o&10&YFO+;>r>E$HbLv?0- zZSLe>=1nm4l!=pT)SAp5f=L!p`4L?FzXIo9Vr%m>L$2a|(LH|m(vXpX$2qF!(E5%Z zVCrZm1Im3pSM^q*HMdy9l3>y9*T^igMt@OU?7s(f2zE*SW|(z-JiL~tk&gb9nBWst zEql~aAN7uxnSeZ-d&r;k$TY^+#9ZCz+SRYS*)bpZb67HbJJd1v#LW=b;FoC^BCx+0 zqKXY;K)x{&vszCan^3W0jy%8L{RLFar>E}`eJAgUKqr^eW1ex_TL@XQ2G+`Fhrz9gB zccgMmeWOse?`ksSp4I5%qN$eC4X$YF!#SLC?`7Dtm6nr{Pw&_L`d;E2uOm?cjDqSm9fT^UY_(mxb84?|moL+BUIq`#(w(9|?X?Vk!cUK772m&1<6`c{R=Ye%8~Xgqp`v zY6#QZDuh6by?n}$qk=nRaqKIgSZWYnS*6Sy_AujekVmo2Z;RraD|Cq6MWsFDNq1+3 zy|??GLRHT(pRaIyk3PAkx0NS^OCE51h`~}gAdeys81)9VUNAk4XS#yi*-HAn3R)KC z_B9zJu<=0k1BM=8dX9Xz#GMoPtZz0Y^Do@$blrEQEt2fPeih&;P^&0qAch$P9ticM zA6of@qT=bEojXDv_@zUG>s~a6znvi{q3W)lO=9BS`ZA*C-Ab&*kI`fasa zt6S(md#EGu!n1wR?fL$-7vh$+tZBD;9hQ-HEe*s{GnI`_PUFerp5Ao>rL~@;8q7T%uLNH%R2=%g|1eo^cE>$@Bop@KP7*KQGwPBNR0-^`WPT@!7n+{b@!Y93sE`2hoQ z_56J52US^GT68Q>>hQMXcl(9i{fLbt%G8Svg6MlTM`;qurc)q$07m>pNuAXO7||jwO~@!P?RnC-cTDMbupuA=H}U z&2FW%k(NUm7Aw2_N;~7%6=|e&gqnT&NQN;AIDNs1Esj9v-nKvtd;YbrTh(8yyBqF` z@abZqQbrS~G_6kN*G0RyUMwPbmNie924Zs9_5PHdVWDbvx{cPYqH33xUv6EuehZPf zdFLKno_)=43ZGg=1dIdt*FGIjG$fiXdU!QQH~o{S-TXh3O0zy37Pm3n#OJsu2qb5| zGm}r&wEaeXH%-^{$>SF`uzj9D@rP9allM+RABU%{bUb5>;~Y~~G({??p-JQcfM~<3 zWQ46RFZb>Tri(efg|*Z4Z&2yxH@UpMjb2Ina%Bv}jz2GIb9kobr{@v6$7Yho?t2ns z2al#Xu8Ry%#0ZKcj2`<`59LB_KFO{A&TXUcx9{2p27P|C>#I9_cmAaQ6>w!VBxCU( zT)6Pv66)qjP&mV50~~oCD`)`EBl=KHqz`&7%wWz2-P!Iv{c1(uhi|FLryGc#>Lgc| zH6-i|GQ8slk4nd=$t_I1M7u6aam8a>Y|dZw{l)ZV8SZSQgT-l9DKx}z80Vf_zdifZ z2k2k~mPHoDTl9(D^_EWk|@!YSE8hsK*=Z zg?M1umY1cT=4*GopG}cqP?7%t^Gf9qAG*Ie1M)R}VPux3;@~sOad5%rkc0CO0*ntJ z0PXH-)(DZnb_C$|CyE@4;oNrlSn>3w%dE+gJ7q2j9C3@Z2b=+#@${lyk>eZyJ!n|t zu^zR8UCWabHSF_Ck1SV@Y#!9K8yn|NYq{*LE@!q`8gXwb$Km5}VuS)c@@w9e;5|6? z_CNF~u}#8COUEL(+M|Nqe%Z6>Y3v%(%v%SInnAn7$dD0?oN~lxit9Zua&LYeUBxzR{8;6J4eVW6G}Z-Mr2R zYPTkA5pa|x_pkkd7*3^Y#TYK2zI{7Sxq)}*sWi)YRDr>0r%>7d0C52PYITfa->9{y zfFs4};gA0SO#cAnDo(MaPkGbY6rK<(rcUAHEr#c59C~C??`-Z9s0Hb@lV!$|Eh9uA zm&_#rAB!G4*G+5jIHmspZ8iN2{*2RXs?@BLT!W*dnl+Y3)1-hLV>?|!enI~Lm2J8O z#k@L}gfp{Czn%!PN&VjQv~%AbzL>1unw|vxEEQzgr%58Sy7vrXz!)cywjVOfx+7m2Dv{*dZ@Cod)Qg2I2gxDo`&2IQ;9Vd_=cNbRjt^3@#XC@|U5ZlsAZf~c#{;z6K9)|PS$aWu~? zjmT$d8}aXf@4%_XlN?n>gO+@0k8Z7DH}~LW&(X900KHfwBw+d1Nch&E{>pAM`_Ydx z_bZ#^4%OPx^Mx2QFP(=n0J#SQ(*tDWj`Yx_h^Ka~6jl*WJkbROd8e*@c%^74FBBS9Gqs5V;S=_po#<10 zq5-2BH1)=5G2{mo;g#lrI}Y>Y8fWovDRN29X$T&sjUd_P;O3gg&;d$$rH&Vn=M^|e z9I>M(nsH&Yjxkae-)C`7FrxC<$)w(f+C_;09k~3dW>pLHqPQw>y{JyC>$_;8sj_w> zPi+O596s&`gmAkdRFzv!pa}QJFU7fB+t9eTIj5 zasiG!1LkXIr)cm%qY#b5_=x7MV1-i~SD#wU^$)>v#%46$l#DKJl*riiz!fceV!#u$ z5PY)Ptt!Ubdy!u7l(u~8uTn*-D5SILOE2vtP0jDxqu+Fd22JoYMh}qWR=VGU7d%nk zB0(9Y^=0l#N5q{a)^}DHQYNZ~;y6yllYY}5d=ET|zUpqP)AbD{(S^&|+((9Gh+zQp z9<|BF=Db+ra8flA+%a%CByc{Tjc9b;b_+-(xLB2~@!<%BrG0tF*l3+gr(Z)Bjj8n|#2R!fx-#tFyT{$z?lVcJ{g_Fh;9{zo)rKoUq6=d1L^bNkX zV=eHtg5)&B<6z0FsM)iOAEjab6lgHPrbDUS#y@D_qcLU-7sxW7;vcByx1=Z;&o$MC z=O3J@$7z(hwmL29zy>tyhQuC_IsX8={{UqF01x86l^}EP^s9UOcrPuawYfngGOpHN zZ*N2ApIXjYNh}vnrrx=dXxA}HMm%`)jo(qgZ+?91=bsdGKTc0oiE#U#gQe@YFG_Fl zCgA402R_u`I3)HogBkX(4AMKcbFYmLEL}mlJ!@_@XE5}uYT)~ z*1bMN{{WZ6<+WYcj!teTA6j2Gc0ubcSE%m(A>HXZUZ*95HXi4>YjT|!={0*_QnHWXF zj1KuDz6DeCIaBcWUq(1*u$LL*pBYVR^wh9IGARrQUvwUy1q{1aQ; zPPzTU2EoBAoM*LlOIc%{*(Q&akwYo=HOF{G{M&;04V>OM$>;+>8uF-ke52!M^<`5sk*>%C@PpZJv*P~1f$*xp;D zR{}g_Z5Yoz$iV$;dx4zenhvF+SnJkPT35ex-fqby7Nrd^mw{grjq7SJ;0Xk?nMY0Kp-lwJagRn`Bu-SWOval-a+#O!Oxfk zkLg^q;l7Hu(FV7PRoI^vL&*RfXZ}^#Euc$yBZe`AMGB+X3h8x;;m24pDp4rWsXLs! zmT{`ActL^QqTI0(sXc+>jcxMVua4q z;aSvMGwTyhw$R*5hFHTEV&o8VKo}YN`&3;Es5+9zRBE68KONj|7fBew1OuRGi98*h9el@bHL_<*Sf9NhoI?~ zcH2VDsiZRT!@Rvo zspxjPjrO&9durFP$k58=fOg-8{8697Pivk-q6$3$wO=Jf7I>qvdJ3A zhjOq4AFpb0A4-X%9O*WP{{Uj2$W+cTUSlM7$)lqlP@U;W#~CLSo_^KuEK=A{91)BR zdRM$S*r9Y%P~~z52ilhu zY;%*6JO2PdQqu&C_owaWwFj0<$u_1o&M58Tju_Vng91{{Tp*RU5%2^gF0}m&M`8)$zMHnxZ7w8&6p*ab!Q-M4&cHw?)4LDOxu3*N zl+*3?fqJmW$#hxBGPcxiG62po&T)@gXuk+arQD#mX7I-Rjy&kr?#n2ZWH{ic$6_cE z=~j_Ab+d_Ccnz~8ekP=VdwNy1!AFy_XxD3frd!=9{?P$HQQoFjGvT(Q5!2@oqi+F&pDP z9m=u#5X1V{B??aifn9AMILw*;(B)yC2hzMiK9s%^uXfELWgH69P``Ncc;lA&jN!W z2QA#v+kMBRyA5e~Hvr&Ma9r>!(W82JW=6*buwXu>R!0%pIK*|0iRQj?k+KS5!E;ynH7XqalBvG^={HlBiL-X3D z7?&IhuW-dAnp_9xWCe$ADe+r`J*wHF1DsG<6gM?D5{U7{aoUWLgXQ^A5DI=&cjACS zI$>yPRClAz%4Ad-9}UPfNXZK)%m}H`xMc@5QnCZuj|9YUaf%HCx%tIuX?1I6?|P22 z$&eK1j@Sd-vYLiGUgN@8b&!1tBZr!S9QtNtcwzT%vHgNr%t{>*%_{LkeKGjYTC_V87aAWS?Qq04Fuo!Kzbr+#5NgRv}ehMM2i| zxb+)NsV9MT8+exUi_iD$Vn5VVU!;1Wc}}+{(|(f~{{Y^q zhp1^>7r{A16hdCP^kbB^8v`upL%D*VLh&gsN%_H zpX`p&!5}0OSa5JhmTOh2bazwT#))^;7MB4IR5qFvkHj`;I&VpJUY8@uWvpogxC%^` z@7uj}A4S%G4;*R=*Vnxn*0T?&1Dbg0E!nqxjpmk+PhCr zbD~-H&xAjfWvA$_q@<4&J+MGB*Qqxx2EsnGS> zm_{*&Z>esVTZ0pA$iE)_>(~IEMN}+Za1Z|g!?afSTbCcqQlIqMKqo=dWIy550{;L? z^YZv#sIRzg{$;0rOJmxQ0qQEJ#p_T10Fa`hui=2P4s6yY?tCz!_ zL2>DfwfG<2E%C-{*u_$|sjaCZPLI`>{gRLTjd<&K%5eQHuMefRf2~sb4!wVt$6D+! z?-yA2s{W|zJC2{zz|!=ib$f9et4lj#D8b|91D0d&fIaFN>K9R&I)7hJ{{SM!g>%%p zb3|ji(=|~U?y_m_N8|-rgQaUQWK(#Xub{KkV#yj)MnBNGbKfGmtV@>5a0O!DROtfX%WriF@6D{~kH}TB^`A|F^Xgka+JZs+)~(lRbvlf` zr=-{9OgVAFJ2xXxq?>X`9lWYI`@OiTbn7meXo8Jy*FIsgKbWOnr|I`UF12EPKYBT? ze6%`*`GYQ5q3&9f`O{u_;kCP#X4<0KQJ<`I@f0>5Dsyqdac$203I< z2>d+jvksuQw~`$9Ux|_7;*&G>+RI%0SG2#l+`@f6ByynRdZAJUW1!Y;Vdz-;c1wGO z)E82P*^1!;A$~#bbDUzj<;IC<>uccy43@-fbr~cBoF86l4zJSISJ%yUg&=1+apC7D z*SN(#9;J>i=8uo|?f&GRo=tGU^p8;3U)@Qez_NJuQ7&DF%Hx1Y{C`esON`YOpHD?; zduw%XDx<_PF;&Nj-=6$%aaZxS(oWEL^R6zbW1daW;TrMoQKmG3Uax)>xaXS0;#wsy zmlf~AoCPaH6?2@_gc0G|9$usAKA*r7i5tmDF+*l2`9g(u5a-7tM#2%sxGv-t~CQ3x_t1v z+}#r+NspDxeWM|XiPgx?t%#iktzrW>x>V;Rbc4-6?Na+G28fe zEZdj>HnBbYv(KM2S^VIVadwLsz_< zb!^Z|BTY`;E)OoW3_h~I(tohDm^u0XyJkP!?b{7JGXK4 zq||z6MbtG9x7}H*++YT9#BRrI9#bnmnwYs<3727SJd>`=F+Y7yS6W@M|Ty<74X7%>oGm^pS&@O`fyxDvLeg}i;aUl z$r;6QAsX%kfyb40E}pwv-5T0;82KXo-yr-!zJu zZbc9RXz@ldk~2ZeXjE*92R)5FS0kP(NsUKp9Jn0fmv!(q)`o2uIp^M)*!;byfeWn` zJ8i;-Dofe7GLjDPbxuDV@@ zbSEA2Q}lf%D`okBEuQ36rPPr_Y|6PUZ|)ok)< z=0XMrYN3l6;)yUmUT7gigbWN+OqLm9O%@kq?cWYnOR zO2L++%rJR9>4IhTrb{yJ&S)Y~RnL_px2Y5lv<%=JQ(jP4%QSf)^B~4EQKch3R5>I& z`_QD5CW!)W08*@k9t}omIqywbPI;mO^HTEFqJT?1#6hGd2r@|bK2=q43IdCao@&k( z@%1Qv+6pG&zY2L7_N2d#(eFyyD=b`qNy)BsCAO0&sv+qnM7S7@B4dA^!lN^Qab9w(-LhP3`_DAg{d5)xUtL9}t6Jcp6TJ*uAPN7SddR*7Ywb_sGh=i9lf z9-N-q9dg{;Fp^op67%;*at25lIQ?qU?($<5?a93G{8>WCCLc2aj^3E6Ws$}FtC9wW z(@y=E5?w}iNPrtwFgZQJr*dX{W{tNW*E|M~L2GK;t;?{{T8TvSfmF<{Tk6|mwOg5@)9u0WBQTHNIqo+AIS04{y>yTP zuKpRhK{g3TkSKhOJSbt>yvdWrdpkv2+)zO^NR1~E=!ngbk?PJOA+tZbxXI0XGF zD;>>%xcn+nAP;(B0F3rEY+V(yIwv5WXu)uODbb+ZGunti&oo?skTBp8xZ@`S-ieUi z>k095Q~NfJ8o_d?Qbm$Mw{Z#&2iRnRp4`?a{{WTCw9t~>ViDfM{iLJjF1Z*Wel?3O zt{COXWFp!-P3_3HwVq@RDTNCajBjD**mpScu4U4{6s~Uiku>YL;g&0j#Bmj1yFvae z_C9`{htjbFuXO~~r`4dpc;=l#IgAai1~a%G066YB`c&S5u+;B0>%CV`&?30F69$r7 zf}%Kbxdl#hyF6o@0ot=eQ1i_-Xt6@Oor|bCvrW;jKFf0IWiYh8_y7D94&<{B3P{^_Pkq=Y@*S&Io^zc3Wx$eE8#Rnsvz(FjuPXXy@}~~`(}Au0 z6k8dBJkRG|`GL;!`P0Az((U8D3SPmm1r_tYHDUfG27@Jrogh9Yoo4y(x&=A?DrVez zQebAL$iD>NFLJU?3q+8T`aYa<-!`fKR2e#3r(gd7sWkrp{{T#b`3kt(&uVrleH?%3 zL0KlenfK_vmw&yXS&#NXAIQ-E0MK5P=WdROPnujxxcr{9!F_2M6XHh`s>&C4BRI}* zNc;^~Ej}9NEA$>YvUqIG9);=t3~*kk5^-N9ZA5359 z;RTU+G$f zQ~A??HJ*PBblG>ti(}O_3-1|!1FO>*v(Y900A}Vz{%J%>dbTc4PFYEh{?AiUpZJhg zpnkOVKJ}L;!hJG-bN4NeSJba2KT*Bw32eT}A4Y{omNu3C-vgFaZQDo8I3x`HDg?Tp zTnML0I$e?}Fl3Aslf@yKfi1j|%XaO+;<_qh82sppfQ)B~Nc=0)@zaD~A5ouH&o1X| zg}20vJjQAD8%u8!OtpXSLWlGfCbp$T0s}*wNb!}5PYsV|#%m4nJ5%aSCsfgIbm_zz zL|0LZz^DsPzC}QUpSr&`c;`NK1FLSey)&$0yo+0t=bkeikBU4$vt$ST*zb@Z<|8|Z z0DveqL!n#4c9I90_2gWilVX5c+wQ` z({T|ifIuLz%JK>CiU(e%sW()0F0gegh$hgWg67#$;gw?oHDaMb!Q9)y&v0?bz^P<5 z9}=ugSCL;vxiXvYccIK{(l@v}qn_tGa zKMb!4g5tsCEJJvypL58pGV-~rZj`+H4!3V9I7T~L=xStK5)PTtI)Mz@1^jsX*77bt z-8I8X7C8z)uKxfki6LY>hEfmZTsOli0Q4BeXwk~812k=-BS)3zicO%XTyiR4hbK6q zEF(F^Y2D!FA$c))@nJQ69-ZX9Cp3klO$N$7+!r7(5D^A;IlWSiPxbI|_*uL7yu3fIP)aWf{Q1H7e5AKQoNd zIoENLD%KKws!VngI0P+TL1>-!;*QZOE^&`at>q5Vp4(1kDp(4K zH#;yFy(4>ql@_wyqDEC%Z>VLN&PmNciHkl>Neo2>5<;9(I&^Xq$vjjTu~}8T z%{Ey;zy_eSgm4Zio;F7{31sgadeRv|^Q!ic(yZ7W>JrI~CJsd-O~-nO^9mEs-89MhuhW>q6_Q^^O?uJ10SNR}O?9Tx;Dk+f&-ky-M)q=qM3 z>6Y46(#v1 z_ix(YxPmeY3v;zp9&zYLmQSU1ABEStd>R}UKW?{KcCMBUQpqL5%w4ve8RADaRvz57*jF76xnxxfb_1GqJ~=^mcdjglnVEvA^#MdGfldPKy?dxb%O4<`k)jB%QL zc%Oi|R}7$=s78}#44O8dXJaDA;{BnU#+F7_!P}AY44ixNGhH9yj<2U&L28#*7c=X# zMZRAUNaaR*WaJNDF7=AG(mD#)UXtHc>lr1yc|K>B+=h*m`AER+IUVbx{5aBjV%i&v z?w)I_hLMbOUCi!$VmpvqVx`?EW1HN)Vz`}5+DoM%X zDlvmubxo*QWDzT!Yoz%<$*iv(mjwvq2R(5WkZTb$LeQo3a0LxYtQ_=0ergZcF0I>t|;;n~j zaSWtu31 zqFlUTHUI)(HVDq++xU;Ib-YqygH{uq^^e2(wS8JWa@J`gy_O_PxRUNj<8q)5z~Q*# zka!;TWvX;nPRcCx4Q2~TuVA-G8s^|~#e02m&N2;caa@zB`kE;9d)qBq`a34Hm5gF} zb_N9ethvCz_w*Gh5Z`KA9iFSwvFQykXoAMkO1zE8!I8cBP;p$JR?%z*rKsB&K2Nr9 zY?Jf4Z7Sb})&Bs5R?Fc>QbVg`qEXjQn)(bP2LOb8s2>5?b_c_T(bV(!_+Pl zA97|NlN#6jHN4_`7C+-r{OXUWV%e@0j~4{K%8Ig`ff<=R`yvR8I_?aYl|Tc~ERA07$ZXQ9Lo_+N2~$(ulF-;~Y|M8i>)v4IN_x z%8aq&%M{@qCgM;GbK0C2+Kshy$ggqfL9l~V=e0SofRkR5MNZ^qqfI{Zuzqw=2V0EE zkSgNXVMc1E*x=OUm$1zwxnV_+a0e9@Tb9lbIHoM#`JuVo)RYmDO}PA{6g`Z`CW*Ck z=R}Qa5DKXsxT3Cm8hI4EdF@bw4_a`^G`Oadk1B*6jB{S57R`Hcs6q7L*c2dhOU+6k z;(5(@Hak-YIpV#xr4VyAPkI@AdDok@4e&hb5LS9&l7`3LhRi6WK*m$+LTOJt(+lR9 zSYs4YAm*3enq;AqnrRehgPKE0;uFqjWYbx*Mu>iY-dihPJ*~MjM!e*3KUx8@1sq1( zoqOcqpP;0}zZd_ti!-zU%M zRz}NAf(KTG?j4u`B=V3(4>$ngt1WfsHj!J}w}WsQixnp?pDx^fbuQxIy7-rDWJuJo z8vxy(GJBAHYb@iDx?@=O)FadF7~6PZX;#Q$7p*WwjkpX0u|GmED^sRh zSnZ!uzq?Djn7(plknL|JRU^OG^sE$8c#q)-T)7xPr2X$+bNW^N>ZBSioxG5=x0uU- zS-9MAF`RbCII8lmaHqkm(qq2X?;?`HTG+WEW^{H%$8Z>r$DS*D@TctG=>&nUBcJT3 zS+`>VpS;<|JM)bBish}Uwv}ON6d?VflYEHiepVyX+rQVnN$I7U;P{tFz69j7^R%3> z87e(V_Rcx&LrIZhp6A#n)D}9umHzQ#brj(a7{a7CC($ht?NmcW+ zcHCvN=i40DLda#vKb3k+IU|u;D3WQ84o?~4l!1?07(K#}Nbl`Y@9&|qfu^0pg&D?s z)v{_T#Ij!iat}1ta;MsXV|g9rs$4~#jxwbA^NKn7yt~scaxso0mNt$=S4J3RWdt9< z)-O@$YfF2x~nr^$j(q zywmNk+9fhZur|kwkDKMe-Q~tlpVp#$J+PK8nQMgUO+A)v!dbvc7~qh>gGd--&|~qg zrc{naaUQF*ztDB7p=EhA7T1>nCmVo7jm8UkZDE1)4pgzoMbd-KsYhWXq^vSLd-(%6$Diga52tC(@gH3`T}h_u*0#22 zKFf%VF~|-#WR=Lt=Y!;V;+?e~A|kmRZr$Ua1fCaJBXtanry+qE$Uc~-s*&Z*0><7e zJ4mf^TlziLy?dwP8h z2VI#c$){AUrj>a5!i#IGS%eHwZ&zYIR#H2IgY+PedgVQ3)Y0qTXSutzTRVl_9L$Ji z&em*ldk8ry{7C6p9UA9IvDEJFmvIPxc#K$#{pR2RPXJ?{eDTS(3)x!ASlkie zWgGw(BRR%#f=La|Pio@pa%5jAq7Yrp2SID*->aj$-#mrnX#{3*8p_NzWy^b<_VV|x zg`nSAbr)7HrEz|xqjrkc)m-i>7n8L8(U4Aa$RCAr4I`;-EjoVY#Qyg3;^;{!Fe?C< zA^VS%;N)$`nf0!L@KaB*xz+yP9CIx2sdn*Pl39l&5C=SrgXQbVqo~@J=b)F;j3|01}Q-1q->?wI{`y@+YX=5&AX+ny@Ls6YMS@cP@k{h0@(A8WOIlhn6>rqW7b_TekoVTSR;MGOX zRrJbSNfdhipD0!%G0Gn!*NpK(Ua8QXu@&r+{{Z(^(!c&yHKTGf?Pjy3?uMJI!7}ih zzHU!qd8@9ejDFv`WgIb9ABwGWwCWosKfAZpL;)Gu(WoSI&(Qv)57yB*O9qBuYn- zpswZ_qQ>kynrZ-GcGK0#In72SlbqAhj%iv7e3H^aD53bzokIlor-nJjCeevu{prfU zR6tHBfzD`A2|=mBsxW+c(}U1tBxf{?MF+(9r15AFkeH&PAso~(EfHt;s6tCBk7^qb zC~Y=#%?aiAs6gHz8RnSMd(?K8m}K{;K+OL1H*P2`C%rYKqLBx?(6gU9W4$$B9jVd- zVRsKo8|0q&uLtv{7Rln8iDS@tp7isI61P0jGAINTpm|XC6GtRsm`*qpd;-tT*=1II zsO{TmsZ;5)t(duz7lU$-?G9Ows2-K0dkq&)z3{B(-*LA*NF53Ma4NDr+Lov+=8@Vk zc8#QvKAykQxgM))g~45TC=w?^eq5jD@ARoq&k1ms;zvP%JT@5vJl3%+EHs#9A~uk> z%H$lJXP!v@LaHxq*HgZUEu#@ecn-=2)g8&jNNQr`$_;W!(P6gKpqABSwT0pS!fD@O zZ2i?LIL>p)?VfQ^wQUynMw&m`5g5#PhbG@<2}deQ8a6LMVe0*$GS^}9w&{41IPjGj^d_kNf>px-<|mm zo7j=%&V70HtQjJ`K+A%+RJoGoa%6uI`?Gf>_Tc{jN`b#kK_SW9e!MX9Ou@P zr!1q?HDdF@aMoxEkjB6a^MFb04iCLw>T_7w6Aql~ZwT<&w!Zd}hkO-0W5{HF6v|MH z;}%4YxzUrQTnOC%X$HT9yqWzrM`v#(jU4X}X(Tk4(0b%Gz%Z;6wy3Gd2l6 zTd+wWb6kSUT(+{*?5EHT!%HzTn~R9lNh5$k1gJUgMTOMdW}P^LU0 z6<2}IaC>>zPd2VNq4N0^rrFcetaSS=HXCahB)_{avUxc;ZtU!F&#o&4f7CY}OVyC+ zD>~0_Jj)&)JZwOF`5!WPH3Tl1ZKcSUcaq5R#wOVS+lgBnd*rad?Sbb*=tQ^B?`P^O zt@l^3hg(Z^LzQeCyK+8I4{r6726<|8E$@ZkZM#pPtgWr>En>E|g<^&^+K4mqijp?* z%~14Kt8uRA5NekZ#~GJ6So4k!IL|%7?^i$?^l5RyM7Uu)`PYCnhza^p5e|FQx+3JS z)%81FE;W+qz8mg^Ui|rIAC+;xiyE!{_fT6TI~LIpN%o6r31^769DfS}G3mv3S*>85 z0#5`|u5*V#!#|Btb!SCsTJ4L*mhf8K?b{v1Yq6C1{oH5hb6GRR%~@;`M=khi)T^M& ze`>SKsaZAlT~Y!WH;|%CoadYjXU~r+yXp?8y6E1v7d=5~4b7#}N#R`DZ4WGDsX)t~ zSw3Nt#vi$?MSz>8`t)cmb7^tm%^FEF4XW66CAq;E?a$%mSAAbSzg6{Y_h$A&*2loN zw2snwiH`$xfsl+0k-^4rIOeH_N4AHQZ;`TfEr-~@Zs~e8?bYa*3#*w-qAX(~C1fXP z8Q}YKR-GTLSZaN51^j(3V`Pl@vI!=+nH+)v8Ej*aGlG9Q#aQ(2r9Jh;dIh$vBqBg0 zV`$2|2L-@lPdFnU-%99xJ<(06#S`4uBV-vFU9n%u3I9A~e$gPXTuyFC&jXYQ1i;wrLW}M6$fxaq7~{^| z`r@tmy~NuTL(vAOEtr#4g4!9$&d6DqDD*i4nw()L(x&))_Trbr_N!FD;z2N)PYB|t z6drWafO%2gN-ORy5@=o-S9ETqFg{@S{Ka7PtC_kU{aH01wAfr0cZ%W5$rmUAUUJ?0 zW3W8dzh2U{0pZ=~7foq)k`mFg6AYod?^Bg&g>k~Jr{R*scCxQeLVj5aVmz~KwOd=jFNo0rIW(p$(`h;zsX05HhTK?9ug*w;?ebUh14>1#c9=H^$_WOg?3tZM1CPXUqJ;<#H5X|gtsGaFOk~~k&gJ|+TTXcj~I5_ed zq4?2f8^`5MyPbcqi|&6Ld9_8aob_LhC5JxEGEFZm^g3ROr3CW6Su{-#ZR7Q)iDLfx z)pwnGurH5wU;Svm`4&HRT7J;f<3SaVQC(O;{{Y#*u;V^wEQIIrrZoDboXgeP(`0kD z<~08R%7Ux8^)#MzWAV(}w~2nNkD|xKfa~(fZjlf4n^-az!z=3YgSQ8q1{{0WKf2Z@ zU0(Ly=Tz2bjU{D|W%CdQc;%Q@`u57&@)H%@DJq{P9lgh)_Z5-WdO3hH>94=P?w{{| z2hZ`X7``&ak^K1Rs$CjOw2Zc|ujo-m?{TMVcM+oe#jgSX0N%}IZaUAQinxzNpK}iK z&PiX;)|T?dTX|Y3;b`4^t1bwrQ&~d)0D2@md5qUWKM_Jp>=eG6UD-(|#6FjeLc`KX zr%z4UT_wQhs?0aiLc zT95QE3xAaAeT(Ai1t#u0)jS%RcL^e@XR{8*rg*0u8t!Oy`h9jkQv|IT zb6}ncZJH#raZKRniU5?8PA)}nXrjX4Vw!Q0?Lsp78fNBW&lMXDW$;`EP*#>y}urG zE@=ogD1nRf-o3xxfLPN@8WbrcN$*Hy_o#)>ts!%c^(Zz=Gap)OO`kf5U*4Kzqd+8? zed&f;jL+#!Em1_9i6&h6(|ligkGi}vX_QG}_vW5GF+fiAUEiHG5O_XRoDOL>6JEh1 zp4BXeyz@lefyECPuQBwaL9t5{UL4a`B+}#FpbyXOL>nua6chnY3KSrm`DBkO4vZ$3 zTDNE-J6dJhh)5qw@wU01-fPC51VnCk$^$X>9nDPW{XKOHhHcrFHw=-lcEBXy?id_< z*Ec0WF(bzXp0M{5`0z*aL*osxyJf})m$eE>VzCJbkX*S=NIYlIdQ^>eIABJ*d9ilK z`!F-GdLMeGw}Ztgdv!1nt8%4DE$NEE^2#Loh14vUO}6_*=nruy{oG^_6dyl&qcgI| z&OTN>@GvT7sU_B_s|fAmd7SYaaj7Q**m+ScG%MNWW$?%-xntxHr`EM&m!}oI_aQ&0 zZpuJBJ?iL7vny@%s|j?ryVPTbYmypfATUkGXc_Ou4l&J1>FX<73n@jzM=i{_1Z>XE z2>={=d;b6`p9)(=AKEXR#UYO{hRN;-+;DjFMw)`#4z8KZe_6 z1mrIq1E0i-hi)y>PZDT6XK2gDvHt+woN#|pS^JxZGD|#>i-_3#hC*}4>S>>}yf-<0 zE;U+yte)8 zTd{OMD(@uWSPXN> zN+k?m%nxDuduFkB+MKU+M(HqqP^G(p+m6&-6IU8!!sa<-ONBTE8yL@H_*MFRPaM?y zisi}ml=`NJp~DrMTwB|uY$6dU{cQOnwK4-8s^29pb z#Vojurw5Tzl0RBaGf%g$v4YYB2^=U9K<9ueU_n~<+8byTj+SVA zSXwReCgme~95DJHS~Cr*SmOX>{VNlyb>hI0J)V_$F(mF*i0vplNEl}$gT^_O3OP5R zIA!$*xITuLcnysGeXBr$K^5ZSgj$$eiir~FKK^q^Tw}dPXT>zRrr0f#w*%))c}POa zhDP#LBQ5MRQT$3nHsdyMjfD|2iiAho&uBGJdYt%TFtEMxJfVCm4@__k-T&BazCFWipe**B*vh&2$9>% zaPh2o+p)soi(s#Ea5=#z9(AI(>WfM3VUJa_xw@9!7Art5M#Nyl8SV$Tz|K85te%Gy zS1$*Nbp*yNmWn{i8C0ppar{Gf>+M?04wJa)uB2IHmKi2S5~LdhhiDAMwhr8ahk$d) z8RnWe_VZyaq`TGisbI9zbaUbaSW-Auu&Sy*b`D4bm zE;Eg}Z!>~;=RZ#>O~ zucqm3b*5N?0J-q9E3)o68TnOC!a2##co-tIU|W7?-GWb(qU{uf3X5tI-UNj=6;K9C zWS??RnWH0?ASlTvf$Liz3fPO6NFt{83Zo?OF;VYk2s{{w$7f;Z>^-Qm&Eb;@V6oxb z)7Vs5#zswiv*MqLy=grzo&2l2_dQ2V>Asz0$=m@xy{RkT(x|oWuIb$l^=!3GN9@zG z2?F>sKgx51l~O#9op@YWa%Yy@akun4a*~R+=)Lo#sCq`HW2@=$>Kb*xp4Lq4kSQ6# z!Qg;RA>_?>(onL@H^alRM*7`3B>J*iHv)p|TarjnCSLpeZbeeOS7r%!80C;^5V_S3o z09;bW0c>{itMO{`V&*JaV~oyA7M*b=__cL0K7MWE8%L=XGg|4XtmT zC)8)Su8Y+6(lv`do8f)A6EQQUK_mE2wQ|jGs(*J9*ME)eb^M840FFgtu-gKu`r%#z-FH-?b7q2ZKqpG3`jn>E3`tf<0+iij9}1 zNbEB~M3zY`qL|DoCy2!0=9k1Gn`k!%VkwSjO)tGPxjuCi7#keMn8%tzeJMd0rrHa! zq@OxYY3w+p#T}%vzgqSwO(y{GXwU@b23xPrp>s~A|!J|R2p{v814&#pWXcu>SSHcQs=XAaYhZ{wQ z7|Y~;&zijHiFRu?_b{ZfL^#`)0ggP%^NNkr`Ws%!9J1kIAd|JeWBygHlUKH|)26YG z7L8j9tC7wJ2a(#ixpObE9zbXH*{<~sNQa3-G@fH<{C%k*LwR)^EX3Ny%LF*jx8qZP zX3X*Ij&_ZtBtIZ+TyxH7tSt0xJ;YN<;<^x?PPy~rzI`f(k{m^M7qI(5r|F6pPPv_C znGcOAT=IW_0(*O!k0j{muqk+>Ze5vikHlxvt8{A%>)Z8?A%+=k(y&$I*FEaSZ97q& zx#ptSUj>SExSfG?;hA?5 z!>|X+uV;@#&~)JoM{lTV1aT_6fSLXsxxxD2R#Hc}omX<~fVXhC$@B)bVR5cI5!bmc z2&cRe*{rfMjNy4I4l_W9O}M#sj?yQ%Wf>(`82sx`VJ){$*vLEtV|53GDZ$;7$n)dB zdbPT1rRn(A*4-^Fk+w?_kZeqH0~3Ls=Q!uo)o(ZYNl2%<(i2D1Vf%ir?{cKHP+BNp zgz`Ro_s_SsYK+Y-%(b7deEKYZX^$=@+)r#Ij8$3bRIztTV{x zk>yYvg`5c(H-vCDo&fZ#Dl~w}7PCoa>f1nUXPvOOkgopgV>D$8R%=;%gz)E+hLLoJx?WIQz=H^2}Sy>t99u6X{qjts?3>+qS*F zWhn!PVv#2~RXM{9=aIlU`qrI3K5m2cI@++O@2G-K59>?f9(k>sSeuDk!1MfTBk>DU zzOl1O^o>r=+Eij?NnKRBfyfL_GtXn{2*q0U5%DxjrSN5#_+*5O8}sV9^RJ%QSJYnf zQm~fdSnTd(SG&8rzu9&rqMVpvU8=`8P*n5t zbMJzD#aQ(3RBQTYOj~Js2A6*otdRWo4R3~yZx1cAEr1Cb=iK?#KUrzI1-6@ECr)c3 z<`#^s!bCCs(m4yajGXh&Ip@7c=>21;bqkZGMbrrrUrCrIn77L7oMRj*@5mh1c&4P$ zXLMX^8pl_lgx%`SE=TU-Xl@s4F9e^Jim~8xp84}MJtN|8P+QolDN^V>Hj$2ZDC7(t z0p#;l{Ylb&TyJ+o(Q2E^86topy6wj92pf+%<0hhMoh5m3e=V!&I-FN(tif;^O{5Kg zhahrKBz<_JvUW>HVCc@hx4VuP)vuy?=Y$nlkcExEiCc^V@0=X*TYGIn*5=i&qH@rv zB#<+}`d2gPts|^#V79f7O^Z*qQMu%KVG>EkIVc;9`t!|S>76sGUs}#JtJtm0&OBHx zPo-PVQX;()DceuhmKSZhqgh+4iDCf!l|?k2G2pZk!&WoaM>ru zkTJ>k{{XCJq;&48nzo!Sd?tm#+bSR6e>M7p#a$Lw(kD|{No?~*!qr~Ce|x6Hw>I_- zeHK8HIQVoN<-NzYJiX|-?N$b?J>9%l5g6x9>5#-9BBT&UxF8?GzIgbLqD`mIbZj?x z3=Zjj&9KNj9R9vv%vaF*W&Nh9r!DP^v&7)wl^~B$N0Ih5pVjPbdXmoa%TkNLyS9k# zgS%mr?S3-F5Acj+VEOjXo(n2e81T=P;=Pi^kz6Otr9;Wd+Qb~=%Ypv@SfX4>Z#?a6 z_mQe2v~jUt;RN{(z>M>gTRy4i>0km&E11@MbZeQ;RQP|~Q_05(k>4D9)+<$l-rnLi zx%*FxBW6P|1gFj1sLn@!PsX{qA+|`hastN6dpX72#UoBsDFOEqF^#$H$gKiv8?@z$ zTuCsL$p+RW9N|ip9kKF}$Q)zX(8Gx?q)Vw}FXF3V7-6xU!1T)CrOaEa6+C3J;n-Um)WI z9DoNTl6?(zFNb$aVXfb^5=L~HBoT@5u>w^_*JVA)!Czj-oYy(lH9I|LsNuGP-04=4 zN)>S)(Fqt;EJ?zW2Y)Z+H>RMMRn#r?9Tw`{9&%E7q=&`csvY+qU;>X~cq6dB8GdE7 zSIRzvf@3U8CfN`$$PT4WJ#cvyDP~xrQ5$ZR^SF=U=eN|=4^L?)NwP>D=4+Ql+8x~d zqXUd*2e*6*b>ufzmrbcyhnzM((`*m+fN{A!91kwd*z=0b@gAR7sL)C}ekJ95uc4!+ z%>+aI&mlpT7QQca|vPB@uu>F379x+Y2K1RL^k}{0kW!)a-HCDS3 zPkO_AZH-SuO>wn%#XL>BXvq1o{cCV*G4V#m=Thi>M$XROON)jPl#LX#96FPLJ+oW- zT>U*>IXA_W=Z@@bqw3zK*L3|~Qin!c2(0v*IPRs6NXat?BOqh${qHIVQgx?LLwTxA zrA1&Y>@2PBO3tj#;!wPT2pks2;Zgd&(^Z1{?&3Wn_U$!o5<5_B<1A8Pju?3kDzmBV zwLLK}p=(9B`&Umv706qK2Hm3zxtU4HA1`n@CcTagE|V_TQEmSK#s2_PojK%+c3$;A zTI<$2ma^6sx_jAaaa*(a!HkXKfrc1A;;9bqk)Emsgiix3qmX z<$Z;=D3BIHFns?23XiAg)KiofEBk-R7F>jYaKTwdccQteIbu)w=pbW7byN*_{ z)8{eC8Ar@SD>gghxzFQPGA@pEId61NwoPkm)D}`T-Nf7cb|B<}Kp>3soYtk&^tyLy zH0^ogvJuZFP8&5HAJfL`NoLk;8V8?iEKkIdanIAgZ))Q=F~@DuF+~=jjd^W;a{zKB z*ckFXM?a-~G{CDKYl`1#mW}ZHtK@4)4c(ru!6xv>h@-@eU^j3%YQ@m?k(}~xTv{Jj zi-ksV?&D9k*W|K)+G*kHD$-4#-K?xQjQQ>MrgYA*w)Zck-7U*V_tM0ZFvTB@7(I)vT-0CCi4!`L|E`B$h|F=WTjlbYo?NW8P^ zO*Z0LV?j9BuIkMm7C@hSY4gP-9aE&s6q1LXRy9{`(Gitm0AcDfMQ1cidvT~o4ENHR z(&p|6twJdX3Y?O2k&u1pR&f2dPPB^|Y4lm69_Bd-A536XPo?Rtu1K{R51@2js?p}O zf)BK}j)|){%)AcWxIFfv>D_13m-AgmZK#56V`*e98z?Bhd6bd!0go)#A^3Xo zORQ*8wn}PRid^>Kv@Iw274N}#dAM}_ty6vMPwkI9Jfuv zk}90-c;(xJ=yB!ruAAjvXR7Gqq|fQX>W$ibk0fo0f$2wvQb<-jj>dv%9EDQMconap z^^E;DP@FkWy7ws8X(6e`?}ogS7D9a}(VQOt0G&m*R`8U_HnuWBq5=zGlV5hnFH#Cs zR*~VPwn?IDIOCowL?>|VP!O&-JmRE5AztS+l7}1*7Fk1q#W~2Rt2&X8X*^(Cy-~H6 z+9iSqYW8@?6&+<5?@eh(k)gGc&~imJuzS!vUwZJ6XOmGT1kg>3p2IZR0Op(GVDZf{ zAoQKpk@5c zDycoqKc+75jVGmeH?C=@oZ~!Hg@Yr5L8OA6fyu=#a6IU~7S89=E#9J!}{5wdMW!N&(Ve{PXAKf*O zit<>=5&Vos*2kEs$%k0*Tg8)};~4Yj^Q+z|B$eYu zm&I(cWl#wnv5wxkK9s7zK-UEkwh1uV6I`&y=+#J4IPdxT3aeon%HXareREshpw?ow z(w53Bcq=C53ZrwA-*>O$n#AF+B|aP_pCz^;U}MaaTe0L>6^aGq0#-4v8NlIMR)Ksd&5bM>neK{fvX5A=?zOZ#WC z>1l7FkO>TO#(X)KJQgJ2jQ$_azNyipy^l+`OW|&}Apn(a)QHsjFR-tcI!~xI_?|Nc zzkhF%SSqu8$G0Q5BkPZ9`a`2Nd#mj=W7SM4sK%hn5;$TM=V>_OBzaduOOh_oxfdJ~ z`kFmPJ4U_J^-r=(Za0*YF!9I-@UZ*CKA`>;iG$+2P@^UOn<1M!cECaWNHwVHJpyf7 zb(S4_Q?LN@6}*8I{+XQE~ zeO3jI zXQ9F2>Dq>+3qRjJFlBSkC0R(SeM3Rj;JwsM#P=5(O~gc$vJs@Y<96l1$vD8r{*Sg% zUodwj_&G08+xQyht7E2JtoDIT%xbO^!{;~(bDVBJFF4L?L;nD0_$`v^ZC_KpjwH38 z!nX!cyQOe5lbo=}mv1_#>&P_JwEH%fC}&7_c!zmuGr`Tq-_*aM}xQbhZ~4v#@z9q+*Ut8>YYmWShKygyq{INxlGJn zHI1MkeZ$B=xXH(<=QYqQhgi*Kw$n+c*&hpGxM>>VZ22}gQgP%@9`&j#e`1#0d1gOV z>u#>;%c!iiD@{7h6p|4ee~2&-eCIrmA)b9G*Bx7`bv5F}2Bj9CX0d?jXX9KmWaNyZ z44eV!jFHV*$?)4umHyLleRp?$U8HN7_OUE{$j6cL1XN8ELF&GjyP354mr%7xm@E^& zh6@wM(5y-O%zk0ohow083dt6NeY>qE)NSqb>l^P5(h{pFPYon*l>GiK)8Eu~?V4_} zt98DYsjbWD6Ra-LypUL@i5p-AKR6i&k05IQ08P=aj;u9@zP3pv<J+OVEN4HbK2;yAwJ9w0?-tVj8%woU zXpFybfrUBF7XxrS{uMa4nd3fx^QcbS$JZtX>LAUDF@^r_5ESk`u*JYw^Q28lHp302&yxk%10vy_4eY)^}P?MEPP!?5oxGJ zD~7&O-UPA`NXP>#$;tUh{Oge!-wT69njW|6JqM}!c3a5f@Gq7kC*F&c&wK{t=#nW#r4?bKHmw@-;4IXA1ak-In~ z<92=cs2YXK0=E#z#wnB_b^|EKImSJ*JgSVcl%$Z+Ks0-{yM`+{-a;XM;EZD(j(msC zeXBzA>SA*|vf>MEk;je^a0tm5{vfA3bB|G2?K@7plT&+m!ldgP$%gHPQct#U57xCh zeyb*#qB6^D7FL&u6r+>!js`~~kbOH;lw1|A%yhzCJh0-%+C`1$l!P&DDvAf)$QkZW zrgP18UxvCO^v0l~}&4mF=SYG%6LrJm4@>l_!FroPm&Wkxz%^mA>M>PTj2P z(OJ(0jj)E}QMovV*vb`{07-6qh6(b|YM%&i33rAMfxC7A2j3iiRnI;V+l$C$>T8RM z=bC0QLS~bDtbO*m<0mYBocn5AjdJOIH`F)2f>8z5f8@?sUGb>9vn9V%Yb^C$>JB@}Sw>Ti)8G&9sXo zPBNr_E_u#-k199gUnHJ6W0uybqt>Y@D@TaWnW}AnrZhcvc&;@|5j?T3#c?B{+nnt? zNXXBTtDN#hCMe_0lN7f>(<6#%nMbVjk56k7!zKQn9ifawyCitQLD@)BGBf2@N~;nC zjBQl_1yO(i>;*d&hixt8YM;`-`&V_1AknRk?p ze(DYe2l-ZmR%xsf8zBTUEP%SCa;9(_X*ldT1b6bIPjH`R5yUXx-mYV>^^}^o~edX_7MR$k;q~VlqD}_1_O-({(B2df7Jku1;RRCQK1tk~#viNSn}xQ|Cf! zdv-@d1SMoSkQl}s`|;10dd_@0)xT(I(?LJUaI!P1{{ZaC;Cth>YAep`u`M>Hc=5gy z;hhXJbAptYEfk$oq*`C;+uGI#p51c`n89fMl=lPaT-HlVW_I0e6B5Cgv|}ZQVtvJS zH&Dn__9GR{b?Z}Usmlo%T;zQ#Z%x%rt&Nmt#=Fr>EbRr^yB5%kf(SC~8;|4Ok}C^1 zye|sY82lhadr*9!_s7@HrBV*{*!8k|vo6)_Z2A*P(#DHq>!;fXvz9{xcw;ecIc>g~ ztGKj1A{|-vXmsm%@8r(wg+LtVgZTT^H$mNowtHqc+sLg?Zq?^?I-K#%FPWVTaEprI z$n>X3-RTTa}Inf-w!M#LgMRZj&4n#yAzPq@}8LTCAFxspNe29uST-!QyZU zIQ;4M@U+BCaj|~jKg|1jR!^*Yvrf`lO+W1)>+KmRh%>c_`E#Fq9Czlk{S&L+&(v}0 zV(H%I{l(SP@yonyUVJge0V4pO_#XA{J`(8hQ(CO*{g?GPJzrew(ud}CUkr};uNb-X zqmjmVry}RdzO0{<%TOhc3!3oAsVp$Ps3Zyr*l1v4I3ARswhuIi0+6-`6lns-Z(-+N zGM`!#Zl;`%I&FwoAig=yb50W+RBTe>qsEE7l0^RW(<$|!g*4!#((FOr$C(NCrkCcE zVfLqNqLfK&Zu8|%4t|v7Rqajze1>XTCnt#DQpTe^nsTEYc~kbN7|%4Vja&~{&N2-V za=d1OB-$~IiuZ_nfk@Siof1I|ImIK!gbk*nZ#-j@UNW{0c%@OHtRV_K=tpR7^fz`r z%{MgE1j80kdm3qQ4{9Fc%_q~En^03@%b&uNai)w?m?Oshq(qpDg59<}chd+9>TSZybEM=C$9N7Y`{N{M5T9AyM&?ye3qntV{XQ{{uZDx5+l z-a|5HxZvi1@3$lmy;tdFX?>uq$cU3J;>;iB@4&~UQA-4o%iy}OleXM)J*#{qrEAy? zGuE-saWIY;*_@uhcdG~^McUH_U+%ZB`PJ(5>^2thUfnaw5sI;50+t`cj{JGlPNQX- z$t6jnQX7a=atE*el}pN{EwK#`jJ4kNfdGU8NIl2{&lPF=M&83ujyP1!FKzf4P<=m@ zX7o*7O9jOT_2zijbPuC5+?gCy?FAw2Vn64SQn_7|~Xs3ez{n;-?5 zhT=~fao^Yq_2!@UXrm7*yoZ4o-6Z*+EK=zg6NT`gRRg=6WB@$QGny``r(V2r>N2`L zvbNZyRm5-Qg{vVMN52RtsbaA?j$2g@qyj_dkbSD>r(VKcdI@z)v!+=w9w|tRu};JQ zGxVyv8z|%sX1Yj3Uok>CE9bY=`&6d3jIdbWd`-wc{{W3^TzteeTDG&HTx(PQ!_&l8 zMM6e`S6BCZ=LDZRu<1Q}Qnj>}?$+2Tj!*uO8}6JRml^(49X{_!wdlqc`YxK6VRpU5 zi0w0${v~|v2fxZd{VK0hy*E#8ZtpWRafA-Yfnz{803hdsMsFFdPjk)c}1Mv0DZ{@$XxzG|6wvsrcUCPTmR|I+YE*PI& z`q$2vceCByHPp9PE?t2#xGcfE#(P&o>BFQ=YYmdkeX88}pvelifw2Jc!2yT_@_lOj zIi<>EmNd;Kt##9y&6U=lJ)lVKAjOo|Hj);01Cp|O=N@>jZ+T;BWqCA87HA5FLmF^N z?YT3^2ajG&bQ?~uywTh2b7|Lc*gC(4lL5LgbCL@1z#jYzcC6l;Yje~+L49vwrPDjA+Co0LL0rS(7P)<8_MV=&vYzHRWOjl`u;d_O90QPK0E_~l zp8R6E3iT$6IG*$Dj9X^gxSj)?XCF8jN=sS6PSH7LA*z`4hln67A zot{&t|Eg}i%rz*B#K!PU1WrYTr!3OZ*jr*?0qX=b1Zi7w6eHFUgVq#%WBt;sOdM; zTWMt8MX_C41}^cF-9rGxV>meV?~Ze4lZ&+ZErl5_rO>VQ(RHrrnv`={f^*=KOnr!5 zrvz>|K3K@_<;bmU)uy>QIH zfsyHgG20+_?^W8@QVl}d2*j7YO9#Smrd^N%kLK!`_3-9lJxf$i^00covanZIU9$832&{ zrH(xDoTo05D=3CC#5_`=410GOVm@yBMtrN4Sq%ZEFAM@gr1=ON&plOD#AeDm>6bz3>+RoRYcuRA(G+?lD$rV+cvk zehTs_HAw9@*Lrq`rwKO49A*4Cp@l#)IXDWV&A8@?px!>Gr={?S#*%&1AckBI;`i^x zaz2UBwGO1z*6!%dEt*fhdzH9kA@E4~Nk1_sBmw+FyIyj6=j+XOaAcNWnyDEl6&?iv zy$yJeQ~FcWKD1Qlf@?#Ni4>Ps_ZBx&YC3FYUl8uPv|N7o{AuX_0QtR-HDhCS1?93_ z+ejpd#zO!{)ExFc-d=Sx3Fs=a_d&3_mFA91t0|qgJKD@+-hE2)r2FGO_2K>@_(~l{ zUbYXGUVWL~>D^vhb+acZozFZIj{H&+Rmszep_O&bY;N!Vw5LD#Ys05MyRU11jOqUX z!CqQl0BY%NnmVhm;r3;gK<%3KR}P+&JZt*T{{YpG{{VuVbP31Z*Y$tJqW=K)>ZkAy zyZuoc`SuTHPsr_&UO2{cihy)GkT5!fRs7-q0QTz%@ncr%4v>P@T|Zm6m`tsaaU4hH z48Pq$$IVsb_!C_Pd!jUY?g=`kbd%o|;A`f#__fx9wd4o?0L!QOQ7!&1^=pJva~YJJ zVQtWVO3MBX)~_hH`MwH1kR*8=il*wF9z9;bqJ{C|`J!WveMNEs@eizm7yDc%&j#H; z%B$`^Ep>!D!Fys<9A*XQ@)U9WC)6;_N-ch%eOFHJUxWId zrSGV(%&t9SO^=RC43ApNb=ICFfp23UI`G&YRc@MIwg=7B3QkTjV4wl?-TKh(v|U!} z-Qb&DMn@Pw>LdREj*bt4dY&n%ar%oswWgA;PHubu0F@>Wn604reb);@_IrE1 zKUH)fTr75OjS)SDJVT!^G5K>`dVAeNsI_O-bvfXc>Qd`8GGSH)Rm@=*KX)ew&nM2T z>E8(Fj(ysyNqt4tbzE;9V(!a5Lgwt=%W|@`^G>^!3M)L1w+vg*jAI!97b!1ilfcX=H;EsNwvC< zPPdNj!`v)m#bCJSA5;9zOxYs(RcA|T7Z)8rVRNcHX?Jj&iu|exIUoQD2aiKme`t%i zIj-azX(VQJkc_NG2^7x=`P0qJen8Vr$UaqKQWnwg2>MsNA?cc7pZqk2a&v$ww#XKR zbR_UGLoon(BD~T&9`)i$@7kFrDAKAt>)1hF&poLRY~qMSG6C398BTG>W;9MFdO61t^qy)l^tXTxTYdP!HW4 zP&{0Y^o=3MCyHMqconb*dT#^F(3)Op3zO$ZaW;f6gPzsqkeU%{P-t#MQDceCDvvU0 zxu%x#%?VG~Xvf?(RE_|qHaw}Di0w#CEL%gghi4viyRqj&%{(bSRbrAWabt@U=S>9p zW}YK8;TiMIO~{)W@Xhw-x++}G6^U0!_+$9_RkCVpYow=AOShWG!;(UPG3SlV{{SkXzq}Go z?wAFDTw^C6^eS?jZ?VM>g)z8x?pr8Fu*EejEfU~9OP)WA13vzLfTzV6094o988)Xb0Clg-+G%zQ=6^tkT0A>T*&)lqkR> zBfe`bB>4NBN4VsxV|b+JBW(wp;Ir3tZ?(_FMm{hC;D72WG&*9mGRt&lX?Ef$&f@&2x0d2XiZar(e(bOTmp)y)^I8pIu5I+V^vh$7i{Z+s zcPJ#12W$i9{b**?wJJz7imevUL7M5+;6WTGf@Nhbf$Ox4=bREd*RSSV>&t6+VT{2S z?rq^L2K*idF}EGhr!^PtX&s2MXbIWfhy)?eBmwPJcQ=t*&Ei-~Dj(ut{O8DKtBuZg zF2Hg~n}2N9x7SnZQ^K<$;bSV`wmSj_F;!NZuBS-DV6os6z@Z1a)MnZPNx8Qj&y4!_ zqTTJ&brnk+fi^MoDB~x}wA<{TBwUkkr@6JY7R}-KWyuVq?;LsgAIhtQkGWaBc%_lN zdy>}^tN;w{U`fZf7^9`DG-)g-vUw{101^B<{#9gR)Oi)IN7)qdx5!XN0nYr8P7ebd z*1Jm7hODa&yF71hv3O8M`_My-e88y3mOSder8JuxsIM;dJBGHlv??DX?-Tqhj&s4} z@$2bQwb``0sakz5$!+xYK^iZNH+Y!OIl;$&Y+`=$3b+4y>g>b(K?S#vsq#R6*D7{45OyFoo_S^E2UlhZ6ByLYa6)n_X7RsTR({JtI!gKGCKp`k}MGV&!(=s1Beo zJY*C6%}tx)6qa$r7OkLOY1hr&WxzlVc^M-=O1Bn$5c;flrb}hi9PUAJaU!tipWVA~ zexj^;mK$wHMwVET8<)2tR4szJ2a(SN5D3TRT3c~VrdGMDMI-gZj|huT&^4R8h94mtM}dGE39ekZwirQd__oI5|Hmwtk!%rM=Rxv~4x@n+v8}ONSE0r^dkq zYOR$kla|Vi{v_nqqCU3Jmq<6#UB{T?4)LSzeYwFtjf3AEsbQ7<_e7Kz-6!gLZPu$` zE$6-P;ge~VEz`p;JC!U+Bk;vqTU)_vZ5_3=Nfa?I)I}U96-(0{SEcn0#E|Kw+(#bv zaxgpN1J}J-YZn(*mfkhJn7hZ{j6TN4nFNe}c&!R?+qo39*8zyIGX5KwtNtvx0ITv!k`(9F5=W+fZ7aS3amC@Jp*y|Fk`hCkwcPlJn zILQb&(?`$TGZLQ{&)L=-UZpW5zarMP!Z`Q}TcO5OP;QF?^7O`mo zzJ={?E~RppF##(w=KwO3$0P43?c^$>Qqi>gx#EvPkxVNWgENj>cL1>-T(QaX+vSl{ zJ|*-%tJ9Za`A>}lFO1TH7^&TWG4IFXPo`@xpw(%#Scm!Y_YYBK4p#mQr<;@01O^|e>%@mz07YEA5mz(XrzG_ zXJi5Y0K1h88?YG9pBesjrO>)k?)GbKg(HUTRK)HGja+~Rc>W>~eOr!2L+Sl0(Z#Tn z#hUTv5d}y~Zp#4O@!dB7GJF1gm*JOA+v(O)T}6F%>hqHwu$9(0K;vsg9^6uuK2)kl<4(_|Cm~c12DvZ& zrO=H7OL5%IA|J#l{*~8GG2Xd<{i7lhbghH`0CxBf&Nlx5Dz7&%W8>mT`TE+|#&W8p z)s~5-l+~jQyaS#^SoH0fGLp-|AFWyoVIAp_J7b#WZMJE02|P%uLB?@Z`kjn&!uU`a z4&ts|mvAJ4eN735{0so3Rl7t|X7~QhAVZ>R*6om1c;6pT!}?cS${xdt>VJ2cw9oj99MNdaBV zL5)cV<|ofM0Q%(nS36gScg+>aemCw?bv^E+j_Os>EHc>s(a_$)KQ=klwF?saQTCgS4hnXTiIqH-O=%s|HlM%Lwl zfU2oJ{ zzQ6wf&34f|8XP5LNSy+qBaDmzkM>3dM%$exJ7rb;OSwfyegVKS3mQWa&M4j>q%u)f9su}9B8rK$*NmQ& z-R7k-S_}e;_Cexg?qP^xwfG@W9cmH0zyG*5Xred^=i74>n|KWf!kl_`?NKjoVv!h_o!9PyTZKMfTF&vQNGp@iF_%6Y zmM#1_{&X17D!6vZ9k6&Fw8+r-0opbyNeXenK6v%3O)3a3Ig;Au;Wo077s>-;1G-QM z;~rf8w0lIoS?u6rxqvd1e5ni1C$Yh%>2c3>b8l@F z5?p11tZ^<`&U1l|PEBjNZ%44!Bc3@m+h`+-546u3e(Xr9Gqkb(EZ}eteru)l4!6-d zK)1K);cpz?i1&nw&xIS0+KNeN}y<+TL5~npT$taERlu z5uXBz9=Ztteay>V4GwWG>OX5w=seQjk zmQ6lubz!yRNfK^zf=R#!1Gk-K;_6S^VbZ<-r%Ht8&3jI)Nw`=K$6|QI}5YFA$j58%ieK%rFT7c^qee zJ!@a5b$^AVT19X)%`Z-9x0SY6nYM9;1aO%=d7k(ks-vr9(0Y>I=J!a`E@NnN*7qTm zVJD35C*@V|jFHIvD$C*j07~6jcvkJFO=&v;;=R9AxOBf;F=`0^Re9_V--YZ1fxJw~pD47Ff(h7v(s@ z0HEah;QLmmsX8xT({Frj6YSTbRVX8qmn*lB6^=RQvF+OxpXqCPMzAffC)KQ$7x30Y zYSZL^K2kBj86DTmdU6zJ+CWWsAJWq6IwrQQ`#$zr1j?#*5#vMw1R_W`WPna{#w(`l zom-_?=~K@mkm$lA316Ee0!A_qW9lm}uYT@TeH!8xx}1fHrLjb7V~`spD(ugZBPX}l zJJhx&3rmYT$GJCgOn-jPd=_#r!;#7CH*!6}$QC?~5o#j1sdp^;SE*o5rk3VNV6=u= zGV*<#c(P;+A`#?Ie@yXRZ$q5w79#EKWVKde5hQhHAaRp|anF`(=Wc?YYZijuXJ8SFqXiAMutx4jxv!c&EOflxDby`3inrM$Y_10FgULz8GIO8v#eFwx ztZ6sOpJLQ)XK64~V}?BTh z(2hmHOl~asvfpm9-V8A;Td=bvY8Pl62R!gBw3W|i(2 zS-3>Bc$Vp27DBOr#O`d2`El5vF5UfNXmeV}my;p9MPRDW&JUJ21RQrhl$xY!mX_A= z_-(Q_`GWHK803C`odV7_c8wM-sKDXE^T($>vJF+X<3W6z9WvuowU*cRTgs~xo6>ZV zd`@`ILHtPP74!$gy;faQr=$B^!b`Y`SwWac{2XNAR~?8Q&m5CqFhQu8?v~mHX#`EW zG66;p&lv|lsXl~PR`@4%ar;{8<5AXxdlMLw=H4Fu3*9FH5OSCTf4-GQFf z>v##jbvza*0$~(8=SnV^ars~e6OG_5Kv(wdMca2mxj)aqv z>B@Hl3Q5UaoN--=W=n%IgJ(n93C=jLVxQ-nSHGQEyj`OFVb^=QP}LUgIOmrvT(8{PBvw{BnoI=(S_c%gE2@!}`|jWF9Le z@rFMeps$WUvv^yN{_#iks`4+Dj|cN3jp*S-Pq2biWOM1>r?BI4@tUKzSuI7gN69js z*#7DAtE@XKFCznkT-f$COSuwhB8`+wBB^7A&zbZT*O-RcfH9BfSC*YAbE;dU^Gw#z ze*glnOB0~bNGsiT#+sd{{VxI2DNs%`_`oXPPne9tbJ>cd?w!) zR|pt@4XmJoIXEj_dui+k6}L|QN;BpAK~>N9sEIN2%>XNT3Q&aein@@AvRYm39kWGS z;78r6Z94wXP&0jwUt-v~9SCZcZFFrsX#UU_e}$2|Z>}nXucCue(dV($XEu64K=(6=2xEpA zL)!{``Q&7An#pyZqQh3T);enP-tOkxFx(_%TMOl%GCaOtkUh&`fB4%xt%Rp z8DwIxZWACNZ8*;-JY&P<7wCiPDlrF z$;t0odpOK;T;I)?h98zBL9w0o`+5u=;&+fpPUDhJ(pNrU@;h-&l1r<- zCTR&to>TJ@#FboRbNCzpeCrIFk#S>dw*KG4wmvXL+Eq>z_VWiPI2`fi>0Lif_)QJo zm2atQT8+h!@SU!skpL->dFMM$a7aDtn(3F9x_e#PNnp)>>w^@~?ISx$JdOthagO}e z)Y`9CbpvUz>k?{r7BV>!+N5|=sz&pGa##V|8Rv`+D*O?Id!Z%IW3T)q)USc?y;grJq3E8K(YjjFWzy0@Zgv?Ymphm!z{owjpU$*9 zhC5eJ6{z+zNpdD!@tST9aC1)FGfA3#z}hAadujEh&$TYvn~0(?ieJK?w~A7~N?qt} zfVUA|alS&2#Xz&YXvVCbc9Ma<;*S7IQgLgH1dGDHCt(x}EXz)%(?lhS+{kR-g zw)5JhqEkK-w%V2jCv^(W$@2rp)8367h6vKq2_7i+VVfmJ_FuGk zM8;cg{&AmAtvcS`;%Q38ks~P1Wp!RZ6G_Kp0g+|SP6iHpaaL<@yhZkFnJw4?tWcfJ z$vD72PSsASAdyu_c0NjT%}>*>8%)#^;^HQ7fRKUqbB;N|t02;S$>g@w?4=qgmMfz& zgW2TK^8j6+J5(_H+?D&kz%rZIe>BRsAF@$Cy6XZFq)`BhKxQ|fO=V>(++9BSn zBwzu83ggq>q4iD8iFLQIwzo3yOp6DGROBD;Hha{Ig>cE(&rBtF64F!QrMK`f0P=Ek zj^@0h#Ze8r6whpVnwm80txofEMxcVLDBXdaV4ASMn@`bXWYc^xJ~qGo;;;K9t32|h zDvRV}Gdjy0#wl2t6yW|8Kv`5UQX)CWr)caV;OIh@roGMbym6iM3?fk zo*#$1jD_?bpM^3R?Sq*miKQpifEoO%#`xeUve2w8Zl~RQs6}l8{L%m!-9~(X1b(&A zwO*a*dGyP1(|t(^z2nA;<=IPl4Zk9c`{eOiZi~HxM7obwO9&^B^KTu>8521JuALloMe2I| zI!=+N>2|k@+vmQDC6DatIVD(N7V-zw)e30+4bz&ex~`#l4csmd+3mF?c4@i$>9-+) zCmA65@li!Na0M$eny+1I`j^^al5;dt7S1K!Iqno-5V!-6Zrs&pM_uSIthD;|gG~y= zc1IMhh1}y9Dn<{~yNjBHM^)9B20EKBy;Ud0g_JV$K&PNxZg#sgI~)IrLA3Q`gjGB z818^}9(Z*5LC$-6_pN53)|Q$MlGc4MV_|D%hxct9=*;|rOZ?d#nUZ`{b^@E1jYmJb13z4x`DA`I2KEgK>hd?9;>ga8;DT9qh8>1`f-rwGT&GL;tA2>_ z$87gxm1zF}basrf-ZC{L-ZDc5kynGxKptN7 z)6E#I!YH$E@d_O}?_9Qq!c?1AP#qwQXT&SW4ds9cY#s>DwP5vqe_m@zZSU<2@}Xk8 zp+M2U&A4TOZtQ!Sul=Acbl5HJ?qh?&B1?&+eeCk~=OeHjVB~Ut8-Awik*(U3uzyLy7KO45>Dh0pn-sJaHF}v{4-m- z{+GVJ>9^J$BZ}Q4!m=YS96rssVhQ6U5IN751!JsM?HUO0Mct&*vGAHtn}9oK83Wt! ztDS4E^=)F}8*N778RCu6PlFE7fOlbmpZ9Z*I?n|O-S-*UMReMggpU+JJ5Rtb5F?T? zjllN6@9kE47gd`jn%d&a!&zbHXUUAJ>&3B|aRrfX;w@g{}J+;oWrV$VjJg*;# zxWj|AE0fP;&#yk!&9$DE)NC(ewVD%&CMdE4+@9X!ft+{1tv-!-_IsT?NcX7?;L7oa z9PoCK2Zda=ZP$3X|5IWuB_Yx$p<_h+>GZUn;xlj@;}(TOKwD90l1Tn zGJL%Kxv098+grsX64|_H+f;7I0}G7)MyBZcVMs;8ISY&obGc6#$T`8}@y{H3(mKb> zC30J6_d2GhacgyTJhMWMi7ZOz#BG~`sN4^h?0Iv~d{ejI3SJ*;N<5S1HT5ohv}Oi*?JwepWsCa zfi{(mizzudU{5DK`Sq>Z2Rzc;`xRx9j7ue=6%ZqcW2m1uuGPljly& zN(G)HHSBXspL*~<)Oi4#LGCNXX~BhiaK#m9oDAB1Gg;4xa2a|NR$P79L;nE6>fh3| z+lP>PblpK7(ljT%bU?NEy9zozxLox?J5g)`0Go7MDm4DjG{{YIn zGJ#w_rQ|8snnam6Ym9&4YVIkFb6W7e8Ae6P;fnX4YI77EZW*C{rUNm6O%ap~><%|$ z*B+H^h;#3VT3)}W&fQg`-H9f)8>K~zE>s)<*grVIJbQU&u-#qMZ>`(ME~}#(E7?$p zuAl`&$HrLTl1BgnTeEf*(>^I)U)yPLNSJFM6bZy+qm2Bk&mo554{tBamg$!IhO5)a zCDp9-Li_V`ASHcOM_#A6xb)bX0niyUaNv9a{FtFKRd zE^V!2SP^A;E(u?dakK&0=bU-ws5HwPLt&{hUPlPDhXUB*+19F|Y!SfgZlU)y{`d$q! zylJkQ_EvKt@gYsCA@1wJWn<=d9M>QCWAQQ_3r!kj#8(s9EQ#P4*LQ}eCQp3xoMhJW z@8X`AtOc!w)S-crH+EDKpzc5?Ao4iJoodB6IPQ^Dl)D`i0o)p2Dy6pS8wqp^eO#7B zxq!_S2m4pY@Tl`OZ7tocu#4>yEM{g28nK>oF0N@k*Ix+M+HX>;|Z5-7$NoJnJnMKhu%H#~XQgHkvd;L~twP#bBE zEPZRm{pp@{<=+g(I`M^d(zjx8k>SRFlW5*ecnq3$%21UoG{g(fl_*i`N>?6KfD_uKl8=Ek zeJ)Eh-6Vms#Z^G^9m({lFf6k&#F=K~g(j`KuHs{GmfP_Prr>}8GwbhE@o|ti#z*N~ zTr!lGC5)M+EGi|EQgWOQJnK(;9hLlRm|9tis=xwm$Q{^^D$ZJ}$R*x0uoAx2chaCN z@j8+ijiiEm6Uh2iI*8XJBoNu#TZmyxO_+SiDRYP8%Z@ptYc~3IlndcmHO zES3Ajwi;|Qa}S9eq?w2YEC|y;E21vy|-x|7;OoilZgvqLC@!==n(4hd=T(PIgco^+NRVN;1fOjCdy0(Kmge^MLmpi% zx2YtY91dzC-Q8RLqUsBYVwxh6BcqU24n_t%umGfa9_kr(G;ml&rwOfX)+oF-AtjUV zdHEhav-wm;y@pwpS4|Ltfh3$BNyl@+&tr^zO=jnwP<$}wYv*^V^Y2no+&JesP!4(E z)@W{~(L5K)s5QK@8#xSY>4pRna0Wp9@z@V4fp8+dFiUZNa`HwtWPymmCkj2PvfvAO z+Bkki0QUNyKfqUnj~Oh40uYdJ50xt3yD~0IZFc0$E#Z*dAlS!#-U!bHuzdNf+LxN$ z0Flv##0JJ-xq`Q0N*KlWFkQE~$daKWsEUA-Qs2<|O2WVpJc(u-6*yv99Vjw_2{Fad9jG zd?UMHPI16o4=+mA?h5P&bDQEtv{3Gf#HJf*Lgy+lJK%AT>CP)P)?HBsv!-3aX3ARY zZ_7J^08$F^o^nC*_x7%BrrlhNQxLeAM)8(q-f+j%k3Pb*x-F%=ojs;qG(b%x+g?Q_ zO9JF_M|!PJQ(S??BR6SreWvShL95xxy50Aid5?y&4#SW*1mnw}QC8ZvtM>b^7Si6| zPX)g9k_h$y0LP|$^Uo%t^^K$n)X<b$vG$V?NOjVHaNf~M;(PHqNT@y z(-|Bw+9J3nNtAOTCoFu)^`M)hy9We$4=>7-Q)A-eCjeAb13AYvY+{#SSzwM>7iR1` zdKwmTer6r$(STs>-lEI^+kxjqC4^oXf!}}KDmLb$Jji4}F9wn(J71sUpt69+ihv4@ zh6jxK)ANy)^6oQ0Za}C100=bzNm&R$0sIK3%+EEmLM1V702WX<1OAWaP=g%zqN5Qe z0pk?1f|qdFchUFvdKR*}s;#V!g9^dfd-vQ2ag2<6@n1s0t6ImTwxNA@Z442XR-Crj zxH#aSEq>iW5zY^>& z;g?P6_qu5%1~GV%kW`-A6pZAPw{gY^&JAvQdq_Go6GGaB-TabE;z>2Z1s*vI~=O4d}XFcg!Cp5L;PN`%Y&(fmnF&VUr*gyoyZ6bm7LMml> zz$(CwFh}V{vLl7`C4%Z&jf4!WRh%>9`=gWmtE)>Kt9HgrvK0f5QCNS1@W$7>*>cGT z%5&Q|718cdd_%{d^{zwZv!N7<3-ndXgvl-QWY&9Ag`}O>ji6&6g=)1$02~i0%udYg z!Hz&)D4;VnZvOyg>WwmRKnryj>cLO*HQWYIbIovVMT+V$FgPL9kEm>l`V6A(ZU=K( zuu81YkzEmR#_$^10Aae@v{E+OX=%dy*}z| zX@$}$XxxBCM+A01jXqh_8wNOXW1C&lF70pO8no8YNhE9bN#@VWGD4n8=j1|8e^Y~i zH|}8ll9!W9Xp?SKQWTNgg#~~F0C_$_i>psB?P-2)JUrtA8Oj{v>dpDnH49iGODBys zGDAB8821G+_3mqlmn)QP)|DfDWoOaa-hdZfxG~+Eg^DMP65%)lD4YOxmB+Smn#}x2 z=v^MsrM=S))E3EZ9y@l7i8m`KW?!{kf8lAddo6H9R?U{5#egrsle9*o-Wi zdAEw*Sjm=Y3+`Sq^5Z8w;{vO{SI zgHp6JqA@8L9IN9U&zB(n0<^t1t+tNb+n!gj30hs}N z)=^rUJ5{?T&NnvEz(!%-IRQb)7zE^J%wn(l2dbMzy8Wfqm)Do~8&25VZ)n)(%=SDF zF^u=Ae-AprFFjXMqIP)}Q z+LGGASKR4+EBiyLx~|__j?2K(l(eh5AX9cg@BQuue?EKHP*@tLX*PDTX_l!J2^y?w z6;xmWup9s=mocKK!s4}Jn~yKJuN{?s8O}3E$VWay&ZgU{w4X4~H8yCY2tYxKx#knn zDDz2^@jF8>6>~l&h-c&aQMD_)ED9$Nx0OGwu z!$+_wMm+@}r-4$)Ap^))vF%Ph>6t#XLM{b+E9Pmm+5urV4?<}Rk4hAT`qz`5^ei+N U#2(e{-gu+{XSFUUSX58{+0eR0umAu6 literal 0 HcmV?d00001 diff --git a/assets/layers/guidepost/signpost_example.jpg.license b/assets/layers/guidepost/signpost_example.jpg.license new file mode 100644 index 0000000000..c4fea401e4 --- /dev/null +++ b/assets/layers/guidepost/signpost_example.jpg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: Mschaeuble +SPDX-License-Identifier: CC0 \ No newline at end of file From a96a214c03b3438f9ccf96f7c37a2e842ec224e1 Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Sat, 7 Oct 2023 03:37:19 +0200 Subject: [PATCH 04/30] Themes: add guideposts to climbing theme --- assets/layers/guidepost/guidepost.json | 14 ++--- ...post_example.jpg => guidepost_example.jpg} | Bin .../guidepost/guidepost_example.jpg.license | 2 + assets/layers/guidepost/license_info.json | 2 +- .../guidepost/signpost_example.jpg.license | 2 +- assets/themes/climbing/climbing.json | 3 +- src/Logic/State/UserSettingsMetaTagging.ts | 48 ++++-------------- 7 files changed, 23 insertions(+), 48 deletions(-) rename assets/layers/guidepost/{signpost_example.jpg => guidepost_example.jpg} (100%) create mode 100644 assets/layers/guidepost/guidepost_example.jpg.license diff --git a/assets/layers/guidepost/guidepost.json b/assets/layers/guidepost/guidepost.json index 28e2d80b6d..6fd27bcc42 100644 --- a/assets/layers/guidepost/guidepost.json +++ b/assets/layers/guidepost/guidepost.json @@ -32,18 +32,18 @@ "enableImproveAccuracy": "true", "enableRelocation": "false" }, - "title": {}, - "pointRendering": [ + "title": { + "render": { + "en": "Guidepost" + } + }, + "mapRendering": [ { "location": [ "centroid", "point" ], - "marker": [ - { - "icon": "./assets/layers/guidepost/guidepost.svg" - } - ], + "icon": "./assets/layers/guidepost/guidepost.svg", "anchor": "bottom" } ], diff --git a/assets/layers/guidepost/signpost_example.jpg b/assets/layers/guidepost/guidepost_example.jpg similarity index 100% rename from assets/layers/guidepost/signpost_example.jpg rename to assets/layers/guidepost/guidepost_example.jpg diff --git a/assets/layers/guidepost/guidepost_example.jpg.license b/assets/layers/guidepost/guidepost_example.jpg.license new file mode 100644 index 0000000000..cd1b487693 --- /dev/null +++ b/assets/layers/guidepost/guidepost_example.jpg.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: Mschaeuble +SPDX-License-Identifier: CC0-1.0 \ No newline at end of file diff --git a/assets/layers/guidepost/license_info.json b/assets/layers/guidepost/license_info.json index 851b7188ad..e622ffc30a 100644 --- a/assets/layers/guidepost/license_info.json +++ b/assets/layers/guidepost/license_info.json @@ -10,7 +10,7 @@ ] }, { - "path": "signpost_example.jpg", + "path": "guidepost_example.jpg", "license": "CC0-1.0", "authors": [ "Mschaeuble" diff --git a/assets/layers/guidepost/signpost_example.jpg.license b/assets/layers/guidepost/signpost_example.jpg.license index c4fea401e4..cd1b487693 100644 --- a/assets/layers/guidepost/signpost_example.jpg.license +++ b/assets/layers/guidepost/signpost_example.jpg.license @@ -1,2 +1,2 @@ SPDX-FileCopyrightText: Mschaeuble -SPDX-License-Identifier: CC0 \ No newline at end of file +SPDX-License-Identifier: CC0-1.0 \ No newline at end of file diff --git a/assets/themes/climbing/climbing.json b/assets/themes/climbing/climbing.json index f5bb2c2dd1..90f4dc383b 100644 --- a/assets/themes/climbing/climbing.json +++ b/assets/themes/climbing/climbing.json @@ -464,7 +464,8 @@ { "builtin": [ "toilet", - "drinking_water" + "drinking_water", + "guidepost" ], "override": { "minzoom": 15 diff --git a/src/Logic/State/UserSettingsMetaTagging.ts b/src/Logic/State/UserSettingsMetaTagging.ts index 6e568c5c32..33a5ae85b5 100644 --- a/src/Logic/State/UserSettingsMetaTagging.ts +++ b/src/Logic/State/UserSettingsMetaTagging.ts @@ -1,42 +1,14 @@ import { Utils } from "../../Utils" /** This code is autogenerated - do not edit. Edit ./assets/layers/usersettings/usersettings.json instead */ export class ThemeMetaTagging { - public static readonly themeName = "usersettings" + public static readonly themeName = "usersettings" - public metaTaggging_for_usersettings(feat: { properties: Record }) { - Utils.AddLazyProperty(feat.properties, "_mastodon_candidate_md", () => - feat.properties._description - .match(/\[[^\]]*\]\((.*(mastodon|en.osm.town).*)\).*/) - ?.at(1) - ) - Utils.AddLazyProperty( - feat.properties, - "_d", - () => feat.properties._description?.replace(/</g, "<")?.replace(/>/g, ">") ?? "" - ) - Utils.AddLazyProperty(feat.properties, "_mastodon_candidate_a", () => - ((feat) => { - const e = document.createElement("div") - e.innerHTML = feat.properties._d - return Array.from(e.getElementsByTagName("a")).filter( - (a) => a.href.match(/mastodon|en.osm.town/) !== null - )[0]?.href - })(feat) - ) - Utils.AddLazyProperty(feat.properties, "_mastodon_link", () => - ((feat) => { - const e = document.createElement("div") - e.innerHTML = feat.properties._d - return Array.from(e.getElementsByTagName("a")).filter( - (a) => a.getAttribute("rel")?.indexOf("me") >= 0 - )[0]?.href - })(feat) - ) - Utils.AddLazyProperty( - feat.properties, - "_mastodon_candidate", - () => feat.properties._mastodon_candidate_md ?? feat.properties._mastodon_candidate_a - ) - feat.properties["__current_backgroun"] = "initial_value" - } -} + public metaTaggging_for_usersettings(feat: {properties: Record}) { + Utils.AddLazyProperty(feat.properties, '_mastodon_candidate_md', () => feat.properties._description.match(/\[[^\]]*\]\((.*(mastodon|en.osm.town).*)\).*/)?.at(1) ) + Utils.AddLazyProperty(feat.properties, '_d', () => feat.properties._description?.replace(/</g,'<')?.replace(/>/g,'>') ?? '' ) + Utils.AddLazyProperty(feat.properties, '_mastodon_candidate_a', () => (feat => {const e = document.createElement('div');e.innerHTML = feat.properties._d;return Array.from(e.getElementsByTagName("a")).filter(a => a.href.match(/mastodon|en.osm.town/) !== null)[0]?.href }) (feat) ) + Utils.AddLazyProperty(feat.properties, '_mastodon_link', () => (feat => {const e = document.createElement('div');e.innerHTML = feat.properties._d;return Array.from(e.getElementsByTagName("a")).filter(a => a.getAttribute("rel")?.indexOf('me') >= 0)[0]?.href})(feat) ) + Utils.AddLazyProperty(feat.properties, '_mastodon_candidate', () => feat.properties._mastodon_candidate_md ?? feat.properties._mastodon_candidate_a ) + feat.properties['__current_backgroun'] = 'initial_value' + } +} \ No newline at end of file From 6f376291cec7f2f168b27b657d7f1aac25ca825f Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Fri, 6 Oct 2023 14:40:50 +0200 Subject: [PATCH 05/30] Dev: show IP-address instead of when booted (somewhat of a hack) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c843af3732..e929de00a5 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,7 @@ }, "scripts": { "start": "npm run generate:layeroverview && npm run strt", - "strt": "vite --host", + "strt": "vite --host | sed 's/localhost:/127.0.0.1:/g'", "strttest": "export NODE_OPTIONS=--max_old_space_size=8364 && parcel serve test.html assets/templates/*.svg assets/templates/fonts/*.ttf", "watch:css": "tailwindcss -i index.css -o public/css/index-tailwind-output.css --watch", "generate:css": "tailwindcss -i src/index.css -o public/css/index-tailwind-output.css", From 650c1a675c783179482338f449ca1202246916a0 Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Fri, 6 Oct 2023 14:41:22 +0200 Subject: [PATCH 06/30] Fix: fix updating of styles --- src/UI/Map/ShowDataLayer.ts | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/UI/Map/ShowDataLayer.ts b/src/UI/Map/ShowDataLayer.ts index 671b6478de..34ac01078b 100644 --- a/src/UI/Map/ShowDataLayer.ts +++ b/src/UI/Map/ShowDataLayer.ts @@ -16,6 +16,7 @@ import { LayerConfigJson } from "../../Models/ThemeConfig/Json/LayerConfigJson" import PerLayerFeatureSourceSplitter from "../../Logic/FeatureSource/PerLayerFeatureSourceSplitter" import FilteredLayer from "../../Models/FilteredLayer" import SimpleFeatureSource from "../../Logic/FeatureSource/Sources/SimpleFeatureSource" +import { CLIENT_RENEG_LIMIT } from "tls"; class PointRenderingLayer { private readonly _config: PointRenderingConfig @@ -229,7 +230,10 @@ class LineRenderingLayer { const self = this features.features.addCallbackAndRunD(() => self.update(features.features)) - map.on("styledata", () => self.update(features.features)) + map.on("styledata", () => { + self._listenerInstalledOn.clear() + return self.update(features.features); + }) } public destruct(): void { @@ -406,13 +410,10 @@ class LineRenderingLayer { } else { const tags = this._fetchStore(id) this._listenerInstalledOn.add(id) - map.setFeatureState( - { source: this._layername, id }, - this.calculatePropsFor(feature.properties) - ) - tags.addCallbackD((properties) => { - if (!map.getLayer(this._layername)) { - return + tags.addCallbackAndRunD((properties) => { + // Make sure to use 'getSource' here, the layer names are different! + if(map.getSource(this._layername) === undefined){ + return true } map.setFeatureState( { source: this._layername, id }, From 46e7cf58333eda32946b0605fc9c1631a6af51d9 Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Fri, 6 Oct 2023 15:14:51 +0200 Subject: [PATCH 07/30] Some tweaking --- src/UI/Map/MapLibreAdaptor.ts | 22 +++++++++++----------- src/UI/Map/ShowDataLayer.ts | 5 +---- 2 files changed, 12 insertions(+), 15 deletions(-) diff --git a/src/UI/Map/MapLibreAdaptor.ts b/src/UI/Map/MapLibreAdaptor.ts index d41bcc25dd..a94a32f6d1 100644 --- a/src/UI/Map/MapLibreAdaptor.ts +++ b/src/UI/Map/MapLibreAdaptor.ts @@ -1,14 +1,14 @@ -import { Store, UIEventSource } from "../../Logic/UIEventSource" -import type { Map as MLMap } from "maplibre-gl" -import { Map as MlMap, SourceSpecification } from "maplibre-gl" -import { AvailableRasterLayers, RasterLayerPolygon } from "../../Models/RasterLayers" -import { Utils } from "../../Utils" -import { BBox } from "../../Logic/BBox" -import { ExportableMap, MapProperties } from "../../Models/MapProperties" -import SvelteUIElement from "../Base/SvelteUIElement" -import MaplibreMap from "./MaplibreMap.svelte" -import { RasterLayerProperties } from "../../Models/RasterLayerProperties" -import * as htmltoimage from "html-to-image" +import { Store, UIEventSource } from "../../Logic/UIEventSource"; +import type { Map as MLMap } from "maplibre-gl"; +import { Map as MlMap, SourceSpecification } from "maplibre-gl"; +import { AvailableRasterLayers, RasterLayerPolygon } from "../../Models/RasterLayers"; +import { Utils } from "../../Utils"; +import { BBox } from "../../Logic/BBox"; +import { ExportableMap, MapProperties } from "../../Models/MapProperties"; +import SvelteUIElement from "../Base/SvelteUIElement"; +import MaplibreMap from "./MaplibreMap.svelte"; +import { RasterLayerProperties } from "../../Models/RasterLayerProperties"; +import * as htmltoimage from "html-to-image"; /** * The 'MapLibreAdaptor' bridges 'MapLibre' with the various properties of the `MapProperties` diff --git a/src/UI/Map/ShowDataLayer.ts b/src/UI/Map/ShowDataLayer.ts index 34ac01078b..121f007194 100644 --- a/src/UI/Map/ShowDataLayer.ts +++ b/src/UI/Map/ShowDataLayer.ts @@ -230,10 +230,7 @@ class LineRenderingLayer { const self = this features.features.addCallbackAndRunD(() => self.update(features.features)) - map.on("styledata", () => { - self._listenerInstalledOn.clear() - return self.update(features.features); - }) + map.on("styledata", () => self.update(features.features)) } public destruct(): void { From 52e647669482f676a94730e7669a84130bf3cfc7 Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Mon, 9 Oct 2023 00:43:11 +0200 Subject: [PATCH 08/30] Feature: add 'filter'-button shortcut to bottom-left controls --- src/UI/ThemeViewGUI.svelte | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/UI/ThemeViewGUI.svelte b/src/UI/ThemeViewGUI.svelte index a20628a3ae..dff70dc7e7 100644 --- a/src/UI/ThemeViewGUI.svelte +++ b/src/UI/ThemeViewGUI.svelte @@ -52,6 +52,7 @@ import LanguagePicker from "./LanguagePicker" import Locale from "./i18n/Locale" import ShareScreen from "./BigComponents/ShareScreen.svelte" + import { Square3Stack3dIcon } from "@babeard/svelte-heroicons/solid"; export let state: ThemeViewState let layout = state.layout @@ -170,6 +171,10 @@
+ state.guistate.openFilterView()}> + + + Date: Mon, 9 Oct 2023 00:52:06 +0200 Subject: [PATCH 09/30] Don't ignore escape if a textfield is selected --- src/UI/Base/Hotkeys.ts | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/UI/Base/Hotkeys.ts b/src/UI/Base/Hotkeys.ts index 6b4f073650..e40d921664 100644 --- a/src/UI/Base/Hotkeys.ts +++ b/src/UI/Base/Hotkeys.ts @@ -22,7 +22,14 @@ export default class Hotkeys { }[] >([]) - private static textElementSelected(): boolean { + private static textElementSelected(event: KeyboardEvent): boolean { + if(event.ctrlKey || event.altKey){ + // This is an event with a modifier-key, lets not ignore it + return false + } + if(event.key === "Escape"){ + return false // Another not-printable character that should not be ignored + } return ["input", "textarea"].includes(document?.activeElement?.tagName?.toLowerCase()) } public static RegisterHotkey( @@ -68,7 +75,7 @@ export default class Hotkeys { }) } else if (key["shift"] !== undefined) { document.addEventListener(type, function (event) { - if (Hotkeys.textElementSelected()) { + if (Hotkeys.textElementSelected(event)) { // A text element is selected, we don't do anything special return } @@ -86,7 +93,7 @@ export default class Hotkeys { }) } else if (key["nomod"] !== undefined) { document.addEventListener(type, function (event) { - if (Hotkeys.textElementSelected()) { + if (Hotkeys.textElementSelected(event)) { // A text element is selected, we don't do anything special return } From 285fe9ab833b0cf0c0ea00a011457a83bd7ad6d2 Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Mon, 9 Oct 2023 01:26:12 +0200 Subject: [PATCH 10/30] UX: place 'add new button' in bottom-left instead of having a dynamic pin --- langs/en.json | 2 +- langs/nl.json | 2 +- .../Sources/LastClickFeatureSource.ts | 68 +- src/Logic/Web/ThemeViewStateHashActor.ts | 2 +- src/Models/Constants.ts | 2 +- src/Models/ThemeViewState.ts | 1164 +++++++++-------- src/UI/ThemeViewGUI.svelte | 193 +-- 7 files changed, 730 insertions(+), 703 deletions(-) diff --git a/langs/en.json b/langs/en.json index 7167afb957..7bd2338e5b 100644 --- a/langs/en.json +++ b/langs/en.json @@ -124,7 +124,7 @@ "pleaseLogin": "Please log in to add a new feature", "presetInfo": "The new POI will have {tags}", "stillLoading": "The data is still loading. Please wait a bit before you add a new feature.", - "title": "Add a new feature?", + "title": "Add a new feature", "warnVisibleForEveryone": "Your addition will be visible for everyone", "wrongType": "This feature is not a node or a way and can not be imported", "zoomInFurther": "Zoom in further to add a feature.", diff --git a/langs/nl.json b/langs/nl.json index 70101f0b20..03f4923e48 100644 --- a/langs/nl.json +++ b/langs/nl.json @@ -124,7 +124,7 @@ "pleaseLogin": "Gelieve je aan te melden om een object toe te voegen", "presetInfo": "Het nieuwe object krijgt de attributen {tags}", "stillLoading": "De data worden nog geladen. Nog even geduld en dan kan je een object toevoegen.", - "title": "Nieuw object toevoegen?", + "title": "Nieuw object toevoegen", "warnVisibleForEveryone": "Je toevoeging is voor iedereen zichtbaar", "wrongType": "Dit object is geen punt of lijn en kan daarom niet geïmporteerd worden", "zoomInFurther": "Gelieve verder in te zoomen om een object toe te voegen.", diff --git a/src/Logic/FeatureSource/Sources/LastClickFeatureSource.ts b/src/Logic/FeatureSource/Sources/LastClickFeatureSource.ts index ecd8d82884..b45169c0ad 100644 --- a/src/Logic/FeatureSource/Sources/LastClickFeatureSource.ts +++ b/src/Logic/FeatureSource/Sources/LastClickFeatureSource.ts @@ -1,10 +1,11 @@ -import LayoutConfig from "../../../Models/ThemeConfig/LayoutConfig" -import { WritableFeatureSource } from "../FeatureSource" -import { ImmutableStore, Store, UIEventSource } from "../../UIEventSource" -import { Feature, Point } from "geojson" -import { TagUtils } from "../../Tags/TagUtils" -import BaseUIElement from "../../../UI/BaseUIElement" -import { Utils } from "../../../Utils" +import LayoutConfig from "../../../Models/ThemeConfig/LayoutConfig"; +import { WritableFeatureSource } from "../FeatureSource"; +import { ImmutableStore, Store, UIEventSource } from "../../UIEventSource"; +import { Feature, Point } from "geojson"; +import { TagUtils } from "../../Tags/TagUtils"; +import BaseUIElement from "../../../UI/BaseUIElement"; +import { Utils } from "../../../Utils"; +import { OsmTags } from "../../../Models/OsmFeature"; /** * Highly specialized feature source. @@ -12,8 +13,14 @@ import { Utils } from "../../../Utils" */ export class LastClickFeatureSource implements WritableFeatureSource { public readonly features: UIEventSource = new UIEventSource([]) + private i: number = 0 + private readonly hasNoteLayer: string + private readonly renderings: string[]; + private readonly hasPresets: string; constructor(location: Store<{ lon: number; lat: number }>, layout: LayoutConfig) { + this.hasNoteLayer = layout.layers.some((l) => l.id === "note") ? "yes" : "no" + this.hasPresets= layout.layers.some((l) => l.presets?.length > 0) ? "yes" : "no" const allPresets: BaseUIElement[] = [] for (const layer of layout.layers) for (let i = 0; i < (layer.presets ?? []).length; i++) { @@ -26,35 +33,36 @@ export class LastClickFeatureSource implements WritableFeatureSource { allPresets.push(html) } - const renderings = Utils.Dedup( + this.renderings = Utils.Dedup( allPresets.map((uiElem) => Utils.runningFromConsole ? "" : uiElem.ConstructElement().innerHTML ) ) - let i = 0 - location.addCallbackAndRunD(({ lon, lat }) => { - const properties = { - lastclick: "yes", - id: "last_click_" + i, - has_note_layer: layout.layers.some((l) => l.id === "note") ? "yes" : "no", - has_presets: layout.layers.some((l) => l.presets?.length > 0) ? "yes" : "no", - renderings: renderings.join(""), - number_of_presets: "" + renderings.length, - first_preset: renderings[0], - } - i++ - - const point = >{ - type: "Feature", - properties, - geometry: { - type: "Point", - coordinates: [lon, lat], - }, - } - this.features.setData([point]) + this.features.setData([this.createFeature(lon, lat)]) }) } + + public createFeature(lon: number, lat: number): Feature { + const properties: OsmTags = { + lastclick: "yes", + id: "last_click_" + this.i, + has_note_layer: this.hasNoteLayer , + has_presets:this.hasPresets , + renderings: this.renderings.join(""), + number_of_presets: "" +this. renderings.length, + first_preset: this.renderings[0], + } + this. i++ + + return >{ + type: "Feature", + properties, + geometry: { + type: "Point", + coordinates: [lon, lat], + }, + } + } } diff --git a/src/Logic/Web/ThemeViewStateHashActor.ts b/src/Logic/Web/ThemeViewStateHashActor.ts index b10208f09e..93a2326e9f 100644 --- a/src/Logic/Web/ThemeViewStateHashActor.ts +++ b/src/Logic/Web/ThemeViewStateHashActor.ts @@ -175,7 +175,7 @@ export default class ThemeViewStateHashActor { } private back() { - console.log("Got a back event") + console.trace("Got a back event") const state = this._state // history.pushState(null, null, window.location.pathname); if (state.selectedElement.data) { diff --git a/src/Models/Constants.ts b/src/Models/Constants.ts index ad68f6979e..39aa182ad7 100644 --- a/src/Models/Constants.ts +++ b/src/Models/Constants.ts @@ -58,7 +58,7 @@ export default class Constants { importHelperUnlock: 5000, } - static readonly minZoomLevelToAddNewPoint = Constants.isRetina() ? 18 : 19 + static readonly minZoomLevelToAddNewPoint = Constants.isRetina() ? 17 : 18 /** * Used by 'PendingChangesUploader', which waits this amount of seconds to upload changes. * (Note that pendingChanges might upload sooner if the popup is closed or similar) diff --git a/src/Models/ThemeViewState.ts b/src/Models/ThemeViewState.ts index a15f6b0569..6a03a41d9e 100644 --- a/src/Models/ThemeViewState.ts +++ b/src/Models/ThemeViewState.ts @@ -1,62 +1,58 @@ -import LayoutConfig from "./ThemeConfig/LayoutConfig" -import { SpecialVisualizationState } from "../UI/SpecialVisualization" -import { Changes } from "../Logic/Osm/Changes" -import { Store, UIEventSource } from "../Logic/UIEventSource" +import LayoutConfig from "./ThemeConfig/LayoutConfig"; +import { SpecialVisualizationState } from "../UI/SpecialVisualization"; +import { Changes } from "../Logic/Osm/Changes"; +import { Store, UIEventSource } from "../Logic/UIEventSource"; +import { FeatureSource, IndexedFeatureSource, WritableFeatureSource } from "../Logic/FeatureSource/FeatureSource"; +import { OsmConnection } from "../Logic/Osm/OsmConnection"; +import { ExportableMap, MapProperties } from "./MapProperties"; +import LayerState from "../Logic/State/LayerState"; +import { Feature, Point, Polygon } from "geojson"; +import FullNodeDatabaseSource from "../Logic/FeatureSource/TiledFeatureSource/FullNodeDatabaseSource"; +import { Map as MlMap } from "maplibre-gl"; +import InitialMapPositioning from "../Logic/Actors/InitialMapPositioning"; +import { MapLibreAdaptor } from "../UI/Map/MapLibreAdaptor"; +import { GeoLocationState } from "../Logic/State/GeoLocationState"; +import FeatureSwitchState from "../Logic/State/FeatureSwitchState"; +import { QueryParameters } from "../Logic/Web/QueryParameters"; +import UserRelatedState from "../Logic/State/UserRelatedState"; +import LayerConfig from "./ThemeConfig/LayerConfig"; +import GeoLocationHandler from "../Logic/Actors/GeoLocationHandler"; +import { AvailableRasterLayers, RasterLayerPolygon, RasterLayerUtils } from "./RasterLayers"; +import LayoutSource from "../Logic/FeatureSource/Sources/LayoutSource"; +import StaticFeatureSource from "../Logic/FeatureSource/Sources/StaticFeatureSource"; +import FeaturePropertiesStore from "../Logic/FeatureSource/Actors/FeaturePropertiesStore"; +import PerLayerFeatureSourceSplitter from "../Logic/FeatureSource/PerLayerFeatureSourceSplitter"; +import FilteringFeatureSource from "../Logic/FeatureSource/Sources/FilteringFeatureSource"; +import ShowDataLayer from "../UI/Map/ShowDataLayer"; +import TitleHandler from "../Logic/Actors/TitleHandler"; +import ChangeToElementsActor from "../Logic/Actors/ChangeToElementsActor"; +import PendingChangesUploader from "../Logic/Actors/PendingChangesUploader"; +import SelectedElementTagsUpdater from "../Logic/Actors/SelectedElementTagsUpdater"; +import { BBox } from "../Logic/BBox"; +import Constants from "./Constants"; +import Hotkeys from "../UI/Base/Hotkeys"; +import Translations from "../UI/i18n/Translations"; +import { GeoIndexedStoreForLayer } from "../Logic/FeatureSource/Actors/GeoIndexedStore"; +import { LastClickFeatureSource } from "../Logic/FeatureSource/Sources/LastClickFeatureSource"; +import { MenuState } from "./MenuState"; +import MetaTagging from "../Logic/MetaTagging"; +import ChangeGeometryApplicator from "../Logic/FeatureSource/Sources/ChangeGeometryApplicator"; import { - FeatureSource, - IndexedFeatureSource, - WritableFeatureSource, -} from "../Logic/FeatureSource/FeatureSource" -import { OsmConnection } from "../Logic/Osm/OsmConnection" -import { ExportableMap, MapProperties } from "./MapProperties" -import LayerState from "../Logic/State/LayerState" -import { Feature, Point, Polygon } from "geojson" -import FullNodeDatabaseSource from "../Logic/FeatureSource/TiledFeatureSource/FullNodeDatabaseSource" -import { Map as MlMap } from "maplibre-gl" -import InitialMapPositioning from "../Logic/Actors/InitialMapPositioning" -import { MapLibreAdaptor } from "../UI/Map/MapLibreAdaptor" -import { GeoLocationState } from "../Logic/State/GeoLocationState" -import FeatureSwitchState from "../Logic/State/FeatureSwitchState" -import { QueryParameters } from "../Logic/Web/QueryParameters" -import UserRelatedState from "../Logic/State/UserRelatedState" -import LayerConfig from "./ThemeConfig/LayerConfig" -import GeoLocationHandler from "../Logic/Actors/GeoLocationHandler" -import { AvailableRasterLayers, RasterLayerPolygon, RasterLayerUtils } from "./RasterLayers" -import LayoutSource from "../Logic/FeatureSource/Sources/LayoutSource" -import StaticFeatureSource from "../Logic/FeatureSource/Sources/StaticFeatureSource" -import FeaturePropertiesStore from "../Logic/FeatureSource/Actors/FeaturePropertiesStore" -import PerLayerFeatureSourceSplitter from "../Logic/FeatureSource/PerLayerFeatureSourceSplitter" -import FilteringFeatureSource from "../Logic/FeatureSource/Sources/FilteringFeatureSource" -import ShowDataLayer from "../UI/Map/ShowDataLayer" -import TitleHandler from "../Logic/Actors/TitleHandler" -import ChangeToElementsActor from "../Logic/Actors/ChangeToElementsActor" -import PendingChangesUploader from "../Logic/Actors/PendingChangesUploader" -import SelectedElementTagsUpdater from "../Logic/Actors/SelectedElementTagsUpdater" -import { BBox } from "../Logic/BBox" -import Constants from "./Constants" -import Hotkeys from "../UI/Base/Hotkeys" -import Translations from "../UI/i18n/Translations" -import { GeoIndexedStoreForLayer } from "../Logic/FeatureSource/Actors/GeoIndexedStore" -import { LastClickFeatureSource } from "../Logic/FeatureSource/Sources/LastClickFeatureSource" -import { MenuState } from "./MenuState" -import MetaTagging from "../Logic/MetaTagging" -import ChangeGeometryApplicator from "../Logic/FeatureSource/Sources/ChangeGeometryApplicator" -import { NewGeometryFromChangesFeatureSource } from "../Logic/FeatureSource/Sources/NewGeometryFromChangesFeatureSource" -import OsmObjectDownloader from "../Logic/Osm/OsmObjectDownloader" -import ShowOverlayRasterLayer from "../UI/Map/ShowOverlayRasterLayer" -import { Utils } from "../Utils" -import { EliCategory } from "./RasterLayerProperties" -import BackgroundLayerResetter from "../Logic/Actors/BackgroundLayerResetter" -import SaveFeatureSourceToLocalStorage from "../Logic/FeatureSource/Actors/SaveFeatureSourceToLocalStorage" -import BBoxFeatureSource from "../Logic/FeatureSource/Sources/TouchesBboxFeatureSource" -import ThemeViewStateHashActor from "../Logic/Web/ThemeViewStateHashActor" -import NoElementsInViewDetector, { - FeatureViewState, -} from "../Logic/Actors/NoElementsInViewDetector" -import FilteredLayer from "./FilteredLayer" -import { PreferredRasterLayerSelector } from "../Logic/Actors/PreferredRasterLayerSelector" -import { ImageUploadManager } from "../Logic/ImageProviders/ImageUploadManager" -import { Imgur } from "../Logic/ImageProviders/Imgur" + NewGeometryFromChangesFeatureSource +} from "../Logic/FeatureSource/Sources/NewGeometryFromChangesFeatureSource"; +import OsmObjectDownloader from "../Logic/Osm/OsmObjectDownloader"; +import ShowOverlayRasterLayer from "../UI/Map/ShowOverlayRasterLayer"; +import { Utils } from "../Utils"; +import { EliCategory } from "./RasterLayerProperties"; +import BackgroundLayerResetter from "../Logic/Actors/BackgroundLayerResetter"; +import SaveFeatureSourceToLocalStorage from "../Logic/FeatureSource/Actors/SaveFeatureSourceToLocalStorage"; +import BBoxFeatureSource from "../Logic/FeatureSource/Sources/TouchesBboxFeatureSource"; +import ThemeViewStateHashActor from "../Logic/Web/ThemeViewStateHashActor"; +import NoElementsInViewDetector, { FeatureViewState } from "../Logic/Actors/NoElementsInViewDetector"; +import FilteredLayer from "./FilteredLayer"; +import { PreferredRasterLayerSelector } from "../Logic/Actors/PreferredRasterLayerSelector"; +import { ImageUploadManager } from "../Logic/ImageProviders/ImageUploadManager"; +import { Imgur } from "../Logic/ImageProviders/Imgur"; /** * @@ -67,559 +63,573 @@ import { Imgur } from "../Logic/ImageProviders/Imgur" * It ties up all the needed elements and starts some actors. */ export default class ThemeViewState implements SpecialVisualizationState { - readonly layout: LayoutConfig - readonly map: UIEventSource - readonly changes: Changes - readonly featureSwitches: FeatureSwitchState - readonly featureSwitchIsTesting: Store - readonly featureSwitchUserbadge: Store + readonly layout: LayoutConfig; + readonly map: UIEventSource; + readonly changes: Changes; + readonly featureSwitches: FeatureSwitchState; + readonly featureSwitchIsTesting: Store; + readonly featureSwitchUserbadge: Store; - readonly featureProperties: FeaturePropertiesStore + readonly featureProperties: FeaturePropertiesStore; - readonly osmConnection: OsmConnection - readonly selectedElement: UIEventSource - readonly selectedElementAndLayer: Store<{ feature: Feature; layer: LayerConfig }> - readonly mapProperties: MapProperties & ExportableMap - readonly osmObjectDownloader: OsmObjectDownloader + readonly osmConnection: OsmConnection; + readonly selectedElement: UIEventSource; + readonly selectedElementAndLayer: Store<{ feature: Feature; layer: LayerConfig }>; + readonly mapProperties: MapProperties & ExportableMap; + readonly osmObjectDownloader: OsmObjectDownloader; - readonly dataIsLoading: Store - /** - * Indicates if there is _some_ data in view, even if it is not shown due to the filters - */ - readonly hasDataInView: Store + readonly dataIsLoading: Store; + /** + * Indicates if there is _some_ data in view, even if it is not shown due to the filters + */ + readonly hasDataInView: Store; - readonly guistate: MenuState - readonly fullNodeDatabase?: FullNodeDatabaseSource + readonly guistate: MenuState; + readonly fullNodeDatabase?: FullNodeDatabaseSource; - readonly historicalUserLocations: WritableFeatureSource> - readonly indexedFeatures: IndexedFeatureSource & LayoutSource - readonly currentView: FeatureSource> - readonly featuresInView: FeatureSource - readonly newFeatures: WritableFeatureSource - readonly layerState: LayerState - readonly perLayer: ReadonlyMap - readonly perLayerFiltered: ReadonlyMap + readonly historicalUserLocations: WritableFeatureSource>; + readonly indexedFeatures: IndexedFeatureSource & LayoutSource; + readonly currentView: FeatureSource>; + readonly featuresInView: FeatureSource; + readonly newFeatures: WritableFeatureSource; + readonly layerState: LayerState; + readonly perLayer: ReadonlyMap; + readonly perLayerFiltered: ReadonlyMap; - readonly availableLayers: Store - readonly selectedLayer: UIEventSource - readonly userRelatedState: UserRelatedState - readonly geolocation: GeoLocationHandler + readonly availableLayers: Store; + readonly selectedLayer: UIEventSource; + readonly userRelatedState: UserRelatedState; + readonly geolocation: GeoLocationHandler; - readonly imageUploadManager: ImageUploadManager + readonly imageUploadManager: ImageUploadManager; - readonly lastClickObject: WritableFeatureSource - readonly overlayLayerStates: ReadonlyMap< - string, - { readonly isDisplayed: UIEventSource } - > - /** - * All 'level'-tags that are available with the current features - */ - readonly floors: Store + readonly addNewPoint: UIEventSource = new UIEventSource(false); - constructor(layout: LayoutConfig) { - Utils.initDomPurify() - this.layout = layout - this.featureSwitches = new FeatureSwitchState(layout) - this.guistate = new MenuState( - this.featureSwitches.featureSwitchWelcomeMessage.data, - layout.id - ) - this.map = new UIEventSource(undefined) - const initial = new InitialMapPositioning(layout) - this.mapProperties = new MapLibreAdaptor(this.map, initial) - const geolocationState = new GeoLocationState() + readonly lastClickObject: LastClickFeatureSource; + readonly overlayLayerStates: ReadonlyMap< + string, + { readonly isDisplayed: UIEventSource } + >; + /** + * All 'level'-tags that are available with the current features + */ + readonly floors: Store; + private readonly newPointDialog: FilteredLayer; - this.featureSwitchIsTesting = this.featureSwitches.featureSwitchIsTesting - this.featureSwitchUserbadge = this.featureSwitches.featureSwitchEnableLogin + constructor(layout: LayoutConfig) { + Utils.initDomPurify(); + this.layout = layout; + this.featureSwitches = new FeatureSwitchState(layout); + this.guistate = new MenuState( + this.featureSwitches.featureSwitchWelcomeMessage.data, + layout.id + ); + this.map = new UIEventSource(undefined); + const initial = new InitialMapPositioning(layout); + this.mapProperties = new MapLibreAdaptor(this.map, initial); + const geolocationState = new GeoLocationState(); - this.osmConnection = new OsmConnection({ - dryRun: this.featureSwitches.featureSwitchIsTesting, - fakeUser: this.featureSwitches.featureSwitchFakeUser.data, - oauth_token: QueryParameters.GetQueryParameter( - "oauth_token", - undefined, - "Used to complete the login" - ), + this.featureSwitchIsTesting = this.featureSwitches.featureSwitchIsTesting; + this.featureSwitchUserbadge = this.featureSwitches.featureSwitchEnableLogin; + + this.osmConnection = new OsmConnection({ + dryRun: this.featureSwitches.featureSwitchIsTesting, + fakeUser: this.featureSwitches.featureSwitchFakeUser.data, + oauth_token: QueryParameters.GetQueryParameter( + "oauth_token", + undefined, + "Used to complete the login" + ) + }); + this.userRelatedState = new UserRelatedState( + this.osmConnection, + layout?.language, + layout, + this.featureSwitches, + this.mapProperties + ); + this.userRelatedState.fixateNorth.addCallbackAndRunD((fixated) => { + this.mapProperties.allowRotating.setData(fixated !== "yes"); + }); + this.selectedElement = new UIEventSource(undefined, "Selected element"); + this.selectedLayer = new UIEventSource(undefined, "Selected layer"); + + this.selectedElementAndLayer = this.selectedElement.mapD( + (feature) => { + const layer = this.selectedLayer.data; + if (!layer) { + return undefined; + } + return { layer, feature }; + }, + [this.selectedLayer] + ); + + this.geolocation = new GeoLocationHandler( + geolocationState, + this.selectedElement, + this.mapProperties, + this.userRelatedState.gpsLocationHistoryRetentionTime + ); + + this.availableLayers = AvailableRasterLayers.layersAvailableAt(this.mapProperties.location); + + const self = this; + this.layerState = new LayerState(this.osmConnection, layout.layers, layout.id); + + { + const overlayLayerStates = new Map }>(); + for (const rasterInfo of this.layout.tileLayerSources) { + const isDisplayed = QueryParameters.GetBooleanQueryParameter( + "overlay-" + rasterInfo.id, + rasterInfo.defaultState ?? true, + "Wether or not overlayer layer " + rasterInfo.id + " is shown" + ); + const state = { isDisplayed }; + overlayLayerStates.set(rasterInfo.id, state); + new ShowOverlayRasterLayer(rasterInfo, this.map, this.mapProperties, state); + } + this.overlayLayerStates = overlayLayerStates; + } + + { + /* Setup the layout source + * A bit tricky, as this is heavily intertwined with the 'changes'-element, which generate a stream of new and changed features too + */ + + if (this.layout.layers.some((l) => l._needsFullNodeDatabase)) { + this.fullNodeDatabase = new FullNodeDatabaseSource(); + } + + const layoutSource = new LayoutSource( + layout.layers, + this.featureSwitches, + this.mapProperties, + this.osmConnection.Backend(), + (id) => self.layerState.filteredLayers.get(id).isDisplayed, + this.fullNodeDatabase + ); + + this.indexedFeatures = layoutSource; + + let currentViewIndex = 0 + const empty = []; + this.currentView = new StaticFeatureSource( + this.mapProperties.bounds.map((bbox) => { + if (!bbox) { + return empty; + } + currentViewIndex++; + return [ + bbox.asGeoJson({ + zoom: this.mapProperties.zoom.data, + ...this.mapProperties.location.data, + id: "current_view_"+currentViewIndex + }) + ]; }) - this.userRelatedState = new UserRelatedState( - this.osmConnection, - layout?.language, - layout, - this.featureSwitches, - this.mapProperties - ) - this.userRelatedState.fixateNorth.addCallbackAndRunD((fixated) => { - this.mapProperties.allowRotating.setData(fixated !== "yes") - }) - this.selectedElement = new UIEventSource(undefined, "Selected element") - this.selectedLayer = new UIEventSource(undefined, "Selected layer") - - this.selectedElementAndLayer = this.selectedElement.mapD( - (feature) => { - const layer = this.selectedLayer.data - if (!layer) { - return undefined - } - return { layer, feature } - }, - [this.selectedLayer] - ) - - this.geolocation = new GeoLocationHandler( - geolocationState, - this.selectedElement, - this.mapProperties, - this.userRelatedState.gpsLocationHistoryRetentionTime - ) - - this.availableLayers = AvailableRasterLayers.layersAvailableAt(this.mapProperties.location) - - const self = this - this.layerState = new LayerState(this.osmConnection, layout.layers, layout.id) + ); + this.featuresInView = new BBoxFeatureSource(layoutSource, this.mapProperties.bounds); + this.dataIsLoading = layoutSource.isLoading; + const indexedElements = this.indexedFeatures; + this.featureProperties = new FeaturePropertiesStore(indexedElements); + this.changes = new Changes( { - const overlayLayerStates = new Map }>() - for (const rasterInfo of this.layout.tileLayerSources) { - const isDisplayed = QueryParameters.GetBooleanQueryParameter( - "overlay-" + rasterInfo.id, - rasterInfo.defaultState ?? true, - "Wether or not overlayer layer " + rasterInfo.id + " is shown" - ) - const state = { isDisplayed } - overlayLayerStates.set(rasterInfo.id, state) - new ShowOverlayRasterLayer(rasterInfo, this.map, this.mapProperties, state) - } - this.overlayLayerStates = overlayLayerStates - } + dryRun: this.featureSwitches.featureSwitchIsTesting, + allElements: indexedElements, + featurePropertiesStore: this.featureProperties, + osmConnection: this.osmConnection, + historicalUserLocations: this.geolocation.historicalUserLocations + }, + layout?.isLeftRightSensitive() ?? false + ); + this.historicalUserLocations = this.geolocation.historicalUserLocations; + this.newFeatures = new NewGeometryFromChangesFeatureSource( + this.changes, + indexedElements, + this.featureProperties + ); + layoutSource.addSource(this.newFeatures); + const perLayer = new PerLayerFeatureSourceSplitter( + Array.from(this.layerState.filteredLayers.values()).filter( + (l) => l.layerDef?.source !== null + ), + new ChangeGeometryApplicator(this.indexedFeatures, this.changes), { - /* Setup the layout source - * A bit tricky, as this is heavily intertwined with the 'changes'-element, which generate a stream of new and changed features too - */ - - if (this.layout.layers.some((l) => l._needsFullNodeDatabase)) { - this.fullNodeDatabase = new FullNodeDatabaseSource() - } - - const layoutSource = new LayoutSource( - layout.layers, - this.featureSwitches, - this.mapProperties, - this.osmConnection.Backend(), - (id) => self.layerState.filteredLayers.get(id).isDisplayed, - this.fullNodeDatabase - ) - - this.indexedFeatures = layoutSource - - const empty = [] - let currentViewIndex = 0 - this.currentView = new StaticFeatureSource( - this.mapProperties.bounds.map((bbox) => { - if (!bbox) { - return empty - } - currentViewIndex++ - return [ - bbox.asGeoJson({ - zoom: this.mapProperties.zoom.data, - ...this.mapProperties.location.data, - id: "current_view", - }), - ] - }) - ) - this.featuresInView = new BBoxFeatureSource(layoutSource, this.mapProperties.bounds) - this.dataIsLoading = layoutSource.isLoading - - const indexedElements = this.indexedFeatures - this.featureProperties = new FeaturePropertiesStore(indexedElements) - this.changes = new Changes( - { - dryRun: this.featureSwitches.featureSwitchIsTesting, - allElements: indexedElements, - featurePropertiesStore: this.featureProperties, - osmConnection: this.osmConnection, - historicalUserLocations: this.geolocation.historicalUserLocations, - }, - layout?.isLeftRightSensitive() ?? false - ) - this.historicalUserLocations = this.geolocation.historicalUserLocations - this.newFeatures = new NewGeometryFromChangesFeatureSource( - this.changes, - indexedElements, - this.featureProperties - ) - layoutSource.addSource(this.newFeatures) - - const perLayer = new PerLayerFeatureSourceSplitter( - Array.from(this.layerState.filteredLayers.values()).filter( - (l) => l.layerDef?.source !== null - ), - new ChangeGeometryApplicator(this.indexedFeatures, this.changes), - { - constructStore: (features, layer) => - new GeoIndexedStoreForLayer(features, layer), - handleLeftovers: (features) => { - console.warn( - "Got ", - features.length, - "leftover features, such as", - features[0].properties - ) - }, - } - ) - this.perLayer = perLayer.perLayer + constructStore: (features, layer) => + new GeoIndexedStoreForLayer(features, layer), + handleLeftovers: (features) => { + console.warn( + "Got ", + features.length, + "leftover features, such as", + features[0].properties + ); + } } - this.perLayer.forEach((fs) => { - new SaveFeatureSourceToLocalStorage( - this.osmConnection.Backend(), - fs.layer.layerDef.id, - 15, - fs, - this.featureProperties, - fs.layer.layerDef.maxAgeOfCache - ) - }) + ); + this.perLayer = perLayer.perLayer; + } + this.perLayer.forEach((fs) => { + new SaveFeatureSourceToLocalStorage( + this.osmConnection.Backend(), + fs.layer.layerDef.id, + 15, + fs, + this.featureProperties, + fs.layer.layerDef.maxAgeOfCache + ); + }); + this.newPointDialog = this.layerState.filteredLayers.get("last_click"); - this.floors = this.featuresInView.features.stabilized(500).map((features) => { - if (!features) { - return [] - } - const floors = new Set() - for (const feature of features) { - let level = feature.properties["_level"] - if (level) { - const levels = level.split(";") - for (const l of levels) { - floors.add(l) - } - } else { - floors.add("0") // '0' is the default and is thus _always_ present - } - } - const sorted = Array.from(floors) - // Sort alphabetically first, to deal with floor "A", "B" and "C" - sorted.sort() - sorted.sort((a, b) => { - // We use the laxer 'parseInt' to deal with floor '1A' - const na = parseInt(a) - const nb = parseInt(b) - if (isNaN(na) || isNaN(nb)) { - return 0 - } - return na - nb - }) - sorted.reverse(/* new list, no side-effects */) - return sorted - }) - - const lastClick = (this.lastClickObject = new LastClickFeatureSource( - this.mapProperties.lastClickLocation, - this.layout - )) - - this.osmObjectDownloader = new OsmObjectDownloader( - this.osmConnection.Backend(), - this.changes - ) - - this.perLayerFiltered = this.showNormalDataOn(this.map) - - this.hasDataInView = new NoElementsInViewDetector(this).hasFeatureInView - this.imageUploadManager = new ImageUploadManager( - layout, - Imgur.singleton, - this.featureProperties, - this.osmConnection, - this.changes - ) - - this.initActors() - this.addLastClick(lastClick) - this.drawSpecialLayers() - this.initHotkeys() - this.miscSetup() - if (!Utils.runningFromConsole) { - console.log("State setup completed", this) + this.floors = this.featuresInView.features.stabilized(500).map((features) => { + if (!features) { + return []; + } + const floors = new Set(); + for (const feature of features) { + let level = feature.properties["_level"]; + if (level) { + const levels = level.split(";"); + for (const l of levels) { + floors.add(l); + } + } else { + floors.add("0"); // '0' is the default and is thus _always_ present } + } + const sorted = Array.from(floors); + // Sort alphabetically first, to deal with floor "A", "B" and "C" + sorted.sort(); + sorted.sort((a, b) => { + // We use the laxer 'parseInt' to deal with floor '1A' + const na = parseInt(a); + const nb = parseInt(b); + if (isNaN(na) || isNaN(nb)) { + return 0; + } + return na - nb; + }); + sorted.reverse(/* new list, no side-effects */); + return sorted; + }); + + const lastClick = (this.lastClickObject = new LastClickFeatureSource( + this.mapProperties.lastClickLocation, + this.layout + )); + + this.osmObjectDownloader = new OsmObjectDownloader( + this.osmConnection.Backend(), + this.changes + ); + + this.perLayerFiltered = this.showNormalDataOn(this.map); + + this.hasDataInView = new NoElementsInViewDetector(this).hasFeatureInView; + this.imageUploadManager = new ImageUploadManager( + layout, + Imgur.singleton, + this.featureProperties, + this.osmConnection, + this.changes + ); + + this.initActors(); + // TODO remove this.addLastClick(lastClick); + this.drawSpecialLayers(); + this.initHotkeys(); + this.miscSetup(); + if (!Utils.runningFromConsole) { + console.log("State setup completed", this); } + } - public showNormalDataOn(map: Store): ReadonlyMap { - const filteringFeatureSource = new Map() - this.perLayer.forEach((fs, layerName) => { - const doShowLayer = this.mapProperties.zoom.map( - (z) => - (fs.layer.isDisplayed?.data ?? true) && z >= (fs.layer.layerDef?.minzoom ?? 0), - [fs.layer.isDisplayed] - ) + public showNormalDataOn(map: Store): ReadonlyMap { + const filteringFeatureSource = new Map(); + this.perLayer.forEach((fs, layerName) => { + const doShowLayer = this.mapProperties.zoom.map( + (z) => + (fs.layer.isDisplayed?.data ?? true) && z >= (fs.layer.layerDef?.minzoom ?? 0), + [fs.layer.isDisplayed] + ); - if (!doShowLayer.data && this.featureSwitches.featureSwitchFilter.data === false) { - /* This layer is hidden and there is no way to enable it (filterview is disabled or this layer doesn't show up in the filter view as the name is not defined) - * - * This means that we don't have to filter it, nor do we have to display it - * - * Note: it is tempting to also permanently disable the layer if it is not visible _and_ the layer name is hidden. - * However, this is _not_ correct: the layer might be hidden because zoom is not enough. Zooming in more _will_ reveal the layer! - * */ - return - } - const filtered = new FilteringFeatureSource( - fs.layer, - fs, - (id) => this.featureProperties.getStore(id), - this.layerState.globalFilters - ) - filteringFeatureSource.set(layerName, filtered) + if (!doShowLayer.data && this.featureSwitches.featureSwitchFilter.data === false) { + /* This layer is hidden and there is no way to enable it (filterview is disabled or this layer doesn't show up in the filter view as the name is not defined) + * + * This means that we don't have to filter it, nor do we have to display it + * + * Note: it is tempting to also permanently disable the layer if it is not visible _and_ the layer name is hidden. + * However, this is _not_ correct: the layer might be hidden because zoom is not enough. Zooming in more _will_ reveal the layer! + * */ + return; + } + const filtered = new FilteringFeatureSource( + fs.layer, + fs, + (id) => this.featureProperties.getStore(id), + this.layerState.globalFilters + ); + filteringFeatureSource.set(layerName, filtered); - new ShowDataLayer(map, { - layer: fs.layer.layerDef, - features: filtered, - doShowLayer, - selectedElement: this.selectedElement, - selectedLayer: this.selectedLayer, - fetchStore: (id) => this.featureProperties.getStore(id), - }) - }) - return filteringFeatureSource + new ShowDataLayer(map, { + layer: fs.layer.layerDef, + features: filtered, + doShowLayer, + selectedElement: this.selectedElement, + selectedLayer: this.selectedLayer, + fetchStore: (id) => this.featureProperties.getStore(id) + }); + }); + return filteringFeatureSource; + } + + /** + * Various small methods that need to be called + */ + private miscSetup() { + this.userRelatedState.markLayoutAsVisited(this.layout); + + this.selectedElement.addCallbackAndRunD((feature) => { + // As soon as we have a selected element, we clear the selected element + // This is to work around maplibre, which'll _first_ register the click on the map and only _then_ on the feature + // The only exception is if the last element is the 'add_new'-button, as we don't want it to disappear + if (feature.properties.id === "last_click") { + return; + } + this.lastClickObject.features.setData([]); + }); + + if (this.layout.customCss !== undefined && window.location.pathname.indexOf("theme") >= 0) { + Utils.LoadCustomCss(this.layout.customCss); } + } + private initHotkeys() { + Hotkeys.RegisterHotkey( + { nomod: "Escape", onUp: true }, + Translations.t.hotkeyDocumentation.closeSidebar, + () => { + this.selectedElement.setData(undefined); + this.guistate.closeAll(); + } + ); + + Hotkeys.RegisterHotkey( + { + nomod: "b" + }, + Translations.t.hotkeyDocumentation.openLayersPanel, + () => { + if (this.featureSwitches.featureSwitchFilter.data) { + this.guistate.openFilterView(); + } + } + ); + + Hotkeys.RegisterHotkey( + { shift: "O" }, + Translations.t.hotkeyDocumentation.selectMapnik, + () => { + this.mapProperties.rasterLayer.setData(AvailableRasterLayers.osmCarto); + } + ); + const setLayerCategory = (category: EliCategory) => { + const available = this.availableLayers.data; + const current = this.mapProperties.rasterLayer; + const best = RasterLayerUtils.SelectBestLayerAccordingTo( + available, + category, + current.data + ); + console.log("Best layer for category", category, "is", best.properties.id); + current.setData(best); + }; + + Hotkeys.RegisterHotkey( + { nomod: "O" }, + Translations.t.hotkeyDocumentation.selectOsmbasedmap, + () => setLayerCategory("osmbasedmap") + ); + + Hotkeys.RegisterHotkey({ nomod: "M" }, Translations.t.hotkeyDocumentation.selectMap, () => + setLayerCategory("map") + ); + + Hotkeys.RegisterHotkey( + { nomod: "P" }, + Translations.t.hotkeyDocumentation.selectAerial, + () => setLayerCategory("photo") + ); + } + + private addLastClick(last_click: LastClickFeatureSource) { + // The last_click gets a _very_ special treatment as it interacts with various parts + + this.featureProperties.trackFeatureSource(last_click); + this.indexedFeatures.addSource(last_click); + + last_click.features.addCallbackAndRunD((features) => { + if (this.selectedLayer.data?.id === "last_click") { + // The last-click location moved, but we have selected the last click of the previous location + // So, we update _after_ clearing the selection to make sure no stray data is sticking around + this.selectedElement.setData(undefined); + this.selectedElement.setData(features[0]); + } + }); + + new ShowDataLayer(this.map, { + features: new FilteringFeatureSource(this.newPointDialog, last_click), + doShowLayer: this.featureSwitches.featureSwitchEnableLogin, + layer: this.newPointDialog.layerDef, + selectedElement: this.selectedElement, + selectedLayer: this.selectedLayer, + onClick: (feature: Feature) => { + if (this.mapProperties.zoom.data < Constants.minZoomLevelToAddNewPoint) { + this.map.data.flyTo({ + zoom: Constants.minZoomLevelToAddNewPoint, + center: this.mapProperties.lastClickLocation.data + }); + return; + } + // We first clear the selection to make sure no weird state is around + this.selectedLayer.setData(undefined); + this.selectedElement.setData(undefined); + + this.selectedElement.setData(feature); + this.selectedLayer.setData(this.newPointDialog.layerDef); + } + }); + } + + public openNewDialog() { + this.selectedLayer.setData(undefined); + this.selectedElement.setData(undefined); + + const { lon, lat } = this.mapProperties.location.data; + const feature = this.lastClickObject.createFeature(lon, lat) + this.featureProperties.trackFeature(feature) + this.selectedElement.setData(feature); + this.selectedLayer.setData(this.newPointDialog.layerDef); + } + + /** + * Add the special layers to the map + */ + private drawSpecialLayers() { + type AddedByDefaultTypes = (typeof Constants.added_by_default)[number] + const empty = []; /** - * Various small methods that need to be called + * A listing which maps the layerId onto the featureSource */ - private miscSetup() { - this.userRelatedState.markLayoutAsVisited(this.layout) - - this.selectedElement.addCallbackAndRunD((feature) => { - // As soon as we have a selected element, we clear the selected element - // This is to work around maplibre, which'll _first_ register the click on the map and only _then_ on the feature - // The only exception is if the last element is the 'add_new'-button, as we don't want it to disappear - if (feature.properties.id === "last_click") { - return - } - this.lastClickObject.features.setData([]) - }) - - if (this.layout.customCss !== undefined && window.location.pathname.indexOf("theme") >= 0) { - Utils.LoadCustomCss(this.layout.customCss) - } + const specialLayers: Record< + Exclude | "current_view", + FeatureSource + > = { + home_location: this.userRelatedState.homeLocation, + gps_location: this.geolocation.currentUserLocation, + gps_location_history: this.geolocation.historicalUserLocations, + gps_track: this.geolocation.historicalUserLocationsTrack, + selected_element: new StaticFeatureSource( + this.selectedElement.map((f) => (f === undefined ? empty : [f])) + ), + range: new StaticFeatureSource( + this.mapProperties.maxbounds.map((bbox) => + bbox === undefined ? empty : [bbox.asGeoJson({ id: "range" })] + ) + ), + current_view: this.currentView + }; + if (this.layout?.lockLocation) { + const bbox = new BBox(this.layout.lockLocation); + this.mapProperties.maxbounds.setData(bbox); + ShowDataLayer.showRange( + this.map, + new StaticFeatureSource([bbox.asGeoJson({ id: "range" })]), + this.featureSwitches.featureSwitchIsTesting + ); + } + const currentViewLayer = this.layout.layers.find((l) => l.id === "current_view"); + if (currentViewLayer?.tagRenderings?.length > 0) { + const params = MetaTagging.createExtraFuncParams(this); + this.featureProperties.trackFeatureSource(specialLayers.current_view); + specialLayers.current_view.features.addCallbackAndRunD((features) => { + MetaTagging.addMetatags( + features, + params, + currentViewLayer, + this.layout, + this.osmObjectDownloader, + this.featureProperties + ); + }); } - private initHotkeys() { - Hotkeys.RegisterHotkey( - { nomod: "Escape", onUp: true }, - Translations.t.hotkeyDocumentation.closeSidebar, - () => { - this.selectedElement.setData(undefined) - this.guistate.closeAll() - } - ) + const rangeFLayer: FilteredLayer = this.layerState.filteredLayers.get("range"); - Hotkeys.RegisterHotkey( - { - nomod: "b", - }, - Translations.t.hotkeyDocumentation.openLayersPanel, - () => { - if (this.featureSwitches.featureSwitchFilter.data) { - this.guistate.openFilterView() - } - } - ) + const rangeIsDisplayed = rangeFLayer?.isDisplayed; - Hotkeys.RegisterHotkey( - { shift: "O" }, - Translations.t.hotkeyDocumentation.selectMapnik, - () => { - this.mapProperties.rasterLayer.setData(AvailableRasterLayers.osmCarto) - } - ) - const setLayerCategory = (category: EliCategory) => { - const available = this.availableLayers.data - const current = this.mapProperties.rasterLayer - const best = RasterLayerUtils.SelectBestLayerAccordingTo( - available, - category, - current.data - ) - console.log("Best layer for category", category, "is", best.properties.id) - current.setData(best) - } - - Hotkeys.RegisterHotkey( - { nomod: "O" }, - Translations.t.hotkeyDocumentation.selectOsmbasedmap, - () => setLayerCategory("osmbasedmap") - ) - - Hotkeys.RegisterHotkey({ nomod: "M" }, Translations.t.hotkeyDocumentation.selectMap, () => - setLayerCategory("map") - ) - - Hotkeys.RegisterHotkey( - { nomod: "P" }, - Translations.t.hotkeyDocumentation.selectAerial, - () => setLayerCategory("photo") - ) + if ( + !QueryParameters.wasInitialized(FilteredLayer.queryParameterKey(rangeFLayer.layerDef)) + ) { + rangeIsDisplayed?.syncWith(this.featureSwitches.featureSwitchIsTesting, true); } - private addLastClick(last_click: LastClickFeatureSource) { - // The last_click gets a _very_ special treatment as it interacts with various parts + this.layerState.filteredLayers.forEach((flayer) => { + const id = flayer.layerDef.id; + const features: FeatureSource = specialLayers[id]; + if (features === undefined) { + return; + } - const last_click_layer = this.layerState.filteredLayers.get("last_click") - this.featureProperties.trackFeatureSource(last_click) - this.indexedFeatures.addSource(last_click) + this.featureProperties.trackFeatureSource(features); + new ShowDataLayer(this.map, { + features, + doShowLayer: flayer.isDisplayed, + layer: flayer.layerDef, + selectedElement: this.selectedElement, + selectedLayer: this.selectedLayer + }); + }); + } - last_click.features.addCallbackAndRunD((features) => { - if (this.selectedLayer.data?.id === "last_click") { - // The last-click location moved, but we have selected the last click of the previous location - // So, we update _after_ clearing the selection to make sure no stray data is sticking around - this.selectedElement.setData(undefined) - this.selectedElement.setData(features[0]) - } - }) + /** + * Setup various services for which no reference are needed + */ + private initActors() { + // Unselect the selected element if it is panned out of view + this.mapProperties.bounds.stabilized(250).addCallbackD((bounds) => { + const selected = this.selectedElement.data; + if (selected === undefined) { + return; + } + const bbox = BBox.get(selected); + if (!bbox.overlapsWith(bounds)) { + this.selectedElement.setData(undefined); + } + }); - new ShowDataLayer(this.map, { - features: new FilteringFeatureSource(last_click_layer, last_click), - doShowLayer: this.featureSwitches.featureSwitchEnableLogin, - layer: last_click_layer.layerDef, - selectedElement: this.selectedElement, - selectedLayer: this.selectedLayer, - onClick: (feature: Feature) => { - if (this.mapProperties.zoom.data < Constants.minZoomLevelToAddNewPoint) { - this.map.data.flyTo({ - zoom: Constants.minZoomLevelToAddNewPoint, - center: this.mapProperties.lastClickLocation.data, - }) - return - } - // We first clear the selection to make sure no weird state is around - this.selectedLayer.setData(undefined) - this.selectedElement.setData(undefined) - - this.selectedElement.setData(feature) - this.selectedLayer.setData(last_click_layer.layerDef) - }, - }) - } - - /** - * Add the special layers to the map - */ - private drawSpecialLayers() { - type AddedByDefaultTypes = (typeof Constants.added_by_default)[number] - const empty = [] - /** - * A listing which maps the layerId onto the featureSource - */ - const specialLayers: Record< - Exclude | "current_view", - FeatureSource - > = { - home_location: this.userRelatedState.homeLocation, - gps_location: this.geolocation.currentUserLocation, - gps_location_history: this.geolocation.historicalUserLocations, - gps_track: this.geolocation.historicalUserLocationsTrack, - selected_element: new StaticFeatureSource( - this.selectedElement.map((f) => (f === undefined ? empty : [f])) - ), - range: new StaticFeatureSource( - this.mapProperties.maxbounds.map((bbox) => - bbox === undefined ? empty : [bbox.asGeoJson({ id: "range" })] - ) - ), - current_view: this.currentView, - } - if (this.layout?.lockLocation) { - const bbox = new BBox(this.layout.lockLocation) - this.mapProperties.maxbounds.setData(bbox) - ShowDataLayer.showRange( - this.map, - new StaticFeatureSource([bbox.asGeoJson({ id: "range" })]), - this.featureSwitches.featureSwitchIsTesting - ) - } - const currentViewLayer = this.layout.layers.find((l) => l.id === "current_view") - if (currentViewLayer?.tagRenderings?.length > 0) { - const params = MetaTagging.createExtraFuncParams(this) - this.featureProperties.trackFeatureSource(specialLayers.current_view) - specialLayers.current_view.features.addCallbackAndRunD((features) => { - MetaTagging.addMetatags( - features, - params, - currentViewLayer, - this.layout, - this.osmObjectDownloader, - this.featureProperties - ) - }) - } - - const rangeFLayer: FilteredLayer = this.layerState.filteredLayers.get("range") - - const rangeIsDisplayed = rangeFLayer?.isDisplayed - - if ( - !QueryParameters.wasInitialized(FilteredLayer.queryParameterKey(rangeFLayer.layerDef)) - ) { - rangeIsDisplayed?.syncWith(this.featureSwitches.featureSwitchIsTesting, true) - } - - this.layerState.filteredLayers.forEach((flayer) => { - const id = flayer.layerDef.id - const features: FeatureSource = specialLayers[id] - if (features === undefined) { - return - } - - this.featureProperties.trackFeatureSource(features) - new ShowDataLayer(this.map, { - features, - doShowLayer: flayer.isDisplayed, - layer: flayer.layerDef, - selectedElement: this.selectedElement, - selectedLayer: this.selectedLayer, - }) - }) - } - - /** - * Setup various services for which no reference are needed - */ - private initActors() { - // Unselect the selected element if it is panned out of view - this.mapProperties.bounds.stabilized(250).addCallbackD((bounds) => { - const selected = this.selectedElement.data - if (selected === undefined) { - return - } - const bbox = BBox.get(selected) - if (!bbox.overlapsWith(bounds)) { - this.selectedElement.setData(undefined) - } - }) - - this.selectedElement.addCallback((selected) => { - if (selected === undefined) { - // We did _unselect_ an item - we always remove the lastclick-object - this.lastClickObject.features.setData([]) - this.selectedLayer.setData(undefined) - } - }) - new ThemeViewStateHashActor(this) - new MetaTagging(this) - new TitleHandler(this.selectedElement, this.selectedLayer, this.featureProperties, this) - new ChangeToElementsActor(this.changes, this.featureProperties) - new PendingChangesUploader(this.changes, this.selectedElement) - new SelectedElementTagsUpdater(this) - new BackgroundLayerResetter(this.mapProperties.rasterLayer, this.availableLayers) - new PreferredRasterLayerSelector( - this.mapProperties.rasterLayer, - this.availableLayers, - this.featureSwitches.backgroundLayerId, - this.userRelatedState.preferredBackgroundLayer - ) - } + this.selectedElement.addCallback((selected) => { + if (selected === undefined) { + // We did _unselect_ an item - we always remove the lastclick-object + this.lastClickObject.features.setData([]); + this.selectedLayer.setData(undefined); + } + }); + new ThemeViewStateHashActor(this); + new MetaTagging(this); + new TitleHandler(this.selectedElement, this.selectedLayer, this.featureProperties, this); + new ChangeToElementsActor(this.changes, this.featureProperties); + new PendingChangesUploader(this.changes, this.selectedElement); + new SelectedElementTagsUpdater(this); + new BackgroundLayerResetter(this.mapProperties.rasterLayer, this.availableLayers); + new PreferredRasterLayerSelector( + this.mapProperties.rasterLayer, + this.availableLayers, + this.featureSwitches.backgroundLayerId, + this.userRelatedState.preferredBackgroundLayer + ); + } } diff --git a/src/UI/ThemeViewGUI.svelte b/src/UI/ThemeViewGUI.svelte index dff70dc7e7..43f86cda0b 100644 --- a/src/UI/ThemeViewGUI.svelte +++ b/src/UI/ThemeViewGUI.svelte @@ -1,115 +1,114 @@
@@ -169,22 +168,30 @@
-
- - state.guistate.openFilterView()}> - - +
@@ -319,7 +326,7 @@ new CopyrightPanel(state)} slot="content3" /> -
+
@@ -347,7 +354,9 @@ - + { + selectedElement.setData(undefined) + }}>
From 5be24dbef1647cd2c6c869d86ea5b37f0e072106 Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Mon, 9 Oct 2023 01:53:27 +0200 Subject: [PATCH 11/30] Fix: drag & drop for file selector --- src/UI/Base/FileSelector.svelte | 44 +++++++++++++++++---------------- 1 file changed, 23 insertions(+), 21 deletions(-) diff --git a/src/UI/Base/FileSelector.svelte b/src/UI/Base/FileSelector.svelte index 3a63f86539..9bd5b3f8e6 100644 --- a/src/UI/Base/FileSelector.svelte +++ b/src/UI/Base/FileSelector.svelte @@ -12,7 +12,28 @@ let id = Math.random() * 1000000000 + "" -
+ { + drawAttention = false + dispatcher("submit", inputElement.files) + }} + on:dragend={() => { + console.log("Drag end") + drawAttention = false + }} + on:dragenter|preventDefault|stopPropagation={(e) => { + console.log("Dragging enter") + drawAttention = true + e.dataTransfer.drop = "copy" + }} + on:dragstart={() => { + console.log("DragStart") + drawAttention = false + }} + on:drop|preventDefault|stopPropagation={(e) => { + console.log("Got a 'drop'") + drawAttention = false + dispatcher("submit", e.dataTransfer.files) + }}> @@ -23,26 +44,7 @@ id={"fileinput" + id} {multiple} name="file-input" - on:change|preventDefault={() => { - drawAttention = false - dispatcher("submit", inputElement.files) - }} - on:dragend={() => { - drawAttention = false - }} - on:dragover|preventDefault|stopPropagation={(e) => { - console.log("Dragging over!") - drawAttention = true - e.dataTransfer.drop = "copy" - }} - on:dragstart={() => { - drawAttention = false - }} - on:drop|preventDefault|stopPropagation={(e) => { - console.log("Got a 'drop'") - drawAttention = false - dispatcher("submit", e.dataTransfer.files) - }} + type="file" />
From d43f8c00800e38ea5c0cca8484ac7586e67cf368 Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Mon, 9 Oct 2023 02:07:25 +0200 Subject: [PATCH 12/30] Feature: close floatover if pressed outside of it, fix #1647 --- src/Logic/Web/ThemeViewStateHashActor.ts | 1 - src/UI/Base/FloatOver.svelte | 3 ++- src/UI/ThemeViewGUI.svelte | 8 +++----- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/Logic/Web/ThemeViewStateHashActor.ts b/src/Logic/Web/ThemeViewStateHashActor.ts index 93a2326e9f..8527d8d796 100644 --- a/src/Logic/Web/ThemeViewStateHashActor.ts +++ b/src/Logic/Web/ThemeViewStateHashActor.ts @@ -175,7 +175,6 @@ export default class ThemeViewStateHashActor { } private back() { - console.trace("Got a back event") const state = this._state // history.pushState(null, null, window.location.pathname); if (state.selectedElement.data) { diff --git a/src/UI/Base/FloatOver.svelte b/src/UI/Base/FloatOver.svelte index 701b19b397..b86baf78d3 100644 --- a/src/UI/Base/FloatOver.svelte +++ b/src/UI/Base/FloatOver.svelte @@ -11,8 +11,9 @@
{dispatch("close")}} > -
+
{}}>
diff --git a/src/UI/ThemeViewGUI.svelte b/src/UI/ThemeViewGUI.svelte index 43f86cda0b..5bbbed5a3c 100644 --- a/src/UI/ThemeViewGUI.svelte +++ b/src/UI/ThemeViewGUI.svelte @@ -267,7 +267,7 @@ - + state.guistate.themeIsOpened.setData(false)}>
@@ -339,7 +339,7 @@ - state.guistate.backgroundLayerSelectionIsOpened.setData(false)}> + {state.guistate.backgroundLayerSelectionIsOpened.setData(false)}}>
- { - selectedElement.setData(undefined) - }}> + state.guistate.menuIsOpened.setData(false) }>
From bde5878fedae0eac20db8bb4c6eca81f904e20fd Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Mon, 9 Oct 2023 02:52:22 +0200 Subject: [PATCH 13/30] Fix: correct minzoom on all themes --- .../layers/ticket_machine/ticket_machine.json | 2 +- .../ticket_validator/ticket_validator.json | 2 +- assets/themes/bag/bag.json | 10 +++---- assets/themes/blind_osm/blind_osm.json | 4 +-- assets/themes/climbing/climbing.json | 5 +++- assets/themes/healthcare/healthcare.json | 4 +-- assets/themes/onwheels/onwheels.json | 16 ++++++------ assets/themes/pets/pets.json | 6 ++--- assets/themes/stations/stations.json | 26 +++++++++---------- .../street_lighting/street_lighting.json | 4 +-- assets/themes/transit/transit.json | 11 +++----- 11 files changed, 45 insertions(+), 45 deletions(-) diff --git a/assets/layers/ticket_machine/ticket_machine.json b/assets/layers/ticket_machine/ticket_machine.json index 6292fc6f92..8f60ae2345 100644 --- a/assets/layers/ticket_machine/ticket_machine.json +++ b/assets/layers/ticket_machine/ticket_machine.json @@ -20,7 +20,7 @@ ] } }, - "minzoom": 19, + "minzoom": 18, "title": { "render": { "en": "Ticket Machine", diff --git a/assets/layers/ticket_validator/ticket_validator.json b/assets/layers/ticket_validator/ticket_validator.json index b0b93552a5..16520ec3a1 100644 --- a/assets/layers/ticket_validator/ticket_validator.json +++ b/assets/layers/ticket_validator/ticket_validator.json @@ -13,7 +13,7 @@ "source": { "osmTags": "amenity=ticket_validator" }, - "minzoom": 19, + "minzoom": 18, "title": { "render": { "en": "Ticket Validator", diff --git a/assets/themes/bag/bag.json b/assets/themes/bag/bag.json index 8f33320cbf..204226ed31 100644 --- a/assets/themes/bag/bag.json +++ b/assets/themes/bag/bag.json @@ -47,7 +47,7 @@ "osmTags": "building~*", "maxCacheAge": 0 }, - "minzoom": 19, + "minzoom": 18, "calculatedTags": [ "_surface:strict:=feat(get)('_surface')" ], @@ -154,7 +154,7 @@ }, "maxCacheAge": 0 }, - "minzoom": 19, + "minzoom": 18, "mapRendering": [ { "label": { @@ -194,7 +194,7 @@ "osmTags": "identificatie~*", "maxCacheAge": 0 }, - "minzoom": 19, + "minzoom": 18, "calculatedTags": [ "_overlaps_with_buildings=overlapWith(feat)('osm:buildings').filter(f => f.feat.properties.id.indexOf('-') < 0)", "_overlaps_with=feat(get)('_overlaps_with_buildings').find(f => f.overlap > 1 /* square meter */ )", @@ -379,7 +379,7 @@ "osmTags": "identificatie~*", "maxCacheAge": 0 }, - "minzoom": 19, + "minzoom": 18, "calculatedTags": [ "_closed_osm_addr:=closest(feat)('osm:adresses').properties", "_bag_obj:addr:housenumber=`${feat.properties.huisnummer}${feat.properties.huisletter}${(feat.properties.toevoeging != '') ? '-' : ''}${feat.properties.toevoeging}`", @@ -427,4 +427,4 @@ } ], "hideFromOverview": true -} \ No newline at end of file +} diff --git a/assets/themes/blind_osm/blind_osm.json b/assets/themes/blind_osm/blind_osm.json index 9532bd0cf2..29acc68352 100644 --- a/assets/themes/blind_osm/blind_osm.json +++ b/assets/themes/blind_osm/blind_osm.json @@ -68,7 +68,7 @@ { "builtin": "kerbs", "override": { - "minzoom": 19, + "minzoom": 18, "mapRendering": [ { "iconBadges": [ @@ -112,4 +112,4 @@ }, "stairs" ] -} \ No newline at end of file +} diff --git a/assets/themes/climbing/climbing.json b/assets/themes/climbing/climbing.json index 90f4dc383b..f34d3e69c4 100644 --- a/assets/themes/climbing/climbing.json +++ b/assets/themes/climbing/climbing.json @@ -468,7 +468,10 @@ "guidepost" ], "override": { - "minzoom": 15 + "minzoom": 15, + "mapRendering": [{ + "iconSize": "30,30" + }] } } ], diff --git a/assets/themes/healthcare/healthcare.json b/assets/themes/healthcare/healthcare.json index 488a88fe3c..bcc2c2d96b 100644 --- a/assets/themes/healthcare/healthcare.json +++ b/assets/themes/healthcare/healthcare.json @@ -111,8 +111,8 @@ "=presets": [], "=name": null, "override": { - "minzoom": 19 + "minzoom": 18 } } ] -} \ No newline at end of file +} diff --git a/assets/themes/onwheels/onwheels.json b/assets/themes/onwheels/onwheels.json index 38544e0b7b..047ade494c 100644 --- a/assets/themes/onwheels/onwheels.json +++ b/assets/themes/onwheels/onwheels.json @@ -32,7 +32,7 @@ { "builtin": "indoors", "override": { - "minzoom": 19, + "minzoom": 18, "name": null, "passAllFeatures": true } @@ -43,7 +43,7 @@ "name": null, "tagRendering": null, "title": "null", - "minzoom": 19, + "minzoom": 18, "shownByDefault": false } }, @@ -71,7 +71,7 @@ { "builtin": "entrance", "override": { - "minzoom": 19, + "minzoom": 18, "mapRendering": [ { "icon": "circle:white;./assets/themes/onwheels/entrance.svg" @@ -131,7 +131,7 @@ { "builtin": "kerbs", "override": { - "minzoom": 19, + "minzoom": 18, "syncSelection": "theme-only", "mapRendering": [ { @@ -289,7 +289,7 @@ { "builtin": "toilet", "override": { - "minzoom": 19, + "minzoom": 18, "syncSelection": "theme-only", "mapRendering": [ { @@ -349,7 +349,7 @@ { "builtin": "reception_desk", "override": { - "minzoom": 19, + "minzoom": 18, "syncSelection": "theme-only" } }, @@ -357,7 +357,7 @@ { "builtin": "elevator", "override": { - "minzoom": 19, + "minzoom": 18, "syncSelection": "theme-only", "mapRendering": [ { @@ -524,4 +524,4 @@ ] }, "enableDownload": true -} \ No newline at end of file +} diff --git a/assets/themes/pets/pets.json b/assets/themes/pets/pets.json index 650095516c..d9eaa8963d 100644 --- a/assets/themes/pets/pets.json +++ b/assets/themes/pets/pets.json @@ -165,7 +165,7 @@ { "builtin": "food", "override": { - "minzoom": 19, + "minzoom": 18, "filter": null, "name": null } @@ -181,7 +181,7 @@ { "builtin": "shops", "override": { - "minzoom": 19, + "minzoom": 18, "filter": null, "presets": [ { @@ -220,4 +220,4 @@ } ], "credits": "Niels Elgaard Larsen" -} \ No newline at end of file +} diff --git a/assets/themes/stations/stations.json b/assets/themes/stations/stations.json index 8b242d1af4..c358d0a3be 100644 --- a/assets/themes/stations/stations.json +++ b/assets/themes/stations/stations.json @@ -32,7 +32,7 @@ { "builtin": "indoors", "override": { - "minzoom": 19, + "minzoom": 18, "passAllFeatures": true, "mapRendering": [ {}, @@ -50,7 +50,7 @@ { "builtin": "stairs", "override": { - "minzoom": 19 + "minzoom": 18 } }, { @@ -130,7 +130,7 @@ ] }, "presets": null, - "minzoom": 19 + "minzoom": 18 } }, { @@ -143,7 +143,7 @@ ] }, "presets": null, - "minzoom": 19, + "minzoom": 18, "mapRendering": [ { "icon": "circle:white;./assets/themes/stations/bicycle_parking.svg" @@ -161,7 +161,7 @@ ] }, "presets": null, - "minzoom": 19, + "minzoom": 18, "mapRendering": [ { "icon": "circle:white;./assets/themes/stations/rental_bicycle.svg" @@ -179,7 +179,7 @@ ] }, "presets": null, - "minzoom": 19 + "minzoom": 18 } }, { @@ -195,7 +195,7 @@ ] }, "presets": null, - "minzoom": 19, + "minzoom": 18, "mapRendering+": [ { "color": "#00f", @@ -214,7 +214,7 @@ ] }, "presets": null, - "minzoom": 19, + "minzoom": 18, "mapRendering+": [ { "color": "yellow", @@ -235,13 +235,13 @@ "clock" ], "override": { - "minzoom": 19 + "minzoom": 18 } }, { "builtin": "bench", "override": { - "minzoom": 19, + "minzoom": 18, "mapRendering": [ { "icon": "./assets/themes/stations/bench.svg" @@ -252,7 +252,7 @@ { "builtin": "drinking_water", "override": { - "minzoom": 19, + "minzoom": 18, "mapRendering": [ { "icon": "circle:white;./assets/themes/stations/drinking_water.svg" @@ -293,7 +293,7 @@ "zh_Hant": "時刻表" } }, - "minzoom": 19, + "minzoom": 18, "source": { "osmTags": { "and": [ @@ -412,4 +412,4 @@ ] } ] -} \ No newline at end of file +} diff --git a/assets/themes/street_lighting/street_lighting.json b/assets/themes/street_lighting/street_lighting.json index 0ba9c2ab49..7d1c8da7fc 100644 --- a/assets/themes/street_lighting/street_lighting.json +++ b/assets/themes/street_lighting/street_lighting.json @@ -221,7 +221,7 @@ ] } }, - "minzoom": 19, + "minzoom": 18, "title": { "render": { "en": "Street", @@ -361,4 +361,4 @@ } ], "credits": "Robin van der Linde" -} \ No newline at end of file +} diff --git a/assets/themes/transit/transit.json b/assets/themes/transit/transit.json index f2d2af6650..3f98526768 100644 --- a/assets/themes/transit/transit.json +++ b/assets/themes/transit/transit.json @@ -36,22 +36,19 @@ { "builtin": "bike_parking", "override": { - "minzoom": 19, - "minzoomVisible": 19 + "minzoom": 18 } }, { "builtin": "parking", "override": { - "minzoom": 19, - "minzoomVisible": 19 + "minzoom": 18 } }, { "builtin": "shelter", "override": { - "minzoom": 19, - "minzoomVisible": 19, + "minzoom": 18, "source": { "osmTags": { "and": [ @@ -67,4 +64,4 @@ } ], "credits": "Robin van der Linde" -} \ No newline at end of file +} From 66c69602af517c7f37d2cc06e02b4672878ae6dc Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Mon, 9 Oct 2023 02:53:30 +0200 Subject: [PATCH 14/30] UI: align some buttons, fix #1651 --- src/UI/Popup/DeleteFlow/DeleteWizard.svelte | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/UI/Popup/DeleteFlow/DeleteWizard.svelte b/src/UI/Popup/DeleteFlow/DeleteWizard.svelte index 66781eabba..a92e4de38e 100644 --- a/src/UI/Popup/DeleteFlow/DeleteWizard.svelte +++ b/src/UI/Popup/DeleteFlow/DeleteWizard.svelte @@ -92,7 +92,7 @@ {#if currentState === "start"} - Date: Tue, 10 Oct 2023 13:27:56 +0200 Subject: [PATCH 15/30] Themes: add guidepost theme --- assets/themes/guideposts/guideposts.json | 14 ++++++++++++++ src/Models/ThemeConfig/LayoutConfig.ts | 3 +++ 2 files changed, 17 insertions(+) create mode 100644 assets/themes/guideposts/guideposts.json diff --git a/assets/themes/guideposts/guideposts.json b/assets/themes/guideposts/guideposts.json new file mode 100644 index 0000000000..30a857b7be --- /dev/null +++ b/assets/themes/guideposts/guideposts.json @@ -0,0 +1,14 @@ +{ + "id": "guideposts", + "title": { + "en": "Guideposts" + }, + "description": { + "en": "Guideposts (also known as fingerposts or finger posts) are often found along official hiking, cycling, skiing or horseback riding routes to indicate the directions to different destinations. Additionally, they are often named after a region or place and show the altitude.\n\nThe position of a signpost can be used by a hiker/biker/rider/skier as a confirmation of the current position, especially if they use a printed map without a GPS receiver. " + }, + "icon": "./assets/layers/guidepost/guidepost.svg", + "startZoom": 2, + "layers": [ + "guidepost" + ] +} diff --git a/src/Models/ThemeConfig/LayoutConfig.ts b/src/Models/ThemeConfig/LayoutConfig.ts index 9781734dd8..3a88dc569d 100644 --- a/src/Models/ThemeConfig/LayoutConfig.ts +++ b/src/Models/ThemeConfig/LayoutConfig.ts @@ -94,6 +94,9 @@ export default class LayoutConfig implements LayoutInformation { } const context = this.id this.credits = json.credits + if(!json.title){ + throw `The theme ${json.id} does not have a title defined.` + } this.language = json.mustHaveLanguage ?? Object.keys(json.title) this.usedImages = Array.from( new ExtractImages(official, undefined) From 02da68a62e24e02d7e3a9ce21b62445f511f080b Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Wed, 11 Oct 2023 00:35:34 +0200 Subject: [PATCH 16/30] CI: add studio to caddyfile --- scripts/hetzner/config/Caddyfile | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/scripts/hetzner/config/Caddyfile b/scripts/hetzner/config/Caddyfile index 27b328008f..139a801862 100644 --- a/scripts/hetzner/config/Caddyfile +++ b/scripts/hetzner/config/Caddyfile @@ -22,3 +22,9 @@ report.mapcomplete.org { to http://127.0.0.1:2600 } } + +studio.mapcomplete.org { + reverse_proxy { + to http://127.0.0.1:1235 + } +} From 17503d5bfb63c5c03968c96f15c2c5a674a3ede3 Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Wed, 11 Oct 2023 01:41:42 +0200 Subject: [PATCH 17/30] Fix: hide some elements of the UI if they are disabled by a featureSwitch --- .../Sources/LastClickFeatureSource.ts | 50 ++-- src/Logic/State/FeatureSwitchState.ts | 2 +- src/Models/ThemeViewState.ts | 92 +++--- src/UI/Base/TabbedGroup.svelte | 180 ++++++------ src/UI/BigComponents/ThemeIntroPanel.svelte | 183 ++++++------ src/UI/ThemeViewGUI.svelte | 261 +++++++++--------- 6 files changed, 402 insertions(+), 366 deletions(-) diff --git a/src/Logic/FeatureSource/Sources/LastClickFeatureSource.ts b/src/Logic/FeatureSource/Sources/LastClickFeatureSource.ts index b45169c0ad..40705891f0 100644 --- a/src/Logic/FeatureSource/Sources/LastClickFeatureSource.ts +++ b/src/Logic/FeatureSource/Sources/LastClickFeatureSource.ts @@ -12,57 +12,57 @@ import { OsmTags } from "../../../Models/OsmFeature"; * Based on a lon/lat UIEVentSource, will generate the corresponding feature with the correct properties */ export class LastClickFeatureSource implements WritableFeatureSource { - public readonly features: UIEventSource = new UIEventSource([]) - private i: number = 0 - private readonly hasNoteLayer: string - private readonly renderings: string[]; - private readonly hasPresets: string; + public readonly features: UIEventSource = new UIEventSource([]); + public readonly hasNoteLayer: boolean; + public readonly renderings: string[]; + public readonly hasPresets: boolean; + private i: number = 0; constructor(location: Store<{ lon: number; lat: number }>, layout: LayoutConfig) { - this.hasNoteLayer = layout.layers.some((l) => l.id === "note") ? "yes" : "no" - this.hasPresets= layout.layers.some((l) => l.presets?.length > 0) ? "yes" : "no" - const allPresets: BaseUIElement[] = [] + this.hasNoteLayer = layout.layers.some((l) => l.id === "note"); + this.hasPresets = layout.layers.some((l) => l.presets?.length > 0); + const allPresets: BaseUIElement[] = []; for (const layer of layout.layers) for (let i = 0; i < (layer.presets ?? []).length; i++) { - const preset = layer.presets[i] - const tags = new ImmutableStore(TagUtils.KVtoProperties(preset.tags)) + const preset = layer.presets[i]; + const tags = new ImmutableStore(TagUtils.KVtoProperties(preset.tags)); const { html } = layer.mapRendering[0].RenderIcon(tags, false, { noSize: true, - includeBadges: false, - }) - allPresets.push(html) + includeBadges: false + }); + allPresets.push(html); } this.renderings = Utils.Dedup( allPresets.map((uiElem) => Utils.runningFromConsole ? "" : uiElem.ConstructElement().innerHTML ) - ) + ); location.addCallbackAndRunD(({ lon, lat }) => { - this.features.setData([this.createFeature(lon, lat)]) - }) + this.features.setData([this.createFeature(lon, lat)]); + }); } public createFeature(lon: number, lat: number): Feature { const properties: OsmTags = { lastclick: "yes", id: "last_click_" + this.i, - has_note_layer: this.hasNoteLayer , - has_presets:this.hasPresets , + has_note_layer: this.hasNoteLayer ? "yes" : "no", + has_presets: this.hasPresets ? "yes" : "no", renderings: this.renderings.join(""), - number_of_presets: "" +this. renderings.length, - first_preset: this.renderings[0], - } - this. i++ + number_of_presets: "" + this.renderings.length, + first_preset: this.renderings[0] + }; + this.i++; return >{ type: "Feature", properties, geometry: { type: "Point", - coordinates: [lon, lat], - }, - } + coordinates: [lon, lat] + } + }; } } diff --git a/src/Logic/State/FeatureSwitchState.ts b/src/Logic/State/FeatureSwitchState.ts index 3fda13afd6..a12835fbb1 100644 --- a/src/Logic/State/FeatureSwitchState.ts +++ b/src/Logic/State/FeatureSwitchState.ts @@ -99,7 +99,7 @@ export default class FeatureSwitchState extends OsmConnectionFeatureSwitches { ) this.featureSwitchCommunityIndex = FeatureSwitchUtils.initSwitch( "fs-community-index", - true, + this.featureSwitchEnableLogin.data, "Disables/enables the button to get in touch with the community" ) this.featureSwitchExtraLinkEnabled = FeatureSwitchUtils.initSwitch( diff --git a/src/Models/ThemeViewState.ts b/src/Models/ThemeViewState.ts index 6a03a41d9e..f52da26f50 100644 --- a/src/Models/ThemeViewState.ts +++ b/src/Models/ThemeViewState.ts @@ -338,7 +338,6 @@ export default class ThemeViewState implements SpecialVisualizationState { ); this.initActors(); - // TODO remove this.addLastClick(lastClick); this.drawSpecialLayers(); this.initHotkeys(); this.miscSetup(); @@ -417,52 +416,61 @@ export default class ThemeViewState implements SpecialVisualizationState { } ); - Hotkeys.RegisterHotkey( - { - nomod: "b" - }, - Translations.t.hotkeyDocumentation.openLayersPanel, - () => { - if (this.featureSwitches.featureSwitchFilter.data) { - this.guistate.openFilterView(); + this.featureSwitches.featureSwitchBackgroundSelection.addCallbackAndRun(enable => { + if(!enable){ + return } - } - ); + Hotkeys.RegisterHotkey( + { + nomod: "b" + }, + Translations.t.hotkeyDocumentation.openLayersPanel, + () => { + if (this.featureSwitches.featureSwitchFilter.data) { + this.guistate.openFilterView(); + } + } + ); + Hotkeys.RegisterHotkey( + { shift: "O" }, + Translations.t.hotkeyDocumentation.selectMapnik, + () => { + this.mapProperties.rasterLayer.setData(AvailableRasterLayers.osmCarto); + } + ); + const setLayerCategory = (category: EliCategory) => { + const available = this.availableLayers.data; + const current = this.mapProperties.rasterLayer; + const best = RasterLayerUtils.SelectBestLayerAccordingTo( + available, + category, + current.data + ); + console.log("Best layer for category", category, "is", best.properties.id); + current.setData(best); + }; + + Hotkeys.RegisterHotkey( + { nomod: "O" }, + Translations.t.hotkeyDocumentation.selectOsmbasedmap, + () => setLayerCategory("osmbasedmap") + ); + + Hotkeys.RegisterHotkey({ nomod: "M" }, Translations.t.hotkeyDocumentation.selectMap, () => + setLayerCategory("map") + ); + + Hotkeys.RegisterHotkey( + { nomod: "P" }, + Translations.t.hotkeyDocumentation.selectAerial, + () => setLayerCategory("photo") + ); + return true + }) - Hotkeys.RegisterHotkey( - { shift: "O" }, - Translations.t.hotkeyDocumentation.selectMapnik, - () => { - this.mapProperties.rasterLayer.setData(AvailableRasterLayers.osmCarto); - } - ); - const setLayerCategory = (category: EliCategory) => { - const available = this.availableLayers.data; - const current = this.mapProperties.rasterLayer; - const best = RasterLayerUtils.SelectBestLayerAccordingTo( - available, - category, - current.data - ); - console.log("Best layer for category", category, "is", best.properties.id); - current.setData(best); - }; - Hotkeys.RegisterHotkey( - { nomod: "O" }, - Translations.t.hotkeyDocumentation.selectOsmbasedmap, - () => setLayerCategory("osmbasedmap") - ); - Hotkeys.RegisterHotkey({ nomod: "M" }, Translations.t.hotkeyDocumentation.selectMap, () => - setLayerCategory("map") - ); - Hotkeys.RegisterHotkey( - { nomod: "P" }, - Translations.t.hotkeyDocumentation.selectAerial, - () => setLayerCategory("photo") - ); } private addLastClick(last_click: LastClickFeatureSource) { diff --git a/src/UI/Base/TabbedGroup.svelte b/src/UI/Base/TabbedGroup.svelte index 7e33100e70..35d9a865b1 100644 --- a/src/UI/Base/TabbedGroup.svelte +++ b/src/UI/Base/TabbedGroup.svelte @@ -1,20 +1,32 @@
@@ -29,41 +41,31 @@ >
- {#if $$slots.title1} - twJoin("tab", selected && "primary")}> -
- Tab 0 -
-
- {/if} - {#if $$slots.title1} - twJoin("tab", selected && "primary")}> -
- -
-
- {/if} - {#if $$slots.title2} - twJoin("tab", selected && "primary")}> -
- -
-
- {/if} - {#if $$slots.title3} - twJoin("tab", selected && "primary")}> -
- -
-
- {/if} - {#if $$slots.title4} - twJoin("tab", selected && "primary")}> -
- -
-
- {/if} + twJoin("tab", selected && "primary", !$condition0 && "hidden")}> +
+ Tab 0 +
+
+ twJoin("tab", selected && "primary", !$condition1 && "hidden")}> +
+ +
+
+ twJoin("tab", selected && "primary", !$condition2 && "hidden")}> +
+ +
+
+ twJoin("tab", selected && "primary", !$condition3 && "hidden")}> +
+ +
+
+ twJoin("tab", selected && "primary", !$condition4 && "hidden")}> +
+ +
+
@@ -75,16 +77,24 @@ - + +
+ - + +
+ - + +
+ - + +
+
@@ -92,44 +102,44 @@
diff --git a/src/UI/BigComponents/ThemeIntroPanel.svelte b/src/UI/BigComponents/ThemeIntroPanel.svelte index a109862d57..a65c28906b 100644 --- a/src/UI/BigComponents/ThemeIntroPanel.svelte +++ b/src/UI/BigComponents/ThemeIntroPanel.svelte @@ -1,47 +1,48 @@
@@ -62,61 +63,67 @@
-
- {#if $currentGPSLocation !== undefined || $geopermission === "prompt"} - - - {:else if $geopermission === "requested"} - - {:else if $geopermission === "denied"} - - {:else} - - {/if} -
-
- state.guistate.themeIsOpened.setData(false)} - on:searchIsValid={(isValid) => { +
+ + {#if $currentGPSLocation !== undefined || $geopermission === "prompt"} + + + {:else if $geopermission === "requested"} + + {:else if $geopermission === "denied"} + + {:else} + + {/if} + + + + +
+
+ state.guistate.themeIsOpened.setData(false)} + on:searchIsValid={(isValid) => { searchEnabled = isValid }} - perLayer={state.perLayer} - {selectedElement} - {selectedLayer} - {triggerSearch} - /> + perLayer={state.perLayer} + {selectedElement} + {selectedLayer} + {triggerSearch} + /> +
+
- -
+
diff --git a/src/UI/ThemeViewGUI.svelte b/src/UI/ThemeViewGUI.svelte index 5bbbed5a3c..878b4ee16f 100644 --- a/src/UI/ThemeViewGUI.svelte +++ b/src/UI/ThemeViewGUI.svelte @@ -1,114 +1,114 @@
@@ -169,19 +169,28 @@
- - - - + + {#if state.lastClickObject.hasPresets || state.lastClickObject.hasNoteLayer} + + {/if} + +
- state.guistate.openFilterView()}> - - - - + + state.guistate.openFilterView()}> + + + + + + { @@ -267,9 +276,9 @@ - state.guistate.themeIsOpened.setData(false)}> + state.guistate.themeIsOpened.setData(false)}> - +
- - - - + +
@@ -310,6 +317,7 @@ /> {/each}
+
@@ -356,7 +364,8 @@ state.guistate.menuIsOpened.setData(false) }> - +
-
@@ -442,12 +450,15 @@
- - + + + new OpenJosm(state.osmConnection, state.mapProperties.bounds).SetClass("w-full")} - /> - + /> + + +
From 33565ff4c15d50e07692cb93b0476892d5284077 Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Wed, 11 Oct 2023 01:53:39 +0200 Subject: [PATCH 18/30] CI: attempt to fix translation resources --- scripts/build.sh | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/scripts/build.sh b/scripts/build.sh index 6649d61cab..84330572f3 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -48,11 +48,10 @@ fi export NODE_OPTIONS=--max-old-space-size=7000 vite build $SRC_MAPS - - # Copy the layer files, as these might contain assets (e.g. svgs) cp -r assets/layers/ dist/assets/layers/ cp -r assets/themes/ dist/assets/themes/ cp -r assets/svg/ dist/assets/svg/ cp -r langs/layers/ dist/assets/langs/layers/ +ls dist/assets/langs/layers/ export NODE_OPTIONS="" From 3aa49b86979e71c4776562b0b25d3b6e314d9c37 Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Wed, 11 Oct 2023 03:03:14 +0200 Subject: [PATCH 19/30] CI: attempt to fix translation resources --- scripts/build.sh | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/scripts/build.sh b/scripts/build.sh index 84330572f3..741f22c42b 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -8,8 +8,7 @@ rm -rf dist/* rm -rf .cache mkdir dist 2> /dev/null mkdir dist/assets 2> /dev/null -mkdir dist/assets/langs 2> /dev/null -mkdir dist/assets/langs/layers 2> /dev/null + export NODE_OPTIONS="--max-old-space-size=8192" @@ -52,6 +51,8 @@ vite build $SRC_MAPS cp -r assets/layers/ dist/assets/layers/ cp -r assets/themes/ dist/assets/themes/ cp -r assets/svg/ dist/assets/svg/ +mkdir dist/assets/langs 2> /dev/null +mkdir dist/assets/langs/layers 2> /dev/null cp -r langs/layers/ dist/assets/langs/layers/ ls dist/assets/langs/layers/ export NODE_OPTIONS="" From 7362dc210f188ffe6c20eb109661a267876b8885 Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Wed, 11 Oct 2023 03:47:23 +0200 Subject: [PATCH 20/30] CI: attempt to fix build --- scripts/build.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/build.sh b/scripts/build.sh index 741f22c42b..33cc890618 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -51,8 +51,8 @@ vite build $SRC_MAPS cp -r assets/layers/ dist/assets/layers/ cp -r assets/themes/ dist/assets/themes/ cp -r assets/svg/ dist/assets/svg/ -mkdir dist/assets/langs 2> /dev/null -mkdir dist/assets/langs/layers 2> /dev/null +mkdir dist/assets/langs +mkdir dist/assets/langs/layers cp -r langs/layers/ dist/assets/langs/layers/ ls dist/assets/langs/layers/ export NODE_OPTIONS="" From 8f5ba2153a87394e0543bb2dad1d52e5f5ddab4b Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Wed, 11 Oct 2023 04:09:18 +0200 Subject: [PATCH 21/30] CI: attempt to fix build --- scripts/build.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/build.sh b/scripts/build.sh index 33cc890618..b530e79eac 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -53,6 +53,6 @@ cp -r assets/themes/ dist/assets/themes/ cp -r assets/svg/ dist/assets/svg/ mkdir dist/assets/langs mkdir dist/assets/langs/layers -cp -r langs/layers/ dist/assets/langs/layers/ +cp -r langs/layers/ dist/assets/langs/ ls dist/assets/langs/layers/ export NODE_OPTIONS="" From 236643eefad7b99e5756722ebb2a55bce1dc9a54 Mon Sep 17 00:00:00 2001 From: riQQ Date: Wed, 11 Oct 2023 10:59:43 +0200 Subject: [PATCH 22/30] Editorial improvements in CONTRIBUTING docs --- CONTRIBUTING.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d2e224fb6e..05f9499b9f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -6,22 +6,22 @@ Hi! Thanks for checking out how to contribute to MapComplete! There are multiple ways to contribute: - Translating MapComplete to your own language can be done - on [this website](https://hosted.weblate.org/projects/mapcomplete/) + on [the Weblate website](https://hosted.weblate.org/projects/mapcomplete/) - If you encounter a bug, the [issue tracker](https://github.com/pietervdvn/MapComplete/issues) is the place to be - A good start to contribute is to create a single map layer showing features which interest you. Read more about [making your own theme](/Docs/Making_Your_Own_Theme.md). - If you want to improve a theme, create a new theme, spot a typo in the repo... the best way is to open a pull request. -People who stick around and contribute in a meaningful way, _might_ be granted write access to the repository (except . This is +People who stick around and contribute in a meaningful way, _might_ be granted write access to the repository. This is done on a purely subjective basis, e.g. after a few pull requests and if you are a member of the OSM community. Rights of contributors ----------------------- If you have write access to the repository, you can make a fork of an already existing branch and push this new branch -to github. This means that this branch will be _automatically built_ and be **deployed** +to GitHub. This means that this branch will be _automatically built_ and be **deployed** to `https://pietervdvn.github.io/mc/`. You can see the deploy process -on [Github Actions](https://github.com/pietervdvn/MapComplete/actions). Don't worry about pushing too much. These -deploys are free and totally automatic. They might fail if something is wrong, but this will hinder no-one. +on [GitHub Actions](https://github.com/pietervdvn/MapComplete/actions). Don't worry about pushing too much. These +deploys are free and totally automatic. They might fail if something is wrong, but this will hinder no one. Additionaly, some other maintainer might step in and merge the latest develop with your branch, making later pull requests easier. @@ -58,6 +58,6 @@ again to start fresh. What not to contribute ---------------------- -I'm currently _not_ accepting files for integration with some editor. There are hundreds of editors out there, if every single one of them needs a file in the repo, this ends up as a mess. -Furthermore, MapComplete doesn't want to encourage or discourage some editors. +I'm currently _not_ accepting files for integration with some text editor. There are hundreds of editors out there, if every single one of them needs a file in the repo, this ends up as a mess. +Furthermore, MapComplete doesn't want to encourage or discourage some text editors. At last, these files are hard to maintain and are hard to detect if they have fallen out of use. From 89afe9102fe5b04ea0484d06cc48149045f879ff Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Wed, 11 Oct 2023 17:57:16 +0200 Subject: [PATCH 23/30] Update: remove stamen entries, replace them with stadiamaps alternatives; remove CartoDB (as it has PNG tiles), see #1652 --- assets/themes/width/width.json | 4 +- src/Models/RasterLayers.ts | 584 ++++++++++++--------------- src/assets/global-raster-layers.json | 119 +++--- 3 files changed, 325 insertions(+), 382 deletions(-) diff --git a/assets/themes/width/width.json b/assets/themes/width/width.json index 6d0bfb1147..9778da3293 100644 --- a/assets/themes/width/width.json +++ b/assets/themes/width/width.json @@ -32,7 +32,7 @@ 51.190748429411705 ] ], - "defaultBackgroundId": "CartoDB.DarkMatterNoLabels", + "defaultBackgroundId": "alidade.smooth_dark", "layers": [ { "id": "street_with_width", @@ -271,4 +271,4 @@ ] } ] -} \ No newline at end of file +} diff --git a/src/Models/RasterLayers.ts b/src/Models/RasterLayers.ts index 9ef7f9250a..47162b6908 100644 --- a/src/Models/RasterLayers.ts +++ b/src/Models/RasterLayers.ts @@ -1,174 +1,122 @@ -import { Feature, Polygon } from "geojson"; -import * as editorlayerindex from "../assets/editor-layer-index.json"; -import * as globallayers from "../assets/global-raster-layers.json"; -import { BBox } from "../Logic/BBox"; -import { Store, Stores } from "../Logic/UIEventSource"; -import { GeoOperations } from "../Logic/GeoOperations"; -import { RasterLayerProperties } from "./RasterLayerProperties"; +import { Feature, Polygon } from "geojson" +import * as editorlayerindex from "../assets/editor-layer-index.json" +import * as globallayers from "../assets/global-raster-layers.json" +import { BBox } from "../Logic/BBox" +import { Store, Stores } from "../Logic/UIEventSource" +import { GeoOperations } from "../Logic/GeoOperations" +import { RasterLayerProperties } from "./RasterLayerProperties" export class AvailableRasterLayers { - public static EditorLayerIndex: (Feature & - RasterLayerPolygon)[] = editorlayerindex.features; - public static globalLayers: RasterLayerPolygon[] = globallayers.layers.map( - (properties) => - { + public static EditorLayerIndex: (Feature & + RasterLayerPolygon)[] = editorlayerindex.features + public static globalLayers: RasterLayerPolygon[] = globallayers.layers.map( + (properties) => + { + type: "Feature", + properties, + geometry: BBox.global.asGeometry(), + } + ) + public static readonly osmCartoProperties: RasterLayerProperties = { + id: "osm", + name: "OpenStreetMap", + url: "https://tile.openstreetmap.org/{z}/{x}/{y}.png", + attribution: { + text: "OpenStreetMap", + url: "https://openStreetMap.org/copyright", + }, + best: true, + max_zoom: 19, + min_zoom: 0, + category: "osmbasedmap", + } + + public static readonly osmCarto: RasterLayerPolygon = { type: "Feature", - properties, - geometry: BBox.global.asGeometry() - } - ); - public static readonly osmCartoProperties: RasterLayerProperties = { - id: "osm", - name: "OpenStreetMap", - url: "https://tile.openstreetmap.org/{z}/{x}/{y}.png", - attribution: { - text: "OpenStreetMap", - url: "https://openStreetMap.org/copyright" - }, - best: true, - max_zoom: 19, - min_zoom: 0, - category: "osmbasedmap" - }; + properties: AvailableRasterLayers.osmCartoProperties, + geometry: BBox.global.asGeometry(), + } - public static readonly osmCarto: RasterLayerPolygon = { - type: "Feature", - properties: AvailableRasterLayers.osmCartoProperties, - geometry: BBox.global.asGeometry() - }; + public static readonly maptilerDefaultLayer: RasterLayerPolygon = { + type: "Feature", + properties: { + name: "MapTiler", + url: "https://api.maptiler.com/maps/15cc8f61-0353-4be6-b8da-13daea5f7432/style.json?key=GvoVAJgu46I5rZapJuAy", + category: "osmbasedmap", + id: "maptiler", + type: "vector", + attribution: { + text: "Maptiler", + url: "https://www.maptiler.com/copyright/", + }, + }, + geometry: BBox.global.asGeometry(), + } - public static readonly maptilerDefaultLayer: RasterLayerPolygon = { - type: "Feature", - properties: { - name: "MapTiler", - url: "https://api.maptiler.com/maps/15cc8f61-0353-4be6-b8da-13daea5f7432/style.json?key=GvoVAJgu46I5rZapJuAy", - category: "osmbasedmap", - id: "maptiler", - type: "vector", - attribution: { - text: "Maptiler", - url: "https://www.maptiler.com/copyright/" - } - }, - geometry: BBox.global.asGeometry() - }; + public static readonly vectorLayers = [ + AvailableRasterLayers.maptilerDefaultLayer, + AvailableRasterLayers.osmCarto, + ] - public static readonly maptilerCarto: RasterLayerPolygon = { - type: "Feature", - properties: { - name: "MapTiler Carto", - url: "https://api.maptiler.com/maps/openstreetmap/style.json?key=GvoVAJgu46I5rZapJuAy", - category: "osmbasedmap", - id: "maptiler.carto", - type: "vector", - attribution: { - text: "Maptiler", - url: "https://www.maptiler.com/copyright/" - } - }, - geometry: BBox.global.asGeometry() - }; - - public static readonly maptilerBackdrop: RasterLayerPolygon = { - type: "Feature", - properties: { - name: "MapTiler Backdrop", - url: "https://api.maptiler.com/maps/backdrop/style.json?key=GvoVAJgu46I5rZapJuAy", - category: "osmbasedmap", - id: "maptiler.backdrop", - type: "vector", - attribution: { - text: "Maptiler", - url: "https://www.maptiler.com/copyright/" - } - }, - geometry: BBox.global.asGeometry() - }; - public static readonly americana: RasterLayerPolygon = { - type: "Feature", - properties: { - name: "Americana", - url: "https://zelonewolf.github.io/openstreetmap-americana/style.json", - category: "osmbasedmap", - id: "americana", - type: "vector", - attribution: { - text: "Americana", - url: "https://github.com/ZeLonewolf/openstreetmap-americana/" - } - }, - geometry: BBox.global.asGeometry() - }; - - public static readonly vectorLayers = [ - AvailableRasterLayers.maptilerDefaultLayer, - AvailableRasterLayers.osmCarto, - AvailableRasterLayers.maptilerCarto, - AvailableRasterLayers.maptilerBackdrop, - AvailableRasterLayers.americana - ]; - - public static layersAvailableAt( - location: Store<{ lon: number; lat: number }> - ): Store { - const availableLayersBboxes = Stores.ListStabilized( - location.mapD((loc) => { - const lonlat: [number, number] = [loc.lon, loc.lat]; - return AvailableRasterLayers.EditorLayerIndex.filter((eliPolygon) => - BBox.get(eliPolygon).contains(lonlat) - ); - }) - ); - const available = Stores.ListStabilized( - availableLayersBboxes.map((eliPolygons) => { - const loc = location.data; - const lonlat: [number, number] = [loc.lon, loc.lat]; - const matching: RasterLayerPolygon[] = eliPolygons.filter((eliPolygon) => { - if (eliPolygon.geometry === null) { - return true; // global ELI-layer - } - return GeoOperations.inside(lonlat, eliPolygon); - }); - matching.push(...AvailableRasterLayers.globalLayers); - matching.unshift(...AvailableRasterLayers.vectorLayers); - return matching; - }) - ); - return available; - } + public static layersAvailableAt( + location: Store<{ lon: number; lat: number }> + ): Store { + const availableLayersBboxes = Stores.ListStabilized( + location.mapD((loc) => { + const lonlat: [number, number] = [loc.lon, loc.lat] + return AvailableRasterLayers.EditorLayerIndex.filter((eliPolygon) => + BBox.get(eliPolygon).contains(lonlat) + ) + }) + ) + return Stores.ListStabilized( + availableLayersBboxes.map((eliPolygons) => { + const loc = location.data + const lonlat: [number, number] = [loc.lon, loc.lat] + const matching: RasterLayerPolygon[] = eliPolygons.filter((eliPolygon) => { + if (eliPolygon.geometry === null) { + return true // global ELI-layer + } + return GeoOperations.inside(lonlat, eliPolygon) + }) + matching.push(...AvailableRasterLayers.globalLayers) + return matching + }) + ) + } } export class RasterLayerUtils { - /** - * Selects, from the given list of available rasterLayerPolygons, a rasterLayer. - * This rasterlayer will be of type 'preferredCategory' and will be of the 'best'-layer (if available). - * Returns 'undefined' if no such layer is available - * @param available - * @param preferredCategory - * @param ignoreLayer - */ - public static SelectBestLayerAccordingTo( - available: RasterLayerPolygon[], - preferredCategory: string, - ignoreLayer?: RasterLayerPolygon - ): RasterLayerPolygon { - let secondBest: RasterLayerPolygon = undefined; - for (const rasterLayer of available) { - if (rasterLayer === ignoreLayer) { - continue; - } - const p = rasterLayer.properties; - if (p.category === preferredCategory) { - if (p.best) { - return rasterLayer; + /** + * Selects, from the given list of available rasterLayerPolygons, a rasterLayer. + * This rasterlayer will be of type 'preferredCategory' and will be of the 'best'-layer (if available). + * Returns 'undefined' if no such layer is available + * @param available + * @param preferredCategory + * @param ignoreLayer + */ + public static SelectBestLayerAccordingTo( + available: RasterLayerPolygon[], + preferredCategory: string, + ignoreLayer?: RasterLayerPolygon + ): RasterLayerPolygon { + let secondBest: RasterLayerPolygon = undefined + for (const rasterLayer of available) { + if (rasterLayer === ignoreLayer) { + continue + } + const p = rasterLayer.properties + if (p.category === preferredCategory) { + if (p.best) { + return rasterLayer + } + if (!secondBest) { + secondBest = rasterLayer + } + } } - if (!secondBest) { - secondBest = rasterLayer; - } - } + return secondBest } - return secondBest; - } } export type RasterLayerPolygon = Feature @@ -180,165 +128,165 @@ export type RasterLayerPolygon = Feature * which was then converted with http://borischerny.com/json-schema-to-typescript-browser/ */ export interface EditorLayerIndexProperties extends RasterLayerProperties { - /** - * The name of the imagery source - */ - readonly name: string; - /** - * Whether the imagery name should be translated - */ - readonly i18n?: boolean; - readonly type: - | "tms" - | "wms" - | "bing" - | "scanex" - | "wms_endpoint" - | "wmts" - | "vector"; /* Vector is not actually part of the ELI-spec, we add it for vector layers */ - /** - * A rough categorisation of different types of layers. See https://github.com/osmlab/editor-layer-index/blob/gh-pages/CONTRIBUTING.md#categories for a description of the individual categories. - */ - readonly category?: - | "photo" - | "map" - | "historicmap" - | "osmbasedmap" - | "historicphoto" - | "qa" - | "elevation" - | "other"; - /** - * A URL template for imagery tiles - */ - readonly url: string; - readonly min_zoom?: number; - readonly max_zoom?: number; - /** - * explicit/implicit permission by the owner for use in OSM - */ - readonly permission_osm?: "explicit" | "implicit" | "no"; - /** - * A URL for the license or permissions for the imagery - */ - readonly license_url?: string; - /** - * A URL for the privacy policy of the operator or false if there is no existing privacy policy for tis imagery. - */ - readonly privacy_policy_url?: string | boolean; - /** - * A unique identifier for the source; used in imagery_used changeset tag - */ - readonly id: string; - /** - * A short English-language description of the source - */ - readonly description?: string; - /** - * The ISO 3166-1 alpha-2 two letter country code in upper case. Use ZZ for unknown or multiple. - */ - readonly country_code?: string; - /** - * Whether this imagery should be shown in the default world-wide menu - */ - readonly default?: boolean; - /** - * Whether this imagery is the best source for the region - */ - readonly best?: boolean; - /** - * The age of the oldest imagery or data in the source, as an RFC3339 date or leading portion of one - */ - readonly start_date?: string; - /** - * The age of the newest imagery or data in the source, as an RFC3339 date or leading portion of one - */ - readonly end_date?: string; - /** - * HTTP header to check for information if the tile is invalid - */ - readonly no_tile_header?: { /** - * This interface was referenced by `undefined`'s JSON-Schema definition - * via the `patternProperty` "^.*$". + * The name of the imagery source */ - [k: string]: string[] | null - }; - /** - * 'true' if tiles are transparent and can be overlaid on another source - */ - readonly overlay?: boolean & string; - readonly available_projections?: string[]; - readonly attribution?: { - readonly url?: string - readonly text?: string - readonly html?: string - readonly required?: boolean - }; - /** - * A URL for an image, that can be displayed in the list of imagery layers next to the name - */ - readonly icon?: string; - /** - * A link to an EULA text that has to be accepted by the user, before the imagery source is added. Can contain {lang} to be replaced by a current user language wiki code (like FR:) or an empty string for the default English text. - */ - readonly eula?: string; - /** - * A URL for an image, that is displayed in the mapview for attribution - */ - readonly "logo-image"?: string; - /** - * Customized text for the terms of use link (default is "Background Terms of Use") - */ - readonly "terms-of-use-text"?: string; - /** - * Specify a checksum for tiles, which aren't real tiles. `type` is the digest type and can be MD5, SHA-1, SHA-256, SHA-384 and SHA-512, value is the hex encoded checksum in lower case. To create a checksum save the tile as file and upload it to e.g. https://defuse.ca/checksums.htm. - */ - readonly "no-tile-checksum"?: string; - /** - * header-name attribute specifies a header returned by tile server, that will be shown as `metadata-key` attribute in Show Tile Info dialog - */ - readonly "metadata-header"?: string; - /** - * Set to `true` if imagery source is properly aligned and does not need imagery offset adjustments. This is used for OSM based sources too. - */ - readonly "valid-georeference"?: boolean; - /** - * Size of individual tiles delivered by a TMS service - */ - readonly "tile-size"?: number; - /** - * Whether tiles status can be accessed by appending /status to the tile URL and can be submitted for re-rendering by appending /dirty. - */ - readonly "mod-tile-features"?: string; - /** - * HTTP headers to be sent to server. It has two attributes header-name and header-value. May be specified multiple times. - */ - readonly "custom-http-headers"?: { - readonly "header-name"?: string - readonly "header-value"?: string - }; - /** - * Default layer to open (when using WMS_ENDPOINT type). Contains list of layer tag with two attributes - name and style, e.g. `"default-layers": ["layer": { name="Basisdata_NP_Basiskart_JanMayen_WMTS_25829" "style":"default" } ]` (not allowed in `mirror` attribute) - */ - readonly "default-layers"?: { - layer?: { - "layer-name"?: string - "layer-style"?: string - [k: string]: unknown + readonly name: string + /** + * Whether the imagery name should be translated + */ + readonly i18n?: boolean + readonly type: + | "tms" + | "wms" + | "bing" + | "scanex" + | "wms_endpoint" + | "wmts" + | "vector" /* Vector is not actually part of the ELI-spec, we add it for vector layers */ + /** + * A rough categorisation of different types of layers. See https://github.com/osmlab/editor-layer-index/blob/gh-pages/CONTRIBUTING.md#categories for a description of the individual categories. + */ + readonly category?: + | "photo" + | "map" + | "historicmap" + | "osmbasedmap" + | "historicphoto" + | "qa" + | "elevation" + | "other" + /** + * A URL template for imagery tiles + */ + readonly url: string + readonly min_zoom?: number + readonly max_zoom?: number + /** + * explicit/implicit permission by the owner for use in OSM + */ + readonly permission_osm?: "explicit" | "implicit" | "no" + /** + * A URL for the license or permissions for the imagery + */ + readonly license_url?: string + /** + * A URL for the privacy policy of the operator or false if there is no existing privacy policy for tis imagery. + */ + readonly privacy_policy_url?: string | boolean + /** + * A unique identifier for the source; used in imagery_used changeset tag + */ + readonly id: string + /** + * A short English-language description of the source + */ + readonly description?: string + /** + * The ISO 3166-1 alpha-2 two letter country code in upper case. Use ZZ for unknown or multiple. + */ + readonly country_code?: string + /** + * Whether this imagery should be shown in the default world-wide menu + */ + readonly default?: boolean + /** + * Whether this imagery is the best source for the region + */ + readonly best?: boolean + /** + * The age of the oldest imagery or data in the source, as an RFC3339 date or leading portion of one + */ + readonly start_date?: string + /** + * The age of the newest imagery or data in the source, as an RFC3339 date or leading portion of one + */ + readonly end_date?: string + /** + * HTTP header to check for information if the tile is invalid + */ + readonly no_tile_header?: { + /** + * This interface was referenced by `undefined`'s JSON-Schema definition + * via the `patternProperty` "^.*$". + */ + [k: string]: string[] | null } - [k: string]: unknown - }[]; - /** - * format to use when connecting tile server (when using WMS_ENDPOINT type) - */ - readonly format?: string; - /** - * If `true` transparent tiles will be requested from WMS server - */ - readonly transparent?: boolean & string; - /** - * minimum expiry time for tiles in seconds. The larger the value, the longer entry in cache will be considered valid - */ - readonly "minimum-tile-expire"?: number; + /** + * 'true' if tiles are transparent and can be overlaid on another source + */ + readonly overlay?: boolean & string + readonly available_projections?: string[] + readonly attribution?: { + readonly url?: string + readonly text?: string + readonly html?: string + readonly required?: boolean + } + /** + * A URL for an image, that can be displayed in the list of imagery layers next to the name + */ + readonly icon?: string + /** + * A link to an EULA text that has to be accepted by the user, before the imagery source is added. Can contain {lang} to be replaced by a current user language wiki code (like FR:) or an empty string for the default English text. + */ + readonly eula?: string + /** + * A URL for an image, that is displayed in the mapview for attribution + */ + readonly "logo-image"?: string + /** + * Customized text for the terms of use link (default is "Background Terms of Use") + */ + readonly "terms-of-use-text"?: string + /** + * Specify a checksum for tiles, which aren't real tiles. `type` is the digest type and can be MD5, SHA-1, SHA-256, SHA-384 and SHA-512, value is the hex encoded checksum in lower case. To create a checksum save the tile as file and upload it to e.g. https://defuse.ca/checksums.htm. + */ + readonly "no-tile-checksum"?: string + /** + * header-name attribute specifies a header returned by tile server, that will be shown as `metadata-key` attribute in Show Tile Info dialog + */ + readonly "metadata-header"?: string + /** + * Set to `true` if imagery source is properly aligned and does not need imagery offset adjustments. This is used for OSM based sources too. + */ + readonly "valid-georeference"?: boolean + /** + * Size of individual tiles delivered by a TMS service + */ + readonly "tile-size"?: number + /** + * Whether tiles status can be accessed by appending /status to the tile URL and can be submitted for re-rendering by appending /dirty. + */ + readonly "mod-tile-features"?: string + /** + * HTTP headers to be sent to server. It has two attributes header-name and header-value. May be specified multiple times. + */ + readonly "custom-http-headers"?: { + readonly "header-name"?: string + readonly "header-value"?: string + } + /** + * Default layer to open (when using WMS_ENDPOINT type). Contains list of layer tag with two attributes - name and style, e.g. `"default-layers": ["layer": { name="Basisdata_NP_Basiskart_JanMayen_WMTS_25829" "style":"default" } ]` (not allowed in `mirror` attribute) + */ + readonly "default-layers"?: { + layer?: { + "layer-name"?: string + "layer-style"?: string + [k: string]: unknown + } + [k: string]: unknown + }[] + /** + * format to use when connecting tile server (when using WMS_ENDPOINT type) + */ + readonly format?: string + /** + * If `true` transparent tiles will be requested from WMS server + */ + readonly transparent?: boolean & string + /** + * minimum expiry time for tiles in seconds. The larger the value, the longer entry in cache will be considered valid + */ + readonly "minimum-tile-expire"?: number } diff --git a/src/assets/global-raster-layers.json b/src/assets/global-raster-layers.json index c33f71e080..25c0edf0a1 100644 --- a/src/assets/global-raster-layers.json +++ b/src/assets/global-raster-layers.json @@ -1,97 +1,92 @@ { "layers": [ { - "id": "Stamen.TonerLite", - "name": "Toner Lite (by Stamen)", - "url": "https://stamen-tiles-{switch:a,b,c,d}.a.ssl.fastly.net/toner-lite/{z}/{x}/{y}.png", + "name": "Americana", + "url": "https://zelonewolf.github.io/openstreetmap-americana/style.json", "category": "osmbasedmap", + "id": "americana", + "type": "vector", "attribution": { - "html": "Map tiles by
Stamen Design, CC BY 3.0 — Map data {attribution.OpenStreetMap}" - }, - "min_zoom": 0, - "max_zoom": 20 + "text": "Americana", + "url": "https://github.com/ZeLonewolf/openstreetmap-americana/" + } }, { - "id": "Stamen.TonerBackground", - "name": "Toner Background - no labels (by Stamen)", + "name": "MapTiler Backdrop", + "url": "https://api.maptiler.com/maps/backdrop/style.json?key=GvoVAJgu46I5rZapJuAy", "category": "osmbasedmap", - "url": "https://stamen-tiles-{switch:a,b,c,d}.a.ssl.fastly.net/toner-background/{z}/{x}/{y}.png", + "id": "maptiler.backdrop", + "type": "vector", "attribution": { - "html": "Map tiles by Stamen Design, CC BY 3.0 — Map data {attribution.OpenStreetMap}" - }, - "min_zoom": 0, - "max_zoom": 20 + "text": "Maptiler", + "url": "https://www.maptiler.com/copyright/" + } }, { - "id": "Stamen.Watercolor", - "name": "Watercolor (by Stamen)", + "name": "MapTiler Carto", + "url": "https://api.maptiler.com/maps/openstreetmap/style.json?key=GvoVAJgu46I5rZapJuAy", "category": "osmbasedmap", - "url": "https://stamen-tiles-{switch:a,b,c,d}.a.ssl.fastly.net/watercolor/{z}/{x}/{y}.png", + "id": "maptiler.carto", + "type": "vector", "attribution": { - "html": "Map tiles by Stamen Design, CC BY 3.0 — Map data {attribution.OpenStreetMap}" - }, - "min_zoom": 0, - "max_zoom": 20 + "text": "Maptiler", + "url": "https://www.maptiler.com/copyright/" + } }, { - "id": "CartoDB.Positron", - "name": "Positron (by CartoDB)", - "url": "https://{switch:a,b,c,d}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}@2x.png", + "name": "Alidade Smooth", + "url": "https://tiles-eu.stadiamaps.com/styles/alidade_smooth.json?key=14c5a900-7137-42f7-9cb9-fff0f4696f75", + "category": "osmbasedmap", + "id": "alidade.smooth", + "type": "vector", "attribution": { - "html": "CARTO" - }, - "max_zoom": 20, - "category": "osmbasedmap" + "text": "Alidade", + "url": "https://stadiamaps.com/" + } }, { - "id": "CartoDB.PositronNoLabels", - "name": "Positron - no labels (by CartoDB)", - "url": "https://{switch:a,b,c,d}.basemaps.cartocdn.com/light_nolabels/{z}/{x}/{y}.png", + "name": "Alidade Smooth Dark", + "url": "https://tiles-eu.stadiamaps.com/styles/alidade_smooth_dark.json?key=14c5a900-7137-42f7-9cb9-fff0f4696f75", "category": "osmbasedmap", + "id": "alidade.smooth_dark", + "type": "vector", "attribution": { - "html": "CARTO" - }, - "max_zoom": 20 + "text": "Alidade/Stadiamaps", + "url": "https://stadiamaps.com/" + } }, { - "id": "CartoDB.Voyager", - "name": "Voyager (by CartoDB)", - "url": "https://{switch:a,b,c,d}.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}.png", + "name": "Stamen Terrain", + "url": "https://tiles-eu.stadiamaps.com/styles/stamen_terrain.json?key=14c5a900-7137-42f7-9cb9-fff0f4696f75", "category": "osmbasedmap", + "id": "stamen.terrain", + "type": "vector", "attribution": { - "html": "CARTO" - }, - "max_zoom": 20 + "text": "Stamen/Stadiamaps", + "url": "https://stadiamaps.com/" + } }, { - "id": "CartoDB.VoyagerNoLabels", - "name": "Voyager - no labels (by CartoDB)", - "url": "https://{switch:a,b,c,d}.basemaps.cartocdn.com/rastertiles/voyager_nolabels/{z}/{x}/{y}.png", + "name": "Stamen Toner", + "url": "https://tiles-eu.stadiamaps.com/styles/stamen_toner.json?key=14c5a900-7137-42f7-9cb9-fff0f4696f75", "category": "osmbasedmap", + "id": "stamen.toner", + "type": "vector", "attribution": { - "html": "CARTO" - }, - "max_zoom": 20 + "text": "Stamen/Stadiamaps", + "url": "https://stadiamaps.com/" + } }, { - "id": "CartoDB.DarkMatter", - "name": "Dark Matter (by CartoDB)", - "url": "https://{switch:a,b,c,d}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}.png", + "url": "https://tiles-eu.stadiamaps.com/styles/osm_bright.json", + "name": "StadiaMaps OSM Bright", "category": "osmbasedmap", + "id": "stadia.bright", + "type": "vector", "attribution": { - "html": "CARTO" - }, - "max_zoom": 20 - }, - { - "id": "CartoDB.DarkMatterNoLabels", - "name": "Dark Matter - no labels (by CartoDB)", - "url": "https://{switch:a,b,c,d}.basemaps.cartocdn.com/dark_nolabels/{z}/{x}/{y}.png", - "category": "osmbasedmap", - "attribution": { - "html": "CARTO" - }, - "max_zoom": 20 + "text": "Stadiamaps", + "url": "https://stadiamaps.com/" + } } ] } From 5e2a98924c2a0efdb1f75a213efe7114df5e0cae Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Wed, 11 Oct 2023 19:16:33 +0200 Subject: [PATCH 24/30] Fix: add carto vector layers again --- src/Models/RasterLayers.ts | 6 +-- src/assets/global-raster-layers.json | 68 ++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+), 4 deletions(-) diff --git a/src/Models/RasterLayers.ts b/src/Models/RasterLayers.ts index 47162b6908..d0f8a88d58 100644 --- a/src/Models/RasterLayers.ts +++ b/src/Models/RasterLayers.ts @@ -53,10 +53,7 @@ export class AvailableRasterLayers { geometry: BBox.global.asGeometry(), } - public static readonly vectorLayers = [ - AvailableRasterLayers.maptilerDefaultLayer, - AvailableRasterLayers.osmCarto, - ] + public static readonly vectorLayers = [AvailableRasterLayers.maptilerDefaultLayer] public static layersAvailableAt( location: Store<{ lon: number; lat: number }> @@ -79,6 +76,7 @@ export class AvailableRasterLayers { } return GeoOperations.inside(lonlat, eliPolygon) }) + matching.push(AvailableRasterLayers.maptilerDefaultLayer) matching.push(...AvailableRasterLayers.globalLayers) return matching }) diff --git a/src/assets/global-raster-layers.json b/src/assets/global-raster-layers.json index 25c0edf0a1..cb54b006ca 100644 --- a/src/assets/global-raster-layers.json +++ b/src/assets/global-raster-layers.json @@ -87,6 +87,74 @@ "text": "Stadiamaps", "url": "https://stadiamaps.com/" } + }, + { + "url": "https://basemaps.cartocdn.com/gl/positron-gl-style/style.json?key=eyJhbGciOiJIUzI1NiJ9.eyJhIjoiYWNfdW4ybmhlbTciLCJqdGkiOiIwZGQxNjJmNyJ9.uATJpa6QcrtXhph3Bzvk2nX3QsxEw-Q8dj5khUG6hGk", + "name": "Carto Positron", + "category": "osmbasedmap", + "id": "carto.positron", + "type": "vector", + "attribution": { + "text": "CARTO", + "url": "https://carto.com/" + } + }, + { + "url": "https://basemaps.cartocdn.com/gl/dark-matter-gl-style/style.json?key=eyJhbGciOiJIUzI1NiJ9.eyJhIjoiYWNfdW4ybmhlbTciLCJqdGkiOiIwZGQxNjJmNyJ9.uATJpa6QcrtXhph3Bzvk2nX3QsxEw-Q8dj5khUG6hGk", + "name": "Carto Dark Matter", + "category": "osmbasedmap", + "id": "carto.dark_matter", + "type": "vector", + "attribution": { + "text": "CARTO", + "url": "https://carto.com/" + } + }, + { + "url": "https://basemaps.cartocdn.com/gl/voyager-gl-style/style.json?key=eyJhbGciOiJIUzI1NiJ9.eyJhIjoiYWNfdW4ybmhlbTciLCJqdGkiOiIwZGQxNjJmNyJ9.uATJpa6QcrtXhph3Bzvk2nX3QsxEw-Q8dj5khUG6hGk", + "name": "Carto Voyager", + "category": "osmbasedmap", + "id": "carto.voyager", + "type": "vector", + "attribution": { + "text": "CARTO", + "url": "https://carto.com/" + } + }, + + + { + "url": "https://basemaps.cartocdn.com/gl/positron-nolabels-gl-style/style.json?key=eyJhbGciOiJIUzI1NiJ9.eyJhIjoiYWNfdW4ybmhlbTciLCJqdGkiOiIwZGQxNjJmNyJ9.uATJpa6QcrtXhph3Bzvk2nX3QsxEw-Q8dj5khUG6hGk", + "name": "Carto Positron (no labels)", + "category": "osmbasedmap", + "id": "carto.positron_no_labels", + "type": "vector", + "attribution": { + "text": "CARTO", + "url": "https://carto.com/" + } + }, + { + "url": "https://basemaps.cartocdn.com/gl/dark-matter-nolabels-gl-style/style.json?key=eyJhbGciOiJIUzI1NiJ9.eyJhIjoiYWNfdW4ybmhlbTciLCJqdGkiOiIwZGQxNjJmNyJ9.uATJpa6QcrtXhph3Bzvk2nX3QsxEw-Q8dj5khUG6hGk", + "name": "Carto Dark Matter (no labels)", + "category": "osmbasedmap", + "id": "carto.dark_matter_no_labels", + "type": "vector", + "attribution": { + "text": "CARTO", + "url": "https://carto.com/" + } + }, + { + "url": "https://basemaps.cartocdn.com/gl/voyager-nolabels-gl-style/style.json?key=eyJhbGciOiJIUzI1NiJ9.eyJhIjoiYWNfdW4ybmhlbTciLCJqdGkiOiIwZGQxNjJmNyJ9.uATJpa6QcrtXhph3Bzvk2nX3QsxEw-Q8dj5khUG6hGk", + "name": "Carto Voyager (no labels)", + "category": "osmbasedmap", + "id": "carto.voyager_no_labels", + "type": "vector", + "attribution": { + "text": "CARTO", + "url": "https://carto.com/" + } } ] } From d997c90352a0d70f2cd33108af4de16f58444868 Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Wed, 11 Oct 2023 19:19:28 +0200 Subject: [PATCH 25/30] Themes: add Stamen Watercolor again, see #1652 --- src/assets/global-raster-layers.json | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/assets/global-raster-layers.json b/src/assets/global-raster-layers.json index cb54b006ca..e23152532d 100644 --- a/src/assets/global-raster-layers.json +++ b/src/assets/global-raster-layers.json @@ -76,6 +76,15 @@ "text": "Stamen/Stadiamaps", "url": "https://stadiamaps.com/" } + }, { + "name": "Stamen Watercolor", + "url": "https://tiles.stadiamaps.com/tiles/stamen_watercolor/{z}/{x}/{y}.jpg?key=14c5a900-7137-42f7-9cb9-fff0f4696f75", + "category": "osmbasedmap", + "id": "stamen.watercolor", + "attribution": { + "text": "Stamen/Stadiamaps", + "url": "https://stadiamaps.com/" + } }, { "url": "https://tiles-eu.stadiamaps.com/styles/osm_bright.json", From 09504e18ecb4b85ceb6562fcdb0e21034e496ad8 Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Wed, 11 Oct 2023 19:04:31 +0200 Subject: [PATCH 26/30] Fix: include 'source' and tile URLs from vector tile sources into CSP, see #1652 --- scripts/generateLayouts.ts | 104 +++++++++++++++++++++---------------- 1 file changed, 59 insertions(+), 45 deletions(-) diff --git a/scripts/generateLayouts.ts b/scripts/generateLayouts.ts index e7e8d676fd..17805270d8 100644 --- a/scripts/generateLayouts.ts +++ b/scripts/generateLayouts.ts @@ -1,20 +1,20 @@ -import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFile, writeFileSync } from "fs"; -import Locale from "../src/UI/i18n/Locale"; -import Translations from "../src/UI/i18n/Translations"; -import { Translation } from "../src/UI/i18n/Translation"; -import all_known_layouts from "../src/assets/generated/known_themes.json"; -import { LayoutConfigJson } from "../src/Models/ThemeConfig/Json/LayoutConfigJson"; -import LayoutConfig from "../src/Models/ThemeConfig/LayoutConfig"; -import xml2js from "xml2js"; -import ScriptUtils from "./ScriptUtils"; -import { Utils } from "../src/Utils"; -import SpecialVisualizations from "../src/UI/SpecialVisualizations"; -import Constants from "../src/Models/Constants"; -import { AvailableRasterLayers, RasterLayerPolygon } from "../src/Models/RasterLayers"; -import { ImmutableStore } from "../src/Logic/UIEventSource"; -import * as crypto from "crypto"; -import * as eli from "../src/assets/editor-layer-index.json"; -import * as eli_global from "../src/assets/global-raster-layers.json"; +import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFile, writeFileSync } from "fs" +import Locale from "../src/UI/i18n/Locale" +import Translations from "../src/UI/i18n/Translations" +import { Translation } from "../src/UI/i18n/Translation" +import all_known_layouts from "../src/assets/generated/known_themes.json" +import { LayoutConfigJson } from "../src/Models/ThemeConfig/Json/LayoutConfigJson" +import LayoutConfig from "../src/Models/ThemeConfig/LayoutConfig" +import xml2js from "xml2js" +import ScriptUtils from "./ScriptUtils" +import { Utils } from "../src/Utils" +import SpecialVisualizations from "../src/UI/SpecialVisualizations" +import Constants from "../src/Models/Constants" +import { AvailableRasterLayers, RasterLayerPolygon } from "../src/Models/RasterLayers" +import { ImmutableStore } from "../src/Logic/UIEventSource" +import * as crypto from "crypto" +import * as eli from "../src/assets/editor-layer-index.json" +import * as eli_global from "../src/assets/global-raster-layers.json" const sharp = require("sharp") const template = readFileSync("theme.html", "utf8") @@ -61,9 +61,9 @@ async function createSocialImage(layout: LayoutConfig, template: "" | "Wide"): P if (!layout.icon.endsWith(".svg")) { console.warn( "Not creating a social image for " + - layout.id + - " as it is _not_ a .svg: " + - layout.icon + layout.id + + " as it is _not_ a .svg: " + + layout.icon, ) return undefined } @@ -85,7 +85,7 @@ async function createSocialImage(layout: LayoutConfig, template: "" | "Wide"): P delete svg["defs"] delete svg["$"] let templateSvg = await ScriptUtils.ReadSvg( - "./public/assets/SocialImageTemplate" + template + ".svg" + "./public/assets/SocialImageTemplate" + template + ".svg", ) templateSvg = Utils.WalkJson( templateSvg, @@ -104,7 +104,7 @@ async function createSocialImage(layout: LayoutConfig, template: "" | "Wide"): P return false } return mightBeTokenToReplace.circle[0]?.$?.style?.indexOf("fill:#ff00ff") >= 0 - } + }, ) const builder = new xml2js.Builder() @@ -116,7 +116,7 @@ async function createSocialImage(layout: LayoutConfig, template: "" | "Wide"): P async function createManifest( layout: LayoutConfig, - alreadyWritten: string[] + alreadyWritten: string[], ): Promise<{ manifest: any whiteIcons: string[] @@ -210,15 +210,17 @@ function asLangSpan(t: Translation, tag = "span"): string { let previousSrc: Set = new Set() let eliUrlsCached: string[] -function eliUrls(): string[] { + +async function eliUrls(): Promise { if (eliUrlsCached) { return eliUrlsCached } const urls: string[] = [] const regex = /{switch:([^}]+)}/ - const rasterLayers = [...AvailableRasterLayers.vectorLayers, ...eli.features, ...eli_global.layers.map(properties => ({properties})) ] - for (const feature of rasterLayers) { - const url = (feature).properties.url + const rasterLayers = [...AvailableRasterLayers.vectorLayers, ...eli.features, ...eli_global.layers.map(properties => ({ properties }))] + for (const feature of rasterLayers) { + const f = feature + const url = f.properties.url const match = url.match(regex) if (match) { const domains = match[1].split(",") @@ -227,17 +229,28 @@ function eliUrls(): string[] { } else { urls.push(url) } + + if (f.properties.type === "vector") { + // We also need to whitelist eventual sources + const styleSpec = await Utils.downloadJsonCached(f.properties.url, 1000 * 120) + for (const key in styleSpec.sources) { + const url = styleSpec.sources[key].url + urls.push(url) + } + urls.push(...(styleSpec["tiles"] ?? [])) + } + } eliUrlsCached = urls return urls } -function generateCsp( +async function generateCsp( layout: LayoutConfig, options: { scriptSrcs: string[] - } -): string { + }, +): Promise { const apiUrls: string[] = [ "'self'", ...Constants.defaultOverpassUrls, @@ -247,12 +260,12 @@ function generateCsp( "https://pietervdvn.goatcounter.com", ] .concat(...SpecialVisualizations.specialVisualizations.map((sv) => sv.needsUrls)) - .concat(...eliUrls()) + .concat(...await eliUrls()) const geojsonSources: string[] = layout.layers.map((l) => l.source?.geojsonSource) const hosts = new Set() const eliLayers: RasterLayerPolygon[] = AvailableRasterLayers.layersAvailableAt( - new ImmutableStore({ lon: 0, lat: 0 }) + new ImmutableStore({ lon: 0, lat: 0 }), ).data const vectorLayers = eliLayers.filter((l) => l.properties.type === "vector") const vectorSources = vectorLayers.map((l) => l.properties.url) @@ -279,14 +292,14 @@ function generateCsp( "connect-src items for theme", layout.id, "(extra sources: ", - newSrcs.join(" ") + ")" + newSrcs.join(" ") + ")", ) previousSrc = hosts const csp: Record = { "default-src": "'self'", "script-src": ["'self'", "https://gc.zgo.at/count.js", ...(options?.scriptSrcs ?? [])].join( - " " + " ", ), "img-src": "* data:", // maplibre depends on 'data:' to load "connect-src": connectSrc.join(" "), @@ -316,12 +329,12 @@ const removeOtherLanguagesHash = crypto async function createLandingPage(layout: LayoutConfig, manifest, whiteIcons, alreadyWritten) { Locale.language.setData(layout.language[0]) const targetLanguage = layout.language[0] - const ogTitle = Translations.T(layout.title).textFor(targetLanguage).replace(/"/g, '\\"') + const ogTitle = Translations.T(layout.title).textFor(targetLanguage).replace(/"/g, "\\\"") const ogDescr = Translations.T( - layout.shortDescription ?? "Easily add and edit geodata with OpenStreetMap" + layout.shortDescription ?? "Easily add and edit geodata with OpenStreetMap", ) .textFor(targetLanguage) - .replace(/"/g, '\\"') + .replace(/"/g, "\\\"") let ogImage = layout.socialImage let twitterImage = ogImage if (ogImage === LayoutConfig.defaultSocialImage && layout.official) { @@ -386,34 +399,34 @@ async function createLandingPage(layout: LayoutConfig, manifest, whiteIcons, alr const loadingText = Translations.t.general.loadingTheme.Subs({ theme: layout.title }) const templateLines = template.split("\n") const removeOtherLanguagesReference = templateLines.find( - (line) => line.indexOf("./src/UI/RemoveOtherLanguages.js") >= 0 + (line) => line.indexOf("./src/UI/RemoveOtherLanguages.js") >= 0, ) let output = template .replace("Loading MapComplete, hang on...", asLangSpan(loadingText, "h1")) .replace( "Made with OpenStreetMap", - Translations.t.general.poweredByOsm.textFor(targetLanguage) + Translations.t.general.poweredByOsm.textFor(targetLanguage), ) .replace(/.*/s, themeSpecific) .replace( //, - generateCsp(layout, { + await generateCsp(layout, { scriptSrcs: [`'sha256-${removeOtherLanguagesHash}'`], - }) + }), ) .replace(removeOtherLanguagesReference, "") .replace( /.*/s, - asLangSpan(layout.shortDescription) + asLangSpan(layout.shortDescription), ) .replace( /.*/s, - "" + "", ) .replace( /.*\/src\/index\.ts.*/, - `` + ``, ) return output @@ -504,13 +517,14 @@ async function main(): Promise { title: { en: "MapComplete" }, description: { en: "A thematic map viewer and editor based on OpenStreetMap" }, }), - alreadyWritten + alreadyWritten, ) const manif = JSON.stringify(manifest, undefined, 2) writeFileSync("public/index.webmanifest", manif) } +ScriptUtils.fixUtils() main().then(() => { console.log("All done!") }) From 2e3bddc90088190befda69f7f3667a804fe9daf4 Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Wed, 11 Oct 2023 21:46:41 +0000 Subject: [PATCH 27/30] Update CONTRIBUTING.md Co-authored-by: riQQ --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 05f9499b9f..6acc1b1eee 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -11,7 +11,7 @@ There are multiple ways to contribute: - A good start to contribute is to create a single map layer showing features which interest you. Read more about [making your own theme](/Docs/Making_Your_Own_Theme.md). - If you want to improve a theme, create a new theme, spot a typo in the repo... the best way is to open a pull request. -People who stick around and contribute in a meaningful way, _might_ be granted write access to the repository. This is +People who stick around and contribute in a meaningful way, _might_ be granted write access to the repository (except the branches *master* and *develop*). This is done on a purely subjective basis, e.g. after a few pull requests and if you are a member of the OSM community. Rights of contributors From 713e53c41a840ae1e66cb9a36e06e0e9361285bd Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Thu, 12 Oct 2023 00:43:42 +0200 Subject: [PATCH 28/30] Fix: background layer csp fixe --- scripts/generateLayouts.ts | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/scripts/generateLayouts.ts b/scripts/generateLayouts.ts index 17805270d8..c6ec058f59 100644 --- a/scripts/generateLayouts.ts +++ b/scripts/generateLayouts.ts @@ -233,16 +233,31 @@ async function eliUrls(): Promise { if (f.properties.type === "vector") { // We also need to whitelist eventual sources const styleSpec = await Utils.downloadJsonCached(f.properties.url, 1000 * 120) - for (const key in styleSpec.sources) { + for (const key of Object.keys(styleSpec.sources)) { const url = styleSpec.sources[key].url + if(!url){ + continue + } + let urlClipped = url + if(url.indexOf("?") > 0){ + urlClipped = url?.substring(0, url.indexOf("?")) + } + console.log("Source url ",key,url) urls.push(url) + if(urlClipped.endsWith(".json")){ + const tileInfo = await Utils.downloadJsonCached(url, 1000*120) + urls.push(tileInfo["tiles"] ?? []) + } + } urls.push(...(styleSpec["tiles"] ?? [])) + urls.push(styleSpec["sprite"]) + urls.push(styleSpec["glyphs"]) } } eliUrlsCached = urls - return urls + return Utils.NoNull(urls).sort() } async function generateCsp( From 0e80b7470c2614d7271f804f106977bd09753f8f Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Thu, 12 Oct 2023 02:15:27 +0200 Subject: [PATCH 29/30] Chore: remove obsolete classes --- src/UI/Input/Checkboxes.ts | 98 --------------------------------- src/UI/Input/InputElementMap.ts | 61 -------------------- 2 files changed, 159 deletions(-) delete mode 100644 src/UI/Input/Checkboxes.ts delete mode 100644 src/UI/Input/InputElementMap.ts diff --git a/src/UI/Input/Checkboxes.ts b/src/UI/Input/Checkboxes.ts deleted file mode 100644 index 222d39fb1f..0000000000 --- a/src/UI/Input/Checkboxes.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { InputElement } from "./InputElement" -import { UIEventSource } from "../../Logic/UIEventSource" -import { Utils } from "../../Utils" -import BaseUIElement from "../BaseUIElement" -import InputElementMap from "./InputElementMap" -import Translations from "../i18n/Translations" - -/** - * @deprecated - */ -export class CheckBox extends InputElementMap { - constructor(el: BaseUIElement | string, defaultValue?: boolean) { - super( - new CheckBoxes([Translations.W(el)]), - (x0, x1) => x0 === x1, - (t) => t.length > 0, - (x) => (x ? [0] : []) - ) - if (defaultValue !== undefined) { - this.GetValue().setData(defaultValue) - } - } -} - -/** - * A list of individual checkboxes - * The value will contain the indexes of the selected checkboxes - */ -export default class CheckBoxes extends InputElement { - private static _nextId = 0 - private readonly value: UIEventSource - private readonly _elements: BaseUIElement[] - - constructor(elements: BaseUIElement[], value = new UIEventSource([])) { - super() - this.value = value - this._elements = Utils.NoNull(elements) - this.SetClass("flex flex-col") - } - - IsValid(ts: number[]): boolean { - return ts !== undefined - } - - GetValue(): UIEventSource { - return this.value - } - - protected InnerConstructElement(): HTMLElement { - const formTag = document.createElement("form") - - const value = this.value - const elements = this._elements - - for (let i = 0; i < elements.length; i++) { - let inputI = elements[i] - const input = document.createElement("input") - const id = CheckBoxes._nextId - CheckBoxes._nextId++ - input.id = "checkbox" + id - - input.type = "checkbox" - input.classList.add("p-1", "cursor-pointer", "m-3", "pl-3", "mr-0") - - const label = document.createElement("label") - label.htmlFor = input.id - label.appendChild(input) - label.appendChild(inputI.ConstructElement()) - label.classList.add("block", "w-full", "p-2", "cursor-pointer") - - formTag.appendChild(label) - - value.addCallbackAndRunD((selectedValues) => { - input.checked = selectedValues.indexOf(i) >= 0 - - if (input.checked) { - label.classList.add("checked") - } else { - label.classList.remove("checked") - } - }) - - input.onchange = () => { - // Index = index in the list of already checked items - const index = value.data.indexOf(i) - if (input.checked && index < 0) { - value.data.push(i) - value.ping() - } else if (index >= 0) { - value.data.splice(index, 1) - value.ping() - } - } - } - - return formTag - } -} diff --git a/src/UI/Input/InputElementMap.ts b/src/UI/Input/InputElementMap.ts deleted file mode 100644 index 4e49c38b51..0000000000 --- a/src/UI/Input/InputElementMap.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { InputElement } from "./InputElement" -import { Store, UIEventSource } from "../../Logic/UIEventSource" - -/** - * @deprecated - */ -export default class InputElementMap extends InputElement { - private readonly _inputElement: InputElement - private isSame: (x0: X, x1: X) => boolean - private readonly fromX: (x: X) => T - private readonly toX: (t: T) => X - private readonly _value: UIEventSource - - constructor( - inputElement: InputElement, - isSame: (x0: X, x1: X) => boolean, - toX: (t: T) => X, - fromX: (x: X) => T, - extraSources: Store[] = [] - ) { - super() - this.isSame = isSame - this.fromX = fromX - this.toX = toX - this._inputElement = inputElement - const self = this - this._value = inputElement.GetValue().sync( - (t) => { - const newX = toX(t) - const currentX = self.GetValue()?.data - if (isSame(currentX, newX)) { - return currentX - } - return newX - }, - extraSources, - (x) => { - return fromX(x) - } - ) - } - - GetValue(): UIEventSource { - return this._value - } - - IsValid(x: X): boolean { - if (x === undefined) { - return false - } - const t = this.fromX(x) - if (t === undefined) { - return false - } - return this._inputElement.IsValid(t) - } - - protected InnerConstructElement(): HTMLElement { - return this._inputElement.ConstructElement() - } -} From f954a93b594b8902407b8f3ce65702e913b7e82f Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Thu, 12 Oct 2023 14:27:28 +0200 Subject: [PATCH 30/30] Add more debug info --- src/Utils.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/Utils.ts b/src/Utils.ts index 1f196de6ef..180c727cbc 100644 --- a/src/Utils.ts +++ b/src/Utils.ts @@ -1098,7 +1098,16 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be } return { content: data } } catch (e) { - console.error("Could not parse ", data, "due to", e, "\n", e.stack) + console.error( + "Could not parse the response of", + url, + "which contains", + data, + "due to", + e, + "\n", + e.stack + ) return { error: "malformed", url } } }