Skip to content

Commit 23374f3

Browse files
cvelascofpelson
authored andcommitted
Add Albers Equal Area Projection and some test as per #2496 (#2943)
Add Albers Equal Area Projection and NetCDF rules to handle it
1 parent 21a545d commit 23374f3

File tree

5 files changed

+305
-1
lines changed

5 files changed

+305
-1
lines changed

lib/iris/coord_systems.py

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -934,3 +934,84 @@ def as_cartopy_crs(self):
934934

935935
def as_cartopy_projection(self):
936936
return self.as_cartopy_crs()
937+
938+
939+
class AlbersEqualArea(CoordSystem):
940+
"""
941+
A coordinate system in the Albers Conical Equal Area projection.
942+
943+
"""
944+
945+
grid_mapping_name = "albers_conical_equal_area"
946+
947+
def __init__(self, latitude_of_projection_origin=0.0,
948+
longitude_of_central_meridian=0.0,
949+
false_easting=0.0, false_northing=0.0,
950+
standard_parallels=(20.0, 50.0),
951+
ellipsoid=None):
952+
"""
953+
Constructs a Albers Conical Equal Area coord system.
954+
955+
Kwargs:
956+
957+
* latitude_of_projection_origin
958+
True latitude of planar origin in degrees.
959+
Defaults to 0.
960+
961+
* longitude_of_central_meridian
962+
True longitude of planar central meridian in degrees.
963+
Defaults to 0.
964+
965+
* false_easting
966+
X offset from planar origin in metres. Defaults to 0.
967+
968+
* false_northing
969+
Y offset from planar origin in metres. Defaults to 0.
970+
971+
* standard_parallels
972+
The one or two latitudes of correct scale.
973+
Defaults to (20,50).
974+
* ellipsoid
975+
:class:`GeogCS` defining the ellipsoid.
976+
977+
"""
978+
#: True latitude of planar origin in degrees.
979+
self.latitude_of_projection_origin = latitude_of_projection_origin
980+
#: True longitude of planar central meridian in degrees.
981+
self.longitude_of_central_meridian = longitude_of_central_meridian
982+
#: X offset from planar origin in metres.
983+
self.false_easting = false_easting
984+
#: Y offset from planar origin in metres.
985+
self.false_northing = false_northing
986+
#: The one or two latitudes of correct scale.
987+
self.standard_parallels = standard_parallels
988+
#: Ellipsoid definition.
989+
self.ellipsoid = ellipsoid
990+
991+
def __repr__(self):
992+
return ("AlbersEqualArea(latitude_of_projection_origin={!r},"
993+
" longitude_of_central_meridian={!r}, false_easting={!r},"
994+
" false_northing={!r}, standard_parallels={!r},"
995+
" ellipsoid={!r})").format(
996+
self.latitude_of_projection_origin,
997+
self.longitude_of_central_meridian,
998+
self.false_easting,
999+
self.false_northing,
1000+
self.standard_parallels,
1001+
self.ellipsoid)
1002+
1003+
def as_cartopy_crs(self):
1004+
if self.ellipsoid is not None:
1005+
globe = self.ellipsoid.as_cartopy_globe()
1006+
else:
1007+
globe = ccrs.Globe()
1008+
return ccrs.AlbersEqualArea(
1009+
central_longitude=self.longitude_of_central_meridian,
1010+
central_latitude=self.latitude_of_projection_origin,
1011+
false_easting=self.false_easting,
1012+
false_northing=self.false_northing,
1013+
standard_parallels=self.standard_parallels,
1014+
globe=globe)
1015+
1016+
def as_cartopy_projection(self):
1017+
return self.as_cartopy_crs()

lib/iris/fileformats/_pyke_rules/fc_rules_cf.krb

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,25 @@ fc_provides_grid_mapping_lambert_azimuthal_equal_area
176176
facts_cf.provides(coordinate_system, lambert_azimuthal_equal_area)
177177
python engine.rule_triggered.add(rule.name)
178178

