Skip to content

Commit 83bf9a6

Browse files
committed
Add public API for coastline drawing
1 parent 5f26169 commit 83bf9a6

File tree

2 files changed

+105
-1
lines changed

2 files changed

+105
-1
lines changed

mosaic/__init__.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,16 @@
11
from __future__ import annotations
22

33
from mosaic import datasets
4+
from mosaic.coastlines import coastlines
45
from mosaic.contour import contour, contourf
56
from mosaic.descriptor import Descriptor
67
from mosaic.polypcolor import polypcolor
78

8-
__all__ = ["Descriptor", "contour", "contourf", "datasets", "polypcolor"]
9+
__all__ = [
10+
"Descriptor",
11+
"coastlines",
12+
"contour",
13+
"contourf",
14+
"datasets",
15+
"polypcolor",
16+
]

mosaic/coastlines.py

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
from __future__ import annotations
2+
3+
import cartopy.feature as cfeature
4+
import numpy as np
5+
import shapely
6+
from cartopy.mpl.geoaxes import GeoAxes
7+
8+
from mosaic.contour import ContourGraph, MPASContourGenerator
9+
from mosaic.descriptor import Descriptor
10+
11+
12+
def coastlines(
13+
ax: GeoAxes, descriptor: Descriptor, color: str = "black", **kwargs
14+
) -> None:
15+
"""
16+
Add coastal **outlines** to the current axes using the
17+
coastline information from the MPAS dataset.
18+
19+
Parameters
20+
----------
21+
ax : Axes
22+
The axes to add the coastlines to.
23+
descriptor : Descriptor
24+
The descriptor containing the projection and dataset information.
25+
**kwargs
26+
Additional keyword arguments to pass to ...
27+
"""
28+
29+
if not isinstance(ax, GeoAxes):
30+
msg = (
31+
"Must provide a `cartopy.mpl.geoaxes` instance for "
32+
"`mosaic.coastlines` to work. "
33+
)
34+
raise TypeError(msg)
35+
36+
kwargs["edgecolor"] = color
37+
kwargs["facecolor"] = "none"
38+
39+
generator = MPASCoastlineGenerator(descriptor)
40+
coastlines = generator.create_coastlines()
41+
42+
geometires = shapely.GeometryCollection(
43+
[shapely.LineString(cl) for cl in coastlines]
44+
)
45+
46+
feature = cfeature.ShapelyFeature(geometires, descriptor.projection)
47+
ax.add_feature(feature, **kwargs)
48+
49+
50+
class MPASCoastlineGenerator(MPASContourGenerator):
51+
def __init__(self, descriptor: Descriptor):
52+
# pass a dummy field to the parent class
53+
super().__init__(descriptor, descriptor.ds.nCells)
54+
55+
self.domain = descriptor.projection.domain
56+
self.boundary = descriptor.projection.boundary
57+
58+
shapely.prepare(self.domain)
59+
60+
def create_coastlines(self) -> np.ndarray:
61+
graph = self._create_coastline_graph()
62+
lines = self._split_and_order_graph(graph)
63+
64+
return self._snap_lines_to_boundary(lines)
65+
66+
def _create_coastline_graph(self) -> ContourGraph:
67+
edge_mask = (self.ds.cellsOnEdge == -1).any("TWO").values
68+
69+
vertex_1 = self.ds.verticesOnEdge[edge_mask].isel(TWO=0).values
70+
vertex_2 = self.ds.verticesOnEdge[edge_mask].isel(TWO=1).values
71+
72+
return ContourGraph(vertex_1, vertex_2)
73+
74+
def _snap_lines_to_boundary(self, lines: np.ndarray) -> np.ndarray:
75+
def snap(point: np.ndarray):
76+
return self.boundary.interpolate(
77+
self.boundary.project(shapely.Point(point))
78+
)
79+
80+
for i, line in enumerate(lines):
81+
# only need to snap lines that are not already closed loops
82+
if np.array_equal(line[0], line[-1]):
83+
continue
84+
85+
contain_mask = shapely.contains_xy(self.domain, *line.T)
86+
if not contain_mask.any():
87+
continue
88+
89+
clipped = line[contain_mask]
90+
p0, p1 = snap(clipped[0]), snap(clipped[-1])
91+
92+
lines[i] = np.concatenate(
93+
[np.array(p0.xy).T, clipped, np.array(p1.xy).T]
94+
)
95+
96+
return lines

0 commit comments

Comments
 (0)