Skip to content

Commit a0f8fd4

Browse files
committed
PERF: vectorize shapely_to_path coordinate extraction
Rather than pulling out each sub-geometry .xy's coordinates, we can do one get_coordinates() call and fill in the codes all at once.
1 parent 96c513e commit a0f8fd4

2 files changed

Lines changed: 43 additions & 12 deletions

File tree

lib/cartopy/mpl/path.py

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
from matplotlib.path import Path
1616
import numpy as np
17+
import shapely
1718
import shapely.geometry as sgeom
1819

1920

@@ -106,20 +107,20 @@ def shapely_to_path(shape):
106107
if shape.is_empty:
107108
return Path(np.empty([0, 2]))
108109
elif isinstance(shape, sgeom.LinearRing):
109-
return Path(np.column_stack(shape.xy), closed=True)
110+
return Path(shapely.get_coordinates(shape), closed=True)
110111
elif isinstance(shape, (sgeom.LineString, sgeom.Point)):
111-
return Path(np.column_stack(shape.xy))
112+
return Path(shapely.get_coordinates(shape))
112113
elif isinstance(shape, sgeom.Polygon):
113-
def poly_codes(poly):
114-
codes = np.ones(len(poly.xy[0])) * Path.LINETO
115-
codes[0] = Path.MOVETO
116-
codes[-1] = Path.CLOSEPOLY
117-
return codes
118-
vertices = np.concatenate([np.array(shape.exterior.xy)] +
119-
[np.array(ring.xy) for ring in
120-
shape.interiors], 1).T
121-
codes = np.concatenate([poly_codes(shape.exterior)] +
122-
[poly_codes(ring) for ring in shape.interiors])
114+
rings = [shape.exterior, *shape.interiors]
115+
# Shapely rings include the closing duplicate point, so len(ring.coords)
116+
# gives the number of vertices contributed by each ring.
117+
counts = np.fromiter((len(ring.coords) for ring in rings), dtype=np.intp,
118+
count=len(rings))
119+
vertices = shapely.get_coordinates(shape)
120+
codes = np.full(len(vertices), Path.LINETO, dtype=Path.code_type)
121+
starts = np.cumsum(counts) - counts
122+
codes[starts] = Path.MOVETO
123+
codes[starts + counts - 1] = Path.CLOSEPOLY
123124
return Path(vertices, codes)
124125
elif isinstance(shape, (sgeom.MultiPolygon, sgeom.GeometryCollection,
125126
sgeom.MultiLineString, sgeom.MultiPoint)):

lib/cartopy/tests/mpl/test_path.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
from matplotlib.path import Path
77
import numpy as np
8+
from numpy.testing import assert_array_equal
89
import pytest
910
import shapely.geometry as sgeom
1011

@@ -102,3 +103,32 @@ def test_non_polygon_path_closing(self, path):
102103
closed_path = cpath._ensure_path_closed(path)
103104
assert isinstance(closed_path, Path)
104105
assert closed_path.vertices.size == 0
106+
107+
108+
class Test_shapely_to_path:
109+
def test_polygon_with_multiple_interiors(self):
110+
exterior = sgeom.box(0, 0, 12, 12).exterior.coords
111+
interiors = [sgeom.box(1, 1, 2, 2, ccw=False).exterior.coords,
112+
sgeom.box(4, 4, 5, 6, ccw=False).exterior.coords,
113+
sgeom.box(8, 8, 9, 10, ccw=False).exterior.coords]
114+
poly = sgeom.Polygon(exterior, interiors)
115+
116+
path = cpath.shapely_to_path(poly)
117+
118+
rings = [poly.exterior, *poly.interiors]
119+
expected_vertices = np.concatenate([np.asarray(ring.coords)
120+
for ring in rings])
121+
assert_array_equal(path.vertices, expected_vertices)
122+
123+
# Each ring is 5 vertices long (a closed box), so the codes for
124+
# each ring should be MOVETO, LINETO, LINETO, LINETO, CLOSEPOLY.
125+
expected_codes = np.tile(
126+
[Path.MOVETO, Path.LINETO, Path.LINETO, Path.LINETO, Path.CLOSEPOLY],
127+
len(rings))
128+
assert_array_equal(path.codes, expected_codes)
129+
assert path.codes.dtype == Path.code_type
130+
131+
# The path should round-trip back to an equivalent geometry.
132+
result = cpath.path_to_shapely(path)
133+
assert isinstance(result, sgeom.Polygon)
134+
assert result.equals(poly)

0 commit comments

Comments
 (0)