From b8330557540cf28bcf90ba6e30c2d5b21c846622 Mon Sep 17 00:00:00 2001 From: Ruth Comer <10599679+rcomer@users.noreply.github.com> Date: Sat, 30 Mar 2024 11:01:36 +0000 Subject: [PATCH] Add InsetIndicator artist --- ci/mypy-stubtest-allowlist.txt | 3 + doc/api/index.rst | 1 + doc/api/inset_api.rst | 8 + .../next_api_changes/behavior/27996-REC.rst | 8 + doc/users/next_whats_new/inset_indicator.rst | 18 ++ lib/matplotlib/axes/_axes.py | 110 +++---- lib/matplotlib/axes/_axes.pyi | 9 +- lib/matplotlib/inset.py | 269 ++++++++++++++++++ lib/matplotlib/inset.pyi | 25 ++ lib/matplotlib/meson.build | 2 + .../zoom_inset_connector_styles.png | Bin 0 -> 18960 bytes lib/matplotlib/tests/test_axes.py | 15 +- lib/matplotlib/tests/test_inset.py | 106 +++++++ 13 files changed, 493 insertions(+), 81 deletions(-) create mode 100644 doc/api/inset_api.rst create mode 100644 doc/api/next_api_changes/behavior/27996-REC.rst create mode 100644 doc/users/next_whats_new/inset_indicator.rst create mode 100644 lib/matplotlib/inset.py create mode 100644 lib/matplotlib/inset.pyi create mode 100644 lib/matplotlib/tests/baseline_images/test_inset/zoom_inset_connector_styles.png create mode 100644 lib/matplotlib/tests/test_inset.py diff --git a/ci/mypy-stubtest-allowlist.txt b/ci/mypy-stubtest-allowlist.txt index 06261a543f99..4b6e487a418d 100644 --- a/ci/mypy-stubtest-allowlist.txt +++ b/ci/mypy-stubtest-allowlist.txt @@ -46,3 +46,6 @@ matplotlib.spines.Spine._T # Parameter inconsistency due to 3.10 deprecation matplotlib.figure.FigureBase.get_figure + +# getitem method only exists for 3.10 deprecation backcompatability +matplotlib.inset.InsetIndicator.__getitem__ diff --git a/doc/api/index.rst b/doc/api/index.rst index 53f397a6817a..76b6cd5ffcef 100644 --- a/doc/api/index.rst +++ b/doc/api/index.rst @@ -104,6 +104,7 @@ Alphabetical list of modules: gridspec_api.rst hatch_api.rst image_api.rst + inset_api.rst layout_engine_api.rst legend_api.rst legend_handler_api.rst diff --git a/doc/api/inset_api.rst b/doc/api/inset_api.rst new file mode 100644 index 000000000000..d8b89a106a7a --- /dev/null +++ b/doc/api/inset_api.rst @@ -0,0 +1,8 @@ +******************** +``matplotlib.inset`` +******************** + +.. automodule:: matplotlib.inset + :members: + :undoc-members: + :show-inheritance: diff --git a/doc/api/next_api_changes/behavior/27996-REC.rst b/doc/api/next_api_changes/behavior/27996-REC.rst new file mode 100644 index 000000000000..fe81a34073b8 --- /dev/null +++ b/doc/api/next_api_changes/behavior/27996-REC.rst @@ -0,0 +1,8 @@ +``InsetIndicator`` artist +~~~~~~~~~~~~~~~~~~~~~~~~~ + +`~.Axes.indicate_inset` and `~.Axes.indicate_inset_zoom` now return an instance +of `~matplotlib.inset.InsetIndicator`. Use the +`~matplotlib.inset.InsetIndicator.rectangle` and +`~matplotlib.inset.InsetIndicator.connectors` properties of this artist to +access the objects that were previously returned directly. diff --git a/doc/users/next_whats_new/inset_indicator.rst b/doc/users/next_whats_new/inset_indicator.rst new file mode 100644 index 000000000000..614e830e016c --- /dev/null +++ b/doc/users/next_whats_new/inset_indicator.rst @@ -0,0 +1,18 @@ +``InsetIndicator`` artist +~~~~~~~~~~~~~~~~~~~~~~~~~ + +`~.Axes.indicate_inset` and `~.Axes.indicate_inset_zoom` now return an instance +of `~matplotlib.inset.InsetIndicator` which contains the rectangle and +connector patches. These patches now update automatically so that + +.. code-block:: python + + ax.indicate_inset_zoom(ax_inset) + ax_inset.set_xlim(new_lim) + +now gives the same result as + +.. code-block:: python + + ax_inset.set_xlim(new_lim) + ax.indicate_inset_zoom(ax_inset) diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index 33fc42a4b860..d7b649ae437f 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -16,6 +16,7 @@ import matplotlib.contour as mcontour import matplotlib.dates # noqa: F401, Register date unit converter as side effect. import matplotlib.image as mimage +import matplotlib.inset as minset import matplotlib.legend as mlegend import matplotlib.lines as mlines import matplotlib.markers as mmarkers @@ -419,9 +420,9 @@ def inset_axes(self, bounds, *, transform=None, zorder=5, **kwargs): return inset_ax @_docstring.interpd - def indicate_inset(self, bounds, inset_ax=None, *, transform=None, + def indicate_inset(self, bounds=None, inset_ax=None, *, transform=None, facecolor='none', edgecolor='0.5', alpha=0.5, - zorder=4.99, **kwargs): + zorder=None, **kwargs): """ Add an inset indicator to the Axes. This is a rectangle on the plot at the position indicated by *bounds* that optionally has lines that @@ -433,18 +434,19 @@ def indicate_inset(self, bounds, inset_ax=None, *, transform=None, Parameters ---------- - bounds : [x0, y0, width, height] + bounds : [x0, y0, width, height], optional Lower-left corner of rectangle to be marked, and its width - and height. + and height. If not set, the bounds will be calculated from the + data limits of *inset_ax*, which must be supplied. - inset_ax : `.Axes` + inset_ax : `.Axes`, optional An optional inset Axes to draw connecting lines to. Two lines are drawn connecting the indicator box to the inset Axes on corners chosen so as to not overlap with the indicator box. transform : `.Transform` Transform for the rectangle coordinates. Defaults to - `ax.transAxes`, i.e. the units of *rect* are in Axes-relative + ``ax.transAxes``, i.e. the units of *rect* are in Axes-relative coordinates. facecolor : :mpltype:`color`, default: 'none' @@ -469,15 +471,20 @@ def indicate_inset(self, bounds, inset_ax=None, *, transform=None, Returns ------- - rectangle_patch : `.patches.Rectangle` - The indicator frame. + inset_indicator : `.inset.InsetIndicator` + An artist which contains - connector_lines : 4-tuple of `.patches.ConnectionPatch` - The four connector lines connecting to (lower_left, upper_left, - lower_right upper_right) corners of *inset_ax*. Two lines are - set with visibility to *False*, but the user can set the - visibility to True if the automatic choice is not deemed correct. + inset_indicator.rectangle : `.Rectangle` + The indicator frame. + inset_indicator.connectors : 4-tuple of `.patches.ConnectionPatch` + The four connector lines connecting to (lower_left, upper_left, + lower_right upper_right) corners of *inset_ax*. Two lines are + set with visibility to *False*, but the user can set the + visibility to True if the automatic choice is not deemed correct. + + .. versionchanged:: 3.10 + Previously the rectangle and connectors tuple were returned. """ # to make the Axes connectors work, we need to apply the aspect to # the parent Axes. @@ -487,51 +494,13 @@ def indicate_inset(self, bounds, inset_ax=None, *, transform=None, transform = self.transData kwargs.setdefault('label', '_indicate_inset') - x, y, width, height = bounds - rectangle_patch = mpatches.Rectangle( - (x, y), width, height, + indicator_patch = minset.InsetIndicator( + bounds, inset_ax=inset_ax, facecolor=facecolor, edgecolor=edgecolor, alpha=alpha, zorder=zorder, transform=transform, **kwargs) - self.add_patch(rectangle_patch) - - connects = [] - - if inset_ax is not None: - # connect the inset_axes to the rectangle - for xy_inset_ax in [(0, 0), (0, 1), (1, 0), (1, 1)]: - # inset_ax positions are in axes coordinates - # The 0, 1 values define the four edges if the inset_ax - # lower_left, upper_left, lower_right upper_right. - ex, ey = xy_inset_ax - if self.xaxis.get_inverted(): - ex = 1 - ex - if self.yaxis.get_inverted(): - ey = 1 - ey - xy_data = x + ex * width, y + ey * height - p = mpatches.ConnectionPatch( - xyA=xy_inset_ax, coordsA=inset_ax.transAxes, - xyB=xy_data, coordsB=self.transData, - arrowstyle="-", zorder=zorder, - edgecolor=edgecolor, alpha=alpha) - connects.append(p) - self.add_patch(p) - - # decide which two of the lines to keep visible.... - pos = inset_ax.get_position() - bboxins = pos.transformed(self.get_figure(root=False).transSubfigure) - rectbbox = mtransforms.Bbox.from_bounds( - *bounds - ).transformed(transform) - x0 = rectbbox.x0 < bboxins.x0 - x1 = rectbbox.x1 < bboxins.x1 - y0 = rectbbox.y0 < bboxins.y0 - y1 = rectbbox.y1 < bboxins.y1 - connects[0].set_visible(x0 ^ y0) - connects[1].set_visible(x0 == y1) - connects[2].set_visible(x1 == y0) - connects[3].set_visible(x1 ^ y1) - - return rectangle_patch, tuple(connects) if connects else None + self.add_artist(indicator_patch) + + return indicator_patch def indicate_inset_zoom(self, inset_ax, **kwargs): """ @@ -555,22 +524,23 @@ def indicate_inset_zoom(self, inset_ax, **kwargs): Returns ------- - rectangle_patch : `.patches.Rectangle` - Rectangle artist. - - connector_lines : 4-tuple of `.patches.ConnectionPatch` - Each of four connector lines coming from the rectangle drawn on - this axis, in the order lower left, upper left, lower right, - upper right. - Two are set with visibility to *False*, but the user can - set the visibility to *True* if the automatic choice is not deemed - correct. + inset_indicator : `.inset.InsetIndicator` + An artist which contains + + inset_indicator.rectangle : `.Rectangle` + The indicator frame. + + inset_indicator.connectors : 4-tuple of `.patches.ConnectionPatch` + The four connector lines connecting to (lower_left, upper_left, + lower_right upper_right) corners of *inset_ax*. Two lines are + set with visibility to *False*, but the user can set the + visibility to True if the automatic choice is not deemed correct. + + .. versionchanged:: 3.10 + Previously the rectangle and connectors tuple were returned. """ - xlim = inset_ax.get_xlim() - ylim = inset_ax.get_ylim() - rect = (xlim[0], ylim[0], xlim[1] - xlim[0], ylim[1] - ylim[0]) - return self.indicate_inset(rect, inset_ax, **kwargs) + return self.indicate_inset(None, inset_ax, **kwargs) @_docstring.interpd def secondary_xaxis(self, location, functions=None, *, transform=None, **kwargs): diff --git a/lib/matplotlib/axes/_axes.pyi b/lib/matplotlib/axes/_axes.pyi index 186177576067..de89990d7c13 100644 --- a/lib/matplotlib/axes/_axes.pyi +++ b/lib/matplotlib/axes/_axes.pyi @@ -15,6 +15,7 @@ from matplotlib.colors import Colormap, Normalize from matplotlib.container import BarContainer, ErrorbarContainer, StemContainer from matplotlib.contour import ContourSet, QuadContourSet from matplotlib.image import AxesImage, PcolorImage +from matplotlib.inset import InsetIndicator from matplotlib.legend import Legend from matplotlib.legend_handler import HandlerBase from matplotlib.lines import Line2D, AxLine @@ -74,17 +75,17 @@ class Axes(_AxesBase): ) -> Axes: ... def indicate_inset( self, - bounds: tuple[float, float, float, float], + bounds: tuple[float, float, float, float] | None = ..., inset_ax: Axes | None = ..., *, transform: Transform | None = ..., facecolor: ColorType = ..., edgecolor: ColorType = ..., alpha: float = ..., - zorder: float = ..., + zorder: float | None = ..., **kwargs - ) -> Rectangle: ... - def indicate_inset_zoom(self, inset_ax: Axes, **kwargs) -> Rectangle: ... + ) -> InsetIndicator: ... + def indicate_inset_zoom(self, inset_ax: Axes, **kwargs) -> InsetIndicator: ... def secondary_xaxis( self, location: Literal["top", "bottom"] | float, diff --git a/lib/matplotlib/inset.py b/lib/matplotlib/inset.py new file mode 100644 index 000000000000..bab69491303e --- /dev/null +++ b/lib/matplotlib/inset.py @@ -0,0 +1,269 @@ +""" +The inset module defines the InsetIndicator class, which draws the rectangle and +connectors required for `.Axes.indicate_inset` and `.Axes.indicate_inset_zoom`. +""" + +from . import _api, artist, transforms +from matplotlib.patches import ConnectionPatch, PathPatch, Rectangle +from matplotlib.path import Path + + +_shared_properties = ('alpha', 'edgecolor', 'linestyle', 'linewidth') + + +class InsetIndicator(artist.Artist): + """ + An artist to highlight an area of interest. + + An inset indicator is a rectangle on the plot at the position indicated by + *bounds* that optionally has lines that connect the rectangle to an inset + Axes (`.Axes.inset_axes`). + + .. versionadded:: 3.10 + """ + zorder = 4.99 + + def __init__(self, bounds=None, inset_ax=None, zorder=None, **kwargs): + """ + Parameters + ---------- + bounds : [x0, y0, width, height], optional + Lower-left corner of rectangle to be marked, and its width + and height. If not set, the bounds will be calculated from the + data limits of inset_ax, which must be supplied. + + inset_ax : `~.axes.Axes`, optional + An optional inset Axes to draw connecting lines to. Two lines are + drawn connecting the indicator box to the inset Axes on corners + chosen so as to not overlap with the indicator box. + + zorder : float, default: 4.99 + Drawing order of the rectangle and connector lines. The default, + 4.99, is just below the default level of inset Axes. + + **kwargs + Other keyword arguments are passed on to the `.Rectangle` patch. + """ + if bounds is None and inset_ax is None: + raise ValueError("At least one of bounds or inset_ax must be supplied") + + self._inset_ax = inset_ax + + if bounds is None: + # Work out bounds from inset_ax + self._auto_update_bounds = True + bounds = self._bounds_from_inset_ax() + else: + self._auto_update_bounds = False + + x, y, width, height = bounds + + self._rectangle = Rectangle((x, y), width, height, clip_on=False, **kwargs) + + # Connector positions cannot be calculated till the artist has been added + # to an axes, so just make an empty list for now. + self._connectors = [] + + super().__init__() + self.set_zorder(zorder) + + # Initial style properties for the artist should match the rectangle. + for prop in _shared_properties: + setattr(self, f'_{prop}', artist.getp(self._rectangle, prop)) + + def _shared_setter(self, prop, val): + """ + Helper function to set the same style property on the artist and its children. + """ + setattr(self, f'_{prop}', val) + + artist.setp([self._rectangle, *self._connectors], prop, val) + + def set_alpha(self, alpha): + # docstring inherited + self._shared_setter('alpha', alpha) + + def set_edgecolor(self, color): + """ + Set the edge color of the rectangle and the connectors. + + Parameters + ---------- + color : :mpltype:`color` or None + """ + self._shared_setter('edgecolor', color) + + def set_color(self, c): + """ + Set the edgecolor of the rectangle and the connectors, and the + facecolor for the rectangle. + + Parameters + ---------- + c : :mpltype:`color` + """ + self._shared_setter('edgecolor', c) + self._shared_setter('facecolor', c) + + def set_linewidth(self, w): + """ + Set the linewidth in points of the rectangle and the connectors. + + Parameters + ---------- + w : float or None + """ + self._shared_setter('linewidth', w) + + def set_linestyle(self, ls): + """ + Set the linestyle of the rectangle and the connectors. + + ========================================== ================= + linestyle description + ========================================== ================= + ``'-'`` or ``'solid'`` solid line + ``'--'`` or ``'dashed'`` dashed line + ``'-.'`` or ``'dashdot'`` dash-dotted line + ``':'`` or ``'dotted'`` dotted line + ``'none'``, ``'None'``, ``' '``, or ``''`` draw nothing + ========================================== ================= + + Alternatively a dash tuple of the following form can be provided:: + + (offset, onoffseq) + + where ``onoffseq`` is an even length tuple of on and off ink in points. + + Parameters + ---------- + ls : {'-', '--', '-.', ':', '', (offset, on-off-seq), ...} + The line style. + """ + self._shared_setter('linestyle', ls) + + def _bounds_from_inset_ax(self): + xlim = self._inset_ax.get_xlim() + ylim = self._inset_ax.get_ylim() + return (xlim[0], ylim[0], xlim[1] - xlim[0], ylim[1] - ylim[0]) + + def _update_connectors(self): + (x, y) = self._rectangle.get_xy() + width = self._rectangle.get_width() + height = self._rectangle.get_height() + + existing_connectors = self._connectors or [None] * 4 + + # connect the inset_axes to the rectangle + for xy_inset_ax, existing in zip([(0, 0), (0, 1), (1, 0), (1, 1)], + existing_connectors): + # inset_ax positions are in axes coordinates + # The 0, 1 values define the four edges if the inset_ax + # lower_left, upper_left, lower_right upper_right. + ex, ey = xy_inset_ax + if self.axes.xaxis.get_inverted(): + ex = 1 - ex + if self.axes.yaxis.get_inverted(): + ey = 1 - ey + xy_data = x + ex * width, y + ey * height + if existing is None: + # Create new connection patch with styles inherited from the + # parent artist. + p = ConnectionPatch( + xyA=xy_inset_ax, coordsA=self._inset_ax.transAxes, + xyB=xy_data, coordsB=self.axes.transData, + arrowstyle="-", + edgecolor=self._edgecolor, alpha=self.get_alpha(), + linestyle=self._linestyle, linewidth=self._linewidth) + self._connectors.append(p) + else: + # Only update positioning of existing connection patch. We + # do not want to override any style settings made by the user. + existing.xy1 = xy_inset_ax + existing.xy2 = xy_data + existing.coords1 = self._inset_ax.transAxes + existing.coords2 = self.axes.transData + + if existing is None: + # decide which two of the lines to keep visible.... + pos = self._inset_ax.get_position() + bboxins = pos.transformed(self.get_figure(root=False).transSubfigure) + rectbbox = transforms.Bbox.from_bounds(x, y, width, height).transformed( + self._rectangle.get_transform()) + x0 = rectbbox.x0 < bboxins.x0 + x1 = rectbbox.x1 < bboxins.x1 + y0 = rectbbox.y0 < bboxins.y0 + y1 = rectbbox.y1 < bboxins.y1 + self._connectors[0].set_visible(x0 ^ y0) + self._connectors[1].set_visible(x0 == y1) + self._connectors[2].set_visible(x1 == y0) + self._connectors[3].set_visible(x1 ^ y1) + + @property + def rectangle(self): + """`.Rectangle`: the indicator frame.""" + return self._rectangle + + @property + def connectors(self): + """ + 4-tuple of `.patches.ConnectionPatch` or None + The four connector lines connecting to (lower_left, upper_left, + lower_right upper_right) corners of *inset_ax*. Two lines are + set with visibility to *False*, but the user can set the + visibility to True if the automatic choice is not deemed correct. + """ + if self._inset_ax is None: + return + + if self._auto_update_bounds: + self._rectangle.set_bounds(self._bounds_from_inset_ax()) + self._update_connectors() + return tuple(self._connectors) + + def draw(self, renderer): + # docstring inherited + conn_same_style = [] + + # Figure out which connectors have the same style as the box, so should + # be drawn as a single path. + for conn in self.connectors or []: + if conn.get_visible(): + drawn = False + for s in _shared_properties: + if artist.getp(self._rectangle, s) != artist.getp(conn, s): + # Draw this connector by itself + conn.draw(renderer) + drawn = True + break + + if not drawn: + # Connector has same style as box. + conn_same_style.append(conn) + + if conn_same_style: + # Since at least one connector has the same style as the rectangle, draw + # them as a compound path. + artists = [self._rectangle] + conn_same_style + paths = [a.get_transform().transform_path(a.get_path()) for a in artists] + path = Path.make_compound_path(*paths) + + # Create a temporary patch to draw the path. + p = PathPatch(path) + p.update_from(self._rectangle) + p.set_transform(transforms.IdentityTransform()) + p.draw(renderer) + + return + + # Just draw the rectangle + self._rectangle.draw(renderer) + + @_api.deprecated( + '3.10', + message=('Since Matplotlib 3.10 indicate_inset_[zoom] returns a single ' + 'InsetIndicator artist with a rectangle property and a connectors ' + 'property. From 3.12 it will no longer be possible to unpack the ' + 'return value into two elements.')) + def __getitem__(self, key): + return [self._rectangle, self.connectors][key] diff --git a/lib/matplotlib/inset.pyi b/lib/matplotlib/inset.pyi new file mode 100644 index 000000000000..e895fd7be27c --- /dev/null +++ b/lib/matplotlib/inset.pyi @@ -0,0 +1,25 @@ +from . import artist +from .axes import Axes +from .backend_bases import RendererBase +from .patches import ConnectionPatch, Rectangle + +from .typing import ColorType, LineStyleType + +class InsetIndicator(artist.Artist): + def __init__( + self, + bounds: tuple[float, float, float, float] | None = ..., + inset_ax: Axes | None = ..., + zorder: float | None = ..., + **kwargs + ) -> None: ... + def set_alpha(self, alpha: float | None) -> None: ... + def set_edgecolor(self, color: ColorType | None) -> None: ... + def set_color(self, c: ColorType | None) -> None: ... + def set_linewidth(self, w: float | None) -> None: ... + def set_linestyle(self, ls: LineStyleType | None) -> None: ... + @property + def rectangle(self) -> Rectangle: ... + @property + def connectors(self) -> tuple[ConnectionPatch, ConnectionPatch, ConnectionPatch, ConnectionPatch] | None: ... + def draw(self, renderer: RendererBase) -> None: ... diff --git a/lib/matplotlib/meson.build b/lib/matplotlib/meson.build index 657adfd4e835..e8cf4d129f8d 100644 --- a/lib/matplotlib/meson.build +++ b/lib/matplotlib/meson.build @@ -43,6 +43,7 @@ python_sources = [ 'gridspec.py', 'hatch.py', 'image.py', + 'inset.py', 'layout_engine.py', 'legend_handler.py', 'legend.py', @@ -110,6 +111,7 @@ typing_sources = [ 'gridspec.pyi', 'hatch.pyi', 'image.pyi', + 'inset.pyi', 'layout_engine.pyi', 'legend_handler.pyi', 'legend.pyi', diff --git a/lib/matplotlib/tests/baseline_images/test_inset/zoom_inset_connector_styles.png b/lib/matplotlib/tests/baseline_images/test_inset/zoom_inset_connector_styles.png new file mode 100644 index 0000000000000000000000000000000000000000..df8b768725d7b6a3d2554ac452f617bf34593923 GIT binary patch literal 18960 zcmeHvby$>Z*Y5*}xDmxbLD~dGK&2al7Le|el2(|ZOHoj1kuH%EkPrl9Xp!z5LPC&k zW{`%n#=YP7`}RKXch3LkI{R{8T*^H6tb5%nf4_AI(`gX0E= ziK!zGKR27X884s7O(P>CUTzMPTQ_cT-{8N=f1UXe3ia4QkdxEq-;Ze*WomNv_DW zQ-D7lr7WWsG?_mu>tyV`WYO}_%+bvDFpVKwm0@U>HffW};%?Xh6dQHL-G5ieCJ|qr5{LdrsH%$s6_+8b*Bi`^&XYV415G0R=m>EI5 zYfm6V@Q^QZ1VIdM{{Kh*v&O;_mx#S}Z;FZK((&0odU%KYP)e;tyP^ z=H|<_mEPB`UF*_4)^(ZY^K9jABysISTvPkOFQIwfSUkSl>;0QIJ=nvBY_Y9%W+zxr zz50-madk`7R^m_f9YZUs`2N9RUH)RDaFp6!|KPFGQg3e*OB) zNV%J(t*wHRn)no0b@1%I$LvNWpF#0JN*K%KQjvWXv)g6zN5+uFqGT>%;p>sjwfMsk z5(jk76j@oIKCg|2$X1ZzOY4yaT@ewH*R}oeiH#IB9j*c8nPg9qFJzsMR70grn#JRPbp1_`*y^cl0y%Z zvN7VuIc&<@Dk+)5S#g6P)@hdUWCJAH#>C_vN2HuWI@fTjW{eRH=G@B<%}hPvOWu-c z5pS%RM(Dfg(&xH0^=vO7OVy>z6un_~rGjs5ljh4PDh9VDh-8(O zm7yXZUiCHl;N-5LhxNg9G4u?t+_BI8!mOAZ|FK&lS4Ye`SI#)R#2>bF-0$vjzl$JN zpy!^Ig?*QMd0AX1XNXNiL({#bp4nK$1E#2bX z&A+=Y-iwzdf5l4n>Ht&4aqO6k*KYC_W|)y_PkU4IDznvarI$frVPU{C%7)>UXE~cB zb8BS1T)dI)i61qheKg-(+l@TzXk;p}z18l-xL1LH^Xb{$+(JJ zzw4J$V4iAMA)_ac{7TEnkZEpij){#Oa*S762|L{F<~Dj#VwRmM?TB~PanXIvm;_z}JL|XmKNXcVD7{&*Jv}Lp`K?`}4%XK450jCh zcQ;q5G$IDl&Z^`6>2B+-lC9TQ%h-5|*K_%?P2yZp72Vl~S&}&EAjF^c_wOpSnwk&r ze7f+KhSZ?kwHHMtEGT#_vN<&~Gm}aqQp@bD2EMcPiQX?Vyx-GC_Fz+??wJ5T(OezB zYyRDuml?^3PTaW3&$@_@>cARA>gM$Hk{BMmefxH%|C2E-4b6@6wBlk7^Y#Qa&x*p4 zYZsfdwl}A~V0MW-Mwt5Mwk2ogS5zCO*BhqMSs9p1fokJ2TJu@7{XT24I$2 zLc^9hlQ*SGo*_q~9+$#imPbft9!uL*w$4yDIP%8IFD@<$1wA8q>TPLdD_F}u-7L@%V zWPdIDMHVd1Z@75#r z_IcBPX17X_VHA#^>&zsV_s+ z$5)-H$X{@CaTP4Oa?`4Lc~vUJ@rMq6bGL7@lLmXTZrBd7MF3PXxJj_aW6^2U=fW zuco8Jh7}!UwWjg4J$wOzEIL)_U1&yNhlX}kI-f#sGw3UvH-J@{6~cS)*3 zFSiKW=v0@DRp`k`2`RQ*5Ua>E6ezZn8y=pTmyqaE^!$2S49OZ=WbL1{&v1+^tzCLS z4NE{0RBi9CJJ#^R#j(6gJZ!s9hiYY0v*J3zoX#5SlFn$r+=y-8*KF&X_U3aTgi2?QX8uTXPX{olq^0tIZfvqkz-C z%H+<>emyFeNsL2eUA3u+D1?-jmX=_VJUoSq+jDc7OW#Y9RniqM8~nG-!R3ov;4SZ0 zPWIOi9+~@~VJh?EDVEaO56}kcxPWs(dw;W&^gJsc}#<-)Q(5N0i8N~Hib9cJx4x7u;*VB}?- zInC-y@s~%Q@@}O+*VA&YAOqp3&Yu@iIKyzirbxaHH)l|G}p_8L`Ip<6%S ztg58?PQ@lQ^WyIL8hE&SYMM!8Tf|FrzlRd(zn))~8Eh8>QKn;h&nP>mUdKbNh+IUY zkkitK{CwpXFRWL@r!F8D%+E>gHbkag##t#Oyj1}6l1FY0xufJ0 zzcXSd=Wl!TQFk@DUN3R{cc9m%uS|c;U$%Hq4<`oa-_zN7o-Ucx1Ok}7x>GT}V{cIB z?OSPTjvL#gA8ntQ%2zH$KftcP6J3mdfU>!WFYJkE0yh~ySFAnHD@n@M#@pJ~W{F0l zhYs2w^M8Ex>W^M5mdSaqYJwTLz|z`U%d}AxhMP{!&+q9WVJ^6yn={W>ej0ply>amm z_H5hl-ATnqp!D2cg5YH| zDg_Q`mRe5-%VZZg|61%_uw9^)k?BP|j~ey-opdQkXbcIj^#X0JAbvwcd%*g6aeHtu z1~!Y@##=~MH}D@4|4CnIF!&=w{u;X}{-|cHYfHyU&-~-W(3#ZrYA5yq|`|04|APP4n-)#kf zq=uE|oDX>BO#zFFsr5QakHaAz6g43S`_}p0e1Zj^YAe5o<`xUm{={L{m9ylEZ8v$i zxWXa?31_aMk^bt{D;q5QcaHR;@d+f9>$|OmiQWVnpj{*$?RqLob$s8#%)-u}BB z6dCue!js#wM{)S63|GAsSG;VjToB}Pb&T3Wjg_XRrlNW10BPyy!iE+Vo|smtgZTtt z^}`O66(<}_KbAgNNR+9qoy|I0wM65aG_ooHMP%bn@Z_qqvhKMPk?~#y z2%=?fVyOgb0%I7au%=H*v@4#SoZ#3hV=RA?soysKj9a}#jGUajXJjO2Y00+R>hR&i zs%mPf50{i~(}L>5eSK4`uqlr<1s(6ZE(v7h-x^jUa@I67o2?e{AkwQ8i**-ix9#?4 z=fTXZ&twO=szp$xeQNSiQc+3v^*tOI7-&z8U2e#qcdUP=gf3+Bd8c2#^J@abw9&Y7S@>px|$zf|V zGs&mo;(kygJTc7^BxcTU>UoRb-=X`#Y2>QCUBi@8JR&Qn`nM9u@aibjxJgsx3}Np543mUWCT_ zYU>m97Rh#**vH=Wzz*z>j1VYm+vn9)cqoT>3u2X4#>U=RT3XVOUc3x(*!fd`Te5+S zpqu(QnYrOX%RkzR`ZrcAv`v#BN%ef^O$`g$tQL;m-rhB5Ve&)7Lph+FYoMIfid|(| zMqY#5k8elZxlAV!0jvLzYx=^$ahjpoDa;WLR5L4{nlh7@m$z_naT!`47|j&V*GS-fcR;C%C}?4r*% z*3Iqp`84eJayJbg9-eDgjZ%VqV4b?xwc}D1AG4zkaOKOjl+O<5xpfwzA(Qr<@e*em zrNw@2lAN*1xSLvW{_#&d6qC)5P^I(p(+Lej}1BNf${WAhb$Whe|(Jx^bKe z7P(6GMsyD~ZMBbAO#S-Z&6O(8uCLE{`{83{fGoCGrlQOuc^#E|HTY5g&Ls*na;ary z!twl1?u>fwWY6+AEsv<7(S@>d1CM<6q+ojktuq@hs8cRVDXX6f;psE>ttb=905vg< z&~*2Fm9=kGo9ftVI}9EkFQP0lBUjMWB+DR(l1@%e9?D^ntZo;^QRrzc2oW>iYGc}G zj&>J&dr3`0F1IoPF4%ePW9&8)BV zs^#S5hH~0lTP*Xs&3S+ApK8pD;Ky#3VqTlaXOW72pA8UZ-gg9PEk3w&uG0 zAbrL3+(^yWsKLvNcVLz-eqe?+Txe}wU8<~m19HcUt>+ciN?KYOb#;=%d;{xdG?Mlj zdBbhfXLcY11+4Cj%3VpnApOSDG6kzI2Pb*u2E|=Il9Pn8AHvO#jM|^t7Dfpp!fLrk z^@H^-92!FtBkN>WF9k6Or8PD-N(DU&evIS#^#>fNk-P5pqFpQ2hIh7oKcJKIWAzP+ z^`Q1ySO-qPspe>7;rKJ*a?gAPsA2$sQc)sG;-rT5*bCRn2c}x;q)0GTHa0mU(+0Ie z;wi)17(C@c9&To%mfEF{)863VTN#9E^p?+DVw7UVQbX~4?=QivwlC|0uLQ%X*0P1D z1>zN84bZ#E&^q`)w0Q#PjHS-$WXkZeCre;pbZ)6`S5|IX4kL3ov>0!aa?Pzdwp@)8 z^m0G<&Oza)FQ%1yi)=I}dkmnNn7Fu}@$vDZc^nQ$%gET*nJlHELt}q@R4GIq09*6K z=v)@b$}RWQ3w{Cj*$Mp)w3V?>rPtQ~#HJb{{spS6+n;r4_vyVPS#998S2)Xa_wHT$ z65W7jtM{CBBnB@@R(^`w;u$@tK+Qauvm`z)YiM@WQ_;Zb0anWFP44u7jq9gWN$PUO z@?NFfrc;b|#%O5P1l>3DeEs~2_4p|67Tb;X6|{>sv(f|yA}>F0Efj1{QF5WtU6h_7 zV6}U^j9TE`4>T}77+&sp_hH*G{YNaNH?nwnRs`1nFGLUj!zy9da zBkB4e205_WGjIwL+_Yl9Y6FHD#a5y_IrHToab!~39YPN<^=NFQnb52f&buVJ7rNE| z!l%x)+7`1okR=_Bj-(WMq1iIN0Co#QA#%mK$C^i=b9Zu50PX8 zlZt^_wY{~esK4b41%iujK*q@D4^+D_#2{dQ@5f`!qeqVp<-ivI1r);;9bt>LNq3cH zUg1|3*8&O9rrwG}}DgP@f|ATSx)(fMG)X=2m~D ztFEtKF~O?&#?{po^=m;(vbvv#n7M0e#R!UbDGNj9ih-JmS7djYhtF#C;pY?nAYgj% zLjuct$G}Q2+FidG{%FsC*<#Fi^0VQjyn=$Ir>E!8I^ff=)@kA~#(T+15~mlh$pDV+ zm-UHmYfIV94U>hWO!lJp(wy!QxqPEbPg4DLZOQ{y#!XAhF1=$X2>pw>hY`qzU(J5P{?Yn*McUj$ zy_tZ~Zb-n)`16ZUc%neO;Zi38ppr8tY@1Sdk+9^j(`c{Ia#`4}$E8zHCaKV}H68cy z=^EXO|6E3Y;9+dMyDygT@HtBJp9zOQxZ!UJhw|L?%8wdyznWlr(&EpbQ@-3Xp@ZG? z(;pY0kL$0buBqwr+RN2p+HgFmz>n_7;_VJ>b|34Gz01z7$JYYl1o-QZ)|;&y*;lZ0 zZ1jSo=O{fC`P7>#LH{Pd@m?SwxB@S z)HE~enc82?3_+`9H`6j+g#V1mYDvPy1G*z3=$7)j!|$WAx_Vl&RPfx=aOrQfkc-^+ z95M6p7M4V56_u&^&?7`AcG%8xq0o=C_4d6*XBbtSJ&n87|CxF2o|OxDHa#^}q-UV5 z-36NMg}!jSDTMuqH~)#dce^4DVsazjep@CzlryzjrV0BMXMdpfrNBO`GoGU}v{@{P;nA zFq0y9GYXc=Ds@Wr+GYXVes{CLb9Uc|kD}(m@@8DlrVjxY<0}i%W#ee*h7N(yVAcE* zJagv6wxLL@RQ0Dt$L}(<3=C>EHtflU*0+&w6;jee_KOF?UVZ{jIm* zKGz*M%R(qTM2zW#=#JtRTvM~v0cBp_ZgdS7*eoIy{`l#WQqqG46cunfA7Me@LQ=t! zuT_LSG@v*xYv5s8q5pune2+KTz#7OALZa2ckYH|NR_nt5_h#k~lZxShzmRS!yqzC*&MZk*3X{;hp^DOa4L-`yvmP4#bN)|Wq*EeS$`0L=m7j9MLxNcALrTg$ zpb_Vmm-))C+$3*npI#iD+a#|#y)%=%sUp%sFYt8+J6CtH88c2fCkt83m$&HUkWJUo zRvs|ZZ3oDn7GYCS+NYM(jYXVk{goMP>&W><|+=s-4L-##r2YGPo-)17s z_q7%OmU7-q-x;*-a*)a*l;MEe&NcaW>9B^}h5jeEV!H`NMNe^<0FBJe&4FhE^YYDD zM?YeJmC31Ms+I3@MZIQy&)3%kZ-hF|i#|@(y4z^1oj&I+WW+6nV%oUpqGRun6X6+x}Y?=79w-&iNy%i(is_ME_0td$(t*6_|X%{q`FZNE1;=ZMa%G}@C5JPro z!Ib&w`vMv}DFU8>Eg6?MNNrEi;nu(2k*qBFCm?3`yyCdx3wQlsh~F`pa$TC24fXyc zCorz-nSEGnarqS*iY9PDUk2BRd&sv0cJ@VEe6tKmiK!?iP0==ZehrYU`J)!59uxcT zPam6vjEosupNY!i4pbwSmJWEf{Jq53RsJEB?t>~NIp`|BCBUw_zQ=93IuxLOS;xIpSWPD&aa6-P7yCQjmYPdztL(9ES0K zpgBnLt8KH zXm@I2jbrq*<#YblsI;-1Za4#tfKcGGzo7ZZW`egJ2PHKrDDH%$I80wk=i(_ewEMsI zC?xipg#GF0^bQSKxylQmDbWVNpLzW^+*H_3vKb*lq! zLVMp>IM;Ot&|v07Z1!|;FKciInWp63yFNfArKP1oo`3_47ji<)^<+?K_^Mun1?T2d z7Zcd^RWIq4HO%mx2FM2}4ir#2WW%=uB_hJC+Qy0F=O1-IW5-$4!z$LEP$x^2(mbWK z0svPL_Wu3*ABu|9Axd4%Ii3MVn&W=3(>3<)^VZIbVMeb(DG7<~xqLxj7eoZJ2(fDS zc3|7}%h)D{E!V7zBeQOQ@f}FxkH;Sh3)O&whmMoPg+U5A(TUQ&@At>#(nmfCfbB!*vb<3i;$BW5%o9I`L3c+vG_R^E zK0}hn1zEAEsE9$T8X)x3r%z{QWyw83OOp~ahg1Wa6_&Kz_O0%GK#+T>zD;h}GLYNx zA5>({dxaA%1lIiMAb`I+zifWvfXUE&N>&TJbUNlP#Y!cHv$K>HbXA>O2(bGlZu$Kv zC%|G4fOAjy6zIo6JvFc{tCP)<@ML~HZ2g+U3YNMmEaNu{3PEPX(F2WE1FCCoW@b!$ zlfFjiNYNKJjqRbz@b{Do$4Xqd_7_IIwm2u+w`!|tqO}Qy7(+*3=YQx_c&WYj!e~g_ zTUw%mp4I%wyK?1)pgPst2dW9{4W-O{><*$bou6nkQ9#D9Y~(rG$um4hIe)OZcJx2v zSpt08#L~gvSe+dm)d4;p(yw9VFFt`Jl?cpISN3nw=JN3r{(PMpj$T)6$+B}-*f2lK zL+$XvOJv`B{l8;bcbL3eLTE@wG3aJO=?WPBRf(dD$kJ`#Em7Ua?0?tQ1wKa7k(_eP z%~RMoQFaEQ2uw`Anu&!PpOkqopJ;mM?B&{1*`hM?YPH+`%ke!f|KiY*C@A9YCUq3e zL!}KPF6T(i1)yrx#h_%M(ALK^_iJEtQ4@^;T#@ zvQ(vLqN?(n7}xyTKWP^YFvrnfzMu0M!!Dg^4$10DD%!q9_^LCdPDCb2U%%b~*Poqg zjZMSiH#V{|GhZt`J`1{eTJxhJn$xi+1k!M&+s|gR`1r0yFyE_T>^UE2yAwhny6<7k z?{pSw+GqcnaB>UZT3J5wi|0l{E)&S=e*@oreLere{Iy;qkCaB;FRpLI{tNz|YrNN- zHNP!yCya+)>yBL3d}CoU&t(GD5&tBsyh0aps)K_=46UqjHIf$$MMHRaxD7TvEsdL( z_e$|l{CHCy(-H5p+(K-S9EA)VR^RhTRRTt~m&5rH3WiFINYd6!UE}8O95hQn68+Mj zCAt#*{QT@0F=6+0fpouPmV_`T_dECRm#?{&yL+|VIwbC2;wVo`&rRTg+u{MT znau~{o!Tdikl8rx0xis*7#Hh!ytq3pG?BDCn^&82s5D ztBbnhKv%H*S5`7e*J?AK3!Pke_7StyQ(I7@(tE$tKfTUr7nrKEcM`=h{ssSj1r^YUEYo6~?Y>erAm zukdh=*F({B+e`ObSPY#0F}t<(=CHR9jQs5Ec>==$+fz{i#rv$z0P=5H2z?nfE5+VzTI1JPne~KbBKRqgn&?P)5bC~+TS^3u2 zuNBM2mdZDJUrV5r7aA5IGzbazCap|G!hdL3kVh64op}7`h{(udy~OxwV?Bi;g! zU`7nz9iQ8n(rNb}a}W6CzlOOgE)XF)P(xsI4WMKB#P;ReYs$#>$DpE~(MsIY_R*aL z)Pm;R+(4VNi56rrg5eK#pw`f(@~%lGlGJ6kQ1CpE*YZtKMXH>D{(%Wf))Pmp3CxYy z9=2E(y$jJ^>D{icIdn_&RLA)QWDBcYb; z6tQ#1az+^Vv}+4wmkfQcpxExYinz$@o#?sK~Vf}&>sXl0)nZjN_*{3cX=Hw z%Q_CMt&RU!p6T>C*l6d-nTzDB@9AaeS>CD99(}vr#w|FJ1bR!74I)Flbsqp~4E|z) zLZOD%HLs@)72D1K##7_Y!wJmwT%Ao93%4B&av}+)57o`_+1dCDG9zRnC#O5wdZR}O z3<8+`R+%l28m^lO`T71YmczorVradjL?AFAUX0TBVT|lN@PEKx`z4j3u zG_^3*BhdSR$ig%ijB%aZ*0z)*486g?A+$1NI!6%Z2~~CVOlYxT5O&mY7~pC={sws) za;WM$AMcgD;Dhro?yM>;H->f9`tOu>8vmZ*I!3@LS&CUcf5s%}3TdmHVp>ihWR|$f z4#e;Y0H@tQfBpo@_{R?;KpihZisd3UJWIzExsUUTIMqI(uM>tToe1{YX|8n7KUkg_ z#CT$<3G+w>!wDVkC}X)ND(FufsJi7@n1?&D4Kw7>NQ}DU0%v(PmWEa2~8mALTK}UZSerPl%m->v1 zZSmIj@3g}7yaIBHZoji_+}~j^GQ{~hlss4iFB{j^j*bvr4(jAFZf3+RdL2E>w14|` zZA-4pQpu)oNbAF!cd@?du*G+jwgFeUi*E|$aIp83bRSzoW7gdr0@sdsAl@Dil$4Tc z{3xxQot=l!lGU?!k(OJD9%n~)jnZ-ywyP<}4~G7kMFax>kwt`m8o>E~{ra_N9)NHdaO(PGT(7Jf%>##D1@-*SX#K$k zU?bag(>+k6W_(-Hi%p6#zvEg^pbrx)<&N(aF28WMBtoJu-XsU$J6B$+x~8V)H<_X0 z{Wj!BUth|>bfx5a{ZrEQz;+d=Ja&p;a=Dpx(>s*rR!>9Edzf5UKiJ!lSa+Y72DWYHvbR?Th6a#*4vx9MM!=t#_T53g`#j>#oJlj!mADW}vbC{m z7il+pu(zj|E%BOr+3roNnTm?qr@^`AUTuwtvh;S_NZ=y>@It&7LSX>5)NwI1c?>4X zs5HzO@EX6-ZhOJ_MlT?rkN+HX=+fiRX-V^)H6!TdU}@5L8)r_7-VSkS_6=g%6}NoE zA94yo_-~k+nhM(gYzV>vuE1KMDfU_*W}zCUePRjV6Ql{{XJtE3u8( z`VC%eIj`c%>_Nomn%1Y%rE6>Uglh<}XYW^5*qrDQbKjN+%F1gE^;LvS8xtKp5a~4v zDu(%)jmZPHp%X}=`1P|Y%qJeWxNHRt^)pTGZQ!8a?^`n9rtKZwZ9Pa$+{ZKQZPvR0 z{`=k1P3pqQKl#c_<_fNfiHU@YI_2B+vK_uVabNYg-^59;TSAlD^$qordyM-uLPA1L z!cH7RKgTzeWMJd~eE8NLEt;QR&|xZIEn)0BH(V>><6{Y~Hy$uQ&PD7=>{#ky;Awy4 z#ifGf%GOp=*S+=exZ%wwA=6jWVp`31+UPQ%H5=f=KG!{<+1kuE7&pGo$*BQD2$<#p zrxLt}>T5-nv((gQ$X{HtIpCo^?njs@XW8E1k8fky#tj4^QAD~N7CRZ=GTkc*^a|M8Dw{O*0n(?Dq3s?W14DT8 zq%xq<I#Q45m;fMJ&IKN9WTU%RzgQ)3MP*ZQa|Sy@>|mtYS&yIhDOa(Q0&cQu+37DZXRN_LO^5~|~Jy}e!joP$3f>$VJZ z1^^6WaIkKGTQ&Xk*RFHC0!sE*8AM!F#Wt%c?=P?G=5(>oI^g28HY*ucC^7XS^<#qGxnHY@V3$@9^OB~J2ymUV(5%1@4>4t}oZ`oNM zWa{hdQ&v@4Vn)*XzsI2#WALl>%3hVk{ZqIk{#CVZVOCI3$n68Bkzhsq}|j;X7wgNVdLE66%t+Ou*CQ*(arC(A1C04|s% z4_NaS78ft`lQS|gY4Fo47^w~TA7dzqoVt<25&68wb8VLrpfdVdI2Y)KpW_7!kxiyyk}? z)o#=g@0|r7%JK6Z_jO`XQ}^fQh!EyT82-#kORGZx))ua`!V907nQGk>GkfiSZjTtr zO9V3og8^M9L`6v(sYM4PUle~2!S1aFr>?ERj40x*wqQo(4#;En$B!u=-nU30-^qMW zQhNFO15m_&9_>gN&_zC)6ca|Ul{M9140g#UmM?XvxE1T~e>pL@lM@nxDPJjoL?=q0 zF)<^eNy7o2oVp=RP09qdsjF#fewYY$uCO#PU`jSTi6p)>`j+_X(`OUU*2XYpDJd8+ zK&qO69QkQqavuvb$pqMZOX&vD${#4F`os;Zw3O$BFUfe@e@6Gg14CDNe)^)_97EzU zis8zSbmssH)zO2Bzk_*7CP_7#IkOmyQJ2 z=BNcpV6cS*7_`{jbS7tOqK7F?ZJjXBt>*aL)vfW3 z10uv5fA#ux7~dZX!fK)^tUWc47xj)-ewyby>u=#H`tIvj`vJxUJU?0%=6GJ;>(aag zR~xCkWTj8izDsyWK}Z5g*Smxa=RitDQ5!frkN4H6mT#osk<)+c^sQOm6EydEMr#(nOIsZMba2SCg zcY}h|Jd*E@{v&-7O3!009CkbA53}IXWVDO5uTcltaO$F1L1mTqQ-il);5?(GNR_?N)G~uz4ek#(RZ2 z?RVp|b709GP)p0~T6V;(I1{e(j(q~LW1WVMFXYE!7p@KWH2RZ?U|ce86E67*$)a$K z$6b7qNP&on-Yg50{xVxUKBOV_eiOlQxDFyIia@ z!nmCChfBfUFN7`lIG@DH`deLGQR{AP79@CseMeUlX33XNjMP}v(@y`fC>)qDTd9pj=d37cRhxrA|9RT$~B)i-pMF{88{o|y`_bs;}I zEWGd>BorrS&$TV*<8%og;lQ}G=46{sb+*<_T!4GedF@58f8o7{6IW?U=cjnY2)kzd zbs8>1d)E_A4YkIbU{$KZ2iJR6;`NHyQMiyhg#9@h()eDPdQThm(h0}wSJoLFU+UtQ zC0Ar!CB3Vyr_BoUGJ}+d`Uzj}UF3*6MR)Ts)2^o|#%z0<*9mtjz6{KUA%`P1)-ON; zwxB7QA6uN^A4xO1R2UoiMJm{qG5;h85#kV`fkoTdpZ7EyVZ^wck=U}#82Z9VIN?4) zj7x(E$Re(Oi{frl$%kzwQH9IGxjWrc!@{NVprN&RE?kV2oZ4MJ%Cm&SHu-#y4@`HK zKb*|Sj&HckOe0!*ZhZJpQLcspIQfE!h;SVN@%}7_!tsjjT@WoA*5k9oMZ3J(!Ou() zO%ZA;yTqqNxrT8eF-9e73ib41VMSDHp!{-xpHyL|dtz^H)ZgdQ| zpm5%Mgk1_pz>v`~c@Eo!{9*2j;fz=f{kI443i@(3=Z1xuUl5E{<##(Q@3;y|-Rbs! zE%F|YeX!p&_KQ@znj{~kG#}WG;f}4o^KGBgIJc!K@QB$X@N_a*YSo2P!)4WePJ>HH zu^TIr6c`tAM>u8ipjSDyr7mD;SEeS0%R0)YBAk9dm@%1jj0S}J4)@)?ufsIvR~#E9 zdTsovor&eNt-i5ZV_mdMBa6}7%%Yate3W@d;JZ!o?t!(NjZ~!-;2amG1lD?WiwQ@@ zm;1rnmeCOAJQ3*vKlrvln&6qK0c&LywZ;ez{qaxcC>-+J9Ixi<@p0a)@Gt>iZE(vY z7@T)KsK?s_4DRw$y`1g0BducKtD0)NV^?WbIrIzIJU`0X2n-7&I|PplK94EN`O#P_ zzwy(?`rFHej#f5@pDcWapF3JfLau-SH;lkIsnv_<({F8#@rVD=L*d35%Ch|~b;Ae# z`1_3xi;st^Jhim^Egsn$HG4pKb2)Zj7!w;=#>e?-sHjL)%1U;2im2hy-*0V<-2|75 zl`4ESp(>dc`PsIstzaPKZg{PZpi66Es%18Nax59M)c{4!jdKsjjE2xiY)lNdkWdzM ztla8{nT<@@(|%--t}9-`IIkLz-8~8vI?tX1xO)NH$+hv@%TfNej3HJRLSI7bmO5HF z`pjWc``Y*=sp^hs|kZr;&rZHV)?$zGL}QAN`2u% z*PfpmONNYs`G=@Oq_i};>>@TYHC$Aq;vc*)Wh0<4SxvALB#(=j+54?D3Rkr7*(2}d zgwk+X>v>ol4hInyh2MFE%}j~7n~jLibPfv#7dRVR>rgKftS_VD8GL2|?9yAr-wAiN z*t$YGz|Q0>SnWW-bVI{yKWW zZ~&`GAsaKc`Z7i%Y3YV3vLm_TQyv5JPXluI6{6VwYxd>N)`XJ5@?ONTHI0-`K zoqz|E)UiwppW~wr+CE+DE}fZzM8+$!c72~J;LV-=CK3de7?fvOZuJkgB)hX(pKwfZ zDLXc@|0OsBvch^uoS76HuGM_%a$S)S8u$9>S`ouQS1So3{Kv>`Zx8PBy!X{BWrwev z+$8fG(Wip%0M#eOOG2~05MUZ=|@h( zWmA4_EpiK=3x~i6k*;94P)SggiRDcfG5s~h@|9oHQ%d*wSP339){)`VioIr59MDNgl&NSYyn zvY9=cr)!JMs5O{C2D{-h-`5ukd%``f#d7c0Tgnk)E8-ncUs^~?_=@%xx+9A@5JaP0Amu@y1l?{6gX$^UVxoCrfl^eTF3HV za?uzr8*DY7?t~{1olx?wM~)Wg=F>M$8QDFuS7MvL2^E#WkC%zj8-~R;?$&Z360&fd zXvI6bo?P;|4$-e^TQ)=5LcB?!W+#Awk5I{0)UWPck`db!6YIstY!TSXgN9o@b z4-hQH`}&Dj_kN-TdH#%esK~IhC}gAR$K!?{kKL!&+v;S$Li5u=`wK;lAc%Jlp%VFj z@(nC&-Fo7@uOyRSOC~ptN?i!$R)>RMah`#fSft8Df`-Bl;D&8#DYR_fA_eDsjZ#Wq z{!5(&1qGP^0T`RYYiEl8oe9nnEyFOupP2x3r}+Hj4S4qN@2L3>?shb0fS%CjgT7?=S*(Fq3<*o$Jyhb9l@lRA5ur