From bbd617a58d5c0f7294c7afbe028dcf5d0f40b125 Mon Sep 17 00:00:00 2001 From: Matthias Fabian Meyer-Bender Date: Wed, 29 Oct 2025 10:01:24 +0000 Subject: [PATCH 01/10] Enabled rendering of shapes with multiple holes --- src/spatialdata_plot/pl/utils.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/spatialdata_plot/pl/utils.py b/src/spatialdata_plot/pl/utils.py index cf21d212..2e2f1272 100644 --- a/src/spatialdata_plot/pl/utils.py +++ b/src/spatialdata_plot/pl/utils.py @@ -1268,7 +1268,7 @@ def _split_multipolygon_into_outer_and_inner(mp: shapely.MultiPolygon): # type: return interior_coords, exterior_coords -def _make_patch_from_multipolygon(mp: shapely.MultiPolygon) -> mpatches.PathPatch: +def _make_patch_from_multipolygon(mp: MultiPolygon) -> mpatches.PathPatch: # https://matplotlib.org/stable/gallery/shapes_and_collections/donut.html patches = [] @@ -1279,18 +1279,23 @@ def _make_patch_from_multipolygon(mp: shapely.MultiPolygon) -> mpatches.PathPatc else: inside, outside = _split_multipolygon_into_outer_and_inner(mp) if len(inside) > 0: - codes = np.ones(len(inside), dtype=mpath.Path.code_type) * mpath.Path.LINETO - codes[0] = mpath.Path.MOVETO - all_codes = np.concatenate((codes, codes)) + codes_inside = np.ones(len(inside), dtype=mpath.Path.code_type) * mpath.Path.LINETO + codes_inside[0] = mpath.Path.MOVETO + codes_outside = np.ones(len(outside), dtype=mpath.Path.code_type) * mpath.Path.LINETO + codes_outside[0] = mpath.Path.MOVETO + all_codes = np.concatenate((codes_inside, codes_outside)) vertices = np.concatenate((outside, inside[::-1])) else: - all_codes = [] - vertices = np.concatenate(outside) + vertices = np.array(outside) + all_codes = np.ones(len(outside), dtype=mpath.Path.code_type) * mpath.Path.LINETO + all_codes[0] = mpath.Path.MOVETO + patches += [mpatches.PathPatch(mpath.Path(vertices, all_codes))] return patches + def _mpl_ax_contains_elements(ax: Axes) -> bool: """Check if any objects have been plotted on the axes object. From 7dd582755f360025d0c89e83d3b48898505595ae Mon Sep 17 00:00:00 2001 From: Matthias Fabian Meyer-Bender Date: Wed, 29 Oct 2025 10:04:10 +0000 Subject: [PATCH 02/10] Fixed formatting --- src/spatialdata_plot/pl/utils.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/spatialdata_plot/pl/utils.py b/src/spatialdata_plot/pl/utils.py index 2e2f1272..144bcd55 100644 --- a/src/spatialdata_plot/pl/utils.py +++ b/src/spatialdata_plot/pl/utils.py @@ -1268,7 +1268,7 @@ def _split_multipolygon_into_outer_and_inner(mp: shapely.MultiPolygon): # type: return interior_coords, exterior_coords -def _make_patch_from_multipolygon(mp: MultiPolygon) -> mpatches.PathPatch: +def _make_patch_from_multipolygon(mp: shapely.MultiPolygon) -> mpatches.PathPatch: # https://matplotlib.org/stable/gallery/shapes_and_collections/donut.html patches = [] @@ -1295,7 +1295,6 @@ def _make_patch_from_multipolygon(mp: MultiPolygon) -> mpatches.PathPatch: return patches - def _mpl_ax_contains_elements(ax: Axes) -> bool: """Check if any objects have been plotted on the axes object. From 337ec3055766835f4e9773facace254f051f39ef Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 29 Oct 2025 10:06:50 +0000 Subject: [PATCH 03/10] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/spatialdata_plot/pl/utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/spatialdata_plot/pl/utils.py b/src/spatialdata_plot/pl/utils.py index 144bcd55..45ff8010 100644 --- a/src/spatialdata_plot/pl/utils.py +++ b/src/spatialdata_plot/pl/utils.py @@ -1280,7 +1280,7 @@ def _make_patch_from_multipolygon(mp: shapely.MultiPolygon) -> mpatches.PathPatc inside, outside = _split_multipolygon_into_outer_and_inner(mp) if len(inside) > 0: codes_inside = np.ones(len(inside), dtype=mpath.Path.code_type) * mpath.Path.LINETO - codes_inside[0] = mpath.Path.MOVETO + codes_inside[0] = mpath.Path.MOVETO codes_outside = np.ones(len(outside), dtype=mpath.Path.code_type) * mpath.Path.LINETO codes_outside[0] = mpath.Path.MOVETO all_codes = np.concatenate((codes_inside, codes_outside)) @@ -1289,7 +1289,7 @@ def _make_patch_from_multipolygon(mp: shapely.MultiPolygon) -> mpatches.PathPatc vertices = np.array(outside) all_codes = np.ones(len(outside), dtype=mpath.Path.code_type) * mpath.Path.LINETO all_codes[0] = mpath.Path.MOVETO - + patches += [mpatches.PathPatch(mpath.Path(vertices, all_codes))] return patches From 7d559f6d41e092e470213c0ba54beea55ec55834 Mon Sep 17 00:00:00 2001 From: Tim Treis Date: Sat, 15 Nov 2025 16:53:40 +0100 Subject: [PATCH 04/10] added typing --- src/spatialdata_plot/pl/utils.py | 75 +++++++++--------- ...nder_multipolygons_with_multiple_holes.png | Bin 0 -> 5601 bytes tests/pl/test_render_shapes.py | 10 +++ 3 files changed, 48 insertions(+), 37 deletions(-) create mode 100644 tests/_images/Shapes_can_render_multipolygons_with_multiple_holes.png diff --git a/src/spatialdata_plot/pl/utils.py b/src/spatialdata_plot/pl/utils.py index 45ff8010..dbc2ff41 100644 --- a/src/spatialdata_plot/pl/utils.py +++ b/src/spatialdata_plot/pl/utils.py @@ -1246,51 +1246,52 @@ def _get_linear_colormap(colors: list[str], background: str) -> list[LinearSegme return [LinearSegmentedColormap.from_list(c, [background, c], N=256) for c in colors] -def _split_multipolygon_into_outer_and_inner(mp: shapely.MultiPolygon): # type: ignore - # https://stackoverflow.com/a/21922058 - - for geom in mp.geoms: - if geom.geom_type == "MultiPolygon": - exterior_coords = [] - interior_coords = [] - for part in geom: - epc = _split_multipolygon_into_outer_and_inner(part) # Recursive call - exterior_coords += epc["exterior_coords"] - interior_coords += epc["interior_coords"] - elif geom.geom_type == "Polygon": - exterior_coords = geom.exterior.coords[:] - interior_coords = [] - for interior in geom.interiors: - interior_coords += interior.coords[:] +def _collect_polygon_rings( + geom: shapely.Polygon | shapely.MultiPolygon, +) -> list[tuple[np.ndarray, list[np.ndarray]]]: + """Collect exterior/interior coordinate rings from (Multi)Polygons.""" + polygons: list[tuple[np.ndarray, list[np.ndarray]]] = [] + + def _collect(part: shapely.Polygon | shapely.MultiPolygon) -> None: + if part.geom_type == "Polygon": + exterior = np.asarray(part.exterior.coords) + interiors = [np.asarray(interior.coords) for interior in part.interiors] + polygons.append((exterior, interiors)) + elif part.geom_type == "MultiPolygon": + for child in part.geoms: + _collect(child) else: - raise ValueError(f"Unhandled geometry type: {repr(geom.type)}") + raise ValueError(f"Unhandled geometry type: {repr(part.geom_type)}") - return interior_coords, exterior_coords + _collect(geom) + return polygons + + +def _create_ring_codes(length: int) -> npt.NDArray[np.uint8]: + codes = np.ones(length, dtype=mpath.Path.code_type) + codes[0] = mpath.Path.MOVETO + return codes def _make_patch_from_multipolygon(mp: shapely.MultiPolygon) -> mpatches.PathPatch: # https://matplotlib.org/stable/gallery/shapes_and_collections/donut.html patches = [] - for geom in mp.geoms: - if len(geom.interiors) == 0: - # polygon has no holes - patches += [mpatches.Polygon(geom.exterior.coords, closed=True)] - else: - inside, outside = _split_multipolygon_into_outer_and_inner(mp) - if len(inside) > 0: - codes_inside = np.ones(len(inside), dtype=mpath.Path.code_type) * mpath.Path.LINETO - codes_inside[0] = mpath.Path.MOVETO - codes_outside = np.ones(len(outside), dtype=mpath.Path.code_type) * mpath.Path.LINETO - codes_outside[0] = mpath.Path.MOVETO - all_codes = np.concatenate((codes_inside, codes_outside)) - vertices = np.concatenate((outside, inside[::-1])) - else: - vertices = np.array(outside) - all_codes = np.ones(len(outside), dtype=mpath.Path.code_type) * mpath.Path.LINETO - all_codes[0] = mpath.Path.MOVETO - - patches += [mpatches.PathPatch(mpath.Path(vertices, all_codes))] + for exterior, interiors in _collect_polygon_rings(mp): + if len(interiors) == 0: + patches.append(mpatches.Polygon(exterior, closed=True)) + continue + + ring_vertices = [exterior] + ring_codes = [_create_ring_codes(len(exterior))] + for hole in interiors: + reversed_hole = hole[::-1] + ring_vertices.append(reversed_hole) + ring_codes.append(_create_ring_codes(len(reversed_hole))) + + vertices = np.concatenate(ring_vertices) + all_codes = np.concatenate(ring_codes) + patches.append(mpatches.PathPatch(mpath.Path(vertices, all_codes))) return patches diff --git a/tests/_images/Shapes_can_render_multipolygons_with_multiple_holes.png b/tests/_images/Shapes_can_render_multipolygons_with_multiple_holes.png new file mode 100644 index 0000000000000000000000000000000000000000..19502783b3e6e7ca731505a3dbe942aace30b1aa GIT binary patch literal 5601 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 Y(WkT_Fi#oALg1yLs&lPC<<^7$0Yyri!~g&Q literal 0 HcmV?d00001 diff --git a/tests/pl/test_render_shapes.py b/tests/pl/test_render_shapes.py index e8b301a1..f89ad36d 100644 --- a/tests/pl/test_render_shapes.py +++ b/tests/pl/test_render_shapes.py @@ -99,6 +99,16 @@ def _make_multi(): sdata["table"] = table sdata.pl.render_shapes(color="val", fill_alpha=0.3).pl.show() + def test_plot_can_render_multipolygons_with_multiple_holes(self): + square = [(0.0, 0.0), (5.0, 0.0), (5.0, 5.0), (0.0, 5.0), (0.0, 0.0)] + first_hole = [(1.0, 1.0), (2.0, 1.0), (2.0, 2.0), (1.0, 2.0), (1.0, 1.0)] + second_hole = [(3.0, 3.0), (4.0, 3.0), (4.0, 4.0), (3.0, 4.0), (3.0, 3.0)] + multipoly = MultiPolygon([Polygon(square, holes=[first_hole, second_hole])]) + cell_polygon_table = gpd.GeoDataFrame(geometry=gpd.GeoSeries([multipoly])) + sd_polygons = ShapesModel.parse(cell_polygon_table) + sdata = SpatialData(shapes={"two_holes": sd_polygons}) + sdata.pl.render_shapes(element="two_holes").pl.show() + def test_plot_can_color_from_geodataframe(self, sdata_blobs: SpatialData): blob = deepcopy(sdata_blobs) blob["table"].obs["region"] = pd.Categorical(["blobs_polygons"] * blob["table"].n_obs) From 9cb5ab6b9b405bd7187d53fe955f712ea6d51894 Mon Sep 17 00:00:00 2001 From: Tim Treis Date: Sat, 15 Nov 2025 17:20:50 +0100 Subject: [PATCH 05/10] fixed test --- src/spatialdata_plot/pl/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/spatialdata_plot/pl/utils.py b/src/spatialdata_plot/pl/utils.py index dbc2ff41..cccb587e 100644 --- a/src/spatialdata_plot/pl/utils.py +++ b/src/spatialdata_plot/pl/utils.py @@ -1268,7 +1268,7 @@ def _collect(part: shapely.Polygon | shapely.MultiPolygon) -> None: def _create_ring_codes(length: int) -> npt.NDArray[np.uint8]: - codes = np.ones(length, dtype=mpath.Path.code_type) + codes = np.full(length, mpath.Path.LINETO, dtype=mpath.Path.code_type) codes[0] = mpath.Path.MOVETO return codes From ad0a37e40f3dda7a710628e5b3586eb650dadc67 Mon Sep 17 00:00:00 2001 From: Tim Treis Date: Sat, 15 Nov 2025 17:58:16 +0100 Subject: [PATCH 06/10] made test prettier --- tests/pl/test_render_shapes.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/pl/test_render_shapes.py b/tests/pl/test_render_shapes.py index f89ad36d..0e23f67e 100644 --- a/tests/pl/test_render_shapes.py +++ b/tests/pl/test_render_shapes.py @@ -107,7 +107,13 @@ def test_plot_can_render_multipolygons_with_multiple_holes(self): cell_polygon_table = gpd.GeoDataFrame(geometry=gpd.GeoSeries([multipoly])) sd_polygons = ShapesModel.parse(cell_polygon_table) sdata = SpatialData(shapes={"two_holes": sd_polygons}) - sdata.pl.render_shapes(element="two_holes").pl.show() + + fig, ax = plt.subplots() + sdata.pl.render_shapes(element="two_holes").pl.show(ax=ax) + ax.set_xlim(-1, 6) + ax.set_ylim(-1, 6) + + fig.tight_layout() def test_plot_can_color_from_geodataframe(self, sdata_blobs: SpatialData): blob = deepcopy(sdata_blobs) From ba21c7ea275404cccdabb814821f1cdf9351e01e Mon Sep 17 00:00:00 2001 From: Tim Treis Date: Sat, 15 Nov 2025 18:03:25 +0100 Subject: [PATCH 07/10] aded second test --- ...olor_multipolygons_with_multiple_holes.png | Bin 0 -> 7150 bytes ...nder_multipolygons_with_multiple_holes.png | Bin 5601 -> 6101 bytes tests/pl/test_render_shapes.py | 24 ++++++++++++++++++ 3 files changed, 24 insertions(+) create mode 100644 tests/_images/Shapes_can_color_multipolygons_with_multiple_holes.png diff --git a/tests/_images/Shapes_can_color_multipolygons_with_multiple_holes.png b/tests/_images/Shapes_can_color_multipolygons_with_multiple_holes.png new file mode 100644 index 0000000000000000000000000000000000000000..e1a372e2a5bad181d64a2171fc8bd2c4e1f328bb GIT binary patch literal 7150 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 NO;uf$Ql&p1{ug~;BKH6Q literal 0 HcmV?d00001 diff --git a/tests/_images/Shapes_can_render_multipolygons_with_multiple_holes.png b/tests/_images/Shapes_can_render_multipolygons_with_multiple_holes.png index 19502783b3e6e7ca731505a3dbe942aace30b1aa..14f75db3387333f0c9c56e1e532910912e958ddc 100644 GIT binary patch literal 6101 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 Y(WkT_Fi#oALg1yLs&lPC<<^7$0Yyri!~g&Q diff --git a/tests/pl/test_render_shapes.py b/tests/pl/test_render_shapes.py index 0e23f67e..ed2ac906 100644 --- a/tests/pl/test_render_shapes.py +++ b/tests/pl/test_render_shapes.py @@ -115,6 +115,30 @@ def test_plot_can_render_multipolygons_with_multiple_holes(self): fig.tight_layout() + def test_plot_can_color_multipolygons_with_multiple_holes(self): + square = [(0.0, 0.0), (5.0, 0.0), (5.0, 5.0), (0.0, 5.0), (0.0, 0.0)] + first_hole = [(1.0, 1.0), (2.0, 1.0), (2.0, 2.0), (1.0, 2.0), (1.0, 1.0)] + second_hole = [(3.0, 3.0), (4.0, 3.0), (4.0, 4.0), (3.0, 4.0), (3.0, 3.0)] + multipoly = MultiPolygon([Polygon(square, holes=[first_hole, second_hole])]) + cell_polygon_table = gpd.GeoDataFrame(geometry=gpd.GeoSeries([multipoly])) + cell_polygon_table["instance_id"] = [0] + sd_polygons = ShapesModel.parse(cell_polygon_table) + + adata = anndata.AnnData(pd.DataFrame({"value": [1]})) + adata.obs["region"] = "two_holes" + adata.obs["instance_id"] = [0] + adata.obs["category"] = ["holey"] + table = TableModel.parse(adata, region="two_holes", region_key="region", instance_key="instance_id") + + sdata = SpatialData(shapes={"two_holes": sd_polygons}, table=table) + + fig, ax = plt.subplots() + sdata.pl.render_shapes(element="two_holes", color="category").pl.show(ax=ax) + ax.set_xlim(-1, 6) + ax.set_ylim(-1, 6) + + fig.tight_layout() + def test_plot_can_color_from_geodataframe(self, sdata_blobs: SpatialData): blob = deepcopy(sdata_blobs) blob["table"].obs["region"] = pd.Categorical(["blobs_polygons"] * blob["table"].n_obs) From 031b57d474768d9608b853d7d1684b64c9874bc0 Mon Sep 17 00:00:00 2001 From: Tim Treis Date: Sat, 15 Nov 2025 18:07:37 +0100 Subject: [PATCH 08/10] fixed typo --- tests/pl/test_render_shapes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/pl/test_render_shapes.py b/tests/pl/test_render_shapes.py index ed2ac906..4707cf91 100644 --- a/tests/pl/test_render_shapes.py +++ b/tests/pl/test_render_shapes.py @@ -130,7 +130,7 @@ def test_plot_can_color_multipolygons_with_multiple_holes(self): adata.obs["category"] = ["holey"] table = TableModel.parse(adata, region="two_holes", region_key="region", instance_key="instance_id") - sdata = SpatialData(shapes={"two_holes": sd_polygons}, table=table) + sdata = SpatialData(shapes={"two_holes": sd_polygons}, tables=table) fig, ax = plt.subplots() sdata.pl.render_shapes(element="two_holes", color="category").pl.show(ax=ax) From e2a5f39b0435ea0ffe7eece1798153f8d6d157ae Mon Sep 17 00:00:00 2001 From: Tim Treis Date: Sat, 15 Nov 2025 18:13:27 +0100 Subject: [PATCH 09/10] fixed test --- tests/pl/test_render_shapes.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/pl/test_render_shapes.py b/tests/pl/test_render_shapes.py index 4707cf91..11984397 100644 --- a/tests/pl/test_render_shapes.py +++ b/tests/pl/test_render_shapes.py @@ -125,15 +125,15 @@ def test_plot_can_color_multipolygons_with_multiple_holes(self): sd_polygons = ShapesModel.parse(cell_polygon_table) adata = anndata.AnnData(pd.DataFrame({"value": [1]})) - adata.obs["region"] = "two_holes" + adata.obs["region"] = pd.Categorical(["two_holes"] * adata.n_obs) adata.obs["instance_id"] = [0] adata.obs["category"] = ["holey"] table = TableModel.parse(adata, region="two_holes", region_key="region", instance_key="instance_id") - sdata = SpatialData(shapes={"two_holes": sd_polygons}, tables=table) + sdata = SpatialData(shapes={"two_holes": sd_polygons}, tables={"table": table}) fig, ax = plt.subplots() - sdata.pl.render_shapes(element="two_holes", color="category").pl.show(ax=ax) + sdata.pl.render_shapes(element="two_holes", color="category", table_name="table").pl.show(ax=ax) ax.set_xlim(-1, 6) ax.set_ylim(-1, 6) From ef29c0783b6a3c6d9b1e25b22b974f4ecb1f72d2 Mon Sep 17 00:00:00 2001 From: Tim Treis Date: Sat, 15 Nov 2025 18:18:14 +0100 Subject: [PATCH 10/10] added image from runner --- ...olor_multipolygons_with_multiple_holes.png | Bin 7150 -> 7691 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/tests/_images/Shapes_can_color_multipolygons_with_multiple_holes.png b/tests/_images/Shapes_can_color_multipolygons_with_multiple_holes.png index e1a372e2a5bad181d64a2171fc8bd2c4e1f328bb..39d3163acc63012d55b4b6d8192bb0b0e7876dab 100644 GIT binary patch literal 7691 zcmcJUcT|&k_Qqd>$Os6dCW%O6cOo!E=543x6qsD2r5k@2qL}L(0fx* zq7*5i3DP@=C`CH_Zrq)nJ!j{?->&B%kQ0);-+S+KKhN_%R#Q#`aM za^NX^@&0lM9$74(YJhJToSZIB)8P)z-PG9vQ8vXn+B)EDt!|%jvv78?alyqG&Xxi<3)!{cRSr8|*K|&Q=Qck9)-!;>^#FC;Ll6>+ywe!*SZDLJaIYX55xi1Rs>&1*Vl?lb` zWA5>kJD;~N+Zw*G&_f}k$0)_n2;yjwER7&#YG(v{-#%vODYk4^lH}*-SDBlei{fmw z$*(;d)soiG(9p8%^Sb8`)B&VlCICTflcYzZb&P$napLaU!Hj&kiBQq`*8vpNhYxpZ zs!Cv&gc5v~AIpcG&MGJneE9I8)#UqUTa(XK=Rdm-*LcqN7pCZ!+)aArP^ahV=_wiB zo*-qcsHpfPBt)y_1>Z&7tlHwI;u+^U+0ksqSsb#Fv9{1>vpo5sYWr*TMbn0ZXh|R4 ziKb9ZWo6}BqR-}36w4Kl0*u?2%swM;WxR+@FYgiN3$kp!tIy!U*&+9#Dp%{lvOGp! z-NcodcAKr$xvI5ZgSAhV30a;i)A&?@x(yk);3uJ>SUI_eFZc}7f4deq3)eH>lcSST z=Qo;da)9pojT=SwRdZ((3Zsm?KH$yUVlR`5cw4I0hrRPyCA>V?{3o9&xSbBuC==4t<+sijvW&Bq*8E7NUTTMPMX<@LL5GdI)f@F=xYmS^rdx5Zr4l##jLOSrWlTz97} z=1}-4sC}8k$X87|Xf&^-+I10+Zzc)8fNy*w)xr4lp8m0;5j0rqFSb$ zJ(;>W=W5r7NleB*rt205{DM#c>~~tDlc1gFSzqEBMdF>Gu*(VSidtRaM5qa zL;2Mu<@FK&`YN}@?6y~;-tA)0>M3bTQS-+8TTvRCnrmH30#=>La`OYFHYxRPqt;_Z zy!JD6!nU6))X$$kKTzje?YFm*AdaSby)L*%Yd+N+dE@3yt-E(KlarH0d^Q}H#H}2s zxcT^cuF*@RmfG}j^YAFqF!FYQc0@jXnh0YfhNgc_Z?all0B4vnN6J_zB(joXFS}@I zYo~wtVmo}t^TgfZn#$#sm5Z+PS|^y8c(}Q5uq%XhyR=K3gW}+>AX4`VqY9Rnm%FRo z9oj$|>NndQV#6qq4LUOcQ$GEY>sPN{jS;cJlhhH!Rj^FjZE+wiUc%cNPK-RlCMJpG z9t=Gtia{Si&O8t>UHkG$bAPkl|2Qiv;j+tgCY+Pk-sViH73nNm%u)LL_wOnS2a)!j|0a{2h+<{Tvr^Z%Ob6?Loxk z5;gosLy1BP9{!u=|6CLZV!-t)a*yx$FUV`g|Hjio_?|8fmcv-~Y|WA~!bzP7f=9*C zee-4HH_Y7F@K|z#{(E4BAR~fx(&G~ou8l`T#n2SUH*}Lx0}6?5+dwf(c$LIlv`N2Y zH*mK(;ygD$zZ>QlB7MlX;mM;%9Vy|G*^`szYF!jR1|8= znXLIS=r?y;RU2={B)q=Uv_Hb2{MthA#T4!ln>CCqi_q;q7zK=Sq3ByzroVeke)p&u zOiN1}sC2PpIG1Z|fsMTJ@zn~&YmsNa9l&V5+36;hY(A!=*Dgk)NJ2Fl8X8ddQ5Fcid_lFOB8BI@q7UeNEwtUeMA5GYXeVZ#YCm0w~6rZ29 znr?kbO-=2LVMR71%xKI(JYpn$t4Kl!vR0AY3!E^W_zzh4ulK;;bkx)nuBWh)G95Iy5G=x%o!M1cKDP>qli}W_FZVkwjdkZ-StyY;JC< zDBNc|Y|VJ4GFIh4bXz~lbNZ#_@#Du+gIK+0sTP<1s(*7^n_aF)6z`SyX;3wBbfYfR zU%pKh=TQ(t8BID9iN(sdwLeY?!Zjyu1_+1nK7|=QFD$GHJ*BCq_mX37A;10wc`46+SGH#aXc^YHR^Z+xG)Xx2nKeY1`>02E^6-=4W6y_Uhf@! z^Wc@_apZN-v#0{-0%3RsekUgdmCotMj>-ynf=f_Y?^Q0O^okBLVe4g zKYxDW*sMeZ&N?^QU)bOdm3bXnCyE%=IN9jN6^kwEQ-v=?^&7eSB9ra#V ztoBU@P*7 zZ+v0>glyd(DC1FBulH?+)#C;2l%6Z#j0r5f`6_`vonB|~(XSb19xs3Feia~O%B>!t zIP`wX`MiKYJFJG$bwVcUQ>9A=Z;Qsv$GEsK6yn$3FSlx7Un$oZM6cuHQ*-~pgSB@5 zJa-Cq^%fs<@rz$>&Q|DLA&d%;K3ztX-d^j&Arn|en-Uk(6 z@u_1tr!or?ssU4a;^@&N*!}a3^imNVaT4CzV`F3bbv~8tLR9pu5iL_)L^glhmB@V$ z*oO63H4Va8{JO}Pzhp;ixm3>|`5I9q(FMst6IC`G0;CL@ol?N?Spn!?aIU~ITRuO2^s z+}zrlyHcs0tD6LHU;Dl36pNtAy(;%5LTl?yK-sJL^?OwuR`cCipetR^*cHGX@Xo;J z>4WhS*new?qCR-fc)&eZmh#;U)}^zY%bH(*E4jjOJ1nx>n{0}F%LdCG^2rWhWPh(5 ziu04>i9xi?$q%ULd*jSTBu^Ya&h`86GNz`c!{OJ0=sfpva^M9Hf!85>QGAKpqCQqj zi;ItMQN#F$g~gZJ0whsrDuG|qL^vwj+FGi&M(^ITYLu0lp;jExZ#|H}AEm@3Ft8o` zj7h*qD?!q?#A|I{Km3;WV(GBIDmW5ViM5D5I2LPQa~3JT8Zg4Pz8_m23jd1uohS5M z%qC=Q2l=_S$$=hE;}eQ)`UtQfy?JtI;&i5 zmi)-IgKqx`4zE4k{J*SR~Zo|6j9YeD3qvx}b8q;k7XVs!LLsHGUX(PofvdT9U0d7zzzNcGN`qPq7W zOc(6@B%>t_?bu~v!^YOk>PGlly6Vh0o2720S7`U$HOU=x^Ass7AMrB)j>7ouRQ%4) z9v9a5I|kXGcCHlR;?EwYbYX~yiXwYVn6wvPD`!~Qo3D)@V?<60KtDqupn3iJqdg$# z%V=USAa0|?TDN8u_;C>1EJy$_|9JS%wBt|mg~bDrfX$cwLg@78GS{v_iSr>t$f~Lu zn7fN0%sbv^U=v{t4P!3|3zH4{;y|gAs^n&>vx`fq`G;eFlKHUb3>uuVcR4I&ZqO%K zzz$gm5^MH_GuI)CD0=MJF%CsyIGe+~7#YgTr04N*ae0Y}XI$sH-u+bLklH`iZh;DQ z?`*DIv}it_AXS9&Zn#fjF}pjC82(0hRQ)|((pMiuH$_H9rUZ!hmW%SD)av+O{6gke z%_dv5pZq$w9c&Srgw-uTy?RMHlin6?_4_pxF1;a^OtOSEcahi#JpOj~Q2Fk|@089z zzUUn}b?GL!da#l@dD|0xL4%c|wkwU-x>M{bV9%9(ZSLfFFijNs{IS+F-M4<5CPly$ zjFR>0Y-e+Od*Aj_{r-a%3Z!EL&NL>51y<(ybLSMmnrMczO2md8L^c@BXePn`=YiSJ zfmz9cwae!7EnvM6y1w`+9YO~V2Z*0TF-FKN5bXQdimtZy3kXKg*C~;Wz(PuVLc$X6 zAR!Z6X9DI{DsG z&l@k|&M@1NYMDb2{vNF68P;$G)^K9=Eq(oLSS3^po7D8qb1+vb#Bbc27gV?FaF}=d z!L)}3t^IBSJH(24UC%%nsu1rr5@Jvv0H0tZyNjPaEAPAg!&UHEKG+fw3*ToXp^&xa z%)8SW2h!WxsYpRNcCwF1LZgCYO90U!xB+NJERYPe#C3ew>C4H>%g%X5wfcCGyB%jB z{lYN+g94oAyU!1K&csQYjo1np)#$>e#hp2UjGk&@ybdG78@%p@x|gL(@mI|F=NtTW zu>ZjXJ&%1nvB&!&$YHNWYDWnbEw3RIc@ z1k;}Q_AS|ng))D~Pfky>L=r{7EJw3Nzwc-0G^+LL(O<~S%IXB!x5*1dkQlI-z$3}I zxx6P%obV))#SWr1l@$3$AP?dKZrpm@+bIhWkGc_-oBO9dy$66 zANwf*jqin@4%$~MQ&LiTE##F)L_|#DP9xH2Fo^dMb~6x(hY8((UV?JbqrCx}{JFaA zuiL}BknX0zQGD7u{bzA?bu(d|8npME{1_*m0eL&@(%?<2lPjcxh6b4o_6^P`3q5T! ze|%S$Mx2<_xcB-(N793a#zx4*$L{~Epr*#~)Fs=e9O%o=vQtx2Sozd1h+6P7$e9G$YmJQ1#+vouRP8j#BiV9u$yco~r)ZSdZ3EUMT8I57UfO)lE zU9u~&>EoB!oO}*;zX#8$6dPt43uvB`nF*{S|5DT;K3z3|KxFm42@!-k*!rU4VywFQ z7*1s0G)W!ii3qnv3RU8-%%Kg6?}T!GfVWL}@S)Wa5JKM!JOL#BAUt#&7y6=%{j44a z1sN#4;4S3PYl#wY$KWT(0b&j?MlU#oS={mZCeB>>BGbl0g{8*`@t5`W+YCcF+Ke zNoE=lcy37ecemC^O`)ga!lre&p2|b$mHhtwd;JOfas%*t%53i){|#2ih^;~tl7ZVT zWUhTFY?*zl+6^ALVQQL04rt(!`MHdQUJ%OeEX{LpiSr~?DOXJVh{;iOn7Hp&30VjT zx2osCa2AuCLcX^Ow{vlEU8k2=eM(Xfarr(jN0uBO9fH=48&5dSU%sq&s|rWX!b)&+ z8bJ(ORMe@@Du(RiF1w`ndO&Od-miBxfYuerCiL`W2!65TIGj*9&62^+Jl0=m2HntI zWYJRNu)7C$COCK3wWXks{Cpp1ln?SLvS4tT?!52;M2Bz;mS>b7Dd~o$>f{@h_F}B9 zt#KRQ#`JC2_sGc%1T@-yes$#R?#JSwIQqj$FvVC1c;WEQ!Aq47=K1YzDdNv6$E3nh zh{bm3K|<{sX7J+OpvS9UDZ8%2*+xW0POS{@x(|CQfEg!cs;8X3c_;8Mj)K`#`^iz} z5wL!Ecz9Z7%r=*&@TttgcQ$r!J6T(2b67#g3>QKaR=dBq%_8QQs9Cr}x87jQVfisu z9ee`$;M<~wu5og5!V>5>BKkQI5;6?B-3hS93%1=w?;p0i<4pD6JagTbhIv!_#$LS& z;Ww@`1T0q}I(5U8uN5ck-c$%X-C5i&nNwyrIKMfQ&EdJ>4*HzMpW7E^qQy~g>b*3n6$~8D_&gbOk!rj1j z=jtJCAo+`M&&LMVr3!JGCzgDJ(v+MZ{3X->ylDxTup{u{lmhib;WB>&{wd0-$P#2s G?*AV?_hgp< literal 7150 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 NO;uf$Ql&p1{ug~;BKH6Q