179+
#
180+
# Context:
181+
# This rule will trigger iff a grid_mapping() case specific fact
182+
# has been asserted that refers to a albers conical equal area.
183+
#
184+
# Purpose:
185+
# Creates the albers conical equal area coordinate system.
186+
#
187+
fc_provides_grid_mapping_albers_equal_area
188+
foreach
189+
facts_cf.grid_mapping($grid_mapping)
190+
check is_grid_mapping(engine, $grid_mapping, CF_GRID_MAPPING_ALBERS)
191+
assert
192+
python cf_grid_var = engine.cf_var.cf_group.grid_mappings[$grid_mapping]
193+
python coordinate_system = build_albers_equal_area_coordinate_system(engine, cf_grid_var)
194+
python engine.provides['coordinate_system'] = coordinate_system
195+
facts_cf.provides(coordinate_system, albers_equal_area)
196+
python engine.rule_triggered.add(rule.name)
197+
179198

180199
#
181200
# Context:
@@ -774,6 +793,45 @@ fc_build_coordinate_projection_y_lambert_azimuthal_equal_area
774793
coord_system=engine.provides['coordinate_system'])
775794
python engine.rule_triggered.add(rule.name)
776795

796+
#
797+
# Context:
798+
# This rule will trigger iff a projection_x_coordinate coordinate exists and
799+
# a albers conical equal area coordinate system exists.
800+
#
801+
# Purpose:
802+
# Add the projection_x_coordinate coordinate into the cube.
803+
#
804+
fc_build_coordinate_projection_x_albers_equal_area
805+
foreach
806+
facts_cf.provides(coordinate, projection_x_coordinate, $coordinate)
807+
facts_cf.provides(coordinate_system, albers_equal_area)
808+
assert
809+
python cf_coord_var = engine.cf_var.cf_group.coordinates[$coordinate]
810+
python build_dimension_coordinate(engine, cf_coord_var,
811+
coord_name=CF_VALUE_STD_NAME_PROJ_X,
812+
coord_system=engine.provides['coordinate_system'])
813+
python engine.rule_triggered.add(rule.name)
814+
815+
816+
#
817+
# Context:
818+
# This rule will trigger iff a projection_y_coordinate coordinate exists and
819+
# a albers conical equal area coordinate system exists.
820+
#
821+
# Purpose:
822+
# Add the projection_y_coordinate coordinate into the cube.
823+
#
824+
fc_build_coordinate_projection_y_albers_equal_area
825+
foreach
826+
facts_cf.provides(coordinate, projection_y_coordinate, $coordinate)
827+
facts_cf.provides(coordinate_system, albers_equal_area)
828+
assert
829+
python cf_coord_var = engine.cf_var.cf_group.coordinates[$coordinate]
830+
python build_dimension_coordinate(engine, cf_coord_var,
831+
coord_name=CF_VALUE_STD_NAME_PROJ_Y,
832+
coord_system=engine.provides['coordinate_system'])
833+
python engine.rule_triggered.add(rule.name)
834+
777835
#
778836
# Context:
779837
# This rule will trigger iff a CF time coordinate exists.
@@ -1387,6 +1445,37 @@ fc_extras
13871445

13881446
return cs
13891447

