Skip to content

Commit f30db0d

Browse files
authored
Oblique and Rotated Mercator (#5548)
* Introduce new coord system classes. * Add loading code for oblique mercator. * Fix for azimuth check. * Add saving code for oblique mercator. * Fix to rotated repr. * Scale factor wording fix. * Tests first pass. * Temp test disable. * Temp RotatedMercator test disable. * Deprecate RotatedMercator. * Revert "Temp RotatedMercator test disable." This reverts commit 27c486f. * First attempted fix for RM test inheritance. * Revert "Temp test disable." This reverts commit a81507c. * Fix warnings doctests. * Add deprecation test for RotatedMercator. * Oblique Mercator loading tests. * Oblique Mercator loading deprecation test. * Saving test for Oblique Mercator. * Fix isinstance() check. * What's New entry. * Temp test disable. * More temp test disabling. * WIP testing. * WIP testing. * Revert "More temp test disabling." This reverts commit ff251b7. * Revert "Temp test disable." This reverts commit 77eba55. * Use RotatedMercator inheritance for isinstance() check. * Check grid_mapping_name instead of using isinstance(). * Better type hinting. * Use return over yield in a fixture. * Duck typing comment. * Better grid_mapping_name checking. * Better structure for test parameterisation.
1 parent 0b50a2c commit f30db0d

File tree

10 files changed

+734
-6
lines changed

10 files changed

+734
-6
lines changed

docs/src/further_topics/filtering_warnings.rst

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,9 @@ Warnings:
4747

4848
>>> my_operation()
4949
...
50-
iris/coord_systems.py:454: IrisUserWarning: Setting inverse_flattening does not affect other properties of the GeogCS object. To change other properties set them explicitly or create a new GeogCS instance.
50+
iris/coord_systems.py:456: IrisUserWarning: Setting inverse_flattening does not affect other properties of the GeogCS object. To change other properties set them explicitly or create a new GeogCS instance.
5151
warnings.warn(wmsg, category=iris.exceptions.IrisUserWarning)
52-
iris/coord_systems.py:821: IrisDefaultingWarning: Discarding false_easting and false_northing that are not used by Cartopy.
52+
iris/coord_systems.py:823: IrisDefaultingWarning: Discarding false_easting and false_northing that are not used by Cartopy.
5353
warnings.warn(
5454

5555
Warnings can be suppressed using the Python warnings filter with the ``ignore``
@@ -110,7 +110,7 @@ You can target specific Warning messages, e.g.
110110
... warnings.filterwarnings("ignore", message="Discarding false_easting")
111111
... my_operation()
112112
...
113-
iris/coord_systems.py:454: IrisUserWarning: Setting inverse_flattening does not affect other properties of the GeogCS object. To change other properties set them explicitly or create a new GeogCS instance.
113+
iris/coord_systems.py:456: IrisUserWarning: Setting inverse_flattening does not affect other properties of the GeogCS object. To change other properties set them explicitly or create a new GeogCS instance.
114114
warnings.warn(wmsg, category=iris.exceptions.IrisUserWarning)
115115

116116
::
@@ -125,10 +125,10 @@ Or you can target Warnings raised by specific lines of specific modules, e.g.
125125
.. doctest:: filtering_warnings
126126

127127
>>> with warnings.catch_warnings():
128-
... warnings.filterwarnings("ignore", module="iris.coord_systems", lineno=454)
128+
... warnings.filterwarnings("ignore", module="iris.coord_systems", lineno=456)
129129
... my_operation()
130130
...
131-
iris/coord_systems.py:821: IrisDefaultingWarning: Discarding false_easting and false_northing that are not used by Cartopy.
131+
iris/coord_systems.py:823: IrisDefaultingWarning: Discarding false_easting and false_northing that are not used by Cartopy.
132132
warnings.warn(
133133

134134
::
@@ -188,7 +188,7 @@ module during execution:
188188
... )
189189
... my_operation()
190190
...
191-
iris/coord_systems.py:454: IrisUserWarning: Setting inverse_flattening does not affect other properties of the GeogCS object. To change other properties set them explicitly or create a new GeogCS instance.
191+
iris/coord_systems.py:456: IrisUserWarning: Setting inverse_flattening does not affect other properties of the GeogCS object. To change other properties set them explicitly or create a new GeogCS instance.
192192
warnings.warn(wmsg, category=iris.exceptions.IrisUserWarning)
193193

194194
----

docs/src/whatsnew/latest.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,10 @@ This document explains the changes made to Iris for this release
3434
:class:`UserWarning`\s for richer filtering. The full index of
3535
sub-categories can be seen here: :mod:`iris.exceptions` . (:pull:`5498`)
3636

37+
#. `@trexfeathers`_ added the :class:`~iris.coord_systems.ObliqueMercator`
38+
and :class:`~iris.coord_systems.RotatedMercator` coordinate systems,
39+
complete with NetCDF loading and saving. (:pull:`5548`)
40+
3741

3842
🐛 Bugs Fixed
3943
=============

lib/iris/coord_systems.py

Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,13 @@
1010

1111
from abc import ABCMeta, abstractmethod
1212
from functools import cached_property
13+
import re
1314
import warnings
1415

1516
import cartopy.crs as ccrs
1617
import numpy as np
1718

19+
from iris._deprecation import warn_deprecated
1820
import iris.exceptions
1921

2022

@@ -1634,3 +1636,197 @@ def as_cartopy_crs(self):
16341636

16351637
def as_cartopy_projection(self):
16361638
return self.as_cartopy_crs()
1639+
1640+
1641+
class ObliqueMercator(CoordSystem):
1642+
"""
1643+
A cylindrical map projection, with XY coordinates measured in metres.
1644+
1645+
Designed for regions not well suited to :class:`Mercator` or
1646+
:class:`TransverseMercator`, as the positioning of the cylinder is more
1647+
customisable.
1648+
1649+
See Also
1650+
--------
1651+
:class:`RotatedMercator`
1652+
1653+
"""
1654+
1655+
grid_mapping_name = "oblique_mercator"
1656+
1657+
def __init__(
1658+
self,
1659+
azimuth_of_central_line,
1660+
latitude_of_projection_origin,
1661+
longitude_of_projection_origin,
1662+
false_easting=None,
1663+
false_northing=None,
1664+
scale_factor_at_projection_origin=None,
1665+
ellipsoid=None,
1666+
):
1667+
"""
1668+
Constructs an ObliqueMercator object.
1669+
1670+
Parameters
1671+
----------
1672+
azimuth_of_central_line : float
1673+
Azimuth of centerline clockwise from north at the center point of
1674+
the centre line.
1675+
latitude_of_projection_origin : float
1676+
The true longitude of the central meridian in degrees.
1677+
longitude_of_projection_origin: float
1678+
The true latitude of the planar origin in degrees.
1679+
false_easting: float, optional
1680+
X offset from the planar origin in metres.
1681+
Defaults to 0.0 .
1682+
false_northing: float, optional
1683+
Y offset from the planar origin in metres.
1684+
Defaults to 0.0 .
1685+
scale_factor_at_projection_origin: float, optional
1686+
Scale factor at the central meridian.
1687+
Defaults to 1.0 .
1688+
ellipsoid: :class:`GeogCS`, optional
1689+
If given, defines the ellipsoid.
1690+
1691+
Examples
1692+
--------
1693+
>>> from iris.coord_systems import GeogCS, ObliqueMercator
1694+
>>> my_ellipsoid = GeogCS(6371229.0, None, 0.0)
1695+
>>> ObliqueMercator(90.0, -22.0, -59.0, -25000.0, -25000.0, 1., my_ellipsoid)
1696+
ObliqueMercator(azimuth_of_central_line=90.0, latitude_of_projection_origin=-22.0, longitude_of_projection_origin=-59.0, false_easting=-25000.0, false_northing=-25000.0, scale_factor_at_projection_origin=1.0, ellipsoid=GeogCS(6371229.0))
1697+
1698+
"""
1699+
#: Azimuth of centerline clockwise from north.
1700+
self.azimuth_of_central_line = float(azimuth_of_central_line)
1701+
1702+
#: True latitude of planar origin in degrees.
1703+
self.latitude_of_projection_origin = float(
1704+
latitude_of_projection_origin
1705+
)
1706+
1707+
#: True longitude of planar origin in degrees.
1708+
self.longitude_of_projection_origin = float(
1709+
longitude_of_projection_origin
1710+
)
1711+
1712+
#: X offset from planar origin in metres.
1713+
self.false_easting = _arg_default(false_easting, 0)
1714+
1715+
#: Y offset from planar origin in metres.
1716+
self.false_northing = _arg_default(false_northing, 0)
1717+
1718+
#: Scale factor at the central meridian.
1719+
self.scale_factor_at_projection_origin = _arg_default(
1720+
scale_factor_at_projection_origin, 1.0
1721+
)
1722+
1723+
#: Ellipsoid definition (:class:`GeogCS` or None).
1724+
self.ellipsoid = ellipsoid
1725+
1726+
def __repr__(self):
1727+
return (
1728+
"{!s}(azimuth_of_central_line={!r}, "
1729+
"latitude_of_projection_origin={!r}, "
1730+
"longitude_of_projection_origin={!r}, false_easting={!r}, "
1731+
"false_northing={!r}, scale_factor_at_projection_origin={!r}, "
1732+
"ellipsoid={!r})".format(
1733+
self.__class__.__name__,
1734+
self.azimuth_of_central_line,
1735+
self.latitude_of_projection_origin,
1736+
self.longitude_of_projection_origin,
1737+
self.false_easting,
1738+
self.false_northing,
1739+
self.scale_factor_at_projection_origin,
1740+
self.ellipsoid,
1741+
)
1742+
)
1743+
1744+
def as_cartopy_crs(self):
1745+
globe = self._ellipsoid_to_globe(self.ellipsoid, None)
1746+
1747+
return ccrs.ObliqueMercator(
1748+
central_longitude=self.longitude_of_projection_origin,
1749+
central_latitude=self.latitude_of_projection_origin,
1750+
false_easting=self.false_easting,
1751+
false_northing=self.false_northing,
1752+
scale_factor=self.scale_factor_at_projection_origin,
1753+
azimuth=self.azimuth_of_central_line,
1754+
globe=globe,
1755+
)
1756+
1757+
def as_cartopy_projection(self):
1758+
return self.as_cartopy_crs()
1759+
1760+
1761+
class RotatedMercator(ObliqueMercator):
1762+
"""
1763+
:class:`ObliqueMercator` with ``azimuth_of_central_line=90``.
1764+
1765+
As noted in CF versions 1.10 and earlier:
1766+
1767+
The Rotated Mercator projection is an Oblique Mercator projection
1768+
with azimuth = +90.
1769+
1770+
.. deprecated:: 3.8.0
1771+
This coordinate system was introduced as already scheduled for removal
1772+
in a future release, since CF version 1.11 onwards now requires use of
1773+
:class:`ObliqueMercator` with ``azimuth_of_central_line=90.`` .
1774+
Any :class:`RotatedMercator` instances will always be saved to NetCDF
1775+
as the ``oblique_mercator`` grid mapping.
1776+
1777+
"""
1778+
1779+
def __init__(
1780+
self,
1781+
latitude_of_projection_origin,
1782+
longitude_of_projection_origin,
1783+
false_easting=None,
1784+
false_northing=None,
1785+
scale_factor_at_projection_origin=None,
1786+
ellipsoid=None,
1787+
):
1788+
"""
1789+
Constructs a RotatedMercator object.
1790+
1791+
Parameters
1792+
----------
1793+
latitude_of_projection_origin : float
1794+
The true longitude of the central meridian in degrees.
1795+
longitude_of_projection_origin: float
1796+
The true latitude of the planar origin in degrees.
1797+
false_easting: float, optional
1798+
X offset from the planar origin in metres.
1799+
Defaults to 0.0 .
1800+
false_northing: float, optional
1801+
Y offset from the planar origin in metres.
1802+
Defaults to 0.0 .
1803+
scale_factor_at_projection_origin: float, optional
1804+
Scale factor at the central meridian.
1805+
Defaults to 1.0 .
1806+
ellipsoid: :class:`GeogCS`, optional
1807+
If given, defines the ellipsoid.
1808+
1809+
"""
1810+
message = (
1811+
"iris.coord_systems.RotatedMercator is deprecated, and will be "
1812+
"removed in a future release. Instead please use "
1813+
"iris.coord_systems.ObliqueMercator with "
1814+
"azimuth_of_central_line=90 ."
1815+
)
1816+
warn_deprecated(message)
1817+
1818+
super().__init__(
1819+
90.0,
1820+
latitude_of_projection_origin,
1821+
longitude_of_projection_origin,
1822+
false_easting,
1823+
false_northing,
1824+
scale_factor_at_projection_origin,
1825+
ellipsoid,
1826+
)
1827+
1828+
def __repr__(self):
1829+
# Remove the azimuth argument from the parent repr.
1830+
result = super().__repr__()
1831+
result = re.sub(r"azimuth_of_central_line=\d*\.?\d*, ", "", result)
1832+
return result

lib/iris/fileformats/_nc_load_rules/actions.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,14 @@ def action_default(engine):
156156
None,
157157
hh.build_geostationary_coordinate_system,
158158
),
159+
hh.CF_GRID_MAPPING_OBLIQUE: (
160+
None,
161+
hh.build_oblique_mercator_coordinate_system,
162+
),
163+
hh.CF_GRID_MAPPING_ROTATED_MERCATOR: (
164+
None,
165+
hh.build_oblique_mercator_coordinate_system,
166+
),
159167
}
160168

