Skip to content

Commit db45aaa

Browse files
committed
ENH: Add HEALPix and rHEALPix projections
1 parent 6471087 commit db45aaa

File tree

7 files changed

+171
-1
lines changed

7 files changed

+171
-1
lines changed

docs/make_projection.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,8 @@ def utm_plot():
9595
'OSGB': 4, 'LambertZoneII': 4.1, 'EuroPP': 5,
9696
'Geostationary': 6, 'NearsidePerspective': 7,
9797
'EckertI': 8.1, 'EckertII': 8.2, 'EckertIII': 8.3,
98-
'EckertIV': 8.4, 'EckertV': 8.5, 'EckertVI': 8.6}
98+
'EckertIV': 8.4, 'EckertV': 8.5, 'EckertVI': 8.6,
99+
'HEALPix': 9.1, 'RHEALPix': 9.2}
99100

100101

101102
def find_projections():

lib/cartopy/crs.py

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

18341834

1835+
class HEALPix(Projection):
1836+
"""
1837+
Hierarchical Equal Area isoLatitude Pixelisation (HEALPix) of a 2-sphere is an
1838+
algorithm for pixelisation of the 2-sphere based on subdivision of a distorted
1839+
rhombic dodecahedron. The HEALPix projection is area preserving and can be used
1840+
with a spherical and ellipsoidal model. It was initially developed for mapping
1841+
cosmic background microwave radiation.
1842+
1843+
"""
1844+
_wrappable = True
1845+
1846+
def __init__(self, central_longitude=0, globe=None):
1847+
"""
1848+
Parameters
1849+
----------
1850+
central_longitude: optional
1851+
The true longitude of the central meridian in degrees.
1852+
Defaults to 0.
1853+
1854+
globe: optional
1855+
An instance of :class:`cartopy.crs.Globe`. If omitted, a default
1856+
globe with a spherical radius of 6370997 meters is created.
1857+
"""
1858+
proj4_params = [('proj', 'healpix'),
1859+
('lon_0', central_longitude)]
1860+
super().__init__(proj4_params, globe=globe)
1861+
# Defaults to the PROJ sphere with semimajor axis 6370997m
1862+
w = (self.globe.semimajor_axis or 6370997) * np.pi
1863+
h = w/2
1864+
self.bounds = [-w, w, -h, h]
1865+
self._threshold = w / 1e4
1866+
1867+
self._boundary = sgeom.LinearRing(
1868+
[(-w, h/2), (-3/4*w, h), (-w/2, h/2), (-w/4, h), (0, h/2),
1869+
(w/4, h), (w/2, h/2), (3/4*w, h), (w, h/2),
1870+
(w, -h/2), (3/4*w, -h), (w/2, -h/2), (w/4, -h), (0, -h/2),
1871+
(-w/4, -h), (-w/2, -h/2), (-3/4*w, -h), (-w, -h/2), (-w, h/2)])
1872+
1873+
@property
1874+
def boundary(self):
1875+
return self._boundary
1876+
1877+
1878+
class RHEALPix(Projection):
1879+
"""
1880+
Also known as rHEALPix in PROJ, this projection is an extension of the
1881+
HEALPix projection to present rectangles, rather than triangles, at the
1882+
north and south poles.
1883+
1884+
Parameters
1885+
----------
1886+
central_longitude: optional
1887+
The true longitude of the central meridian in degrees.
1888+
Defaults to 0.
1889+
north_square : int
1890+
The position for the north pole square. Must be one of 0, 1, 2 or 3.
1891+
0 would have the north pole square aligned with the left-most square,
1892+
and 3 would be aligned with the right-most.
1893+
south_square : int
1894+
The position for the south pole square. Must be one of 0, 1, 2 or 3.
1895+
"""
1896+
_wrappable = True
1897+
1898+
def __init__(self, central_longitude=0, north_square=0, south_square=0, globe=None):
1899+
valid_square_pos = [0, 1, 2, 3]
1900+
if north_square not in valid_square_pos:
1901+
raise ValueError(f'north_square must be one of {valid_square_pos}')
1902+
if south_square not in valid_square_pos:
1903+
raise ValueError(f'south_square must be one of {valid_square_pos}')
1904+
1905+
proj4_params = [('proj', 'rhealpix'),
1906+
('north_square', north_square),
1907+
('south_square', south_square),
1908+
('lon_0', central_longitude)]
1909+
super().__init__(proj4_params)
1910+
1911+
# Defaults to the PROJ sphere with semimajor axis 6370997m
1912+
w = (self.globe.semimajor_axis or 6370997) * np.pi
1913+
h = 3/4 * w
1914+
box_h = w / 4
1915+
self.bounds = [-w, w, -h, h]
1916+
self._threshold = w / 1e4
1917+
1918+
self._boundary = sgeom.LinearRing([
1919+
# Left edge
1920+
(-w, box_h),
1921+
# Cut-out for the north square
1922+
(-w + north_square * w/2, box_h),
1923+
(-w + north_square * w/2, h),
1924+
(-w + (north_square + 1) * w/2, h),
1925+
(-w + (north_square + 1) * w/2, box_h),
1926+
# Right edge
1927+
(w, box_h),
1928+
(w, -box_h),
1929+
# Cut-out for the south square
1930+
(-w + (south_square + 1) * w/2, -box_h),
1931+
(-w + (south_square + 1) * w/2, -h),
1932+
(-w + south_square * w/2, -h),
1933+
(-w + south_square * w/2, -box_h),
1934+
# Left edge
1935+
(-w, -box_h),
1936+
(-w, box_h)
1937+
])
1938+
1939+
@property
1940+
def boundary(self):
1941+
return self._boundary
1942+
1943+
18351944
class LambertAzimuthalEqualArea(Projection):
18361945
"""
18371946
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
@@ -169,6 +169,7 @@ def test_simple_global():
169169
ccrs.EqualEarth,
170170
ccrs.Gnomonic,
171171
ccrs.Hammer,
172+
ccrs.HEALPix,
172173
pytest.param((ccrs.InterruptedGoodeHomolosine, dict(emphasis='land')),
173174
id='InterruptedGoodeHomolosine'),
174175
ccrs.LambertCylindrical,
@@ -185,6 +186,7 @@ def test_simple_global():
185186
pytest.param((ccrs.RotatedPole,
186187
dict(pole_latitude=45, pole_longitude=180)),
187188
id='RotatedPole'),
189+
ccrs.RHEALPix,
188190
ccrs.Stereographic,
189191
ccrs.SouthPolarStereo,
190192
pytest.param((ccrs.TransverseMercator, dict(approx=True)),

0 commit comments

Comments
 (0)