1448+
################################################################################
1449+
def build_albers_equal_area_coordinate_system(engine, cf_grid_var):
1450+
"""
1451+
Create a albers conical equal area coordinate system from the CF-netCDF
1452+
grid mapping variable.
1453+
1454+
"""
1455+
major, minor, inverse_flattening = _get_ellipsoid(cf_grid_var)
1456+
1457+
latitude_of_projection_origin = getattr(
1458+
cf_grid_var, CF_ATTR_GRID_LAT_OF_PROJ_ORIGIN, None)
1459+
longitude_of_central_meridian = getattr(
1460+
cf_grid_var, CF_ATTR_GRID_LON_OF_CENT_MERIDIAN, None)
1461+
false_easting = getattr(
1462+
cf_grid_var, CF_ATTR_GRID_FALSE_EASTING, None)
1463+
false_northing = getattr(
1464+
cf_grid_var, CF_ATTR_GRID_FALSE_NORTHING, None)
1465+
standard_parallels = getattr(
1466+
cf_grid_var, CF_ATTR_GRID_STANDARD_PARALLEL, None)
1467+
1468+
ellipsoid = None
1469+
if major is not None or minor is not None or \
1470+
inverse_flattening is not None:
1471+
ellipsoid = iris.coord_systems.GeogCS(major, minor,
1472+
inverse_flattening)
1473+
1474+
cs = iris.coord_systems.AlbersEqualArea(
1475+
latitude_of_projection_origin, longitude_of_central_meridian,
1476+
false_easting, false_northing, standard_parallels, ellipsoid)
1477+
1478+
return cs
13901479

13911480
################################################################################
13921481
def get_attr_units(cf_var, attributes):

lib/iris/fileformats/netcdf.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1866,6 +1866,19 @@ def add_ellipsoid(ellipsoid):
18661866
cf_var_grid.false_easting = cs.false_easting
18671867
cf_var_grid.false_northing = cs.false_northing
18681868