161169

lib/iris/fileformats/_nc_load_rules/helpers.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import pyproj
2424

2525
import iris
26+
from iris._deprecation import warn_deprecated
2627
import iris.aux_factory
2728
from iris.common.mixin import _get_valid_standard_name
2829
import iris.coord_systems
@@ -124,6 +125,8 @@
124125
CF_GRID_MAPPING_TRANSVERSE = "transverse_mercator"
125126
CF_GRID_MAPPING_VERTICAL = "vertical_perspective"
126127
CF_GRID_MAPPING_GEOSTATIONARY = "geostationary"
128+
CF_GRID_MAPPING_OBLIQUE = "oblique_mercator"
129+
CF_GRID_MAPPING_ROTATED_MERCATOR = "rotated_mercator"
127130

128131
#
129132
# CF Attribute Names.
@@ -154,6 +157,7 @@
154157
CF_ATTR_GRID_STANDARD_PARALLEL = "standard_parallel"
155158
CF_ATTR_GRID_PERSPECTIVE_HEIGHT = "perspective_point_height"
156159
CF_ATTR_GRID_SWEEP_ANGLE_AXIS = "sweep_angle_axis"
160+
CF_ATTR_GRID_AZIMUTH_CENT_LINE = "azimuth_of_central_line"
157161
CF_ATTR_POSITIVE = "positive"
158162
CF_ATTR_STD_NAME = "standard_name"
159163
CF_ATTR_LONG_NAME = "long_name"
@@ -893,6 +897,58 @@ def build_geostationary_coordinate_system(engine, cf_grid_var):
893897
return cs
894898

