From c1703b3d9e1baa930123d5d31ea5104b6fa97638 Mon Sep 17 00:00:00 2001 From: Vaclav Petras Date: Fri, 17 Mar 2023 17:25:50 -0400 Subject: [PATCH 01/10] v.move.points: Move points by values from raster map WIP code for move of vector features positions by X and Y values given in two raster maps. This can move linear features without breaking the visual connection as points falling into the same raster cell are shifted by the same value. Alternative name would be v.rast.move because the big deal is that it is using raster to do the moving. The one example included now shows the principle, moves in X direction both ways depending on the raster values. My use case is randomizing a set of lines (connected and not connected). Feedback welcome. --- src/vector/v.move.points/Makefile | 7 ++ src/vector/v.move.points/examples.ipynb | 112 ++++++++++++++++++++ src/vector/v.move.points/v.move.points.html | 59 +++++++++++ src/vector/v.move.points/v.move.points.py | 91 ++++++++++++++++ src/vector/v.move.points/v_move_points.png | Bin 0 -> 59261 bytes 5 files changed, 269 insertions(+) create mode 100644 src/vector/v.move.points/Makefile create mode 100644 src/vector/v.move.points/examples.ipynb create mode 100644 src/vector/v.move.points/v.move.points.html create mode 100644 src/vector/v.move.points/v.move.points.py create mode 100644 src/vector/v.move.points/v_move_points.png diff --git a/src/vector/v.move.points/Makefile b/src/vector/v.move.points/Makefile new file mode 100644 index 0000000000..ece0d7a91d --- /dev/null +++ b/src/vector/v.move.points/Makefile @@ -0,0 +1,7 @@ +MODULE_TOPDIR = ../.. + +PGM = v.move.points + +include $(MODULE_TOPDIR)/include/Make/Script.make + +default: script diff --git a/src/vector/v.move.points/examples.ipynb b/src/vector/v.move.points/examples.ipynb new file mode 100644 index 0000000000..fa15b33d86 --- /dev/null +++ b/src/vector/v.move.points/examples.ipynb @@ -0,0 +1,112 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Graphics for Description of r.series\n", + "\n", + "Requires _pngquant_, _optipng_ and ImageMagic _mogrify_." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "\n", + "from IPython.display import Image\n", + "\n", + "import grass.script as gs\n", + "import grass.jupyter as gj" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "gj.init(\"~/grassdata/nc_basic_spm_grass7/user1\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "!g.region vector=roadsmajor res=100\n", + "!r.mapcalc expression=\"a = 1000*sin(row())\"\n", + "!r.mapcalc expression=\"b = 0\"\n", + "!v.move.points input=roadsmajor output=roads_moved x_raster=a y_raster=b" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "!g.region vector=roadsmajor,roads_moved\n", + "!r.colors map=a color=difference --q\n", + "!v.patch input=roadsmajor,roads_moved output=merged --q\n", + "!v.buffer input=merged output=buffer distance=1000 --q" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "plot = gj.Map(use_region=True, width=700)\n", + "plot.d_background(color=\"white\")\n", + "plot.d_rast(map=\"a\")\n", + "plot.d_vect(map=\"buffer\", color=\"white\", fill_color=\"white\", flags=\"s\")\n", + "plot.d_vect(map=\"roadsmajor\", color=\"blue\", width=5, legend_label=\"Original\")\n", + "plot.d_vect(map=\"roads_moved\", color=\"red\", width=5, legend_label=\"Shifted\")\n", + "plot.d_legend_vect(flags=\"b\", at=(60,10))\n", + "plot.d_legend(raster=\"a\", at=\"8,35,85,90\", flags=\"b\", title=\"X shift\")\n", + "plot.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "filename = \"v_move_points.png\"\n", + "plot.save(filename)\n", + "!mogrify -trim {filename}\n", + "!pngquant --ext \".png\" -f {filename}\n", + "!optipng -o7 {filename}\n", + "Image(filename)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.10" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/src/vector/v.move.points/v.move.points.html b/src/vector/v.move.points/v.move.points.html new file mode 100644 index 0000000000..393caf877b --- /dev/null +++ b/src/vector/v.move.points/v.move.points.html @@ -0,0 +1,59 @@ +

DESCRIPTION

+ +v.move.points takes values from raster maps and adds them to X and Y +coordinates of features in a vector map. + +

NOTES

+ +

EXAMPLES

+ +

Shift in X direction

+ +
+ + Two roads networks +
+ + Figure: Original (blue) and shifted (red) road network and the X shift + values in diverging blue-white-red colors (red shift right, blue shift + left, white no shift) + +
+ +

SEE ALSO

+ + + + +

AUTHOR

+ +Vaclav Petras, NCSU Center for Geospatial Analytics diff --git a/src/vector/v.move.points/v.move.points.py b/src/vector/v.move.points/v.move.points.py new file mode 100644 index 0000000000..02ee46bd7c --- /dev/null +++ b/src/vector/v.move.points/v.move.points.py @@ -0,0 +1,91 @@ +#!/usr/bin/env python3 + +############################################################################ +# +# MODULE: v.move.points +# +# AUTHOR(S): Vaclav Petras +# +# PURPOSE: Update a column values using Python expressions +# +# COPYRIGHT: (C) 2023 by Vaclav Petras and the GRASS Development Team +# +# This program is free software under the GNU General Public +# License (>=v2). Read the file COPYING that comes with GRASS +# for details. +# +############################################################################# + +# %module +# % description: Move points by distance specified in a raster +# % keyword: vector +# % keyword: +# % keyword: +# % keyword: +# %end +# %option G_OPT_V_INPUT +# %end +# %option G_OPT_R_INPUT +# %key: x_raster +# %end +# %option G_OPT_R_INPUT +# %key: y_raster +# %end +# %option G_OPT_V_OUTPUT +# %end + +import grass.script as gs + + +def move_vector(input_vector_name, output_vector_name, x_raster_name, y_raster_name): + # Lazy import ctypes + # pylint: disable=import-outside-toplevel + from grass.pygrass.gis.region import Region + from grass.pygrass.raster import RasterSegment + from grass.pygrass.utils import coor2pixel + from grass.pygrass.vector import VectorTopo + from grass.pygrass.vector.geometry import Line, Point + + x_raster = RasterSegment(x_raster_name) + x_raster.open("r") + y_raster = RasterSegment(y_raster_name) + y_raster.open("r") + + region = Region() + + with VectorTopo(input_vector_name, mode="r") as input_vector, VectorTopo( + output_vector_name, mode="w" + ) as output_vector: + for feature in input_vector: + new_point_list = [] + for point in feature: + pixel = coor2pixel((point.x, point.y), region) + x_value = x_raster[int(pixel[0]), int(pixel[1])] + y_value = y_raster[int(pixel[0]), int(pixel[1])] + point = Point(point.x + x_value, point.y + y_value) + new_point_list.append(point) + new_line = Line(new_point_list) + output_vector.write(new_line) + + x_raster.close() + y_raster.close() + + +def main(): + options, unused_flags = gs.parser() + + input_vector_name = options["input"] + x_raster_name = options["x_raster"] + y_raster_name = options["y_raster"] + output_vector_name = options["output"] + + move_vector( + input_vector_name=input_vector_name, + x_raster_name=x_raster_name, + y_raster_name=y_raster_name, + output_vector_name=output_vector_name, + ) + + +if __name__ == "__main__": + main() diff --git a/src/vector/v.move.points/v_move_points.png b/src/vector/v.move.points/v_move_points.png new file mode 100644 index 0000000000000000000000000000000000000000..c9ae2da22b99202b47ae50dc908c115404d94745 GIT binary patch literal 59261 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 z zPPYgz(}it%;19ZDh_G6tfVY#=Yv8!bp~ovC*%MLR*wb>O=vxfqfcCnd2#)&5o~hi# zEA_Dt;a^v{0U~()x;oD|Yi6S3gu!Sz{3|%LSMJtye~9jnK#^X|pC+ghTL=flXMWZ4 zc`c2`oN5K;8?bQ3pY_OLD3@5>+3>FmU$b_-L zQX#A6>itBrPb{*^&Us6bMa|Z3);CfxjqMIf=S7y!N$VfKzL%3D7^A05#~fc#2`Qs< zTm~QaaouGlQvWe#m(7AM$|Cy5bEphw8r*i_kCM)&SD4oYiMYu}X85#_qAQIbe$T^S z4c-nT%wCZ(y~_oa({BU4?vmpFtw9;#XQkMGw;3m4u$A|$Xea#EPWtC5CSp3XM%|$S zr{V5l0iMzHaGVJ9D8or)NL`eXfCQQ|r@!$SO%5j_6t%UiuN!YPwMehY$jr3C+r%U! zQ357ue$_7osj?7qQem3PsI%=iF+5t=B#T?uic8`X`0eHoBG1;)E!JwArOd9nRS~6- zK8i--Z|(gX7ZXy7#fRzhHB^ejLgq#3%PPvKX#D0Dv2(M^Wtpc>R2*6TRBsWr7R}%G zZwWj;kKir*;FA;!cxyv80BoF3GiLO_- za)#BKHU25GG&k_r>7u8)*2A>U+Q|wxR4pxFn^@$^{4G;hZdCouWwG zC6bAd2>gwk*+{zh5jgjfd$H<~TfXWOP3Hdy#$@R63tkk~63tx2v;}JPRr?84EPWtFpY3#M+8}lBm%=P_rze39l6V)i2sRF5@ZX2)I>Tkg#gh-w7n=OQD_G#~hW$E$*B(Enf2uO_iS63^`LdD15Zzm4Ln>5Fy8pIs-EQm8Uy)XrnraPFj>s|K)01ux=5ypUUiYZa+RP zYpo33Vwp4gUJpvjwV{e!168otJ6tNK6 z&L6~Qo*o!eboAdi1OW_#NwFr0wx*3q^3hR;#qkuH8~eSo;w6H>C9MK(^AX`iyxl%} zsc2*I?tHuHd62>t!G#iBP79qI6Efpv2uj9ee2R2ZsJ7_Db zR+!0~hqxdQ`h=-E3mbcHsn;PT?t^fCJM|`SZAh{^ukrR{oZ4OtTtlZx@yy1`OVxjNNtfbU!c0406YT{hnI7D?+!@0IgTV zZ|&I)b*j)|PNXs%;jJv~vB;DyeQGRox`$5G?yJ+zyFsX+n~s52gX;lbL zzPJaVTq0#1{HEd^^KcmZV584tXY~*m%pGM>whBA%bUBZ7wh@Uc%i9ilO&k2xuDv9a z6S7H1LRARPvlP6G9X7tico$J)b1Ozgv%-e0H>wSOIBB`6XC!d+d??C;F4c?jK=AVfmhT!2c>EU)}KVzbljm9RSEdtHdRj1l5j zEC<%hi|pzC+^aG_wi$C_LYXJn_LRnotb(Ki&zJ?J%3r_R0{pKfdq{K@lFolj?0nA^tCIda z@@*QOaCvgIDMt>2HX-buI6%wv-eRa|agP-5!F8GBfEFd{K6%SkylNnwG>Ef4cZEj9 zOb!2B*AtG8drnZF+1A(b@GK ziP+(%mN&QY={7#}78vz!lh;j{q2Ww5xRRLOj`C_m*=6ieCQw@VqIDqOa6Ah&Dka%Xfx_G zLWfy7Dv7Y=Q~(om&p@Z`#4~e)v*!#MjzW?%XxUro7S9MrW~P4$R@dSkF3ip#cOTQe zmc!Q^V__h)KrPj@Fa{eYrPJ$##r(nZ+#k!(V}JB78K1@JA-XQQ-p}4|^YIE`*A?-F z?_M;);<9AI*#84YK)JsX^U;kSpM1G*i@69max!*h=UHXp?lA;Q;NKNOgXuxr$8{%LU%Hk}eAkIIhzl z*&`B(l%FuEE4O$Se_gqQ;k#7zv)%aJL9u*uRf8|jh2rVgQ( zc~;L?FsxHPd8FRtF;2{dYVD3cD)6ynMSI&duv|K>#ZXNu@&7Ga)g48w6KIe8#tHHV z2%Kr)nDjI$+>oBpp24i+rfx?H&b)(gJXh&J9dgkXbkD`gKf=Q(i_4cbo!q1rI^D?5 zD;&uVL4P$WzDd)Aj_w$o=?L0X?~qFG?uVdR7cU@d&vo{=4w zRo)p$$q&ZpeZ|JBnb*-bh>y!2j6~i;0+W~Jc-^jbI%ZXdrR$gc_ZWq1%|QqD6nPsH zPAN24m}7HBJ@kr0FTSXI$lo%desPSkjdXCo;Cx5W1C!TOhb!EJXX?F{(6fohbgyIp z(o*?-5`op8p=~$0)h$OZ5MgKmYjjvfMo92g1E0MOmPyZkNPxAyUF&q3xm*~T9Q@;s z!@q3S7(1tBKBBVF7>*lM`0KnPtg3rsGJ*b_*_b0BtmR zPt-Ae$Ud3jNdP%w{g4&!Ov~CcIHt?VSRVTVp_j{vH)Wv5cN6GLmY&>f z1MAq_2FH>1t<(`}QHX;ku%O3fMjGUR?fGdI&M`O$Z09vt@k?vmcu8G!KAYXx5zwm= zl&~E!3Hd6-!ZRJ(E4bJJbkIj)y5%hM;42^4T@#}q^wW<8d!W!zE$22Lp~Hpo49jgG zUwTBoJlIjN?xOnWs7aY4tzY82B=8Z+rtA*w6MWPGm~WY{XW1u0{Ibw7Xj-l zK~dq+boPAPN5=}cq>R~rof8CQ2Cpe+llb&8w$iA65&(zU>;P*!SBi$iH$ee>KCoWr zqBtyhr)U`u6c;ob&Q>w2V zdUyylwL8SLqdQ)#Gnrd-*Qg48YYCv@|o#Wi`zSXz-l51iw^uFXE*xCw(V~F0N$4)?Odu=7Pgq}OaOLqG53zwSLtOgc6dni1^{YwAJbauSJg|q zt1Y}KrN8UJHEHyh>Dip#*uu6pWqZ2D%wozoEI$LH+fOtYV6bk^#EGi`SS1*D?<360 z@rFKlmCUV=-=d~j1JS4WCYAD<=hzqef2r=k5|7Kak8l@*-byGBlo(!|MxC5yJ+7t` zWH9}Zivl>Ha7=8!jUBX9F$N?OfZbB_^wP+(=zVlC&;`T^plA#ml4<@pTE!$M2__;Q zF$w9r!Y8s7yG0YYy3gcAk1`#&N}uWo@RAX`Am^L`z*d%5m_L+(^w;I3d3S!1sB5WRf(L_3#?_!&mP z5kxB0!PC3taYEkQAAqyapG`1rJ*;N%M$2{g;-OHltFfXJPELs~XhAdQa~)o#OGkzb z-u!mcanjKvD94FAvWX4da-5PMwQ}`UrU7IH%9IVRvA9mi^xX)(S?nDPj>f}n9waL{ zM9tCbC1>%AeMVuJC>4Yq^{_^8ApHOmO0N>uxUZdSRn6914+I#z#ORT}QQkASnEK%W zKn>sw`R6V9>L&Rr9290X#8y9scdz5W;K*kMDt!u*A2+C0j2%(raAoqX`Oh+!xg(XFO#vJM^E z;3zAUI(D>kty0*apY`I8J*=_8AqRUT!%&Q1J2-1(Ie}@{HFeV&y3EBWt8ElG0B0Z< zB+iR&rj0!gpLg(C%E*IYTf1os7ymj*Twyp`qMTJTkFHAR3S#!fte2@%0V<{AQj~pP z{7ur7t_s3#wnC!$NSlXwoLLfF@k{^NpRv(llk%8?j4#Lqz)se|X5Q1zwc^0*2e5=i z0(s@4W)}E$i=00l8hOW%#WbDSrgGXzR}$Um7;HmIsjyqNm-udBuL<}j(#)sEe5*K&*a zNrVE+D|m=f{l#&qUDyTI4FtSH+eNeU#TZ(tv$2H*s&jDieBuBCY1 z;Je&={qYD2OycfSa~y5+NY68+`~XTR*843&u?4~fF5wl$LrV6L4_R}7<6=7(tC}sh z7c6OjQu7}vw+Drw2gZZk%dlP#y-5AgP}S3U%Ajn}rvAS8$zA6Rg4VKaKH$WjfzD9_ zNp(&}v;=4Iv+V~j&HaTbUvtDCJ6QG7{4x9R9tWpPuTKJ&XkH?sMP)=_t=@xxb)AhJ zcVy?cb*LBf+$*mK57>!1;qTh+i#19#nKz{G^Bn}<1fsBQfkqVtb|ZVOmt4b>fhqr- zxI9K`<2PMYKX`{yvpgKjBjpA{i9ADigt% zy=%KSmdR0284*g&r`x$$g)HX{6vdc4srhp}cAb(wxl}*X0***|nOPspg(gya0@19t z%3vHHVtxyYZOIJike634F```(tev!gC@?Od&THr_Hw+$c*JtK1@?A6+IKQi(O4kLX`bpA72>^a=0b*4+kRpQ5CSW4p&U zuja{uT_movvfA=7+8vqoRu4sww;Q}v1rcr?qGc} zF{p08Up!c4~ zaX5{4qInbUB$pM*`v9y^ZltENcCJevcnX4`YA&rdIrhW z0+GdUE>=v1SF%uGzUubc&|J3~&H9<)+c96c!iBHG7upS8YNG9hJwk|A5tmT)O$(KQL>+5XXQgN60xqJ z*;wFEx{(|^Ab2mglgL3B3404C`Z-l`jwoMPC&#M>uk{O%V$1eM3`pc)oq6J$@kV;}!c#4&kWDp6REUjjI06g=FVB;qPnd<9%+ zCz+{5+J8tuuyib}%ilJ17|olBR=yBH1fM2RUkjF$7O*UJjO@i?5{mJ~WJ{Z}v)qFu zt$ByrI_6yrdR@50R{zKYxRVd1k30k$c~EQvF~pj-w{x|K=yMxFG=_ZYj%#-!h()-)LF^DtIKpHiSDx|U#l}}c_KMH(5F}Y-KGoJiZ)dHE*xB}3 zEa^l_y@2|nl#-MN>!Om`Y;EUiU13ZA>Li72KV!`x*qscj(NLKB=Z@23dJxGW4!46z zgwYNp{881#lKYl!(G{(Xf1Dg*>GyoA&%)8xQ*-vEerTV}SLw|{(T^DXdyxr@` zkBzTE4+^zni^&a6ZL7c7#&Mrvtl=atGkB5p2euS_{buULMd3=vUS@t1d5wO5` z(P!o4w9RJYV@=|_?FDZZch!Ov$$}0->d4Mm5LMhIaZGu&3= zDv6`OextkEW^rt|(6(`Z!Uj4Fz&??2$e|_}@4^30lE%+fd)m;>)gmHOGK8s(aQ2~c z%B_f9lwKiZ$+kgj?&Yi=Mzmx)NJKjEc*)i$u#qCbbPzv$ioxzkq_WN?a>gC~kWd@k z>uo~xNy>5@`6764w3AX;6~y2qyyR^W%>q)skxxdNv^D^SD~p2VFbSB^Me-doj3)tG z{3aw4Z`zwM5WEr_InBjKIBs0O z-fr;Hx*hPc$qCf%PBp}$+$_Z_H1#3|mYVZVlJq(lpi(cVk}(rX!nW8sQ7K4j5~4YG zoTK1sn}&Y*EYnHyd*qXse8qQ3$RaRvkuwk4xmw4##Cj=*8fPctI=mxAR(wGjBf<-2k#a^!f(!?IXxpvZC zt`dQtlUTl(*yEjwSB9GSM6L0XFmB|{W#())ZKpT3#l~52Zd(L@-Hc=Bjkb(Ei?NHt zJE>u5FyMFv!5dHt4*@tCnt<8P)grDm3*3_?K*E@Qg+e)|SwLf|^5P7~`Exy!&p9 zqe}x+yhAU>K?xN>@qj2P2jw7?L#!5g(>Q9x;>WQXx8Ej(jDv0wPp0nSoET}f}r8W;IFWh zzOYA}9PExfmDbM1dd)?GNb#fK)ZP2|wx1=x+luFQfidtN$?UNdL3c9-#wvUuaZqRm zJSSJLl9^B(Qq5O5iLG#09Ui!}#jqa*z}X#u-uxEH7zsROq-}L6r|9Y0z|80&iG*DVh&GhH-=_0Pd1(l)K2k zGlqVWPWZL0aghWJIpxK%BkUq`e>+%=O=h|&1-Md^a8<#4_q}by_|Gs|#m@fhMJIOz zN`vZ8AJW;-GA4Q@tcTK=jTr@2 zhnxt%1bZp~fI))BY`5y-$f&foiKLW6TOka;3x<`!BG`y@N8ZVe*V_+XSu=COPsd-1 zR(PohMXesgzq5yGnBz!HIw-u(ClWhS!>%#6u!3Z4bkHxY*>3d6d`;#ZH`?&^F`r1% z3eVsz@Lokg1hLhTb6~@|P1mkB+t(2@tr;XK(c|bHYs>#2gdy8d)_&%S&I6e}6S`*( zn9O|fg#jc$2c$riXiT9Zh7cuAgoI-+7q%6m`t*-!%nZY^%_$uK-cx3NfYYUhVPeg> z0@;_Mcq!Myrvo*Sj2l^&x2A=KMdUKJSj%L5`(EIbG#o?2-|ZvDi6aQardkh)o1lc=x)j}yO#}lp(jSK z4T?)R;JTwf`^S@fauV7#mE>cAlMwR^x3Y8qc*AlmeBe9{gB3oK-PmXNcjQ>V8Xd7< z7eR3#Q?{`k2@A_&O=jRWJk|qy=@*2rgUt7;aBd&JPUgbXt)4&x6T%M`PlZaPLDYQT zAzR}xQGPD~EhMYil(QqkTTh40oQTEUx##{Znuqp8=pNo;_E`8Ha-Pj`qI~y>BCUsN z8|xQ}7!BjX)`!!u1BLTT0OUA1ms0| z^TrsBnI~EF+Qy2=S|CAdcDBfThe`7}qevh!Z@Jpq8NB5#gsJQ^HBc(rqc=P~wilPw ziFSaNlgad8sF$TO^V$89aii)Xfo^mfe~g?l_BeH|1$Dv6>8QeF|CxKqHDiKP|Vi zhL|Z(u*1-f2Jaa8oQsvHRuJ70uRhrzG0y6@)eTp@4dw!Jz|?A zz+c~~;O&k*Jr25AyBxl1HB~|x-!OoB0=A(kTaW`aK>Pzveqtkd`{Gy!9A;v)b})F2 zREfsb)k2b}8(Yrq$-BY56Gqj|VyI{a$5ByvBL(j%_~;ok_c=c%I{M6Tk`{eqzan7S zk@u=~Q@Cd4d9MX$HLR&YkuvulKK#K<0@CoTC8*WVp`IqECxP?0EM>)mUmpFhk(ywsKQi(5UHUv)j8}?WTsmknQ z$B8kmn#0g)9{*14u?X~EA5u|q%OG}Fa;xIYU;g2lB%@~KpZ;*ei87zN<+!ToLN*Y> zpjCt9VY3e7i^&kF1z0sIiknA1(UR|Nij9_SfH_!uNS*l%sdA z7S}fj4$vEFaYbg4A+wbgk()xQB33uzFs>1wURKzF;1%T3{1zyC8zjrz7AVc7%7Vig z{w4W7$iBFQJ-|t(+bH1i9)`W))qrt0L|yOy|4vhN=YUf0Ki#T1j5)w-$uTfGT>7pI zh7$go_FENI4Yd%&WAa~p{QAL_6p!e|0tX@4lO7q2>#hS_1~QwM*^j06Fz9B!%u$*c z!p9>^;0tEmAT&x1RSi|;wYB9HrL0k%T3n1g8-fKjmkgTcum240=gU3$naG~nQQ#G1 zxwHx5YOS5r`J!3y#5_%D7=0%xgq{s31evgF^KtTUegXGkH_aon?)4Sru$QhVSl`L4 zR!!nm9=`)m*XwcEK>&+ggTWw!xV-k6wCv}$omscxL=Mlqt!KIBKc+K)R^Fgkx+Ld0 z$d?`NS;!5$nBh=DH*OjZMq$9KD`h@^1xFD7tAScwwq?+DbgpKMiutt5jU5VJqgD<)aVymt-9U#uE5TIbGl?XAsGjKkj|}u@p*7{JJ53F_KXn=d(CXDVJKe z4ZHbI(Btflit@L|`ItxO$O<7(C|jt_k$0*`0r%?mY?E18SUzb` zpv+R`b-Km>EaewbUqXzA*Mx{eMWFchxV}qu>?6-;xA`3kUSX)Bp1V$pIw?~M4*Wr$ zNsRFuDVb7#*J66W%#C?lbg<`+zConev(pKn8~MhTOC_7dycH|$<8ZQvwLcfW>3@~VojgWcgZ%sX~lZoo{6Te3t0aZ zRdTka@#@c3V@v07)uF5zfi|L~var-?sCFQh39G70aI`St$|(cFxC&3u|g3zheCjIYCmQ;*&^knpmw zEXDl;AS!-9zX|i#h&%!waizb03r#(2=08-xE^!$wl{ylkf~v?}r-Dwsu?Il++EIr$ z;EKCJ)w95F{v+U1K6MpiDYEn<$$AzuDkOsfK;!xb>O7(L)_thgiO3C$zQkp3b|`p- zH*$gmgyKpkwK&KHMHCy9!^sW3;bw=i7X+D=#?lWR3U9*G&oKK7_tu(oi|ybATzZ}4bx1>&>U3Uo$|Ctl$rUKups=bTV z!ds;B7OMPwt#iOD`Na9LiPTcPK^ZJW*JcT>w~TL7 zq2h!iTJ6xy^A6qcu`=uY{0V(!Z?%VKq2)F}f}`m_wN14~i8 zsDr^PyjI*GCzWKtf$`KFWWMBCZuo$Xkx#YT=b@j=50r6t3Ai$eenI$tEISytau9cv zUAh?U%V{CUxy5hBnV7^}BBX9ZSY5FEtDa+Y9xu>6E2#Yjl)3&RP-x{+lYbWOrysKVL|vMfCh5xq)}vQhVJ9U4DTwmJFtGA~`7DNrouLc)Bf)@{fnUO9_< zr0vNI%}Bf`8ggy8xI9P(Ic{x14CHgXhbzY=;>sw(rZNxDO&}4^z?#LXb`BNCGen>D z1l^|DeU}(`FPWSZa`>3{rLy;O^HJWf#ZI1kzr^s>&-pv@HCB-qcy(738Ah zP0N=M#pHFqv`x7lm1^pD8cAKI{0%t{Z%zi|aYY=HYdpq}-|So>KVB;Ikf`&Kr{?yP zMX^C;ZFwX~n-ca~JhWkUOBm=!lPNPYoQb`tp$xWuj&y5QEp$^)x+h134WTpP9G`+@t={u4E~LT)&@xx{sLOg;n!Z>Y2aln4iJrg zE*kbnOhQGR6nnwJhBr)yKOtO+ry>2IcmL!LGavlCnmBi6MiS4&jiVl770Fh}>G0&rceiRf%JMJm-gUCfEP`0(%6+KPyyOpbJ@zPSX2Wdh07(}>cVg4njA#PgMo$|ZHT7Fv2tO3rXB=Wtdq~L!>`TnR5$IYtRhxo>k=r=g9Hfa`Z z_UrP8Lb>##D(q#p1q|b~qz(#3XM?)sP_bZQa6md6y`;2^d%?oiE!zyIdXY1KI(+2x zjM>Mhog|UuU%x(Q$uWH1z2v&tJz*M+!rj%m3J-0(Hdo+(`n;wHZkxy%wM5qzFr`&Qz z62uvNz#C*SaL--ZnLS`C8||nYz)hC&#TXyV_11AvdkA>B$S{x5G=FF}%Yu(tDJ|jmBkcqP4IQL}*Mw#Dg z>w^u)A^73a1`72{sJdq-fY(S}6_xO-lY+}oi644T1(%*hZDmcHmjtki8>mY!L}BXUz%4*}4b^Mg#;6`GExVhCN8Yh0t^Z^7dkF@)J62^9_|j z_rg)7o)+1gqVwx2$^~Dc0vyhf9>Z$%rKP2A{8!|-cZGY91K)YO!HUCQ(mYrt;s>oI z*bNY$H|Pu^fFI%|&HM;-Ldg_bI(Htm3PDf9Me6b89;aBpO>5_%%RZ?zY2MacmszWL z{LBy!FhA)N_m|qdFMZpYlI=I5fQDGGugg*Kk7&&w(`C;8<=F1M_sj=M$#4d#*!My8 zI%IQxND7(i`fMP6zG(Sd+NrLpH#e3klh~7J=)eAJYRwHhj^jKZFIh{XoNreBvi8Zd zXV30FeE9D-M;{z${PWL$|F7koVFIdYWcxYHOhwG@v}`V8UVcf3@E$Fx zxq%PD>7B?~M|@mPc5t84n8gisEfKVL<*r+vXu_BkzWqpqsf)nZ$1EM}wEY{(}%k zf*t<8`cdfQv4eU`pDMgOLiedC2waArYs600eh}AZAQp)I{%8CDXYWnmqPWtv@q;vU zv$=o*CMqa`BD;#afQV?j;4a36c0ol&6w{({!Cg@_+AZBpH!dhD?#qZxMAN}m@m#W>D|3 zQ7$OUj*i6L`O?@i{fL=(CBwhKQupE~V}m3~XeaqSRRmu_F5=Q56`Cd!^hD z!IphwPw(ZVV2K_&Ek>sIL=?Um|Kj|EXa$uZ?}-xHjXzQJ_+m9+H+#HTM<#=`(QIS_ zt{A2{#VS@yOQhoP#ZYRxoRTQD0Ucm5fq$7+!kq!}?-t9IFWs=(E?jUN)YnwQMhqrW z>$kYJ)A7mFqtaFRikHU^9oTymmH|Ataqodcj^zFR-O~Fg#5>TC1#e^OMtmD+w-Xbs zS+6>X$jOFEVdyyST}Eot>>;E`0&>tycH)m4ER=n=&G_g{z54EEoa1vWn@&y0Wt3 zGQ*9^{p>M^wo4VD=oqYjg}zOUCtrVNw>3LM_HY9?IS*V)z#;gW*No^xJB81Of-X<7 z$C;54!d->b3*MoA@&0we{-bYPs0ev+B&{}>v<-Pody`5e?IY&n1=B)V)tgv`sahOm z{oJV|&qVs#u*6UWJtab+FXdCPT!n@C#V}+jc*AWZC7rxHwa4AL?-R3&nMZGW4jFpT zMx0OV7E-#HP-A^@ybqYW?Zt(Tav+Rl&7}v)LH*WC(o{fEEZ**`J|yfs`pnL>ASOo+ zT;eePEQ;hcR*?{{Oj209F@njrvD~m&Bnn_UDWLRN^sp(>28L8bpGX|U(BO*y-qKHU z0@)S?5HA}f?=o=*YN&E+>UcWGtw}VRpAsutP(sggWD$1`DREH{f^}|`k}g_dUv=}& z2eS3LPS11@<~p#_$kOMK@{M_pm0O%fby_Ut=y{wdmC~C^Vt;xcXHzcvig>@m&z!q* z{^r@UmoJ>?yLapA1aZ2SO+<2MUQvbQi79!pLjX$QzaI@l_(T|+gcwJhJ}*5s=xv%?7N$_WZO zc>(WI_uZfaB$pf<$QH5*Bz+Ual=Rx;Zr?j6cr4Nobw1PrhnjC#!LrEEtT=a1Y;Lr; z4C>Tb)*KpmVpXj~W2oy!0>?hMj7<#t%Ddsc|M0JHaO=@3xuDSu;uc3%-8EE^y5S_9 zjpVKBHtjjF1CN;tOW@k4Qs{vay$|;?C6tE}!_#WwrI4JkoCz&ct7qon5_^ZJrcTXk zx4AQWzY0kY8eu;nhRV=iZHulEUu|y^UD3Qjl9KLGW6jCpH&5RO#b4zBumok2N*ak+ zDtam1iE{5@`A~X;#h{leySXHEEZ~a>;mzOkmSvD&W0;j)j-|mSz1;XM!8qFK23*?5 z!Wk$Cz6koWp&9nROCAv)J zKracuS1v==O~#R=D_gC21M5wm)O4pp`^VyC3nQi5`IIzdG*E{on>Pu6S?cWFprkP* zX-EOytq~{e9H^GO!F_2h?=t$Vao33xSedlK8Tp_W3zJHr2a5Eox6toQunBt|aPdd| zJjwB5b!wAPS1tIGQOLaPMMrH}n&-PQJ=uXJGNzmc%-#*h9kDmH6RGOHhmFbH>goNz zdqYoG=nPWNj6+_=2&zLZFj_YC+omx3Eo>Ecqs1ilcd)({vH39WETNuI0H&kAjbG?g zl@?6~1wr@|jCO0YC1L5Y#2YVmC7!>H{63fL`{A4=%90n~kO_W>`KFk)Ozzwk=?UfH zjgCNm#_<5GM=I3{N#)00@=wgbID5qkLsy{DAtF)}>A;$U>}36MI2kec>5q8608}IM zsU*{TZC~2t!pYsneM)mT{)V9y)NSfY+z(Gk#O-Um>7@l(cY9ieh)&H57H4|MibheH zfZx*~ZewOZyERc(*TFqoP=EYS{Rk0i_mN<# z3Gvh#pfr#W?oHbTOW^dC>@FJn$<9w=goTGNaS95>3pw+XUYtGv6F>D1b5mg7nO9l6 z&E8h5fS3+tMOiIb+@=KH&K^z1SeX-E)*TJ!|}Uev{W6MEk4pNxGJV)E45n_LfhfLbx*~awv)whkJ&OG z4V^ONCDE}lBr%Q2ORKRvgYE&0dKV0tBly`OB&iI#TLEhc$cRX$fgu+?o~Gw~&~OE`?zBA}w*7sKr|v7E;hCce?vHv(G zaPq3uvZd|PiNtY{Ev`_vTZ_VxXUzc%k&|qvL+)C1Rr?1n5;Z|oIy|fi>_(Z}8yUF~ zON_nySx<#!tLu%XWtlSMB`Lwl#NT_A-Y6OD&N$d8?V+I#i)}(lOPZtX7TvN=ZvGWI zab<##rFqeLoZj|JA(r-CNhHV+;>dxbxrW4_wQ31O&j*gONCHMwoU5Ox#al}G&(7W* zJNt4k!~kxKPfu*YH18is)V zd)jx0Te^jLE^`Y&^+8EQC)|8;uow)7uRe)-N~ux>r~eL{C$|`-Iq9&V!X7&H-dNGPH4y7f>C+Hn27zJ}QARbpv8<~o;~%@F z#8PXpaKGqEig#CRdTZ^D$MGIa_VS7&@N9#nw%GtlWZ~H)`uc9i1WR*$u}aYEO>$fz zPQp{y-^A2iv|d;wJ(7_@*p-n#Sgv3|k@J(kq1Q%|%3p>UV9n`kbI zZ`TV2Uqx=~DqJb-`o;UeK}CSYRtvnaYuNgP6`HdB_NN@G7T@D^dovAP!oygmYkbtp z#q^|sRwj}BoW6Mzs|NPfC zU%vOzQ<4J0EjuzM^ddE|@&splre(I^X`@}0RfpJ%x<$kSQZ>dD33UWE*mnejcC_$J zJ}?x%+IVqFj<`~KEjtf_UW$NNGZ*04?l;-$c@EB5Bqk(iQ$od9r3)HI6=*CsUT{AB z0W1@;u!)AZc0^nsLzm7xdl!zp?S&ODRdY+-4Q-@{>kX^fkGpPi>((G%RH|{OvD=^; zDT}4(&LFnF9cKF5sL2{>830-0i(vHd_OPO2mCqBx1C?QKLY*$Y>sKe64 zY1yNLgHg-u_=93#uf>Ip+OnosFNMgvj!CBAomT7V+Nzdi4$oRil^CVydzKW~W1f<3 ziTwzVUtPZJ(fp01XVg$^McjhC;)B>gzJ@bbu3S0K;mSEu@9kS0`bCV9wXIwP%C~JA zgZt!Fq2bHjHCtI~cl0{8tb(r)QC!UF14auU4#w#PH`bTFIlWfrjv2SJ=;h^=f$KL< zI!Q017UtZUldx#mxi5awYlG$juE;+O7wzn|oBdMEr@1Qzn8d-yxS)m88(-$P5|oKj zPrepC)C*AVm0>SmA(mJ=ja6%$-Jb3i_x6H*ieNR(mD;<({d z1FpS)4r_7!q}jffCE|Urx6rnP2`c^xaKh{0QrbmhStY8>lW@2siU1)Ut99{}j@fIe zi09@}h`dB!htUO{_?$}U1t(q#$EQ7FF7;XpqOU3(o44L&HF(Qxa^}mEcRPB-;kZcK z%S7B2MNM+p(TDs-Y@1h!lGDmnSad{GMdMHf_s~0_6zP7ufR7N3Rx!<7I}q0a&;xDu zXr7P30cy`sf8{RP$!*wNp?AVws<8*E#T#fA`s?i%)nv=2eyhlF*Wd?n0k=0gz$P!g z*zZ+RIfDD>;NQwh;Y47OGQjiXsf7q#1OK@sjbo2EIjw@-cr+!VVxyS`fxc3c?|gXH z@+iMpkv$f28^{!(-x%;?u^H*qV44TLAc>MGM6pETek8=i{jUAw-_usL*t60%XaDb-;R_n zMKghTWq=*T`e2+C248!u(H5RvurA4Mw>b6|KZtZu&Z^aX&;=V-tSY``Su!}t)}DO@~z_YNPvYCH9X-@<(foyOK2T`podN{&w@dq!Q79XsiUvoJU?ip>L*F(m#LG3JBcw?v zU-0lx$lMfBsU4TtGgvIgapv6ln`iGwY*@5${_M%qYusn0c0bG5M2En@(vA5Bko3v$j zohGx*!irGD`Zj4G&{IcDm{){{(NDC$>udW~vUL`@-CA#<5#3hGzknC#09L>ssD%(N z=*-%NU-%;~mlhmS2s%2Xmv83s79As7_M)qES2xo1O?1HCZSLudgN0Zd7?7!vlVrmL z1r7C%6WdZQYuie^KsgbLgkby7XNx%QHmFsOiX?@E;EHqUso<$ zz53Cz%4*@SDaq|fKNZ3hgm-?u7nJB|+;%kNjIeP#PQ}Sx)zPj8{*#$APvEXt$6zv9 z8=`@y;ndef%s29?%asLv6#^C-wz)#hC#ZIACWLT(_9~jbgF@}u+s~oyW_pk@cyvtG zh^AYUckF**_NH(lV5w44vcZ89>hvX4Mqt`4)VqopM)Wt@x)fU}_SPk$qTfzeirOZ{#44iz@U3q1oCA zG1SIrWL^A&i*7}t1`Fhnl7-^ZkSLENtX&?bwFb))mkX;Hd2u)gcqp_TW6G83R?3~f zdbgGWBfB0V7;0W-G9x5}zceUo%i;?t_qe#?gE7{Mqx+FLqK@-*Ado zpTD?#`R-5twqsT4y!920Wo*E$zAg0aAv8mo^*gk=-DbZDLBUl4)-q({q*OM`3*yRb z#A*X9M?aU7yeF8J4$EDW_ozZ3dVHD?8?%dcv6Ym|4zQQWA-|K;Ka+cGVV|i#`!%;5 z5&OkXENFY2FjmFLE}p)yk4(@t@FolsbLw45lAs^a{@QJEQoku+5i-C^J7A+o-M5Zu?H*XpakX|bXUuddPHv#IM2-z5{SVT=Ch33*EbHi%!6N9AO7O8ArHj<* z*SK6(uu&oG(9zDxPC{ULJie+;@*_-^%GQLCz-jz~eL3(rzhv=5?=_E_=-_#tFtFfA9uPvP?hXf`Lq`FqK+my(gj#v zQXQn0DSeBy&xoUwISE)-Co5ESmE0xme>CrEMMc&6c}r%mTwhgDb+vruiqcI-ic2hR zWxe$flG*-*x+h3$Jfe?9PoW9lA@NZf7CxpIhM07CS4wCK$|8ERg!bR#*GoSOWB(Qg;G%1GxRAs0l!72>==jP3L%nMh0PA0fXEY3YY@Dj3w1>A z^2TbcV!qQ|Fd5I`Zgk9Lk(Lbg=P#4;lIPBHIDbkag&WRXIe+%@{fiq8t-4y~rr6YN zadoBpUz~h(cbTEYQnP#UVvF;F4|kAfIjwe)#CZl&x!{wU8;um+WP_r;&ye!p>^VZ=7?+ZzN{2(Fb?9=D7b-;ce)!R9B7oXHyJ&;poa4(p0V$ekN+CT z_F6uBmuY(-N>cm2U`0`sXa6M9IZqxuw2yUYSk`)FIaV_zSTDUHXxZN)=fMfqF=5UM zwyR{JC5D=)~(sHc(*5h4PBM{SG|ALysCxO)zys^ zSJxjgu#Y1LZihn(Z$P8er!rGFw|^iCmdUuA{yI9KJ5dmnQ%Wrn-&s;f2!kv(CLO~B^v&p#RciQ(J-HsqyHExmk=*I+Ksy8Bz%^|)b6F0rYRf&m#T3Fs z*06MxIXO_IVII`uR5|4mh1amye#CN}%N$FWR|EDc1(H895vAX;l6ShBqu8|pGo?A& zd!IqJ%!7|eR!{0IsipoDtO|GeGWW54yTPvVbg!$ysLcEtdSl7gLG|8}tkkiJFREDk z0qUr8LTU{`z4pW|>Jh7o+eTcY#jvonjkmCK9+hz<6f*3#i*1drMV^?f3aId&5S#wh z?64p31y`DS*-n=i19mC}lFU8~72R0KSR2;6JIs#t7Ku&rbc{+Wb1xv~E~d>w;~j0{ z>Q%_TN<3SqkcnwSsU=S+n*}TUEhh`!{F)cnWNop*{wosn0rFIxu=5&P@5K&}iJbv% z%v-%<{^ZuV81x2<#iBDBiVyEv|JGJ7+{2D7B722`(WkdEyNVc8v1|JViNdRU zqF1-Z^`(Fplmc73VHgUM#~t|$o7^90>*R_XY=U9Sh~%MD(JWm9V`fdy-g@h;ZMR!( z-EM8$wr#t2ZQHhO+qP}<>%Px-en2LZoLtEynT$~x?{Tw7xkFOF3d3B)wAudHGg@ou58uun;wdc_r7a<-0RF{i725n)Z`B_4Mb z2W3recclI0PRM_AGjGX`(ar#wE=j-ke{fHZ>St3qv#v%O<<+TEB(_*o^HW5Z2RrP) zto6pPQaxEzR|ubPO%-xHS!vG#^Y1O3pgd0MQQbC5avr3&4i96tl&d+&fEV?i#$#Pu z1i5Z%p(=%4p@MoEY7cU(%DcfyjnZhfxB_N>1s0$RwhqOIfp^Ic!#ux05~Kaj?L7-8 zpA@LFY8iL*sSwj;`s6dpp4C4)873S{!;NJ2u9>Vm`uP!Y3Pfv4qtWU5X`03JrnX6* zo0>cVk#{|^Ai?$}bX7HmI2iHZvsCZ@njQbVU`bTDbNyPblPo9UPPsddEzbGkh%f zK2MWGEj9iERaQJJr-}}PSti=J4w!0Hdb(H^m;)TdTbdh>_E}7LO5D`!AHfX*VjoCu zT$dxn(>_k<1a<7?Yo6HLSbcURsE3NJ0m&GhjD5d{PF6?Vj-1tU+geRO3U5aW3htOI zwB=iB9Uf07xQ5Ek9Gno!2(G`PF^YgNWQs>oS|w>9$}JXcYelQ(RAG+2!&o{fy)m{0 z2f9I7Kyx)c*kp~>=(kG;F6)%x6*nbJc3F=jhlLI^q<7#_nitKH)l%(Fseq|c!1BCU zmxoHX3~lTtS_cnkoj`-5_~^pvrC#nlN!FQu$79*76jCdyGgG_Us*-$l6_ zbnM*nDle6}gV519)A?pWUT*w6eHae%vtz*euTgE@-6xoWQ0hq{J+2X9Nvg)#)<&<3 zQpEz=g~a3E>i`91xn9%8^icDb1}jAXF9nIvZ5-ecCO#2vSyZvD_P6XlX1Ch+1=gpJ zo@z89;?K6#MR<;u+?DR9ujtk&hoHc;#41hW9O~$uww1(+Njfs@1R!A)&JnA8w+g@R zD_ey1oA6~(fCbhJL6lOwveKmc+01Ri_je|?> z254Lf#_cr>TdBujcin7l+QW>;F(pPlF?G;L3N&8DN?}|b?kkX@vXmyk%*VoqGPKIl z=P*O7Ay?6}(GQ;qkfYANLoeUu(uk+i)??dyR$u0z%I<};rEm=8@jax3h^0K8`3|#^ zl9>v!#y?c$$ZIm~>xa`+fLot=WEe&pQz+M4QgsU&_)lWo+zMD!@9*LcmXj-5@%6-& zsW#=%hNGA!kV+-`y@Q9wvR!Ll_Q z`zNDTAz|{V#2J%bAri4;Jce|z#kZN+30LW)YfgF)2fD?yOg_j8s1ni~MVYju=C3M0 zJM#e$dBrI#0e!vZz$y_d(1$ARDJ=gNWX|h7A1L3aXdpgFAAKQY9lj zPy;f2aV+tsgf1%I3O4{faoG-=OKKHVT}jn|>(gdnV8MIqrXo z^6AeM!R`;Y1#Ncu82}riWI^3yFu)_87_H+5QM$#X%KIyGF~_AN?ytKuyN%_2BxH{w z{4@WfT+en8bsUN?hJ2=fmmjCg>qguM)ZJn<%p*bdKIm5i2YIP+P}%4>v=SvaktJnW z5l9@YA{9zMYOR?)@Idp%g50ASlI@_!_oxb!DR!|ex^=aZzh)`-r)>^9&w*4QFtj^N zv;i~iMKp2={P^xDDJbRgFhZ@9CY&21VohEdvA^UyqyQn*s#^M*C>?-kGx2s$XR3@u z^J1+dkY8w6E3~oq^Xg~sFn7V-!2}QFkJyC`bRs;inx2eIpUY>YH+d5xT|HSw|q-?IiN}aAnLr=8wyO?qQKmsqM1(Rd)ejQQiVQa$Vvp!BR65+k#$l)UB6Kq<~rX zbdxXv0>C0~^D5$&2Ei>tnygiESVkGVA-33?sXAun0oMCIy( zt%r1=J35I43?bq2nlz9;!%hJC25bBE*F3*P!6vhr3LQmG|3^gqYxB+`Dc%lc=Wu0n z{;GLEd)hmkqQS@+IP{o%iq(=)`_4Bh%mE!?Ee+=x7lVjd9W@$fNa!9g+{*Nv(TMc0 z>HD%+BYG>cCSm@WRs!Mf}ZDzt-$ik>(wNpnY5mzTz%3>-`z&PfS$r!#gbJ~<>X7h_m298IE zVOh_|tHOX2ZQTt1gfRp-kEF}mF5|;`uwaHWL61m4i<_vgphOsfb*6?=dxb!?(nxq^ z+bE}nlq{{?`BDz(#E59gG1oVW>0;AQpEDYQ)qqKE)#gyGs>?}2Sno={fHs=d9 z;4{Jj6?@24qlZ~Wg*@L=wOon*TLSmW zb4ISwrY+XwcTU%WLx@9<8`2dIrdlK`rkS#%t!T)Z9S6z3jtwj(_Kuh(OYwVfsj35; z3UORWjp_$tqT|uI~1jaZl(~DSXQQNXs7nRfuiS@0*`7 z=b**Rj2i1cjyfAO-h+H(&ek`t65GLg(XmY_=jhp zOn=XuxTGb@YR?uP7UJsv9MvLaS_qALX2M=_p~roE70}7itFGSXcAT2ih?E1pUCPRMTprDoj{EOl9MF?Q$d-t0&0q^< zily=_-i>RVyQmvpkG!0YmnpQQOT!2(zXlQN2vC$a8{=MWhXd^%7457Vy80^P;hvae zbuevI=yh&U@o%M^>Y32O5d@PR{EK37lYkI};16XF81W ziqG3nGrv;qI!%(WpZE_e0oL?ymWq=}+B>9JMh+FQOCyeIaBrNW_p=+-h;=^SF`O>c zv{vWoV$zC9J!6v|O`|s0=!fstZq|D2xQ#W`SYh@y6?fe*LR04^uQZF|N5ai)f<6K0;8Cz&8%<$DVP4&K|$hinv-DQ);d!#-^_uDG|#vj^Lv zuj>PCF7Hs&l2Y!cg!oQDZNA+g5%z97tPZlPzcGx?(6EZK8+$E8neBHwNbjH94&chVsmZ;7aAjF@vczCIqP~Rbd_W#f z(UnMjs9voHihurj(1y*$%jFN@tpTFGJjt!YOft%!bDVL#z z+PvDP@fpy+wkk&SvcD>fK^EK0Zc;bgg%DaMpQ2cf>YxXr0)DU-hgF?)<45_Z+hqFi z!{o98cXWJAn6z>>b}26X+6IB7=g%}+%VJR=ERgQF%Z!0zt@D6z27B~jI(X?%sQH>S);530)AA|IbOe_D`J*l5 z)pRXoqiPou9W|gIu37Tde0>~nR}~PrZ~H0ETyAf1+6zDO_W8Q}(|isURqIFi)t+Fu z$}qB%_0PZc$7X+Oz}aw9$^EeZ{l?1H>6ixw)AEDi$1O#ov9v>^v?H_KsdlDVKkZW% zS(e9r0ZMbbv;3n8A%7d$;c*Mg;QN^LmOsng=a9FZN35cYBIf<&#=f(Slz5!D$=ND) z?(F;76M-Z@=IH|Ox;z}L9NUKg_s1C1(tE{R=n`p5?UR&V4Hb%u<92pN88)a239;qfP~S$LpZxK<$`eU+s?6)*Jk2I?JhRiy{uFr*` zEDx7M1O8xk5%T8!N${f2J6uu>UMs&OQnu0;bzUcMPRm&AsdWTlS8Jhg4DO(~$UFR9 zWor}|%fQDbqc^zBA1WDr>!CvytmC$i7tWLEW-<^r`LJ^C;X%el-!nx^j6&8w?${(g zy%%c$sTbB`Mx}F2zJ1}OH^$GC#3ShlXhR)L&x{{cr@BC;J+pZdi&xtSJTdYroUZLb z+ytJF#EA8j062=l&9D0r0G{XlBSzaqEaiRtYaNXw?mmr;9$Fa;9K(i7YCTnl!*FjNNdm)Mj*!tJki85Z-M?fFoN~dc9qnK+wbC0H^a%Xp^0O;hEBl;9Q#R^-2KqSb$Ds#=G>{vIzxhJl{@_*D z?t*{ZL#wOy!po7?v8+ z{rF*JNfY*ZE^Si*y#SKH)zvl|Pbbp2?Yj!>LUjF6 z^OUDdvG{Hf<6qC-kQMFRqt@@f$F-iyt&oc?trBiwb4=K*uQIyH1q?;+A>cN5+&)Zm z@lLF5^n=(gM{L_8R#y0|`h(gs9Cb}iBu6P|S5vI3zB;#1-OHD)9(x}Xvgl#N4Xf1p z2THVZ#Rc!{m7}lonKakXpbtL}Y)gCUqtLIL0SwV>pHu%XCwoG_F{Q16Q!8Tz{aBL_ zC!`laN2zmPN4JUxwa&e|YYfGuoXmJ$i9u58FXn5Z;Z_B6>&JZafR3SQEOviT86j_o zDWKbQpTASN(%dh=u}UQTt8S6?-Zt1sV2h6K#!Jqgkf`=9Hh?CcTk+EldJ z4c+#OnqzNpxFE=^LRuLO*}@8=)gjRHrPPQC)8F2@v!K|j*P3Ii-R9kC;B8>V zH7S5>)zQ0HEfpP@2liPBWGl0!xsXA1`Vam1z|qq%TAk-%^X`R^&hm`QFy0&9Rm$+& zSOYENadtlc8Kn93{P>RHUcqhw!<=WS>VUDff;-DDmeG>kHH)9vo)oh$*>@E&?kZsI z;sLa|%0GVC`u_PDdDH>df{_v3JL@hLC$<3|S^tF{!!WdWi@8QLMfiZbOuCs$ht)xMTcYd&ph>sf{q~0OU zyy3WVl37RUBby9VCqUcCBir4c0H!)mY}w(|O*n_#BDyz4cR$6)EW_#@)hZTO3cJcr zjUNe{J9mib$)_fa))idYxPELpE}B#_&@gw5yB6KlE(+k!a43p5TCjOV9%VgQjf5R# zC8Z%CvjYtj+>Um6#9>+b-RxLG!>Hu2g{dqT1-@rgRDYRT`Nu7(mpSI}x4%5BQ0>x0 z+6GGG50^C@BEJHw%(Iw9Pm0fjx159&*mU8$StDV{9v@wExgHpS?xlB+f@jxL{^T*A zJ(aYTM5fT5*AP0-;4`RVhL=u3D{_3WVw`+iJD_sf+>d57t5Qnho+KOY+3zTw;i)KO z==9-BVNe(7*a2=&5`8?|F8YrwL;7}o;{#B8be&*Fp7ay#KA@Et&Tu(ug(GemTLOMK zpRMDpz7m*lZ1JkuJ8scek!Z-2A|dAsW-FT;HCr5XBrVd~GkrXT?b$Ak2y`Y*oQs#? z9cz=UYCRl(3B#fSzaoU_BK>_XX>da~C>Qy7*JXS&MqqzDC$dw1TkYL1)tTuj1o$sC z<}_uWzXl!wy!a*>5#SiugNPp^ zNln|HE?Mn%41Thcve}MeY89JG@!B#jMMLB)ycer*kc0s0*UE+rBZ#rdn!A2Qyakg< zy#!rWNh?c@OL9EmG6fb!VAq<-vWY~_q+?jmnqyBMy)mH|hIVPK7L1;sXy5meS7MkO z$h*9CFE0>q_2SW2x!}XnDatJt%%tV}Hb?42Ye~Y-m`y6Rdq!Ge?j)(7gq{(fdnxnx znI@wD(WJUzkKX<>*Nor=!?*eMf%`l_<>z^N^p*6sSM*il{qcbJRW-DqkcQx3A7Z<7 zDp9f?C2DOxdc{rStkAT$%N)bX6r*xqC`zMjP8l#1>GI1mNgJ}1#f&kjrsS#Ruae>M zq5S$>Y&CYoA&mZ!Weg`9T30QgC2pq;ov(JKeB&KGci;2l%k%2Uhp2I`6bHNOJ_6!ur8t{+Rj&wtu~D6S!J9f?wCy&S@ro6gaF*B+k+|3igR4ioE!TV)r#eh067AaTgOx=|zfwKxgYg@97ckBLn81Bcb z=>gl({DQnmUhB&EfJxi3m{34=heLn8;X&k@vwEBM@EDLgGe^<4 zQ$Rx_ed)V3X?A%IT|TikPu*ip5c|~C^#!F3IAhszKPXCHz1HTop#FytDV-oSxGG8= z*)1+`0|9}5v2|m^3ikVH;6h~}NYc=m^2Ciee3o6&ba&X6#Jo9cH8Um8VEv8 z^N~|ZXwAu7-{qH#O5?WPA*#%|1O;fSgEIeBnfH<^KZ!18uz@acu2254c)s zB=^`t%kJi?+)}wWf0QMayE_E1oZE<-&_sr3cSN+VW5X02En}N!Xkl$wyMLZ*ZT81K zL6eeiEK;vX?|A-G3ZiEy=wh%svo!*>7l1FsAO($tJEk#`?owLXqwB$;6&rzw|8#r#MqTFf6i0$_C8nDg% zDgW+I`p=II9Epr48K8HJ#Nh)w8g%`Bot@{UUm=C)>3@=2Md@Ftjqb9TNP4biLaY2m zeFWq_(fDrQ6i(v;c6i5M?z>@xbkieB@fo5N`}FmR_I&a*z-{tRhqJ3d8{^>?wLCL# z94Z1I^8?U+>#$-c7u(`-tMTcJoz;JwYV|d9TJMU6-o4jJg1VA7V+Beyj9?J4Zz#Q2 z3p1w|{o;HPXWx+sT{o7)T1ftZfaV&A=~H?Ay=E_WFVfUBgbIGQeI%jNshs^>Ya{KS zo4~VeD&a zo(s_PK=1^t$S)(GV^}XH%sA+4_OnBsTTCjmhQQ-eob#=9 zz2>8H_(S60B5M)H3-s+RDEL1RkzGMF`ZWLPUvCw{6pWDe=uik=Eb4?D zf3#E=-+SOpjW0Qx?#J&sBF6eO5@I2>$pvg@4(de-YsD>K_!Bqd!+i8EP{B=}C7Yyg zQ)q;D1w>ZU^Zqbrte>SKB|f!$(PVedhd`dtur;{BCxr*rTats_Gld+kADa`8Xq4SK zPBqM?(hL}29KIUCy2R8Xy1I7a(L*XjJI4*qWZ5i^3#gvw=;J`1O@_gp@Sp>qjjJd& zRq^X2`Zgd{9c^y;XG0wCeWGZ{Jh$sEr|L8(wEu-tI>}S?`7*n@?k538@J}@z6nc1siYoi7HgJK-v+=dQ7I{XRA~waWU$w2tPO(&sSSIh z`dh?q9VCSIJzEtvH)T3Kx91y30@~1q6n8c z$7A>_^ePzx7B_xkK~Mfb%>7^eKoDIW8z}-Fkl)U&Nw+dW+)c zvKTr*@DO4Rx&FMUJ1jQgPhm^_C_d+-EmVn9#Q7_LSPHjzCu{+x!}>MFFA}pg^Z7Hh z9qIqK?e+>|N^N0M={3jG!xxnGJ^m`yrNb5TeRS8lhO5ZzPg{O zfl^F_Vn|~RWemFnr|GY}ccq)JP6QvAPwg#yS_=tj?r(0n{-0ZY&ww6hV$8nYh~ULnu>98` z{yw$e3FyEX<7@ZB>)C@NcZIz*UjKO4{Le@R-$3;tL>R%xZbf*{cVS<~ZE5emJb(C< z$cXF+TePxtX3z(%#B3~w(s*du(a9T+&j)fdOX~GjYWgwfqgjP|hAa5vZ9gslaP|`R zaRk~^Wh9Gu;+EOZ*vGv57m{74)6x-Z$Bu){(U{hhjsEdZ+SXR|x1}s}v7WKN2-l!= z*sKPgl|*uNPP92_RuR)+|J!T~AA-_`zKTQ0xOCFr4%uvjC84Jd!~;|Q&Lh++o5 z+` zz<9kd(NC=H6o?5c9g@rg=i-}CrC!LU8#PoS@Fg$&x<82zfuROUfx zT{0Q{fj;5@O_@HQnJlK&G3fj3?^^+pH(-Z&=y00OxOy%FEl*=*85scHWAA{}FY0e0 zW6f&x<7wW(0K=NDZuON6Hgli*oBgI4ySQ)A8nuL33%-Pm6?i%Yc_yM1vIrBt-}^E| z<4L>1{~Z)c+DIlcSRg2aLFk3{8<{N64CGXrflTs(Flge0;HZq}f^53bj&t{xEv6GX zQAyXLkl&87p{)~t`!ccErXv_KOn(zR=rFxqx1^k93!mJ& znzniNb{xDB6$bfKQ=I}X%vuXR0wmo`18*ZcY@D~YB5)r9nUfBIAQk)%QSlDwBhdozK`O_L-Gr-<3aeDQCU} zc>#csh=>Um@__-NB$et(yx1PUSw5C&1aLPX`^*(n$^=}4%Qx1&+G~55@fj)?ZrDe& zSXznVa_9JeyJ8KVi2n(UCMb6SozfOuAEbnI_go`gKl*~W9mNO8L0jH_G2V$^*$z#{ z+-3X&abSKf5G|RbXQt&vf@uVnucrk-M%O0#^7P z>92Q4{*xd9(MajY-rOgl{9k_;5rbg#49{L6Te%fAIT(Wj7&tC%b(o?;Q&w#Mi;#^d zd{RPYeePa>9htWDYSunt{m)S3T0!kW;1p)ylNYp_B_*_!!JhEQ{W90dWr3t$GjgObvM02)FZlIrA^QNiPwdiq$O0kY8(VX>mnAvG<`+cKWb3?6C#P(iJhbq1Dy0f+?4E#+%? zG^>A<&G^O;ReOvzMA>)4h5Ml>?oZ7@oj*_FaPmJ&)QcX07D${DjJ62AAGC*-cb(;mA|XQC z?Nqz0{-I-49E;>G3=MxE46ZpN_e(g-1$ba78Zq5DbIO(K3Gkl-4>&@P(C1*;i(5KU zxx~(kcggm@S#PkIuKd-ItMMmxKjoIkeR9bo`1WZ~`eiieT|eoyO10a)AK%%?{SpJV?4Qg-(o^6{v^HdJ`p6h)1leJ zK8bVX|Maq2*}3r16(qEDh<0~}10gtwy?+nGI=2h&3HY32>a9pI?ky&23|AH1ONHe- zU)iun{2=%{6Z2Wt#4IQYdnNOYLdu0BlHY;YvEr>0rjF6mNCfR(#Hw}CjUPa@)lfrUp3b>q!8PupZI%T(HT5^`6oWT(gfb)|oUc zLP)*rQ$ZoRjllk&nYfYt)}_?6hO1v)BUs0rFjo^+^P@K1uKiowLj~dh}FN5Xc24|f#iWXpvZ5Xq?}*&{)G|uxUO#^l5KPFP;$;wWI)YE)au{0 z4{pTF)djI3=Jx(CR3T%4;M+UzeNIL_xXEsW#Xi^1Yat^9p;2vgif#Bub5Q$%h877^ zsKLm0be}_So*8$+ycTJQ?2;m4HaR?t!~N?>VXcNyQ#U2>B0siOC9O)<)UYX{@G0$A)PxmKU^FzkI_LJfPP zju}}acJ$xw+S>KZ4>>DyE+6mXJ*cY zB%czq=}k~Mp-~lrO3ss}JYEU7IH)5p&lLu~z@ah5yXj*(z2N*yn#!RP!xw)u`C#l6 z-=i}VnBXvr@p3tja#Eor&bZS zT9J!}q)MLGqLa4MFTN%$WS9E;rT%BV%-^M^ zW2yWDD*|u=J;|#QVAtST9dR4;YL$0Y)5u@90zA+juu|fBu47$(o?P+veqX5msyQB$HR{TxRwuQS0T4&rh4IZ`C{cp=|Rt{wf*6b-r#t* zW4`T*D9W3W@AC)~cqbu81&)SLnulD6MPAES>D$2kT^$VhQ)~E(IfO>C&=|%prmeZ^Po?R50Oe6gU!tCiKgp&(1B@8jqgk#+XTEgwEi_mg^rkJVqzv& zqP_#jN*FM!2@)b+YZN8Z%r_lMaPfdbE3jQJt>*bNEXE0jE+wy*-O8UW&|OThK*Xkg84l5+f+xncmo4@=LDq58PEVNU%+c*id+8P} z@&Lqm@>iXESNjd@h-dgCXi=#j#4|OIxnx(=>0&UgGlGrQzk7s+&Wuks*7DsEbqyPh zz#M+wjxx2Ql3d~W=6(8J#z;JgiZ$Rr$_! zq&<|Js%hyFwiZx_H9t9Ui@qJQNc_zu97|!a=?4fMi|w5sYiypGFFPMMPLo9Sek)Xy zFSV;FEg}>``7;i;RI7d39NX{a44xR4874Yc?|Y)Tv#+7=E$d>pV65?Mw#*i5&eXr1 za{Jq=akNo~n4g^J^T{>n(~w`ZCA z*lAT?S>$f-!fM63(Mj1mMQ6dj8bj(`G}X5m5vR>Y*(`3MHNb-aFH_7Yh-_-^ExC}~ zG+4rqMe|wuTKe9q^9a@|d;1K3tt?SW57wtIi`ne^XmdKGlu%!73Az{N*l;B5l3eXz zz$@GiF{dehsoxMmSV<=MxF~KoOOKeh^7FmiG?tfgu$(~3A>mEt1Ss_euLZ>6ZbJGq zXHA59UYNG`V3(9ldO4|S>tTm#PA7!mCh=1D{zsIu)(e--e2XHwu+`vVOp|XsAL)FcZSqYCL|Cr*yriCS=Ireh z$ce4zgbTdYiNV3D8!DM!qYe+fm186BkI+;f5AhU!D4h5sznT6L173h!eYLJ;C#l3Y z@uv_nO^rY%VdDbFbtJ2_BL*_FRg}tt+i)&D1Zqpx#2y(f2&cpGvvldod>;e5dhs>h zLsMQ|7*YDr$@_I8LpEeNdXkOcVkO@VQRlN zBwp*UGJQ=(GN=_=fwDyg92y^O1dwpt>X1I!VHQa&+E?~!7HPf+uu?yN@Gl3K+G*%#y`1E~3O5iAV3&iGyK zyrx(ok+|hv>~Nm5?;rNq+=zd|LtKN!mA@+r|LSJhlJj5@>b5xPE1E&uxggHOQcwFj zEis}hD|r!sMgn=0x#1ZG#r%&q5%=4Y>W%O!jKpynx z4zG$3%{Clnfb~MH?=ow4i$&?4uz50|An1@Rs|=@c)dI?r_LkJa_GQogHi%?Rw#h;@ zAN;~I%9C9Q=O@Upzurnl6_au3`|^r|4L_}kCHj!{E9p*G*HPL7JQZ?;k_^VFa?pi= z-enYZcb}~Kv6(R)DWc=(V4(Q;&N{EJ!68Wh#7`{!(rR4&Sb^N6gk8&8@YM;KVgB6a z_r;hC!A+XV9j4o$=8zuye3W{ag!i_toopfdcf6B#KtfbP>ZsMc#UcSE3;57&FEEBY z40ObGlpZ=RDRV#C&oP86Tk3k%J9-^va+jfB)L!XDeR{Xdq5G0KcUyafH^Q(w>^<^s z+>i4nZ7C~iL)njlJ=ec5Ue+Lq*Ia(hP`3s=f9K*LT7ufX4@w?Ri@XM>652=$fx zsa*vDkfut|%fa?KooL6C$G*=k)Ip;R?@SqsPoYAudJjihh)=nRd;Lk!>gIun-eHc_ zfIh?xfE*TaY!Y|4g<6iR9__m$Iud(HDy!5hGOFI&Yxgcq2IwOk!I#=Kj%&+-bW>`& zOwt7CzSFw&4m!nL99)ur96BNYck;^c3s5<<)O~~10~g&T~w+^Hk0U+>BV^1{^W}idPbPLnKFutZ^Kbj z$Iniz#k3P?^f~XtO2#W7(kmdAxPRdAZGeF+UudlRt6FV6vJP^<*e}!2!!?SJ(t5IP zu(=d~Cpp$X3QRy$V=ShhoOJgu|A>rb%|bOION5U~Lls_P^4Bs@z{h=C9ye*<{w>N& zot@^<4xEMzkk0$0uj&|fpJMV5+62UI)AJkB#|DPMPWRNTQ^kjh6|6|KWUZ}8 zxau68w01{v<|S&P>rfR68g_wI_Go5Gh=q0QM=a?(|>WN~GSFDkR;?Y->*<5?kAH$Vk4G}j9GQFIJx z-9y()TAMgrC=3X8dyslYJ&=^D@k#3cqZ9uZ!bu0)M;F`Kuv#}l%_SYkN-Y|TtE|GqSOd0qdJ=yqMWLvy7(e!=c9s z8Pc!YFQ}g`!O)-{Q!#87IfjeDUQf56VIX;d!z5o|CVh+A8sxW+lSW$;MLp1icUEVU zICX47;b=eD_*nQxx6%-GPBasStmSA&JNAfoYhDaqSA!&ZOm0Q{NCn*>`cWydA|+9sHJn=2cbV znQdqzUa1{f0Lmhyn-qpht_?U?ghjdspig@ZOaI&9X9e~e@CTqhNrNBbRx1A4GE}=? zE5y*>W}}?~o;X1lPr`CdQra;6bsEetFqUP3WhE(p@mUHHfdOQa7L|adsNtZ1CnY_# zVg1Og*#N5;MSJ{js~RZbl?}V92-YF@{)P7T?U4C?ff*~Pf!I0CnQiKPj+hE_Tgqy!Z_jHhxjm$o^&39$)jCUY?5)m*FknLYLr5 z@rY$(Bv7p)+@DT?8|7G|KAj5ZoguKMQSK$MIaj&kcpJ$*VFokk=ni_>`F}IU%+5x zPa&0^04benl18iTPYJhRl?mz!s_*nUQuUV;Ew zkCcKq5i9I9Rc5n&-^CbxKWFl8Lz@@KoiU57+k>w|Y?7(3ow(F4rWSs}NVc^O<|p>D znlhG)Zv?Yv&wX?&PmUziCiv`79Q*89a5gT4d31~gA~&J6c;^^iRSb(oKJgU9JQQZ% zXK>aB3K&A5~YfZ98OHIfeyJC$Hl>9OuQ+ow3M?^_&{9LQ= z&>yxwfQXh>bV71u%3jeQl45aD6MpI3XMp4P-*a*CugcL#CKx7gin)pGwq*MH2{yF- zyP0gI*zwM44I}rymvb(yzeS*TG>E)$SJfz0f0{q0tk83A@qQ6jLTzD^G?pqRfx1O6VzP9w{53b)-)FR}xP{}j(5pNAzbc?{U`@gl z%kMOSkYpNDk;U;z-Y0e{u+Wurg>WJl<4lOo8|w_KhvImGCy1egPl63_IGVnb^VV_Z zE)^m8{(ZF3yOvocfxG^mgQ9gFz#SXU_P}#-yX26D1Bc-9L2}f>A&c2-{%g1_fq%Rz z+$*qCEUb3`=ipyGenEfEHR`yAaHaXYa~wY?zt&7vtBu_Cc_F^ft%OV0YHv!S8-Mp< zI%9(H6!!N>B+8rb*(w zho6LifzcM#W4NF;GFuRPb(%ULY|Z;be*cY1-<0<(WLK#1L>BaU8Z{MPnM60*>$`t| zjZi7E`{k6=3mhq$^^9;iFfhw3Jm`kEcc^pN0cnQmVbSA`6JnUg?m--v@TFAuQi$=9 za6x$!TBn=4WVEYS*Sg<8e^0eLA`yj|yh!m4?e4txM8yGE;*^Zn`^H_X>$Cd0OY?<> zBqYbGaWHyOo8`=5B$^hKT@)ILM3PM&d#X(EfJxPq<OpNje9!eSseA?nT5l7cMto1L#WeBIa!mpwL)jB0$ zOV`TeoEq(TjTpH>!rYSGrfPgYBQHCo8#SWNfOYh;{=q{{EDkDW7G*jin$ep8oOfqc zUzV(&nLph=Be9LP6K1ruvpM6`ib?gPr_# zu?3v8Dz7t%@n69>yVZ^?_i~_(MLPD6G=?Bzjry}p&)Xn^qOb_mq0DM|KC(}3FjduL@0abo?K08ruIB28 z{5q~=H9cs-Q(~gN$kq33`V;GO9zHEX*CI*=OJ1Cd99G@!PFml3dr+041~&@}9md{; z1AbCHjIJ@AksCn=fql7MgZ?d_=9&!lZ@v?ogWCR;6?_~wpjn+HRuzKQeAZeeWFI?k z2B&}A8Nw{{M2XL+4pIFnTYe9BA>S?xUDlI?tib!bMia*wVFhH4L&UZzU`*E~0Ze#9 zS7`bvpyh8q{eLxGbzD?U7p5cxmJkG_yStPwK~fr|yBlPe5&=o+UJ&V$2I-}{yQDi8 zmPYbhU;XyqyL0ZD6VI75^Ze!tos6|etYEXcsQX;IPTmp*N3>mvrl!5&*WK;eiqiN3 z;SIO?B0@ko&nW@1uM-$nj5u!@idl|a?bD4v9i>n?qOfW@Sx3LeZ zV&4j0844P*i?ign8FmbZcKVi`^Yut>)R-@GlwgmF!wTgCisc3m>Aet6@o&rlfN;;)Th%cVF9cHS=W zhGMbA&r)FYhzJPgi;U5YP6s*HJbI zn&5YS6d<`@(Sh@dzN~Rb+Nv_fB?B~maf#^P#xpttg{*R%0=Ha_B0(gJqTW1u{=bF+ zI1!K@e)t9tJzQxHm2%!OV$mK!s^3QGE--tBO~qQ3i^p%>l2wMZ4I#Oz*aG+0b%>56 z-+h&CH;A{HJ%9x1TQFB8aEq);1$u4`r=xkH2S|0R%=JG~P42n-$V2FR7$F1LMjvb? zh=4;CS00x$x{_HVo*>0t8L~Lu6-$PH94pky-j$HI_3+w&{aYP{E`2D`gw;BF2CjCY zlbahz4&Ey5?C9w{?fyCV@K9L3zYr6X5!2}LwbOa1)NUWq%1C8LIV6_LDNkIi%XZ zSGI?Wizt>(m@c#TlZtH#aYfQpLmhCwaPnjOp9RQuw%}OD~Cdi{Xl%tRHlhKaTxj z%GfxYa*YkZkGkekS$-MRQ`>*QT&h$mqBS@>i`4G@2{8(@L+@gnG$viGJEb(^ZxxVI z^)}t+reQDDC=Qah6Hq`FrAd7*kIL)rBKPd=irrrVgOpi!Pqx;ip0YA zd9LLAl565krrfC~w$3Yg=V4A*^8<{6$+VX3tC&Ow+O7H^<0VMCr$l%ER z6G8+e>K{L4QyAUtBJvkeBj#`B?&UW2WuJs7sN_mgt`xQZ-U})#=S7&nn53S@{${XC zaGa7qhx>cCy&xeWI!z5kj^e#}|AEUI_VEp$0dzCwvWrpYu69yA-MEHpX2$6*cdPZ_RhG9+*qRfIa@+X`kuxo0UQ z;;#FpQpEqGpp4h#t^wpVi~{YrG4(i$p_a|N`QQ*W>=o*&ml)K>9rIEqDi@u6hVS$q znwjRURJh0|N-|A>ChWeBLuy1qIVK4DyWbz-ldk{E_nCL?7mj55ggP(Fx$%zdLOw2j z{oBS`Qh11f+QL0)9HD?vBB1V>YiM#7Xj=y+Nd2lSc{~43zJ|N_ z@~c8D#dHvg0}O51O3%%vPEc}gL+0q`#17Wg3pfz|>0$n`3ugmxr)0$TxsnuXPEugf zq*9=ywAtj-(3so?`z`zMs~H24v7sx3+K9c>tR8zo54|yBmpOfD+q-bFQ1OtP;<+4% zzr_l}@ATsRu6vZfJUoz`RH^PB%8sTD1!HzH5=hzzSd5*EBdYAG6MDw@$qjYq4*2}# z5b{ENi>J8RY6WspAI^maN+CYw1#F$pV-jgdepK2s=5$LxL3GJjBCdXr|GzRoRUo|% zFs(r3Pcs%!GdqujFDLfd6T?f1vHgviocK#rm8b^JHy&aU&=Sklg6|3H!&h)$@G>RU zy*^$c5_ODH77i3U}5> zB_(xel5*;o5}ky0@8>vVJqQ$y0~o!##?}h$Nr6|Ykc8`j3+Dd}W9@em?A9NA3b&^IclpCLMJKeKPX^X=8 zXQ<8OBo$|(!)Wh4)z}5bA{8k&;6i<7{;Hbgj)rWBlNz}(B-F9%mln37jZOMC1-ta~ z!FDijSTkbdw0M5>`7zX^tKR&JYnOi9%4fl>9mEWu@sB)q)VW<#J%YtGPoHT{JpPfx zE}}z_a=^*$@1Y@Wl{s^X;}MeJ_!RrBnalF5W0i=4M*X$z=KVo!fr)$OQ~>ELQ32!p z(RV$ub$7KU%8t7~G#U@6%{x4(C8pY(k@iL4G*e8UQVvNV%I%1E_j0zj>~VqDou_xK z%=@ArrGcXm1SMO2z2t%4n>F+vm$a%|h9`g7Ua_<55|>GGr2syMs}M&;8!D{Z6`w`hJVajRTb2KrWdty6`CCmB2M;hyaM7a94R0lG1tqcsQw z$!>%X(By{YZX0A`dULPsk~50Fvu-%ukT>~y8YJQa&)a3(6n_`{7nt1>q^RSoXNa~; z7ZLa)yzU*v;(WIiJ^G?4G9{NT;8@3wog9uQc;< ziM}zwT%u&lm8a`76~kxTfT#}Fva+A6W7ZHfr9a**+lC;LW^$ck*Ev;%zx@bkT#VnY z-zxLKhPS09v@`+lN_bYZ@B;Xz5#m~k4VTDSHq#nte?a9v0K1yT4=fMRdz`F3hesQQ zL9%k}(F2o9OY>+FXpb4JeiOl$!T-R-!sDF*5BIZeHz1rY zyhoC2Frr%UW2_JIcK8zeX3f`f%D>QXSNpX4R_ zNI@fM#(t0wUJ_Liaz>8g?jB|?TvEauzs%Wz|Gnr^?@W2h;rZCm2s)22TMqNF*4wo) zsDUYKA!n>`<_<^OVRNL=NHF2#68Ubr4_I2n*B4Le1`neHQE~$OL;=s4PJZ2iJGp74 z@$%fY|KvQjU5!2yi=Y|X%x6$RdyA6tZ7CL0ZOOe6RB}wSw-OSIlkT2_>gk07;6?rt zOD%#lDPbZ_MdR>JVt)ZVj3tlGiGd^=MkQ24*B5Wv@tIhFti<}J!S(I5yV^-jUts2# z7Vy26j(9;cvNZ4V(3l)Ug7?R3;NyA44}Yy?%K8K8$nn6WJuo8;z>U8;0bXK57CjeD z&OEvwQ4NQTCioUvbM}wRuJxr+AR5ze+ZQL+?V%gdu1Oi&cJ=l%=(pV!A$jQcNo&^8 zvtNWmimXPd<_*L8>leOLBT@P3-@Wo=XXbMiIULHtEZ#TAz9d`uT9z{ZWE^4TRcnVL zz40M%ckPz{mARA;PkxN*kbGoZfZ_OVm{N%-SZdVtlK)ttazB4P*x@}5&eA(dGi*GV ziVl&j7oyF>ci~fvHNf8H5c}!Rrw0JKR z)Wbe@FFJ0Srna`u;v$~#eZ+%(#|YM=>1$9JbE{p1Ytpr;KLFS`Og^ll&nY9T|FMN% z^R-}xf);7ak2I4w{Hz?)f#$4j4tiV+;uK{>7hH&m+zFU#O_tz%AF;2qSuMh!8TN zjhVWNij1cZMvoow|Nd+2eaQj^wb)iPxvX&?6Q6m5$mF?_1nJTCRecCL2WVZRwISTSYkK)1N*10^?jOvMS9dJmW#X@zUqZ0r*C#x&1dM`PvvsTx}jx-NSWgl_?8g#LRI%0i<%oYz1 z)>_8V0<)Vzuii8Q15Eq!GIBKNXuo;2D4=?*E2bm+GL5$68RHK3_YX&zxzV3_y9-`H z8iNx096#?VvSzgmY86=PzeRz3oyjLMDtjdy+ z$mIKmYQZxm(S9x^cB%VIUjO&kb^884B{yGc`T88#NE;$~@WeaRUWSU_@O`k|qkg+m zp-wf>+b2%PKu^ywosopBgm#TLvQS@ZeU+^0vO2qD68iuICilQ6YVcQLUaDmC zGu3G84Dj8TR&6_!yiE(A0xbg>RO>nvAk-lEie5waIRrl*A|} zP&Cq>>mK)YO}^>hY#8GxZS^`^C{v_v8Xw&bAuiQWRcUg4>G(1uU8Z5EhUEu5Sh@CJ zb2hqGyb3MKoVt+1E3E650jPF{eTOF8xk?P@Kw;sVeeb0-{EjY+ImqmTiWnKf6pG7A zDqf`6EB<`;rKVzLvjD@-y%dUj=(*XMH6xY3T{`@VgSnx1Ai*4hE! zN^}uB%FaOifHa??sDQuy`o1Idem^jZXtZNAeJ`fY_{yZHjMPmZNBkwC8eU}!2FjJ> zP{ikJ@{jw$Iwidgt>9u(i`B{9Lwc^+ul>vEql51MNmH~cl-7-JTd|lu9V&KCuvT{eAeUfwneX#uuT^R+yY5&gyngZnWY9B8 zv=d4yElP@rUT5g~N%dCw=RS;yrz-+a_@yIuO4qk4l+K|}@6^q5mIkG*w_p>}Q2!EY z5o@@5z>KDgPB?ZQN9asl)_6(3zbw*JB)i7)|rc)Lg4C zf8Z3WjU^kKlcT+1_z9FFuN8eV^pTcWcaPIt{d9lf1^N1;PZ1SMUke3d``7pF2Hr9B zv|MEVL&%(@#PU-{F>>Exg9Z1wjbuvX}JQKHDva_>WTOY?y&ka;Q`b9%RZ>OFw zIo-p$*Y)%zoSaSf3pGAGcC3QYL34lLEloc4M&rWBkHdOL1tErG&@r?eYP(fO%lnX@ z>Fdwh=a_fR$g>-&LN;VsIFS>m$U94jc%t}E9ZF+2ZadoI1v1)Q4};&14xPDkHi{yd zvc7@c8LK=Z^*-V@6(yE^0gi4?ZR6j`J%5+Mo|^>eeMCNg&?M@Vrd|$Xc`a7lh1Vi5qh@Ag9Poy0INvXLLYiZ9RNArMU9lzE zRlG?}bv z*DR67n#02Vuwh#2>-Ty%c|HQV+IH?$UztV~YMeantiYEixWyFCV=x zRJQRigV+5Mr9a(CbvwP`D}?!=-pVJbMk#NnmD$DDKX0l(NEdavnl&!Y^&scLU6~h= zLs}u4{;$w=Y>}3-$l494hl(f*r7k{d9!1=5hmyrtj7pi-Y)^KwsD8Mv=5QttZu0yd z@m;XA^JE_nStX{=_(LtTm0+&~$b8Y{=3MeWAd#)S3b9MjSLly6t;dW*m)92{DkFRp z_?-SJzkt?&C9fxmHuHM5k(&JnZvK)7f0X^SVnv^P%~J2x>>q!VV);Mn{YClGhJatR z=fpgpR3dX{o$qX-)4zGGZJ-vt>NG)>3W- z2=@KcsQz!@P(Ae)4>EH*4#c(=|zx7 zme{XHh{U|Ar|4b&Y1{L$Nf@=0bP=^H^YHJIz?lq^ZlE&Y+uc&X`Qh{Ji2q1&qR~j3 zn;6$yYPn8k)*6hCPiW4$bJU|h%A1lRH5Sxh4JE8s24i>TR-oNtRx4QYGBI=@L1G-6D$|nXX~7znrQ6*$Ks~?dg(`%k~}n*e3l? zmxn(~eDZ2wf;G+yG+g_7%;6S1mXc(l<_G`Jo2G)!nIG~!5?0p=eT#|4NxiDw;;Kd? z=nP}xwT%?*&skXveX;wU09l1+TpqK`7zv{ zF2j`tilEEpOzXOGslko&I>kSgo8TOT_11MXbuxcRmH-96;2JRJs$G1hfUVOyy|Da% zs~3$sU+4F5WaF3D{5ych4m+@3N}DfN@7T3A9&xxb5vKO+oHi8-u+J7awuT+W;A zVogV!8``_z6yq4=1E_Gh;_^bM@@#I$&()%q1a@_S1RRHkrmOc5ImKfdAe_`7!GS^9 zrR#O;Q0C{)1i2KI7$0DDA;R;VYc8yG*^O_=Hu{fj>O%GBiH`iUlXqMio_X741|O}@ zsxU*iohwybt#L|p;px|56Qsp~p+R;_@cr4!`(+&#{roS|7&din$v>k$1FL@?8TQA0 zqT4W4{autgQIKE++6M?fHM9j9X2w1|TmYhncVLW$Bc3P~5AnG}k0b~wqaH@K*j{w7 zUPy?q5(O`d${};o~CzF4v}ti(|oMPQ%lrxv(kcwOqG%p!{G?$#8%C zQ?4)6>-bah_1i6->KGq;KRx>*g_oJt1&Dg}LKaqs-&@zULLVTwgMAzzwLZ?+z0lSD zpX{CxyPsjpWjOR}6IIaZCT0BG!%`MQx;+1cm_cgamDXU zH7289Z5pXTVogLdFfl5EKOHHE$WgPCcin-yMGFc{GJ)9(dFjD`JIfA7)zB1xAn;a% z2ZZfZ>GY>`oMRK3fP)(2E zc8D#_SzzBf;Ha{esTms&B>W+-qJNFQspG%SXIMbJnG2G;(wTr?p*r2J9$wLGEtU?E zubjexN!xO2Xo~aQ_awgi%IO}D{G!rXuH~y_rsE%R#4KH~YVtaj?5>vqb40EEqqg$v zQCdOf?C>x^q2(v)S`2mZ^dSlpwo@|aPH9rAoa%Z(<8Bqmh(P?|l$9pL8~&M1=TJke z0*AnbX=Lfya7Jbn5WTb|Uj2H4%WGeGROixHWcIc*=sU#4z8wt!iBXPPS&`fwi1T3HBCa|1{pgko$D7? zgyhr?8N(RaQ{2i~EC4b>240(nm)@N9XS!8w_qgIFSf?t_p`Rp;2{ord zjrRh9A@a}MA_=(9o_)F*YHbHIK#Ll`x|$KBAh`Xul*F!(dgbmiso-^5RO&$ROJYR2 zc*uRYz&=>e0w=V5?~r&qRkOEKe}hJLaQ+RbEiX4&9J#QE7a-=8MO_T=CY}~@C;*#z zohEISV`HGrSNQ@xrpvG^9;NG)u$;5w2Gy23LyEsE=k>s^&-wkGd}ssW_RX_Q>eS1^ zAR1c12C>=bGPV&QAs*AWQD+`qF%Vbwl$D#S{i4K-h2hSb}Wg??#RkgAN@DcqBXG=)7ASyYg+kVvc0h~Wo4P3Qk+B5x4d_8k2CdGMHNKm8fT ziv>2DUDVyUeTIYpn2KO=JD)kf4%nzy)cD&m@b&VTF21v3ZrHnL?OUu#%kRuVFEXp? zDaiQvq4{Qsxh`n~7F3WaB^Fha(gH1}aYbJD!tez0jz#vUjb4Y7=iMe1tqNlJ;X{C>^3SYD%JGS4qdSt-Gr~)>{pR(M1-#IIU?61TF*29a5jOlp)YbmOq zDPofv=n|J)?>}s{AF6Oh1cuvB-$FeAEGY9|qOBDz743xM(_+j>kU$~|A@2wS@_L4?dcd<&#ts;&e}@l3o28^dl%(b$-;18= zLKnHBR{pe+>Q=n5K6bA3a-y2dcs2Q6HebT&qaBO1hYnVror1&Uas&FjleTaWxfn_5Tz2SjGZ?f6|G2d&CpIgz|yraNUCT7 zFmK$9_G^Ui2M~ht zu=4G?=!?2gz4=y>dV6HO+n{x5Wz8bU0C39CM9UTmjF%@2g>lgCa8-Xgie0jsyJuQZ zeMjPdv#nYgNnFxoZ40Kx`7aJoQX1J=7%JaPTUfnmqv<@c)kst z6X(|B;EV6g1dEa-R>J_@!r<}aoTLJEoIWqkU&4uNmUDP=S3E(SqX?d>cq9;3WtK{z zN4^2dOT5McnUUa^q&|)EjhRS5ah~rfwJn(1>GHS0VGP6j)vOE1k(HwFu9*lRQo>2O2+B(cdtw$r#ykN-V$H6LxmIN4(50oohyDHLzkS+E>x zXEXeNwyrm(2}jG=}A8L9gDtyy=(dC@n!fUYoxE9Ue*AAn5^)7*(ur#(GrPFGG!^-GMtn?p0L zJI8xYp|lR_R$UdR)iqAOky>K(iVu|awQJ;> zk^6qB`i&UIE&(kG$SL$F+D}ges|)IGXLuiu{0)@W&Rm99kV~u=Cy->EWb$j z`!meq5M49mWb>PemcGmh!B(ufvoqw|%h({!?L;Tlu$&~~Q4Fg_T;z5B!=842t+p2UjC*t;vLG0x0H6EQbd5&%g(8z5mJKD)BqQ;>G zN241%D$Elb6zBE!^y;v7=TZg>ubPH8bxJos!h(U^XQWs6I|Xbsu)vw0Qq$&#n8s`y zb?jZd-JDB_Z&4_IropO+o)B6>7H=!3?9Q3&RPpq1wtQg)Y&lP8wX!WVhA2qg2=Jf$ MTNRmdDU-ne0i*Hi4gdfE literal 0 HcmV?d00001 From 6a4384f71ec6ccdff8d5fbcfcec50cc9e139eb51 Mon Sep 17 00:00:00 2001 From: Vaclav Petras Date: Fri, 17 Mar 2023 22:23:16 -0400 Subject: [PATCH 02/10] Copy first category, assume layer 1, convert NULLs to zeros --- src/vector/v.move.points/v.move.points.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/vector/v.move.points/v.move.points.py b/src/vector/v.move.points/v.move.points.py index 02ee46bd7c..c61a46334e 100644 --- a/src/vector/v.move.points/v.move.points.py +++ b/src/vector/v.move.points/v.move.points.py @@ -34,17 +34,22 @@ # %option G_OPT_V_OUTPUT # %end +import math + import grass.script as gs def move_vector(input_vector_name, output_vector_name, x_raster_name, y_raster_name): # Lazy import ctypes # pylint: disable=import-outside-toplevel + import ctypes + from grass.pygrass.gis.region import Region from grass.pygrass.raster import RasterSegment from grass.pygrass.utils import coor2pixel from grass.pygrass.vector import VectorTopo from grass.pygrass.vector.geometry import Line, Point + from grass.lib.vector import Vect_cat_set, Vect_cat_get x_raster = RasterSegment(x_raster_name) x_raster.open("r") @@ -57,14 +62,23 @@ def move_vector(input_vector_name, output_vector_name, x_raster_name, y_raster_n output_vector_name, mode="w" ) as output_vector: for feature in input_vector: + first_cat = ctypes.c_int() + Vect_cat_get(feature.c_cats, 1, ctypes.byref(first_cat)) new_point_list = [] for point in feature: pixel = coor2pixel((point.x, point.y), region) x_value = x_raster[int(pixel[0]), int(pixel[1])] y_value = y_raster[int(pixel[0]), int(pixel[1])] + if math.isnan(x_value): + # NULL is zero. + # Implement also null is warning (boolean) and null is error (raise here). + x_value = 0 + if math.isnan(y_value): + y_value = 0 point = Point(point.x + x_value, point.y + y_value) new_point_list.append(point) new_line = Line(new_point_list) + Vect_cat_set(new_line.c_cats, 1, first_cat.value) output_vector.write(new_line) x_raster.close() From 57d3cacfd0996adc5e64cd01a220057d0f13ea86 Mon Sep 17 00:00:00 2001 From: Vaclav Petras Date: Tue, 21 Mar 2023 16:34:32 -0400 Subject: [PATCH 03/10] Handle null values and extent mismatch --- src/vector/v.move.points/v.move.points.py | 89 ++++++++++++++++++++--- 1 file changed, 78 insertions(+), 11 deletions(-) diff --git a/src/vector/v.move.points/v.move.points.py b/src/vector/v.move.points/v.move.points.py index c61a46334e..a64f120b39 100644 --- a/src/vector/v.move.points/v.move.points.py +++ b/src/vector/v.move.points/v.move.points.py @@ -31,6 +31,14 @@ # %option G_OPT_R_INPUT # %key: y_raster # %end +# %option +# % key: nulls +# % type: string +# % options: zeros,warning,error +# % label: Handling of null values +# % description: Null (no-data) values in rasters will be considered zeros, cause a warning or an error +# % description: zeros;Null value will be converted to zeros;warning;A null value will cause a warning (one for each raster) and will be converted to zero;error;A null value will cause an error +# %end # %option G_OPT_V_OUTPUT # %end @@ -39,7 +47,17 @@ import grass.script as gs -def move_vector(input_vector_name, output_vector_name, x_raster_name, y_raster_name): +class NullValue(RuntimeError): + """Raised when null value is not allowed and encountered""" + + +class OutOfBounds(RuntimeError): + """Raised when vector extent is large than region (raster) extent""" + + +def move_vector( + input_vector_name, output_vector_name, x_raster_name, y_raster_name, handle_nulls +): # Lazy import ctypes # pylint: disable=import-outside-toplevel import ctypes @@ -58,6 +76,8 @@ def move_vector(input_vector_name, output_vector_name, x_raster_name, y_raster_n region = Region() + x_null = y_null = 0 + with VectorTopo(input_vector_name, mode="r") as input_vector, VectorTopo( output_vector_name, mode="w" ) as output_vector: @@ -67,20 +87,63 @@ def move_vector(input_vector_name, output_vector_name, x_raster_name, y_raster_n new_point_list = [] for point in feature: pixel = coor2pixel((point.x, point.y), region) + if ( + pixel[0] < 0 + or pixel[0] >= region.rows + or pixel[1] < 0 + or pixel[1] >= region.cols + ): + raise OutOfBounds( + _( + "Part of the vector ({x}, {y}) is outside of extent " + "defined by the computational region" + ).format(x=point.x, y=point.y) + ) x_value = x_raster[int(pixel[0]), int(pixel[1])] y_value = y_raster[int(pixel[0]), int(pixel[1])] if math.isnan(x_value): - # NULL is zero. - # Implement also null is warning (boolean) and null is error (raise here). - x_value = 0 + if handle_nulls == "zeros": + x_value = 0 + elif handle_nulls == "warning": + x_null += 1 + x_value = 0 + else: + raise NullValue( + _( + "Null value encountered in {raster} (X) at {x},{y}" + ).format(raster=x_raster_name, x=point.x, y=point.y) + ) if math.isnan(y_value): - y_value = 0 + if handle_nulls == "zeros": + y_value = 0 + elif handle_nulls == "warning": + y_null += 1 + y_value = 0 + else: + raise NullValue( + _( + "Null value encountered in {raster} (Y) at {x},{y}" + ).format(raster=y_raster_name, x=point.x, y=point.y) + ) point = Point(point.x + x_value, point.y + y_value) new_point_list.append(point) new_line = Line(new_point_list) Vect_cat_set(new_line.c_cats, 1, first_cat.value) output_vector.write(new_line) + if x_null: + gs.warning( + _("Null value encountered in {raster} (X): {n}x").format( + raster=x_raster_name, n=x_null + ) + ) + if y_null: + gs.warning( + _("Null value encountered in {raster} (Y): {n}x").format( + raster=y_raster_name, n=y_null + ) + ) + x_raster.close() y_raster.close() @@ -93,12 +156,16 @@ def main(): y_raster_name = options["y_raster"] output_vector_name = options["output"] - move_vector( - input_vector_name=input_vector_name, - x_raster_name=x_raster_name, - y_raster_name=y_raster_name, - output_vector_name=output_vector_name, - ) + try: + move_vector( + input_vector_name=input_vector_name, + x_raster_name=x_raster_name, + y_raster_name=y_raster_name, + output_vector_name=output_vector_name, + handle_nulls=options["nulls"], + ) + except (NullValue, OutOfBounds) as error: + gs.fatal(error) if __name__ == "__main__": From f86aa4b1a1913436d5d83b2a788a76f1c4ca82de Mon Sep 17 00:00:00 2001 From: Vaclav Petras Date: Mon, 10 Jul 2023 11:43:01 -0400 Subject: [PATCH 04/10] Rename from v.move.points to v.rast.move because raster plays an important role and it now moves vertices (points) of lines, not points. --- src/vector/{v.move.points => v.rast.move}/Makefile | 2 +- .../{v.move.points => v.rast.move}/examples.ipynb | 4 ++-- .../v.rast.move.html} | 6 +++--- .../v.move.points.py => v.rast.move/v.rast.move.py} | 2 +- .../v_rast_move.png} | Bin 5 files changed, 7 insertions(+), 7 deletions(-) rename src/vector/{v.move.points => v.rast.move}/Makefile (81%) rename src/vector/{v.move.points => v.rast.move}/examples.ipynb (95%) rename src/vector/{v.move.points/v.move.points.html => v.rast.move/v.rast.move.html} (87%) rename src/vector/{v.move.points/v.move.points.py => v.rast.move/v.rast.move.py} (99%) rename src/vector/{v.move.points/v_move_points.png => v.rast.move/v_rast_move.png} (100%) diff --git a/src/vector/v.move.points/Makefile b/src/vector/v.rast.move/Makefile similarity index 81% rename from src/vector/v.move.points/Makefile rename to src/vector/v.rast.move/Makefile index ece0d7a91d..28c1185e67 100644 --- a/src/vector/v.move.points/Makefile +++ b/src/vector/v.rast.move/Makefile @@ -1,6 +1,6 @@ MODULE_TOPDIR = ../.. -PGM = v.move.points +PGM = v.rast.move include $(MODULE_TOPDIR)/include/Make/Script.make diff --git a/src/vector/v.move.points/examples.ipynb b/src/vector/v.rast.move/examples.ipynb similarity index 95% rename from src/vector/v.move.points/examples.ipynb rename to src/vector/v.rast.move/examples.ipynb index fa15b33d86..0d82de5d0d 100644 --- a/src/vector/v.move.points/examples.ipynb +++ b/src/vector/v.rast.move/examples.ipynb @@ -41,7 +41,7 @@ "!g.region vector=roadsmajor res=100\n", "!r.mapcalc expression=\"a = 1000*sin(row())\"\n", "!r.mapcalc expression=\"b = 0\"\n", - "!v.move.points input=roadsmajor output=roads_moved x_raster=a y_raster=b" + "!v.rast.move input=roadsmajor output=roads_moved x_raster=a y_raster=b" ] }, { @@ -79,7 +79,7 @@ "metadata": {}, "outputs": [], "source": [ - "filename = \"v_move_points.png\"\n", + "filename = \"v_rast_move.png\"\n", "plot.save(filename)\n", "!mogrify -trim {filename}\n", "!pngquant --ext \".png\" -f {filename}\n", diff --git a/src/vector/v.move.points/v.move.points.html b/src/vector/v.rast.move/v.rast.move.html similarity index 87% rename from src/vector/v.move.points/v.move.points.html rename to src/vector/v.rast.move/v.rast.move.html index 393caf877b..8341ef8c43 100644 --- a/src/vector/v.move.points/v.move.points.html +++ b/src/vector/v.rast.move/v.rast.move.html @@ -1,6 +1,6 @@

DESCRIPTION

-v.move.points takes values from raster maps and adds them to X and Y +v.rast.move takes values from raster maps and adds them to X and Y coordinates of features in a vector map.

NOTES

@@ -10,8 +10,8 @@

EXAMPLES

Shift in X direction

- - Two roads networks + + Two roads networks
Figure: Original (blue) and shifted (red) road network and the X shift diff --git a/src/vector/v.move.points/v.move.points.py b/src/vector/v.rast.move/v.rast.move.py similarity index 99% rename from src/vector/v.move.points/v.move.points.py rename to src/vector/v.rast.move/v.rast.move.py index a64f120b39..31667fba71 100644 --- a/src/vector/v.move.points/v.move.points.py +++ b/src/vector/v.rast.move/v.rast.move.py @@ -2,7 +2,7 @@ ############################################################################ # -# MODULE: v.move.points +# MODULE: v.rast.move # # AUTHOR(S): Vaclav Petras # diff --git a/src/vector/v.move.points/v_move_points.png b/src/vector/v.rast.move/v_rast_move.png similarity index 100% rename from src/vector/v.move.points/v_move_points.png rename to src/vector/v.rast.move/v_rast_move.png From 7b92a6dac9c4dc40b6508b6b0020e45fc4b96760 Mon Sep 17 00:00:00 2001 From: Vaclav Petras Date: Mon, 10 Jul 2023 16:17:53 -0400 Subject: [PATCH 05/10] Warn about the nulls by default. Warn when integers are used (different null check is needed). --- src/vector/v.rast.move/v.rast.move.py | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/src/vector/v.rast.move/v.rast.move.py b/src/vector/v.rast.move/v.rast.move.py index 31667fba71..8f40b69c42 100644 --- a/src/vector/v.rast.move/v.rast.move.py +++ b/src/vector/v.rast.move/v.rast.move.py @@ -35,6 +35,7 @@ # % key: nulls # % type: string # % options: zeros,warning,error +# % answer: warning # % label: Handling of null values # % description: Null (no-data) values in rasters will be considered zeros, cause a warning or an error # % description: zeros;Null value will be converted to zeros;warning;A null value will cause a warning (one for each raster) and will be converted to zero;error;A null value will cause an error @@ -131,23 +132,23 @@ def move_vector( Vect_cat_set(new_line.c_cats, 1, first_cat.value) output_vector.write(new_line) + detail = _("(set nulls to 'zeros' to silence this warning)") if x_null: gs.warning( - _("Null value encountered in {raster} (X): {n}x").format( - raster=x_raster_name, n=x_null + _("Null value encountered in {raster} (X): {n}x {detail}").format( + raster=x_raster_name, n=x_null, detail=detail ) ) if y_null: gs.warning( - _("Null value encountered in {raster} (Y): {n}x").format( - raster=y_raster_name, n=y_null + _("Null value encountered in {raster} (Y): {n}x {detail}").format( + raster=y_raster_name, n=y_null, detail=detail ) ) x_raster.close() y_raster.close() - def main(): options, unused_flags = gs.parser() @@ -156,6 +157,17 @@ def main(): y_raster_name = options["y_raster"] output_vector_name = options["output"] + for name in set([x_raster_name, y_raster_name]): + raster_type = gs.raster_info(name)["datatype"] + if raster_type == "CELL": + gs.warning( + _( + "Raster input {name} is CELL (integer), but null-value " + "handling is supported only for floating-point rasters " + "(FCELL and DCELL)" + ).format(name=name) + ) + try: move_vector( input_vector_name=input_vector_name, From 0382686aef7108511d54161bfd6a3d558135862c Mon Sep 17 00:00:00 2001 From: Vaclav Petras Date: Mon, 10 Jul 2023 16:37:26 -0400 Subject: [PATCH 06/10] Add tests --- src/vector/v.rast.move/tests/conftest.py | 64 +++++++++++++++++ .../v.rast.move/tests/test_v_rast_move.py | 72 +++++++++++++++++++ 2 files changed, 136 insertions(+) create mode 100644 src/vector/v.rast.move/tests/conftest.py create mode 100644 src/vector/v.rast.move/tests/test_v_rast_move.py diff --git a/src/vector/v.rast.move/tests/conftest.py b/src/vector/v.rast.move/tests/conftest.py new file mode 100644 index 0000000000..1ddb52a880 --- /dev/null +++ b/src/vector/v.rast.move/tests/conftest.py @@ -0,0 +1,64 @@ +"""Setup dataset for v.rast.move test""" + +from types import SimpleNamespace + +import pytest + +import grass.script as gs + +LINES = """\ +VERTI: +L 9 1 + 0.0984456 0.61658031 + 0.15025907 0.68264249 + 0.2642487 0.76943005 + 0.39119171 0.79792746 + 0.52202073 0.80181347 + 0.62953368 0.78367876 + 0.68134715 0.73056995 + 0.71243523 0.64637306 + 0.73445596 0.54663212 + 1 1 +L 8 1 + 0.09455959 0.40544041 + 0.31217617 0.40673575 + 0.61917098 0.40932642 + 0.75518135 0.41580311 + 0.86658031 0.41580311 + 0.86528497 0.35621762 + 0.87305699 0.20595855 + 0.87823834 0.08290155 + 1 2 +""" + + +@pytest.fixture(scope="module") +def line_dataset(tmp_path_factory): + """Create a session and fill mapset with data""" + tmp_path = tmp_path_factory.mktemp("line_dataset") + location = "test" + lines_name = "lines" + gs.core._create_location_xy(tmp_path, location) # pylint: disable=protected-access + with gs.setup.init(tmp_path / location): + gs.write_command( + "v.in.ascii", input="-", output=lines_name, stdin=LINES, format="standard" + ) + gs.run_command("g.region", vector=lines_name, grow=0.1, res=0.01, flags="a") + + x_name = "x" + y_name = "y" + x_value = 2.0 + y_value = 5.3 + nulls_name = "null_only" + gs.mapcalc(f"{x_name} = {x_value}") + gs.mapcalc(f"{y_name} = {y_value}") + gs.mapcalc(f"{nulls_name} = 0.0 + null()") + + yield SimpleNamespace( + name=lines_name, + x=x_name, + y=y_name, + x_value=x_value, + y_value=y_value, + nulls=nulls_name, + ) diff --git a/src/vector/v.rast.move/tests/test_v_rast_move.py b/src/vector/v.rast.move/tests/test_v_rast_move.py new file mode 100644 index 0000000000..771e008812 --- /dev/null +++ b/src/vector/v.rast.move/tests/test_v_rast_move.py @@ -0,0 +1,72 @@ +"""Test v.rast.move""" + +import pytest + +import grass.script as gs + + +def test_displacement_result(line_dataset): + """Check geometry of the result""" + result = "result" + gs.run_command( + "v.rast.move", + input=line_dataset.name, + x_raster=line_dataset.x, + y_raster=line_dataset.y, + output=result, + ) + old_metadata = gs.vector_info(line_dataset.name) + metadata = gs.vector_info(result) + + for item in ["lines", "nodes"]: + assert old_metadata[item] == metadata[item] + assert metadata["north"] < line_dataset.y_value + 1 + assert metadata["south"] > line_dataset.y_value + assert metadata["west"] > line_dataset.x_value + assert metadata["east"] < line_dataset.x_value + 1 + + +@pytest.mark.parametrize("nulls", ["zeros", "warning", "error"]) +def test_null_options_accepted(line_dataset, nulls): + """Check that all values for the nulls option are accepted""" + result = f"result_nulls_{nulls}" + gs.run_command( + "v.rast.move", + input=line_dataset.name, + x_raster=line_dataset.x, + y_raster=line_dataset.y, + output=result, + nulls=nulls, + ) + + +def test_nulls_as_zeros(line_dataset): + """Check that transforming nulls to zeros works""" + result = "result_zeros" + gs.run_command( + "v.rast.move", + input=line_dataset.name, + x_raster=line_dataset.nulls, + y_raster=line_dataset.nulls, + output=result, + nulls="zeros", + ) + old_metadata = gs.vector_info(line_dataset.name) + metadata = gs.vector_info(result) + + for item in ["lines", "nodes", "north", "south", "east", "west"]: + assert old_metadata[item] == metadata[item], item + + +def test_nulls_fail(line_dataset): + """Check that an error is generated for nulls""" + result = "result_fails" + with pytest.raises(gs.CalledModuleError): + gs.run_command( + "v.rast.move", + input=line_dataset.name, + x_raster=line_dataset.nulls, + y_raster=line_dataset.nulls, + output=result, + nulls="error", + ) From 0d1199c2d099501b92445787624e0934b2a7f99a Mon Sep 17 00:00:00 2001 From: Vaclav Petras Date: Mon, 10 Jul 2023 16:37:45 -0400 Subject: [PATCH 07/10] Add Python doc strings --- src/vector/v.rast.move/v.rast.move.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/vector/v.rast.move/v.rast.move.py b/src/vector/v.rast.move/v.rast.move.py index 8f40b69c42..80c4ddc9c8 100644 --- a/src/vector/v.rast.move/v.rast.move.py +++ b/src/vector/v.rast.move/v.rast.move.py @@ -6,7 +6,7 @@ # # AUTHOR(S): Vaclav Petras # -# PURPOSE: Update a column values using Python expressions +# PURPOSE: Move individual vertices of features # # COPYRIGHT: (C) 2023 by Vaclav Petras and the GRASS Development Team # @@ -17,7 +17,7 @@ ############################################################################# # %module -# % description: Move points by distance specified in a raster +# % description: Move vertices by distance specified in a raster # % keyword: vector # % keyword: # % keyword: @@ -43,6 +43,8 @@ # %option G_OPT_V_OUTPUT # %end +"""Produce new vector map with vertices moved based on raster values""" + import math import grass.script as gs @@ -59,6 +61,10 @@ class OutOfBounds(RuntimeError): def move_vector( input_vector_name, output_vector_name, x_raster_name, y_raster_name, handle_nulls ): + """Move features in existing vector vertex by vertex + + Does not call fatal, but raises exceptions. + """ # Lazy import ctypes # pylint: disable=import-outside-toplevel import ctypes @@ -149,7 +155,9 @@ def move_vector( x_raster.close() y_raster.close() + def main(): + """Process options and check inputs before calling the processing function""" options, unused_flags = gs.parser() input_vector_name = options["input"] From c145fdcb77a5b7753fb239fbcef358f2bf59ae54 Mon Sep 17 00:00:00 2001 From: Vaclav Petras Date: Tue, 11 Jul 2023 10:22:26 -0400 Subject: [PATCH 08/10] Add documentation --- src/vector/v.rast.move/examples.ipynb | 2 +- src/vector/v.rast.move/v.rast.move.html | 79 ++++++++++++++++++------- 2 files changed, 57 insertions(+), 24 deletions(-) diff --git a/src/vector/v.rast.move/examples.ipynb b/src/vector/v.rast.move/examples.ipynb index 0d82de5d0d..4ce41c6555 100644 --- a/src/vector/v.rast.move/examples.ipynb +++ b/src/vector/v.rast.move/examples.ipynb @@ -39,7 +39,7 @@ "outputs": [], "source": [ "!g.region vector=roadsmajor res=100\n", - "!r.mapcalc expression=\"a = 1000*sin(row())\"\n", + "!r.mapcalc expression=\"a = 1000 * sin(row())\"\n", "!r.mapcalc expression=\"b = 0\"\n", "!v.rast.move input=roadsmajor output=roads_moved x_raster=a y_raster=b" ] diff --git a/src/vector/v.rast.move/v.rast.move.html b/src/vector/v.rast.move/v.rast.move.html index 8341ef8c43..20a5141677 100644 --- a/src/vector/v.rast.move/v.rast.move.html +++ b/src/vector/v.rast.move/v.rast.move.html @@ -1,14 +1,59 @@

DESCRIPTION

v.rast.move takes values from raster maps and adds them to X and Y -coordinates of features in a vector map. +coordinates of features in a vector map vertex by vertex. + +Null values in rasters are turned into zeros by default and a warning is +generated. This behavior can be modified by the nulls option to +either silence the warning with explicit nulls="zeros" +or the warning can be turned into an error with nulls="error". + +The rasters are loaded based on the computational region, so the most +advantageous use of resources is to set the computational region to +match the vector. To avoid issues with vector coordinates at the border +of the computational region, it is best to also grow the region one cell +on each side. Vector features outside of the computational region always +result in an error being reported (regardless of the nulls option), +but the rasters can have any extent as along as the computational region +is set to match the vector.

NOTES

+Unlike v.perturb which moves points randomly, v.rast.move +works on vertices of lines and uses same value for all vertices at a given +cell. Unlike v.transform used with raster values in attribute columns, +v.rast.move operates on individual vertices in the line, not on the +whole line (attributes are associated with features, not their vertices). +

EXAMPLES

Shift in X direction

+ + +This example uses the North Carolina sample dataset. + +Set the computational region to match the vector map and use +100-meter resolution. + +
+g.region vector=roadsmajor res=100
+
+ +Generate rasters for a shift in X direction (one raster is a wave, the other is zero): + +
+g.region vector=roadsmajor res=100
+r.mapcalc expression="a = 1000 * sin(row())"
+r.mapcalc expression="b = 0"
+
+ +Use the rasters to move the vector: + +
+v.rast.move input=roadsmajor output=roads_moved x_raster=a y_raster=b
+
+
Two roads networks @@ -24,36 +69,24 @@

SEE ALSO

  • - v.db.addcolumn - to add a new column (to be filled with values later), + v.transform + for changing coordinates for the whole vector map or + feature by feature based on the attributes,
  • - v.db.update - for attribute updates using SQL, + v.perturb + for randomly changing point positions by small amounts,
  • - db.execute - to execute general SQL statements, + r.mapcalc + for generating or adjusting the raster maps,
  • - v.db.addtable - to add a new table to an existing vector map, -
  • -
  • - v.db.connect - to find details about attribute storage, -
  • -
  • - v.db.join - to add columns from one table to another, -
  • -
  • - v.db.select - to obtain values from an attribute and test WHERE clauses. + g.region + to set the computational region before the computation.
-

AUTHOR

-Vaclav Petras, NCSU Center for Geospatial Analytics +Vaclav Petras, NCSU Center for Geospatial Analytics, GeoForAll Lab From e12ed0eb8f95946f4dda39761738de42c4b5e19c Mon Sep 17 00:00:00 2001 From: Vaclav Petras Date: Tue, 18 Jul 2023 14:34:48 -0400 Subject: [PATCH 09/10] Process lines only. Fix typo. --- src/vector/v.rast.move/tests/conftest.py | 24 +++++++++++++++++++ .../v.rast.move/tests/test_v_rast_move.py | 15 ++++++++++++ src/vector/v.rast.move/v.rast.move.html | 3 ++- src/vector/v.rast.move/v.rast.move.py | 6 +++-- 4 files changed, 45 insertions(+), 3 deletions(-) diff --git a/src/vector/v.rast.move/tests/conftest.py b/src/vector/v.rast.move/tests/conftest.py index 1ddb52a880..84b20610e6 100644 --- a/src/vector/v.rast.move/tests/conftest.py +++ b/src/vector/v.rast.move/tests/conftest.py @@ -31,6 +31,24 @@ 1 2 """ +MIX = """\ +VERTI: +L 9 1 + 0.0984456 0.61658031 + 0.15025907 0.68264249 + 0.2642487 0.76943005 + 0.39119171 0.79792746 + 0.52202073 0.80181347 + 0.62953368 0.78367876 + 0.68134715 0.73056995 + 0.71243523 0.64637306 + 0.73445596 0.54663212 + 1 1 +P 1 1 + 0.09455000 0.40540000 + 1 3 +""" + @pytest.fixture(scope="module") def line_dataset(tmp_path_factory): @@ -38,6 +56,7 @@ def line_dataset(tmp_path_factory): tmp_path = tmp_path_factory.mktemp("line_dataset") location = "test" lines_name = "lines" + mix_name = "mix" gs.core._create_location_xy(tmp_path, location) # pylint: disable=protected-access with gs.setup.init(tmp_path / location): gs.write_command( @@ -54,6 +73,10 @@ def line_dataset(tmp_path_factory): gs.mapcalc(f"{y_name} = {y_value}") gs.mapcalc(f"{nulls_name} = 0.0 + null()") + gs.write_command( + "v.in.ascii", input="-", output=mix_name, stdin=MIX, format="standard" + ) + yield SimpleNamespace( name=lines_name, x=x_name, @@ -61,4 +84,5 @@ def line_dataset(tmp_path_factory): x_value=x_value, y_value=y_value, nulls=nulls_name, + mix_name=mix_name, ) diff --git a/src/vector/v.rast.move/tests/test_v_rast_move.py b/src/vector/v.rast.move/tests/test_v_rast_move.py index 771e008812..cbbec40822 100644 --- a/src/vector/v.rast.move/tests/test_v_rast_move.py +++ b/src/vector/v.rast.move/tests/test_v_rast_move.py @@ -70,3 +70,18 @@ def test_nulls_fail(line_dataset): output=result, nulls="error", ) + +def test_non_lines_ignored(line_dataset): + """Check geometry of the result""" + result = "result_mix" + gs.run_command( + "v.rast.move", + input=line_dataset.mix_name, + x_raster=line_dataset.x, + y_raster=line_dataset.y, + output=result, + ) + metadata = gs.vector_info(result) + assert metadata["lines"] == 1 + assert metadata["points"] == 0 + assert metadata["nodes"] == 2 \ No newline at end of file diff --git a/src/vector/v.rast.move/v.rast.move.html b/src/vector/v.rast.move/v.rast.move.html index 20a5141677..60af494107 100644 --- a/src/vector/v.rast.move/v.rast.move.html +++ b/src/vector/v.rast.move/v.rast.move.html @@ -1,7 +1,8 @@

DESCRIPTION

v.rast.move takes values from raster maps and adds them to X and Y -coordinates of features in a vector map vertex by vertex. +coordinates of features in a vector map vertex by vertex. Works on lines +only, other features are ignored and not included in the result. Null values in rasters are turned into zeros by default and a warning is generated. This behavior can be modified by the nulls option to diff --git a/src/vector/v.rast.move/v.rast.move.py b/src/vector/v.rast.move/v.rast.move.py index 80c4ddc9c8..02213728ee 100644 --- a/src/vector/v.rast.move/v.rast.move.py +++ b/src/vector/v.rast.move/v.rast.move.py @@ -55,7 +55,7 @@ class NullValue(RuntimeError): class OutOfBounds(RuntimeError): - """Raised when vector extent is large than region (raster) extent""" + """Raised when vector extent is larger than region (raster) extent""" def move_vector( @@ -74,7 +74,7 @@ def move_vector( from grass.pygrass.utils import coor2pixel from grass.pygrass.vector import VectorTopo from grass.pygrass.vector.geometry import Line, Point - from grass.lib.vector import Vect_cat_set, Vect_cat_get + from grass.lib.vector import Vect_cat_set, Vect_cat_get, GV_LINE x_raster = RasterSegment(x_raster_name) x_raster.open("r") @@ -89,6 +89,8 @@ def move_vector( output_vector_name, mode="w" ) as output_vector: for feature in input_vector: + if feature.gtype != GV_LINE: + continue first_cat = ctypes.c_int() Vect_cat_get(feature.c_cats, 1, ctypes.byref(first_cat)) new_point_list = [] From 3dbe0beb1b3aecca2d9171e29c86e20486282e28 Mon Sep 17 00:00:00 2001 From: Vaclav Petras Date: Tue, 18 Jul 2023 15:04:00 -0400 Subject: [PATCH 10/10] Fix whitespace --- src/vector/v.rast.move/tests/test_v_rast_move.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/vector/v.rast.move/tests/test_v_rast_move.py b/src/vector/v.rast.move/tests/test_v_rast_move.py index cbbec40822..8414da6676 100644 --- a/src/vector/v.rast.move/tests/test_v_rast_move.py +++ b/src/vector/v.rast.move/tests/test_v_rast_move.py @@ -71,6 +71,7 @@ def test_nulls_fail(line_dataset): nulls="error", ) + def test_non_lines_ignored(line_dataset): """Check geometry of the result""" result = "result_mix" @@ -84,4 +85,4 @@ def test_non_lines_ignored(line_dataset): metadata = gs.vector_info(result) assert metadata["lines"] == 1 assert metadata["points"] == 0 - assert metadata["nodes"] == 2 \ No newline at end of file + assert metadata["nodes"] == 2