From 901528b8854bf9a2b2ffd660966e507a342e0d7f Mon Sep 17 00:00:00 2001 From: Martin Yeo Date: Tue, 18 Oct 2022 17:35:23 +0100 Subject: [PATCH 01/10] ObliqueMercator class. --- lib/cartopy/crs.py | 65 ++++++++++++++++++++++++++++++++++++ lib/cartopy/mpl/gridliner.py | 1 + 2 files changed, 66 insertions(+) diff --git a/lib/cartopy/crs.py b/lib/cartopy/crs.py index 30c4170e9..720ea3b6b 100644 --- a/lib/cartopy/crs.py +++ b/lib/cartopy/crs.py @@ -3089,6 +3089,71 @@ def y_limits(self): return self._y_limits +class ObliqueMercator(Projection): + """ + An Oblique Mercator projection. + + """ + _wrappable = True + + def __init__(self, central_longitude=0.0, central_latitude=0.0, + false_easting=0.0, false_northing=0.0, + scale_factor=1.0, azimuth=0.0, globe=None): + """ + Parameters + ---------- + central_longitude: optional + The true longitude of the central meridian in degrees. + Defaults to 0. + central_latitude: optional + The true latitude of the planar origin in degrees. Defaults to 0. + false_easting: optional + X offset from the planar origin in metres. Defaults to 0. + false_northing: optional + Y offset from the planar origin in metres. Defaults to 0. + scale_factor: optional + Scale factor at the central meridian. Defaults to 1. + azimuth: optional + Azimuth of centerline clockwise from north at the center point of + the centre line. Defaults to 0. + + globe: optional + An instance of :class:`cartopy.crs.Globe`. If omitted, a default + globe is created. + + Notes + ----- + The 'Rotated Mercator' projection can be achieved using Oblique + Mercator with `azimuth` ``=90``. + + """ + + proj4_params = [('proj', 'omerc'), ('lonc', central_longitude), + ('lat_0', central_latitude), ('k', scale_factor), + ('x_0', false_easting), ('y_0', false_northing), + ('alpha', azimuth), ('units', 'm')] + + super().__init__(proj4_params, globe=globe) + + self.threshold = 1e4 + + @property + def boundary(self): + x0, x1 = self.x_limits + y0, y1 = self.y_limits + return sgeom.LinearRing([(x0, y0), (x0, y1), + (x1, y1), (x1, y0), + (x0, y0)]) + + @property + def x_limits(self): + return (-2e7, 2e7) + + @property + def y_limits(self): + return (-1e7, 1e7) + + class _BoundaryPoint: def __init__(self, distance, kind, data): """ diff --git a/lib/cartopy/mpl/gridliner.py b/lib/cartopy/mpl/gridliner.py index 58d993c92..9eddc9a37 100644 --- a/lib/cartopy/mpl/gridliner.py +++ b/lib/cartopy/mpl/gridliner.py @@ -45,6 +45,7 @@ cartopy.crs.LambertConformal, cartopy.crs.TransverseMercator, cartopy.crs.Gnomonic, + cartopy.crs.ObliqueMercator, ) From f9b99caf5f49b2cdaf70211522f878140b1e03bb Mon Sep 17 00:00:00 2001 From: Martin Yeo Date: Wed, 19 Oct 2022 17:13:09 +0100 Subject: [PATCH 02/10] Avoid ObliqueMercator coastline folding for 90 azimuth. --- lib/cartopy/crs.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/cartopy/crs.py b/lib/cartopy/crs.py index 720ea3b6b..86c5bef85 100644 --- a/lib/cartopy/crs.py +++ b/lib/cartopy/crs.py @@ -3128,6 +3128,10 @@ def __init__(self, central_longitude=0.0, central_latitude=0.0, """ + if np.isclose(azimuth, 90): + # Exactly 90 causes coastline 'folding'. + azimuth -= 1e-3 + proj4_params = [('proj', 'omerc'), ('lonc', central_longitude), ('lat_0', central_latitude), ('k', scale_factor), ('x_0', false_easting), ('y_0', false_northing), From 7092712e29f518d50fb8310cb265a719c640ba9c Mon Sep 17 00:00:00 2001 From: Martin Yeo Date: Thu, 20 Oct 2022 16:26:14 +0100 Subject: [PATCH 03/10] ObliqueMercator better x_limits and y_limits. --- lib/cartopy/crs.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/lib/cartopy/crs.py b/lib/cartopy/crs.py index 86c5bef85..dd84127aa 100644 --- a/lib/cartopy/crs.py +++ b/lib/cartopy/crs.py @@ -3132,6 +3132,8 @@ def __init__(self, central_longitude=0.0, central_latitude=0.0, # Exactly 90 causes coastline 'folding'. azimuth -= 1e-3 + self._x_offset = math.sin(math.radians(azimuth)) + proj4_params = [('proj', 'omerc'), ('lonc', central_longitude), ('lat_0', central_latitude), ('k', scale_factor), ('x_0', false_easting), ('y_0', false_northing), @@ -3151,11 +3153,14 @@ def boundary(self): @property def x_limits(self): - return (-2e7, 2e7) + x_lims = np.array([-2., 2.]) + x_lims += self._x_offset + x_lims *= 1e7 + return tuple(x_lims) @property def y_limits(self): - return (-1e7, 1e7) + return (-2e7, 2e7) class _BoundaryPoint: From 954741b4dd1c5aad123b02c46982f8dda950f92d Mon Sep 17 00:00:00 2001 From: Martin Yeo Date: Thu, 20 Oct 2022 16:33:46 +0100 Subject: [PATCH 04/10] ObliqueMercator reference in make_projection.py. --- docs/make_projection.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/make_projection.py b/docs/make_projection.py index 0a503f518..c1adad149 100644 --- a/docs/make_projection.py +++ b/docs/make_projection.py @@ -90,7 +90,8 @@ def utm_plot(): PRJ_SORT_ORDER = {'PlateCarree': 1, 'Mercator': 2, 'Mollweide': 2, 'Robinson': 2, - 'TransverseMercator': 2, 'LambertCylindrical': 2, + 'TransverseMercator': 2, 'ObliqueMercator': 2, + 'LambertCylindrical': 2, 'LambertConformal': 2, 'EquidistantConic': 2, 'Stereographic': 2, 'Miller': 2, 'Orthographic': 2, 'UTM': 2, 'AlbersEqualArea': 2, From ceeac492611c429b04908cf51fa76d2254149e3f Mon Sep 17 00:00:00 2001 From: Martin Yeo Date: Fri, 21 Oct 2022 15:35:01 +0100 Subject: [PATCH 05/10] Undo inflexible x_limit offsetting in ObliqueMercator. --- lib/cartopy/crs.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/lib/cartopy/crs.py b/lib/cartopy/crs.py index dd84127aa..0e114b0a4 100644 --- a/lib/cartopy/crs.py +++ b/lib/cartopy/crs.py @@ -3132,8 +3132,6 @@ def __init__(self, central_longitude=0.0, central_latitude=0.0, # Exactly 90 causes coastline 'folding'. azimuth -= 1e-3 - self._x_offset = math.sin(math.radians(azimuth)) - proj4_params = [('proj', 'omerc'), ('lonc', central_longitude), ('lat_0', central_latitude), ('k', scale_factor), ('x_0', false_easting), ('y_0', false_northing), @@ -3153,10 +3151,7 @@ def boundary(self): @property def x_limits(self): - x_lims = np.array([-2., 2.]) - x_lims += self._x_offset - x_lims *= 1e7 - return tuple(x_lims) + return (-2e7, 2e7) @property def y_limits(self): From 189ae5ab6a3bd34e74807eb38de86907b754b8b1 Mon Sep 17 00:00:00 2001 From: Martin Yeo Date: Fri, 3 Mar 2023 14:23:26 +0000 Subject: [PATCH 06/10] Couple ObliqueMercator limits to Mercator limits. --- lib/cartopy/crs.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/lib/cartopy/crs.py b/lib/cartopy/crs.py index 0e114b0a4..fbf073524 100644 --- a/lib/cartopy/crs.py +++ b/lib/cartopy/crs.py @@ -3139,7 +3139,18 @@ def __init__(self, central_longitude=0.0, central_latitude=0.0, super().__init__(proj4_params, globe=globe) - self.threshold = 1e4 + # Couple limits to those of Mercator - delivers acceptable plots, and + # Mercator has been through much more scrutiny. + mercator = Mercator( + central_longitude=central_longitude, + globe=globe, + false_easting=false_easting, + false_northing=false_northing, + scale_factor=scale_factor, + ) + self._x_limits = mercator.x_limits + self._y_limits = mercator.y_limits + self.threshold = mercator.threshold @property def boundary(self): @@ -3151,11 +3162,11 @@ def boundary(self): @property def x_limits(self): - return (-2e7, 2e7) + return self._x_limits @property def y_limits(self): - return (-2e7, 2e7) + return self._y_limits class _BoundaryPoint: From f2e3a5bc28bec1cefe2dcfdb73236ddcb3a8ef34 Mon Sep 17 00:00:00 2001 From: Martin Yeo Date: Fri, 21 Oct 2022 17:00:00 +0100 Subject: [PATCH 07/10] ObliqueMercator tests. --- .../tests/crs/test_oblique_mercator.py | 190 ++++++++++++++++++ ...est_global_map_ObliqueMercator_default.png | Bin 0 -> 12179 bytes ...est_global_map_ObliqueMercator_rotated.png | Bin 0 -> 13189 bytes lib/cartopy/tests/mpl/test_mpl_integration.py | 4 + 4 files changed, 194 insertions(+) create mode 100644 lib/cartopy/tests/crs/test_oblique_mercator.py create mode 100644 lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/test_global_map_ObliqueMercator_default.png create mode 100644 lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/test_global_map_ObliqueMercator_rotated.png diff --git a/lib/cartopy/tests/crs/test_oblique_mercator.py b/lib/cartopy/tests/crs/test_oblique_mercator.py new file mode 100644 index 000000000..36f8f3a0d --- /dev/null +++ b/lib/cartopy/tests/crs/test_oblique_mercator.py @@ -0,0 +1,190 @@ +# Copyright Cartopy Contributors +# +# This file is part of Cartopy and is released under the LGPL license. +# See COPYING and COPYING.LESSER in the root of the repository for full +# licensing details. +""" +Tests for the Oblique Mercator projection. + +""" + +from copy import deepcopy +from typing import Dict, List, NamedTuple, Tuple + +from matplotlib import pyplot as plt +import numpy as np +import pytest + +import cartopy.crs as ccrs +from .helpers import check_proj_params + + +@pytest.fixture +def oblique_mercator() -> ccrs.ObliqueMercator: + return ccrs.ObliqueMercator() + + +@pytest.fixture +def rotated_mercator() -> ccrs.ObliqueMercator: + return ccrs.ObliqueMercator(azimuth=90.0) + + +@pytest.fixture +def plate_carree() -> ccrs.PlateCarree: + return ccrs.PlateCarree() + + +class TestCrsArgs: + point_a_plate_carree = (-3.474083, 50.727301) + point_b_plate_carree = (0.5, 50.5) + + class ParamTuple(NamedTuple): + id: str + crs_kwargs: dict + proj_kwargs: Dict[str, str] + expected_a: Tuple[float, float] + expected_b: Tuple[float, float] + + param_list: List[ParamTuple] = [ + ParamTuple( + "default", + dict(), + dict(), + (-245106.75804, 5626768.52447), + (35451.51708, 5595849.69689), + ), + ParamTuple( + "azimuth", + dict(azimuth=90.0), + dict(alpha="89.999"), + (-386712.17018, 6540102.97351), + (55680.57266, 6500330.56121), + ), + ParamTuple( + "central_longitude", + dict(central_longitude=90.0), + dict(lonc="90.0"), + (-4739202.85619, 10329047.01897), + (-4786583.034, 9966930.01085), + ), + ParamTuple( + "central_latitude", + dict(central_latitude=45.0), + dict(lat_0="45.0"), + (-245269.04118, 642564.31415), + (35474.56405, 611638.73957), + ), + ParamTuple( + "false_easting_northing", + dict(false_easting=1000000, false_northing=-2000000), + dict(x_0="1000000", y_0="-2000000"), + (754893.24196, 3626768.52447), + (1035451.51708, 3595849.69689), + ), + ParamTuple( + "scale_factor", + # Number inherited from test_mercator.py . + dict(scale_factor=0.939692620786), + dict(k="0.939692620786"), + (-230325.01183, 5287432.86131), + (33313.52899, 5258378.6672), + ), + ParamTuple( + "globe", + dict(globe=ccrs.Globe(ellipse="sphere")), + dict(ellps="sphere"), + (-244502.86059, 5646357.44304), + (35364.23322, 5615460.21872), + ), + ParamTuple( + "combo", + dict( + azimuth=90.0, + central_longitude=90.0, + central_latitude=45.0, + false_easting=1000000, + false_northing=-2000000, + scale_factor=0.939692620786, + globe=ccrs.Globe(ellipse="sphere"), + ), + dict( + alpha="89.999", + lonc="90.0", + lat_0="45.0", + x_0="1000000", + y_0="-2000000", + k="0.939692620786", + ellps="sphere", + ), + (-4279982.08123, 1916861.68937), + (-4138080.80706, 1631302.04295), + ), + ] + param_ids: List[str] = [p.id for p in param_list] + + @pytest.fixture(autouse=True, params=param_list, ids=param_ids) + def make_variant_inputs(self, request) -> None: + inputs: TestCrsArgs.ParamTuple = request.param + + self.oblique_mercator = ccrs.ObliqueMercator(**inputs.crs_kwargs) + proj_kwargs_expected = dict( + ellps="WGS84", + lonc="0.0", + lat_0="0.0", + k="1.0", + x_0="0.0", + y_0="0.0", + alpha="0.0", + units="m", + ) + proj_kwargs_expected.update(inputs.proj_kwargs) + self.proj_params = {f"{k}={v}" for k, v in proj_kwargs_expected.items()} + + self.expected_a = inputs.expected_a + self.expected_b = inputs.expected_b + + def test_proj_params(self): + check_proj_params("omerc", self.oblique_mercator, self.proj_params) + + def test_transform_point(self, plate_carree): + # (Point equivalence has been confirmed via plotting). + src_expected = ( + (self.point_a_plate_carree, self.expected_a), + (self.point_b_plate_carree, self.expected_b), + ) + for src, expected in src_expected: + res = self.oblique_mercator.transform_point( + *src, + src_crs=plate_carree, + ) + np.testing.assert_array_almost_equal(res, expected, decimal=5) + + +@pytest.fixture +def oblique_variants( + oblique_mercator, + rotated_mercator, +) -> Tuple[ccrs.ObliqueMercator, ccrs.ObliqueMercator, ccrs.ObliqueMercator]: + """Setup three ObliqueMercator objects, two identical, for eq testing.""" + default = oblique_mercator + alt_1 = rotated_mercator + alt_2 = deepcopy(rotated_mercator) + return default, alt_1, alt_2 + + +def test_equality(oblique_variants): + """Check == and != operators of ccrs.ObliqueMercator.""" + default, alt_1, alt_2 = oblique_variants + assert alt_1 == alt_2 + assert alt_1 != default + assert hash(alt_1) != hash(default) + assert hash(alt_1) == hash(alt_2) + + +@pytest.mark.parametrize("reverse_coord", [False, True], ids=["xy_order", "yx_order"]) +def test_nan(oblique_mercator, plate_carree, reverse_coord): + coord = (0.0, np.NaN) + if reverse_coord: + coord = tuple(reversed(coord)) + res = oblique_mercator.transform_point(*coord, src_crs=plate_carree) + assert np.all(np.isnan(res)) diff --git a/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/test_global_map_ObliqueMercator_default.png b/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/test_global_map_ObliqueMercator_default.png new file mode 100644 index 0000000000000000000000000000000000000000..1153d3c99fab414c1d39648fcbcd0db5b2530605 GIT binary patch literal 12179 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 ToAAPdJVIVZMY>GVB=Elg&S<|^ literal 0 HcmV?d00001 diff --git a/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/test_global_map_ObliqueMercator_rotated.png b/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/test_global_map_ObliqueMercator_rotated.png new file mode 100644 index 0000000000000000000000000000000000000000..410b83efc812c2822406e25f07e7e585eb3ff8d7 GIT binary patch literal 13189 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 dmYyPD=Sn`D3Fu{|EO`kKh0R literal 0 HcmV?d00001 diff --git a/lib/cartopy/tests/mpl/test_mpl_integration.py b/lib/cartopy/tests/mpl/test_mpl_integration.py index ad8cc0cb6..2527cf679 100644 --- a/lib/cartopy/tests/mpl/test_mpl_integration.py +++ b/lib/cartopy/tests/mpl/test_mpl_integration.py @@ -188,6 +188,10 @@ def test_simple_global(): ccrs.SouthPolarStereo, pytest.param((ccrs.TransverseMercator, dict(approx=True)), id='TransverseMercator'), + pytest.param((ccrs.ObliqueMercator, dict(azimuth=0.)), + id='ObliqueMercator_default'), + pytest.param((ccrs.ObliqueMercator, dict(azimuth=90., central_latitude=-22)), + id='ObliqueMercator_rotated'), ]) @pytest.mark.mpl_image_compare( tolerance=0.97 if MPL_VERSION.release[:2] < (3, 5) else 0.5, From 528e2f1eade7237245db57094afd0bfe7826a068 Mon Sep 17 00:00:00 2001 From: Martin Yeo Date: Mon, 6 Mar 2023 16:24:07 +0000 Subject: [PATCH 08/10] Stickler fixes. --- lib/cartopy/tests/crs/test_oblique_mercator.py | 9 ++++++--- lib/cartopy/tests/mpl/test_mpl_integration.py | 11 +++++++---- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/lib/cartopy/tests/crs/test_oblique_mercator.py b/lib/cartopy/tests/crs/test_oblique_mercator.py index 36f8f3a0d..242f71e3f 100644 --- a/lib/cartopy/tests/crs/test_oblique_mercator.py +++ b/lib/cartopy/tests/crs/test_oblique_mercator.py @@ -11,7 +11,6 @@ from copy import deepcopy from typing import Dict, List, NamedTuple, Tuple -from matplotlib import pyplot as plt import numpy as np import pytest @@ -138,7 +137,9 @@ def make_variant_inputs(self, request) -> None: units="m", ) proj_kwargs_expected.update(inputs.proj_kwargs) - self.proj_params = {f"{k}={v}" for k, v in proj_kwargs_expected.items()} + self.proj_params = { + f"{k}={v}" for k, v in proj_kwargs_expected.items() + } self.expected_a = inputs.expected_a self.expected_b = inputs.expected_b @@ -181,7 +182,9 @@ def test_equality(oblique_variants): assert hash(alt_1) == hash(alt_2) -@pytest.mark.parametrize("reverse_coord", [False, True], ids=["xy_order", "yx_order"]) +@pytest.mark.parametrize( + "reverse_coord", [False, True], ids=["xy_order", "yx_order"] +) def test_nan(oblique_mercator, plate_carree, reverse_coord): coord = (0.0, np.NaN) if reverse_coord: diff --git a/lib/cartopy/tests/mpl/test_mpl_integration.py b/lib/cartopy/tests/mpl/test_mpl_integration.py index 2527cf679..35fe439fc 100644 --- a/lib/cartopy/tests/mpl/test_mpl_integration.py +++ b/lib/cartopy/tests/mpl/test_mpl_integration.py @@ -188,10 +188,13 @@ def test_simple_global(): ccrs.SouthPolarStereo, pytest.param((ccrs.TransverseMercator, dict(approx=True)), id='TransverseMercator'), - pytest.param((ccrs.ObliqueMercator, dict(azimuth=0.)), - id='ObliqueMercator_default'), - pytest.param((ccrs.ObliqueMercator, dict(azimuth=90., central_latitude=-22)), - id='ObliqueMercator_rotated'), + pytest.param( + (ccrs.ObliqueMercator, dict(azimuth=0.)), id='ObliqueMercator_default' + ), + pytest.param( + (ccrs.ObliqueMercator, dict(azimuth=90., central_latitude=-22)), + id='ObliqueMercator_rotated', + ), ]) @pytest.mark.mpl_image_compare( tolerance=0.97 if MPL_VERSION.release[:2] < (3, 5) else 0.5, From 765442f1246828dac21ae9e28cea2862d8a85841 Mon Sep 17 00:00:00 2001 From: Martin Yeo Date: Mon, 6 Mar 2023 16:30:16 +0000 Subject: [PATCH 09/10] Adjust test_oblique_mercator equality tolerance. --- lib/cartopy/tests/crs/test_oblique_mercator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/cartopy/tests/crs/test_oblique_mercator.py b/lib/cartopy/tests/crs/test_oblique_mercator.py index 242f71e3f..9323ca35a 100644 --- a/lib/cartopy/tests/crs/test_oblique_mercator.py +++ b/lib/cartopy/tests/crs/test_oblique_mercator.py @@ -158,7 +158,7 @@ def test_transform_point(self, plate_carree): *src, src_crs=plate_carree, ) - np.testing.assert_array_almost_equal(res, expected, decimal=5) + np.testing.assert_array_almost_equal(res, expected, decimal=4) @pytest.fixture From 7ec343b9dade3571a8ce90d482cbf3d533e2aec9 Mon Sep 17 00:00:00 2001 From: Martin Yeo Date: Fri, 10 Mar 2023 16:44:13 +0000 Subject: [PATCH 10/10] Review actions. --- lib/cartopy/crs.py | 1 - .../tests/crs/test_oblique_mercator.py | 20 ++++++++++--------- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/lib/cartopy/crs.py b/lib/cartopy/crs.py index fbf073524..fcbdd6c5c 100644 --- a/lib/cartopy/crs.py +++ b/lib/cartopy/crs.py @@ -3116,7 +3116,6 @@ def __init__(self, central_longitude=0.0, central_latitude=0.0, azimuth: optional Azimuth of centerline clockwise from north at the center point of the centre line. Defaults to 0. - globe: optional An instance of :class:`cartopy.crs.Globe`. If omitted, a default globe is created. diff --git a/lib/cartopy/tests/crs/test_oblique_mercator.py b/lib/cartopy/tests/crs/test_oblique_mercator.py index 9323ca35a..9ee670619 100644 --- a/lib/cartopy/tests/crs/test_oblique_mercator.py +++ b/lib/cartopy/tests/crs/test_oblique_mercator.py @@ -36,6 +36,16 @@ def plate_carree() -> ccrs.PlateCarree: class TestCrsArgs: point_a_plate_carree = (-3.474083, 50.727301) point_b_plate_carree = (0.5, 50.5) + proj_kwargs_default = dict( + ellps="WGS84", + lonc="0.0", + lat_0="0.0", + k="1.0", + x_0="0.0", + y_0="0.0", + alpha="0.0", + units="m", + ) class ParamTuple(NamedTuple): id: str @@ -127,16 +137,8 @@ def make_variant_inputs(self, request) -> None: self.oblique_mercator = ccrs.ObliqueMercator(**inputs.crs_kwargs) proj_kwargs_expected = dict( - ellps="WGS84", - lonc="0.0", - lat_0="0.0", - k="1.0", - x_0="0.0", - y_0="0.0", - alpha="0.0", - units="m", + self.proj_kwargs_default, **inputs.proj_kwargs ) - proj_kwargs_expected.update(inputs.proj_kwargs) self.proj_params = { f"{k}={v}" for k, v in proj_kwargs_expected.items() }