diff --git a/eomaps/eomaps.py b/eomaps/eomaps.py index 144bccd07..9cbcbf8ee 100644 --- a/eomaps/eomaps.py +++ b/eomaps/eomaps.py @@ -26,7 +26,7 @@ import matplotlib as mpl import matplotlib.pyplot as plt import matplotlib.patches as mpatches -from matplotlib.path import Path as mpath +import matplotlib.path as mpath from cartopy import crs as ccrs @@ -1266,16 +1266,53 @@ def set_extent_to_location(self, location, annotate=False, user_agent=None): else: _log.info(f"Centering Map to:\n {r['display_name']}") - def set_frame(self, rounded=0, **kwargs): + def _set_gdf_path_boundary(self, gdf, set_extent=True): + geom = gdf.to_crs(self.crs_plot).unary_union.boundary + + if geom.geom_type == "MultiLineString": + boundary_linestrings = geom.geoms + elif geom.geom_type == "LineString": + boundary_linestrings = [geom] + + vertices, codes = [], [] + for g in boundary_linestrings: + x, y = g.xy + codes.extend( + [mpath.Path.MOVETO, *[mpath.Path.LINETO] * len(x), mpath.Path.CLOSEPOLY] + ) + vertices.extend([(x[0], y[0]), *zip(x, y), (x[-1], y[-1])]) + + path = mpath.Path(vertices, codes) + + self.ax.set_boundary(path, self.ax.transData) + if set_extent: + x0, y0 = np.min(vertices, axis=0) + x1, y1 = np.max(vertices, axis=0) + + self.set_extent([x0, x1, y0, y1], gdf.crs) + + def set_frame(self, rounded=0, gdf=None, set_extent=True, **kwargs): """ Set the properties of the map boundary and the background patch. + You can either use a rectangle with rounded corners or arbitrary geometries + provided as a geopandas.GeoDataFrame. + Parameters ---------- rounded : float, optional If provided, use a rectangle with rounded corners as map boundary line. The corners will be rounded with respect to the provided fraction (0=no rounding, 1=max. radius). The default is None. + gdf : geopandas.GeoDataFrame or path + A geopandas.GeoDataFrame that contains geometries that should be used as + map-frame. + + If a path (string or pathlib.Path) is provided, the corresponding file + will be read as a geopandas.GeoDataFrame and the boundaries of the + contained geometries will be used as map-boundary. + + The default is None. kwargs : Additional kwargs to style the boundary line (e.g. the spine) and the background patch @@ -1298,6 +1335,8 @@ def set_frame(self, rounded=0, **kwargs): >>> m.add_feature.preset.ocean() >>> m.set_frame(fc="r", ec="b", lw=3, rounded=.2) + Customize the map-boundary style + >>> import matplotlib.patheffects as pe >>> m = Maps() >>> m.add_feature.preset.ocean(fc="k") @@ -1306,6 +1345,15 @@ def set_frame(self, rounded=0, **kwargs): >>> rounded=.5, >>> path_effects=[pe.withStroke(linewidth=7, foreground="m")]) + Set the map-boundary to a custom polygon (in this case the boarder of Austria) + + >>> m = Maps() + >>> m.add_feature.preset.land(fc="k") + >>> # Get a GeoDataFrame with all country-boarders from NaturalEarth + >>> gdf = m.add_feature.cultural.admin_0_countries.get_gdf() + >>> # set the map-boundary to the Austrian country-boarder + >>> m.set_frame(gdf = gdf[gdf.NAME=="Austria"]) + """ for key in ("fc", "facecolor"): @@ -1314,7 +1362,14 @@ def set_frame(self, rounded=0, **kwargs): self.ax.spines["geo"].update(kwargs) - if rounded: + if gdf is not None: + assert ( + rounded == 0 + ), "EOmaps: using rounded > 0 is not supported for gdf frames!" + + self._set_gdf_path_boundary(self._handle_gdf(gdf), set_extent=set_extent) + + elif rounded: assert ( rounded <= 1 ), "EOmaps: rounded corner fraction must be between 0 and 1" @@ -1366,14 +1421,13 @@ def cb(*args, **kwargs): y0 + r, ] - path = mpath(np.column_stack((xs, ys))) + path = mpath.Path(np.column_stack((xs, ys))) self.ax.set_boundary(path, transform=self.crs_plot) self.BM._before_fetch_bg_actions.append(cb) self.ax._EOmaps_rounded_spine_attached = True - self.redraw("__SPINES__") - self.redraw("__BG__") + self.redraw() @staticmethod def set_clipboard_kwargs(**kwargs):