Skip to content

Commit 27da50e

Browse files
committed
Removed debugging junk, added comments about algorithm, reduced duplicate code
1 parent 813d2eb commit 27da50e

File tree

5 files changed

+102
-105
lines changed

5 files changed

+102
-105
lines changed

lib/cartopy/crs.py

Lines changed: 48 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -312,84 +312,17 @@ def _project_multiline(self, geometry, src_crs):
312312
else:
313313
return []
314314

315-
def _project_multipolygon(self, geometry, src_crs):
316-
# Project the polygon exterior/interior rings.
317-
# Each source ring will result in either a ring, or one or more
318-
# lines.
319-
rings = []
320-
multi_lines = []
321-
for polygon in geometry:
322-
if src_crs.is_geodetic():
323-
is_ccw = True
324-
else:
325-
is_ccw = polygon.exterior.is_ccw
326-
for src_ring in [polygon.exterior] + list(polygon.interiors):
327-
p_rings, p_mline = self._project_linear_ring(src_ring, src_crs)
328-
if p_rings:
329-
rings.extend(p_rings)
330-
if len(p_mline) > 0:
331-
multi_lines.append(p_mline)
332-
333-
# Convert any lines to rings by attaching them to the boundary.
334-
if multi_lines:
335-
rings.extend(self._attach_lines_to_boundary(multi_lines,
336-
is_ccw))
337-
338-
# Resolve all the inside vs. outside rings, and convert to the
339-
# final MultiPolygon.
340-
exteriors, interiors = self._rings_to_multi_polygon(rings, is_ccw)
341-
if exteriors or interiors:
342-
if not interiors:
343-
multi_poly = sgeom.MultiPolygon(exteriors)
344-
elif not exteriors:
345-
multi_poly_holes = ops.unary_union(interiors)
346-
boundary_poly = self.domain
347-
multi_poly = boundary_poly.difference(multi_poly_holes)
348-
if isinstance(multi_poly, sgeom.Polygon):
349-
multi_poly = sgeom.MultiPolygon([multi_poly])
350-
else:
351-
multi_poly_bits = ops.unary_union(exteriors)
352-
multi_poly_holes = ops.unary_union(interiors)
353-
holes_envelope = multi_poly_holes.envelope
354-
if holes_envelope.contains(multi_poly_bits.envelope):
355-
print("everything flipped")
356-
multi_poly = holes_envelope.difference(
357-
multi_poly_holes.difference(multi_poly_bits)
358-
)
359-
else:
360-
multi_poly = multi_poly_bits.difference(multi_poly_holes)
361-
if isinstance(multi_poly, sgeom.Polygon):
362-
multi_poly = sgeom.MultiPolygon([multi_poly])
363-
else:
364-
multi_poly = sgeom.MultiPolygon()
365-
# geoms = []
366-
# for geom in geometry.geoms:
367-
# r = self._project_polygon(geom, src_crs)
368-
# if r:
369-
# geoms.extend(r.geoms)
370-
# if geoms:
371-
# result = sgeom.MultiPolygon(geoms)
372-
# else:
373-
# result = sgeom.MultiPolygon()
374-
return multi_poly
375-
376-
def _project_polygon(self, polygon, src_crs):
377-
"""
378-
Return the projected polygon(s) derived from the given polygon.
379-
380-
"""
315+
def _make_rings_lines(self, polygon, rings, multi_lines, src_crs):
381316
# Determine orientation of polygon.
382317
# TODO: Consider checking the internal rings have the opposite
383318
# orientation to the external rings?
384319
if src_crs.is_geodetic():
385320
is_ccw = True
386321
else:
387322
is_ccw = polygon.exterior.is_ccw
388-
# Project the polygon exterior/interior rings.
323+
# Project the polygon exterior/interior rings
389324
# Each source ring will result in either a ring, or one or more
390325
# lines.
391-
rings = []
392-
multi_lines = []
393326
for src_ring in [polygon.exterior] + list(polygon.interiors):
394327
p_rings, p_mline = self._project_linear_ring(src_ring, src_crs)
395328
if p_rings:
@@ -400,7 +333,9 @@ def _project_polygon(self, polygon, src_crs):
400333
# Convert any lines to rings by attaching them to the boundary.
401334
if multi_lines:
402335
rings.extend(self._attach_lines_to_boundary(multi_lines, is_ccw))
336+
return rings, multi_lines, is_ccw
403337

