Skip to content

Commit fd369bd

Browse files
committed
ENH: Add HEALPix and rHEALPix projections
1 parent 940e9f2 commit fd369bd

File tree

8 files changed

+204
-4
lines changed

8 files changed

+204
-4
lines changed

docs/make_projection.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,11 @@
66
import inspect
77
from pathlib import Path
88
import textwrap
9+
import warnings
910

1011
import matplotlib.pyplot as plt
1112
import numpy as np
13+
from pyproj.exceptions import CRSError
1214

1315
import cartopy.crs as ccrs
1416

@@ -96,7 +98,8 @@ def utm_plot():
9698
'Geostationary': 6, 'NearsidePerspective': 7,
9799
'EckertI': 8.1, 'EckertII': 8.2, 'EckertIII': 8.3,
98100
'EckertIV': 8.4, 'EckertV': 8.5, 'EckertVI': 8.6,
99-
'Spilhaus': 9}
101+
'Spilhaus': 9,
102+
'HEALPix': 9.1, 'RHEALPix': 9.2}
100103

101104

102105
def find_projections():
@@ -162,7 +165,14 @@ def prj_class_sorter(cls):
162165
# Get instance arguments and number of plots
163166
instance_args = SPECIFIC_PROJECTION_KWARGS.get(prj, {})
164167

165-
prj_inst, instance_repr = create_instance(prj, instance_args)
168+
try:
169+
prj_inst, instance_repr = create_instance(prj, instance_args)
170+
except CRSError:
171+
# Installed PROJ may be too old/new to support this projection
172+
table.write('\n')
173+
warnings.warn(
174+
f'Projection {prj.__name__} not supported by this version of PROJ')
175+
continue
166176

167177
aspect = (np.diff(prj_inst.x_limits) /
168178
np.diff(prj_inst.y_limits))[0]

docs/source/reference/projections.rst

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -509,13 +509,34 @@ Spilhaus
509509

510510
.. autoclass:: cartopy.crs.Spilhaus
511511

512+
HEALPix
513+
-------
514+
515+
.. autoclass:: cartopy.crs.HEALPix
516+
517+
.. plot::
518+
519+
import matplotlib.pyplot as plt
520+
import cartopy.crs as ccrs
521+
522+
plt.figure(figsize=(6, 3))
523+
ax = plt.axes(projection=ccrs.HEALPix())
524+
ax.coastlines(resolution='110m')
525+
ax.gridlines()
526+
527+
528+
RHEALPix
529+
--------
530+
531+
.. autoclass:: cartopy.crs.RHEALPix
532+
512533
.. plot::
513534

514535
import matplotlib.pyplot as plt
515536
import cartopy.crs as ccrs
516537

517-
plt.figure(figsize=(3.0004, 3))
518-
ax = plt.axes(projection=ccrs.Spilhaus())
538+
plt.figure(figsize=(4, 3))
539+
ax = plt.axes(projection=ccrs.RHEALPix())
519540
ax.coastlines(resolution='110m')
520541
ax.gridlines()
521542

lib/cartopy/crs.py

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1862,6 +1862,115 @@ def __init__(self):
18621862
self.bounds = [-5242.32, 1212512.16, 1589155.51, 2706796.21]
18631863

18641864

