diff --git a/cnmaps/_bundled_skills/platforms/codex/cnmaps-python-assistant/SKILL.md b/cnmaps/_bundled_skills/platforms/codex/cnmaps-python-assistant/SKILL.md index df047f5..4761903 100644 --- a/cnmaps/_bundled_skills/platforms/codex/cnmaps-python-assistant/SKILL.md +++ b/cnmaps/_bundled_skills/platforms/codex/cnmaps-python-assistant/SKILL.md @@ -42,6 +42,7 @@ If the user is mixing `cnmaps` with scientific Python or GIS tooling and the res - Draw multiple records or polygons: `draw_maps` - Clip `contourf`: `clip_contours_by_map` - Clip `pcolormesh`: `clip_pcolormesh_by_map` +- Clip `imshow` / hillshade images: `clip_imshow_by_map` - Clip `quiver`: `clip_quiver_by_map` - Clip `scatter`: `clip_scatter_by_map` - Clip contour labels: `clip_clabels_by_map` diff --git a/cnmaps/_bundled_skills/platforms/codex/cnmaps-python-assistant/references/api-cheatsheet.md b/cnmaps/_bundled_skills/platforms/codex/cnmaps-python-assistant/references/api-cheatsheet.md index cd390e3..987c34c 100644 --- a/cnmaps/_bundled_skills/platforms/codex/cnmaps-python-assistant/references/api-cheatsheet.md +++ b/cnmaps/_bundled_skills/platforms/codex/cnmaps-python-assistant/references/api-cheatsheet.md @@ -161,6 +161,10 @@ Use after `ax.pcolormesh`. Use after `ax.quiver`. +### `clip_imshow_by_map(image, map_polygon, ax=None, extent=None, set_extent=False)` + +Use after `ax.imshow`, especially for hillshade or RGB image layers created from DEM data. + ### `clip_streamplot_by_map(streamplot, map_polygon, ax=None, extent=None, set_extent=False)` Use after `ax.streamplot`. diff --git a/cnmaps/_bundled_skills/platforms/codex/cnmaps-python-assistant/references/plotting-patterns.md b/cnmaps/_bundled_skills/platforms/codex/cnmaps-python-assistant/references/plotting-patterns.md index f90a3f4..789dcd9 100644 --- a/cnmaps/_bundled_skills/platforms/codex/cnmaps-python-assistant/references/plotting-patterns.md +++ b/cnmaps/_bundled_skills/platforms/codex/cnmaps-python-assistant/references/plotting-patterns.md @@ -267,6 +267,39 @@ draw_map(china, ax=ax, color="black", linewidth=1.0) plt.show() ``` +## Clip Hillshade Or Imshow Images + +```python +import cartopy.crs as ccrs +import matplotlib.pyplot as plt +from matplotlib.colors import LightSource +from cnmaps import clip_imshow_by_map, draw_map, get_adm_maps +from cnmaps.sample import load_dem + +lons, lats, dem = load_dem() +hillshade = LightSource(azdeg=315, altdeg=45).shade( + dem, + cmap=plt.cm.Greys, + vert_exag=0.8, + blend_mode="overlay", +) +china = get_adm_maps(country="中国", level="国", record="first", only_polygon=True) + +fig = plt.figure(figsize=(10, 10)) +ax = fig.add_subplot(111, projection=ccrs.PlateCarree()) +image = ax.imshow( + hillshade, + extent=[lons.min(), lons.max(), lats.min(), lats.max()], + origin="lower", + transform=ccrs.PlateCarree(), +) + +clip_imshow_by_map(image, china) +draw_map(china, ax=ax, color="black", linewidth=1.0) +ax.set_extent(china.get_extent(), crs=ccrs.PlateCarree()) +plt.show() +``` + ## Clip Streamlines ```python diff --git a/cnmaps/drawing.py b/cnmaps/drawing.py index f595c4b..095817b 100644 --- a/cnmaps/drawing.py +++ b/cnmaps/drawing.py @@ -180,6 +180,26 @@ def clip_pcolormesh_by_map(mesh, map_polygon: MapPolygon, ax=None, extent=None, _set_clip_box_if_possible(mesh, ax) +def clip_imshow_by_map(image, map_polygon: MapPolygon, ax=None, extent=None, set_extent=False): + """ + 使用边界几何裁剪 `ax.imshow()` 返回的图像对象。 + + 参数: + image: `ax.imshow()` 的返回对象。 + map_polygon (MapPolygon): 地图边界对象。 + ax (GeoAxes, 可选): 目标坐标轴。 + extent (tuple, 可选): 可选裁剪范围。 + set_extent (bool, 可选): 是否同步设置坐标轴范围。 + """ + ax = _resolve_axes(image, ax=ax) + + clip = _make_clip_path(map_polygon, ax=ax, extent=extent) + _set_extent_if_needed(ax, extent=extent, set_extent=set_extent) + + image.set_clip_path(clip) + _set_clip_box_if_possible(image, ax) + + def clip_quiver_by_map(quiver, map_polygon: MapPolygon, ax=None, extent=None, set_extent=False): """ 使用边界几何裁剪 `ax.quiver()` 返回的箭矢对象。 diff --git a/docs/source/_static/clip-china-hillshade.png b/docs/source/_static/clip-china-hillshade.png new file mode 100644 index 0000000..79f2361 Binary files /dev/null and b/docs/source/_static/clip-china-hillshade.png differ diff --git a/docs/source/content/api-ref.rst b/docs/source/content/api-ref.rst index ecd1cdc..35b3bea 100644 --- a/docs/source/content/api-ref.rst +++ b/docs/source/content/api-ref.rst @@ -304,6 +304,23 @@ drawing模块主要存放与绘图相关的函数 若为 ``True`` 且同时传入 ``extent``,则自动设置坐标范围。 +.. py:function:: clip_imshow_by_map(image, map_polygon, ax=None, extent=None, set_extent=False) + :module: cnmaps.drawing + + 对 ``imshow`` 图像按地图边界裁剪,常用于山地阴影图(hillshade)或 RGB 栅格底图。 + + :param image: + ``ax.imshow()`` 的返回值。 + :param map_polygon: + 地图边界对象;支持单个 ``MapPolygon``、列表或 ``GeoDataFrame``。 + :param ax: + 坐标轴;默认优先使用 ``image.axes``,拿不到时再回退到当前轴。 + :param extent: + 可选的经纬度范围 ``[left, right, lower, upper]``。 + :param bool set_extent: + 若为 ``True`` 且同时传入 ``extent``,则自动设置坐标范围。 + + .. py:function:: clip_streamplot_by_map(streamplot, map_polygon, ax=None, extent=None, set_extent=False) :module: cnmaps.drawing diff --git a/docs/source/content/usage.rst b/docs/source/content/usage.rst index b5db7a2..e693e7c 100644 --- a/docs/source/content/usage.rst +++ b/docs/source/content/usage.rst @@ -487,6 +487,43 @@ cnmaps可以很方便地对地图进行合并,例如我们可以将北京、 .. image:: ../_static/clip-china-quiver.png +剪切山地阴影图(imshow / hillshade) + +.. code:: python + + import cartopy.crs as ccrs + import matplotlib.pyplot as plt + from matplotlib.colors import LightSource + from cnmaps import get_adm_maps, clip_imshow_by_map, draw_map + from cnmaps.sample import load_dem + + lons, lats, dem = load_dem() + hillshade = LightSource(azdeg=315, altdeg=45).shade( + dem, + cmap=plt.cm.Greys, + vert_exag=0.8, + blend_mode='overlay', + ) + + fig = plt.figure(figsize=(10, 10)) + ax = fig.add_subplot(111, projection=ccrs.PlateCarree()) + map_polygon = get_adm_maps(country='中国', record='first', only_polygon=True) + + image = ax.imshow( + hillshade, + extent=[lons.min(), lons.max(), lats.min(), lats.max()], + origin='lower', + transform=ccrs.PlateCarree(), + ) + + clip_imshow_by_map(image, map_polygon) + draw_map(map_polygon, color='k', linewidth=1) + ax.set_extent(map_polygon.get_extent()) + + plt.show() + +.. image:: ../_static/clip-china-hillshade.png + 剪切流线图(streamplot) .. code:: python diff --git a/tests/test_docs_examples.py b/tests/test_docs_examples.py index 41c344e..e4b6838 100644 --- a/tests/test_docs_examples.py +++ b/tests/test_docs_examples.py @@ -5,10 +5,12 @@ import cartopy.crs as ccrs import matplotlib.pyplot as plt import numpy as np +from matplotlib.colors import LightSource from cnmaps import ( clip_clabels_by_map, clip_contours_by_map, + clip_imshow_by_map, clip_pcolormesh_by_map, clip_quiver_by_map, clip_scatter_by_map, @@ -124,7 +126,7 @@ def test_docs_union_example(): def test_docs_clip_examples(): - """覆盖 usage.rst 中 contourf、pcolormesh、quiver、scatter、clabel 裁剪示例。""" + """覆盖 usage.rst 中 contourf、imshow、pcolormesh、quiver、scatter、clabel 裁剪示例。""" china = get_adm_maps(country="中国", record="first", only_polygon=True) @@ -146,6 +148,25 @@ def test_docs_clip_examples(): draw_map(china, ax=ax, color="k", linewidth=1) _savefig(fig, "usage", "clip-china-contourf.png") + fig = plt.figure(figsize=(10, 10)) + ax = fig.add_subplot(111, projection=ccrs.PlateCarree()) + hillshade = LightSource(azdeg=315, altdeg=45).shade( + dem, + cmap=plt.cm.Greys, + vert_exag=0.8, + blend_mode="overlay", + ) + image = ax.imshow( + hillshade, + extent=[lons_dem.min(), lons_dem.max(), lats_dem.min(), lats_dem.max()], + origin="lower", + transform=ccrs.PlateCarree(), + ) + clip_imshow_by_map(image, china, ax=ax) + draw_map(china, ax=ax, color="k", linewidth=1) + ax.set_extent(china.get_extent()) + _savefig(fig, "usage", "clip-china-hillshade.png") + fig = plt.figure(figsize=(10, 10)) ax = fig.add_subplot(111, projection=ccrs.PlateCarree()) mesh = ax.pcolormesh( diff --git a/tests/test_drawing.py b/tests/test_drawing.py index 4b154ed..074fd99 100644 --- a/tests/test_drawing.py +++ b/tests/test_drawing.py @@ -5,6 +5,7 @@ import numpy as np import matplotlib.pyplot as plt import cartopy.crs as ccrs +from matplotlib.colors import LightSource from matplotlib.patches import FancyArrowPatch from cnmaps import ( @@ -12,6 +13,7 @@ get_adm_names, clip_clabels_by_map, clip_contours_by_map, + clip_imshow_by_map, clip_streamplot_by_map, draw_map, draw_maps, @@ -183,6 +185,42 @@ def test_clip_pcolormesh(): plt.close() +def test_clip_imshow(): + """测试剪切 imshow / hillshade 图.""" + + lons, lats, dem = load_dem() + hillshade = LightSource(azdeg=315, altdeg=45).shade( + dem, + cmap=plt.cm.Greys, + vert_exag=0.8, + blend_mode="overlay", + ) + + for map_arg in map_args: + name = map_arg["name"] + + fig = plt.figure(figsize=(10, 10)) + ax = fig.add_subplot(111, projection=ccrs.PlateCarree()) + map_polygon = get_adm_maps(**map_arg) + + image = ax.imshow( + hillshade, + extent=[lons.min(), lons.max(), lats.min(), lats.max()], + origin="lower", + transform=ccrs.PlateCarree(), + ) + + clip_imshow_by_map(image, map_polygon) + assert image.get_clip_path() is not None + + draw_map(map_polygon, linewidth=1) + ax.set_extent(map_polygon.get_extent(buffer=1)) + savefp = os.path.join("./tmp", "test_clip_imshow", f"{name}.png") + os.makedirs(os.path.dirname(savefp), exist_ok=True) + plt.savefig(savefp, bbox_inches="tight") + plt.close() + + def test_clip_contour(): """测试剪切等值线."""