From 634a9e762e4dd66ab61fd05119305cc7a37e5bb5 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Tue, 13 Nov 2018 03:41:39 -0500 Subject: [PATCH] Add Equal Earth projection. --- docs/source/crs/projections.rst | 16 +++ lib/cartopy/crs.py | 48 +++++++++ lib/cartopy/tests/crs/test_equal_earth.py | 92 ++++++++++++++++++ .../multiple_projections520.png | Bin 0 -> 10648 bytes lib/cartopy/tests/mpl/test_mpl_integration.py | 23 +++++ 5 files changed, 179 insertions(+) create mode 100644 lib/cartopy/tests/crs/test_equal_earth.py create mode 100644 lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/multiple_projections520.png diff --git a/docs/source/crs/projections.rst b/docs/source/crs/projections.rst index ed75d663e1..0fc32999a1 100644 --- a/docs/source/crs/projections.rst +++ b/docs/source/crs/projections.rst @@ -460,6 +460,22 @@ EckertVI ax.gridlines() +EqualEarth +---------- + +.. autoclass:: cartopy.crs.EqualEarth + +.. plot:: + + import matplotlib.pyplot as plt + import cartopy.crs as ccrs + + plt.figure(figsize=(6.1637, 3)) + ax = plt.axes(projection=ccrs.EqualEarth()) + ax.coastlines(resolution='110m') + ax.gridlines() + + Gnomonic -------- diff --git a/lib/cartopy/crs.py b/lib/cartopy/crs.py index 69afe02d84..7ff9d8b38e 100644 --- a/lib/cartopy/crs.py +++ b/lib/cartopy/crs.py @@ -1689,6 +1689,54 @@ class EckertVI(_Eckert): _proj_name = 'eck6' +class EqualEarth(_WarpedRectangularProjection): + """ + An Equal Earth projection. + + This projection is pseudocylindrical, and equal area. Parallels are + unequally-spaced straight lines, while meridians are equally-spaced arcs. + + It is intended for world maps. + + References + ---------- + Bojan Šavrič, Tom Patterson & Bernhard Jenny (2018) The Equal Earth map + projection, International Journal of Geographical Information Science, + DOI: 10.1080/13658816.2018.1504949 + + """ + + def __init__(self, central_longitude=0, false_easting=None, + false_northing=None, globe=None): + """ + Parameters + ---------- + central_longitude: float, optional + The central longitude. Defaults to 0. + false_easting: float, optional + X offset from planar origin in metres. Defaults to 0. + false_northing: float, optional + Y offset from planar origin in metres. Defaults to 0. + globe: :class:`cartopy.crs.Globe`, optional + If omitted, a default globe is created. + + """ + if PROJ4_VERSION < (5, 2, 0): + raise ValueError('The EqualEarth projection requires Proj version ' + '5.2.0, but you are using {}.' + .format('.'.join(PROJ4_VERSION))) + + proj_params = [('proj', 'eqearth'), ('lon_0', central_longitude)] + super(EqualEarth, self).__init__(proj_params, central_longitude, + false_easting=false_easting, + false_northing=false_northing, + globe=globe) + + @property + def threshold(self): + return 1e5 + + class Mollweide(_WarpedRectangularProjection): """ A Mollweide projection. diff --git a/lib/cartopy/tests/crs/test_equal_earth.py b/lib/cartopy/tests/crs/test_equal_earth.py new file mode 100644 index 0000000000..8c2e7f9126 --- /dev/null +++ b/lib/cartopy/tests/crs/test_equal_earth.py @@ -0,0 +1,92 @@ +# (C) British Crown Copyright 2018, Met Office +# +# This file is part of cartopy. +# +# cartopy is free software: you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License as published by the +# Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# cartopy is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with cartopy. If not, see . +""" +Tests for the Equal Earth coordinate system. + +""" + +from __future__ import (absolute_import, division, print_function) + +import numpy as np +from numpy.testing import assert_almost_equal +import pytest + +import cartopy.crs as ccrs + + +pytestmark = pytest.mark.skipif(ccrs.PROJ4_VERSION < (5, 2, 0), + reason='Proj is too old.') + + +def check_proj_params(crs, other_args): + expected = other_args | {'proj=eqearth', 'no_defs'} + proj_params = set(crs.proj4_init.lstrip('+').split(' +')) + assert expected == proj_params + + +def test_default(): + eqearth = ccrs.EqualEarth() + other_args = {'ellps=WGS84', 'lon_0=0'} + check_proj_params(eqearth, other_args) + + assert_almost_equal(eqearth.x_limits, + [-17243959.0622169, 17243959.0622169]) + assert_almost_equal(eqearth.y_limits, + [-8392927.59846646, 8392927.59846646]) + # Expected aspect ratio from the paper. + assert_almost_equal(np.diff(eqearth.x_limits) / np.diff(eqearth.y_limits), + 2.05458, decimal=5) + + +def test_offset(): + crs = ccrs.EqualEarth() + crs_offset = ccrs.EqualEarth(false_easting=1234, false_northing=-4321) + other_args = {'ellps=WGS84', 'lon_0=0', 'x_0=1234', 'y_0=-4321'} + check_proj_params(crs_offset, other_args) + assert tuple(np.array(crs.x_limits) + 1234) == crs_offset.x_limits + assert tuple(np.array(crs.y_limits) - 4321) == crs_offset.y_limits + + +def test_eccentric_globe(): + globe = ccrs.Globe(semimajor_axis=1000, semiminor_axis=500, + ellipse=None) + eqearth = ccrs.EqualEarth(globe=globe) + other_args = {'a=1000', 'b=500', 'lon_0=0'} + check_proj_params(eqearth, other_args) + + assert_almost_equal(eqearth.x_limits, + [-2248.43664092550, 2248.43664092550]) + assert_almost_equal(eqearth.y_limits, + [-1094.35228122148, 1094.35228122148]) + # Expected aspect ratio from the paper. + assert_almost_equal(np.diff(eqearth.x_limits) / np.diff(eqearth.y_limits), + 2.05458, decimal=5) + + +@pytest.mark.parametrize('lon', [-10.0, 10.0]) +def test_central_longitude(lon): + eqearth = ccrs.EqualEarth(central_longitude=lon) + other_args = {'ellps=WGS84', 'lon_0={}'.format(lon)} + check_proj_params(eqearth, other_args) + + assert_almost_equal(eqearth.x_limits, + [-17243959.0622169, 17243959.0622169], decimal=5) + assert_almost_equal(eqearth.y_limits, + [-8392927.59846646, 8392927.59846646]) + # Expected aspect ratio from the paper. + assert_almost_equal(np.diff(eqearth.x_limits) / np.diff(eqearth.y_limits), + 2.05458, decimal=5) diff --git a/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/multiple_projections520.png b/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/multiple_projections520.png new file mode 100644 index 0000000000000000000000000000000000000000..b6774e0307dc847267f6f99bfc76090f149e6dd7 GIT binary patch literal 10648 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