1869+
# albers conical equal area
1870+
elif isinstance(cs,
1871+
iris.coord_systems.AlbersEqualArea):
1872+
if cs.ellipsoid:
1873+
add_ellipsoid(cs.ellipsoid)
1874+
cf_var_grid.longitude_of_central_meridian = (
1875+
cs.longitude_of_central_meridian)
1876+
cf_var_grid.latitude_of_projection_origin = (
1877+
cs.latitude_of_projection_origin)
1878+
cf_var_grid.false_easting = cs.false_easting
1879+
cf_var_grid.false_northing = cs.false_northing
1880+
cf_var_grid.standard_parallel = (cs.standard_parallels)
1881+
18691882
# other
18701883
else:
18711884
warnings.warn('Unable to represent the horizontal '
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
# (C) British Crown Copyright 2015 - 2018, Met Office
2+
#
3+
# This file is part of Iris.
4+
#
5+
# Iris is free software: you can redistribute it and/or modify it under
6+
# the terms of the GNU Lesser General Public License as published by the
7+
# Free Software Foundation, either version 3 of the License, or
8+
# (at your option) any later version.
9+
#
10+
# Iris is distributed in the hope that it will be useful,
11+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
# GNU Lesser General Public License for more details.
14+
#
15+
# You should have received a copy of the GNU Lesser General Public License
16+
# along with Iris. If not, see <http://www.gnu.org/licenses/>.
17+
"""
18+
Unit tests for the :class:`iris.coord_systems.AlbersEqualArea` class.
19+
20+
"""
21+
22+
from __future__ import (absolute_import, division, print_function)
23+
from six.moves import (filter, input, map, range, zip) # noqa
24+
25+
# Import iris.tests first so that some things can be initialised before
26+
# importing anything else.
27+
import iris.tests as tests
28+
29+
import cartopy.crs as ccrs
30+
from iris.coord_systems import GeogCS, AlbersEqualArea
31+
32+
33+
class Test_as_cartopy_crs(tests.IrisTest):
34+
def setUp(self):
35+
self.latitude_of_projection_origin = 0.0
36+
self.longitude_of_central_meridian = 0.0
37+
self.semi_major_axis = 6377563.396
38+
self.semi_minor_axis = 6356256.909
39+
self.false_easting = 0.0
40+
self.false_northing = 0.0
41+
self.standard_parallels = (-18., -36.)
42+
self.ellipsoid = GeogCS(self.semi_major_axis, self.semi_minor_axis)
43+
self.aea_cs = AlbersEqualArea(
44+
self.latitude_of_projection_origin,
45+
self.longitude_of_central_meridian,
46+
self.false_easting,
47+
self.false_northing,
48+
self.standard_parallels,
49+
ellipsoid=self.ellipsoid)
50+
51+
def test_crs_creation(self):
52+
res = self.aea_cs.as_cartopy_crs()
53+
globe = ccrs.Globe(semimajor_axis=self.semi_major_axis,
54+
semiminor_axis=self.semi_minor_axis,
55+
ellipse=None)
56+
expected = ccrs.AlbersEqualArea(
57+
self.longitude_of_central_meridian,
58+
self.latitude_of_projection_origin,
59+
self.false_easting,
60+
self.false_northing,
61+
self.standard_parallels,
62+
globe=globe)
63+
self.assertEqual(res, expected)
64+
65+
66+
class Test_as_cartopy_projection(tests.IrisTest):
67+
def setUp(self):
68+
self.latitude_of_projection_origin = 0.0
69+
self.longitude_of_central_meridian = 0.0
70+
self.semi_major_axis = 6377563.396
71+
self.semi_minor_axis = 6356256.909
72+
self.false_easting = 0.0
73+
self.false_northing = 0.0
74+
self.standard_parallels = (-18., -36.)
75+
self.ellipsoid = GeogCS(self.semi_major_axis, self.semi_minor_axis)
76+
self.aea_cs = AlbersEqualArea(
77+
self.latitude_of_projection_origin,
78+
self.longitude_of_central_meridian,
79+
self.false_easting,
80+
self.false_northing,
81+
self.standard_parallels,
82+
ellipsoid=self.ellipsoid)
83+
84+
def test_projection_creation(self):
85+
res = self.aea_cs.as_cartopy_projection()
86+
globe = ccrs.Globe(semimajor_axis=self.semi_major_axis,
87+
semiminor_axis=self.semi_minor_axis,
88+
ellipse=None)
89+
expected = ccrs.AlbersEqualArea(
90+
self.latitude_of_projection_origin,
91+
self.longitude_of_central_meridian,
92+
self.false_easting,
93+
self.false_northing,
94+
self.standard_parallels,
95+
globe=globe)
96+
self.assertEqual(res, expected)
97+
98+
99+
if __name__ == '__main__':
100+
tests.main()

lib/iris/tests/unit/fileformats/netcdf/test_Saver.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@
3434
from iris._lazy_data import as_lazy_data
3535
from iris.coord_systems import (GeogCS, TransverseMercator, RotatedGeogCS,
3636
LambertConformal, Mercator, Stereographic,
37-
LambertAzimuthalEqualArea)
37+
LambertAzimuthalEqualArea,
38+
AlbersEqualArea)
3839
from iris.coords import DimCoord
3940
from iris.cube import Cube
4041
from iris.fileformats.netcdf import Saver
@@ -785,6 +786,26 @@ def test_laea_cs(self):
785786
}
786787
self._test(coord_system, expected)
787788

789+
def test_aea_cs(self):
790+
coord_system = AlbersEqualArea(
791+
latitude_of_projection_origin=52,
792+
longitude_of_central_meridian=10,
793+
false_easting=100,
794+
false_northing=200,
795+
standard_parallels=(38, 50),
796+
ellipsoid=GeogCS(6377563.396, 6356256.909))
797+
expected = {'grid_mapping_name': b'albers_conical_equal_area',
798+
'latitude_of_projection_origin': 52,
799+
'longitude_of_central_meridian': 10,
800+
'false_easting': 100,
801+
'false_northing': 200,
802+
'standard_parallel': (38, 50),
803+
'semi_major_axis': 6377563.396,
804+
'semi_minor_axis': 6356256.909,
805+
'longitude_of_prime_meridian': 0,
806+
}
807+
self._test(coord_system, expected)
808+
788809

789810
class Test__create_cf_cell_measure_variable(tests.IrisTest):
790811
# Saving of masked data is disallowed.

0 commit comments

Comments
 (0)