Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 80 additions & 0 deletions examples/gallery/images/grdpolygon.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
"""
Clipping grid to a complex polygon
==================================
The :func:`pygmt.grdcut` function allows you to extract a subregion from a
grid. In this example we use a complex polygon (GeoDataFrame or GMT ASCII file)
to crop the grid, invert it, or do both.
"""

# %%
import geopandas as gpd
import pygmt
from shapely.geometry import Polygon

fig = pygmt.Figure()

# Define region of interest around Iceland
region = [-28, -10, 62, 68]

# Load sample grid (3 arc-minutes global relief) in target area
grid = pygmt.datasets.load_earth_relief(resolution="03m", region=region)

# Create a more complex polygon (irregular shape) around a smaller ROI
complex_poly = Polygon(
[
(-26, 63),
(-23, 63.5),
(-20, 64),
(-18, 65),
(-19, 66),
(-22, 66.5),
(-25, 66),
(-27, 65),
(-26, 63),
]
)
gdf = gpd.GeoDataFrame({"geometry": [complex_poly]}, crs="EPSG:4326")

# Original grid
fig.basemap(
region=region,
projection="M12c",
frame=["WSne+toriginal grid", "xa5f1", "ya2f1"],
)
fig.grdimage(grid=grid, cmap="oleron")

# Cropped grid
fig.shift_origin(xshift="w+0.5c")
cropped_grid = pygmt.grdcut(grid=grid, polygon=gdf, crop=True)
fig.basemap(
region=region,
projection="M12c",
frame=["WSne+tcropped", "xa5f1", "ya2f1"],
)
fig.grdimage(grid=cropped_grid, cmap="oleron")

# Inverted grid
fig.shift_origin(xshift="w+0.5c")
inverted_grid = pygmt.grdcut(grid=grid, polygon=gdf, invert=True)
fig.basemap(
region=region,
projection="M12c",
frame=["WSne+tinverted", "xa5f1", "ya2f1"],
)
fig.grdimage(grid=inverted_grid, cmap="oleron")

# Cropped + inverted grid
fig.shift_origin(xshift="w+0.5c")
cropped_inverted_grid = pygmt.grdcut(grid=grid, polygon=gdf, crop=True, invert=True)
fig.basemap(
region=region,
projection="M12c",
frame=["WSne+tcropped and inverted", "xa5f1", "ya2f1"],
)
fig.grdimage(grid=cropped_inverted_grid, cmap="oleron")

# Shared colorbar
fig.colorbar(frame=["x+lElevation", "y+lm"], position="JMR+o0.5c/0c+w8c")

fig.show()
30 changes: 28 additions & 2 deletions pygmt/src/grdcut.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"""

from collections.abc import Sequence
from contextlib import ExitStack
from typing import Literal

import xarray as xr
Expand All @@ -14,14 +15,15 @@
build_arg_list,
data_kind,
fmt_docstring,
tempfile_from_geojson,
use_alias,
)

__doctest_skip__ = ["grdcut"]


@fmt_docstring
@use_alias(N="extend", S="circ_subregion", Z="z_subregion", f="coltypes")
@use_alias(N="extend", S="circ_subregion", Z="z_subregion", f="coltypes", F="polygon")
def grdcut(
grid: PathLike | xr.DataArray,
kind: Literal["grid", "image"] = "grid",
Expand All @@ -30,6 +32,8 @@ def grdcut(
region: Sequence[float | str] | str | None = None,
verbose: Literal["quiet", "error", "warning", "timing", "info", "compat", "debug"]
| bool = False,
crop: bool = False,
invert: bool = False,
**kwargs,
) -> xr.DataArray | None:
r"""
Expand Down Expand Up @@ -86,6 +90,17 @@ def grdcut(
NaNs, append **+N** to strip off such columns before (optionally)
considering the range of the core subset for further reduction of the
area.
polygon : str, geopandas.GeoDataFrame, or shapely.geometry
Extract a subregion using a polygon. Can be either:
- A GMT ASCII polygon file (`.gmt`)
- A geopandas.GeoDataFrame (must have CRS EPSG:4326)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if that specific CRS is necessary, of if a CRS is needed actually. Other input types (ASCII file, shapely.geometry.Polygon) don't even have a CRS.

- A shapely.geometry.Polygon or MultiPolygon
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you add a unit test to confirm that MultiPolygon does work?

The polygon can have holes or multiple rings.
Optional modifiers:
- crop : bool
If True, crop the output grid region to the bounding box of the polygon.
- invert : bool
If True, invert the selection, setting all nodes inside the polygon to NaN.

{verbose}
{coltypes}
Expand Down Expand Up @@ -130,12 +145,23 @@ def grdcut(
)
aliasdict.merge(kwargs)

with Session() as lib:
with Session() as lib, ExitStack() as stack:
with (
lib.virtualfile_in(check_kind="raster", data=grid) as vingrd,
lib.virtualfile_out(kind=outkind, fname=outgrid) as voutgrd,
):
aliasdict["G"] = voutgrd

if "F" in kwargs and kwargs["F"] is not None:
polygon_input = kwargs["F"]
modifiers = ("+c" * crop) + ("+i" * invert)

if not isinstance(polygon_input, (str, bytes)):
tmpfile = stack.enter_context(tempfile_from_geojson(polygon_input))
aliasdict["F"] = tmpfile + modifiers
else:
aliasdict["F"] = str(polygon_input) + modifiers

lib.call_module(
module="grdcut", args=build_arg_list(aliasdict, infile=vingrd)
)
Expand Down
Loading