338+
def _construct_multipolygon(self, rings, is_ccw):
404339
# Resolve all the inside vs. outside rings, and convert to the
405340
# final MultiPolygon.
406341
exteriors, interiors = self._rings_to_multi_polygon(rings, is_ccw)
@@ -418,7 +353,6 @@ def _project_polygon(self, polygon, src_crs):
418353
multi_poly_holes = ops.unary_union(interiors)
419354
holes_envelope = multi_poly_holes.envelope
420355
if holes_envelope.contains(multi_poly_bits.envelope):
421-
print("everything flipped")
422356
multi_poly = holes_envelope.difference(
423357
multi_poly_holes.difference(multi_poly_bits)
424358
)
@@ -431,6 +365,31 @@ def _project_polygon(self, polygon, src_crs):
431365

432366
return multi_poly
433367

368+
def _project_multipolygon(self, geometry, src_crs):
369+
rings = []
370+
multi_lines = []
371+
polygons = []
372+
for polygon in geometry:
373+
rings, multi_lines, is_ccw = self._make_rings_lines(
374+
polygon, rings, multi_lines, src_crs
375+
)
376+
polygons.extend(list(self._construct_multipolygon(rings, is_ccw)))
377+
rings = []
378+
multi_lines = []
379+
return sgeom.MultiPolygon(polygons)
380+
381+
def _project_polygon(self, polygon, src_crs):
382+
"""
383+
Return the projected polygon(s) derived from the given polygon.
384+
385+
"""
386+
rings = []
387+
multi_lines = []
388+
rings, multi_lines, is_ccw = self._make_rings_lines(
389+
polygon, rings, multi_lines, src_crs
390+
)
391+
return self._construct_multipolygon(rings, is_ccw)
392+
434393
def _attach_lines_to_boundary(self, multi_line_strings, is_ccw):
435394
"""
436395
Return a list of LinearRings by attaching the ends of the given lines
@@ -627,6 +586,13 @@ def makes_valid_ring(line_string):
627586
return linear_rings
628587

629588
def _rings_to_multi_polygon(self, rings, is_ccw):
589+
# _project_linear_rings sometimes creates multiple exterior rings
590+
# encircling the entire domain or almost the entire domain
591+
# In this case, the method is:
592+
# for each exterior ring find all interior rings contained by it
593+
# create polygons
594+
# if there are leftover interior rings, store them as Polygons to
595+
# be added as holes by upstream multipolygon constructors
630596
exterior_rings = []
631597
interior_rings = []
632598
for ring in rings:
@@ -646,38 +612,35 @@ def _rings_to_multi_polygon(self, rings, is_ccw):
646612
for i, interior_ring in enumerate(interior_rings[:]):
647613
if prep_polygon.contains(interior_ring):
648614
holes.append(interior_ring)
649-
# interior_rings.remove(interior_ring)
650615
interior_ring_flags.append(i)
651616
elif polygon.crosses(interior_ring):
652617
# Likely that we have an invalid geometry such as
653618
# that from #509 or #537.
654619
holes.append(interior_ring)
655-
# interior_rings.remove(interior_ring)
656620
interior_ring_flags.append(i)
657-
# if polygon.bounds == self.domain.bounds and not holes:
658-
# pass
659621
else:
660622
polygon = sgeom.Polygon(exterior_ring, holes=holes)
661623
polygon_bits.append(polygon)
662624
interior_rings = [ring for i, ring in enumerate(interior_rings)
663625
if i not in interior_ring_flags]
664626

665627
extra_holes = []
666-
# Any left over "interior" rings need "inverting" with respect
667-
# to the boundary.
668628
if interior_rings:
669629
boundary_poly = self.domain
670-
x3, y3, x4, y4 = boundary_poly.bounds
671-
bx = (x4 - x3) * 0.1
672-
by = (y4 - y3) * 0.1
673-
x3 -= bx
674-
y3 -= by
675-
x4 += bx
676-
y4 += by
677630
polygon_bits = [poly.buffer(0) for poly in polygon_bits]
678631
if isinstance(self, PlateCarree):
679-
# This should only run when called by
680-
# _ViewClippedPathPatch.draw
632+
# to set the initial plot extent _gen_axes_patch calls
633+
# _ViewClippedPathPatch.draw, which calls
634+
# _rings_to_multi_polygon with a geometry from project_geometry
635+
#
636+
# if set_extent() is not invoked this geometry represents the
637+
# region in [-180, 180, -90, 90] that projects to infinity in
638+
# the target projection
639+
#
640+
# the geometry sometimes is an interior ring with no exterior
641+
# ring
642+
#
643+
# if this happens we have to invert the interior ring
681644
polygon = sgeom.Polygon(interior_rings[0])
682645
# Invert the polygon
683646
polygon = boundary_poly.difference(polygon)
@@ -689,22 +652,6 @@ def _rings_to_multi_polygon(self, rings, is_ccw):
689652
polygon = polygon.buffer(0)
690653
if not polygon.is_empty:
691654
extra_holes.append(polygon)
692-
# if polygon_bits or extra_holes:
693-
# if not extra_holes:
694-
# multi_poly = sgeom.MultiPolygon(polygon_bits)
695-
# elif not polygon_bits:
696-
# multi_poly_holes = ops.unary_union(extra_holes)
697-
# multi_poly = boundary_poly.difference(multi_poly_holes)
698-
# if isinstance(multi_poly, sgeom.Polygon):
699-
# multi_poly = sgeom.MultiPolygon([multi_poly])
700-
# else:
701-
# multi_poly_bits = ops.unary_union(polygon_bits)
702-
# multi_poly_holes = ops.unary_union(extra_holes)
703-
# multi_poly = multi_poly_bits.difference(multi_poly_holes)
704-
# if isinstance(multi_poly, sgeom.Polygon):
705-
# multi_poly = sgeom.MultiPolygon([multi_poly])
706-
# else:
707-
# multi_poly = sgeom.MultiPolygon()
708655
return polygon_bits, extra_holes
709656

710657
def quick_vertices_transform(self, vertices, src_crs):

lib/cartopy/mpl/feature_artist.py

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -145,9 +145,17 @@ def xy_samples(self, lon_sample, lat_sample):
145145
x_samples, x_sep = np.linspace(*x_lims, 400,
146146
endpoint=True, retstep=True)
147147

148+
if 0. not in x_samples:
149+
x_samples = np.insert(x_samples,
150+
np.searchsorted(x_samples, 0.), 0.)
151+
148152
y_samples, y_sep = np.linspace(*y_lims, 400,
149153
endpoint=True, retstep=True)
150154

155+
if 0. not in y_samples:
156+
y_samples = np.insert(y_samples,
157+
np.searchsorted(y_samples, 0.), 0.)
158+
151159
return x_samples, x_sep, y_samples, y_sep
152160
else:
153161
# We need to sample the central longitude and latitude, the
@@ -200,6 +208,10 @@ def build_invalid_geom(self):
200208

201209
fig_ = figure.Figure()
202210
ax_ = fig_.add_subplot()
211+
# Method: create contours around the points that project to infinity
212+
# Then contract the exterior rings and expand the interior rings to
213+
# buffer the contours
214+
# Create Polygons from the rings
203215
fcontour = ax_.contourf(x_grid, y_grid,
204216
inds_bin, levels=[0.5, 1.5])
205217

@@ -209,19 +221,25 @@ def build_invalid_geom(self):
209221
poly = path.to_polygons()
210222
if poly:
211223
# 0th path is polygon exterior
212-
# Following paths are interior rings
224+
# Subsequent paths are interior rings
213225
exterior = poly[0]
214226
exterior = sgeom.LinearRing(exterior)
215227
offset = max(x_sep, y_sep)
216228
exterior = exterior.parallel_offset(offset, "right",
217229
join_style=3)
230+
# Point of discussion:
231+
# parallel_offset can output a MultiLineString from LinearRing
232+
# input. Do we want to catch this behavior?
233+
if isinstance(exterior, sgeom.MultiLineString):
234+
raise NotImplementedError
218235
exterior.coords = list(exterior.coords)[::-1]
219236
interiors = poly[1:]
220237
interiors_shrunk = []
221238
for interior in interiors:
222239
interior = sgeom.LinearRing(interior)
223240
interior_shrunk = interior.parallel_offset(offset, "right",
224241
join_style=3)
242+
# Again the possibility of a MultiLineString
225243
if not interior_shrunk.is_empty:
226244
interior_shrunk.coords = list(
227245
interior_shrunk.coords)[::-1]
@@ -305,10 +323,8 @@ def draw(self, renderer, *args, **kwargs):
305323
if feature_crs not in ax.projection.invalid_geoms.keys():
306324
invalid_geom = self.build_invalid_geom()
307325
ax.projection.invalid_geoms[feature_crs] = invalid_geom
308-
print("ax != feature, missing")
309326
else:
310327
invalid_geom = ax.projection.invalid_geoms[feature_crs]
311-
print("ax != feature, present")
312328
if not geom.is_valid:
313329
geom = geom.buffer(0)
314330
cleaned_geom = geom.difference(invalid_geom)
@@ -348,7 +364,7 @@ def draw(self, renderer, *args, **kwargs):
348364
)
349365
geom_paths.extend(
350366
cpatch.geos_to_path(projected_geom))
351-
if not key in mapping.keys():
367+
if key not in mapping.keys():
352368
mapping[key] = geom_paths
353369
else:
354370
mapping[key].extend(geom_paths)
198 KB
Loading
48.7 KB
Loading

lib/cartopy/tests/mpl/test_features.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from cartopy.io.ogc_clients import _OWSLIB_AVAILABLE
1313

1414
from cartopy.tests.mpl import ImageTesting
15+
from shapely.geometry import Polygon
1516

1617

1718
@pytest.mark.filterwarnings("ignore:Downloading")
@@ -65,3 +66,36 @@ def test_wfs():
6566
feature = cfeature.WFSFeature(url, typename,
6667
edgecolor='red')
6768
ax.add_feature(feature)
69+
70+
71+
@pytest.mark.natural_earth
72+
@ImageTesting(['UTM_all_zones'])
73+
def test_utm_all():
74+
# this test fails because of padding (?) differences
75+
# probably solved with an rcParam
76+
zones = range(1, 61)
77+
fig = plt.figure(figsize=(18, 6))
78+
for zone in zones:
79+
ax = fig.add_subplot(1, len(zones), zone,
80+
projection=ccrs.UTM(zone=zone,
81+
southern_hemisphere=True))
82+
ax.add_feature(cfeature.LAND, facecolor="tab:blue")
83+
84+
85+
@pytest.mark.natural_earth
86+
@ImageTesting(['nonplatecarree_with_projected_hole'])
87+
def test_nonpc_hole():
88+
lamaz_box = Polygon(((8.5e5, 1.1e6), (-8.5e5, 1.1e6),
89+
(-5.6e5, -1.2e6), (5.6e5, -1.2e6),
90+
(8.5e5, 1.1e6)))
91+
92+
fig = plt.figure(figsize=(20, 7))
93+
proj1 = ccrs.LambertAzimuthalEqualArea(central_longitude=62.,
94+
central_latitude=-50.)
95+
ax1 = fig.add_subplot(1, 2, 1, projection=proj1)
96+
ax1.add_geometries([lamaz_box], crs=proj1, color="tab:blue")
97+
98+
proj2 = ccrs.LambertAzimuthalEqualArea(central_longitude=-118.,
99+
central_latitude=50.)
100+
ax2 = fig.add_subplot(1, 2, 2, projection=proj2)
101+
ax2.add_geometries([lamaz_box], crs=proj1, color="tab:blue")

0 commit comments

Comments
 (0)