895899

900+
################################################################################
901+
def build_oblique_mercator_coordinate_system(engine, cf_grid_var):
902+
"""
903+
Create an oblique mercator coordinate system from the CF-netCDF
904+
grid mapping variable.
905+
906+
"""
907+
ellipsoid = _get_ellipsoid(cf_grid_var)
908+
909+
azimuth_of_central_line = getattr(
910+
cf_grid_var, CF_ATTR_GRID_AZIMUTH_CENT_LINE, None
911+
)
912+
latitude_of_projection_origin = getattr(
913+
cf_grid_var, CF_ATTR_GRID_LAT_OF_PROJ_ORIGIN, None
914+
)
915+
longitude_of_projection_origin = getattr(
916+
cf_grid_var, CF_ATTR_GRID_LON_OF_PROJ_ORIGIN, None
917+
)
918+
scale_factor_at_projection_origin = getattr(
919+
cf_grid_var, CF_ATTR_GRID_SCALE_FACTOR_AT_PROJ_ORIGIN, None
920+
)
921+
false_easting = getattr(cf_grid_var, CF_ATTR_GRID_FALSE_EASTING, None)
922+
false_northing = getattr(cf_grid_var, CF_ATTR_GRID_FALSE_NORTHING, None)
923+
kwargs = dict(
924+
azimuth_of_central_line=azimuth_of_central_line,
925+
latitude_of_projection_origin=latitude_of_projection_origin,
926+
longitude_of_projection_origin=longitude_of_projection_origin,
927+
scale_factor_at_projection_origin=scale_factor_at_projection_origin,
928+
false_easting=false_easting,
929+
false_northing=false_northing,
930+
ellipsoid=ellipsoid,
931+
)
932+
933+
# Handle the alternative form noted in CF: rotated mercator.
934+
grid_mapping_name = getattr(cf_grid_var, CF_ATTR_GRID_MAPPING_NAME)
935+
candidate_systems = dict(
936+
oblique_mercator=iris.coord_systems.ObliqueMercator,
937+
rotated_mercator=iris.coord_systems.RotatedMercator,
938+
)
939+
if grid_mapping_name == "rotated_mercator":
940+
message = (
941+
"Iris will stop loading the rotated_mercator grid mapping name in "
942+
"a future release, in accordance with CF version 1.11 . Instead "
943+
"please use oblique_mercator with azimuth_of_central_line = 90 ."
944+
)
945+
warn_deprecated(message)
946+
del kwargs[CF_ATTR_GRID_AZIMUTH_CENT_LINE]
947+
948+
cs = candidate_systems[grid_mapping_name](**kwargs)
949+
return cs
950+
951+
896952
################################################################################
897953
def get_attr_units(cf_var, attributes):
898954
attr_units = getattr(cf_var, CF_ATTR_UNITS, UNKNOWN_UNIT_STRING)

0 commit comments

Comments
 (0)