Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 84 additions & 0 deletions examples/lines_and_polygons/adaptive_resampling_example.py
Original file line number Diff line number Diff line change
@@ -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()
14 changes: 14 additions & 0 deletions lib/cartopy/crs.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified lib/cartopy/tests/mpl/baseline_images/mpl/test_crs/igh_land.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified lib/cartopy/tests/mpl/baseline_images/mpl/test_crs/igh_ocean.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified lib/cartopy/tests/mpl/baseline_images/mpl/test_ticks/xyticks.png
1 change: 0 additions & 1 deletion lib/cartopy/tests/test_line_string.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
21 changes: 12 additions & 9 deletions lib/cartopy/tests/test_polygon.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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'))
Expand Down
Loading
Loading