1865+
class HEALPix(Projection):
1866+
"""
1867+
Hierarchical Equal Area isoLatitude Pixelisation (HEALPix) of a 2-sphere is an
1868+
algorithm for pixelisation of the 2-sphere based on subdivision of a distorted
1869+
rhombic dodecahedron. The HEALPix projection is area preserving and can be used
1870+
with a spherical and ellipsoidal model. It was initially developed for mapping
1871+
cosmic background microwave radiation.
1872+
1873+
"""
1874+
_wrappable = True
1875+
1876+
def __init__(self, central_longitude=0, globe=None):
1877+
"""
1878+
Parameters
1879+
----------
1880+
central_longitude: optional
1881+
The true longitude of the central meridian in degrees.
1882+
Defaults to 0.
1883+
1884+
globe: optional
1885+
An instance of :class:`cartopy.crs.Globe`. If omitted, a default
1886+
globe with a spherical radius of 6370997 meters is created.
1887+
"""
1888+
proj4_params = [('proj', 'healpix'),
1889+
('lon_0', central_longitude)]
1890+
super().__init__(proj4_params, globe=globe)
1891+
# Defaults to the PROJ sphere with semimajor axis 6370997m
1892+
w = (self.globe.semimajor_axis or 6370997) * np.pi
1893+
h = w/2
1894+
self.bounds = [-w, w, -h, h]
1895+
self._threshold = w / 1e4
1896+
1897+
self._boundary = sgeom.LinearRing(
1898+
[(-w, h/2), (-3/4*w, h), (-w/2, h/2), (-w/4, h), (0, h/2),
1899+
(w/4, h), (w/2, h/2), (3/4*w, h), (w, h/2),
1900+
(w, -h/2), (3/4*w, -h), (w/2, -h/2), (w/4, -h), (0, -h/2),
1901+
(-w/4, -h), (-w/2, -h/2), (-3/4*w, -h), (-w, -h/2), (-w, h/2)])
1902+
1903+
@property
1904+
def boundary(self):
1905+
return self._boundary
1906+
1907+
1908+
class RHEALPix(Projection):
1909+
"""
1910+
Also known as rHEALPix in PROJ, this projection is an extension of the
1911+
HEALPix projection to present rectangles, rather than triangles, at the
1912+
north and south poles.
1913+
1914+
Parameters
1915+
----------
1916+
central_longitude: optional
1917+
The true longitude of the central meridian in degrees.
1918+
Defaults to 0.
1919+
north_square : int
1920+
The position for the north pole square. Must be one of 0, 1, 2 or 3.
1921+
0 would have the north pole square aligned with the left-most square,
1922+
and 3 would be aligned with the right-most.
1923+
south_square : int
1924+
The position for the south pole square. Must be one of 0, 1, 2 or 3.
1925+
"""
1926+
_wrappable = True
1927+
1928+
def __init__(self, central_longitude=0, north_square=0, south_square=0, globe=None):
1929+
valid_square_pos = [0, 1, 2, 3]
1930+
if north_square not in valid_square_pos:
1931+
raise ValueError(f'north_square must be one of {valid_square_pos}')
1932+
if south_square not in valid_square_pos:
1933+
raise ValueError(f'south_square must be one of {valid_square_pos}')
1934+
1935+
proj4_params = [('proj', 'rhealpix'),
1936+
('north_square', north_square),
1937+
('south_square', south_square),
1938+
('lon_0', central_longitude)]
1939+
super().__init__(proj4_params)
1940+
1941+
# Defaults to the PROJ sphere with semimajor axis 6370997m
1942+
w = (self.globe.semimajor_axis or 6370997) * np.pi
1943+
h = 3/4 * w
1944+
box_h = w / 4
1945+
self.bounds = [-w, w, -h, h]
1946+
self._threshold = w / 1e4
1947+
1948+
self._boundary = sgeom.LinearRing([
1949+
# Left edge
1950+
(-w, box_h),
1951+
# Cut-out for the north square
1952+
(-w + north_square * w/2, box_h),
1953+
(-w + north_square * w/2, h),
1954+
(-w + (north_square + 1) * w/2, h),
1955+
(-w + (north_square + 1) * w/2, box_h),
1956+
# Right edge
1957+
(w, box_h),
1958+
(w, -box_h),
1959+
# Cut-out for the south square
1960+
(-w + (south_square + 1) * w/2, -box_h),
1961+
(-w + (south_square + 1) * w/2, -h),
1962+
(-w + south_square * w/2, -h),
1963+
(-w + south_square * w/2, -box_h),
1964+
# Left edge
1965+
(-w, -box_h),
1966+
(-w, box_h)
1967+
])
1968+
1969+
@property
1970+
def boundary(self):
1971+
return self._boundary
1972+
1973+
18651974
class LambertAzimuthalEqualArea(Projection):
18661975
"""
18671976
A Lambert Azimuthal Equal-Area projection.
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# Copyright Crown and Cartopy Contributors
2+
#
3+
# This file is part of Cartopy and is released under the BSD 3-clause license.
4+
# See LICENSE in the root of the repository for full licensing details.
5+
"""
6+
Tests for the HEALPix projection.
7+
8+
"""
9+
import cartopy.crs as ccrs
10+
from .helpers import check_proj_params
11+
12+
13+
def test_defaults():
14+
crs = ccrs.HEALPix()
15+
expected = {'ellps=WGS84', 'lon_0=0'}
16+
check_proj_params('healpix', crs, expected)
17+
18+
19+
def test_central_longitude():
20+
crs = ccrs.HEALPix(central_longitude=124.8)
21+
expected = {'ellps=WGS84', 'lon_0=124.8'}
22+
check_proj_params('healpix', crs, expected)
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# Copyright Crown and Cartopy Contributors
2+
#
3+
# This file is part of Cartopy and is released under the BSD 3-clause license.
4+
# See LICENSE in the root of the repository for full licensing details.
5+
"""
6+
Tests for the RHEALPix projection.
7+
8+
"""
9+
import pytest
10+
11+
import cartopy.crs as ccrs
12+
from .helpers import check_proj_params
13+
14+
15+
def test_defaults():
16+
crs = ccrs.RHEALPix()
17+
expected = {'ellps=WGS84', 'lon_0=0', 'north_square=0', 'south_square=0'}
18+
check_proj_params('rhealpix', crs, expected)
19+
20+
21+
def test_square_positions():
22+
crs = ccrs.RHEALPix(north_square=1, south_square=2)
23+
expected = {'ellps=WGS84', 'lon_0=0', 'north_square=1', 'south_square=2'}
24+
check_proj_params('rhealpix', crs, expected)
25+
26+
def test_invalid_square_positions():
27+
with pytest.raises(ValueError, match='north_square must be'):
28+
ccrs.RHEALPix(north_square=4)
29+
with pytest.raises(ValueError, match='south_square must be'):
30+
ccrs.RHEALPix(south_square=-1)
31+
32+
def test_central_longitude():
33+
crs = ccrs.RHEALPix(north_square=2, central_longitude=-124.8)
34+
expected = {'ellps=WGS84', 'lon_0=-124.8', 'north_square=2',
35+
'south_square=0'}
36+
check_proj_params('rhealpix', crs, expected)
9.61 KB
Loading
8.45 KB
Loading

lib/cartopy/tests/mpl/test_mpl_integration.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,7 @@ def test_simple_global():
173173
ccrs.EqualEarth,
174174
ccrs.Gnomonic,
175175
ccrs.Hammer,
176+
ccrs.HEALPix,
176177
pytest.param((ccrs.InterruptedGoodeHomolosine, dict(emphasis='land')),
177178
id='InterruptedGoodeHomolosine'),
178179
ccrs.LambertCylindrical,
@@ -193,6 +194,7 @@ def test_simple_global():
193194
pytest.param((ccrs.RotatedPole,
194195
dict(pole_latitude=45, pole_longitude=180)),
195196
id='RotatedPole'),
197+
ccrs.RHEALPix,
196198
ccrs.Stereographic,
197199
ccrs.SouthPolarStereo,
198200
pytest.param((ccrs.TransverseMercator, dict(approx=True)),

0 commit comments

Comments
 (0)