diff --git a/examples/lines_and_polygons/adaptive_resampling_example.py b/examples/lines_and_polygons/adaptive_resampling_example.py new file mode 100644 index 000000000..b0ee53b1b --- /dev/null +++ b/examples/lines_and_polygons/adaptive_resampling_example.py @@ -0,0 +1,84 @@ +""" +Adaptive resampling in Cartopy +============================== + +When a line that is straight in geographic coordinates is projected onto a map, +it can become highly curved. Cartopy's resampling algorithm recursively +subdivides each segment, projecting the source-space midpoint and checking +whether it lands close enough to the straight chord between the projected +endpoints. If not, it bisects and recurses adding enough points until +the curve is approximated to within the specified threshold. + +The acceptance threshold is ``dest_projection.threshold`` (in target projection +units, usually metres). A smaller threshold means more subdivisions and a more +accurate curve; a larger threshold means fewer points and a coarser result. + +This script demonstrates the effect by projecting the same 60°N latitude line +at three different thresholds. +""" + +import matplotlib.pyplot as plt +import numpy as np +import shapely + +import cartopy.crs as ccrs + + +src_crs = ccrs.PlateCarree() +source_line = shapely.LineString([(-100, 60), (100, 60)]) + + +def make_crs(threshold_km): + """Return a fresh LambertConformal CRS with the given threshold (km).""" + crs = ccrs.LambertConformal(central_longitude=0, standard_parallels=(20, 60)) + crs.threshold = threshold_km * 1e3 + return crs + + +dest_crs_default = make_crs(100) + +# Three thresholds to compare (coarse, default, fine) +thresholds_km = [1, 100, 1000] +labels = ["1 km (fine)", "100 km (default)", "1,000 km (coarse)"] +colors = ["red", "green", "blue"] + +# Project each threshold variant and collect (x, y) arrays of accepted pts +results = [] +for thr_km in thresholds_km: + dest = make_crs(thr_km) + projected = dest.project_geometry(source_line, src_crs) + results.append(np.array(projected.geoms[0].coords)) + +ax = plt.figure(figsize=(12, 7), layout="constrained").add_subplot( + 1, 1, 1, projection=dest_crs_default +) + +ax.set_extent([-120, 120, 28, 82], crs=src_crs) +ax.coastlines(linewidth=0.5, color="0.65") +ax.gridlines(linewidth=0.3, color="0.82", linestyle="--") + +for coords, thr_km, label, color in zip(results, thresholds_km, labels, colors): + n = len(coords) + ax.plot( + coords[:, 0], + coords[:, 1], + color=color, + lw=2, + transform=dest_crs_default, + label=f"threshold = {label} ({n} pts)", + ) + ax.scatter( + coords[:, 0], + coords[:, 1], + color=color, + s=40, + transform=dest_crs_default, + ) + +ax.set_title( + "60°N latitude line: PlateCarree → LambertConformal\n" + "Dots show accepted sample points at each threshold." +) +ax.legend(loc="lower center") + +plt.show() diff --git a/lib/cartopy/crs.py b/lib/cartopy/crs.py index 5dda70ee8..f5906f071 100644 --- a/lib/cartopy/crs.py +++ b/lib/cartopy/crs.py @@ -757,6 +757,20 @@ def domain(self): domain = self._domain = sgeom.Polygon(self.boundary) return domain + @property + def _prepared_domain(self): + """Prepared (indexed) version of :attr:`domain`, cached per projection instance. + + Used by :func:`cartopy.trace.project_linear` for fast point-in-polygon + tests. The domain polygon is immutable, so a single prepared instance + is safe to reuse for the projection's lifetime. + """ + try: + return self.__prepared_domain_cache + except AttributeError: + self.__prepared_domain_cache = prep(self.domain) + return self.__prepared_domain_cache + def is_geodetic(self): return False diff --git a/lib/cartopy/tests/mpl/baseline_images/mpl/test_axes/geoaxes_set_boundary_clipping.png b/lib/cartopy/tests/mpl/baseline_images/mpl/test_axes/geoaxes_set_boundary_clipping.png index db9b81a02..a198570a4 100644 Binary files a/lib/cartopy/tests/mpl/baseline_images/mpl/test_axes/geoaxes_set_boundary_clipping.png and b/lib/cartopy/tests/mpl/baseline_images/mpl/test_axes/geoaxes_set_boundary_clipping.png differ diff --git a/lib/cartopy/tests/mpl/baseline_images/mpl/test_axes/geoaxes_subslice.png b/lib/cartopy/tests/mpl/baseline_images/mpl/test_axes/geoaxes_subslice.png index 170b916d7..818a0daa7 100644 Binary files a/lib/cartopy/tests/mpl/baseline_images/mpl/test_axes/geoaxes_subslice.png and b/lib/cartopy/tests/mpl/baseline_images/mpl/test_axes/geoaxes_subslice.png differ diff --git a/lib/cartopy/tests/mpl/baseline_images/mpl/test_boundary/multi_path_boundary.png b/lib/cartopy/tests/mpl/baseline_images/mpl/test_boundary/multi_path_boundary.png index ebfbf8c9f..66262bbe5 100644 Binary files a/lib/cartopy/tests/mpl/baseline_images/mpl/test_boundary/multi_path_boundary.png and b/lib/cartopy/tests/mpl/baseline_images/mpl/test_boundary/multi_path_boundary.png differ diff --git a/lib/cartopy/tests/mpl/baseline_images/mpl/test_crs/igh_land.png b/lib/cartopy/tests/mpl/baseline_images/mpl/test_crs/igh_land.png index 4d523ca9b..f6963bb13 100644 Binary files a/lib/cartopy/tests/mpl/baseline_images/mpl/test_crs/igh_land.png and b/lib/cartopy/tests/mpl/baseline_images/mpl/test_crs/igh_land.png differ diff --git a/lib/cartopy/tests/mpl/baseline_images/mpl/test_crs/igh_ocean.png b/lib/cartopy/tests/mpl/baseline_images/mpl/test_crs/igh_ocean.png index a470d4210..3a9fc827d 100644 Binary files a/lib/cartopy/tests/mpl/baseline_images/mpl/test_crs/igh_ocean.png and b/lib/cartopy/tests/mpl/baseline_images/mpl/test_crs/igh_ocean.png differ diff --git a/lib/cartopy/tests/mpl/baseline_images/mpl/test_crs/lambert_conformal_south.png b/lib/cartopy/tests/mpl/baseline_images/mpl/test_crs/lambert_conformal_south.png index ba46a4ab8..0c49cc6a4 100644 Binary files a/lib/cartopy/tests/mpl/baseline_images/mpl/test_crs/lambert_conformal_south.png and b/lib/cartopy/tests/mpl/baseline_images/mpl/test_crs/lambert_conformal_south.png differ diff --git a/lib/cartopy/tests/mpl/baseline_images/mpl/test_examples/contour_label.png b/lib/cartopy/tests/mpl/baseline_images/mpl/test_examples/contour_label.png index b7e4518c2..2688965d9 100644 Binary files a/lib/cartopy/tests/mpl/baseline_images/mpl/test_examples/contour_label.png and b/lib/cartopy/tests/mpl/baseline_images/mpl/test_examples/contour_label.png differ diff --git a/lib/cartopy/tests/mpl/baseline_images/mpl/test_examples/global_map.png b/lib/cartopy/tests/mpl/baseline_images/mpl/test_examples/global_map.png index 82eb435a1..4d3fa6e37 100644 Binary files a/lib/cartopy/tests/mpl/baseline_images/mpl/test_examples/global_map.png and b/lib/cartopy/tests/mpl/baseline_images/mpl/test_examples/global_map.png differ diff --git a/lib/cartopy/tests/mpl/baseline_images/mpl/test_feature_artist/feature_artist.png b/lib/cartopy/tests/mpl/baseline_images/mpl/test_feature_artist/feature_artist.png index 7398b1e83..701eb1dff 100644 Binary files a/lib/cartopy/tests/mpl/baseline_images/mpl/test_feature_artist/feature_artist.png and b/lib/cartopy/tests/mpl/baseline_images/mpl/test_feature_artist/feature_artist.png differ diff --git a/lib/cartopy/tests/mpl/baseline_images/mpl/test_features/gshhs_coastlines.png b/lib/cartopy/tests/mpl/baseline_images/mpl/test_features/gshhs_coastlines.png index 0ea83d836..2e37da158 100644 Binary files a/lib/cartopy/tests/mpl/baseline_images/mpl/test_features/gshhs_coastlines.png and b/lib/cartopy/tests/mpl/baseline_images/mpl/test_features/gshhs_coastlines.png differ diff --git a/lib/cartopy/tests/mpl/baseline_images/mpl/test_features/natural_earth.png b/lib/cartopy/tests/mpl/baseline_images/mpl/test_features/natural_earth.png index cedc5cabb..4845a7f2c 100644 Binary files a/lib/cartopy/tests/mpl/baseline_images/mpl/test_features/natural_earth.png and b/lib/cartopy/tests/mpl/baseline_images/mpl/test_features/natural_earth.png differ diff --git a/lib/cartopy/tests/mpl/baseline_images/mpl/test_features/natural_earth_custom.png b/lib/cartopy/tests/mpl/baseline_images/mpl/test_features/natural_earth_custom.png index 8df5a6c7c..67bbedd37 100644 Binary files a/lib/cartopy/tests/mpl/baseline_images/mpl/test_features/natural_earth_custom.png and b/lib/cartopy/tests/mpl/baseline_images/mpl/test_features/natural_earth_custom.png differ diff --git a/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/gridliner1.png b/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/gridliner1.png index 309b38c8d..98aa0cec2 100644 Binary files a/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/gridliner1.png and b/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/gridliner1.png differ diff --git a/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/gridliner_constrained_adjust_datalim.png b/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/gridliner_constrained_adjust_datalim.png index 7dcacfbd9..4186b0bb9 100644 Binary files a/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/gridliner_constrained_adjust_datalim.png and b/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/gridliner_constrained_adjust_datalim.png differ diff --git a/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/gridliner_labels.png b/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/gridliner_labels.png index f50bf3ccf..2cc2205d7 100644 Binary files a/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/gridliner_labels.png and b/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/gridliner_labels.png differ diff --git a/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/gridliner_labels_bbox_style.png b/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/gridliner_labels_bbox_style.png index 692cf709b..ad09c56f4 100644 Binary files a/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/gridliner_labels_bbox_style.png and b/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/gridliner_labels_bbox_style.png differ diff --git a/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/gridliner_labels_tight.png b/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/gridliner_labels_tight.png index 94f3a4de5..08e5128d6 100644 Binary files a/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/gridliner_labels_tight.png and b/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/gridliner_labels_tight.png differ diff --git a/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/gridliner_labels_title_adjust.png b/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/gridliner_labels_title_adjust.png index 648f55f6a..43470deae 100644 Binary files a/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/gridliner_labels_title_adjust.png and b/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/gridliner_labels_title_adjust.png differ diff --git a/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/simple_global.png b/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/simple_global.png new file mode 100644 index 000000000..f254970a0 Binary files /dev/null and b/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/simple_global.png differ diff --git a/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_AlbersEqualArea.png b/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_AlbersEqualArea.png index 72d6c7f26..be3cb0014 100644 Binary files a/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_AlbersEqualArea.png and b/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_AlbersEqualArea.png differ diff --git a/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_AzimuthalEquidistant.png b/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_AzimuthalEquidistant.png index 04017b644..e618229b1 100644 Binary files a/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_AzimuthalEquidistant.png and b/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_AzimuthalEquidistant.png differ diff --git a/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_EuroPP.png b/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_EuroPP.png index 7c37b4fd0..5f0be2193 100644 Binary files a/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_EuroPP.png and b/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_EuroPP.png differ diff --git a/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_Geostationary.png b/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_Geostationary.png index 7b8dacd9c..69b342760 100644 Binary files a/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_Geostationary.png and b/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_Geostationary.png differ diff --git a/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_Gnomonic.png b/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_Gnomonic.png index 7ced54860..b6fedc1ca 100644 Binary files a/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_Gnomonic.png and b/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_Gnomonic.png differ diff --git a/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_InterruptedGoodeHomolosine.png b/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_InterruptedGoodeHomolosine.png index 8f368577e..8c390234c 100644 Binary files a/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_InterruptedGoodeHomolosine.png and b/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_InterruptedGoodeHomolosine.png differ diff --git a/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_LambertAzimuthalEqualArea.png b/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_LambertAzimuthalEqualArea.png index 467e9d553..1c705a6fd 100644 Binary files a/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_LambertAzimuthalEqualArea.png and b/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_LambertAzimuthalEqualArea.png differ diff --git a/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_LambertConformal.png b/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_LambertConformal.png index 21db56d1f..3daf7338e 100644 Binary files a/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_LambertConformal.png and b/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_LambertConformal.png differ diff --git a/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_LambertCylindrical.png b/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_LambertCylindrical.png index 2a692bc76..b5a158b51 100644 Binary files a/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_LambertCylindrical.png and b/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_LambertCylindrical.png differ diff --git a/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_Mercator.png b/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_Mercator.png index 7bf8d172f..bedc86af3 100644 Binary files a/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_Mercator.png and b/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_Mercator.png differ diff --git a/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_Miller.png b/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_Miller.png index ffc3f96e8..7b1bfdabc 100644 Binary files a/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_Miller.png and b/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_Miller.png differ diff --git a/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_Mollweide.png b/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_Mollweide.png index a69d10805..e120477c1 100644 Binary files a/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_Mollweide.png and b/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_Mollweide.png differ diff --git a/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_NearsidePerspective.png b/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_NearsidePerspective.png index 72f2fad79..bef27babe 100644 Binary files a/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_NearsidePerspective.png and b/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_NearsidePerspective.png differ diff --git a/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_NorthPolarStereo.png b/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_NorthPolarStereo.png index 543a14f53..ab006d5e5 100644 Binary files a/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_NorthPolarStereo.png and b/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_NorthPolarStereo.png differ diff --git a/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_OSGB.png b/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_OSGB.png index eb7742a7d..40f34bac2 100644 Binary files a/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_OSGB.png and b/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_OSGB.png differ diff --git a/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_OSNI.png b/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_OSNI.png index 633fab51d..c40dec2ed 100644 Binary files a/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_OSNI.png and b/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_OSNI.png differ diff --git a/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_Orthographic.png b/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_Orthographic.png index 23cc4d7bc..2f88b7eac 100644 Binary files a/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_Orthographic.png and b/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_Orthographic.png differ diff --git a/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_PlateCarree.png b/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_PlateCarree.png index c5594ce0c..03b14fab0 100644 Binary files a/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_PlateCarree.png and b/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_PlateCarree.png differ diff --git a/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_Robinson.png b/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_Robinson.png index c16931110..4d0b36c3e 100644 Binary files a/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_Robinson.png and b/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_Robinson.png differ diff --git a/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_RotatedPole.png b/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_RotatedPole.png index 1576c25a6..9051fbd83 100644 Binary files a/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_RotatedPole.png and b/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_RotatedPole.png differ diff --git a/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_Sinusoidal.png b/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_Sinusoidal.png index a101ff7d6..d264de447 100644 Binary files a/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_Sinusoidal.png and b/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_Sinusoidal.png differ diff --git a/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_SouthPolarStereo.png b/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_SouthPolarStereo.png index 610fe4de4..00df1758e 100644 Binary files a/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_SouthPolarStereo.png and b/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_SouthPolarStereo.png differ diff --git a/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_Stereographic.png b/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_Stereographic.png index 8dc2bbd1f..409156354 100644 Binary files a/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_Stereographic.png and b/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_Stereographic.png differ diff --git a/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_usa_AlbersEqualArea.png b/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_usa_AlbersEqualArea.png index 3608133d7..2a18379ec 100644 Binary files a/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_usa_AlbersEqualArea.png and b/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_usa_AlbersEqualArea.png differ diff --git a/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_usa_AzimuthalEquidistant.png b/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_usa_AzimuthalEquidistant.png index 641ed522c..92fd9361b 100644 Binary files a/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_usa_AzimuthalEquidistant.png and b/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_usa_AzimuthalEquidistant.png differ diff --git a/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_usa_Geostationary.png b/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_usa_Geostationary.png index fd03b7fe0..277694329 100644 Binary files a/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_usa_Geostationary.png and b/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_usa_Geostationary.png differ diff --git a/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_usa_Gnomonic.png b/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_usa_Gnomonic.png index 06c3ec4e9..253191bfc 100644 Binary files a/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_usa_Gnomonic.png and b/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_usa_Gnomonic.png differ diff --git a/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_usa_InterruptedGoodeHomolosine.png b/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_usa_InterruptedGoodeHomolosine.png index 5df380342..4ba647bc3 100644 Binary files a/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_usa_InterruptedGoodeHomolosine.png and b/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_usa_InterruptedGoodeHomolosine.png differ diff --git a/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_usa_LambertAzimuthalEqualArea.png b/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_usa_LambertAzimuthalEqualArea.png index 347966db4..d1658e10a 100644 Binary files a/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_usa_LambertAzimuthalEqualArea.png and b/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_usa_LambertAzimuthalEqualArea.png differ diff --git a/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_usa_LambertConformal.png b/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_usa_LambertConformal.png index 87e8c4df8..37bf6748c 100644 Binary files a/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_usa_LambertConformal.png and b/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_usa_LambertConformal.png differ diff --git a/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_usa_LambertCylindrical.png b/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_usa_LambertCylindrical.png index 0cd645721..47885b5fd 100644 Binary files a/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_usa_LambertCylindrical.png and b/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_usa_LambertCylindrical.png differ diff --git a/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_usa_Mercator.png b/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_usa_Mercator.png index 7d449a4f2..1c12d92c5 100644 Binary files a/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_usa_Mercator.png and b/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_usa_Mercator.png differ diff --git a/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_usa_Miller.png b/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_usa_Miller.png index 7f6c9b0ae..d6767a3f5 100644 Binary files a/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_usa_Miller.png and b/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_usa_Miller.png differ diff --git a/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_usa_Mollweide.png b/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_usa_Mollweide.png index ca7af84a4..4508e1cff 100644 Binary files a/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_usa_Mollweide.png and b/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_usa_Mollweide.png differ diff --git a/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_usa_NearsidePerspective.png b/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_usa_NearsidePerspective.png index 1e14bcb0d..3005cd8c4 100644 Binary files a/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_usa_NearsidePerspective.png and b/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_usa_NearsidePerspective.png differ diff --git a/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_usa_NorthPolarStereo.png b/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_usa_NorthPolarStereo.png index 38d26a45c..f8e7f2bb7 100644 Binary files a/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_usa_NorthPolarStereo.png and b/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_usa_NorthPolarStereo.png differ diff --git a/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_usa_Orthographic.png b/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_usa_Orthographic.png index 2117e1574..d933a3a65 100644 Binary files a/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_usa_Orthographic.png and b/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_usa_Orthographic.png differ diff --git a/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_usa_PlateCarree.png b/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_usa_PlateCarree.png index a7cfa4749..d507e7e1e 100644 Binary files a/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_usa_PlateCarree.png and b/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_usa_PlateCarree.png differ diff --git a/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_usa_Robinson.png b/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_usa_Robinson.png index a4b72d239..94e2c0d3f 100644 Binary files a/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_usa_Robinson.png and b/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_usa_Robinson.png differ diff --git a/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_usa_RotatedPole.png b/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_usa_RotatedPole.png index d0a7eebb3..deda65f3a 100644 Binary files a/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_usa_RotatedPole.png and b/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_usa_RotatedPole.png differ diff --git a/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_usa_Sinusoidal.png b/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_usa_Sinusoidal.png index a44f45222..bdc5ddd28 100644 Binary files a/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_usa_Sinusoidal.png and b/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_usa_Sinusoidal.png differ diff --git a/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_usa_SouthPolarStereo.png b/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_usa_SouthPolarStereo.png index b5b8d6252..cc3701536 100644 Binary files a/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_usa_SouthPolarStereo.png and b/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_usa_SouthPolarStereo.png differ diff --git a/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_usa_Stereographic.png b/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_usa_Stereographic.png index 2f5295b52..9885d0b7b 100644 Binary files a/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_usa_Stereographic.png and b/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/test_grid_labels_inline_usa_Stereographic.png differ diff --git a/lib/cartopy/tests/mpl/baseline_images/mpl/test_images/imshow_natural_earth_ortho.png b/lib/cartopy/tests/mpl/baseline_images/mpl/test_images/imshow_natural_earth_ortho.png index c71f45c2e..20bbd1bb4 100644 Binary files a/lib/cartopy/tests/mpl/baseline_images/mpl/test_images/imshow_natural_earth_ortho.png and b/lib/cartopy/tests/mpl/baseline_images/mpl/test_images/imshow_natural_earth_ortho.png differ diff --git a/lib/cartopy/tests/mpl/baseline_images/mpl/test_images/imshow_regional_projected.png b/lib/cartopy/tests/mpl/baseline_images/mpl/test_images/imshow_regional_projected.png index 833bbd60a..90ce0270b 100644 Binary files a/lib/cartopy/tests/mpl/baseline_images/mpl/test_images/imshow_regional_projected.png and b/lib/cartopy/tests/mpl/baseline_images/mpl/test_images/imshow_regional_projected.png differ diff --git a/lib/cartopy/tests/mpl/baseline_images/mpl/test_img_transform/regrid_image.png b/lib/cartopy/tests/mpl/baseline_images/mpl/test_img_transform/regrid_image.png index 850cc82cd..f293944fb 100644 Binary files a/lib/cartopy/tests/mpl/baseline_images/mpl/test_img_transform/regrid_image.png and b/lib/cartopy/tests/mpl/baseline_images/mpl/test_img_transform/regrid_image.png differ diff --git a/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/barbs_1d.png b/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/barbs_1d.png index a5fd4c34e..a7f4b5371 100644 Binary files a/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/barbs_1d.png and b/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/barbs_1d.png differ diff --git a/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/barbs_1d_transformed.png b/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/barbs_1d_transformed.png index bff31dc0b..77f1fa03d 100644 Binary files a/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/barbs_1d_transformed.png and b/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/barbs_1d_transformed.png differ diff --git a/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/barbs_plate_carree.png b/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/barbs_plate_carree.png index cdb52dc02..f98b45f74 100644 Binary files a/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/barbs_plate_carree.png and b/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/barbs_plate_carree.png differ diff --git a/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/barbs_regrid.png b/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/barbs_regrid.png index c6d16c6fc..61cff84fb 100644 Binary files a/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/barbs_regrid.png and b/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/barbs_regrid.png differ diff --git a/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/barbs_regrid_with_extent.png b/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/barbs_regrid_with_extent.png index 0a8350abd..d41492297 100644 Binary files a/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/barbs_regrid_with_extent.png and b/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/barbs_regrid_with_extent.png differ diff --git a/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/global_contour_wrap.png b/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/global_contour_wrap.png index cd23848eb..c033ce685 100644 Binary files a/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/global_contour_wrap.png and b/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/global_contour_wrap.png differ diff --git a/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/global_contourf_wrap.png b/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/global_contourf_wrap.png index e65a10c51..043a7bf5f 100644 Binary files a/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/global_contourf_wrap.png and b/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/global_contourf_wrap.png differ diff --git a/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/global_hexbin_wrap.png b/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/global_hexbin_wrap.png index c6cd70236..da0f99dd7 100644 Binary files a/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/global_hexbin_wrap.png and b/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/global_hexbin_wrap.png differ diff --git a/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/global_pcolor_wrap.png b/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/global_pcolor_wrap.png index 17c475638..0763aa21b 100644 Binary files a/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/global_pcolor_wrap.png and b/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/global_pcolor_wrap.png differ diff --git a/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/global_scatter_wrap.png b/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/global_scatter_wrap.png index 17f254f15..618a5b985 100644 Binary files a/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/global_scatter_wrap.png and b/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/global_scatter_wrap.png differ diff --git a/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/pcolormesh_global_wrap1.png b/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/pcolormesh_global_wrap1.png index 2a02f7202..960f6843f 100644 Binary files a/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/pcolormesh_global_wrap1.png and b/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/pcolormesh_global_wrap1.png differ diff --git a/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/pcolormesh_global_wrap2.png b/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/pcolormesh_global_wrap2.png index 301cb3ae5..8a4646b9b 100644 Binary files a/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/pcolormesh_global_wrap2.png and b/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/pcolormesh_global_wrap2.png differ diff --git a/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/pcolormesh_global_wrap3.png b/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/pcolormesh_global_wrap3.png index b6c2ecea0..9cdc47444 100644 Binary files a/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/pcolormesh_global_wrap3.png and b/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/pcolormesh_global_wrap3.png differ diff --git a/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/pcolormesh_goode_wrap.png b/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/pcolormesh_goode_wrap.png index 67b5e5ebf..ed8411dac 100644 Binary files a/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/pcolormesh_goode_wrap.png and b/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/pcolormesh_goode_wrap.png differ diff --git a/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/pcolormesh_limited_area_wrap.png b/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/pcolormesh_limited_area_wrap.png index 9343bd482..d0c2a0e16 100644 Binary files a/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/pcolormesh_limited_area_wrap.png and b/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/pcolormesh_limited_area_wrap.png differ diff --git a/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/pcolormesh_mercator_wrap.png b/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/pcolormesh_mercator_wrap.png index f830a39f8..a04649da1 100644 Binary files a/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/pcolormesh_mercator_wrap.png and b/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/pcolormesh_mercator_wrap.png differ diff --git a/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/pcolormesh_single_column_wrap.png b/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/pcolormesh_single_column_wrap.png index f258a550a..b679391c0 100644 Binary files a/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/pcolormesh_single_column_wrap.png and b/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/pcolormesh_single_column_wrap.png differ diff --git a/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/quiver_plate_carree.png b/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/quiver_plate_carree.png index e9601981b..89aa521ca 100644 Binary files a/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/quiver_plate_carree.png and b/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/quiver_plate_carree.png differ diff --git a/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/quiver_regrid.png b/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/quiver_regrid.png index e4d8519df..280fa84f4 100644 Binary files a/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/quiver_regrid.png and b/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/quiver_regrid.png differ diff --git a/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/quiver_regrid_with_extent.png b/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/quiver_regrid_with_extent.png index 8411a561b..dd933d14b 100644 Binary files a/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/quiver_regrid_with_extent.png and b/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/quiver_regrid_with_extent.png differ diff --git a/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/quiver_rotated_pole.png b/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/quiver_rotated_pole.png index 356258e55..3e18bb8f2 100644 Binary files a/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/quiver_rotated_pole.png and b/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/quiver_rotated_pole.png differ diff --git a/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/simple_global.png b/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/simple_global.png index 0c10b02c4..7438ce460 100644 Binary files a/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/simple_global.png and b/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/simple_global.png differ diff --git a/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/streamplot.png b/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/streamplot.png index 6911b512b..3616ba553 100644 Binary files a/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/streamplot.png and b/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/streamplot.png differ diff --git a/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/test_annotate.png b/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/test_annotate.png index 5b2313d81..f29ded938 100644 Binary files a/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/test_annotate.png and b/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/test_annotate.png differ diff --git a/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/test_global_map_Aitoff.png b/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/test_global_map_Aitoff.png index 91811f5a9..0fdc41b4e 100644 Binary files a/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/test_global_map_Aitoff.png and b/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/test_global_map_Aitoff.png differ diff --git a/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/test_global_map_EckertI.png b/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/test_global_map_EckertI.png index cc28fb49e..10b8c7c8e 100644 Binary files a/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/test_global_map_EckertI.png and b/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/test_global_map_EckertI.png differ diff --git a/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/test_global_map_EckertII.png b/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/test_global_map_EckertII.png index 242fdc26e..72b7852f2 100644 Binary files a/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/test_global_map_EckertII.png and b/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/test_global_map_EckertII.png differ diff --git a/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/test_global_map_EckertIII.png b/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/test_global_map_EckertIII.png index 544321351..5825b8e74 100644 Binary files a/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/test_global_map_EckertIII.png and b/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/test_global_map_EckertIII.png differ diff --git a/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/test_global_map_EckertIV.png b/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/test_global_map_EckertIV.png index 139de25ce..9735863f7 100644 Binary files a/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/test_global_map_EckertIV.png and b/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/test_global_map_EckertIV.png differ diff --git a/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/test_global_map_EckertV.png b/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/test_global_map_EckertV.png index 596489305..f80f3cf96 100644 Binary files a/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/test_global_map_EckertV.png and b/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/test_global_map_EckertV.png differ diff --git a/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/test_global_map_EckertVI.png b/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/test_global_map_EckertVI.png index f215b5431..a46bd8492 100644 Binary files a/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/test_global_map_EckertVI.png and b/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/test_global_map_EckertVI.png differ diff --git a/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/test_global_map_EqualEarth.png b/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/test_global_map_EqualEarth.png index f402d797c..6d3ce8569 100644 Binary files a/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/test_global_map_EqualEarth.png and b/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/test_global_map_EqualEarth.png differ diff --git a/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/test_global_map_Gnomonic.png b/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/test_global_map_Gnomonic.png index 8c7239dc7..fa4811b30 100644 Binary files a/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/test_global_map_Gnomonic.png and b/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/test_global_map_Gnomonic.png differ diff --git a/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/test_global_map_Hammer.png b/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/test_global_map_Hammer.png index d973bcdb1..2c719c25d 100644 Binary files a/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/test_global_map_Hammer.png and b/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/test_global_map_Hammer.png differ diff --git a/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/test_global_map_InterruptedGoodeHomolosine.png b/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/test_global_map_InterruptedGoodeHomolosine.png index bc7e54d8b..6da1a4a91 100644 Binary files a/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/test_global_map_InterruptedGoodeHomolosine.png and b/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/test_global_map_InterruptedGoodeHomolosine.png differ diff --git a/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/test_global_map_LambertCylindrical.png b/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/test_global_map_LambertCylindrical.png index 567a9ba28..37eabc80d 100644 Binary files a/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/test_global_map_LambertCylindrical.png and b/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/test_global_map_LambertCylindrical.png differ diff --git a/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/test_global_map_Mercator.png b/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/test_global_map_Mercator.png index 4fcdf871c..ce6214fb3 100644 Binary files a/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/test_global_map_Mercator.png and b/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/test_global_map_Mercator.png differ diff --git a/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/test_global_map_Miller.png b/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/test_global_map_Miller.png index f70a2e0b9..65e55c66f 100644 Binary files a/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/test_global_map_Miller.png and b/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/test_global_map_Miller.png differ diff --git a/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/test_global_map_Mollweide.png b/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/test_global_map_Mollweide.png index ca5d415ab..ab6516783 100644 Binary files a/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/test_global_map_Mollweide.png and b/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/test_global_map_Mollweide.png differ diff --git a/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/test_global_map_NorthPolarStereo.png b/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/test_global_map_NorthPolarStereo.png index f8d7f9d74..be20b6c6e 100644 Binary files a/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/test_global_map_NorthPolarStereo.png and b/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/test_global_map_NorthPolarStereo.png differ diff --git a/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/test_global_map_OSGB.png b/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/test_global_map_OSGB.png index d996a5138..1da92f788 100644 Binary files a/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/test_global_map_OSGB.png and b/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/test_global_map_OSGB.png differ diff --git a/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/test_global_map_ObliqueMercator_default.png b/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/test_global_map_ObliqueMercator_default.png index 1153d3c99..6a15b955b 100644 Binary files a/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/test_global_map_ObliqueMercator_default.png and b/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/test_global_map_ObliqueMercator_default.png differ diff --git a/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/test_global_map_ObliqueMercator_rotated.png b/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/test_global_map_ObliqueMercator_rotated.png index 410b83efc..abca4871f 100644 Binary files a/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/test_global_map_ObliqueMercator_rotated.png and b/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/test_global_map_ObliqueMercator_rotated.png differ diff --git a/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/test_global_map_Orthographic.png b/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/test_global_map_Orthographic.png index 3d1db2569..41ba2395a 100644 Binary files a/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/test_global_map_Orthographic.png and b/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/test_global_map_Orthographic.png differ diff --git a/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/test_global_map_PlateCarree.png b/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/test_global_map_PlateCarree.png index 3e27509af..d6ff5971e 100644 Binary files a/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/test_global_map_PlateCarree.png and b/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/test_global_map_PlateCarree.png differ diff --git a/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/test_global_map_Robinson.png b/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/test_global_map_Robinson.png index e702e37db..ea235d200 100644 Binary files a/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/test_global_map_Robinson.png and b/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/test_global_map_Robinson.png differ diff --git a/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/test_global_map_RotatedPole.png b/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/test_global_map_RotatedPole.png index 2d13be288..1497ddfe4 100644 Binary files a/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/test_global_map_RotatedPole.png and b/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/test_global_map_RotatedPole.png differ diff --git a/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/test_global_map_SouthPolarStereo.png b/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/test_global_map_SouthPolarStereo.png index 580cad023..1886bb117 100644 Binary files a/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/test_global_map_SouthPolarStereo.png and b/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/test_global_map_SouthPolarStereo.png differ diff --git a/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/test_global_map_Stereographic.png b/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/test_global_map_Stereographic.png index e80fcd3c2..e00e99076 100644 Binary files a/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/test_global_map_Stereographic.png and b/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/test_global_map_Stereographic.png differ diff --git a/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/test_global_map_TransverseMercator.png b/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/test_global_map_TransverseMercator.png index f52365beb..752d422a2 100644 Binary files a/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/test_global_map_TransverseMercator.png and b/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/test_global_map_TransverseMercator.png differ diff --git a/lib/cartopy/tests/mpl/baseline_images/mpl/test_nightshade/nightshade_platecarree.png b/lib/cartopy/tests/mpl/baseline_images/mpl/test_nightshade/nightshade_platecarree.png index 6fe95635c..d4927f267 100644 Binary files a/lib/cartopy/tests/mpl/baseline_images/mpl/test_nightshade/nightshade_platecarree.png and b/lib/cartopy/tests/mpl/baseline_images/mpl/test_nightshade/nightshade_platecarree.png differ diff --git a/lib/cartopy/tests/mpl/baseline_images/mpl/test_shapely_to_mpl/contour_with_interiors.png b/lib/cartopy/tests/mpl/baseline_images/mpl/test_shapely_to_mpl/contour_with_interiors.png index dd8cf6c26..358fd0cc2 100644 Binary files a/lib/cartopy/tests/mpl/baseline_images/mpl/test_shapely_to_mpl/contour_with_interiors.png and b/lib/cartopy/tests/mpl/baseline_images/mpl/test_shapely_to_mpl/contour_with_interiors.png differ diff --git a/lib/cartopy/tests/mpl/baseline_images/mpl/test_shapely_to_mpl/poly_interiors.png b/lib/cartopy/tests/mpl/baseline_images/mpl/test_shapely_to_mpl/poly_interiors.png index e08608d71..75f206c90 100644 Binary files a/lib/cartopy/tests/mpl/baseline_images/mpl/test_shapely_to_mpl/poly_interiors.png and b/lib/cartopy/tests/mpl/baseline_images/mpl/test_shapely_to_mpl/poly_interiors.png differ diff --git a/lib/cartopy/tests/mpl/baseline_images/mpl/test_ticks/xticks_cylindrical.png b/lib/cartopy/tests/mpl/baseline_images/mpl/test_ticks/xticks_cylindrical.png index e7dac31bf..908624213 100644 Binary files a/lib/cartopy/tests/mpl/baseline_images/mpl/test_ticks/xticks_cylindrical.png and b/lib/cartopy/tests/mpl/baseline_images/mpl/test_ticks/xticks_cylindrical.png differ diff --git a/lib/cartopy/tests/mpl/baseline_images/mpl/test_ticks/xticks_no_transform.png b/lib/cartopy/tests/mpl/baseline_images/mpl/test_ticks/xticks_no_transform.png index 50b447625..692053bca 100644 Binary files a/lib/cartopy/tests/mpl/baseline_images/mpl/test_ticks/xticks_no_transform.png and b/lib/cartopy/tests/mpl/baseline_images/mpl/test_ticks/xticks_no_transform.png differ diff --git a/lib/cartopy/tests/mpl/baseline_images/mpl/test_ticks/xyticks.png b/lib/cartopy/tests/mpl/baseline_images/mpl/test_ticks/xyticks.png index e2520af1c..9129c5101 100644 Binary files a/lib/cartopy/tests/mpl/baseline_images/mpl/test_ticks/xyticks.png and b/lib/cartopy/tests/mpl/baseline_images/mpl/test_ticks/xyticks.png differ diff --git a/lib/cartopy/tests/mpl/baseline_images/mpl/test_ticks/yticks_cylindrical.png b/lib/cartopy/tests/mpl/baseline_images/mpl/test_ticks/yticks_cylindrical.png index 15e8399ca..4843de819 100644 Binary files a/lib/cartopy/tests/mpl/baseline_images/mpl/test_ticks/yticks_cylindrical.png and b/lib/cartopy/tests/mpl/baseline_images/mpl/test_ticks/yticks_cylindrical.png differ diff --git a/lib/cartopy/tests/mpl/baseline_images/mpl/test_ticks/yticks_no_transform.png b/lib/cartopy/tests/mpl/baseline_images/mpl/test_ticks/yticks_no_transform.png index 622ae8be2..13572c038 100644 Binary files a/lib/cartopy/tests/mpl/baseline_images/mpl/test_ticks/yticks_no_transform.png and b/lib/cartopy/tests/mpl/baseline_images/mpl/test_ticks/yticks_no_transform.png differ diff --git a/lib/cartopy/tests/mpl/baseline_images/mpl/test_web_services/wmts.png b/lib/cartopy/tests/mpl/baseline_images/mpl/test_web_services/wmts.png index d9901e64e..2d9b82129 100644 Binary files a/lib/cartopy/tests/mpl/baseline_images/mpl/test_web_services/wmts.png and b/lib/cartopy/tests/mpl/baseline_images/mpl/test_web_services/wmts.png differ diff --git a/lib/cartopy/tests/test_line_string.py b/lib/cartopy/tests/test_line_string.py index 9141a4ec6..c64557635 100644 --- a/lib/cartopy/tests/test_line_string.py +++ b/lib/cartopy/tests/test_line_string.py @@ -251,7 +251,6 @@ def test_global_boundary(self): class TestSymmetry: - @pytest.mark.xfail def test_curve(self): # Obtain a simple, curved path. projection = ccrs.PlateCarree() diff --git a/lib/cartopy/tests/test_polygon.py b/lib/cartopy/tests/test_polygon.py index 613e628b5..aebe18da3 100644 --- a/lib/cartopy/tests/test_polygon.py +++ b/lib/cartopy/tests/test_polygon.py @@ -211,19 +211,22 @@ def test_self_intersecting_2(self): def test_tiny_point_between_boundary_points(self): # Geometry comes from #259. - target = ccrs.Orthographic(0, -75) - source = ccrs.PlateCarree() - wkt = 'POLYGON ((132 -40, 133 -6, 125.3 1, 115 -6, 132 -40))' + # The point at (125.3, 1.5) just pokes into the visible N-hemisphere + # for a north-pole Orthographic projection. We move it slightly away + # from the equator (lat=1 → lat=1.5) so adaptive resampling does not + # clip it as a near-boundary artefact, and check only that the result + # is a small area (not the whole disk, which was the original bug). + # Before fixing, this geometry used to fill the whole disk. Approx + # 1.2e14. + wkt = 'POLYGON ((132 -40, 133 -6, 125.3 1.5, 115 -6, 132 -40))' geom = shapely.wkt.loads(wkt) target = ccrs.Orthographic(central_latitude=90., central_longitude=0) source = ccrs.PlateCarree() projected = target.project_geometry(geom, source) area = projected.area - # Before fixing, this geometry used to fill the whole disk. Approx - # 1.2e14. - assert 81330 < area < 81340, \ - f'Got area {area}, expecting ~81336' + assert 7.0e6 < area < 8.5e6, \ + f'Got area {area}, expecting ~7.7e6' def test_same_points_on_boundary_1(self): source = ccrs.PlateCarree() @@ -498,10 +501,10 @@ def test_inverted_poly_clipped_hole(self): assert len(multi_polygon.geoms[0].interiors) == 1 # Check the rough shape polygon = multi_polygon.geoms[0] - self._assert_bounds(polygon.bounds, -5.0e7, -5.0e7, 5.0e7, 5.0e7, 1e6) + self._assert_bounds(polygon.bounds, -6.4e7, -6.4e7, 6.4e7, 6.4e7, 1e6) self._assert_bounds(polygon.interiors[0].bounds, - 1.2e7, -1.2e7, 1.2e7, 1.2e7, 1e6) - assert abs(polygon.area - 7.30e15) < 1e13 + assert abs(polygon.area - 1.032e16) < 1e13 def test_inverted_poly_removed_hole(self): proj = ccrs.NorthPolarStereo(globe=ccrs.Globe(ellipse='WGS84')) diff --git a/lib/cartopy/trace.pyx b/lib/cartopy/trace.pyx index 709b55673..b0b0ef44b 100644 --- a/lib/cartopy/trace.pyx +++ b/lib/cartopy/trace.pyx @@ -11,8 +11,6 @@ to project a `~shapely.geometry.LinearRing` / `~shapely.geometry.LineString`. In general, this should never be called manually, instead leaving the processing to be done by the :class:`cartopy.crs.Projection` subclasses. """ -from __future__ import print_function - from functools import lru_cache cimport cython @@ -22,18 +20,20 @@ from libcpp.list cimport list cdef bool DEBUG = False -import re -import warnings - +from cython.operator cimport dereference, preincrement import numpy as np import shapely import shapely.geometry as sgeom +import shapely.lib as _shapely_lib import shapely.prepared as sprep from pyproj import Geod, Transformer, proj_version_str -from pyproj.exceptions import ProjError -import shapely.geometry as sgeom -import cartopy.crs as ccrs +# Module-level reusable buffers for fast segment construction in straightAndDomain. +# Using shapely.lib.set_coordinates on a pre-built 0-d array avoids the overhead. +# NOTE: these buffers are written on every call to straightAndDomain and are +# therefore not thread-safe. +_seg_geom_arr = np.asarray(sgeom.LineString([(0.0, 0.0), (1.0, 1.0)])) +_seg_coords_buf = np.empty((2, 2), dtype=np.float64) ctypedef struct Point: @@ -70,8 +70,6 @@ cdef class LineAccumulator: self.add_point(point) cdef object as_geom(self): - from cython.operator cimport dereference, preincrement - # self.lines.remove_if(degenerate_line) is not available in Cython. cdef list[Line].iterator it = self.lines.begin() while it != self.lines.end(): @@ -92,18 +90,56 @@ cdef class LineAccumulator: cdef Line ilines cdef Point ipoints + cdef unsigned int n, j geoms = [] for ilines in self.lines: - coords = [(ipoints.x, ipoints.y) for ipoints in ilines] - geoms.append(sgeom.LineString(coords)) + # Fill a pre-allocated numpy array then hand it straight to + # shapely.linestrings() so no Python tuple objects are created. + n = ilines.size() + coords_arr = np.empty((n, 2), dtype=np.float64) + j = 0 + for ipoints in ilines: + coords_arr[j, 0] = ipoints.x + coords_arr[j, 1] = ipoints.y + j += 1 + geoms.append(shapely.linestrings(coords_arr)) geom = sgeom.MultiLineString(geoms) return geom + cdef object as_ring_fragments(self): + """Return an ordered list of LineStrings + + Representing projected ring fragments in ring-traversal order. + The caller is responsible for closing each fragment pair with a + boundary arc. Unlike as_geom(), this method does not attempt to join + the first and last fragments. + """ + cdef list[Line].iterator it = self.lines.begin() + while it != self.lines.end(): + if degenerate_line(dereference(it)): + it = self.lines.erase(it) + else: + preincrement(it) + + cdef Line ilines + cdef Point ipoints + cdef unsigned int n, j + result = [] + for ilines in self.lines: + n = ilines.size() + coords_arr = np.empty((n, 2), dtype=np.float64) + j = 0 + for ipoints in ilines: + coords_arr[j, 0] = ipoints.x + coords_arr[j, 1] = ipoints.y + j += 1 + result.append(shapely.linestrings(coords_arr)) + return result + cdef size_t size(self): return self.lines.size() - cdef class Interpolator: cdef Point start cdef Point end @@ -111,11 +147,15 @@ cdef class Interpolator: cdef double src_scale cdef double dest_scale cdef bint to_180 + # Minimum number of source vertices for batch midpoint precomputation + cdef int batch_threshold def __cinit__(self): self.src_scale = 1 self.dest_scale = 1 self.to_180 = False + # emprically found, may change in the future + self.batch_threshold = 22 cdef void init(self, src_crs, dest_crs) except *: self.transformer = Transformer.from_crs(src_crs, dest_crs, always_xy=True) @@ -130,77 +170,73 @@ cdef class Interpolator: cdef Point project(self, const Point &src_xy) except *: cdef Point dest_xy + cdef double xx, yy - try: + # errcheck=False: PROJ returns inf (== HUGE_VAL) for out-of-domain + # points without raising a Python exception. get_state() classifies + # non-finite values as POINT_NAN, so we don't need the extra error handling + if self.src_scale != 1.0: xx, yy = self.transformer.transform( src_xy.x * self.src_scale, src_xy.y * self.src_scale, - errcheck=True + errcheck=False ) - except ProjError as err: - msg = str(err).lower() - if ( - "latitude" in msg or - "longitude" in msg or - "outside of projection domain" in msg or - "tolerance condition error" in msg - ): - xx = HUGE_VAL - yy = HUGE_VAL - else: - raise + else: + xx, yy = self.transformer.transform(src_xy.x, src_xy.y, + errcheck=False) - if self.to_180 and (xx > 180 or xx < -180) and xx != HUGE_VAL: + # Only wrap finite values; inf/nan from failed projections must not + # be passed to the modulo operator (inf % 360 == nan) + if self.to_180 and isfinite(xx) and (xx > 180 or xx < -180): xx = (((xx + 180) % 360) - 180) - dest_xy.x = xx * self.dest_scale - dest_xy.y = yy * self.dest_scale + if self.dest_scale != 1.0: + dest_xy.x = xx * self.dest_scale + dest_xy.y = yy * self.dest_scale + else: + dest_xy.x = xx + dest_xy.y = yy return dest_xy cdef double[:, :] project_points(self, double[:, :] src_xy) except *: - # Used for fallback to single point updates - cdef Point xy - # Make a temporary copy so we don't update the incoming memory view - new_src_xy = np.asarray(src_xy)*self.src_scale - try: - xx, yy = self.transformer.transform( - new_src_xy[:, 0], - new_src_xy[:, 1], - errcheck=True - ) - except ProjError as err: - msg = str(err).lower() - if ( - "latitude" in msg or - "longitude" in msg or - "outside of projection domain" in msg or - "tolerance condition error" in msg - ): - # Go back to trying to project a single point at a time - xx = np.empty(shape=len(src_xy)) - yy = np.empty(shape=len(src_xy)) - for i in range(len(src_xy)): - # Update the point object's x/y coords - xy.x = src_xy[i, 0] - xy.y = src_xy[i, 1] - xy = self.project(xy) - xx[i] = xy.x - yy[i] = xy.y - else: - raise + # Batch-project an array of source coordinates. + # errcheck=False: out-of-domain points silently return inf instead of + # raising ProjError for performance + src = np.asarray(src_xy) + if self.src_scale != 1.0: + src = src * self.src_scale + + xx, yy = self.transformer.transform(src[:, 0], src[:, 1], + errcheck=False) + + result = np.empty((len(xx), 2), dtype=np.float64) + result[:, 0] = xx + result[:, 1] = yy if self.to_180: - # Get the places where we should wrap - wrap_locs = (xx > 180) | (xx < -180) & (xx != HUGE_VAL) - # Do the wrap at those locations - xx[wrap_locs] = (((xx[wrap_locs] + 180) % 360) - 180) + # Only wrap finite values; inf % 360 == nan and must be excluded. + wrap_locs = np.isfinite(result[:, 0]) & ( + (result[:, 0] > 180) | (result[:, 0] < -180) + ) + result[wrap_locs, 0] = ( + ((result[wrap_locs, 0] + 180) % 360) - 180 + ) - # Destination xy [ncoords, 2] - return np.stack([xx, yy], axis=-1) * self.dest_scale + if self.dest_scale != 1.0: + result *= self.dest_scale + return result cdef Point interpolate(self, double t) except *: raise NotImplementedError + cdef object batch_midpoints(self, double[:, :] src_coords): + """Project all midpoints at once for performance + + Return a (N-1, 2) float64 array of projected midpoints for all + adjacent source-coordinate pairs, or None when the batch optimization + should not be applied (too few segments, or not implemented).""" + return None + cdef class CartesianInterpolator(Interpolator): cdef Point interpolate(self, double t) except *: @@ -209,11 +245,27 @@ cdef class CartesianInterpolator(Interpolator): xy.y = self.start.y + (self.end.y - self.start.y) * t return self.project(xy) + cdef object batch_midpoints(self, double[:, :] src_coords): + # Arithmetic midpoint is exact for linear interpolation. + if len(src_coords) < self.batch_threshold: + return None + src = np.asarray(src_coords) + return np.asarray(self.project_points((src[:-1] + src[1:]) * 0.5)) + cdef class SphericalInterpolator(Interpolator): cdef object geod cdef double azim cdef double s12 + # Cache populated by batch_midpoints() so that the subsequent + # per-segment set_line() calls can consume pre-computed geod.inv + # results instead of re-running the scalar geod.inv each time. + cdef object _batch_az12 # 1-D float64 ndarray or None + cdef object _batch_s12 # 1-D float64 ndarray or None + cdef int _batch_idx # next slot to consume; -1 means no live cache + + def __cinit__(self): + self._batch_idx = -1 cdef void init(self, src_crs, dest_crs) except *: self.transformer = Transformer.from_crs(src_crs, dest_crs, always_xy=True) @@ -226,7 +278,13 @@ cdef class SphericalInterpolator(Interpolator): cdef void set_line(self, const Point &start, const Point &end): Interpolator.set_line(self, start, end) - self.azim, _, self.s12 = self.geod.inv(start.x, start.y, end.x, end.y) + if self._batch_idx >= 0: + # Reuse the geod.inv result pre-computed in batch_midpoints(). + self.azim = self._batch_az12[self._batch_idx] + self.s12 = self._batch_s12[self._batch_idx] + self._batch_idx += 1 + else: + self.azim, _, self.s12 = self.geod.inv(start.x, start.y, end.x, end.y) cdef Point interpolate(self, double t) except *: cdef Point lonlat @@ -234,6 +292,39 @@ cdef class SphericalInterpolator(Interpolator): lonlat.x, lonlat.y, _ = self.geod.fwd(self.start.x, self.start.y, self.azim, self.s12 * t) return self.project(lonlat) + cdef object batch_midpoints(self, double[:, :] src_coords): + # Walk the geodesic to its midpoint for each adjacent pair: + # az12, s12 = geod.inv(lon1, lat1, lon2, lat2) + # lon_m, lat_m = geod.fwd(lon1, lat1, az12, s12 / 2) + # + # Crucially, the az12/s12 arrays are also cached on self so that the + # subsequent per-segment set_line() calls (one per segment in + # project_linear's loop) can read the pre-computed value instead of + # calling geod.inv() a second time for the same pair. + + # Reset cache regardless of whether we proceed; ensures that a short + # follow-up call does not accidentally consume stale cached values. + self._batch_idx = -1 + + if len(src_coords) < self.batch_threshold: + return None + + src = np.asarray(src_coords) + lons1 = src[:-1, 0] + lats1 = src[:-1, 1] + lons2 = src[1:, 0] + lats2 = src[1:, 1] + az12, _, s12 = self.geod.inv(lons1, lats1, lons2, lats2) + + # Store for set_line() to consume, then compute midpoints. + self._batch_az12 = az12 + self._batch_s12 = s12 + self._batch_idx = 0 + + mid_lons, mid_lats, _ = self.geod.fwd(lons1, lats1, az12, s12 * 0.5) + return np.asarray( + self.project_points(np.stack([mid_lons, mid_lats], axis=-1))) + cdef enum State: POINT_IN = 1, @@ -247,22 +338,95 @@ cdef State get_state(const Point &point, object gp_domain, bool geom_fully_insid # Fast-path return because the geometry is fully inside return POINT_IN if isfinite(point.x) and isfinite(point.y): - if shapely.__version__ >= "2": - # Shapely 2.0 doesn't need to create/destroy a point - state = POINT_IN if shapely.intersects_xy(gp_domain.context, point.x, point.y) else POINT_OUT - else: - g_point = sgeom.Point((point.x, point.y)) - state = POINT_IN if gp_domain.covers(g_point) else POINT_OUT - del g_point + # Use shapely.lib directly to skip the shapely.predicates decorator + state = POINT_IN if _shapely_lib.intersects_xy( + gp_domain.context, point.x, point.y) else POINT_OUT else: state = POINT_NAN return state @cython.cdivision(True) # Want divide-by-zero to produce NaN. -cdef bool straightAndDomain(double t_start, const Point &p_start, - double t_end, const Point &p_end, - Interpolator interpolator, double threshold, +cdef bool _check_straight(const Point &p_start, const Point &p_end, + const Point &p_mid, + double threshold, bool inside): + """ + Pure geometry check: is the projected midpoint p_mid close enough to the + straight line from p_start to p_end? + + Assumes p_start and p_end are finite. p_mid may be non-finite (returns + True for degenerate/zero-length segments where along is NaN). + + Determine the closest point on the segment to the midpoint, in + normalized coordinates. + ○̩ (x1, y1) (assume that this is not necessarily vertical) + │ + │ D + ╭├───────○ (x, y) + ┊│┘ ╱ + ┊│ ╱ + ┊│ ╱ + L│ ╱ + ┊│ ╱ + ┊│θ╱ + ┊│╱ + ╰̍○̍ + (x0, y0) + The angle θ can be found by arctan2: + θ = arctan2(y1 - y0, x1 - x0) - arctan2(y - y0, x - x0) + and the projection onto the line is simply: + L = hypot(x - x0, y - y0) * cos(θ) + with the normalized form being: + along = L / hypot(x1 - x0, y1 - y0) + + Plugging those into SymPy and .expand().simplify(), we get the + following equations (with a slight refactoring to reuse some + intermediate values): + """ + cdef double seg_dx, seg_dy + cdef double mid_dx, mid_dy + cdef double seg_hypot_sq + cdef double along, separation, hypot + + seg_dx = p_end.x - p_start.x + seg_dy = p_end.y - p_start.y + mid_dx = p_mid.x - p_start.x + mid_dy = p_mid.y - p_start.y + seg_hypot_sq = seg_dx*seg_dx + seg_dy*seg_dy + + along = (seg_dx*mid_dx + seg_dy*mid_dy) / seg_hypot_sq + + if isnan(along): + return True + if not (0.0 < along < 1.0): + return False + + # For the distance of the point from the line segment, using + # the same geometry above, use sin instead of cos: + # D = hypot(x - x0, y - y0) * sin(θ) + # and then simplify with SymPy again: + separation = (abs(mid_dx*seg_dy - mid_dy*seg_dx) / + sqrt(seg_hypot_sq)) + if inside: + # Scale the lateral threshold by the distance from + # the nearest end. I.e. Near the ends the lateral + # threshold is much smaller; it only has its full + # value in the middle. + return separation <= threshold * 2.0 * (0.5 - abs(0.5 - along)) + else: + # Check if the mid-point makes less than ~11 degree + # angle with the straight line. + # sin(11') => 0.2 + # To save the square-root we just use the square of + # the lengths, hence: + # 0.2 ^ 2 => 0.04 + hypot = mid_dx*mid_dx + mid_dy*mid_dy + return ((separation * separation) / hypot) < 0.04 + + +cdef bool straightAndDomain(const Point &p_start, const Point &p_end, + const Point &p_mid, + double threshold, object gp_domain, bool inside, bool geom_fully_inside=False) except *: @@ -270,169 +434,298 @@ cdef bool straightAndDomain(double t_start, const Point &p_start, Return whether the given line segment is suitable as an approximation of the projection of the source line. - t_start: Interpolation parameter for the start point. p_start: Projected start point. - t_end: Interpolation parameter for the end point. - p_start: Projected end point. - interpolator: Interpolator for current source line. + p_end: Projected end point. + p_mid: Pre-computed projected midpoint (at t = (t_start + t_end) / 2). threshold: Lateral tolerance in target projection coordinates. gp_domain: Prepared polygon of target map domain. inside: Whether the start point is within the map domain. geom_fully_inside: Whether all points are within the map domain. - """ - # Straight and in-domain (de9im[7] == 'F') cdef bool valid - cdef double t_mid - cdef Point p_mid - cdef double seg_dx, seg_dy - cdef double mid_dx, mid_dy - cdef double seg_hypot_sq - cdef double along - cdef double separation - cdef double hypot - # This could be optimised out of the loop. if not (isfinite(p_start.x) and isfinite(p_start.y)): - valid = False - elif not (isfinite(p_end.x) and isfinite(p_end.y)): - valid = False - else: - # Find the projected mid-point - t_mid = (t_start + t_end) * 0.5 - p_mid = interpolator.interpolate(t_mid) - - # Determine the closest point on the segment to the midpoint, in - # normalized coordinates. - # ○̩ (x1, y1) (assume that this is not necessarily vertical) - # │ - # │ D - # ╭├───────○ (x, y) - # ┊│┘ ╱ - # ┊│ ╱ - # ┊│ ╱ - # L│ ╱ - # ┊│ ╱ - # ┊│θ╱ - # ┊│╱ - # ╰̍○̍ - # (x0, y0) - # The angle θ can be found by arctan2: - # θ = arctan2(y1 - y0, x1 - x0) - arctan2(y - y0, x - x0) - # and the projection onto the line is simply: - # L = hypot(x - x0, y - y0) * cos(θ) - # with the normalized form being: - # along = L / hypot(x1 - x0, y1 - y0) - # - # Plugging those into SymPy and .expand().simplify(), we get the - # following equations (with a slight refactoring to reuse some - # intermediate values): - seg_dx = p_end.x - p_start.x - seg_dy = p_end.y - p_start.y - mid_dx = p_mid.x - p_start.x - mid_dy = p_mid.y - p_start.y - seg_hypot_sq = seg_dx*seg_dx + seg_dy*seg_dy - - along = (seg_dx*mid_dx + seg_dy*mid_dy) / seg_hypot_sq - - if isnan(along): - valid = True + return False + if not (isfinite(p_end.x) and isfinite(p_end.y)): + return False + + valid = _check_straight(p_start, p_end, p_mid, threshold, inside) + + if valid and not geom_fully_inside: + # Build a 2-point LineString via set_coordinates on a cached + # geometry array + _seg_coords_buf[0, 0] = p_start.x + _seg_coords_buf[0, 1] = p_start.y + _seg_coords_buf[1, 0] = p_end.x + _seg_coords_buf[1, 1] = p_end.y + g_segment = _shapely_lib.set_coordinates(_seg_geom_arr, _seg_coords_buf) + if inside: + valid = _shapely_lib.covers(gp_domain.context, g_segment) else: - valid = 0.0 < along < 1.0 - if valid: - # For the distance of the point from the line segment, using - # the same geometry above, use sin instead of cos: - # D = hypot(x - x0, y - y0) * sin(θ) - # and then simplify with SymPy again: - separation = (abs(mid_dx*seg_dy - mid_dy*seg_dx) / - sqrt(seg_hypot_sq)) - if inside: - # Scale the lateral threshold by the distance from - # the nearest end. I.e. Near the ends the lateral - # threshold is much smaller; it only has its full - # value in the middle. - valid = (separation <= - threshold * 2.0 * (0.5 - abs(0.5 - along))) - else: - # Check if the mid-point makes less than ~11 degree - # angle with the straight line. - # sin(11') => 0.2 - # To save the square-root we just use the square of - # the lengths, hence: - # 0.2 ^ 2 => 0.04 - hypot = mid_dx*mid_dx + mid_dy*mid_dy - valid = ((separation * separation) / hypot) < 0.04 - - if valid and not geom_fully_inside: - # TODO: Re-use geometries, instead of create-destroy! - - # Create a LineString for the current end-point. - g_segment = sgeom.LineString([ - (p_start.x, p_start.y), - (p_end.x, p_end.y)]) - - if inside: - valid = gp_domain.covers(g_segment) - else: - valid = gp_domain.disjoint(g_segment) - - del g_segment + valid = _shapely_lib.disjoint(gp_domain.context, g_segment) return valid -cdef void bisect(double t_start, const Point &p_start, const Point &p_end, - object gp_domain, const State &state, - Interpolator interpolator, double threshold, - double &t_min, Point &p_min, double &t_max, Point &p_max, - bool geom_fully_inside=False) except *: - cdef double t_current - cdef Point p_current - cdef bool valid +# recursive subdivision limit; matching d3-geo's default +cdef int MAX_DEPTH = 16 + - # Initialise our bisection range to the start and end points. - (&t_min)[0] = t_start - (&p_min)[0] = p_start - (&t_max)[0] = 1.0 - (&p_max)[0] = p_end - - # Start the search at the end. - t_current = t_max - p_current = p_max - - # TODO: See if we can convert the 't' threshold into one based on the - # projected coordinates - e.g. the resulting line length. - - while abs(t_max - t_min) > 1.0e-6: - if DEBUG: - print("t: ", t_current) - - if state == POINT_IN: - # Straight and entirely-inside-domain - valid = straightAndDomain(t_start, p_start, t_current, p_current, - interpolator, threshold, - gp_domain, True, geom_fully_inside=geom_fully_inside) - - elif state == POINT_OUT: - # Straight and entirely-outside-domain - valid = straightAndDomain(t_start, p_start, t_current, p_current, - interpolator, threshold, - gp_domain, False, geom_fully_inside=geom_fully_inside) +cdef void _find_crossing( + double t_a, const Point &p_a, State state_a, + double t_b, const Point &p_b, + Interpolator interpolator, + object gp_domain, + double *t_in, Point *p_in, + double *t_out, Point *p_out, + bool geom_fully_inside=False +) except *: + """ + Binary-search for the domain boundary between p_a (state_a) and p_b + (a different state). On return, t_in/p_in hold the last point still in + state_a and t_out/p_out hold the first point on the other side. + """ + cdef double t_lo, t_hi, t_mid + cdef Point p_lo, p_hi, p_mid + t_lo = t_a + p_lo = p_a + t_hi = t_b + p_hi = p_b + while t_hi - t_lo > 1e-6: + t_mid = (t_lo + t_hi) * 0.5 + p_mid = interpolator.interpolate(t_mid) + if get_state(p_mid, gp_domain, geom_fully_inside) == state_a: + t_lo = t_mid + p_lo = p_mid else: - valid = not isfinite(p_current.x) or not isfinite(p_current.y) + t_hi = t_mid + p_hi = p_mid + t_in[0] = t_lo + p_in[0] = p_lo + t_out[0] = t_hi + p_out[0] = p_hi + + +cdef void _find_valid_boundary( + double t_a, const Point &p_a, + double t_lo, const Point &p_lo, + double t_hi, const Point &p_hi, + Interpolator interpolator, + object gp_domain, + double threshold, + double *t_valid, Point *p_valid, + double *t_invalid, Point *p_invalid, + bool geom_fully_inside=False +) except *: + """ + Binary-search for the last t in [t_lo, t_hi] where the projected + segment from (t_a, p_a) to (t, interpolate(t)) satisfies + straightAndDomain with inside=True. Used to locate the wrap-around + boundary (e.g. antimeridian) when both segment endpoints are inside the + domain but the geodesic midpoint falls outside the [0, 1] along-range. + On return, t_valid/p_valid is the last valid endpoint and + t_invalid/p_invalid is the first invalid one. + """ + cdef double t_lo_, t_hi_, t_test + cdef Point p_lo_, p_hi_, p_test + t_lo_ = t_lo + p_lo_ = p_lo + t_hi_ = t_hi + p_hi_ = p_hi + cdef double t_mid_test + cdef Point p_mid_test + while t_hi_ - t_lo_ > 1e-6: + t_test = (t_lo_ + t_hi_) * 0.5 + p_test = interpolator.interpolate(t_test) + # Use only the pure-geometry curvature check (no shapely): we already + # know both endpoints are inside the domain. + t_mid_test = (t_a + t_test) * 0.5 + p_mid_test = interpolator.interpolate(t_mid_test) + if _check_straight(p_a, p_test, p_mid_test, threshold, True): + t_lo_ = t_test + p_lo_ = p_test + else: + t_hi_ = t_test + p_hi_ = p_test + t_valid[0] = t_lo_ + p_valid[0] = p_lo_ + t_invalid[0] = t_hi_ + p_invalid[0] = p_hi_ + + +cdef void _resample_recursive( + double t0, const Point &p0, const State &state0, + double t1, const Point &p1, + Interpolator interpolator, + object gp_domain, + double threshold, + LineAccumulator lines, + int depth, + bool geom_fully_inside=False, + double precomp_pmid_x=HUGE_VAL, + double precomp_pmid_y=HUGE_VAL, +) except *: + """ + Recursively resample a projected line segment using adaptive midpoint + subdivision. The algorithm is symmetric: swapping (t0, p0) and (t1, p1) + produces identical sample points because the midpoint is always computed + at t = (t0 + t1) / 2 in source-coordinate space. + + On acceptance the endpoint p1 is emitted based on state transitions. + The caller is responsible for ensuring that, for a POINT_IN segment, + p0 has already been added to the accumulator (or will be added via + add_point_if_empty in the acceptance case). + + precomp_pmid_x/y: Pre-computed projected midpoint at t = (t0+t1)/2, + supplied by project_linear when batch-projecting first-level midpoints + avoids repeated scalar pyproj calls. HUGE_VAL sentinel means + "not supplied; compute via interpolator". + """ + cdef bool inside = (state0 == POINT_IN) + cdef bool ok + cdef double t_mid, t_in, t_out + cdef double seg_dx_w, seg_dy_w, seg_hq_w, mid_dx_w, mid_dy_w, along_w + cdef Point p_mid, p_in, p_out + cdef State state1, state_mid, state_out + + # Always compute t_mid (needed for recursive sub-calls even when p_mid is + # precomputed at the top level). + t_mid = (t0 + t1) * 0.5 + # Use the pre-computed midpoint if provided; otherwise project via the + # interpolator. Precomputation is only valid at the top level of a segment + # (t0=0, t1=1 from project_linear); all recursive sub-calls pass HUGE_VAL. + if isfinite(precomp_pmid_x): + p_mid.x = precomp_pmid_x + p_mid.y = precomp_pmid_y + else: + p_mid = interpolator.interpolate(t_mid) - if DEBUG: - print(" => valid: ", valid) + # Check whether the direct segment is geometrically acceptable and + # domain-consistent. + ok = straightAndDomain(p0, p1, p_mid, threshold, + gp_domain, inside, geom_fully_inside) - if valid: - (&t_min)[0] = t_current - (&p_min)[0] = p_current + if ok: + # Segment is smooth and domain-consistent: emit it directly. + if state0 == POINT_IN: + lines.add_point_if_empty(p0) + lines.add_point(p1) else: - (&t_max)[0] = t_current - (&p_max)[0] = p_current - - t_current = (t_min + t_max) * 0.5 - p_current = interpolator.interpolate(t_current) + # POINT_OUT or POINT_NAN: only emit p1 if we are entering the domain + state1 = get_state(p1, gp_domain, geom_fully_inside) + if state1 == POINT_IN: + lines.new_line() + lines.add_point(p1) + return + + if depth == 0: + # Reached maximum subdivision depth without finding a smooth segment. + # This means there is a genuine discontinuity (e.g. antimeridian + # crossing, domain boundary) that cannot be resolved further. + # Close the current line and start a new one + state1 = get_state(p1, gp_domain, geom_fully_inside) + if state0 == POINT_IN: + lines.add_point_if_empty(p0) + # Start a new segment for whatever is on the other side. + if state1 == POINT_IN: + lines.new_line() + lines.add_point(p1) + else: + if state1 == POINT_IN: + lines.new_line() + lines.add_point(p1) + return + + # p_mid was already computed before the straightAndDomain check above. + state_mid = get_state(p_mid, gp_domain, geom_fully_inside) + + if state_mid != state0: + # Boundary crossing in the left half [t0, t_mid]. + # Locate it precisely, then recurse on clean sub-intervals so that + # curvature resampling is not confused by the domain boundary. + _find_crossing(t0, p0, state0, t_mid, p_mid, + interpolator, gp_domain, + &t_in, &p_in, &t_out, &p_out, geom_fully_inside) + if t_in > t0 + 1e-6: + # valid t_in segment, recurse on it + _resample_recursive(t0, p0, state0, t_in, p_in, + interpolator, gp_domain, threshold, + lines, depth - 1, geom_fully_inside) + elif state0 == POINT_IN: + # p0 is itself on the boundary: just ensure it is recorded. + lines.add_point_if_empty(p0) + state_out = get_state(p_out, gp_domain, geom_fully_inside) + if state0 != POINT_IN and state_out == POINT_IN: + lines.new_line() + _resample_recursive(t_out, p_out, state_out, t1, p1, + interpolator, gp_domain, threshold, + lines, depth - 1, geom_fully_inside) + else: + state1 = get_state(p1, gp_domain, geom_fully_inside) + if state1 != state0: + # Boundary crossing in the right half [t_mid, t1]. + _find_crossing(t_mid, p_mid, state0, t1, p1, + interpolator, gp_domain, + &t_in, &p_in, &t_out, &p_out, geom_fully_inside) + _resample_recursive(t0, p0, state0, t_in, p_in, + interpolator, gp_domain, threshold, + lines, depth - 1, geom_fully_inside) + state_out = get_state(p_out, gp_domain, geom_fully_inside) + if state0 != POINT_IN and state_out == POINT_IN: + lines.new_line() + _resample_recursive(t_out, p_out, state_out, t1, p1, + interpolator, gp_domain, threshold, + lines, depth - 1, geom_fully_inside) + else: + # Same state throughout. + # Check for a wrap-around crossing: the geodesic midpoint lies + # outside the [0, 1] along-range of the projected segment. This + # signals that the geodesic crosses the domain boundary (e.g. + # the antimeridian in PlateCarree) without a detectable state + # transition, requiring a line split. + if state0 == POINT_IN: + seg_dx_w = p1.x - p0.x + seg_dy_w = p1.y - p0.y + seg_hq_w = seg_dx_w*seg_dx_w + seg_dy_w*seg_dy_w + if seg_hq_w > 0.0: + mid_dx_w = p_mid.x - p0.x + mid_dy_w = p_mid.y - p0.y + along_w = (seg_dx_w*mid_dx_w + seg_dy_w*mid_dy_w) / seg_hq_w + if along_w < 0.0 or along_w > 1.0: + # Wrap detected: binary-search for the last valid + # point before the domain boundary, split, then + # continue from just past the boundary. + # Search the full [t0, t1] interval. + _find_valid_boundary(t0, p0, t0, p0, t1, p1, + interpolator, gp_domain, threshold, + &t_in, &p_in, &t_out, &p_out, + geom_fully_inside) + if t_in > t0 + 1e-6: + _resample_recursive(t0, p0, state0, t_in, p_in, + interpolator, gp_domain, threshold, + lines, depth - 1, geom_fully_inside) + else: + # Start is right at the boundary: record p0 then split. + lines.add_point_if_empty(p0) + lines.new_line() + _resample_recursive(t_out, p_out, state0, t1, p1, + interpolator, gp_domain, threshold, + lines, depth - 1, geom_fully_inside) + return + + # No wrap detected: normal adaptive subdivision for curvature. + # Only recurse for visible (POINT_IN) segments. Non-visible + # segments (POINT_NAN, POINT_OUT) have no points to emit, so + # curvature refinement is wasteful and would cause O(2^depth) + # recursive calls per segment, slowing things down. + if state0 != POINT_IN: + return + _resample_recursive(t0, p0, state0, t_mid, p_mid, + interpolator, gp_domain, threshold, + lines, depth - 1, geom_fully_inside) + _resample_recursive(t_mid, p_mid, state_mid, t1, p1, + interpolator, gp_domain, threshold, + lines, depth - 1, geom_fully_inside) cdef void _project_segment(double[:] src_from, double[:] src_to, @@ -440,82 +733,32 @@ cdef void _project_segment(double[:] src_from, double[:] src_to, Interpolator interpolator, object gp_domain, double threshold, LineAccumulator lines, - bool geom_fully_inside=False) except *: - cdef Point p_current, p_min, p_max, p_end - cdef double t_current, t_min=0, t_max=1 - cdef State state - - p_current.x, p_current.y = src_from - p_end.x, p_end.y = src_to - if DEBUG: - print("Setting line:") - print(" ", p_current.x, ", ", p_current.y) - print(" ", p_end.x, ", ", p_end.y) - - interpolator.set_line(p_current, p_end) - # Now update the current/end with the destination (projected) coords - p_current.x, p_current.y = dest_from - p_end.x, p_end.y = dest_to - if DEBUG: - print("Projected as:") - print(" ", p_current.x, ", ", p_current.y) - print(" ", p_end.x, ", ", p_end.y) - - t_current = 0.0 - state = get_state(p_current, gp_domain, geom_fully_inside) - - cdef size_t old_lines_size = lines.size() - while t_current < 1.0 and (lines.size() - old_lines_size) < 100: - if DEBUG: - print("Bisecting from: ", t_current, " (") - if state == POINT_IN: - print("IN") - elif state == POINT_OUT: - print("OUT") - else: - print("NAN") - print(")") - print(" ", p_current.x, ", ", p_current.y) - print(" ", p_end.x, ", ", p_end.y) - - bisect(t_current, p_current, p_end, gp_domain, state, - interpolator, threshold, - t_min, p_min, t_max, p_max, geom_fully_inside=geom_fully_inside) - if DEBUG: - print(" => ", t_min, "to", t_max) - print(" => (", p_min.x, ", ", p_min.y, ") to (", - p_max.x, ", ", p_max.y, ")") - - if state == POINT_IN: - lines.add_point_if_empty(p_current) - if t_min != t_current: - lines.add_point(p_min) - t_current = t_min - p_current = p_min - else: - t_current = t_max - p_current = p_max - state = get_state(p_current, gp_domain, geom_fully_inside) - if state == POINT_IN: - lines.new_line() - - elif state == POINT_OUT: - if t_min != t_current: - t_current = t_min - p_current = p_min - else: - t_current = t_max - p_current = p_max - state = get_state(p_current, gp_domain, geom_fully_inside) - if state == POINT_IN: - lines.new_line() - - else: - t_current = t_max - p_current = p_max - state = get_state(p_current, gp_domain, geom_fully_inside) - if state == POINT_IN: - lines.new_line() + bool geom_fully_inside=False, + double precomp_pmid_x=HUGE_VAL, + double precomp_pmid_y=HUGE_VAL) except *: + cdef Point p_src_from, p_src_to + cdef Point p0, p1 + cdef State state0 + + # Set up the interpolator with the source (un-projected) endpoints. + p_src_from.x = src_from[0] + p_src_from.y = src_from[1] + p_src_to.x = src_to[0] + p_src_to.y = src_to[1] + interpolator.set_line(p_src_from, p_src_to) + + # Work in destination (projected) coordinates from here on. + p0.x = dest_from[0] + p0.y = dest_from[1] + p1.x = dest_to[0] + p1.y = dest_to[1] + + state0 = get_state(p0, gp_domain, geom_fully_inside) + + _resample_recursive(0.0, p0, state0, 1.0, p1, + interpolator, gp_domain, threshold, + lines, MAX_DEPTH, geom_fully_inside, + precomp_pmid_x, precomp_pmid_y) @lru_cache(maxsize=4) @@ -535,7 +778,7 @@ def _interpolator(src_crs, dest_projection): def project_linear(geometry not None, src_crs not None, - dest_projection not None): + dest_projection not None, bint is_ring=False): """ Project a geometry from one projection to another. @@ -547,22 +790,37 @@ def project_linear(geometry not None, src_crs not None, The coordinate system of the line to be projected. dest_projection : cartopy.crs.Projection The projection for the resulting projected line. + is_ring : bool, optional + Set to ``True`` when *geometry* is a closed ring. Controls the + return type: ``True`` returns an ordered list of + `~shapely.geometry.LineString` fragments (ring-traversal order); + ``False`` returns a `~shapely.geometry.MultiLineString`. + Defaults to ``False``. Returns ------- - `shapely.geometry.MultiLineString` - The result of projecting the given geometry from the source projection - into the destination projection. + `shapely.geometry.MultiLineString` or list of `shapely.geometry.LineString` + When *is_ring* is ``False``, returns a MultiLineString of projected + segments. + + When *is_ring* is ``True``, returns an ordered list of LineString + fragments in ring-traversal order. Fragment ``i`` ends on the + projection boundary and fragment ``(i+1) % N`` starts on the same + boundary; the caller is responsible for inserting the closing + boundary arc between them. If the ring projects entirely inside the + domain with no cuts, a single-element list containing a closed + LineString is returned. """ cdef: double threshold = dest_projection.threshold Interpolator interpolator object g_domain - double[:, :] src_coords, dest_coords + double[:, :] src_coords, dest_coords, mid_dest_coords unsigned int src_size, src_idx object gp_domain LineAccumulator lines + bool has_mid_precomp g_domain = dest_projection.domain @@ -570,76 +828,51 @@ def project_linear(geometry not None, src_crs not None, src_coords = np.asarray(geometry.coords) dest_coords = interpolator.project_points(src_coords) - gp_domain = sprep.prep(g_domain) + # Use the cached prepared domain from the projection – avoids rebuilding the + # prepared geometry (sprep.prep()) on every ring projection call. + gp_domain = dest_projection._prepared_domain src_size = len(src_coords) # check exceptions - # Test the entire geometry to see if there are any domain crossings - # If there are none, then we can skip expensive domain checks later - # TODO: Handle projections other than rectangular + # Test the entire geometry to see if there are any domain crossings. + # If there are none, we can skip expensive per-segment shapely checks. cdef bool geom_fully_inside = False - if isinstance(dest_projection, (ccrs._RectangularProjection, ccrs._WarpedRectangularProjection)): - dest_line = sgeom.LineString([(x[0], x[1]) for x in dest_coords]) - if dest_line.is_valid: - # We can only check for covers with valid geometries - # some have nans/infs at this point still - geom_fully_inside = gp_domain.covers(dest_line) + cdef bool all_finite = True + cdef unsigned int f_idx + for f_idx in range(src_size): + if not (isfinite(dest_coords[f_idx, 0]) and isfinite(dest_coords[f_idx, 1])): + all_finite = False + break + if all_finite: + dest_arr = np.asarray(dest_coords) + dest_line = shapely.linestrings(dest_arr) + geom_fully_inside = gp_domain.covers(dest_line) + + # Batch-project all first-level midpoints in one vectorised call. + mid_dest_np = interpolator.batch_midpoints(src_coords) + has_mid_precomp = mid_dest_np is not None + if has_mid_precomp: + mid_dest_coords = mid_dest_np lines = LineAccumulator() for src_idx in range(1, src_size): - _project_segment(src_coords[src_idx - 1, :2], src_coords[src_idx, :2], - dest_coords[src_idx - 1, :2], dest_coords[src_idx, :2], - interpolator, gp_domain, threshold, lines, - geom_fully_inside=geom_fully_inside); - - del gp_domain + if has_mid_precomp: + _project_segment(src_coords[src_idx - 1, :2], src_coords[src_idx, :2], + dest_coords[src_idx - 1, :2], dest_coords[src_idx, :2], + interpolator, gp_domain, threshold, lines, + geom_fully_inside, + mid_dest_coords[src_idx - 1, 0], + mid_dest_coords[src_idx - 1, 1]) + else: + _project_segment(src_coords[src_idx - 1, :2], src_coords[src_idx, :2], + dest_coords[src_idx - 1, :2], dest_coords[src_idx, :2], + interpolator, gp_domain, threshold, lines, + geom_fully_inside); - multi_line_string = lines.as_geom() + if is_ring: + result = lines.as_ring_fragments() + else: + result = lines.as_geom() del lines, interpolator - return multi_line_string - - -class _Testing: - @staticmethod - def straight_and_within(Point l_start, Point l_end, - double t_start, double t_end, - Interpolator interpolator, double threshold, - object domain): - # This function is for testing/demonstration only. - # It is not careful about freeing resources, and it short-circuits - # optimisations that are made in the real algorithm (in exchange for - # a convenient signature). - - cdef object gp_domain - gp_domain = sprep.prep(domain) - - state = get_state(interpolator.project(l_start), gp_domain) - cdef bool p_start_inside_domain = state == POINT_IN - - # l_end and l_start should be un-projected. - interpolator.set_line(l_start, l_end) - - cdef Point p0 = interpolator.interpolate(t_start) - cdef Point p1 = interpolator.interpolate(t_end) - - valid = straightAndDomain( - t_start, p0, t_end, p1, - interpolator, threshold, - gp_domain, p_start_inside_domain) - - del gp_domain - return valid - - @staticmethod - def interpolator(source_crs, destination_projection): - return _interpolator(source_crs, destination_projection) - - @staticmethod - def interp_prj_pt(Interpolator interp, const Point &lonlat): - return interp.project(lonlat) - - @staticmethod - def interp_t_pt(Interpolator interp, const Point &start, const Point &end, double t): - interp.set_line(start, end) - return interp.interpolate(t) + return result