Skip to content

Commit

Permalink
Merge pull request #2283 from greglucas/non-earth-projections
Browse files Browse the repository at this point in the history
FIX: Handle non-earth bodies within Projections
  • Loading branch information
dopplershift authored Nov 30, 2023
2 parents f0991f4 + a6c3419 commit 234a077
Show file tree
Hide file tree
Showing 6 changed files with 57 additions and 32 deletions.
2 changes: 1 addition & 1 deletion lib/cartopy/crs.py
Original file line number Diff line number Diff line change
Expand Up @@ -1783,7 +1783,7 @@ def __init__(self, central_longitude=-96.0, central_latitude=39.0,
lons[1:-1] = np.linspace(central_longitude - 180 + 0.001,
central_longitude + 180 - 0.001, n)

points = self.transform_points(PlateCarree(), lons, lats)
points = self.transform_points(PlateCarree(globe=globe), lons, lats)

self._boundary = sgeom.LinearRing(points)
mins = np.min(points, axis=0)
Expand Down
2 changes: 1 addition & 1 deletion lib/cartopy/mpl/geoaxes.py
Original file line number Diff line number Diff line change
Expand Up @@ -1499,7 +1499,7 @@ def gridlines(self, crs=None, draw_labels=False,
the Y axis.
"""
if crs is None:
crs = ccrs.PlateCarree()
crs = ccrs.PlateCarree(globe=self.projection.globe)
from cartopy.mpl.gridliner import Gridliner
gl = Gridliner(
self, crs=crs, draw_labels=draw_labels, xlocator=xlocs,
Expand Down
36 changes: 22 additions & 14 deletions lib/cartopy/mpl/ticker.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,6 @@ class _PlateCarreeFormatter(Formatter):
rectangular projection (e.g. Plate Carree, Mercator).
"""

_target_projection = ccrs.PlateCarree()

def __init__(self, direction_label=True, degree_symbol='°',
number_format='g', transform_precision=1e-8, dms=False,
minute_symbol='′', second_symbol='″',
Expand Down Expand Up @@ -52,21 +49,14 @@ def __init__(self, direction_label=True, degree_symbol='°',
cardinal_labels = {}
self._cardinal_labels = cardinal_labels
self._decimal_point = decimal_point
self._source_projection = None
self._target_projection = None

def __call__(self, value, pos=None):
if self.axis is not None and isinstance(self.axis.axes, GeoAxes):

# We want to produce labels for values in the familiar Plate Carree
# projection, so convert the tick values from their own projection
# before formatting them.
source = self.axis.axes.projection
if not isinstance(source, (ccrs._RectangularProjection,
ccrs.Mercator)):
raise TypeError("This formatter cannot be used with "
"non-rectangular projections.")
if self._source_projection is not None:
projected_value = self._apply_transform(value,
self._target_projection,
source)
self._source_projection)

# Round the transformed value using a given precision for display
# purposes. Transforms can introduce minor rounding errors that
Expand Down Expand Up @@ -137,6 +127,24 @@ def _get_dms(self, x):
secs = np.round((y - mins) * 60, self._precision - 3)
return x, degs, mins, secs

def set_axis(self, axis):
super().set_axis(axis)

# Set the source and target projections for the formatter
# setting them to None if we aren't interacting with a GeoAxes
if self.axis is None or not isinstance(self.axis.axes, GeoAxes):
self._source_projection = None
self._target_projection = None
return

self._source_projection = self.axis.axes.projection
if not isinstance(self._source_projection, (ccrs._RectangularProjection,
ccrs.Mercator)):
raise TypeError("This formatter cannot be used with "
"non-rectangular projections.")
# The transforms need to use the same globe
self._target_projection = ccrs.PlateCarree(globe=self._source_projection.globe)

def set_locs(self, locs):
Formatter.set_locs(self, locs)
if not self._auto_hide:
Expand Down
8 changes: 8 additions & 0 deletions lib/cartopy/tests/crs/test_lambert_conformal.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,14 @@ def test_specific_lambert():
check_proj_params('lcc', crs, other_args)


def test_lambert_moon():
moon = ccrs.Globe(ellipse=None, semimajor_axis=1737400, semiminor_axis=1737400)
crs = ccrs.LambertConformal(globe=moon)
other_args = {'a=1737400', 'b=1737400', 'lat_0=39.0', 'lat_1=33', 'lat_2=45',
'lon_0=-96.0', 'x_0=0.0', 'y_0=0.0'}
check_proj_params('lcc', crs, other_args)


class Test_LambertConformal_standard_parallels:
def test_single_value(self):
crs = ccrs.LambertConformal(standard_parallels=[1.])
Expand Down
14 changes: 12 additions & 2 deletions lib/cartopy/tests/mpl/test_gridliner.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@

@pytest.mark.natural_earth
# Robinson projection is slightly better in Proj 6+.
@pytest.mark.mpl_image_compare(filename='gridliner1.png', tolerance=0.7)
@pytest.mark.mpl_image_compare(filename='gridliner1.png', tolerance=0.73)
def test_gridliner():
ny, nx = 2, 4

Expand Down Expand Up @@ -308,7 +308,7 @@ def test_grid_labels_inline(proj):
else:
kwargs = {}
ax = fig.add_subplot(projection=proj(**kwargs))
ax.gridlines(draw_labels=True, auto_inline=True)
ax.gridlines(crs=ccrs.PlateCarree(), draw_labels=True, auto_inline=True)
ax.coastlines(resolution="110m")
return fig

Expand Down Expand Up @@ -589,3 +589,13 @@ def test_gridliner_labels_zoom():
# After zoom, we may not be using all the available labels.
assert len(gl._all_labels) == 24
assert gl._labels == gl._all_labels[:20]


def test_gridliner_with_globe():
fig = plt.figure()
proj = ccrs.PlateCarree(globe=ccrs.Globe(semimajor_axis=12345))
ax = fig.add_subplot(1, 1, 1, projection=proj)
gl = ax.gridlines()
fig.draw_without_rendering()

assert gl in ax.artists
27 changes: 13 additions & 14 deletions lib/cartopy/tests/mpl/test_ticker.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,16 +27,15 @@
@pytest.mark.parametrize('cls', [LatitudeFormatter, LongitudeFormatter])
def test_formatter_bad_projection(cls):
formatter = cls()
formatter.axis = Mock(axes=Mock(GeoAxes, projection=ccrs.Orthographic()))
match = r'This formatter cannot be used with non-rectangular projections\.'
with pytest.raises(TypeError, match=match):
formatter(0)
formatter.set_axis(Mock(axes=Mock(GeoAxes, projection=ccrs.Orthographic())))


def test_LatitudeFormatter():
formatter = LatitudeFormatter()
p = ccrs.PlateCarree()
formatter.axis = Mock(axes=Mock(GeoAxes, projection=p))
formatter.set_axis(Mock(axes=Mock(GeoAxes, projection=p)))
test_ticks = [-90, -60, -30, 0, 30, 60, 90]
result = [formatter(tick) for tick in test_ticks]
expected = ['90°S', '60°S', '30°S', '0°', '30°N', '60°N', '90°N']
Expand All @@ -46,7 +45,7 @@ def test_LatitudeFormatter():
def test_LatitudeFormatter_direction_label():
formatter = LatitudeFormatter(direction_label=False)
p = ccrs.PlateCarree()
formatter.axis = Mock(axes=Mock(GeoAxes, projection=p))
formatter.set_axis(Mock(axes=Mock(GeoAxes, projection=p)))
test_ticks = [-90, -60, -30, 0, 30, 60, 90]
result = [formatter(tick) for tick in test_ticks]
expected = ['-90°', '-60°', '-30°', '0°', '30°', '60°', '90°']
Expand All @@ -56,7 +55,7 @@ def test_LatitudeFormatter_direction_label():
def test_LatitudeFormatter_degree_symbol():
formatter = LatitudeFormatter(degree_symbol='')
p = ccrs.PlateCarree()
formatter.axis = Mock(axes=Mock(GeoAxes, projection=p))
formatter.set_axis(Mock(axes=Mock(GeoAxes, projection=p)))
test_ticks = [-90, -60, -30, 0, 30, 60, 90]
result = [formatter(tick) for tick in test_ticks]
expected = ['90S', '60S', '30S', '0', '30N', '60N', '90N']
Expand All @@ -66,7 +65,7 @@ def test_LatitudeFormatter_degree_symbol():
def test_LatitudeFormatter_number_format():
formatter = LatitudeFormatter(number_format='.2f', dms=False)
p = ccrs.PlateCarree()
formatter.axis = Mock(axes=Mock(GeoAxes, projection=p))
formatter.set_axis(Mock(axes=Mock(GeoAxes, projection=p)))
test_ticks = [-90, -60, -30, 0, 30, 60, 90]
result = [formatter(tick) for tick in test_ticks]
expected = ['90.00°S', '60.00°S', '30.00°S', '0.00°',
Expand All @@ -77,7 +76,7 @@ def test_LatitudeFormatter_number_format():
def test_LatitudeFormatter_mercator():
formatter = LatitudeFormatter()
p = ccrs.Mercator()
formatter.axis = Mock(axes=Mock(GeoAxes, projection=p))
formatter.set_axis(Mock(axes=Mock(GeoAxes, projection=p)))
test_ticks = [-15496570.739707904, -8362698.548496634,
-3482189.085407435, 0.0, 3482189.085407435,
8362698.548496634, 15496570.739707898]
Expand All @@ -89,7 +88,7 @@ def test_LatitudeFormatter_mercator():
def test_LatitudeFormatter_small_numbers():
formatter = LatitudeFormatter(number_format='.7f', dms=False)
p = ccrs.PlateCarree()
formatter.axis = Mock(axes=Mock(GeoAxes, projection=p))
formatter.set_axis(Mock(axes=Mock(GeoAxes, projection=p)))
test_ticks = [40.1275150, 40.1275152, 40.1275154]
result = [formatter(tick) for tick in test_ticks]
expected = ['40.1275150°N', '40.1275152°N', '40.1275154°N']
Expand All @@ -101,7 +100,7 @@ def test_LongitudeFormatter_direction_label():
dateline_direction_label=True,
zero_direction_label=True)
p = ccrs.PlateCarree()
formatter.axis = Mock(axes=Mock(GeoAxes, projection=p))
formatter.set_axis(Mock(axes=Mock(GeoAxes, projection=p)))
test_ticks = [-180, -120, -60, 0, 60, 120, 180]
result = [formatter(tick) for tick in test_ticks]
expected = ['-180°', '-120°', '-60°', '0°', '60°', '120°', '180°']
Expand All @@ -120,7 +119,7 @@ def test_LongitudeFormatter_central_longitude(central_longitude, kwargs,
expected):
formatter = LongitudeFormatter(**kwargs)
p = ccrs.PlateCarree(central_longitude=central_longitude)
formatter.axis = Mock(axes=Mock(GeoAxes, projection=p))
formatter.set_axis(Mock(axes=Mock(GeoAxes, projection=p)))
test_ticks = [-180, -120, -60, 0, 60, 120, 180]
result = [formatter(tick) for tick in test_ticks]
assert result == expected
Expand All @@ -130,7 +129,7 @@ def test_LongitudeFormatter_degree_symbol():
formatter = LongitudeFormatter(degree_symbol='',
dateline_direction_label=True)
p = ccrs.PlateCarree()
formatter.axis = Mock(axes=Mock(GeoAxes, projection=p))
formatter.set_axis(Mock(axes=Mock(GeoAxes, projection=p)))
test_ticks = [-180, -120, -60, 0, 60, 120, 180]
result = [formatter(tick) for tick in test_ticks]
expected = ['180W', '120W', '60W', '0', '60E', '120E', '180E']
Expand All @@ -141,7 +140,7 @@ def test_LongitudeFormatter_number_format():
formatter = LongitudeFormatter(number_format='.2f', dms=False,
dateline_direction_label=True)
p = ccrs.PlateCarree()
formatter.axis = Mock(axes=Mock(GeoAxes, projection=p))
formatter.set_axis(Mock(axes=Mock(GeoAxes, projection=p)))
test_ticks = [-180, -120, -60, 0, 60, 120, 180]
result = [formatter(tick) for tick in test_ticks]
expected = ['180.00°W', '120.00°W', '60.00°W', '0.00°',
Expand All @@ -152,7 +151,7 @@ def test_LongitudeFormatter_number_format():
def test_LongitudeFormatter_mercator():
formatter = LongitudeFormatter(dateline_direction_label=True)
p = ccrs.Mercator()
formatter.axis = Mock(axes=Mock(GeoAxes, projection=p))
formatter.set_axis(Mock(axes=Mock(GeoAxes, projection=p)))
test_ticks = [-20037508.342783064, -13358338.895188706,
-6679169.447594353, 0.0, 6679169.447594353,
13358338.895188706, 20037508.342783064]
Expand All @@ -170,7 +169,7 @@ def test_LongitudeFormatter_small_numbers(central_longitude,
formatter = LongitudeFormatter(number_format='.7f', dms=False,
zero_direction_label=zero_direction_label)
p = ccrs.PlateCarree(central_longitude=central_longitude)
formatter.axis = Mock(axes=Mock(GeoAxes, projection=p))
formatter.set_axis(Mock(axes=Mock(GeoAxes, projection=p)))
test_ticks = [-17.1142343, -17.1142340, -17.1142337]
result = [formatter(tick) for tick in test_ticks]
assert result == expected
Expand Down

0 comments on commit 234a077

Please sign in to comment.