diff -Nru python-cartopy-0.17.0+dfsg/.appveyor.yml python-cartopy-0.18.0+dfsg/.appveyor.yml --- python-cartopy-0.17.0+dfsg/.appveyor.yml 1970-01-01 00:00:00.000000000 +0000 +++ python-cartopy-0.18.0+dfsg/.appveyor.yml 2020-05-03 08:12:47.000000000 +0000 @@ -0,0 +1,48 @@ +environment: + matrix: + - PYTHON_VERSION: "3.6" + CONDA_INSTALL_LOCN: "C:\\Miniconda36-x64" + PACKAGES: "cython numpy matplotlib-base proj pykdtree scipy" + - PYTHON_VERSION: "2.7" + CONDA_INSTALL_LOCN: "C:\\Miniconda-x64" + PACKAGES: "cython=0.28 numpy=1.10.0 matplotlib=1.5.1 nose proj4=4.9.1 scipy=0.16.0 mock msinttypes futures" + +install: + # Install miniconda + # ----------------- + - set PATH=%CONDA_INSTALL_LOCN%;%CONDA_INSTALL_LOCN%\scripts;%PATH%; + + # Create the testing environment + # ------------------------------ + - conda config --set always_yes yes --set changeps1 no --set show_channel_urls yes + - conda config --add channels conda-forge + - conda config --add channels conda-forge/label/testing + - if %PYTHON_VERSION%==2.7 conda config --set restore_free_channel true + - if %PYTHON_VERSION%==3.6 conda update conda --yes + - set ENV_NAME=test-environment + - set PACKAGES=%PACKAGES% pillow pytest filelock pep8 pyshp shapely six requests pyepsg owslib + - conda create -n %ENV_NAME% python=%PYTHON_VERSION% %PACKAGES% + - activate %ENV_NAME% + - set INCLUDE=%CONDA_PREFIX%\Library\include;%INCLUDE% + - set LIB=%CONDA_PREFIX%\Library\lib;%LIB% + + # Conda debug + # ----------- + - conda list -n %ENV_NAME% + - conda list -n %ENV_NAME% --explicit + - conda info -a + +build_script: + # Install cartopy + # --------------- + - pip install --no-deps . + - python -c "import cartopy; print('Version ', cartopy.__version__)" + +test_script: + - set MPLBACKEND=Agg + - pytest --pyargs cartopy + +artifacts: + - path: cartopy_test_output + name: cartopy_test_output + type: zip diff -Nru python-cartopy-0.17.0+dfsg/benchmarks/asv.conf.json python-cartopy-0.18.0+dfsg/benchmarks/asv.conf.json --- python-cartopy-0.17.0+dfsg/benchmarks/asv.conf.json 1970-01-01 00:00:00.000000000 +0000 +++ python-cartopy-0.18.0+dfsg/benchmarks/asv.conf.json 2020-05-03 08:12:47.000000000 +0000 @@ -0,0 +1,70 @@ +{ + // The version of the config file format. Do not change, unless + // you know what you are doing. + "version": 1, + + // The name of the project being benchmarked + "project": "cartopy", + + // The project's homepage + "project_url": "https://github.com/scitools/cartopy", + + // The URL or local path of the source code repository for the + // project being benchmarked + "repo": "../", + + // List of branches to benchmark. If not provided, defaults to "master" + // (for git) or "default" (for mercurial). + // "branches": ["master"], // for git + + // The tool to use to create environments. May be "conda", + // "virtualenv" or other value depending on the plugins in use. + // If missing or the empty string, the tool will be automatically + // determined by looking for tools on the PATH environment + // variable. + "environment_type": "conda", + + // the base URL to show a commit for the project. + // "show_commit_url": "http://github.com/owner/project/commit/", + + // The Pythons you'd like to test against. If not provided, defaults + // to the current version of Python used to run `asv`. + // "pythons": ["2.7", "3.6"], + + // The list of conda channel names to be searched for benchmark + // dependency packages in the specified order + //"conda_channels": ["conda-forge", "defaults"], + + // The matrix of dependencies to test. Each key is the name of a + // package (in PyPI) and the values are version numbers. An empty + // list or empty string indicates to just test against the default + // (latest) version. null indicates that the package is to not be + // installed. If the package to be tested is only available from + // PyPi, and the 'environment_type' is conda, then you can preface + // the package name by 'pip+', and the package will be installed via + // pip (with all the conda available packages installed first, + // followed by the pip installed packages). + // + "matrix": { + "numpy": [""], + "cython": [""], + "matplotlib": [""], + "proj4": [""], + "pykdtree": [""], + "scipy": [""], + "fiona": [""] + }, + + // The directory (relative to the current directory) that benchmarks are + // stored in. If not provided, defaults to "benchmarks" + "benchmark_dir": "cases", + "env_dir": "envs", + + // The number of characters to retain in the commit hashes. + "hash_length": 12 + + // `asv` will cache results of the recent builds in each + // environment, making them faster to install next time. This is + // the number of builds to keep, per environment. + // "build_cache_size": 2, +} diff -Nru python-cartopy-0.17.0+dfsg/benchmarks/cases/__init__.py python-cartopy-0.18.0+dfsg/benchmarks/cases/__init__.py --- python-cartopy-0.17.0+dfsg/benchmarks/cases/__init__.py 1970-01-01 00:00:00.000000000 +0000 +++ python-cartopy-0.18.0+dfsg/benchmarks/cases/__init__.py 2020-05-03 08:12:47.000000000 +0000 @@ -0,0 +1,18 @@ +# (C) British Crown Copyright 2019, 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 . + +from __future__ import (absolute_import, division, print_function) diff -Nru python-cartopy-0.17.0+dfsg/benchmarks/cases/mpl_redraw.py python-cartopy-0.18.0+dfsg/benchmarks/cases/mpl_redraw.py --- python-cartopy-0.17.0+dfsg/benchmarks/cases/mpl_redraw.py 1970-01-01 00:00:00.000000000 +0000 +++ python-cartopy-0.18.0+dfsg/benchmarks/cases/mpl_redraw.py 2020-05-03 08:12:47.000000000 +0000 @@ -0,0 +1,49 @@ +# (C) British Crown Copyright 2019, 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 . + +from __future__ import (absolute_import, division, print_function) + +import cartopy.crs as ccrs +import matplotlib.pyplot as plt +import io + + +# No need for anything other than the agg backend, and we don't want +# windows popping up as we are running these tests. +plt.switch_backend('agg') + + +def create_pc_png(): + fig = plt.figure() + ax = fig.add_subplot(1, 1, 1, projection=ccrs.PlateCarree()) + ax.coastlines() + ax.stock_img() + plt.savefig(io.BytesIO(), format='png') + plt.close(fig) + + +def time_basic_draw_speed(): + create_pc_png() + + +def time_second_figure(): + # Successive figures with Axes of the same projection + # could have various caching mechanisms in place. + # At the time of writing, there is no + # noticable performance speedup during the second figure :( + create_pc_png() + create_pc_png() diff -Nru python-cartopy-0.17.0+dfsg/benchmarks/cases/project_linear.py python-cartopy-0.18.0+dfsg/benchmarks/cases/project_linear.py --- python-cartopy-0.17.0+dfsg/benchmarks/cases/project_linear.py 1970-01-01 00:00:00.000000000 +0000 +++ python-cartopy-0.18.0+dfsg/benchmarks/cases/project_linear.py 2020-05-03 08:12:47.000000000 +0000 @@ -0,0 +1,64 @@ +# (C) British Crown Copyright 2019, 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 . + +from __future__ import (absolute_import, division, print_function) + +import cartopy.io.shapereader as shpreader +import cartopy.crs as ccrs +import shapely.geometry as sgeom + + +class Oceans: + def prepare(self): + shpfilename = shpreader.natural_earth( + resolution='50m', category='physical', name='ocean') + reader = shpreader.Reader(shpfilename) + oceans = reader.geometries() + oceans = sgeom.MultiPolygon(oceans) + self.geoms = oceans + + +OCEAN = Oceans() + + +def use_setup(setup_fn): + # A decorator to create a decorator... + def decorator(test_func): + # This decorator attaches the setup function to the test. + test_func.setup = setup_fn + return test_func + return decorator + + +@use_setup(OCEAN.prepare) +def time_ocean_pc(): + ccrs.PlateCarree().project_geometry(OCEAN.geoms) + + +@use_setup(OCEAN.prepare) +def time_ocean_np(): + ccrs.NorthPolarStereo().project_geometry(OCEAN.geoms) + + +@use_setup(OCEAN.prepare) +def time_ocean_rob(): + ccrs.Robinson().project_geometry(OCEAN.geoms) + + +@use_setup(OCEAN.prepare) +def time_ocean_igh(): + ccrs.InterruptedGoodeHomolosine().project_geometry(OCEAN.geoms) diff -Nru python-cartopy-0.17.0+dfsg/.circleci/config.yml python-cartopy-0.18.0+dfsg/.circleci/config.yml --- python-cartopy-0.17.0+dfsg/.circleci/config.yml 2018-11-17 07:25:32.000000000 +0000 +++ python-cartopy-0.18.0+dfsg/.circleci/config.yml 2020-05-03 08:12:47.000000000 +0000 @@ -1,7 +1,7 @@ # Circle CI configuration file # https://circleci.com/docs/ -version: 2 +version: 2.1 ########################################### @@ -23,8 +23,6 @@ # a separate environment. name: Setup conda environment command: | - # Update conda to fix https://github.com/conda/conda/issues/6811 - conda install -n base conda conda config --set always_yes yes --set changeps1 no --set show_channel_urls yes conda create -n test-environment python=$PYTHON_VERSION conda config --add channels conda-forge @@ -32,8 +30,7 @@ deps-run: &deps-install name: Install Python dependencies command: | - source activate test-environment - conda install --quiet \ + conda install -n test-environment --quiet \ numpy \ matplotlib \ scipy \ @@ -49,7 +46,7 @@ pykdtree \ $EXTRA_PACKAGES \ --file docs/doc-requirements.txt - conda list + conda list -n test-environment cp-run: &cp-install name: Install Cartopy @@ -61,8 +58,7 @@ name: Build documentation command: | source activate test-environment - make html - working_directory: docs + make -C docs html ########################################## @@ -89,33 +85,6 @@ - store_artifacts: path: docs/build/html - - run: - name: "Built documentation is available at:" - command: echo "${CIRCLE_BUILD_URL}/artifacts/${CIRCLE_NODE_INDEX}/${CIRCLE_WORKING_DIRECTORY/#\~/$HOME}/docs/build/html/index.html" - - docs-python2: - docker: - - image: continuumio/miniconda:latest - steps: - - checkout - - - run: *apt-install - - run: - <<: *env-setup - environment: - PYTHON_VERSION: 2 - - run: *deps-install - - run: *cp-install - - - run: *doc-build - - - store_artifacts: - path: docs/build/html - - - run: - name: "Built documentation is available at:" - command: echo "${CIRCLE_BUILD_URL}/artifacts/${CIRCLE_NODE_INDEX}/${CIRCLE_WORKING_DIRECTORY/#\~/$HOME}/docs/build/html/index.html" - ######################################### # Defining workflows gets us parallelism. @@ -126,4 +95,3 @@ build: jobs: - docs-python3 - - docs-python2 diff -Nru python-cartopy-0.17.0+dfsg/CONTRIBUTING.md python-cartopy-0.18.0+dfsg/CONTRIBUTING.md --- python-cartopy-0.17.0+dfsg/CONTRIBUTING.md 2018-11-17 07:25:32.000000000 +0000 +++ python-cartopy-0.18.0+dfsg/CONTRIBUTING.md 1970-01-01 00:00:00.000000000 +0000 @@ -1,32 +0,0 @@ -How to contribute -================= - -Cartopy is driven by a community of people all passionate about -seeing cartopy flourish as a world class mapping package for Python. -This page lists the guidelines for contributors which -will help ease the process of getting your hard work accepted back into -the cartopy repository. - - -Getting started ---------------- - -1. If you've not already got one, sign up for a - [GitHub account](https://github.com/signup/free). -1. Fork the Cartopy repository, create your new fix/feature branch, and - start committing code. We broadly follow the [gitwash guidelines](https://matthew-brett.github.io/pydagogue/gitwash/git_development.html). -1. Remember to add appropriate documentation and tests to supplement any new or changed functionality. -1. If you're not already on it (and would like to be), please add yourself to the - contributors list (docs/source/contributors.rst) - - -Submitting changes ------------------- - -1. Read and sign the Contributor Licence Agreement (CLA) if you have not already done so. - - See the [governance page](http://scitools.org.uk/governance.html) - for the CLA and what to do with it. -1. Push your branch to your fork of cartopy. -1. Submit your pull request. -1. Sit back and wait for the core Cartopy development team to review your code. - diff -Nru python-cartopy-0.17.0+dfsg/.coveragerc python-cartopy-0.18.0+dfsg/.coveragerc --- python-cartopy-0.17.0+dfsg/.coveragerc 1970-01-01 00:00:00.000000000 +0000 +++ python-cartopy-0.18.0+dfsg/.coveragerc 2020-05-03 08:12:47.000000000 +0000 @@ -0,0 +1,24 @@ +# +# .coveragerc to control coverage.py +# + +[run] +branch = True +omit = + lib/cartopy/examples/* + lib/cartopy/sphinxext/* + lib/cartopy/tests/* + lib/cartopy/_version.py + *.pxd +plugins = Cython.Coverage + +[paths] +source = + lib/cartopy + /home/travis/miniconda/envs/test-environment/lib/python?.?/site-packages/cartopy + +[report] +exclude_lines = + pragma: no cover + def __repr__ + if __name__ == .__main__.: diff -Nru python-cartopy-0.17.0+dfsg/debian/changelog python-cartopy-0.18.0+dfsg/debian/changelog --- python-cartopy-0.17.0+dfsg/debian/changelog 2020-02-21 14:50:38.000000000 +0000 +++ python-cartopy-0.18.0+dfsg/debian/changelog 2020-05-04 03:51:43.000000000 +0000 @@ -1,3 +1,46 @@ +python-cartopy (0.18.0+dfsg-1) unstable; urgency=medium + + * Team upload. + * New upstream release. + * Move from experimental to unstable. + + -- Bas Couwenberg Mon, 04 May 2020 05:51:43 +0200 + +python-cartopy (0.18.0~rc1+dfsg-1~exp1) experimental; urgency=medium + + * Team upload. + * New upstream release candidate. + + -- Bas Couwenberg Mon, 27 Apr 2020 10:35:25 +0200 + +python-cartopy (0.18.0~b2+dfsg-1~exp1) experimental; urgency=medium + + * Team upload. + * New upstream beta release. + * Drop no-network.patch, applied upstream. Refresh remaining patches. + * Update copyright file. + + -- Bas Couwenberg Mon, 13 Apr 2020 15:15:53 +0200 + +python-cartopy (0.18.0~b1+dfsg-1~exp2) experimental; urgency=medium + + * Team upload. + * Add patch to fix FTBFS due to test failures. + (closes: #951767) + + -- Bas Couwenberg Fri, 21 Feb 2020 15:27:07 +0100 + +python-cartopy (0.18.0~b1+dfsg-1~exp1) experimental; urgency=medium + + * Team upload. + * New upstream beta release. + * Update watch file for pre-releases. + * Update copyright years for Met Office. + * Drop patches applied upstream. Refresh remaining patches. + * Add patch to mark additional tests that require network. + + -- Bas Couwenberg Tue, 11 Feb 2020 09:52:49 +0100 + python-cartopy (0.17.0+dfsg-9) unstable; urgency=medium * Team upload. diff -Nru python-cartopy-0.17.0+dfsg/debian/copyright python-cartopy-0.18.0+dfsg/debian/copyright --- python-cartopy-0.17.0+dfsg/debian/copyright 2020-02-21 14:49:24.000000000 +0000 +++ python-cartopy-0.18.0+dfsg/debian/copyright 2020-05-04 03:45:57.000000000 +0000 @@ -12,19 +12,13 @@ Files: * Copyright: British Crown - 2010-2018, Met Office + 2010-2020, Met Office License: LGPL-3+ Files: docs/source/_static/version_switch.js Copyright: 2013, Python Software Foundation License: Python-2.0 -Files: docs/source/sphinxext/plot_directive.py -Copyright: 2012-2016, The matplotlib development team -Comment: This file is lifted from the change proposed in - . -License: matplotlib - Files: docs/source/sphinxext/pre_sphinx_gallery.py Copyright: 2015, Óscar Nájera License: BSD-3-Clause @@ -95,57 +89,6 @@ ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -License: matplotlib - License agreement for matplotlib versions 1.3.0 and later - ========================================================= - . - 1. This LICENSE AGREEMENT is between the Matplotlib Development Team - ("MDT"), and the Individual or Organization ("Licensee") accessing and - otherwise using matplotlib software in source or binary form and its - associated documentation. - . - 2. Subject to the terms and conditions of this License Agreement, MDT - hereby grants Licensee a nonexclusive, royalty-free, world-wide license - to reproduce, analyze, test, perform and/or display publicly, prepare - derivative works, distribute, and otherwise use matplotlib - alone or in any derivative version, provided, however, that MDT's - License Agreement and MDT's notice of copyright, i.e., "Copyright (c) - 2012- Matplotlib Development Team; All Rights Reserved" are retained in - matplotlib alone or in any derivative version prepared by - Licensee. - . - 3. In the event Licensee prepares a derivative work that is based on or - incorporates matplotlib or any part thereof, and wants to - make the derivative work available to others as provided herein, then - Licensee hereby agrees to include in any such work a brief summary of - the changes made to matplotlib . - . - 4. MDT is making matplotlib available to Licensee on an "AS - IS" basis. MDT MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR - IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, MDT MAKES NO AND - DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS - FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF MATPLOTLIB - WILL NOT INFRINGE ANY THIRD PARTY RIGHTS. - . - 5. MDT SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF MATPLOTLIB - FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR - LOSS AS A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING - MATPLOTLIB , OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF - THE POSSIBILITY THEREOF. - . - 6. This License Agreement will automatically terminate upon a material - breach of its terms and conditions. - . - 7. Nothing in this License Agreement shall be deemed to create any - relationship of agency, partnership, or joint venture between MDT and - Licensee. This License Agreement does not grant permission to use MDT - trademarks or trade name in a trademark sense to endorse or promote - products or services of Licensee, or any third party. - . - 8. By copying, installing or otherwise using matplotlib , - Licensee agrees to be bound by the terms and conditions of this License - Agreement. - License: Python-2.0 PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2 -------------------------------------------- diff -Nru python-cartopy-0.17.0+dfsg/debian/patches/0001-Skip-tests-failing-on-i386-architectures.patch python-cartopy-0.18.0+dfsg/debian/patches/0001-Skip-tests-failing-on-i386-architectures.patch --- python-cartopy-0.17.0+dfsg/debian/patches/0001-Skip-tests-failing-on-i386-architectures.patch 2020-02-21 14:49:24.000000000 +0000 +++ python-cartopy-0.18.0+dfsg/debian/patches/0001-Skip-tests-failing-on-i386-architectures.patch 2020-05-04 03:45:57.000000000 +0000 @@ -10,15 +10,14 @@ --- a/lib/cartopy/tests/crs/test_lambert_azimuthal_equal_area.py +++ b/lib/cartopy/tests/crs/test_lambert_azimuthal_equal_area.py -@@ -20,6 +20,7 @@ from __future__ import (absolute_import, +@@ -20,12 +20,15 @@ from __future__ import (absolute_import, import numpy as np from numpy.testing import assert_almost_equal import pytest +import sysconfig import cartopy.crs as ccrs - -@@ -31,6 +32,8 @@ def check_proj4_params(crs, other_args): + from .helpers import check_proj_params class TestLambertAzimuthalEqualArea(object): @@ -44,7 +43,7 @@ +@pytest.mark.xfail('i386' in sysconfig.get_config_var('MULTIARCH'), + reason='Limitations of i386 architecture') @ImageTesting(['gshhs_coastlines'], - tolerance=1.7 if MPL_VERSION < '2' else 0) + tolerance=3.3 if MPL_VERSION < '2' else 0.95) def test_gshhs(): --- a/lib/cartopy/tests/mpl/test_images.py +++ b/lib/cartopy/tests/mpl/test_images.py @@ -56,7 +55,7 @@ import types import numpy as np -@@ -169,6 +170,8 @@ def test_pil_Image(): +@@ -186,6 +187,8 @@ def test_pil_Image(): extent=[-180, 180, -90, 90]) diff -Nru python-cartopy-0.17.0+dfsg/debian/patches/0002-no-network.patch python-cartopy-0.18.0+dfsg/debian/patches/0002-no-network.patch --- python-cartopy-0.17.0+dfsg/debian/patches/0002-no-network.patch 2020-02-21 14:49:24.000000000 +0000 +++ python-cartopy-0.18.0+dfsg/debian/patches/0002-no-network.patch 1970-01-01 00:00:00.000000000 +0000 @@ -1,19 +0,0 @@ -From: Bas Couwenberg -Date: Sun, 10 Feb 2019 11:52:26 +0000 -Subject: Mark additional tests as requiring network. - -Forwarded: https://github.com/SciTools/cartopy/issues/1206 ---- - lib/cartopy/tests/mpl/test_nightshade.py | 1 + - 1 file changed, 1 insertion(+) - ---- a/lib/cartopy/tests/mpl/test_nightshade.py -+++ b/lib/cartopy/tests/mpl/test_nightshade.py -@@ -27,6 +27,7 @@ from cartopy.feature.nightshade import N - from cartopy.tests.mpl import ImageTesting - - -+@pytest.mark.network - @ImageTesting(['nightshade_platecarree']) - def test_nightshade_image(): - # Test the actual creation of the image diff -Nru python-cartopy-0.17.0+dfsg/debian/patches/series python-cartopy-0.18.0+dfsg/debian/patches/series --- python-cartopy-0.17.0+dfsg/debian/patches/series 2020-02-21 14:50:12.000000000 +0000 +++ python-cartopy-0.18.0+dfsg/debian/patches/series 2020-05-04 03:48:25.000000000 +0000 @@ -1,5 +1,2 @@ 0001-Skip-tests-failing-on-i386-architectures.patch -0002-no-network.patch -skip-test-with-proj6.patch -test_images.patch test_robinson.patch diff -Nru python-cartopy-0.17.0+dfsg/debian/patches/skip-test-with-proj6.patch python-cartopy-0.18.0+dfsg/debian/patches/skip-test-with-proj6.patch --- python-cartopy-0.17.0+dfsg/debian/patches/skip-test-with-proj6.patch 2020-02-21 14:49:24.000000000 +0000 +++ python-cartopy-0.18.0+dfsg/debian/patches/skip-test-with-proj6.patch 1970-01-01 00:00:00.000000000 +0000 @@ -1,196 +0,0 @@ -Description: Skip tests that fail with PROJ 6. -Author: Bas Couwenberg -Bug: https://github.com/SciTools/cartopy/issues/1140 -Forwarded: not-needed - ---- a/lib/cartopy/tests/crs/test_transverse_mercator.py -+++ b/lib/cartopy/tests/crs/test_transverse_mercator.py -@@ -21,7 +21,10 @@ Tests for the Transverse Mercator projec - - from __future__ import (absolute_import, division, print_function) - -+import os -+ - import numpy as np -+import pytest - - import cartopy.crs as ccrs - -@@ -32,6 +35,7 @@ class TestTransverseMercator(object): - self.point_b = (0.5, 50.5) - self.src_crs = ccrs.PlateCarree() - -+ @pytest.mark.skipif(os.path.exists('/usr/share/proj/proj.db'), reason='Fails with PROJ 6') - def test_default(self): - proj = ccrs.TransverseMercator() - res = proj.transform_point(*self.point_a, src_crs=self.src_crs) -@@ -41,6 +45,7 @@ class TestTransverseMercator(object): - np.testing.assert_array_almost_equal(res, (35474.63566645, - 5596583.41949901)) - -+ @pytest.mark.skipif(os.path.exists('/usr/share/proj/proj.db'), reason='Fails with PROJ 6') - def test_osgb_vals(self): - proj = ccrs.TransverseMercator(central_longitude=-2, - central_latitude=49, -@@ -56,6 +61,7 @@ class TestTransverseMercator(object): - np.testing.assert_array_almost_equal(res, (577274.98380140, - 69740.49227181)) - -+ @pytest.mark.skipif(os.path.exists('/usr/share/proj/proj.db'), reason='Fails with PROJ 6') - def test_nan(self): - proj = ccrs.TransverseMercator() - res = proj.transform_point(0.0, float('nan'), src_crs=self.src_crs) -@@ -71,6 +77,7 @@ class TestOSGB(object): - self.src_crs = ccrs.PlateCarree() - self.nan = float('nan') - -+ @pytest.mark.skipif(os.path.exists('/usr/share/proj/proj.db'), reason='Fails with PROJ 6') - def test_default(self): - proj = ccrs.OSGB() - res = proj.transform_point(*self.point_a, src_crs=self.src_crs) -@@ -80,6 +87,7 @@ class TestOSGB(object): - np.testing.assert_array_almost_equal(res, (577274.98380140, - 69740.49227181)) - -+ @pytest.mark.skipif(os.path.exists('/usr/share/proj/proj.db'), reason='Fails with PROJ 6') - def test_nan(self): - proj = ccrs.OSGB() - res = proj.transform_point(0.0, float('nan'), src_crs=self.src_crs) -@@ -101,6 +109,7 @@ class TestOSNI(object): - res, (275614.26762651594, 386984.206429612), - decimal=0 if ccrs.PROJ4_VERSION < (5, 0, 0) else 6) - -+ @pytest.mark.skipif(os.path.exists('/usr/share/proj/proj.db'), reason='Fails with PROJ 6') - def test_nan(self): - proj = ccrs.OSNI() - res = proj.transform_point(0.0, float('nan'), src_crs=self.src_crs) ---- a/lib/cartopy/tests/mpl/test_set_extent.py -+++ b/lib/cartopy/tests/mpl/test_set_extent.py -@@ -17,10 +17,13 @@ - - from __future__ import (absolute_import, division, print_function) - -+import os -+ - from matplotlib.testing.decorators import cleanup - import matplotlib.pyplot as plt - import numpy as np - from numpy.testing import assert_array_almost_equal, assert_array_equal -+import pytest - - import cartopy.crs as ccrs - -@@ -148,6 +151,7 @@ def test_limits_pcolor(): - plt.close() - - -+@pytest.mark.skipif(os.path.exists('/usr/share/proj/proj.db'), reason='Fails with PROJ 6') - def test_view_lim_autoscaling(): - x = np.linspace(0.12910209, 0.42141822) - y = np.linspace(0.03739792, 0.33029076) ---- a/lib/cartopy/tests/test_crs.py -+++ b/lib/cartopy/tests/test_crs.py -@@ -18,6 +18,7 @@ - from __future__ import (absolute_import, division, print_function) - - from io import BytesIO -+import os - import pickle - - import numpy as np -@@ -106,6 +107,7 @@ class TestCRS(object): - # International 1924 ellipsoid. - assert '+ellps=intl' in proj4_init - -+ @pytest.mark.skipif(os.path.exists('/usr/share/proj/proj.db'), reason='Fails with PROJ 6') - def test_transform_points_nD(self): - rlons = np.array([[350., 352., 354.], [350., 352., 354.]]) - rlats = np.array([[-5., -0., 1.], [-4., -1., 0.]]) -@@ -126,6 +128,7 @@ class TestCRS(object): - assert_arr_almost_eq(unrotated_lon, solx) - assert_arr_almost_eq(unrotated_lat, soly) - -+ @pytest.mark.skipif(os.path.exists('/usr/share/proj/proj.db'), reason='Fails with PROJ 6') - def test_transform_points_1D(self): - rlons = np.array([350., 352., 354., 356.]) - rlats = np.array([-5., -0., 5., 10.]) ---- a/lib/cartopy/tests/test_line_string.py -+++ b/lib/cartopy/tests/test_line_string.py -@@ -18,6 +18,7 @@ - from __future__ import (absolute_import, division, print_function) - - import itertools -+import os - import time - - import numpy as np -@@ -28,6 +29,7 @@ import cartopy.crs as ccrs - - - class TestLineString(object): -+ @pytest.mark.skipif(os.path.exists('/usr/share/proj/proj.db'), reason='Fails with PROJ 6') - def test_out_of_bounds(self): - # Check that a line that is completely out of the map boundary produces - # a valid LineString ---- a/lib/cartopy/tests/test_linear_ring.py -+++ b/lib/cartopy/tests/test_linear_ring.py -@@ -17,6 +17,8 @@ - - from __future__ import (absolute_import, division, print_function) - -+import os -+ - import numpy as np - import pytest - import shapely.geometry as sgeom -@@ -56,6 +58,7 @@ class TestBoundary(object): - assert_intersection_with_boundary(coords[1::-1]) - assert_intersection_with_boundary(coords[-2:]) - -+ @pytest.mark.skipif(os.path.exists('/usr/share/proj/proj.db'), reason='Fails with PROJ 6') - def test_out_of_bounds(self): - # Check that a ring that is completely out of the map boundary - # produces an empty result. ---- a/lib/cartopy/tests/test_polygon.py -+++ b/lib/cartopy/tests/test_polygon.py -@@ -17,6 +17,8 @@ - - from __future__ import (absolute_import, division, print_function) - -+import os -+ - import numpy as np - import pytest - import shapely.geometry as sgeom -@@ -48,6 +50,7 @@ class TestBoundary(object): - # fails. - projection.project_geometry(polygon) - -+ @pytest.mark.skipif(os.path.exists('/usr/share/proj/proj.db'), reason='Fails with PROJ 6') - def test_out_of_bounds(self): - # Check that a polygon that is completely out of the map boundary - # doesn't produce an empty result. -@@ -189,6 +192,7 @@ class TestMisc(object): - assert len(multi_polygon) == 1 - assert len(multi_polygon[0].exterior.coords) == 4 - -+ @pytest.mark.skipif(os.path.exists('/usr/share/proj/proj.db'), reason='Fails with PROJ 6') - def test_self_intersecting_1(self): - # Geometry comes from a matplotlib contourf (see #537) - wkt = ('POLYGON ((366.22000122 -9.71489298, ' -@@ -210,6 +214,7 @@ class TestMisc(object): - assert 2.2e9 < area < 2.3e9, \ - 'Got area {}, expecting ~2.2e9'.format(area) - -+ @pytest.mark.skipif(os.path.exists('/usr/share/proj/proj.db'), reason='Fails with PROJ 6') - def test_self_intersecting_2(self): - # Geometry comes from a matplotlib contourf (see #509) - wkt = ('POLYGON ((343 20, 345 23, 342 25, 343 22, ' -@@ -273,6 +278,7 @@ class TestQuality(object): - # from cartopy.tests.mpl import show - # show(projection, self.multi_polygon) - -+ @pytest.mark.skipif(os.path.exists('/usr/share/proj/proj.db'), reason='Fails with PROJ 6') - def test_split(self): - # Start simple ... there should be two projected polygons. - assert len(self.multi_polygon) == 2 diff -Nru python-cartopy-0.17.0+dfsg/debian/patches/test_images.patch python-cartopy-0.18.0+dfsg/debian/patches/test_images.patch --- python-cartopy-0.17.0+dfsg/debian/patches/test_images.patch 2020-02-21 14:49:24.000000000 +0000 +++ python-cartopy-0.18.0+dfsg/debian/patches/test_images.patch 1970-01-01 00:00:00.000000000 +0000 @@ -1,50 +0,0 @@ -Description: Update tolerances for Matplotlib 3. -Origin: https://github.com/SciTools/cartopy/commits/master/lib/cartopy/tests/mpl/test_images.py - ---- a/lib/cartopy/tests/mpl/test_images.py -+++ b/lib/cartopy/tests/mpl/test_images.py -@@ -1,4 +1,4 @@ --# (C) British Crown Copyright 2011 - 2018, Met Office -+# (C) British Crown Copyright 2011 - 2019, Met Office - # - # This file is part of cartopy. - # -@@ -45,13 +45,18 @@ REGIONAL_IMG = os.path.join(config['repo - # We have an exceptionally large tolerance for the web_tiles test. - # The basemap changes on a regular basis (for seasons) and we really only - # care that it is putting images onto the map which are roughly correct. -+if MPL_VERSION < '2': -+ web_tiles_tolerance = 12 -+else: -+ web_tiles_tolerance = 4.5 -+ -+ - @pytest.mark.natural_earth - @pytest.mark.network - @pytest.mark.xfail(ccrs.PROJ4_VERSION == (5, 0, 0), - reason='Proj returns slightly different bounds.', - strict=True) --@ImageTesting(['web_tiles'], -- tolerance=12 if MPL_VERSION < '2' else 2.9) -+@ImageTesting(['web_tiles'], tolerance=web_tiles_tolerance) - def test_web_tiles(): - extent = [-15, 0.1, 50, 60] - target_domain = sgeom.Polygon([[extent[0], extent[1]], -@@ -135,7 +140,7 @@ def test_imshow(): - - @pytest.mark.natural_earth - @ImageTesting(['imshow_regional_projected'], -- tolerance=10.4 if MPL_VERSION < '2' else 0) -+ tolerance=10.4 if MPL_VERSION < '2' else 0.8) - def test_imshow_projected(): - source_proj = ccrs.PlateCarree() - img_extent = (-120.67660000000001, -106.32104523100001, -@@ -176,7 +181,7 @@ def test_pil_Image(): - reason='Proj Orthographic projection is buggy.', - strict=True) - @ImageTesting(['imshow_natural_earth_ortho'], -- tolerance=4.2 if MPL_VERSION < '2' else 0) -+ tolerance=4.2 if MPL_VERSION < '2' else 0.5) - def test_background_img(): - ax = plt.axes(projection=ccrs.Orthographic()) - ax.background_img(name='ne_shaded', resolution='low') diff -Nru python-cartopy-0.17.0+dfsg/debian/patches/test_robinson.patch python-cartopy-0.18.0+dfsg/debian/patches/test_robinson.patch --- python-cartopy-0.17.0+dfsg/debian/patches/test_robinson.patch 2020-02-21 14:50:23.000000000 +0000 +++ python-cartopy-0.18.0+dfsg/debian/patches/test_robinson.patch 2020-05-04 03:48:39.000000000 +0000 @@ -5,7 +5,7 @@ --- a/lib/cartopy/tests/crs/test_robinson.py +++ b/lib/cartopy/tests/crs/test_robinson.py -@@ -75,6 +75,7 @@ def test_central_longitude(lon): +@@ -119,6 +119,7 @@ def test_central_longitude(lon): [-8625154.6651000, 8625154.6651000], _LIMIT_TOL) @@ -13,7 +13,7 @@ def test_transform_point(): """ Mostly tests the workaround for a specific problem. -@@ -96,6 +97,7 @@ def test_transform_point(): +@@ -140,6 +141,7 @@ def test_transform_point(): assert np.all(np.isnan(result)) diff -Nru python-cartopy-0.17.0+dfsg/debian/watch python-cartopy-0.18.0+dfsg/debian/watch --- python-cartopy-0.17.0+dfsg/debian/watch 2020-02-21 14:49:24.000000000 +0000 +++ python-cartopy-0.18.0+dfsg/debian/watch 2020-05-04 03:45:57.000000000 +0000 @@ -5,4 +5,4 @@ filenamemangle=s/(?:.*?)?(?:rel|v|cartopy)?[\-\_]?(\d\S+)\.(tgz|tbz|txz|(?:tar\.(?:gz|bz2|xz)))/cartopy-$1.$2/,\ repacksuffix=+dfsg \ https://github.com/SciTools/cartopy/releases \ -(?:.*?/archive/)?(?:rel|v|cartopy)?[\-\_]?(\d[\d\-\.]+)\.(?:tgz|tbz|txz|(?:tar\.(?:gz|bz2|xz))) +(?:.*?/archive/)?(?:rel|v|cartopy)?[\-\_]?(\d\S+)\.(?:tgz|tbz|txz|(?:tar\.(?:gz|bz2|xz))) diff -Nru python-cartopy-0.17.0+dfsg/docs/make_projection.py python-cartopy-0.18.0+dfsg/docs/make_projection.py --- python-cartopy-0.17.0+dfsg/docs/make_projection.py 2018-11-17 07:25:32.000000000 +0000 +++ python-cartopy-0.18.0+dfsg/docs/make_projection.py 2020-05-03 08:12:47.000000000 +0000 @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2011 - 2018, Met Office +# (C) British Crown Copyright 2011 - 2019, Met Office # # This file is part of cartopy. # @@ -30,6 +30,9 @@ ccrs.NearsidePerspective: { 'central_longitude': -3.53, 'central_latitude': 50.72, 'satellite_height': 10.0e6}, + ccrs.OSGB: {'approx': False}, + ccrs.OSNI: {'approx': False}, + ccrs.TransverseMercator: {'approx': False}, } diff -Nru python-cartopy-0.17.0+dfsg/docs/source/citation.rst python-cartopy-0.18.0+dfsg/docs/source/citation.rst --- python-cartopy-0.17.0+dfsg/docs/source/citation.rst 2018-11-17 07:25:32.000000000 +0000 +++ python-cartopy-0.18.0+dfsg/docs/source/citation.rst 2020-05-03 08:12:47.000000000 +0000 @@ -16,7 +16,7 @@ title = {Cartopy: a cartographic python library with a Matplotlib interface}, year = {2010 - 2015}, address = {Exeter, Devon }, - url = {http://scitools.org.uk/cartopy} + url = {https://scitools.org.uk/cartopy} } diff -Nru python-cartopy-0.17.0+dfsg/docs/source/conf.py python-cartopy-0.18.0+dfsg/docs/source/conf.py --- python-cartopy-0.17.0+dfsg/docs/source/conf.py 2018-11-17 07:25:32.000000000 +0000 +++ python-cartopy-0.18.0+dfsg/docs/source/conf.py 2020-05-03 08:12:47.000000000 +0000 @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2011 - 2018, Met Office +# (C) British Crown Copyright 2011 - 2020, Met Office # # This file is part of cartopy. # @@ -28,9 +28,14 @@ # # All configuration values have a default; values that are commented out # serve to show the default. - import sys, os +import cartopy +from distutils.version import LooseVersion +import matplotlib +import owslib +from sphinx_gallery.sorting import ExampleTitleSortKey + # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. @@ -54,12 +59,7 @@ # 'sphinx.ext.autosummary', # 'sphinxcontrib.napoleon', (Needs work before this can # be enabled.) - # 'matplotlib.sphinxext.plot_directive' - # We use a local copy of the plot_directive until - # https://github.com/matplotlib/matplotlib/pull/6213 is - # available in order - # to benefit from cached rebuilds of plots. - 'sphinxext.plot_directive', + 'matplotlib.sphinxext.plot_directive', # Monkey-patch sphinx_gallery to handle cartopy's __tags__ # example convention. 'sphinxext.pre_sphinx_gallery', @@ -67,7 +67,7 @@ 'sphinx.ext.napoleon' ] -import matplotlib + matplotlib.use('Agg') # Add any paths that contain templates here, relative to this directory. @@ -92,13 +92,23 @@ # built documents. # # The short X.Y version. -import cartopy version = cartopy.__version__ # The full version, including alpha/beta/rc tags. release = cartopy.__version__ + +if (hasattr(owslib, '__version__') and + LooseVersion(owslib.__version__) >= '0.19.2'): + expected_failing_examples = [] +else: + # OWSLib WMTS support is broken. + expected_failing_examples = [ + 'cartopy/examples/reprojected_wmts.py', + 'cartopy/examples/wmts.py', + 'cartopy/examples/wmts_time.py', + ] + # Sphinx gallery configuration -from sphinx_gallery.sorting import ExampleTitleSortKey sphinx_gallery_conf = { 'examples_dirs': ['../../lib/cartopy/examples'], 'filename_pattern': '^((?!sgskip).)*$', @@ -107,6 +117,7 @@ 'doc_module': ('cartopy',), 'reference_url': {'cartopy': None}, 'backreferences_dir': '../build/backrefs', + 'expected_failing_examples': expected_failing_examples, } # The language for content autogenerated by Sphinx. Refer to documentation @@ -345,13 +356,12 @@ # Example configuration for intersphinx: refer to the Python standard library. -intersphinx_mapping = {'python': ('https://docs.python.org/3', None), - 'matplotlib': ('https://matplotlib.org', None), - 'numpy': ('https://docs.scipy.org/doc/numpy/', None), - 'shapely': ('http://toblerity.org/shapely', None), } - - - +intersphinx_mapping = { + 'python': ('https://docs.python.org/3', None), + 'matplotlib': ('https://matplotlib.org', None), + 'numpy': ('https://docs.scipy.org/doc/numpy/', None), + 'shapely': ('https://shapely.readthedocs.io/en/latest/', None), +} ############ extlinks extension ############ @@ -379,8 +389,8 @@ 'figure.subplot.top': 0.96, 'figure.subplot.left': 0.04, 'figure.subplot.right': 0.96} -plot_formats = [('thumb.png', 20), - 'png', +plot_formats = ['png', + ('thumb.png', 20), 'pdf' ] diff -Nru python-cartopy-0.17.0+dfsg/docs/source/contributors.rst python-cartopy-0.18.0+dfsg/docs/source/contributors.rst --- python-cartopy-0.17.0+dfsg/docs/source/contributors.rst 2018-11-17 07:25:32.000000000 +0000 +++ python-cartopy-0.18.0+dfsg/docs/source/contributors.rst 2020-05-03 08:12:47.000000000 +0000 @@ -38,6 +38,8 @@ * Daryl Herzmann * Robert Redl * Greg Lucas + * Sadie Bartholomew + * Kacper Makuch Thank you! diff -Nru python-cartopy-0.17.0+dfsg/docs/source/crs/projections.rst python-cartopy-0.18.0+dfsg/docs/source/crs/projections.rst --- python-cartopy-0.17.0+dfsg/docs/source/crs/projections.rst 2018-11-17 07:25:32.000000000 +0000 +++ python-cartopy-0.18.0+dfsg/docs/source/crs/projections.rst 2020-05-03 08:12:47.000000000 +0000 @@ -25,9 +25,9 @@ for i in range(0, nplots): central_longitude = 0 if i == 0 else 180 - ax = fig.add_subplot(nplots, 1, i+1, - projection=ccrs.PlateCarree( - central_longitude=central_longitude)) + ax = fig.add_subplot( + nplots, 1, i+1, + projection=ccrs.PlateCarree(central_longitude=central_longitude)) ax.coastlines(resolution='110m') ax.gridlines() @@ -139,7 +139,7 @@ import matplotlib.pyplot as plt import cartopy.crs as ccrs - plt.figure(figsize=(4.0915, 3)) + plt.figure(figsize=(4.0917, 3)) ax = plt.axes(projection=ccrs.Miller()) ax.coastlines(resolution='110m') ax.gridlines() @@ -236,7 +236,8 @@ import cartopy.crs as ccrs plt.figure(figsize=(6, 3)) - ax = plt.axes(projection=ccrs.TransverseMercator()) + ax = plt.axes(projection=ccrs.TransverseMercator( + approx=False)) ax.coastlines(resolution='110m') ax.gridlines() @@ -308,7 +309,8 @@ import cartopy.crs as ccrs plt.figure(figsize=(1.6154, 3)) - ax = plt.axes(projection=ccrs.OSGB()) + ax = plt.axes(projection=ccrs.OSGB( + approx=False)) ax.coastlines(resolution='50m') ax.gridlines() @@ -535,7 +537,8 @@ import cartopy.crs as ccrs plt.figure(figsize=(2.4323, 3)) - ax = plt.axes(projection=ccrs.OSNI()) + ax = plt.axes(projection=ccrs.OSNI( + approx=False)) ax.coastlines(resolution='10m') ax.gridlines() diff -Nru python-cartopy-0.17.0+dfsg/docs/source/developer_interfaces.rst python-cartopy-0.18.0+dfsg/docs/source/developer_interfaces.rst --- python-cartopy-0.17.0+dfsg/docs/source/developer_interfaces.rst 2018-11-17 07:25:32.000000000 +0000 +++ python-cartopy-0.18.0+dfsg/docs/source/developer_interfaces.rst 2020-05-03 08:12:47.000000000 +0000 @@ -33,7 +33,7 @@ An example of specialising this class can be found in :mod:`cartopy.io.shapereader.NEShpDownloader` which enables the downloading of -zipped shapefiles from the ``_ website. All +zipped shapefiles from the ``_ website. All known subclasses of :class:`~cartopy.io.Downloader` are listed below for reference: diff -Nru python-cartopy-0.17.0+dfsg/docs/source/index.rst python-cartopy-0.18.0+dfsg/docs/source/index.rst --- python-cartopy-0.17.0+dfsg/docs/source/index.rst 2018-11-17 07:25:32.000000000 +0000 +++ python-cartopy-0.18.0+dfsg/docs/source/index.rst 2020-05-03 08:12:47.000000000 +0000 @@ -21,6 +21,8 @@ assumptions of spherical data traditionally break down. If you've ever experienced a singularity at the pole or a cut-off at the dateline, it is likely you will appreciate cartopy's unique features! +.. warning:: + The 0.18 release is the final release that will support Python 2.7. .. _getting-started-with-cartopy: @@ -81,8 +83,8 @@ bugs which cover the issue before making a new one). * Help others with cartopy questions on `StackOverflow `_. * Contribute to the documentation fixing typos, adding examples, explaining things more clearly, or even - re-designing its layout/logos etc.. The `documentation source `_ is kept in the same repository as the source code. - * Contribute bug fixes (:issues:`a list of outstanding bugs can be found on github `). + re-designing its layout/logos etc. The `documentation source `_ is kept in the same repository as the source code. + * Contribute bug fixes (:issues:`a list of outstanding bugs can be found on GitHub `). * Contribute enhancements and new features on the issue tracker. * Chat with users and developers in the `Gitter chat room `_. diff -Nru python-cartopy-0.17.0+dfsg/docs/source/matplotlib/advanced_plotting.rst python-cartopy-0.18.0+dfsg/docs/source/matplotlib/advanced_plotting.rst --- python-cartopy-0.17.0+dfsg/docs/source/matplotlib/advanced_plotting.rst 2018-11-17 07:25:32.000000000 +0000 +++ python-cartopy-0.18.0+dfsg/docs/source/matplotlib/advanced_plotting.rst 2020-05-03 08:12:47.000000000 +0000 @@ -5,7 +5,7 @@ mapping visualisations available for scientific data. Thanks to the simplicity of the cartopy interface, in many cases the hardest part of producing such visualisations is getting hold of the data in the first place. To address this, a Python package, -`Iris `_, has been created to make loading and saving data from a +`Iris `_, has been created to make loading and saving data from a variety of gridded datasets easier. Some of the following examples make use of the Iris loading capabilities, while others use the netCDF4 Python package so as to show a range of different approaches to data loading. diff -Nru python-cartopy-0.17.0+dfsg/docs/source/matplotlib/feature_interface.rst python-cartopy-0.18.0+dfsg/docs/source/matplotlib/feature_interface.rst --- python-cartopy-0.17.0+dfsg/docs/source/matplotlib/feature_interface.rst 2018-11-17 07:25:32.000000000 +0000 +++ python-cartopy-0.18.0+dfsg/docs/source/matplotlib/feature_interface.rst 2020-05-03 08:12:47.000000000 +0000 @@ -3,7 +3,7 @@ The cartopy Feature interface ============================= -The :ref:`data copyright, license and attribution ` can be blended on the map using `text annotations (mpl docs) `_ as shown in `feature_creation <../gallery/lines_and_polygons/feature_creation.html>`_. +The :ref:`data copyright, license and attribution ` can be blended on the map using `text annotations (mpl docs) `_ as shown in `feature_creation <../gallery/feature_creation.html>`_. .. currentmodule:: cartopy.feature @@ -33,7 +33,7 @@ To simplify some very common cases, some pre-defined Features exist as :mod:`cartopy.feature` constants. The pre-defined Features are all small-scale (1:110m) -`Natural Earth `_ datasets, and can be added with methods +`Natural Earth `_ datasets, and can be added with methods such as :func:`GeoAxes.add_feature `: ======================= ================================================================ diff -Nru python-cartopy-0.17.0+dfsg/docs/source/matplotlib/gridliner.rst python-cartopy-0.18.0+dfsg/docs/source/matplotlib/gridliner.rst --- python-cartopy-0.17.0+dfsg/docs/source/matplotlib/gridliner.rst 2018-11-17 07:25:32.000000000 +0000 +++ python-cartopy-0.18.0+dfsg/docs/source/matplotlib/gridliner.rst 2020-05-03 08:12:47.000000000 +0000 @@ -1,8 +1,8 @@ -Cartopy map gridlines and tick labels +Cartopy map gridlines and tick labels ===================================== -The :class:`~cartopy.mpl.gridliner.Gridliner` instance, often created by calling the -:meth:`cartopy.mpl.geoaxes.GeoAxes.gridlines` method on a +The :class:`~cartopy.mpl.gridliner.Gridliner` instance, often created by calling the +:meth:`cartopy.mpl.geoaxes.GeoAxes.gridlines` method on a :class:`cartopy.mpl.geoaxes.GeoAxes` instance, has a variety of attributes which can be used to determine draw time behaviour of the gridlines and labels. @@ -11,14 +11,33 @@ The current :class:`~cartopy.mpl.gridliner.Gridliner` interface is likely to undergo a significant change in the versions following v0.6 in order to fix some of the underying limitations of the current implementation. - + .. autoclass:: cartopy.mpl.gridliner.Gridliner :members: :undoc-members: - - + +In this first example, gridines and tick labels are plotted in a +non-rectangular projection, with most default values and +no tuning of the gridliner attributes: + +.. plot:: + :include-source: + + import matplotlib.pyplot as plt + import cartopy.crs as ccrs + + rotated_crs = ccrs.RotatedPole(pole_longitude=120.0, pole_latitude=70.0) + + ax = plt.axes(projection=rotated_crs) + ax.set_extent([-6, 3, 48, 58], crs=ccrs.PlateCarree()) + ax.coastlines(resolution='50m') + ax.gridlines(draw_labels=True, dms=True, x_inline=False, y_inline=False) + + plt.show() + + The following contrived example makes use of many of the features of the Gridliner class to produce customized gridlines and tick labels: @@ -28,22 +47,24 @@ import matplotlib.pyplot as plt import matplotlib.ticker as mticker import cartopy.crs as ccrs - - from cartopy.mpl.gridliner import LONGITUDE_FORMATTER, LATITUDE_FORMATTER - - + + from cartopy.mpl.ticker import (LongitudeFormatter, LatitudeFormatter, + LatitudeLocator) + + ax = plt.axes(projection=ccrs.Mercator()) ax.coastlines() - - gl = ax.gridlines(crs=ccrs.PlateCarree(), draw_labels=True, + + gl = ax.gridlines(crs=ccrs.PlateCarree(), draw_labels=True, linewidth=2, color='gray', alpha=0.5, linestyle='--') - gl.xlabels_top = False - gl.ylabels_left = False + gl.top_labels = False + gl.left_labels = False gl.xlines = False gl.xlocator = mticker.FixedLocator([-180, -45, 0, 45, 180]) - gl.xformatter = LONGITUDE_FORMATTER - gl.yformatter = LATITUDE_FORMATTER + gl.ylocator = LatitudeLocator() + gl.xformatter = LongitudeFormatter() + gl.yformatter = LatitudeFormatter() gl.xlabel_style = {'size': 15, 'color': 'gray'} gl.xlabel_style = {'color': 'red', 'weight': 'bold'} - + plt.show() diff -Nru python-cartopy-0.17.0+dfsg/docs/source/sphinxext/plot_directive.py python-cartopy-0.18.0+dfsg/docs/source/sphinxext/plot_directive.py --- python-cartopy-0.17.0+dfsg/docs/source/sphinxext/plot_directive.py 2018-11-17 07:25:32.000000000 +0000 +++ python-cartopy-0.18.0+dfsg/docs/source/sphinxext/plot_directive.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,871 +0,0 @@ -# This file is lifted from the change proposed in -# https://github.com/matplotlib/matplotlib/pull/6213. -# License: matplotlib BSD-3. -""" -A directive for including a Matplotlib plot in a Sphinx document. - -By default, in HTML output, `plot` will include a .png file with a -link to a high-res .png and .pdf. In LaTeX output, it will include a -.pdf. - -The source code for the plot may be included in one of three ways: - - 1. **A path to a source file** as the argument to the directive:: - - .. plot:: path/to/plot.py - - When a path to a source file is given, the content of the - directive may optionally contain a caption for the plot:: - - .. plot:: path/to/plot.py - - This is the caption for the plot - - Additionally, one may specify the name of a function to call (with - no arguments) immediately after importing the module:: - - .. plot:: path/to/plot.py plot_function1 - - 2. Included as **inline content** to the directive:: - - .. plot:: - - import matplotlib.pyplot as plt - import matplotlib.image as mpimg - import numpy as np - img = mpimg.imread('_static/stinkbug.png') - imgplot = plt.imshow(img) - - 3. Using **doctest** syntax:: - - .. plot:: - A plotting example: - >>> import matplotlib.pyplot as plt - >>> plt.plot([1,2,3], [4,5,6]) - -Options -------- - -The ``plot`` directive supports the following options: - - format : {'python', 'doctest'} - Specify the format of the input - - include-source : bool - Whether to display the source code. The default can be changed - using the `plot_include_source` variable in conf.py - - encoding : str - If this source file is in a non-UTF8 or non-ASCII encoding, - the encoding must be specified using the `:encoding:` option. - The encoding will not be inferred using the ``-*- coding -*-`` - metacomment. - - context : bool or str - If provided, the code will be run in the context of all - previous plot directives for which the `:context:` option was - specified. This only applies to inline code plot directives, - not those run from files. If the ``:context: reset`` option is - specified, the context is reset for this and future plots, and - previous figures are closed prior to running the code. - ``:context:close-figs`` keeps the context but closes previous figures - before running the code. - - nofigs : bool - If specified, the code block will be run, but no figures will - be inserted. This is usually useful with the ``:context:`` - option. - -Additionally, this directive supports all of the options of the -`image` directive, except for `target` (since plot will add its own -target). These include `alt`, `height`, `width`, `scale`, `align` and -`class`. - -Configuration options ---------------------- - -The plot directive has the following configuration options: - - plot_include_source - Default value for the include-source option - - plot_html_show_source_link - Whether to show a link to the source in HTML. - - plot_pre_code - Code that should be executed before each plot. - - plot_basedir - Base directory, to which ``plot::`` file names are relative - to. (If None or empty, file names are relative to the - directory where the file containing the directive is.) - - plot_formats - File formats to generate. List of tuples or strings:: - - [(suffix, dpi), suffix, ...] - - that determine the file format and the DPI. For entries whose - DPI was omitted, sensible defaults are chosen. When passing from - the command line through sphinx_build the list should be passed as - suffix:dpi,suffix:dpi, .... - - plot_html_show_formats - Whether to show links to the files in HTML. - - plot_rcparams - A dictionary containing any non-standard rcParams that should - be applied before each plot. - - plot_apply_rcparams - By default, rcParams are applied when `context` option is not used in - a plot directive. This configuration option overrides this behavior - and applies rcParams before each plot. - - plot_working_directory - By default, the working directory will be changed to the directory of - the example, so the code can get at its data files, if any. Also its - path will be added to `sys.path` so it can import any helper modules - sitting beside it. This configuration option can be used to specify - a central directory (also added to `sys.path`) where data files and - helper modules for all code are located. - - plot_template - Provide a customized template for preparing restructured text. -""" -from __future__ import (absolute_import, division, print_function, - unicode_literals) - -import six -from six.moves import xrange - -import sys, os, shutil, io, re, textwrap -from os.path import relpath -import traceback -import warnings - -if not six.PY3: - import cStringIO - -from docutils.parsers.rst import directives -from docutils.parsers.rst.directives.images import Image -align = Image.align -import sphinx - -sphinx_version = sphinx.__version__.split(".") -# The split is necessary for sphinx beta versions where the string is -# '6b1' -sphinx_version = tuple([int(re.split('[^0-9]', x)[0]) - for x in sphinx_version[:2]]) - -try: - # Sphinx depends on either Jinja or Jinja2 - import jinja2 - def format_template(template, **kw): - return jinja2.Template(template).render(**kw) -except ImportError: - import jinja - def format_template(template, **kw): - return jinja.from_string(template, **kw) - -import matplotlib -import matplotlib.cbook as cbook -try: - with warnings.catch_warnings(record=True): - warnings.simplefilter("error", UserWarning) - matplotlib.use('Agg') -except UserWarning: - import matplotlib.pyplot as plt - plt.switch_backend("Agg") -else: - import matplotlib.pyplot as plt -from matplotlib import _pylab_helpers - -__version__ = 2 - -#------------------------------------------------------------------------------ -# Registration hook -#------------------------------------------------------------------------------ - -def plot_directive(name, arguments, options, content, lineno, - content_offset, block_text, state, state_machine): - return run(arguments, content, options, state_machine, state, lineno) -plot_directive.__doc__ = __doc__ - - -def _option_boolean(arg): - if not arg or not arg.strip(): - # no argument given, assume used as a flag - return True - elif arg.strip().lower() in ('no', '0', 'false'): - return False - elif arg.strip().lower() in ('yes', '1', 'true'): - return True - else: - raise ValueError('"%s" unknown boolean' % arg) - - -def _option_context(arg): - if arg in [None, 'reset', 'close-figs']: - return arg - raise ValueError("argument should be None or 'reset' or 'close-figs'") - - -def _option_format(arg): - return directives.choice(arg, ('python', 'doctest')) - - -def _option_align(arg): - return directives.choice(arg, ("top", "middle", "bottom", "left", "center", - "right")) - - -def mark_plot_labels(app, document): - """ - To make plots referenceable, we need to move the reference from - the "htmlonly" (or "latexonly") node to the actual figure node - itself. - """ - for name, explicit in six.iteritems(document.nametypes): - if not explicit: - continue - labelid = document.nameids[name] - if labelid is None: - continue - node = document.ids[labelid] - if node.tagname in ('html_only', 'latex_only'): - for n in node: - if n.tagname == 'figure': - sectname = name - for c in n: - if c.tagname == 'caption': - sectname = c.astext() - break - - node['ids'].remove(labelid) - node['names'].remove(name) - n['ids'].append(labelid) - n['names'].append(name) - document.settings.env.labels[name] = \ - document.settings.env.docname, labelid, sectname - break - - -def setup(app): - setup.app = app - setup.config = app.config - setup.confdir = app.confdir - - options = {'alt': directives.unchanged, - 'height': directives.length_or_unitless, - 'width': directives.length_or_percentage_or_unitless, - 'scale': directives.nonnegative_int, - 'align': _option_align, - 'class': directives.class_option, - 'include-source': _option_boolean, - 'format': _option_format, - 'context': _option_context, - 'nofigs': directives.flag, - 'encoding': directives.encoding - } - - app.add_directive('plot', plot_directive, True, (0, 2, False), **options) - app.add_config_value('plot_pre_code', None, True) - app.add_config_value('plot_include_source', False, True) - app.add_config_value('plot_html_show_source_link', True, True) - app.add_config_value('plot_formats', ['png', 'hires.png', 'pdf'], True) - app.add_config_value('plot_basedir', None, True) - app.add_config_value('plot_html_show_formats', True, True) - app.add_config_value('plot_rcparams', {}, True) - app.add_config_value('plot_apply_rcparams', False, True) - app.add_config_value('plot_working_directory', None, True) - app.add_config_value('plot_template', None, True) - - app.connect(str('doctree-read'), mark_plot_labels) - -#------------------------------------------------------------------------------ -# Doctest handling -#------------------------------------------------------------------------------ - -def contains_doctest(text): - try: - # check if it's valid Python as-is - compile(text, '', 'exec') - return False - except SyntaxError: - pass - r = re.compile(r'^\s*>>>', re.M) - m = r.search(text) - return bool(m) - - -def unescape_doctest(text): - """ - Extract code from a piece of text, which contains either Python code - or doctests. - - """ - if not contains_doctest(text): - return text - - code = "" - for line in text.split("\n"): - m = re.match(r'^\s*(>>>|\.\.\.) (.*)$', line) - if m: - code += m.group(2) + "\n" - elif line.strip(): - code += "# " + line.strip() + "\n" - else: - code += "\n" - return code - - -def split_code_at_show(text): - """ - Split code at plt.show() - - """ - - parts = [] - is_doctest = contains_doctest(text) - - part = [] - for line in text.split("\n"): - if (not is_doctest and line.strip() == 'plt.show()') or \ - (is_doctest and line.strip() == '>>> plt.show()'): - part.append(line) - parts.append("\n".join(part)) - part = [] - else: - part.append(line) - if "\n".join(part).strip(): - parts.append("\n".join(part)) - return parts - - -def remove_coding(text): - """ - Remove the coding comment, which six.exec_ doesn't like. - """ - sub_re = re.compile("^#\s*-\*-\s*coding:\s*.*-\*-$", flags=re.MULTILINE) - return sub_re.sub("", text) - -#------------------------------------------------------------------------------ -# Template -#------------------------------------------------------------------------------ - - -TEMPLATE = """ -{{ source_code }} - -{{ only_html }} - - {% if source_link or (html_show_formats and not multi_image) %} - ( - {%- if source_link -%} - `Source code <{{ source_link }}>`__ - {%- endif -%} - {%- if html_show_formats and not multi_image -%} - {%- for img in images -%} - {%- for fmt in img.formats -%} - {%- if source_link or not loop.first -%}, {% endif -%} - `{{ fmt }} <{{ dest_dir }}/{{ img.basename }}.{{ fmt }}>`__ - {%- endfor -%} - {%- endfor -%} - {%- endif -%} - ) - {% endif %} - - {% for img in images %} - .. figure:: {{ build_dir }}/{{ img.basename }}.png - {% for option in options -%} - {{ option }} - {% endfor %} - - {% if html_show_formats and multi_image -%} - ( - {%- for fmt in img.formats -%} - {%- if not loop.first -%}, {% endif -%} - `{{ fmt }} <{{ dest_dir }}/{{ img.basename }}.{{ fmt }}>`__ - {%- endfor -%} - ) - {%- endif -%} - - {{ caption }} - {% endfor %} - -{{ only_latex }} - - {% for img in images %} - {% if 'pdf' in img.formats -%} - .. image:: {{ build_dir }}/{{ img.basename }}.pdf - {% endif -%} - {% endfor %} - -{{ only_texinfo }} - - {% for img in images %} - .. image:: {{ build_dir }}/{{ img.basename }}.png - {% for option in options -%} - {{ option }} - {% endfor %} - - {% endfor %} - -""" - -exception_template = """ -.. htmlonly:: - - [`source code <%(linkdir)s/%(basename)s.py>`__] - -Exception occurred rendering plot. - -""" - -# the context of the plot for all directives specified with the -# :context: option -plot_context = dict() - -class ImageFile(object): - def __init__(self, basename, dirname): - self.basename = basename - self.dirname = dirname - self.formats = [] - - def filename(self, format): - return os.path.join(self.dirname, "%s.%s" % (self.basename, format)) - - def filenames(self): - return [self.filename(fmt) for fmt in self.formats] - - -def out_of_date(original, derived): - """ - Return True if derivative is out-of-date wrt original, - both of which are full file paths. - """ - return (not os.path.exists(derived) or - (os.path.exists(original) and - os.stat(derived).st_mtime < os.stat(original).st_mtime)) - - -class PlotError(RuntimeError): - pass - - -def run_code(code, code_path, ns=None, function_name=None): - """ - Import a Python module from a path, and run the function given by - name, if function_name is not None. - """ - - # Change the working directory to the directory of the example, so - # it can get at its data files, if any. Add its path to sys.path - # so it can import any helper modules sitting beside it. - if six.PY2: - pwd = os.getcwdu() - else: - pwd = os.getcwd() - old_sys_path = list(sys.path) - if setup.config.plot_working_directory is not None: - try: - os.chdir(setup.config.plot_working_directory) - except OSError as err: - raise OSError(str(err) + '\n`plot_working_directory` option in' - 'Sphinx configuration file must be a valid ' - 'directory path') - except TypeError as err: - raise TypeError(str(err) + '\n`plot_working_directory` option in ' - 'Sphinx configuration file must be a string or ' - 'None') - sys.path.insert(0, setup.config.plot_working_directory) - elif code_path is not None: - dirname = os.path.abspath(os.path.dirname(code_path)) - os.chdir(dirname) - sys.path.insert(0, dirname) - - # Reset sys.argv - old_sys_argv = sys.argv - sys.argv = [code_path] - - # Redirect stdout - stdout = sys.stdout - if six.PY3: - sys.stdout = io.StringIO() - else: - sys.stdout = cStringIO.StringIO() - - # Assign a do-nothing print function to the namespace. There - # doesn't seem to be any other way to provide a way to (not) print - # that works correctly across Python 2 and 3. - def _dummy_print(*arg, **kwarg): - pass - - try: - try: - code = unescape_doctest(code) - if ns is None: - ns = {} - if not ns: - if setup.config.plot_pre_code is None: - six.exec_(six.text_type("import numpy as np\n" + - "from matplotlib import pyplot as plt\n"), ns) - else: - six.exec_(six.text_type(setup.config.plot_pre_code), ns) - ns['print'] = _dummy_print - if "__main__" in code: - six.exec_("__name__ = '__main__'", ns) - code = remove_coding(code) - six.exec_(code, ns) - if function_name is not None: - six.exec_(function_name + "()", ns) - except (Exception, SystemExit) as err: - raise PlotError(traceback.format_exc()) - finally: - os.chdir(pwd) - sys.argv = old_sys_argv - sys.path[:] = old_sys_path - sys.stdout = stdout - return ns - - -def clear_state(plot_rcparams, close=True): - if close: - plt.close('all') - matplotlib.rc_file_defaults() - matplotlib.rcParams.update(plot_rcparams) - - -def render_figures(code, code_path, output_dir, output_base, context, - function_name, config, context_reset=False, - close_figs=False): - """ - Run a pyplot script and save the low and high res PNGs and a PDF - in *output_dir*. - - Save the images under *output_dir* with file names derived from - *output_base* - """ - # -- Parse format list - default_dpi = {'png': 80, 'hires.png': 200, 'pdf': 200} - formats = [] - plot_formats = config.plot_formats - if isinstance(plot_formats, six.string_types): - # String Sphinx < 1.3, Split on , to mimic - # Sphinx 1.3 and later. Sphinx 1.3 always - # returns a list. - plot_formats = plot_formats.split(',') - for fmt in plot_formats: - if isinstance(fmt, six.string_types): - if ':' in fmt: - suffix,dpi = fmt.split(':') - formats.append((str(suffix), int(dpi))) - else: - formats.append((fmt, default_dpi.get(fmt, 80))) - elif type(fmt) in (tuple, list) and len(fmt)==2: - formats.append((str(fmt[0]), int(fmt[1]))) - else: - raise PlotError('invalid image format "%r" in plot_formats' % fmt) - - # -- Try to determine if all images already exist - - code_pieces = split_code_at_show(code) - - # Look for single-figure output files first - all_exists = True - img = ImageFile(output_base, output_dir) - for format, dpi in formats: - if out_of_date(code_path, img.filename(format)): - all_exists = False - break - img.formats.append(format) - - if all_exists: - return [(code, [img])] - - # Then look for multi-figure output files - results = [] - all_exists = True - n_shows = -1 - for code_piece in code_pieces: - if len(code_pieces) > 1: - if 'plt.show()' in code_piece: - n_shows += 1 - else: - # We don't want to inspect whether an image exists for a code - # piece without a show. - continue - - images = [] - for j in xrange(1000): - if len(code_pieces) > 1: - img = ImageFile('%s_%02d_%02d' % (output_base, n_shows, j), output_dir) - else: - img = ImageFile('%s_%02d' % (output_base, j), output_dir) - for format, dpi in formats: - if out_of_date(code_path, img.filename(format)): - all_exists = False - break - img.formats.append(format) - - # assume that if we have one, we have them all - if not all_exists: - all_exists = (j > 0) - break - images.append(img) - if not all_exists: - break - results.append((code_piece, images)) - - if all_exists: - return results - - # We didn't find the files, so build them - - results = [] - if context: - ns = plot_context - else: - ns = {} - - if context_reset: - clear_state(config.plot_rcparams) - plot_context.clear() - - close_figs = not context or close_figs - n_shows = -1 - for code_piece in code_pieces: - if len(code_pieces) > 1 and 'plt.show()' in code_piece: - n_shows += 1 - - if not context or config.plot_apply_rcparams: - clear_state(config.plot_rcparams, close_figs) - elif close_figs: - plt.close('all') - - run_code(code_piece, code_path, ns, function_name) - - images = [] - fig_managers = _pylab_helpers.Gcf.get_all_fig_managers() - for j, figman in enumerate(fig_managers): - if len(fig_managers) == 1 and len(code_pieces) == 1: - img = ImageFile(output_base, output_dir) - elif len(code_pieces) == 1: - img = ImageFile("%s_%02d" % (output_base, j), output_dir) - else: - img = ImageFile("%s_%02d_%02d" % (output_base, n_shows, j), - output_dir) - images.append(img) - for format, dpi in formats: - try: - figman.canvas.figure.savefig(img.filename(format), dpi=dpi) - except Exception as err: - raise PlotError(traceback.format_exc()) - img.formats.append(format) - - results.append((code_piece, images)) - - if not context or config.plot_apply_rcparams: - clear_state(config.plot_rcparams, close=not context) - - return results - - -def run(arguments, content, options, state_machine, state, lineno): - # The user may provide a filename *or* Python code content, but not both - if arguments and content: - raise RuntimeError("plot:: directive can't have both args and content") - - document = state_machine.document - config = document.settings.env.config - nofigs = 'nofigs' in options - - options.setdefault('include-source', config.plot_include_source) - keep_context = 'context' in options - context_opt = None if not keep_context else options['context'] - - rst_file = document.attributes['source'] - rst_dir = os.path.dirname(rst_file) - - if len(arguments): - if not config.plot_basedir: - source_file_name = os.path.join(setup.app.builder.srcdir, - directives.uri(arguments[0])) - else: - source_file_name = os.path.join(setup.confdir, config.plot_basedir, - directives.uri(arguments[0])) - - # If there is content, it will be passed as a caption. - caption = '\n'.join(content) - - # If the optional function name is provided, use it - if len(arguments) == 2: - function_name = arguments[1] - else: - function_name = None - - with io.open(source_file_name, 'r', encoding='utf-8') as fd: - code = fd.read() - output_base = os.path.basename(source_file_name) - else: - source_file_name = rst_file - code = textwrap.dedent("\n".join(map(str, content))) - counter = document.attributes.get('_plot_counter', 0) + 1 - document.attributes['_plot_counter'] = counter - base, ext = os.path.splitext(os.path.basename(source_file_name)) - output_base = '%s-%d.py' % (base, counter) - function_name = None - caption = '' - - base, source_ext = os.path.splitext(output_base) - if source_ext in ('.py', '.rst', '.txt'): - output_base = base - else: - source_ext = '' - - # ensure that LaTeX includegraphics doesn't choke in foo.bar.pdf filenames - output_base = output_base.replace('.', '-') - - # is it in doctest format? - is_doctest = contains_doctest(code) - if 'format' in options: - if options['format'] == 'python': - is_doctest = False - else: - is_doctest = True - - # determine output directory name fragment - source_rel_name = relpath(source_file_name, setup.confdir) - source_rel_dir = os.path.dirname(source_rel_name) - while source_rel_dir.startswith(os.path.sep): - source_rel_dir = source_rel_dir[1:] - - # build_dir: where to place output files (temporarily) - build_dir = os.path.join(os.path.dirname(setup.app.doctreedir), - 'plot_directive', - source_rel_dir) - # get rid of .. in paths, also changes pathsep - # see note in Python docs for warning about symbolic links on Windows. - # need to compare source and dest paths at end - build_dir = os.path.normpath(build_dir) - - if not os.path.exists(build_dir): - os.makedirs(build_dir) - - # output_dir: final location in the builder's directory - dest_dir = os.path.abspath(os.path.join(setup.app.builder.outdir, - source_rel_dir)) - if not os.path.exists(dest_dir): - os.makedirs(dest_dir) # no problem here for me, but just use built-ins - - # how to link to files from the RST file - dest_dir_link = os.path.join(relpath(setup.confdir, rst_dir), - source_rel_dir).replace(os.path.sep, '/') - try: - build_dir_link = relpath(build_dir, rst_dir).replace(os.path.sep, '/') - except ValueError: - # on Windows, relpath raises ValueError when path and start are on - # different mounts/drives - build_dir_link = build_dir - source_link = dest_dir_link + '/' + output_base + source_ext - - # make figures - try: - results = render_figures(code, - source_file_name, - build_dir, - output_base, - keep_context, - function_name, - config, - context_reset=context_opt == 'reset', - close_figs=context_opt == 'close-figs') - errors = [] - except PlotError as err: - reporter = state.memo.reporter - sm = reporter.system_message( - 2, "Exception occurred in plotting %s\n from %s:\n%s" % (output_base, - source_file_name, err), - line=lineno) - results = [(code, [])] - errors = [sm] - - # Properly indent the caption - caption = '\n'.join(' ' + line.strip() - for line in caption.split('\n')) - - # generate output restructuredtext - total_lines = [] - for j, (code_piece, images) in enumerate(results): - if options['include-source']: - if is_doctest: - lines = [''] - lines += [row.rstrip() for row in code_piece.split('\n')] - else: - lines = ['.. code-block:: python', ''] - lines += [' %s' % row.rstrip() - for row in code_piece.split('\n')] - source_code = "\n".join(lines) - else: - source_code = "" - - if nofigs: - images = [] - - opts = [':%s: %s' % (key, val) for key, val in six.iteritems(options) - if key in ('alt', 'height', 'width', 'scale', 'align', 'class')] - - only_html = ".. only:: html" - only_latex = ".. only:: latex" - only_texinfo = ".. only:: texinfo" - - # Not-None src_link signals the need for a source link in the generated - # html - if j == 0 and config.plot_html_show_source_link: - src_link = source_link - else: - src_link = None - - result = format_template( - config.plot_template or TEMPLATE, - dest_dir=dest_dir_link, - build_dir=build_dir_link, - source_link=src_link, - multi_image=len(images) > 1, - only_html=only_html, - only_latex=only_latex, - only_texinfo=only_texinfo, - options=opts, - images=images, - source_code=source_code, - html_show_formats=config.plot_html_show_formats and not nofigs, - caption=caption) - - total_lines.extend(result.split("\n")) - total_lines.extend("\n") - - if total_lines: - state_machine.insert_input(total_lines, source=source_file_name) - - # copy image files to builder's output directory, if necessary - if not os.path.exists(dest_dir): - cbook.mkdirs(dest_dir) - - for code_piece, images in results: - for img in images: - for fn in img.filenames(): - destimg = os.path.join(dest_dir, os.path.basename(fn)) - if fn != destimg: - shutil.copyfile(fn, destimg) - - # copy script (if necessary) - target_name = os.path.join(dest_dir, output_base + source_ext) - with io.open(target_name, 'w', encoding="utf-8") as f: - if source_file_name == rst_file: - code_escaped = unescape_doctest(code) - else: - code_escaped = code - f.write(code_escaped) - - return errors diff -Nru python-cartopy-0.17.0+dfsg/docs/source/sphinxext/pre_sphinx_gallery.py python-cartopy-0.18.0+dfsg/docs/source/sphinxext/pre_sphinx_gallery.py --- python-cartopy-0.17.0+dfsg/docs/source/sphinxext/pre_sphinx_gallery.py 2018-11-17 07:25:32.000000000 +0000 +++ python-cartopy-0.18.0+dfsg/docs/source/sphinxext/pre_sphinx_gallery.py 2020-05-03 08:12:47.000000000 +0000 @@ -37,8 +37,6 @@ """ from collections import OrderedDict, defaultdict import os.path -import shutil -import tempfile import textwrap import sphinx_gallery @@ -68,6 +66,9 @@ """) +# Directory to place examples, determined from Sphinx configuration. +outdir = None + def example_groups(src_dir): """Return a dictionary of {tag: [example filenames]} for the given dir.""" @@ -136,7 +137,8 @@ build_target_dir = os.path.relpath(target_dir, gallery_conf['src_dir']) seen = set() - tmp_dir = tempfile.mkdtemp() + if not os.path.exists(outdir): + os.makedirs(outdir) for tag, examples in tagged_examples.items(): sorted_listdir = sorted( @@ -149,9 +151,9 @@ 'Generating gallery for %s ' % tag, length=len(sorted_listdir)) for fname in iterator: - write_example(os.path.join(src_dir, fname), tmp_dir) + write_example(os.path.join(src_dir, fname), outdir) intro, time_elapsed = generate_file_rst( - fname, target_dir, tmp_dir, gallery_conf) + fname, target_dir, outdir, gallery_conf) if fname not in seen: seen.add(fname) @@ -188,9 +190,6 @@ fhindex += """.. raw:: html\n
\n\n""" - # Tidy up the temp directory - shutil.rmtree(tmp_dir) - return fhindex, computation_times @@ -200,4 +199,5 @@ def setup(app): - pass + global outdir + outdir = os.path.join(app.srcdir, 'cartopy', 'examples') diff -Nru python-cartopy-0.17.0+dfsg/docs/source/_static/copyright_license.csv python-cartopy-0.18.0+dfsg/docs/source/_static/copyright_license.csv --- python-cartopy-0.17.0+dfsg/docs/source/_static/copyright_license.csv 2018-11-17 07:25:32.000000000 +0000 +++ python-cartopy-0.18.0+dfsg/docs/source/_static/copyright_license.csv 2020-05-03 08:12:47.000000000 +0000 @@ -1,3 +1,3 @@ Data Provider;Cartopy Function;Citation (short);Citation (long);License;Terms of Use Information -OpenStreetMap;:mod:`img_tiles.OSM `;|copy| OpenStreetMap;Map data |copy| OpenStreetMap contributors;`Open Database Licence `_;`Legal FAQ `_ -Natural Earth raster + vector map data;:class:`NaturalEarthFeature `;Made with Natural Earth.;Made with Natural Earth. Free vector and raster map data @ naturalearthdata.com.;`Public Domain `_;`Terms of Use `_ +OpenStreetMap;:mod:`img_tiles.OSM `;|copy| OpenStreetMap;Map data |copy| OpenStreetMap contributors;`Open Database Licence `_;`Legal FAQ `_ +Natural Earth raster + vector map data;:class:`NaturalEarthFeature `;Made with Natural Earth.;Made with Natural Earth. Free vector and raster map data @ naturalearthdata.com.;`Public Domain `_;`Terms of Use `_ diff -Nru python-cartopy-0.17.0+dfsg/docs/source/_static/version_switch.js python-cartopy-0.18.0+dfsg/docs/source/_static/version_switch.js --- python-cartopy-0.17.0+dfsg/docs/source/_static/version_switch.js 2018-11-17 07:25:32.000000000 +0000 +++ python-cartopy-0.18.0+dfsg/docs/source/_static/version_switch.js 2020-05-03 08:12:47.000000000 +0000 @@ -5,7 +5,10 @@ 'use strict'; var all_versions = { - 'latest': '0.15', + 'latest': '0.18', + 'v0.17': '0.17', + 'v0.16': '0.16', + 'v0.15': '0.15', 'v0.14': '0.14', 'v0.13': '0.13', 'v0.12': '0.12', @@ -58,7 +61,7 @@ window.location.href = new_url; }, error: function() { - window.location.href = 'http://scitools.org.uk/cartopy/docs/' + selected; + window.location.href = 'https://scitools.org.uk/cartopy/docs/' + selected; } }); } diff -Nru python-cartopy-0.17.0+dfsg/docs/source/_templates/layout.html python-cartopy-0.18.0+dfsg/docs/source/_templates/layout.html --- python-cartopy-0.17.0+dfsg/docs/source/_templates/layout.html 2018-11-17 07:25:32.000000000 +0000 +++ python-cartopy-0.18.0+dfsg/docs/source/_templates/layout.html 2020-05-03 08:12:47.000000000 +0000 @@ -29,7 +29,7 @@ {% block sidebarlogo %} Cartopy +border="0" alt="Cartopy"/> {% endblock %} @@ -51,6 +51,6 @@ - + {% endblock %} diff -Nru python-cartopy-0.17.0+dfsg/docs/source/tutorials/using_the_shapereader.rst python-cartopy-0.18.0+dfsg/docs/source/tutorials/using_the_shapereader.rst --- python-cartopy-0.17.0+dfsg/docs/source/tutorials/using_the_shapereader.rst 2018-11-17 07:25:32.000000000 +0000 +++ python-cartopy-0.18.0+dfsg/docs/source/tutorials/using_the_shapereader.rst 2020-05-03 08:12:47.000000000 +0000 @@ -11,7 +11,7 @@ `GeoPandas`_ are highly recommended. .. _pyshp: https://github.com/GeospatialPython/pyshp -.. _Fiona: http://toblerity.org/fiona/ +.. _Fiona: https://fiona.readthedocs.io/ .. _GeoPandas: http://geopandas.org/ @@ -33,7 +33,7 @@ Cartopy provides an interface for access to frequently used data such as the `GSHHS `_ dataset and from -the `NaturalEarthData `_ website. +the `NaturalEarthData `_ website. These interfaces allow the user to define the data programmatically, and if the data does not exist on disk, it will be retrieved from the appropriate source (normally by downloading the data from the internet). Currently the interfaces available are: @@ -48,7 +48,7 @@ --------------------- We can acquire the countries dataset from Natural Earth found at -http://www.naturalearthdata.com/downloads/110m-cultural-vectors/110m-admin-0-countries/ +https://www.naturalearthdata.com/downloads/110m-cultural-vectors/110m-admin-0-countries/ by using the :func:`natural_earth` function: diff -Nru python-cartopy-0.17.0+dfsg/docs/source/whats_new.rst python-cartopy-0.18.0+dfsg/docs/source/whats_new.rst --- python-cartopy-0.17.0+dfsg/docs/source/whats_new.rst 2018-11-17 07:25:32.000000000 +0000 +++ python-cartopy-0.18.0+dfsg/docs/source/whats_new.rst 2020-05-03 08:12:47.000000000 +0000 @@ -1,3 +1,127 @@ +What's New in cartopy 0.18 +========================== + +:Release: 0.18.0 +:Date: 3rd May 2020 + +For a full list of included Pull Requests and closed Issues, please see the +`0.18 milestone `_. + +Features +-------- + +* We are very pleased to announce that Greg Lucas has been added to the cartopy + core development team. Greg (@greglucas) added the NightShade feature in the + previous release, and has been instrumental in issue and PR triage leading up + to 0.18. He has also ensured that CI systems have kept working through + various upstream project changes. + +* Kevin Donkers and Phil Elson made the AdaptiveScalar the default for Natural + Earth Features. This will make the default features look much nicer when + plotting on zoomed in axes. (:pull:`1105`) + +* Elliott Sales de Andrade added support for Matplotlib 3.2 and 3.3 + (:pull:`1425`) and Python 3.7 and 3.8 (:pull:`1428`). + +* Alan Snow added the ability to use Proj version 6.x (:pull:`1289`) and + Elliott Sales de Andrade updated a lot of the tests and build issues for this + upgrade (:pull:`1417`). + +* Andrew Huang added the ability to put the meridian and parallel gridline + labels on the gridlines within the plot boundaries rather than + only as labels on the boundary. (:pull:`1089`) + + .. plot:: + :width: 400pt + + import matplotlib.pyplot as plt + import cartopy.crs as ccrs + + fig = plt.figure(figsize=(10, 5)) + ax = plt.axes(projection=ccrs.PlateCarree()) + ax.set_global() + ax.stock_img() + ax.coastlines() + ax.gridlines(x_inline=True, draw_labels=True) + plt.show() + +* Stephane Raynaud added longitude and latitude labeling to all projections. It + was previously restricted to the Mercator and PlateCarree projections. + (:pull:`1117`) + + .. plot:: + :width: 400pt + + import matplotlib.pyplot as plt + import cartopy.crs as ccrs + + fig = plt.figure(figsize=(10, 5)) + ax = plt.axes(projection=ccrs.InterruptedGoodeHomolosine()) + ax.set_global() + ax.stock_img() + ax.coastlines() + ax.gridlines(draw_labels=True) + plt.show() + +* Phil Elson added the (long awaited!) ability to label contours on GeoAxes. A + :ref:`sphx_glr_gallery_contour_labels.py` example has been added to the + gallery demonstrating the new capability. (:pull:`1257`) + + .. figure:: _images/sphx_glr_contour_labels_001.png + :target: gallery/contour_labels.html + :align: center + +* Matthew Bradbury added the ability to query `UK Ordnance Survey + `_ image tiles. (:pull:`1214`) + +* Phil Elson added the ability to fetch image tiles using multiple + threads. (:pull:`1232`) + +* Elliott Sales de Andrade added a + :class:`cartopy.mpl.geoaxes.GeoAxes.GeoSpine` class to replace the + :attr:`cartopy.mpl.geoaxes.GeoAxes.outline_patch` that defines the map + boundary. (:pull:`1213`) + +* Elliott Sales de Andrade improved appearance of plots with tight layout. + (:pull:`1213` and :pull:`1422`) + +* Ryan May fixed the Geostationary projection boundary so that geometries + no longer extend beyond the map domain. (:pull:`1216`) + +* Phil Elson added support for style composition of Features. This means that + the styles set on a Feature when it is created, and when it is added to an + Axes, will be processed consistently. + +Deprecations +------------ +* This will be the last release with Python 2 support. + +* The default value for the ``origin`` argument to + :func:`cartopy.mpl.geoaxes.GeoAxes.imshow` is now ``'upper'`` to match the + default in Matplotlib. + +* The :attr:`cartopy.mpl.geoaxes.GeoAxes.outline_patch` attribute is + deprecated. In its place, use Matplotlib's standard options for controlling + the Axes frame, or access ``GeoAxes.spines['geo']`` directly. + +* The :attr:`cartopy.mpl.geoaxes.GeoAxes.background_patch` attribute is + deprecated. In its place, use Matplotlib's standard options for controlling + the Axes patch, i.e., pass values to the constructor or access + ``GeoAxes.patch`` directly. + +* The gridliner labelling options + :attr:`cartopy.mpl.gridliner.Gridliner.xlabels_top`, + :attr:`cartopy.mpl.gridliner.Gridliner.xlabels_bottom`, + :attr:`cartopy.mpl.gridliner.Gridliner.ylabels_left`, and + :attr:`cartopy.mpl.gridliner.Gridliner.ylabels_right` are deprecated. + Instead, use :attr:`cartopy.mpl.gridliner.Gridliner.top_labels`, + :attr:`cartopy.mpl.gridliner.Gridliner.bottom_labels`, + :attr:`cartopy.mpl.gridliner.Gridliner.left_labels`, or + :attr:`cartopy.mpl.gridliner.Gridliner.right_labels`. + +-------- + + What's New in cartopy 0.17 ========================== @@ -76,7 +200,7 @@ render vector images of the coastlines using a given projection to enable a quick preview. (:pull:`951`, :pull:`1196`) -* Fixes were added by Elliott Sales de Andrade to support the matplotlib 3.x +* Fixes were added by Elliott Sales de Andrade to support the Matplotlib 3.x series. (:pull:`1130`) * Ryan May fixed up the `.Geostationary` and `.NearsidePerspective` projections @@ -153,11 +277,11 @@ * In CartoPy 0.18, the default value for the ``origin`` argument to :func:`cartopy.mpl.geoaxes.GeoAxes.imshow` will change from ``'lower'`` - to ``'upper'`` to match the default in matplotlib. + to ``'upper'`` to match the default in Matplotlib. Incompatible Changes -------------------- -* Support for matplotlib < 1.5.1 and NumPy < 1.10 has been removed. +* Support for Matplotlib < 1.5.1 and NumPy < 1.10 has been removed. -------- @@ -280,11 +404,11 @@ CARTOPY_USER_BACKGROUNDS environment variable. * The Web Map Tile Service (WMTS) interface has been extended so that WMTS - layers can be added to geoaxes in different projections. + layers can be added to GeoAxes in different projections. * The :class:`~cartopy.crs.NearsidePerspective` projection has been added. -* Optional kwargs can now be supplied to the +* Optional keyword arguments can now be supplied to the :meth:`~cartopy.mpl.geoaxes.GeoAxes.add_wmts` method, which will be passed to the OGC WMTS ``gettile`` method. @@ -573,7 +697,7 @@ Hattersley to provide interactive pan and zoom OGC web services support for a Web Map Tile Service (WMTS) aware axes, which is available through the :meth:`~cartopy.mpl.geoaxes.GeoAxes.add_wmts` method. This includes support - for the Google Mercator projection and efficient WTMS tile caching. This new + for the Google Mercator projection and efficient WMTS tile caching. This new capability determines how to match up the available tiles projections with the target projection and chooses the zoom level to best match the pixel density in the rendered image. @@ -811,7 +935,7 @@ Feature API ----------- -A new features api is now available, see :doc:`tutorials/using_the_shapereader`. +A new features API is now available, see :doc:`tutorials/using_the_shapereader`. .. figure:: gallery/images/sphx_glr_features_001.png :target: gallery/features.html diff -Nru python-cartopy-0.17.0+dfsg/.github/CONTRIBUTING.md python-cartopy-0.18.0+dfsg/.github/CONTRIBUTING.md --- python-cartopy-0.17.0+dfsg/.github/CONTRIBUTING.md 1970-01-01 00:00:00.000000000 +0000 +++ python-cartopy-0.18.0+dfsg/.github/CONTRIBUTING.md 2020-05-03 08:12:47.000000000 +0000 @@ -0,0 +1,32 @@ +How to contribute +================= + +Cartopy is driven by a community of people all passionate about +seeing cartopy flourish as a world class mapping package for Python. +This page lists the guidelines for contributors which +will help ease the process of getting your hard work accepted back into +the cartopy repository. + + +Getting started +--------------- + +1. If you've not already got one, sign up for a + [GitHub account](https://github.com/signup/free). +1. Fork the Cartopy repository, create your new fix/feature branch, and + start committing code. We broadly follow the [gitwash guidelines](https://matthew-brett.github.io/pydagogue/gitwash/git_development.html). +1. Remember to add appropriate documentation and tests to supplement any new or changed functionality. +1. If you're not already on it (and would like to be), please add yourself to the + contributors list (docs/source/contributors.rst) + + +Submitting changes +------------------ + +1. Read and sign the Contributor Licence Agreement (CLA) if you have not already done so. + - See the [governance page](http://scitools.org.uk/governance.html) + for the CLA and what to do with it. +1. Push your branch to your fork of cartopy. +1. Submit your pull request. +1. Sit back and wait for the core Cartopy development team to review your code. + diff -Nru python-cartopy-0.17.0+dfsg/.github/workflows/circleci.yml python-cartopy-0.18.0+dfsg/.github/workflows/circleci.yml --- python-cartopy-0.17.0+dfsg/.github/workflows/circleci.yml 1970-01-01 00:00:00.000000000 +0000 +++ python-cartopy-0.18.0+dfsg/.github/workflows/circleci.yml 2020-05-03 08:12:47.000000000 +0000 @@ -0,0 +1,12 @@ +on: [status] +jobs: + circleci_artifacts_redirector_job: + runs-on: ubuntu-latest + name: Run CircleCI artifacts redirector + steps: + - name: GitHub Action step + uses: larsoner/circleci-artifacts-redirector-action@master + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + artifact-path: 0/docs/build/html/index.html + circleci-jobs: docs-python2,docs-python3 diff -Nru python-cartopy-0.17.0+dfsg/.gitignore python-cartopy-0.18.0+dfsg/.gitignore --- python-cartopy-0.17.0+dfsg/.gitignore 2018-11-17 07:25:32.000000000 +0000 +++ python-cartopy-0.18.0+dfsg/.gitignore 2020-05-03 08:12:47.000000000 +0000 @@ -20,6 +20,7 @@ lib/cartopy/geodesic/_geodesic.pyd docs/build lib/cartopy/tests/mpl/output/ +docs/source/cartopy/examples/ docs/source/cartopy_outline.rst docs/source/gallery docs/source/matplotlib/coastlines.p?? @@ -35,6 +36,10 @@ cartopy_test_output + +benchmarks/results/ +benchmarks/envs/ + # pydev files .project .pydevproject diff -Nru python-cartopy-0.17.0+dfsg/INSTALL python-cartopy-0.18.0+dfsg/INSTALL --- python-cartopy-0.17.0+dfsg/INSTALL 2018-11-17 07:25:32.000000000 +0000 +++ python-cartopy-0.18.0+dfsg/INSTALL 2020-05-03 08:12:47.000000000 +0000 @@ -1,17 +1,20 @@ Installing Cartopy ================== -Pre-built binaries ------------------- +Conda pre-built binaries +------------------------ -The easiest route to installing cartopy is through -`Conda `_. For all platforms -installing cartopy can be done with:: +The easiest way to install Cartopy is by using +`Conda `_. If conda is already installed, +installation is as easy as:: conda install -c conda-forge cartopy -Additional options include: - * `Enthought Canopy `_. + +Other pre-built binaries +------------------------ + +Additional pre-built binaries can be found at a variety of sources, including: * Christoph Gohlke (https://www.lfd.uci.edu/~gohlke/pythonlibs/) maintains unofficial Windows binaries of cartopy. * `OSGeo Live `_. @@ -20,45 +23,55 @@ Building from source -------------------- -The latest release of Cartopy is available from -https://github.com/SciTools/Cartopy. +Before building Cartopy from source, you need to **first** install the +required dependencies listed below. Once these are installed, Cartopy can be +installed using the pip installer:: -Once you have satisfied the requirements detailed below, simply run:: + pip install cartopy - python setup.py install +To instead install the most recent version found on the GitHub master branch, +use:: -For non-standard locations, additional build lib & include paths -can be provided as per-usual at build_ext phase:: + pip install git+https://github.com/SciTools/cartopy.git - python setup.py build_ext -I/path/to/include -L/path/to/lib - python setup.py install +Alternatively, you can clone the git repo on your computer and install manually +using the `setup.py` file:: + git clone https://github.com/SciTools/cartopy.git + cd cartopy + # Uncomment the following to specify non-standard include and library paths + # python setup.py build_ext -I/path/to/include -L/path/to/lib + python setup.py install -Requirements -~~~~~~~~~~~~ -These external packages are required to install Cartopy or gain access to -significant Cartopy functionality. - -Many of these packages are available in Linux package managers -such as aptitude and yum. For example, it may be possible to install -Numpy using:: - - apt-get install python-numpy - -If you are installing dependencies with a package manager on Linux, -you may need to install the development packages (look for a "-dev" -suffix) in addition to the core packages. -Many of these dependencies are built as part of Cartopy's conda distribution. -The recipes for these can be found at https://github.com/conda-forge/feedstocks. +Required dependencies +~~~~~~~~~~~~~~~~~~~~~ +In order to install Cartopy, or to access its basic functionality, it will be +necessary to first install **GEOS**, **NumPy**, **Cython**, **Shapely**, +**pyshp** and **six**. Many of these packages can be installed using pip or +other package managers such as apt-get (Linux) and brew (macOS). Many of these +dependencies are built as part of Cartopy's conda distribution, and the recipes +for these packages can be found at https://github.com/conda-forge/feedstocks. + +For macOS, the required dependencies can be installed in the following way:: + + brew install proj geos + pip3 install --upgrade cython numpy pyshp six + # shapely needs to be built from source to link to geos. If it is already + # installed, uninstall it by: pip3 uninstall shapely + pip3 install shapely --no-binary shapely + +If you are installing dependencies with a package manager on Linux, you may +need to install the development packages (look for a "-dev" or "-devel" suffix) in addition +to the core packages. +Further information about the required dependencies can be found here: **Python** 2.7 or later (https://www.python.org/) - Cartopy requires Python 2.7 or later. -**Cython** 0.15.1 or later (https://pypi.python.org/pypi/Cython/) +**Cython** 0.28 or later (https://pypi.python.org/pypi/Cython/) -**NumPy** 1.10 or later (http://www.numpy.org/) +**NumPy** 1.10 or later (https://numpy.org/) Python package for scientific computing including a powerful N-dimensional array object. @@ -70,7 +83,7 @@ Python package for the manipulation and analysis of planar geometric objects. -**pyshp** 1.1.4 or later (https://pypi.python.org/pypi/pyshp) +**pyshp** 2.0 or later (https://pypi.python.org/pypi/pyshp) Pure Python read/write support for ESRI Shapefile format. **PROJ** 4.9.0 or later (https://proj4.org/) @@ -79,39 +92,39 @@ **six** 1.3.0 or later (https://pypi.python.org/pypi/six) Python 2 and 3 compatibility. - Optional Dependencies ~~~~~~~~~~~~~~~~~~~~~ -These are optional packages which you may want to install to enable -additional Cartopy functionality. +To make the most of Cartopy by enabling additional functionality, you may want +to install these optional dependencies. **Matplotlib** 1.5.1 or later (https://matplotlib.org/) - Python package for 2D plotting. This package is required for any - graphical capability. + Python package for 2D plotting. Python package required for any + graphical capabilities. -**GDAL** version 1.10.0 (http://www.gdal.org/) +**GDAL** version 1.10.0 (https://gdal.org/) GDAL is a translator library for raster and vector geospatial data formats, which has powerful data transformation and processing capabilities. **Pillow** 1.7.8 or later (https://pypi.python.org/pypi/Pillow/2.3.0) - Popular fork of PythonImagingLibrary. + A popular fork of PythonImagingLibrary. -**pyepsg** 0.2.0 or later (https://github.com/rhattersley/pyepsg) +**pyepsg** 0.4.0 or later (https://github.com/rhattersley/pyepsg) A simple Python interface to https://epsg.io **pykdtree** 1.2.2 or later (https://github.com/storpipfugl/pykdtree) - Fast kd-tree implementation in Python; used for faster warping of images in - preference to SciPy. + A fast kd-tree implementation that is used for faster warping + of images than SciPy. **SciPy** 0.10 or later (https://www.scipy.org/) - Python package for scientific computing. + A Python package for scientific computing. **OWSLib** 0.8.7 (https://pypi.python.org/pypi/OWSLib) - Python package for client programming with Open Geospatial Consortium - (OGC) web service. Gives access to cartopy ogc clients. + A Python package for client programming with the Open Geospatial + Consortium (OGC) web service, and which gives access to Cartopy ogc + clients. **Fiona** 1.0 or later (https://github.com/Toblerity/Fiona) - Python package for reading shapefiles faster than the default (pyshp). + A Python package for reading shapefiles that is faster than pyshp. Testing Dependencies diff -Nru python-cartopy-0.17.0+dfsg/lib/cartopy/_crs.pxd python-cartopy-0.18.0+dfsg/lib/cartopy/_crs.pxd --- python-cartopy-0.17.0+dfsg/lib/cartopy/_crs.pxd 2018-11-17 07:25:32.000000000 +0000 +++ python-cartopy-0.18.0+dfsg/lib/cartopy/_crs.pxd 2020-05-03 08:12:47.000000000 +0000 @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2010 - 2016, Met Office +# (C) British Crown Copyright 2010 - 2018, Met Office # # This file is part of cartopy. # @@ -16,8 +16,7 @@ # along with cartopy. If not, see . -cdef extern from "proj_api.h": - ctypedef void *projPJ +from ._proj4 cimport projPJ cdef class CRS: diff -Nru python-cartopy-0.17.0+dfsg/lib/cartopy/crs.py python-cartopy-0.18.0+dfsg/lib/cartopy/crs.py --- python-cartopy-0.17.0+dfsg/lib/cartopy/crs.py 2018-11-17 07:25:32.000000000 +0000 +++ python-cartopy-0.18.0+dfsg/lib/cartopy/crs.py 2020-05-03 08:12:47.000000000 +0000 @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2011 - 2018, Met Office +# (C) British Crown Copyright 2011 - 2019, Met Office # # This file is part of cartopy. # @@ -33,7 +33,8 @@ from shapely.prepared import prep import six -from cartopy._crs import CRS, Geodetic, Globe, PROJ4_VERSION +from cartopy._crs import (CRS, Geodetic, Globe, PROJ4_VERSION, + WGS84_SEMIMAJOR_AXIS, WGS84_SEMIMINOR_AXIS) from cartopy._crs import Geocentric # noqa: F401 (flake8 = unused import) import cartopy.trace @@ -41,10 +42,6 @@ __document_these__ = ['CRS', 'Geocentric', 'Geodetic', 'Globe'] -WGS84_SEMIMAJOR_AXIS = 6378137.0 -WGS84_SEMIMINOR_AXIS = 6356752.3142 - - class RotatedGeodetic(CRS): """ Define a rotated latitude/longitude coordinate system with spherical @@ -157,7 +154,10 @@ return minlon, maxlon def _repr_html_(self): - import cgi + if not six.PY2: + from html import escape + else: + from cgi import escape try: # As matplotlib is not a core cartopy dependency, don't error # if it's not available. @@ -179,7 +179,7 @@ # "Rewind" the buffer to the start and return it as an svg string. buf.seek(0) svg = buf.read() - return '{}
{}
'.format(svg, cgi.escape(repr(self))) + return '{}
{}
'.format(svg, escape(repr(self))) def _as_mpl_axes(self): import cartopy.mpl.geoaxes as geoaxes @@ -666,9 +666,8 @@ @property def boundary(self): - # XXX Should this be a LinearRing? w, h = self._half_width, self._half_height - return sgeom.LineString([(-w, -h), (-w, h), (w, h), (w, -h), (-w, -h)]) + return sgeom.LinearRing([(-w, -h), (-w, h), (w, h), (w, -h), (-w, -h)]) @property def x_limits(self): @@ -807,7 +806,7 @@ """ def __init__(self, central_longitude=0.0, central_latitude=0.0, false_easting=0.0, false_northing=0.0, - scale_factor=1.0, globe=None): + scale_factor=1.0, globe=None, approx=None): """ Parameters ---------- @@ -822,15 +821,33 @@ Y offset from the planar origin in metres. Defaults to 0. scale_factor: optional Scale factor at the central meridian. Defaults to 1. + globe: optional An instance of :class:`cartopy.crs.Globe`. If omitted, a default globe is created. + approx: optional + Whether to use Proj's approximate projection (True), or the new + Extended Transverse Mercator code (False). Defaults to True, but + will change to False in the next release. + """ + if approx is None: + warnings.warn('The default value for the *approx* keyword ' + 'argument to TransverseMercator will change ' + 'from True to False after 0.18.', + stacklevel=2) + approx = True proj4_params = [('proj', 'tmerc'), ('lon_0', central_longitude), ('lat_0', central_latitude), ('k', scale_factor), ('x_0', false_easting), ('y_0', false_northing), ('units', 'm')] + if PROJ4_VERSION < (6, 0, 0): + if not approx: + proj4_params[0] = ('proj', 'etmerc') + else: + if approx: + proj4_params += [('approx', None)] super(TransverseMercator, self).__init__(proj4_params, globe=globe) @property @@ -841,7 +858,7 @@ def boundary(self): x0, x1 = self.x_limits y0, y1 = self.y_limits - return sgeom.LineString([(x0, y0), (x0, y1), + return sgeom.LinearRing([(x0, y0), (x0, y1), (x1, y1), (x1, y0), (x0, y0)]) @@ -855,18 +872,25 @@ class OSGB(TransverseMercator): - def __init__(self): + def __init__(self, approx=None): + if approx is None: + warnings.warn('The default value for the *approx* keyword ' + 'argument to OSGB will change from True to ' + 'False after 0.18.', + stacklevel=2) + approx = True super(OSGB, self).__init__(central_longitude=-2, central_latitude=49, scale_factor=0.9996012717, false_easting=400000, false_northing=-100000, - globe=Globe(datum='OSGB36', ellipse='airy')) + globe=Globe(datum='OSGB36', ellipse='airy'), + approx=approx) @property def boundary(self): w = self.x_limits[1] - self.x_limits[0] h = self.y_limits[1] - self.y_limits[0] - return sgeom.LineString([(0, 0), (0, h), (w, h), (w, 0), (0, 0)]) + return sgeom.LinearRing([(0, 0), (0, h), (w, h), (w, 0), (0, 0)]) @property def x_limits(self): @@ -878,7 +902,13 @@ class OSNI(TransverseMercator): - def __init__(self): + def __init__(self, approx=None): + if approx is None: + warnings.warn('The default value for the *approx* keyword ' + 'argument to OSNI will change from True to ' + 'False after 0.18.', + stacklevel=2) + approx = True globe = Globe(semimajor_axis=6377340.189, semiminor_axis=6356034.447938534) super(OSNI, self).__init__(central_longitude=-8, @@ -886,13 +916,14 @@ scale_factor=1.000035, false_easting=200000, false_northing=250000, - globe=globe) + globe=globe, + approx=approx) @property def boundary(self): w = self.x_limits[1] - self.x_limits[0] h = self.y_limits[1] - self.y_limits[0] - return sgeom.LineString([(0, 0), (0, h), (w, h), (w, 0), (0, 0)]) + return sgeom.LinearRing([(0, 0), (0, h), (w, h), (w, 0), (0, 0)]) @property def x_limits(self): @@ -933,7 +964,7 @@ def boundary(self): x0, x1 = self.x_limits y0, y1 = self.y_limits - return sgeom.LineString([(x0, y0), (x0, y1), + return sgeom.LinearRing([(x0, y0), (x0, y1), (x1, y1), (x1, y0), (x0, y0)]) @@ -1062,7 +1093,7 @@ def boundary(self): x0, x1 = self.x_limits y0, y1 = self.y_limits - return sgeom.LineString([(x0, y0), (x0, y1), + return sgeom.LinearRing([(x0, y0), (x0, y1), (x1, y1), (x1, y0), (x0, y0)]) @@ -1144,7 +1175,9 @@ elif secant_latitudes is not None: warnings.warn('secant_latitudes has been deprecated in v0.12. ' 'The standard_parallels keyword can be used as a ' - 'direct replacement.') + 'direct replacement.', + DeprecationWarning, + stacklevel=2) standard_parallels = secant_latitudes elif standard_parallels is None: # The default. Put this as a keyword arg default once @@ -1181,7 +1214,7 @@ self.cutoff = cutoff n = 91 lons = np.empty(n + 2) - lats = np.full(n + 2, cutoff) + lats = np.full(n + 2, float(cutoff)) lons[0] = lons[-1] = 0 lats[0] = lats[-1] = plat if plat == 90: @@ -1299,17 +1332,14 @@ class Miller(_RectangularProjection): + _handles_ellipses = False + def __init__(self, central_longitude=0.0, globe=None): if globe is None: globe = Globe(semimajor_axis=math.degrees(1), ellipse=None) # TODO: Let the globe return the semimajor axis always. a = np.float(globe.semimajor_axis or WGS84_SEMIMAJOR_AXIS) - b = np.float(globe.semiminor_axis or a) - - if b != a or globe.ellipse is not None: - warnings.warn('The proj "mill" projection does not handle ' - 'elliptical globes.') proj4_params = [('proj', 'mill'), ('lon_0', central_longitude)] # See Snyder, 1987. Eqs (11-1) and (11-2) substituting maximums of @@ -1370,6 +1400,8 @@ class Gnomonic(Projection): + _handles_ellipses = False + def __init__(self, central_latitude=0.0, central_longitude=0.0, globe=None): proj4_params = [('proj', 'gnom'), ('lat_0', central_latitude), @@ -1409,12 +1441,14 @@ 'The Stereographic projection in Proj older than ' '5.0.0 incorrectly transforms points when ' 'central_latitude=0. Use this projection with ' - 'caution.') + 'caution.', + stacklevel=2) else: warnings.warn( 'Cannot determine Proj version. The Stereographic ' 'projection may be unreliable and should be used with ' - 'caution.') + 'caution.', + stacklevel=2) proj4_params = [('proj', 'stere'), ('lat_0', central_latitude), ('lon_0', central_longitude), @@ -1424,7 +1458,8 @@ if central_latitude not in (-90., 90.): warnings.warn('"true_scale_latitude" parameter is only used ' 'for polar stereographic projections. Consider ' - 'the use of "scale_factor" instead.') + 'the use of "scale_factor" instead.', + stacklevel=2) proj4_params.append(('lat_ts', true_scale_latitude)) if scale_factor is not None: @@ -1493,18 +1528,22 @@ class Orthographic(Projection): + _handles_ellipses = False + def __init__(self, central_longitude=0.0, central_latitude=0.0, globe=None): if PROJ4_VERSION != (): if (5, 0, 0) <= PROJ4_VERSION < (5, 1, 0): warnings.warn( - 'The Orthographic projection in Proj between 5.0.0 and ' - '5.1.0 incorrectly transforms points. Use this projection ' - 'with caution.') + 'The Orthographic projection in the v5.0.x series of Proj ' + 'incorrectly transforms points. Use this projection with ' + 'caution.', + stacklevel=2) else: warnings.warn( 'Cannot determine Proj version. The Orthographic projection ' - 'may be unreliable and should be used with caution.') + 'may be unreliable and should be used with caution.', + stacklevel=2) proj4_params = [('proj', 'ortho'), ('lon_0', central_longitude), ('lat_0', central_latitude)] @@ -1512,15 +1551,10 @@ # TODO: Let the globe return the semimajor axis always. a = np.float(self.globe.semimajor_axis or WGS84_SEMIMAJOR_AXIS) - b = np.float(self.globe.semiminor_axis or a) - - if b != a: - warnings.warn('The proj "ortho" projection does not appear to ' - 'handle elliptical globes.') # To stabilise the projection of geometries, we reduce the boundary by # a tiny fraction at the cost of the extreme edges. - coords = _ellipse_boundary(a * 0.99999, b * 0.99999, n=61) + coords = _ellipse_boundary(a * 0.99999, a * 0.99999, n=61) self._boundary = sgeom.polygon.LinearRing(coords.T) mins = np.min(coords, axis=1) maxs = np.max(coords, axis=1) @@ -1597,6 +1631,8 @@ """ + _handles_ellipses = False + def __init__(self, central_longitude=0, false_easting=None, false_northing=None, globe=None): """ @@ -1615,17 +1651,6 @@ This projection does not handle elliptical globes. """ - if globe is None: - globe = Globe(semimajor_axis=WGS84_SEMIMAJOR_AXIS, ellipse=None) - - # TODO: Let the globe return the semimajor axis always. - a = globe.semimajor_axis or WGS84_SEMIMAJOR_AXIS - b = globe.semiminor_axis or a - - if b != a or globe.ellipse is not None: - warnings.warn('The proj "{}" projection does not handle ' - 'elliptical globes.'.format(self._proj_name)) - proj4_params = [('proj', self._proj_name), ('lon_0', central_longitude)] super(_Eckert, self).__init__(proj4_params, central_longitude, @@ -1779,6 +1804,8 @@ """ + _handles_ellipses = False + def __init__(self, central_longitude=0, globe=None, false_easting=None, false_northing=None): """ @@ -1797,17 +1824,6 @@ This projection does not handle elliptical globes. """ - if globe is None: - globe = Globe(semimajor_axis=WGS84_SEMIMAJOR_AXIS, ellipse=None) - - # TODO: Let the globe return the semimajor axis always. - a = globe.semimajor_axis or WGS84_SEMIMAJOR_AXIS - b = globe.semiminor_axis or a - - if b != a or globe.ellipse is not None: - warnings.warn('The proj "moll" projection does not handle ' - 'elliptical globes.') - proj4_params = [('proj', 'moll'), ('lon_0', central_longitude)] super(Mollweide, self).__init__(proj4_params, central_longitude, false_easting=false_easting, @@ -1831,6 +1847,8 @@ """ + _handles_ellipses = False + def __init__(self, central_longitude=0, globe=None, false_easting=None, false_northing=None): """ @@ -1857,22 +1875,13 @@ warnings.warn('The Robinson projection in the v4.8.x series ' 'of Proj contains a discontinuity at ' '40 deg latitude. Use this projection with ' - 'caution.') + 'caution.', + stacklevel=2) else: warnings.warn('Cannot determine Proj version. The Robinson ' 'projection may be unreliable and should be used ' - 'with caution.') - - if globe is None: - globe = Globe(semimajor_axis=WGS84_SEMIMAJOR_AXIS, ellipse=None) - - # TODO: Let the globe return the semimajor axis always. - a = globe.semimajor_axis or WGS84_SEMIMAJOR_AXIS - b = globe.semiminor_axis or a - - if b != a or globe.ellipse is not None: - warnings.warn('The proj "robin" projection does not handle ' - 'elliptical globes.') + 'with caution.', + stacklevel=2) proj4_params = [('proj', 'robin'), ('lon_0', central_longitude)] super(Robinson, self).__init__(proj4_params, central_longitude, @@ -2028,14 +2037,12 @@ proj4_params.append(('sweep', sweep_axis)) super(_Satellite, self).__init__(proj4_params, globe=globe) - def _set_bounds(self, max_x, max_y): - false_easting = self.proj4_params['x_0'] - false_northing = self.proj4_params['y_0'] - coords = _ellipse_boundary(max_x, max_y, - false_easting, false_northing, 61) + def _set_boundary(self, coords): self._boundary = sgeom.LinearRing(coords.T) - self._x_limits = -max_x + false_easting, max_x + false_easting - self._y_limits = -max_y + false_northing, max_y + false_northing + mins = np.min(coords, axis=1) + maxs = np.max(coords, axis=1) + self._x_limits = mins[0], maxs[0] + self._y_limits = mins[1], maxs[1] self._threshold = np.diff(self._x_limits)[0] * 0.02 @property @@ -2101,11 +2108,22 @@ # TODO: Let the globe return the semimajor axis always. a = np.float(self.globe.semimajor_axis or WGS84_SEMIMAJOR_AXIS) - b = np.float(self.globe.semiminor_axis or a) h = np.float(satellite_height) - max_x = h * np.arcsin(a / (a + h)) - max_y = h * np.arcsin(b / (a + h)) - self._set_bounds(max_x, max_y) + + # These are only exact for a spherical Earth, owing to assuming a is + # constant. Handling elliptical would be much harder for this. + sin_max_th = a / (a + h) + tan_max_th = a / np.sqrt((a + h) ** 2 - a ** 2) + + # Using Napier's rules for right spherical triangles + # See R2 and R6 (x and y coords are h * b and h * a, respectively): + # https://en.wikipedia.org/wiki/Spherical_trigonometry + t = np.linspace(0, -2 * np.pi, 61) # Clockwise boundary. + coords = np.vstack([np.arctan(tan_max_th * np.cos(t)), + np.arcsin(sin_max_th * np.sin(t))]) + coords *= h + coords += np.array([[false_easting], [false_northing]]) + self._set_boundary(coords) class NearsidePerspective(_Satellite): @@ -2117,6 +2135,9 @@ point (e.g. a satellite). """ + + _handles_ellipses = False + def __init__(self, central_longitude=0.0, central_latitude=0.0, satellite_height=35785831, false_easting=0, false_northing=0, globe=None): @@ -2141,17 +2162,6 @@ This projection does not handle elliptical globes. """ - if globe is None: - globe = Globe(semimajor_axis=WGS84_SEMIMAJOR_AXIS, ellipse=None) - - # TODO: Let the globe return the semimajor axis always. - a = globe.semimajor_axis or WGS84_SEMIMAJOR_AXIS - b = globe.semiminor_axis or a - - if b != a or globe.ellipse is not None: - warnings.warn('The proj "nsper" projection does not handle ' - 'elliptical globes.') - super(NearsidePerspective, self).__init__( projection='nsper', satellite_height=satellite_height, @@ -2161,9 +2171,14 @@ false_northing=false_northing, globe=globe) + # TODO: Let the globe return the semimajor axis always. + a = self.globe.semimajor_axis or WGS84_SEMIMAJOR_AXIS + h = np.float(satellite_height) max_x = a * np.sqrt(h / (2 * a + h)) - self._set_bounds(max_x, max_x) + coords = _ellipse_boundary(max_x, max_x, + false_easting, false_northing, 61) + self._set_boundary(coords) class AlbersEqualArea(Projection): @@ -2288,11 +2303,13 @@ warnings.warn('The Azimuthal Equidistant projection in Proj ' 'older than 4.9.2 incorrectly transforms points ' 'farther than 90 deg from the origin. Use this ' - 'projection with caution.') + 'projection with caution.', + stacklevel=2) else: warnings.warn('Cannot determine Proj version. The Azimuthal ' 'Equidistant projection may be unreliable and ' - 'should be used with caution.') + 'should be used with caution.', + stacklevel=2) proj4_params = [('proj', 'aeqd'), ('lon_0', central_longitude), ('lat_0', central_latitude), @@ -2397,7 +2414,7 @@ # MODIS data products use a Sinusoidal projection of a spherical Earth -# http://modis-land.gsfc.nasa.gov/GCTP.html +# https://modis-land.gsfc.nasa.gov/GCTP.html Sinusoidal.MODIS = Sinusoidal(globe=Globe(ellipse=None, semimajor_axis=6371007.181, semiminor_axis=6371007.181)) diff -Nru python-cartopy-0.17.0+dfsg/lib/cartopy/_crs.pyx python-cartopy-0.18.0+dfsg/lib/cartopy/_crs.pyx --- python-cartopy-0.17.0+dfsg/lib/cartopy/_crs.pyx 2018-11-17 07:25:32.000000000 +0000 +++ python-cartopy-0.18.0+dfsg/lib/cartopy/_crs.pyx 2020-05-03 08:12:47.000000000 +0000 @@ -1,19 +1,8 @@ -# (C) British Crown Copyright 2011 - 2018, Met Office +# Copyright Cartopy Contributors # -# 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 . +# 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. # # cython: embedsignature=True @@ -35,17 +24,9 @@ from cython.operator cimport dereference as deref -cdef extern from "proj_api.h": - ctypedef void *projPJ - projPJ pj_init_plus(char *) - void pj_free(projPJ) - int pj_transform(projPJ, projPJ, long, int, double *, double *, double *) - int pj_is_latlong(projPJ) - char *pj_strerrno(int) - int *pj_get_errno_ref() - char *pj_get_release() - double DEG_TO_RAD - double RAD_TO_DEG +from ._proj4 cimport (pj_init_plus, pj_free, pj_transform, pj_is_latlong, + pj_strerrno, pj_get_errno_ref, pj_get_release, + DEG_TO_RAD, RAD_TO_DEG) cdef double NAN = float('nan') @@ -60,6 +41,9 @@ else: PROJ4_VERSION = () +WGS84_SEMIMAJOR_AXIS = 6378137.0 +WGS84_SEMIMINOR_AXIS = 6356752.3142 + class Proj4Error(Exception): """ @@ -77,6 +61,61 @@ Exception.__init__(self, msg) +def _safe_pj_transform_611(CRS src_crs not None, CRS tgt_crs not None, + int npts, int offset, + np.ndarray[np.double_t] x not None, + np.ndarray[np.double_t] y not None, + np.ndarray[np.double_t] z): + """ + Workaround bug in Proj 6.1.1+ with +to_meter on +proj=ob_tran. + + See https://github.com/OSGeo/proj#1782. + """ + cdef int status + + lonlat = ('latlon', 'latlong', 'lonlat', 'longlat') + + if (src_crs.proj4_params.get('proj', '') == 'ob_tran' and + src_crs.proj4_params.get('o_proj', '') in lonlat and + 'to_meter' in src_crs.proj4_params): + x *= src_crs.proj4_params['to_meter'] + y *= src_crs.proj4_params['to_meter'] + + if z is not None: + status = pj_transform(src_crs.proj4, tgt_crs.proj4, npts, offset, + &x[0], &y[0], &z[0]) + else: + status = pj_transform(src_crs.proj4, tgt_crs.proj4, npts, offset, + &x[0], &y[0], NULL) + + if (tgt_crs.proj4_params.get('proj', '') == 'ob_tran' and + tgt_crs.proj4_params.get('o_proj', '') in lonlat and + 'to_meter' in tgt_crs.proj4_params): + x /= tgt_crs.proj4_params['to_meter'] + y /= tgt_crs.proj4_params['to_meter'] + + return status + + +def _safe_pj_transform_pre_611(CRS src_crs not None, CRS tgt_crs not None, + int npts, int offset, + np.ndarray[np.double_t] x not None, + np.ndarray[np.double_t] y not None, + np.ndarray[np.double_t] z): + if z is not None: + return pj_transform(src_crs.proj4, tgt_crs.proj4, npts, offset, + &x[0], &y[0], &z[0]) + else: + return pj_transform(src_crs.proj4, tgt_crs.proj4, npts, offset, + &x[0], &y[0], NULL) + + +if (6, 1, 1) <= PROJ4_VERSION < (6, 3, 0): + _safe_pj_transform = _safe_pj_transform_611 +else: + _safe_pj_transform = _safe_pj_transform_pre_611 + + class Globe(object): """ Define an ellipsoid and, optionally, how to relate it to the real world. @@ -134,6 +173,10 @@ Define a Coordinate Reference System using proj. """ + + #: Whether this projection can handle ellipses. + _handles_ellipses = True + def __cinit__(self): self.proj4 = NULL @@ -157,7 +200,19 @@ See :class:`~cartopy.crs.Globe` for details. """ - self.globe = globe or Globe() + if globe is None: + if self._handles_ellipses: + globe = Globe() + else: + globe = Globe(semimajor_axis=WGS84_SEMIMAJOR_AXIS, + ellipse=None) + if not self._handles_ellipses: + a = globe.semimajor_axis or WGS84_SEMIMAJOR_AXIS + b = globe.semiminor_axis or a + if a != b or globe.ellipse is not None: + warnings.warn('The "{}" projection does not handle elliptical ' + 'globes.'.format(self.__class__.__name__)) + self.globe = globe self.proj4_params = self.globe.to_proj4_params() self.proj4_params.update(proj4_params) @@ -203,23 +258,38 @@ instance of this class (e.g. an empty tuple). The state will then be added via __getstate__ and __setstate__. + We are forced to this approach because a CRS does not store + the constructor keyword arguments in its state. + """ - return self.__class__, tuple() + return self.__class__, (), self.__getstate__() def __getstate__(self): """Return the full state of this instance for reconstruction in ``__setstate__``. """ - return {'proj4_params': self.proj4_params} + state = self.__dict__.copy() + # Remove the proj4 instance and the proj4_init string, which can + # be re-created (in __setstate__) from the other arguments. + state.pop('proj4', None) + state.pop('proj4_init', None) + state['proj4_params'] = self.proj4_params + return state def __setstate__(self, state): """ Take the dictionary created by ``__getstate__`` and passes it - through to the class's __init__ method. + through to this implementation's __init__ method. """ - self.__init__(self, **state) + # Strip out the key state items for a CRS instance. + CRS_state = {key: state.pop(key) for key in ['proj4_params', 'globe']} + # Put everything else directly into the dict of the instance. + self.__dict__.update(state) + # Call the init of this class to ensure that the projection is + # properly initialised with proj4. + CRS.__init__(self, **CRS_state) # TODO #def __str__ @@ -287,26 +357,26 @@ """ cdef: - double cx, cy + np.ndarray[np.double_t, ndim=1] cx, cy int status - cx = x - cy = y + cx = np.array([x]) + cy = np.array([y]) if src_crs.is_geodetic(): cx *= DEG_TO_RAD cy *= DEG_TO_RAD - status = pj_transform(src_crs.proj4, self.proj4, 1, 1, &cx, &cy, NULL); + status = _safe_pj_transform(src_crs, self, 1, 1, cx, cy, None) if trap and status == -14 or status == -20: # -14 => "latitude or longitude exceeded limits" # -20 => "tolerance condition error" - cx = cy = NAN + cx[0] = cy[0] = np.nan elif trap and status != 0: raise Proj4Error() if self.is_geodetic(): cx *= RAD_TO_DEG cy *= RAD_TO_DEG - return (cx, cy) + return (cx[0], cy[0]) def transform_points(self, CRS src_crs not None, np.ndarray x not None, @@ -381,8 +451,9 @@ # call proj. The result array is modified in place. This is only # safe if npts is not 0. if npts: - status = pj_transform(src_crs.proj4, self.proj4, npts, 3, - &result[0, 0], &result[0, 1], &result[0, 2]) + status = _safe_pj_transform(src_crs, self, npts, 3, + result[:, 0], result[:, 1], + result[:, 2]) if self.is_geodetic(): result[:, :2] = np.rad2deg(result[:, :2]) @@ -535,7 +606,7 @@ super(Geodetic, self).__init__(proj4_params, globe) # XXX Implement fwd such as Basemap's Geod. Would be used in the tissot example. - # Could come from http://geographiclib.sourceforge.net + # Could come from https://geographiclib.sourceforge.io class Geocentric(CRS): diff -Nru python-cartopy-0.17.0+dfsg/lib/cartopy/data/LICENSE python-cartopy-0.18.0+dfsg/lib/cartopy/data/LICENSE --- python-cartopy-0.17.0+dfsg/lib/cartopy/data/LICENSE 2018-11-17 07:25:32.000000000 +0000 +++ python-cartopy-0.18.0+dfsg/lib/cartopy/data/LICENSE 2020-05-03 08:12:47.000000000 +0000 @@ -1,8 +1,8 @@ -HadISST data: http://www.metoffice.gov.uk/hadobs/hadisst/terms_and_conditions.html +HadISST data: https://www.metoffice.gov.uk/hadobs/hadisst/terms_and_conditions.html -The image 50-natural-earth-1-downsampled.png is downsampled from Natural Earth data whose conditions are stated at http://www.naturalearthdata.com/about/terms-of-use/ (public domain). +The image 50-natural-earth-1-downsampled.png is downsampled from Natural Earth data whose conditions are stated at https://www.naturalearthdata.com/about/terms-of-use/ (public domain). -gshhs is LGPL, but their documentation doesn't state a version http://www.soest.hawaii.edu/pwessel/gshhg/. +gshhs is LGPL, but their documentation doesn't state a version https://www.soest.hawaii.edu/pwessel/gshhg/. The same data sets are part of the GMT package which is packaged in linux distributions, including debian. The image Miriam.A2012270.2050.2km.jpg is from the Rapid Response section of the NASA LANCE data, which states it is in the public domain https://earthdata.nasa.gov/faq#ed-rapid-response-faq diff -Nru python-cartopy-0.17.0+dfsg/lib/cartopy/data/raster/natural_earth/images.json python-cartopy-0.18.0+dfsg/lib/cartopy/data/raster/natural_earth/images.json --- python-cartopy-0.17.0+dfsg/lib/cartopy/data/raster/natural_earth/images.json 2018-11-17 07:25:32.000000000 +0000 +++ python-cartopy-0.18.0+dfsg/lib/cartopy/data/raster/natural_earth/images.json 2020-05-03 08:12:47.000000000 +0000 @@ -1,7 +1,7 @@ {"__comment__": "JSON file specifying the image to use for a given type/name and resolution. Read in by cartopy.mpl.geoaxes.read_user_background_images.", "ne_shaded": { "__comment__": "Natural Earth shaded relief", - "__source__": "http://www.naturalearthdata.com/downloads/50m-raster-data/50m-natural-earth-1/", + "__source__": "https://www.naturalearthdata.com/downloads/50m-raster-data/50m-natural-earth-1/", "__projection__": "PlateCarree", "low": "50-natural-earth-1-downsampled.png" } } diff -Nru python-cartopy-0.17.0+dfsg/lib/cartopy/data/raster/sample/Miriam.A2012270.2050.2km.README.txt python-cartopy-0.18.0+dfsg/lib/cartopy/data/raster/sample/Miriam.A2012270.2050.2km.README.txt --- python-cartopy-0.17.0+dfsg/lib/cartopy/data/raster/sample/Miriam.A2012270.2050.2km.README.txt 2018-11-17 07:25:32.000000000 +0000 +++ python-cartopy-0.18.0+dfsg/lib/cartopy/data/raster/sample/Miriam.A2012270.2050.2km.README.txt 2020-05-03 08:12:47.000000000 +0000 @@ -1,5 +1,5 @@ Imagery comes from -http://lance-modis.eosdis.nasa.gov/cgi-bin/imagery/single.cgi?image=Miriam.A2012270.2050.2km.jpg +https://lance-modis.eosdis.nasa.gov/cgi-bin/imagery/single.cgi?image=Miriam.A2012270.2050.2km.jpg Extent: (-120.67660000000001, -106.32104523100001, 13.2301484511245, 30.766899999999502) World file contents: diff -Nru python-cartopy-0.17.0+dfsg/lib/cartopy/_epsg.py python-cartopy-0.18.0+dfsg/lib/cartopy/_epsg.py --- python-cartopy-0.17.0+dfsg/lib/cartopy/_epsg.py 2018-11-17 07:25:32.000000000 +0000 +++ python-cartopy-0.18.0+dfsg/lib/cartopy/_epsg.py 2020-05-03 08:12:47.000000000 +0000 @@ -41,7 +41,8 @@ def __init__(self, code): import pyepsg projection = pyepsg.get(code) - if not isinstance(projection, pyepsg.ProjectedCRS): + if not (isinstance(projection, pyepsg.ProjectedCRS) or + isinstance(projection, pyepsg.CompoundCRS)): raise ValueError('EPSG code does not define a projection') self.epsg_code = code diff -Nru python-cartopy-0.17.0+dfsg/lib/cartopy/examples/aurora_forecast.py python-cartopy-0.18.0+dfsg/lib/cartopy/examples/aurora_forecast.py --- python-cartopy-0.17.0+dfsg/lib/cartopy/examples/aurora_forecast.py 2018-11-17 07:25:32.000000000 +0000 +++ python-cartopy-0.18.0+dfsg/lib/cartopy/examples/aurora_forecast.py 2020-05-03 08:12:47.000000000 +0000 @@ -30,7 +30,7 @@ def aurora_forecast(): """ - Get the latest Aurora Forecast from http://swpc.noaa.gov. + Get the latest Aurora Forecast from https://www.swpc.noaa.gov. Returns ------- @@ -52,7 +52,7 @@ url = ('https://gist.githubusercontent.com/belteshassar/' 'c7ea9e02a3e3934a9ddc/raw/aurora-nowcast-map.txt') # To plot the current forecast instead, uncomment the following line - # url = 'http://services.swpc.noaa.gov/text/aurora-nowcast-map.txt' + # url = 'https://services.swpc.noaa.gov/text/aurora-nowcast-map.txt' response_text = StringIO(urlopen(url).read().decode('utf-8')) img = np.loadtxt(response_text) diff -Nru python-cartopy-0.17.0+dfsg/lib/cartopy/examples/contour_labels.py python-cartopy-0.18.0+dfsg/lib/cartopy/examples/contour_labels.py --- python-cartopy-0.17.0+dfsg/lib/cartopy/examples/contour_labels.py 1970-01-01 00:00:00.000000000 +0000 +++ python-cartopy-0.18.0+dfsg/lib/cartopy/examples/contour_labels.py 2020-05-03 08:12:47.000000000 +0000 @@ -0,0 +1,56 @@ +""" +Contour labels +-------------- + +An example of adding contour labels to matplotlib contours. + +""" +__tags__ = ['Scalar data'] + +import cartopy.crs as ccrs +import matplotlib.pyplot as plt + +from cartopy.examples.waves import sample_data + + +def main(): + fig = plt.figure() + + # Setup a global EckertIII map with faint coastlines. + ax = fig.add_subplot(1, 1, 1, projection=ccrs.EckertIII()) + ax.set_global() + ax.coastlines('110m', alpha=0.1) + + # Use the waves example to provide some sample data, but make it + # more dependent on y for more interesting contours. + x, y, z = sample_data((20, 40)) + z = z * -1.5 * y + + # Add colourful filled contours. + filled_c = ax.contourf(x, y, z, transform=ccrs.PlateCarree()) + + # And black line contours. + line_c = ax.contour(x, y, z, levels=filled_c.levels, + colors=['black'], + transform=ccrs.PlateCarree()) + + # Uncomment to make the line contours invisible. + # plt.setp(line_c.collections, visible=False) + + # Add a colorbar for the filled contour. + fig.colorbar(filled_c, orientation='horizontal') + + # Use the line contours to place contour labels. + ax.clabel( + line_c, # Typically best results when labelling line contours. + colors=['black'], + manual=False, # Automatic placement vs manual placement. + inline=True, # Cut the line where the label will be placed. + fmt=' {:.0f} '.format, # Labes as integers, with some extra space. + ) + + plt.show() + + +if __name__ == '__main__': + main() diff -Nru python-cartopy-0.17.0+dfsg/lib/cartopy/examples/effects_of_the_ellipse.py python-cartopy-0.18.0+dfsg/lib/cartopy/examples/effects_of_the_ellipse.py --- python-cartopy-0.17.0+dfsg/lib/cartopy/examples/effects_of_the_ellipse.py 2018-11-17 07:25:32.000000000 +0000 +++ python-cartopy-0.18.0+dfsg/lib/cartopy/examples/effects_of_the_ellipse.py 2020-05-03 08:12:47.000000000 +0000 @@ -102,7 +102,7 @@ # Make a nice border around the inset axes. effect = Stroke(linewidth=4, foreground='wheat', alpha=0.5) - sub_ax.outline_patch.set_path_effects([effect]) + sub_ax.spines['geo'].set_path_effects([effect]) # Add the land, coastlines and the extent of the Solomon Islands. sub_ax.add_feature(cfeature.LAND) diff -Nru python-cartopy-0.17.0+dfsg/lib/cartopy/examples/favicon.py python-cartopy-0.18.0+dfsg/lib/cartopy/examples/favicon.py --- python-cartopy-0.17.0+dfsg/lib/cartopy/examples/favicon.py 2018-11-17 07:25:32.000000000 +0000 +++ python-cartopy-0.18.0+dfsg/lib/cartopy/examples/favicon.py 2020-05-03 08:12:47.000000000 +0000 @@ -19,22 +19,7 @@ ax.coastlines() ax.gridlines() - - im = ax.stock_img() - - def on_draw(event=None): - """ - Hook into matplotlib's event mechanism to define the clip path of the - background image. - - """ - # Clip the image to the current background boundary. - im.set_clip_path(ax.background_patch.get_path(), - transform=ax.background_patch.get_transform()) - - # Register the on_draw method and call it once now. - fig.canvas.mpl_connect('draw_event', on_draw) - on_draw() + ax.stock_img() # Generate a matplotlib path representing the character "C". fp = FontProperties(family='Bitstream Vera Sans', weight='bold') diff -Nru python-cartopy-0.17.0+dfsg/lib/cartopy/examples/gridliner.py python-cartopy-0.18.0+dfsg/lib/cartopy/examples/gridliner.py --- python-cartopy-0.17.0+dfsg/lib/cartopy/examples/gridliner.py 1970-01-01 00:00:00.000000000 +0000 +++ python-cartopy-0.18.0+dfsg/lib/cartopy/examples/gridliner.py 2020-05-03 08:12:47.000000000 +0000 @@ -0,0 +1,44 @@ +""" +Gridlines and tick labels +------------------------- + +These examples demonstrate how to quickly add longitude +and latitude gridlines and tick labels on a non-rectangular projection. + +As you can see on the first example, +longitude labels may be drawn on left and right sides, +and latitude labels may be drawn on bottom and top sides. +Thanks to the ``dms`` keyword, minutes are used when appropriate +to display fractions of degree. + + +In the second example, labels are still drawn at the map edges +despite its complexity, and some others are also drawn within the map +boundary. + +""" +import cartopy.crs as ccrs +import cartopy.feature as cfeature +import matplotlib.pyplot as plt + +__tags__ = ['Gridlines', 'Tick labels', 'Lines and polygons'] + + +def main(): + + rotated_crs = ccrs.RotatedPole(pole_longitude=120.0, pole_latitude=70.0) + ax0 = plt.axes(projection=rotated_crs) + ax0.set_extent([-6, 1, 47.5, 51.5], crs=ccrs.PlateCarree()) + ax0.add_feature(cfeature.LAND.with_scale('110m')) + ax0.gridlines(draw_labels=True, dms=True, x_inline=False, y_inline=False) + + plt.figure(figsize=(6.9228, 3)) + ax1 = plt.axes(projection=ccrs.InterruptedGoodeHomolosine()) + ax1.coastlines(resolution='110m') + ax1.gridlines(draw_labels=True) + + plt.show() + + +if __name__ == '__main__': + main() diff -Nru python-cartopy-0.17.0+dfsg/lib/cartopy/examples/hurricane_katrina.py python-cartopy-0.18.0+dfsg/lib/cartopy/examples/hurricane_katrina.py --- python-cartopy-0.17.0+dfsg/lib/cartopy/examples/hurricane_katrina.py 2018-11-17 07:25:32.000000000 +0000 +++ python-cartopy-0.18.0+dfsg/lib/cartopy/examples/hurricane_katrina.py 2020-05-03 08:12:47.000000000 +0000 @@ -22,7 +22,7 @@ for Hurricane Katrina (2005). The data was originally sourced from the HURDAT2 dataset from AOML/NOAA: - http://www.aoml.noaa.gov/hrd/hurdat/newhurdat-all.html on 14th Dec 2012. + https://www.aoml.noaa.gov/hrd/hurdat/newhurdat-all.html on 14th Dec 2012. """ lons = [-75.1, -75.7, -76.2, -76.5, -76.9, -77.7, -78.4, -79.0, @@ -41,7 +41,11 @@ def main(): fig = plt.figure() - ax = fig.add_axes([0, 0, 1, 1], projection=ccrs.LambertConformal()) + # to get the effect of having just the states without a map "background" + # turn off the background patch and axes frame + ax = fig.add_axes([0, 0, 1, 1], projection=ccrs.LambertConformal(), + frameon=False) + ax.patch.set_visible(False) ax.set_extent([-125, -66.5, 20, 50], ccrs.Geodetic()) @@ -51,11 +55,6 @@ lons, lats = sample_data() - # to get the effect of having just the states without a map "background" - # turn off the outline and background patches - ax.background_patch.set_visible(False) - ax.outline_patch.set_visible(False) - ax.set_title('US States which intersect the track of ' 'Hurricane Katrina (2005)') diff -Nru python-cartopy-0.17.0+dfsg/lib/cartopy/examples/reprojected_wmts.py python-cartopy-0.18.0+dfsg/lib/cartopy/examples/reprojected_wmts.py --- python-cartopy-0.17.0+dfsg/lib/cartopy/examples/reprojected_wmts.py 2018-11-17 07:25:32.000000000 +0000 +++ python-cartopy-0.18.0+dfsg/lib/cartopy/examples/reprojected_wmts.py 2020-05-03 08:12:47.000000000 +0000 @@ -24,7 +24,7 @@ def plot_city_lights(): # Define resource for the NASA night-time illumination data. - base_uri = 'http://map1c.vis.earthdata.nasa.gov/wmts-geo/wmts.cgi' + base_uri = 'https://map1c.vis.earthdata.nasa.gov/wmts-geo/wmts.cgi' layer_name = 'VIIRS_CityLights_2012' # Create a Cartopy crs for plain and rotated lat-lon projections. diff -Nru python-cartopy-0.17.0+dfsg/lib/cartopy/examples/tube_stations.py python-cartopy-0.18.0+dfsg/lib/cartopy/examples/tube_stations.py --- python-cartopy-0.17.0+dfsg/lib/cartopy/examples/tube_stations.py 2018-11-17 07:25:32.000000000 +0000 +++ python-cartopy-0.18.0+dfsg/lib/cartopy/examples/tube_stations.py 2020-05-03 08:12:47.000000000 +0000 @@ -19,7 +19,7 @@ Return an (n, 2) array of selected London Tube locations in Ordnance Survey GB coordinates. - Source: http://www.doogal.co.uk/london_stations.php + Source: https://www.doogal.co.uk/london_stations.php """ return np.array([[531738., 180890.], [532379., 179734.], @@ -58,9 +58,9 @@ # Plot the locations twice, first with the red concentric circles, # then with the blue rectangle. xs, ys = tube_locations().T - ax.plot(xs, ys, transform=ccrs.OSGB(), + ax.plot(xs, ys, transform=ccrs.OSGB(approx=False), marker=concentric_circle, color='red', markersize=9, linestyle='') - ax.plot(xs, ys, transform=ccrs.OSGB(), + ax.plot(xs, ys, transform=ccrs.OSGB(approx=False), marker=rectangle, color='blue', markersize=11, linestyle='') ax.set_title('London underground locations') diff -Nru python-cartopy-0.17.0+dfsg/lib/cartopy/examples/un_flag.py python-cartopy-0.18.0+dfsg/lib/cartopy/examples/un_flag.py --- python-cartopy-0.17.0+dfsg/lib/cartopy/examples/un_flag.py 2018-11-17 07:25:32.000000000 +0000 +++ python-cartopy-0.18.0+dfsg/lib/cartopy/examples/un_flag.py 2020-05-03 08:12:47.000000000 +0000 @@ -104,9 +104,11 @@ # Equidistant projection). ax = fig.add_axes([0.25, 0.24, 0.5, 0.54], projection=az_eq) - # The background patch and outline patch are not needed in this example. - ax.background_patch.set_facecolor('none') - ax.outline_patch.set_edgecolor('none') + # The background patch is not needed in this example. + ax.patch.set_facecolor('none') + # The Axes frame produces the outer meridian line. + for spine in ax.spines.values(): + spine.update({'edgecolor': 'white', 'linewidth': 2}) # We want the map to go down to -60 degrees latitude. ax.set_extent([-180, 180, -60, 90], ccrs.PlateCarree()) @@ -124,10 +126,10 @@ else: ax.stock_img() - gl = ax.gridlines(crs=pc, linewidth=3, color='white', linestyle='-') - # Meridians every 45 degrees, and 5 parallels. - gl.xlocator = matplotlib.ticker.FixedLocator(np.arange(0, 361, 45)) - parallels = np.linspace(-60, 70, 5, endpoint=True) + gl = ax.gridlines(crs=pc, linewidth=2, color='white', linestyle='-') + # Meridians every 45 degrees, and 4 parallels. + gl.xlocator = matplotlib.ticker.FixedLocator(np.arange(-180, 181, 45)) + parallels = np.arange(-30, 70, 30) gl.ylocator = matplotlib.ticker.FixedLocator(parallels) # Now add the olive branches around the axes. We do this in normalised diff -Nru python-cartopy-0.17.0+dfsg/lib/cartopy/examples/wmts_time.py python-cartopy-0.18.0+dfsg/lib/cartopy/examples/wmts_time.py --- python-cartopy-0.17.0+dfsg/lib/cartopy/examples/wmts_time.py 2018-11-17 07:25:32.000000000 +0000 +++ python-cartopy-0.18.0+dfsg/lib/cartopy/examples/wmts_time.py 2020-05-03 08:12:47.000000000 +0000 @@ -23,7 +23,7 @@ def main(): # URL of NASA GIBS - URL = 'http://gibs.earthdata.nasa.gov/wmts/epsg4326/best/wmts.cgi' + URL = 'https://gibs.earthdata.nasa.gov/wmts/epsg4326/best/wmts.cgi' wmts = WebMapTileService(URL) # Layers for MODIS true color and snow RGB diff -Nru python-cartopy-0.17.0+dfsg/lib/cartopy/feature/__init__.py python-cartopy-0.18.0+dfsg/lib/cartopy/feature/__init__.py --- python-cartopy-0.17.0+dfsg/lib/cartopy/feature/__init__.py 2018-11-17 07:25:32.000000000 +0000 +++ python-cartopy-0.18.0+dfsg/lib/cartopy/feature/__init__.py 2020-05-03 08:12:47.000000000 +0000 @@ -1,19 +1,9 @@ -# (C) British Crown Copyright 2011 - 2018, Met Office +# Copyright Cartopy Contributors # -# 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 . +# 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. + """ This module defines :class:`Feature` instances, for use with ax.add_feature(). @@ -238,7 +228,7 @@ """ A simple interface to Natural Earth shapefiles. - See http://www.naturalearthdata.com/ + See https://www.naturalearthdata.com/ """ def __init__(self, category, name, scale, **kwargs): @@ -270,11 +260,20 @@ scale = Scaler(scale) self.scaler = scale + # Make sure this is a valid resolution + self._validate_scale() @property def scale(self): return self.scaler.scale + def _validate_scale(self): + if self.scale not in ('110m', '50m', '10m'): + raise ValueError( + '{} is not a valid Natural Earth scale. '.format(self.scale) + + 'Valid scales are "110m", "50m", and "10m".' + ) + def geometries(self): """ Returns an iterator of (shapely) geometries for this feature. @@ -474,38 +473,49 @@ return iter(geoms) -BORDERS = NaturalEarthFeature('cultural', 'admin_0_boundary_lines_land', - '110m', edgecolor='black', facecolor='none') -"""Small scale (1:110m) country boundaries.""" +auto_scaler = AdaptiveScaler('110m', (('50m', 50), ('10m', 15))) +"""AdaptiveScaler for NaturalEarthFeature. Default scale is '110m'. +'110m' is used above 50 degrees, '50m' for 50-15 degrees and '10m' below 15 +degrees.""" + + +BORDERS = NaturalEarthFeature( + 'cultural', 'admin_0_boundary_lines_land', + auto_scaler, edgecolor='black', facecolor='never') +"""Automatically scaled country boundaries.""" + + +STATES = NaturalEarthFeature( + 'cultural', 'admin_1_states_provinces_lakes', + auto_scaler, edgecolor='black', facecolor='none') +"""Automatically scaled state and province boundaries.""" -STATES = NaturalEarthFeature('cultural', 'admin_1_states_provinces_lakes', - '110m', edgecolor='black', facecolor='none') -"""Small scale (1:110m) state and province boundaries.""" -COASTLINE = NaturalEarthFeature('physical', 'coastline', '110m', - edgecolor='black', facecolor='none') -"""Small scale (1:110m) coastline, including major islands.""" +COASTLINE = NaturalEarthFeature( + 'physical', 'coastline', auto_scaler, + edgecolor='black', facecolor='never') +"""Automatically scaled coastline, including major islands.""" -LAKES = NaturalEarthFeature('physical', 'lakes', '110m', - edgecolor='face', - facecolor=COLORS['water']) -"""Small scale (1:110m) natural and artificial lakes.""" +LAKES = NaturalEarthFeature( + 'physical', 'lakes', auto_scaler, + edgecolor='face', facecolor=COLORS['water']) +"""Automatically scaled natural and artificial lakes.""" -LAND = NaturalEarthFeature('physical', 'land', '110m', - edgecolor='face', - facecolor=COLORS['land'], zorder=-1) -"""Small scale (1:110m) land polygons, including major islands.""" +LAND = NaturalEarthFeature( + 'physical', 'land', auto_scaler, + edgecolor='face', facecolor=COLORS['land'], zorder=-1) +"""Automatically scaled land polygons, including major islands.""" -OCEAN = NaturalEarthFeature('physical', 'ocean', '110m', - edgecolor='face', - facecolor=COLORS['water'], zorder=-1) -"""Small scale (1:110m) ocean polygons.""" +OCEAN = NaturalEarthFeature( + 'physical', 'ocean', auto_scaler, + edgecolor='face', facecolor=COLORS['water'], zorder=-1) +"""Automatically scaled ocean polygons.""" -RIVERS = NaturalEarthFeature('physical', 'rivers_lake_centerlines', '110m', - edgecolor=COLORS['water'], - facecolor='none') -"""Small scale (1:110m) single-line drainages, including lake centerlines.""" +RIVERS = NaturalEarthFeature( + 'physical', 'rivers_lake_centerlines', auto_scaler, + edgecolor=COLORS['water'], facecolor='never') +"""Automatically scaled single-line drainages, including lake centerlines.""" diff -Nru python-cartopy-0.17.0+dfsg/lib/cartopy/geodesic/_geodesic.pxd python-cartopy-0.18.0+dfsg/lib/cartopy/geodesic/_geodesic.pxd --- python-cartopy-0.17.0+dfsg/lib/cartopy/geodesic/_geodesic.pxd 1970-01-01 00:00:00.000000000 +0000 +++ python-cartopy-0.18.0+dfsg/lib/cartopy/geodesic/_geodesic.pxd 2020-05-03 08:12:47.000000000 +0000 @@ -0,0 +1,43 @@ +# (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 . +# +# cython: embedsignature=True + +cdef extern from "geodesic.h": + # External imports of Proj4.9 functions + cdef struct geod_geodesic: + pass + cdef struct geod_geodesicline: + pass + + void geod_init(geod_geodesic*, double, double) nogil + void geod_direct(geod_geodesic*, double, double, double, double, + double*, double*, double*) nogil + void geod_inverse(geod_geodesic*, double, double, double, double, + double*, double*, double*) nogil + double geod_geninverse(geod_geodesic*, double, double, double, double, + double*, double*, double*, double*, double*, + double*, double*) nogil + void geod_lineinit(geod_geodesicline*, geod_geodesic*, double, double, + double, int) nogil + void geod_genposition(geod_geodesicline*, int, double, double*, + double*, double*, double*, double*, double*, + double*, double*) nogil + + cdef int GEOD_ARCMODE + cdef int GEOD_LATITUDE + cdef int GEOD_LONGITUDE diff -Nru python-cartopy-0.17.0+dfsg/lib/cartopy/geodesic/_geodesic.pyx python-cartopy-0.18.0+dfsg/lib/cartopy/geodesic/_geodesic.pyx --- python-cartopy-0.17.0+dfsg/lib/cartopy/geodesic/_geodesic.pyx 2018-11-17 07:25:32.000000000 +0000 +++ python-cartopy-0.18.0+dfsg/lib/cartopy/geodesic/_geodesic.pyx 2020-05-03 08:12:47.000000000 +0000 @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2015 - 2018, Met Office +# (C) British Crown Copyright 2015 - 2020, Met Office # # This file is part of cartopy. # @@ -30,18 +30,8 @@ from cython.parallel cimport prange import shapely.geometry as sgeom -cdef extern from "geodesic.h": - # External imports of Proj4.9 functions - cdef struct geod_geodesic: - pass - - ctypedef geod_geodesic* geodesic_t - - void geod_init(geodesic_t, double, double) - void geod_direct(geodesic_t, double, double, double, double, - double*, double*, double*) nogil - void geod_inverse(geodesic_t, double, double, double, double, - double*, double*, double*) nogil +from ._geodesic cimport geod_geodesic, geod_init, geod_direct, geod_inverse + cdef class Geodesic: """ @@ -281,13 +271,8 @@ # Polygon. result = self.geometry_length(geometry.exterior) - elif hasattr(geometry, 'coords'): + elif hasattr(geometry, 'coords') and not isinstance(geometry, sgeom.Point): coords = np.array(geometry.coords) - - # LinearRings are (N, 2), whereas LineStrings are (2, N). - if not isinstance(geometry, sgeom.LinearRing): - coords = coords.T - result = self.geometry_length(coords) elif isinstance(geometry, np.ndarray): diff -Nru python-cartopy-0.17.0+dfsg/lib/cartopy/img_transform.py python-cartopy-0.18.0+dfsg/lib/cartopy/img_transform.py --- python-cartopy-0.17.0+dfsg/lib/cartopy/img_transform.py 2018-11-17 07:25:32.000000000 +0000 +++ python-cartopy-0.18.0+dfsg/lib/cartopy/img_transform.py 2020-05-03 08:12:47.000000000 +0000 @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2011 - 2018, Met Office +# (C) British Crown Copyright 2011 - 2020, Met Office # # This file is part of cartopy. # @@ -34,8 +34,8 @@ def mesh_projection(projection, nx, ny, - x_extents=[None, None], - y_extents=[None, None]): + x_extents=(None, None), + y_extents=(None, None)): """ Return sample points in the given projection which span the entire projection range evenly. @@ -69,11 +69,17 @@ """ + def extent(specified, default, index): + if specified[index] is not None: + return specified[index] + else: + return default[index] + # Establish the x-direction and y-direction extents. - x_lower = x_extents[0] or projection.x_limits[0] - x_upper = x_extents[1] or projection.x_limits[1] - y_lower = y_extents[0] or projection.y_limits[0] - y_upper = y_extents[1] or projection.y_limits[1] + x_lower = extent(x_extents, projection.x_limits, 0) + x_upper = extent(x_extents, projection.x_limits, 1) + y_lower = extent(y_extents, projection.y_limits, 0) + y_upper = extent(y_extents, projection.y_limits, 1) # Calculate evenly spaced sample points spanning the # extent - excluding endpoint. diff -Nru python-cartopy-0.17.0+dfsg/lib/cartopy/__init__.py python-cartopy-0.18.0+dfsg/lib/cartopy/__init__.py --- python-cartopy-0.17.0+dfsg/lib/cartopy/__init__.py 2018-11-17 07:25:32.000000000 +0000 +++ python-cartopy-0.18.0+dfsg/lib/cartopy/__init__.py 2020-05-03 08:12:47.000000000 +0000 @@ -1,19 +1,8 @@ -# (C) British Crown Copyright 2011 - 2018, Met Office +# Copyright Cartopy Contributors # -# 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 . +# 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. from __future__ import (absolute_import, division, print_function) diff -Nru python-cartopy-0.17.0+dfsg/lib/cartopy/io/img_nest.py python-cartopy-0.18.0+dfsg/lib/cartopy/io/img_nest.py --- python-cartopy-0.17.0+dfsg/lib/cartopy/io/img_nest.py 2018-11-17 07:25:32.000000000 +0000 +++ python-cartopy-0.18.0+dfsg/lib/cartopy/io/img_nest.py 2020-05-03 08:12:47.000000000 +0000 @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2011 - 2018, Met Office +# (C) British Crown Copyright 2011 - 2019, Met Office # # This file is part of cartopy. # @@ -142,7 +142,7 @@ result = ['{}.{}'.format(froot, ext) for ext in fext_types] def _convert_basename(name): - dirname, basename = os.path.dirname(name), os.path.basename(name) + dirname, basename = os.path.split(name) base, ext = os.path.splitext(basename) if base == base.upper(): result = base.lower() + ext @@ -434,7 +434,7 @@ if target_domain.intersects(domain) and \ not target_domain.touches(domain): if start_tile[0] == target_z: - yield start_tile + yield start_tile else: for tile in self.subtiles(start_tile): for result in self.find_images(target_domain, diff -Nru python-cartopy-0.17.0+dfsg/lib/cartopy/io/img_tiles.py python-cartopy-0.18.0+dfsg/lib/cartopy/io/img_tiles.py --- python-cartopy-0.17.0+dfsg/lib/cartopy/io/img_tiles.py 2018-11-17 07:25:32.000000000 +0000 +++ python-cartopy-0.18.0+dfsg/lib/cartopy/io/img_tiles.py 2020-05-03 08:12:47.000000000 +0000 @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2011 - 2018, Met Office +# (C) British Crown Copyright 2011 - 2020, Met Office # # This file is part of cartopy. # @@ -31,6 +31,7 @@ from __future__ import (absolute_import, division, print_function) from abc import ABCMeta, abstractmethod +import concurrent.futures import warnings from PIL import Image @@ -38,6 +39,7 @@ import numpy as np import six +import cartopy import cartopy.crs as ccrs @@ -48,22 +50,44 @@ A "tile" in this class refers to the coordinates (x, y, z). """ - def __init__(self, desired_tile_form='RGB'): + _MAX_THREADS = 24 + + def __init__(self, desired_tile_form='RGB', + user_agent='CartoPy/' + cartopy.__version__): self.imgs = [] self.crs = ccrs.Mercator.GOOGLE self.desired_tile_form = desired_tile_form + self.user_agent = user_agent + # some providers like osm need a user_agent in the request issue #1341 + # osm may reject requests if there are too many of them, in which case + # a change of user_agent may fix the issue. def image_for_domain(self, target_domain, target_z): tiles = [] - for tile in self.find_images(target_domain, target_z): + + def fetch_tile(tile): try: img, extent, origin = self.get_image(tile) except IOError: - continue + # Some services 404 for tiles that aren't supposed to be + # there (e.g. out of range). + raise img = np.array(img) x = np.linspace(extent[0], extent[1], img.shape[1]) y = np.linspace(extent[2], extent[3], img.shape[0]) - tiles.append([img, x, y, origin]) + return img, x, y, origin + + with concurrent.futures.ThreadPoolExecutor( + max_workers=self._MAX_THREADS) as executor: + futures = [] + for tile in self.find_images(target_domain, target_z): + futures.append(executor.submit(fetch_tile, tile)) + for future in concurrent.futures.as_completed(futures): + try: + img, x, y, origin = future.result() + tiles.append([img, x, y, origin]) + except IOError: + pass img, extent, origin = _merge_tiles(tiles) return img, extent, origin @@ -80,7 +104,7 @@ domain = sgeom.box(x0, y0, x1, y1) if domain.intersects(target_domain): if start_tile[2] == target_z: - yield start_tile + yield start_tile else: for tile in self._subtiles(start_tile): for result in self._find_images(target_domain, target_z, @@ -157,19 +181,24 @@ def get_image(self, tile): if six.PY3: - from urllib.request import urlopen + from urllib.request import urlopen, Request, HTTPError, URLError else: - from urllib2 import urlopen + from urllib2 import urlopen, Request, HTTPError, URLError url = self._image_url(tile) - - fh = urlopen(url) - im_data = six.BytesIO(fh.read()) - fh.close() - img = Image.open(im_data) + try: + request = Request(url, headers={"User-Agent": self.user_agent}) + fh = urlopen(request) + im_data = six.BytesIO(fh.read()) + fh.close() + img = Image.open(im_data) + + except (HTTPError, URLError) as err: + print(err) + img = Image.fromarray(np.full((256, 256, 3), (250, 250, 250), + dtype=np.uint8)) img = img.convert(self.desired_tile_form) - return img, self.tileextent(tile), 'lower' @@ -225,15 +254,15 @@ class MapQuestOSM(GoogleWTS): - # http://developer.mapquest.com/web/products/open/map for terms of use - # http://devblog.mapquest.com/2016/06/15/ + # https://developer.mapquest.com/web/products/open/map for terms of use + # https://devblog.mapquest.com/2016/06/15/ # modernization-of-mapquest-results-in-changes-to-open-tile-access/ # this now requires a sign up to a plan def _image_url(self, tile): x, y, z = tile - url = 'http://otile1.mqcdn.com/tiles/1.0.0/osm/%s/%s/%s.jpg' % ( + url = 'https://otile1.mqcdn.com/tiles/1.0.0/osm/%s/%s/%s.jpg' % ( z, x, y) - mqdevurl = ('http://devblog.mapquest.com/2016/06/15/' + mqdevurl = ('https://devblog.mapquest.com/2016/06/15/' 'modernization-of-mapquest-results-in-changes' '-to-open-tile-access/') warnings.warn('{} will require a log in and and will likely' @@ -242,19 +271,20 @@ class MapQuestOpenAerial(GoogleWTS): - # http://developer.mapquest.com/web/products/open/map for terms of use + # https://developer.mapquest.com/web/products/open/map for terms of use # The following attribution should be included in the resulting image: # "Portions Courtesy NASA/JPL-Caltech and U.S. Depart. of Agriculture, # Farm Service Agency" def _image_url(self, tile): x, y, z = tile - url = 'http://oatile1.mqcdn.com/tiles/1.0.0/sat/%s/%s/%s.jpg' % ( + url = 'https://oatile1.mqcdn.com/tiles/1.0.0/sat/%s/%s/%s.jpg' % ( z, x, y) return url class OSM(GoogleWTS): - # http://developer.mapquest.com/web/products/open/map for terms of use + # https://operations.osmfoundation.org/policies/tiles/ for terms of use + def _image_url(self, tile): x, y, z = tile url = 'https://a.tile.openstreetmap.org/%s/%s/%s.png' % (z, x, y) @@ -281,8 +311,8 @@ attribute this imagery. """ - def __init__(self, style='toner'): - super(Stamen, self).__init__() + def __init__(self, style='toner', desired_tile_form='RGB'): + super(Stamen, self).__init__(desired_tile_form=desired_tile_form) self.style = style def _image_url(self, tile): @@ -318,7 +348,9 @@ def __init__(self): warnings.warn( "The StamenTerrain class was deprecated in v0.17. " - "Please use Stamen('terrain-background') instead.") + "Please use Stamen('terrain-background') instead.", + DeprecationWarning, + stacklevel=2) # NOTE: This subclass of Stamen exists for legacy reasons. # No further Stamen subclasses will be accepted as @@ -497,6 +529,57 @@ yield self.tms_to_quadkey(tile, google=True) +class OrdnanceSurvey(GoogleWTS): + """ + Implement web tile retrieval from Ordnance Survey map data. + To use this tile image source you will need to obtain an + API key from Ordnance Survey. + + For more details on Ordnance Survey layer styles, see + https://apidocs.os.uk/docs/map-styles. + + For the API framework agreement, see + https://developer.ordnancesurvey.co.uk/os-api-framework-agreement. + """ + # API Documentation: https://apidocs.os.uk/docs/os-maps-wmts + def __init__(self, apikey, layer='Road', desired_tile_form='RGB'): + """ + Parameters + ---------- + apikey: required + The authentication key provided by OS to query the maps API + layer: optional + The style of the Ordnance Survey map tiles. One of 'Outdoor', + 'Road', 'Light', 'Night', 'Leisure'. Defaults to 'Road'. + Details about the style of layer can be found at: + + - https://apidocs.os.uk/docs/layer-information + - https://apidocs.os.uk/docs/map-styles + desired_tile_form: optional + Defaults to 'RGB'. + """ + super(OrdnanceSurvey, self).__init__( + desired_tile_form=desired_tile_form) + self.apikey = apikey + + if layer not in ['Outdoor', 'Road', 'Light', 'Night', 'Leisure']: + raise ValueError('Invalid layer {}'.format(layer)) + + self.layer = layer + + def _image_url(self, tile): + x, y, z = tile + url = ('https://api2.ordnancesurvey.co.uk/' + 'mapping_api/v1/service/wmts?' + 'key={apikey}&height=256&width=256&tilematrixSet=EPSG%3A3857&' + 'version=1.0.0&style=true&layer={layer}%203857&' + 'SERVICE=WMTS&REQUEST=GetTile&format=image%2Fpng&' + 'TileMatrix=EPSG%3A3857%3A{z}&TileRow={y}&TileCol={x}') + return url.format(z=z, y=y, x=x, + apikey=self.apikey, + layer=self.layer) + + def _merge_tiles(tiles): """Return a single image, merging the given images.""" if not tiles: diff -Nru python-cartopy-0.17.0+dfsg/lib/cartopy/io/ogc_clients.py python-cartopy-0.18.0+dfsg/lib/cartopy/io/ogc_clients.py --- python-cartopy-0.17.0+dfsg/lib/cartopy/io/ogc_clients.py 2018-11-17 07:25:32.000000000 +0000 +++ python-cartopy-0.18.0+dfsg/lib/cartopy/io/ogc_clients.py 2020-05-03 08:12:47.000000000 +0000 @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2014 - 2018, Met Office +# (C) British Crown Copyright 2014 - 2020, Met Office # # This file is part of cartopy. # @@ -66,7 +66,7 @@ _CRS_TO_OGC_SRS = collections.OrderedDict( [(ccrs.PlateCarree(), 'EPSG:4326'), (ccrs.Mercator.GOOGLE, 'EPSG:900913'), - (ccrs.OSGB(), 'EPSG:27700') + (ccrs.OSGB(approx=True), 'EPSG:27700') ]) # Standard pixel size of 0.28 mm as defined by WMTS. @@ -81,14 +81,14 @@ 'urn:ogc:def:crs:EPSG::3031': 1, 'urn:ogc:def:crs:EPSG::3413': 1, 'urn:ogc:def:crs:EPSG::3857': 1, - 'urn:ogc:def:crs:EPSG:6.18:3:3857': 1 + 'urn:ogc:def:crs:EPSG:6.18.3:3857': 1 } _URN_TO_CRS = collections.OrderedDict( [('urn:ogc:def:crs:OGC:1.3:CRS84', ccrs.PlateCarree()), ('urn:ogc:def:crs:EPSG::4326', ccrs.PlateCarree()), ('urn:ogc:def:crs:EPSG::900913', ccrs.GOOGLE_MERCATOR), - ('urn:ogc:def:crs:EPSG::27700', ccrs.OSGB()), + ('urn:ogc:def:crs:EPSG::27700', ccrs.OSGB(approx=True)), ('urn:ogc:def:crs:EPSG::3031', ccrs.Stereographic( central_latitude=-90, true_scale_latitude=-71)), @@ -97,7 +97,7 @@ central_latitude=90, true_scale_latitude=70)), ('urn:ogc:def:crs:EPSG::3857', ccrs.GOOGLE_MERCATOR), - ('urn:ogc:def:crs:EPSG:6.18:3:3857', ccrs.GOOGLE_MERCATOR) + ('urn:ogc:def:crs:EPSG:6.18.3:3857', ccrs.GOOGLE_MERCATOR) ]) # XML namespace definitions @@ -122,7 +122,9 @@ else: # Convert Image to numpy array (flipping so that origin # is 'lower'). - img, extent = warp_array(np.asanyarray(image)[::-1], + # Convert to RGBA to keep the color palette in the regrid process + # if any + img, extent = warp_array(np.asanyarray(image.convert('RGBA'))[::-1], source_proj=source_projection, source_extent=source_extent, target_proj=output_projection, @@ -136,19 +138,9 @@ # This avoids unsightly grey boundaries appearing when the # extent is limited (i.e. not global). if np.ma.is_masked(img): - if img.shape[2:3] == (3,): - # RGB - old_img = img - img = np.zeros(img.shape[:2] + (4,), dtype=img.dtype) - img[:, :, 0:3] = old_img - img[:, :, 3] = ~ np.any(old_img.mask, axis=2) - if img.dtype.kind == 'u': - img[:, :, 3] *= 255 - elif img.shape[2:3] == (4,): - # RGBA - img[:, :, 3] = np.where(np.any(img.mask, axis=2), 0, - img[:, :, 3]) - img = img.data + img[:, :, 3] = np.where(np.any(img.mask, axis=2), 0, + img[:, :, 3]) + img = img.data # Convert warped image array back to an Image, undoing the # earlier flip. diff -Nru python-cartopy-0.17.0+dfsg/lib/cartopy/io/shapereader.py python-cartopy-0.18.0+dfsg/lib/cartopy/io/shapereader.py --- python-cartopy-0.17.0+dfsg/lib/cartopy/io/shapereader.py 2018-11-17 07:25:32.000000000 +0000 +++ python-cartopy-0.18.0+dfsg/lib/cartopy/io/shapereader.py 2020-05-03 08:12:47.000000000 +0000 @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2011 - 2018, Met Office +# (C) British Crown Copyright 2011 - 2020, Met Office # # This file is part of cartopy. # @@ -28,8 +28,6 @@ >>> len(reader) 3 >>> records = list(reader.records()) - >>> print(type(records[0])) - >>> print(', '.join(str(r) for r in sorted(records[0].attributes.keys()))) comment, ... name, name_alt, ... region, ... >>> print(records[0].attributes['name']) @@ -37,6 +35,7 @@ >>> geoms = list(reader.geometries()) >>> print(type(geoms[0])) + >>> reader.close() """ @@ -59,81 +58,17 @@ except ImportError: pass - __all__ = ['Reader', 'Record'] -def _create_point(shape): - return sgeom.Point(shape.points[0]) - - -def _create_polyline(shape): - if not shape.points: - return sgeom.MultiLineString() - - parts = list(shape.parts) + [None] - bounds = zip(parts[:-1], parts[1:]) - lines = [shape.points[slice(lower, upper)] for lower, upper in bounds] - return sgeom.MultiLineString(lines) - - -def _create_polygon(shape): - if not shape.points: - return sgeom.MultiPolygon() - - # Partition the shapefile rings into outer rings/polygons (clockwise) and - # inner rings/holes (anti-clockwise). - parts = list(shape.parts) + [None] - bounds = zip(parts[:-1], parts[1:]) - outer_polygons_and_holes = [] - inner_polygons = [] - for lower, upper in bounds: - polygon = sgeom.Polygon(shape.points[slice(lower, upper)]) - if polygon.exterior.is_ccw: - inner_polygons.append(polygon) - else: - outer_polygons_and_holes.append((polygon, [])) - - # Find the appropriate outer ring for each inner ring. - # aka. Group the holes with their containing polygons. - for inner_polygon in inner_polygons: - for outer_polygon, holes in outer_polygons_and_holes: - if outer_polygon.contains(inner_polygon): - holes.append(inner_polygon.exterior.coords) - break - - polygon_defns = [(outer_polygon.exterior.coords, holes) - for outer_polygon, holes in outer_polygons_and_holes] - return sgeom.MultiPolygon(polygon_defns) - - -def _make_geometry(geometry_factory, shape): - geometry = None - if shape.shapeType != shapefile.NULL: - geometry = geometry_factory(shape) - return geometry - - -# The mapping from shapefile shapeType values to geometry creation functions. -GEOMETRY_FACTORIES = { - shapefile.POINT: _create_point, - shapefile.POINTZ: _create_point, - shapefile.POLYLINE: _create_polyline, - shapefile.POLYLINEZ: _create_polyline, - shapefile.POLYGON: _create_polygon, - shapefile.POLYGONZ: _create_polygon, -} - - class Record(object): """ A single logical entry from a shapefile, combining the attributes with their associated geometry. """ - def __init__(self, shape, geometry_factory, attributes, fields): + def __init__(self, shape, attributes, fields): self._shape = shape - self._geometry_factory = geometry_factory self._bounds = None # if the record defines a bbox, then use that for the shape's bounds, @@ -141,7 +76,7 @@ if hasattr(shape, 'bbox'): self._bounds = tuple(shape.bbox) - self._geometry = False + self._geometry = None """The cached geometry instance for this Record.""" self.attributes = attributes @@ -174,9 +109,8 @@ shapefile. """ - if self._geometry is False: - self._geometry = _make_geometry(self._geometry_factory, - self._shape) + if not self._geometry and self._shape.shapeType != shapefile.NULL: + self._geometry = sgeom.shape(self._shape) return self._geometry @@ -208,14 +142,11 @@ raise ValueError("Incomplete shapefile definition " "in '%s'." % filename) - # Figure out how to make appropriate shapely geometry instances - shapeType = reader.shapeType - self._geometry_factory = GEOMETRY_FACTORIES.get(shapeType) - if self._geometry_factory is None: - raise ValueError('Unsupported shape type: %s' % shapeType) - self._fields = self._reader.fields + def close(self): + return self._reader.close() + def __len__(self): return self._reader.numRecords @@ -224,32 +155,31 @@ Return an iterator of shapely geometries from the shapefile. This interface is useful for accessing the geometries of the - shapefile where knowledge of the associated metadata is desired. + shapefile where knowledge of the associated metadata is not necessary. In the case where further metadata is needed use the :meth:`~Reader.records` interface instead, extracting the geometry from the record with the :meth:`~Record.geometry` method. """ - geometry_factory = self._geometry_factory for i in range(self._reader.numRecords): shape = self._reader.shape(i) - yield _make_geometry(geometry_factory, shape) + # Skip the shape that can not be represented as geometry. + if shape.shapeType != shapefile.NULL: + yield sgeom.shape(shape) def records(self): """ Return an iterator of :class:`~Record` instances. """ - geometry_factory = self._geometry_factory # Ignore the "DeletionFlag" field which always comes first fields = self._reader.fields[1:] field_names = [field[0] for field in fields] for i in range(self._reader.numRecords): shape_record = self._reader.shapeRecord(i) attributes = dict(zip(field_names, shape_record.record)) - yield Record(shape_record.shape, geometry_factory, attributes, - fields) + yield Record(shape_record.shape, attributes, fields) class FionaReader(object): @@ -293,6 +223,12 @@ d.update(feature['properties']) self._data.append(d) + def close(self): + # TODO: Keep the Fiona handle open until this is called. + # This will enable us to pass down calls for bounding box queries, + # rather than having to have it all in memory. + pass + def __len__(self): return len(self._data) @@ -374,7 +310,7 @@ # Define the NaturalEarth URL template. The natural earth website # returns a 302 status if accessing directly, so we use the naciscdn # URL directly. - _NE_URL_TEMPLATE = ('http://naciscdn.org/naturalearth/{resolution}' + _NE_URL_TEMPLATE = ('https://naciscdn.org/naturalearth/{resolution}' '/{category}/ne_{resolution}_{name}.zip') def __init__(self, diff -Nru python-cartopy-0.17.0+dfsg/lib/cartopy/io/srtm.py python-cartopy-0.18.0+dfsg/lib/cartopy/io/srtm.py --- python-cartopy-0.17.0+dfsg/lib/cartopy/io/srtm.py 2018-11-17 07:25:32.000000000 +0000 +++ python-cartopy-0.18.0+dfsg/lib/cartopy/io/srtm.py 2020-05-03 08:12:47.000000000 +0000 @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2011 - 2018, Met Office +# (C) British Crown Copyright 2011 - 2019, Met Office # # This file is part of cartopy. # @@ -242,7 +242,9 @@ Elevation is in meters. """ warnings.warn("This method has been deprecated. " - "See the \"What's new\" section for v0.12.") + "See the \"What's new\" section for v0.12.", + DeprecationWarning, + stacklevel=2) return SRTM3Source().single_tile(lon, lat) @@ -294,7 +296,9 @@ """ warnings.warn("The fill_gaps function has been deprecated. " - "See the \"What's new\" section for v0.14.") + "See the \"What's new\" section for v0.14.", + DeprecationWarning, + stacklevel=2) # Lazily import osgeo - it is only an optional dependency for cartopy. from osgeo import gdal from osgeo import gdal_array @@ -314,7 +318,9 @@ def srtm_composite(lon_min, lat_min, nx, ny): warnings.warn("This method has been deprecated. " - "See the \"What's new\" section for v0.12.") + "See the \"What's new\" section for v0.12.", + DeprecationWarning, + stacklevel=2) return SRTM3Source().combined(lon_min, lat_min, nx, ny) @@ -382,7 +388,9 @@ """ warnings.warn("This method has been deprecated. " - "See the \"What's new\" section for v0.12.") + "See the \"What's new\" section for v0.12.", + DeprecationWarning, + stacklevel=2) return SRTM3Source().srtm_fname(lon, lat) diff -Nru python-cartopy-0.17.0+dfsg/lib/cartopy/mpl/clip_path.py python-cartopy-0.18.0+dfsg/lib/cartopy/mpl/clip_path.py --- python-cartopy-0.17.0+dfsg/lib/cartopy/mpl/clip_path.py 2018-11-17 07:25:32.000000000 +0000 +++ python-cartopy-0.18.0+dfsg/lib/cartopy/mpl/clip_path.py 2020-05-03 08:12:47.000000000 +0000 @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2013 - 2018, Met Office +# (C) British Crown Copyright 2013 - 2019, Met Office # # This file is part of cartopy. # @@ -62,7 +62,9 @@ warnings.warn("This method has been deprecated. " "You can replace ``clip_path(subject, clip_bbox)`` by " "``subject.clip_to_bbox(clip_bbox)``. " - "See the \"What's new\" section for v0.17.") + "See the \"What's new\" section for v0.17.", + DeprecationWarning, + stacklevel=2) return subject.clip_to_bbox(clip_bbox) diff -Nru python-cartopy-0.17.0+dfsg/lib/cartopy/mpl/contour.py python-cartopy-0.18.0+dfsg/lib/cartopy/mpl/contour.py --- python-cartopy-0.17.0+dfsg/lib/cartopy/mpl/contour.py 1970-01-01 00:00:00.000000000 +0000 +++ python-cartopy-0.18.0+dfsg/lib/cartopy/mpl/contour.py 2020-05-03 08:12:47.000000000 +0000 @@ -0,0 +1,105 @@ +# (C) British Crown Copyright 2011 - 2020, 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 . + +from __future__ import (absolute_import, division, print_function) + +from matplotlib.contour import QuadContourSet +import matplotlib.path as mpath +import numpy as np + + +class GeoContourSet(QuadContourSet): + """ + A contourset designed to handle things like contour labels. + + """ + # nb. No __init__ method here - most of the time a GeoContourSet will + # come from GeoAxes.contour[f]. These methods morph a ContourSet by + # fiddling with instance.__class__. + + def clabel(self, *args, **kwargs): + # nb: contour labelling does not work very well for filled + # contours - it is recommended to only label line contours. + # This is especially true when inline=True. + + # This wrapper exist because mpl does not properly transform + # paths. Instead it simply assumes one path represents one polygon + # (not necessarily the case), and it assumes that + # transform(path.verts) is equivalent to transform_path(path). + # Unfortunately there is no way to easily correct this error, + # so we are forced to pre-transform the ContourSet's paths from + # the source coordinate system to the axes' projection. + # The existing mpl code then has a much simpler job of handling + # pre-projected paths (which can now effectively be transformed + # naively). + + for col in self.collections: + # Snaffle the collection's path list. We will change the + # list in-place (as the contour label code does in mpl). + paths = col.get_paths() + + # The ax attribute is deprecated in MPL 3.3 in favor of + # axes. So, here we test if axes is present and fall back + # on the old self.ax to support MPL versions less than 3.3 + if hasattr(self, "axes"): + data_t = self.axes.transData + else: + data_t = self.ax.transData + + # Define the transform that will take us from collection + # coordinates through to axes projection coordinates. + col_to_data = col.get_transform() - data_t + + # Now that we have the transform, project all of this + # collection's paths. + new_paths = [col_to_data.transform_path(path) for path in paths] + new_paths = [path for path in new_paths if path.vertices.size >= 1] + + # The collection will now be referenced in axes projection + # coordinates. + col.set_transform(data_t) + + # Clear the now incorrectly referenced paths. + del paths[:] + + for path in new_paths: + if path.vertices.size == 0: + # Don't persist empty paths. Let's get rid of them. + continue + + # Split the path if it has multiple MOVETO statements. + codes = np.array( + path.codes if path.codes is not None else [0]) + moveto = codes == mpath.Path.MOVETO + if moveto.sum() <= 1: + # This is only one path, so add it to the collection. + paths.append(path) + else: + # The first MOVETO doesn't need cutting-out. + moveto[0] = False + split_locs = np.flatnonzero(moveto) + + split_verts = np.split(path.vertices, split_locs) + split_codes = np.split(path.codes, split_locs) + + for verts, codes in zip(split_verts, split_codes): + # Add this path to the collection's list of paths. + paths.append(mpath.Path(verts, codes)) + + # Now that we have prepared the collection paths, call on + # through to the underlying implementation. + super(GeoContourSet, self).clabel(*args, **kwargs) diff -Nru python-cartopy-0.17.0+dfsg/lib/cartopy/mpl/feature_artist.py python-cartopy-0.18.0+dfsg/lib/cartopy/mpl/feature_artist.py --- python-cartopy-0.17.0+dfsg/lib/cartopy/mpl/feature_artist.py 2018-11-17 07:25:32.000000000 +0000 +++ python-cartopy-0.18.0+dfsg/lib/cartopy/mpl/feature_artist.py 2020-05-03 08:12:47.000000000 +0000 @@ -1,19 +1,9 @@ -# (C) British Crown Copyright 2011 - 2018, Met Office +# Copyright Cartopy Contributors # -# 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 . +# 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. + """ This module defines the :class:`FeatureArtist` class, for drawing :class:`Feature` instances with matplotlib. @@ -31,6 +21,7 @@ import matplotlib.collections import cartopy.mpl.patch as cpatch +from .style import merge as style_merge, finalize as style_finalize class _GeomKey(object): @@ -164,9 +155,9 @@ geoms = self._feature.intersecting_geometries(extent) # Combine all the keyword args in priority order. - prepared_kwargs = dict(self._feature.kwargs) - prepared_kwargs.update(self._kwargs) - prepared_kwargs.update(kwargs) + prepared_kwargs = style_merge(self._feature.kwargs, + self._kwargs, + kwargs) # Freeze the kwargs so that we can use them as a dict key. We will # need to unfreeze this with dict(frozen) before passing to mpl. @@ -206,8 +197,7 @@ style = prepared_kwargs else: # Unfreeze, then add the computed style, and then re-freeze. - style = dict(prepared_kwargs) - style.update(self._styler(geom)) + style = style_merge(dict(prepared_kwargs), self._styler(geom)) style = _freeze(style) stylised_paths.setdefault(style, []).extend(geom_paths) @@ -218,11 +208,11 @@ # of style items through to a single PathCollection, but that # complexity does not yet justify the effort. for style, paths in stylised_paths.items(): + style = style_finalize(dict(style)) # Build path collection and draw it. - c = matplotlib.collections.PathCollection( - paths, - transform=transform, - **dict(style)) + c = matplotlib.collections.PathCollection(paths, + transform=transform, + **style) c.set_clip_path(ax.patch) c.set_figure(ax.figure) c.draw(renderer) diff -Nru python-cartopy-0.17.0+dfsg/lib/cartopy/mpl/geoaxes.py python-cartopy-0.18.0+dfsg/lib/cartopy/mpl/geoaxes.py --- python-cartopy-0.17.0+dfsg/lib/cartopy/mpl/geoaxes.py 2018-11-17 07:25:32.000000000 +0000 +++ python-cartopy-0.18.0+dfsg/lib/cartopy/mpl/geoaxes.py 2020-05-03 08:12:47.000000000 +0000 @@ -1,19 +1,9 @@ -# (C) British Crown Copyright 2011 - 2018, Met Office +# Copyright Cartopy Contributors # -# 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 . +# 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. + """ This module defines the :class:`GeoAxes` class, for use with matplotlib. @@ -24,19 +14,27 @@ from __future__ import (absolute_import, division, print_function) +import six + import collections import contextlib +import functools import warnings import weakref +if not six.PY2: + import collections.abc as collections_abc +else: + import collections as collections_abc import matplotlib as mpl import matplotlib.artist import matplotlib.axes +import matplotlib.contour from matplotlib.image import imread import matplotlib.transforms as mtransforms import matplotlib.patches as mpatches import matplotlib.path as mpath -import matplotlib.ticker as mticker +import matplotlib.spines as mspines import numpy as np import numpy.ma as ma import shapely.geometry as sgeom @@ -45,6 +43,7 @@ import cartopy.crs as ccrs import cartopy.feature import cartopy.img_transform +import cartopy.mpl.contour import cartopy.mpl.feature_artist as feature_artist import cartopy.mpl.patch as cpatch from cartopy.mpl.slippy_image_artist import SlippyImageArtist @@ -54,7 +53,6 @@ assert mpl.__version__ >= '1.5.1', ('Cartopy is only supported with ' 'Matplotlib 1.5.1 or greater.') - _PATH_TRANSFORM_CACHE = weakref.WeakKeyDictionary() """ A nested mapping from path, source CRS, and target projection to the @@ -238,6 +236,81 @@ self.source_projection) +class _ViewClippedPathPatch(mpatches.PathPatch): + def __init__(self, axes, **kwargs): + self._original_path = mpath.Path(np.empty((0, 2))) + super(_ViewClippedPathPatch, self).__init__(self._original_path, + **kwargs) + self._axes = axes + + def set_boundary(self, path, transform): + self._original_path = path + self.set_transform(transform) + self.stale = True + + def _adjust_location(self): + if self.stale: + self._path = self._original_path.clip_to_bbox(self.axes.viewLim) + + @matplotlib.artist.allow_rasterization + def draw(self, renderer, *args, **kwargs): + self._adjust_location() + super(_ViewClippedPathPatch, self).draw(renderer, *args, **kwargs) + + +class GeoSpine(mspines.Spine): + def __init__(self, axes, **kwargs): + self._original_path = mpath.Path(np.empty((0, 2))) + kwargs.setdefault('clip_on', False) + super(GeoSpine, self).__init__(axes, 'geo', self._original_path, + **kwargs) + self.set_capstyle('butt') + + def set_boundary(self, path, transform): + self._original_path = path + self.set_transform(transform) + self.stale = True + + def _adjust_location(self): + if self.stale: + self._path = self._original_path.clip_to_bbox(self.axes.viewLim) + + def get_window_extent(self, renderer=None): + # make sure the location is updated so that transforms etc are + # correct: + self._adjust_location() + return super(GeoSpine, self).get_window_extent(renderer=renderer) + + @matplotlib.artist.allow_rasterization + def draw(self, renderer): + self._adjust_location() + ret = super(GeoSpine, self).draw(renderer) + self.stale = False + return ret + + def set_position(self, position): + raise NotImplementedError( + 'GeoSpine does not support changing its position.') + + +def _add_transform(func): + """A decorator that adds and validates the transform keyword argument.""" + @functools.wraps(func) + def wrapper(self, *args, **kwargs): + transform = kwargs.get('transform', None) + if transform is None: + transform = self.projection + if (isinstance(transform, ccrs.CRS) and + not isinstance(transform, ccrs.Projection)): + raise ValueError('Invalid transform: Spherical {} ' + 'is not supported - consider using ' + 'PlateCarree/RotatedPole.'.format(func.__name__)) + + kwargs['transform'] = transform + return func(self, *args, **kwargs) + return wrapper + + class GeoAxes(matplotlib.axes.Axes): """ A subclass of :class:`matplotlib.axes.Axes` which represents a @@ -279,17 +352,37 @@ self.projection = kwargs.pop('map_projection') """The :class:`cartopy.crs.Projection` of this GeoAxes.""" - self.outline_patch = None - """The patch that provides the line bordering the projection.""" - - self.background_patch = None - """The patch that provides the filled background of the projection.""" - super(GeoAxes, self).__init__(*args, **kwargs) self._gridliners = [] self.img_factories = [] self._done_img_factory = False + @property + def outline_patch(self): + """ + DEPRECATED. The patch that provides the line bordering the projection. + + Use GeoAxes.spines['geo'] or default Axes properties instead. + """ + warnings.warn("The outline_patch property is deprecated. Use " + "GeoAxes.spines['geo'] or the default Axes properties " + "instead.", + DeprecationWarning, + stacklevel=2) + return self.spines['geo'] + + @property + def background_patch(self): + """ + DEPRECATED. The patch that provides the filled background of the + projection. + """ + warnings.warn('The background_patch property is deprecated. ' + 'Use GeoAxes.patch instead.', + DeprecationWarning, + stacklevel=2) + return self.patch + def add_image(self, factory, *args, **kwargs): """ Add an image "factory" to the Axes. @@ -348,7 +441,7 @@ self._autoscaleXon, self._autoscaleYon) = other @matplotlib.artist.allow_rasterization - def draw(self, renderer=None, inframe=False): + def draw(self, renderer=None, **kwargs): """ Extend the standard behaviour of :func:`matplotlib.axes.Axes.draw`. @@ -359,18 +452,17 @@ """ # If data has been added (i.e. autoscale hasn't been turned off) # then we should autoscale the view. + if self.get_autoscale_on() and self.ignore_existing_data_limits: self.autoscale_view() - if self.outline_patch.reclip or self.background_patch.reclip: - clipped_path = self.outline_patch.orig_path.clip_to_bbox( - self.viewLim) - self.outline_patch._path = clipped_path - self.background_patch._path = clipped_path + # Adjust location of background patch so that new gridlines below are + # clipped correctly. + self.patch._adjust_location() + self.apply_aspect() for gl in self._gridliners: - gl._draw_gridliner(background_patch=self.background_patch) - self._gridliners = [] + gl._draw_gridliner(renderer=renderer) # XXX This interface needs a tidy up: # image drawing on pan/zoom; @@ -384,8 +476,42 @@ transform=factory.crs, *args[1:], **kwargs) self._done_img_factory = True - return matplotlib.axes.Axes.draw(self, renderer=renderer, - inframe=inframe) + return matplotlib.axes.Axes.draw(self, renderer=renderer, **kwargs) + + def _update_title_position(self, renderer): + matplotlib.axes.Axes._update_title_position(self, renderer) + if not self._gridliners: + return + + if self._autotitlepos is not None and not self._autotitlepos: + return + + # Get the max ymax of all top labels + top = -1 + for gl in self._gridliners: + if gl.has_labels(): + for label in (gl.top_label_artists + + gl.left_label_artists + + gl.right_label_artists): + # we skip bottom labels because they are usually + # not at the top + bb = label.get_tightbbox(renderer) + top = max(top, bb.ymax) + if top < 0: + # nothing to do if no label found + return + yn = self.transAxes.inverted().transform((0., top))[1] + if yn <= 1: + # nothing to do if the upper bounds of labels is below + # the top of the axes + return + + # Loop on titles to adjust + titles = (self.title, self._left_title, self._right_title) + for title in titles: + x, y0 = title.get_position() + y = max(1.0, yn) + title.set_position((x, y)) def __str__(self): return '< GeoAxes: %s >' % self.projection @@ -399,8 +525,7 @@ self._tight = True self.set_aspect('equal') - with self.hold_limits(): - self._boundary() + self._boundary() # XXX consider a margin - but only when the map is not global... # self._xmargin = 0.15 @@ -426,22 +551,26 @@ return u'%.4g, %.4g (%f\u00b0%s, %f\u00b0%s)' % (x, y, abs(lat), ns, abs(lon), ew) - def coastlines(self, resolution='110m', color='black', **kwargs): + def coastlines(self, resolution='auto', color='black', **kwargs): """ Add coastal **outlines** to the current axes from the Natural Earth "coastline" shapefile collection. Parameters ---------- - resolution + resolution : str or :class:`cartopy.feature.Scaler`, optional A named resolution to use from the Natural Earth - dataset. Currently can be one of "110m", "50m", and "10m", - or a Scaler object. + dataset. Currently can be one of "auto" (default), "110m", "50m", + and "10m", or a Scaler object. """ kwargs['edgecolor'] = color kwargs['facecolor'] = 'none' feature = cartopy.feature.COASTLINE + + # The coastline feature is automatically scaled by default, but for + # anything else, including custom scaler instances, create a new + # feature which derives from the default one. if resolution != 'auto': feature = feature.with_scale(resolution) @@ -492,10 +621,9 @@ if lons.shape != lats.shape: raise ValueError('lons and lats must have the same shape.') - for i in range(len(lons)): - circle = geod.circle(lons[i], lats[i], rad_km*1e3, - n_samples=n_samples) - geoms.append(sgeom.Polygon(circle)) + for lon, lat in zip(lons, lats): + circle = geod.circle(lon, lat, rad_km*1e3, n_samples=n_samples) + geoms.append(sgeom.Polygon(circle)) feature = cartopy.feature.ShapelyFeature(geoms, ccrs.Geodetic(), **kwargs) @@ -526,7 +654,9 @@ """ warnings.warn('This method has been deprecated.' - ' Please use `add_feature` instead.') + ' Please use `add_feature` instead.', + DeprecationWarning, + stacklevel=2) kwargs.setdefault('edgecolor', 'face') kwargs.setdefault('facecolor', cartopy.feature.COLORS['land']) feature = cartopy.feature.NaturalEarthFeature(category, name, @@ -659,9 +789,8 @@ Parameters ---------- - extent + extents Tuple of floats representing the required extent (x0, x1, y0, y1). - """ # TODO: Implement the same semantics as plt.xlim and # plt.ylim - allowing users to set None for a minimum and/or @@ -701,6 +830,7 @@ 'y_limits=[{ylim[0]}, {ylim[1]}]).') raise ValueError(msg.format(xlim=self.projection.x_limits, ylim=self.projection.y_limits)) + self.set_xlim([x1, x2]) self.set_ylim([y1, y2]) @@ -779,7 +909,7 @@ # Switch on drawing of x axis self.xaxis.set_visible(True) - return super(GeoAxes, self).set_xticks(xticks, minor) + return super(GeoAxes, self).set_xticks(xticks, minor=minor) def set_yticks(self, ticks, minor=False, crs=None): """ @@ -826,7 +956,7 @@ # Switch on drawing of y axis self.yaxis.set_visible(True) - return super(GeoAxes, self).set_yticks(yticks, minor) + return super(GeoAxes, self).set_yticks(yticks, minor=minor) def stock_img(self, name='ne_shaded'): """ @@ -1060,7 +1190,7 @@ plotting methods. """ - if not isinstance(regrid_shape, collections.Sequence): + if not isinstance(regrid_shape, collections_abc.Sequence): target_size = int(regrid_shape) x_range, y_range = np.diff(target_extent)[::2] desired_aspect = x_range / y_range @@ -1070,6 +1200,7 @@ regrid_shape = (target_size, int(target_size / desired_aspect)) return regrid_shape + @_add_transform def imshow(self, img, *args, **kwargs): """ Add the "transform" keyword to :func:`~matplotlib.pyplot.imshow'. @@ -1103,23 +1234,34 @@ origin: {'lower', 'upper'} The origin of the vertical pixels. See :func:`matplotlib.pyplot.imshow` for further details. - Default is ``'lower'``. + Default is ``'upper'``. Prior to 0.18, it was ``'lower'``. """ - transform = kwargs.pop('transform', None) if 'update_datalim' in kwargs: raise ValueError('The update_datalim keyword has been removed in ' 'imshow. To hold the data and view limits see ' 'GeoAxes.hold_limits.') - kwargs.setdefault('origin', 'lower') + transform = kwargs.pop('transform') + extent = kwargs.get('extent', None) + kwargs.setdefault('origin', 'upper') same_projection = (isinstance(transform, ccrs.Projection) and self.projection == transform) - if transform is None or transform == self.transData or same_projection: - if isinstance(transform, ccrs.Projection): - transform = transform._as_mpl_transform(self) + # Only take the shortcut path if the image is within the current + # bounds (+/- threshold) of the projection + x0, x1 = self.projection.x_limits + y0, y1 = self.projection.y_limits + eps = self.projection.threshold + inside_bounds = (extent is None or + (x0 - eps <= extent[0] <= x1 + eps and + x0 - eps <= extent[1] <= x1 + eps and + y0 - eps <= extent[2] <= y1 + eps and + y0 - eps <= extent[3] <= y1 + eps)) + + if (transform is None or transform == self.transData or + same_projection and inside_bounds): result = matplotlib.axes.Axes.imshow(self, img, *args, **kwargs) else: extent = kwargs.pop('extent', None) @@ -1166,16 +1308,13 @@ result = matplotlib.axes.Axes.imshow(self, img, *args, extent=extent, **kwargs) - # clip the image. This does not work as the patch moves with mouse - # movement, but the clip path doesn't - # This could definitely be fixed in matplotlib -# if result.get_clip_path() in [None, self.patch]: -# # image does not already have clipping set, clip to axes patch -# result.set_clip_path(self.outline_patch) return result - def gridlines(self, crs=None, draw_labels=False, xlocs=None, - ylocs=None, **kwargs): + def gridlines(self, crs=None, draw_labels=False, + xlocs=None, ylocs=None, dms=False, + x_inline=None, y_inline=None, auto_inline=True, + xformatter=None, yformatter=None, + **kwargs): """ Automatically add gridlines to the axes, in the given coordinate system, at draw time. @@ -1200,31 +1339,65 @@ used to determine the locations of the gridlines in the y-coordinate of the given CRS. Defaults to None, which implies automatic locating of the gridlines. + dms: bool + When default longitude and latitude locators and formatters are + used, ticks are able to stop on minutes and seconds if minutes is + set to True, and not fraction of degrees. This keyword is passed + to :class:`~cartopy.mpl.gridliner.Gridliner` and has no effect + if xlocs and ylocs are explicitly set. + x_inline: optional + Toggle whether the x labels drawn should be inline. + y_inline: optional + Toggle whether the y labels drawn should be inline. + auto_inline: optional + Set x_inline and y_inline automatically based on projection + xformatter: optional + A :class:`matplotlib.ticker.Formatter` instance to format labels + for x-coordinate gridlines. It defaults to None, which implies the + use of a :class:`cartopy.mpl.ticker.LongitudeFormatter` initiated + with the ``dms`` argument, if the crs is of + :class:`~cartopy.crs.PlateCarree` type. + yformatter: optional + A :class:`matplotlib.ticker.Formatter` instance to format labels + for y-coordinate gridlines. It defaults to None, which implies the + use of a :class:`cartopy.mpl.ticker.LatitudeFormatter` initiated + with the ``dms`` argument, if the crs is of + :class:`~cartopy.crs.PlateCarree` type. + + Keyword Parameters + ------------------ + **kwargs + All other keywords control line properties. These are passed + through to :class:`matplotlib.collections.Collection`. Returns ------- gridliner A :class:`cartopy.mpl.gridliner.Gridliner` instance. - Note - ---- - All other keywords control line properties. These are passed - through to :class:`matplotlib.collections.Collection`. - + Notes + ----- + The "x" and "y" for locations and inline settings do not necessarily + correspond to X and Y, but to the first and second coordinates of the + specified CRS. For the common case of PlateCarree gridlines, these + correspond to longitudes and latitudes. Depending on the projection + used for the map, meridians and parallels can cross both the X axis and + the Y axis. """ if crs is None: crs = ccrs.PlateCarree() from cartopy.mpl.gridliner import Gridliner - if xlocs is not None and not isinstance(xlocs, mticker.Locator): - xlocs = mticker.FixedLocator(xlocs) - if ylocs is not None and not isinstance(ylocs, mticker.Locator): - ylocs = mticker.FixedLocator(ylocs) gl = Gridliner( self, crs=crs, draw_labels=draw_labels, xlocator=xlocs, - ylocator=ylocs, collection_kwargs=kwargs) + ylocator=ylocs, collection_kwargs=kwargs, dms=dms, + x_inline=x_inline, y_inline=y_inline, auto_inline=auto_inline, + xformatter=xformatter, yformatter=yformatter) self._gridliners.append(gl) return gl + def _gen_axes_patch(self): + return _ViewClippedPathPatch(self) + def _gen_axes_spines(self, locations=None, offset=0.0, units='inches'): # generate some axes spines, as some Axes super class machinery # requires them. Just make them invisible @@ -1234,29 +1407,17 @@ units=units) for spine in spines.values(): spine.set_visible(False) + + spines['geo'] = GeoSpine(self) return spines def _boundary(self): """ - Add the map's boundary to this GeoAxes, attaching the appropriate - artists to :data:`.outline_patch` and :data:`.background_patch`. + Add the map's boundary to this GeoAxes. - Note - ---- - The boundary is not the ``axes.patch``. ``axes.patch`` - is made invisible by this method - its only remaining - purpose is to provide a rectilinear clip patch for - all Axes artists. + The :data:`.patch` and :data:`.spines['geo']` are updated to match. """ - # Hide the old "background" patch used by matplotlib - it is not - # used by cartopy's GeoAxes. - self.patch.set_facecolor((1, 1, 1, 0)) - self.patch.set_edgecolor((0.5, 0.5, 0.5)) - self.patch.set_visible(False) - self.background_patch = None - self.outline_patch = None - path, = cpatch.geos_to_path(self.projection.boundary) # Get the outline path in terms of self.transData @@ -1264,17 +1425,16 @@ trans_path = proj_to_data.transform_path(path) # Set the boundary - we can make use of the rectangular clipping. - self.set_boundary(trans_path, use_as_clip_path=False) + self.set_boundary(trans_path) # Attach callback events for when the xlim or ylim are changed. This # is what triggers the patches to be re-clipped at draw time. self.callbacks.connect('xlim_changed', _trigger_patch_reclip) self.callbacks.connect('ylim_changed', _trigger_patch_reclip) - def set_boundary(self, path, transform=None, use_as_clip_path=True): + def set_boundary(self, path, transform=None, use_as_clip_path=None): """ - Given a path, update the :data:`.outline_patch` and - :data:`.background_patch` to take its shape. + Given a path, update :data:`.spines['geo']` and :data:`.patch`. Parameters ---------- @@ -1285,63 +1445,26 @@ this must be convertible to data coordinates, and therefore cannot extend beyond the limits of the axes' projection. - use_as_clip_path : bool, optional - Whether axes.patch should be updated. - Updating axes.patch means that any artists - subsequently created will inherit clipping - from this path, rather than the standard unit - square in axes coordinates. """ + if use_as_clip_path is not None: + warnings.warn( + 'Passing use_as_clip_path to set_boundary is deprecated.', + DeprecationWarning, + stacklevel=2) + if transform is None: transform = self.transData if isinstance(transform, cartopy.crs.CRS): transform = transform._as_mpl_transform(self) - if self.background_patch is None: - background = mpatches.PathPatch(path, edgecolor='none', - facecolor='white', zorder=-2, - clip_on=False, transform=transform) - else: - background = mpatches.PathPatch(path, zorder=-2, clip_on=False) - background.update_from(self.background_patch) - self.background_patch.remove() - background.set_transform(transform) - - if self.outline_patch is None: - outline = mpatches.PathPatch(path, edgecolor='black', - facecolor='none', zorder=2.5, - clip_on=False, transform=transform) - else: - outline = mpatches.PathPatch(path, zorder=2.5, clip_on=False) - outline.update_from(self.outline_patch) - self.outline_patch.remove() - outline.set_transform(transform) - # Attach the original path to the patches. This will be used each time # a new clipped path is calculated. - outline.orig_path = path - background.orig_path = path - - # Attach a "reclip" attribute, which determines if the patch's path is - # reclipped before drawing. A callback is used to change the "reclip" - # state. - outline.reclip = True - background.reclip = True - - # Add the patches to the axes, and also make them available as - # attributes. - self.background_patch = background - self.outline_patch = outline - - if use_as_clip_path: - self.patch = background - - with self.hold_limits(): - self.add_patch(outline) - self.add_patch(background) + self.patch.set_boundary(path, transform) + self.spines['geo'].set_boundary(path, transform) + @_add_transform def contour(self, *args, **kwargs): """ Add the "transform" keyword to :func:`~matplotlib.pyplot.contour'. @@ -1352,22 +1475,16 @@ A :class:`~cartopy.crs.Projection`. """ - t = kwargs.get('transform', None) - if t is None: - t = self.projection - if isinstance(t, ccrs.CRS) and not isinstance(t, ccrs.Projection): - raise ValueError('invalid transform:' - ' Spherical contouring is not supported - ' - ' consider using PlateCarree/RotatedPole.') - if isinstance(t, ccrs.Projection): - kwargs['transform'] = t._as_mpl_transform(self) - else: - kwargs['transform'] = t result = matplotlib.axes.Axes.contour(self, *args, **kwargs) self.autoscale_view() + + # Re-cast the contour as a GeoContourSet. + if isinstance(result, matplotlib.contour.QuadContourSet): + result.__class__ = cartopy.mpl.contour.GeoContourSet return result + @_add_transform def contourf(self, *args, **kwargs): """ Add the "transform" keyword to :func:`~matplotlib.pyplot.contourf'. @@ -1378,18 +1495,9 @@ A :class:`~cartopy.crs.Projection`. """ - t = kwargs.get('transform', None) - if t is None: - t = self.projection - if isinstance(t, ccrs.CRS) and not isinstance(t, ccrs.Projection): - raise ValueError('invalid transform:' - ' Spherical contouring is not supported - ' - ' consider using PlateCarree/RotatedPole.') + t = kwargs['transform'] if isinstance(t, ccrs.Projection): kwargs['transform'] = t = t._as_mpl_transform(self) - else: - kwargs['transform'] = t - # Set flag to indicate correcting orientation of paths if not ccw if isinstance(t, mtransforms.Transform): for sub_trans, _ in t._iter_break_from_left_to_right(): @@ -1400,14 +1508,22 @@ result = matplotlib.axes.Axes.contourf(self, *args, **kwargs) # We need to compute the dataLim correctly for contours. - extent = mtransforms.Bbox.union([col.get_datalim(self.transData) - for col in result.collections - if col.get_paths()]) - self.dataLim.update_from_data_xy(extent.get_points()) + bboxes = [col.get_datalim(self.transData) + for col in result.collections + if col.get_paths()] + if bboxes: + extent = mtransforms.Bbox.union(bboxes) + self.dataLim.update_from_data_xy(extent.get_points()) self.autoscale_view() + + # Re-cast the contour as a GeoContourSet. + if isinstance(result, matplotlib.contour.QuadContourSet): + result.__class__ = cartopy.mpl.contour.GeoContourSet + return result + @_add_transform def scatter(self, *args, **kwargs): """ Add the "transform" keyword to :func:`~matplotlib.pyplot.scatter'. @@ -1418,19 +1534,12 @@ A :class:`~cartopy.crs.Projection`. """ - t = kwargs.get('transform', None) - # Keep this bit - even at mpl v1.2 - if t is None: - t = self.projection - if hasattr(t, '_as_mpl_transform'): - kwargs['transform'] = t._as_mpl_transform(self) - # exclude Geodetic as a valid source CS - if (isinstance(kwargs.get('transform', None), + if (isinstance(kwargs['transform'], InterProjectionTransform) and kwargs['transform'].source_projection.is_geodetic()): raise ValueError('Cartopy cannot currently do spherical ' - 'contouring. The source CRS cannot be a ' + 'scatter. The source CRS cannot be a ' 'geodetic, consider using the cyllindrical form ' '(PlateCarree or RotatedPole).') @@ -1438,6 +1547,7 @@ self.autoscale_view() return result + @_add_transform def pcolormesh(self, *args, **kwargs): """ Add the "transform" keyword to :func:`~matplotlib.pyplot.pcolormesh'. @@ -1448,14 +1558,6 @@ A :class:`~cartopy.crs.Projection`. """ - t = kwargs.get('transform', None) - if t is None: - t = self.projection - if isinstance(t, ccrs.CRS) and not isinstance(t, ccrs.Projection): - raise ValueError('invalid transform:' - ' Spherical pcolormesh is not supported - ' - ' consider using PlateCarree/RotatedPole.') - kwargs.setdefault('transform', t) result = self._pcolormesh_patched(*args, **kwargs) self.autoscale_view() return result @@ -1469,7 +1571,6 @@ See PATCH comments below. """ - import warnings import matplotlib.colors as mcolors import matplotlib.collections as mcoll @@ -1482,13 +1583,20 @@ cmap = kwargs.pop('cmap', None) vmin = kwargs.pop('vmin', None) vmax = kwargs.pop('vmax', None) - shading = kwargs.pop('shading', 'flat').lower() antialiased = kwargs.pop('antialiased', False) kwargs.setdefault('edgecolors', 'None') - allmatch = (shading == 'gouraud') + if matplotlib.__version__ < "3.3": + shading = kwargs.pop('shading', 'flat') + allmatch = (shading == 'gouraud') + X, Y, C = self._pcolorargs('pcolormesh', *args, allmatch=allmatch) + else: + shading = kwargs.pop('shading', 'auto') + if shading is None: + shading = 'auto' + X, Y, C, shading = self._pcolorargs('pcolormesh', *args, + shading=shading) - X, Y, C = self._pcolorargs('pcolormesh', *args, allmatch=allmatch) Ny, Nx = X.shape # convert to one dimensional arrays @@ -1582,7 +1690,8 @@ if collection.get_cmap()._rgba_bad[3] != 0.0: warnings.warn("The colormap's 'bad' has been set, but " "in order to wrap pcolormesh across the " - "map it must be fully transparent.") + "map it must be fully transparent.", + stacklevel=3) # at this point C has a shape of (Ny-1, Nx-1), to_mask has # a shape of (Ny, Nx-1) and pts has a shape of (Ny*Nx, 2) @@ -1635,15 +1744,12 @@ # this method collection._wrapped_collection_fix = pcolor_col - # Clip the QuadMesh to the projection boundary, which is required - # to keep the shading inside the projection bounds. - collection.set_clip_path(self.outline_patch) - # END OF PATCH ############## return collection + @_add_transform def pcolor(self, *args, **kwargs): """ Add the "transform" keyword to :func:`~matplotlib.pyplot.pcolor'. @@ -1654,14 +1760,6 @@ A :class:`~cartopy.crs.Projection`. """ - t = kwargs.get('transform', None) - if t is None: - t = self.projection - if isinstance(t, ccrs.CRS) and not isinstance(t, ccrs.Projection): - raise ValueError('invalid transform:' - ' Spherical pcolor is not supported - ' - ' consider using PlateCarree/RotatedPole.') - kwargs.setdefault('transform', t) result = matplotlib.axes.Axes.pcolor(self, *args, **kwargs) # Update the datalim for this pcolor. @@ -1671,6 +1769,7 @@ self.autoscale_view() return result + @_add_transform def quiver(self, x, y, u, v, *args, **kwargs): """ Plot a field of arrows. @@ -1714,17 +1813,7 @@ grid northward. """ - t = kwargs.get('transform', None) - if t is None: - t = self.projection - if isinstance(t, ccrs.CRS) and not isinstance(t, ccrs.Projection): - raise ValueError('invalid transform:' - ' Spherical quiver is not supported - ' - ' consider using PlateCarree/RotatedPole.') - if isinstance(t, ccrs.Projection): - kwargs['transform'] = t._as_mpl_transform(self) - else: - kwargs['transform'] = t + t = kwargs['transform'] regrid_shape = kwargs.pop('regrid_shape', None) target_extent = kwargs.pop('target_extent', self.get_extent(self.projection)) @@ -1752,6 +1841,7 @@ u, v = self.projection.transform_vectors(t, x, y, u, v) return matplotlib.axes.Axes.quiver(self, x, y, u, v, *args, **kwargs) + @_add_transform def barbs(self, x, y, u, v, *args, **kwargs): """ Plot a field of barbs. @@ -1795,17 +1885,7 @@ grid northward. """ - t = kwargs.get('transform', None) - if t is None: - t = self.projection - if isinstance(t, ccrs.CRS) and not isinstance(t, ccrs.Projection): - raise ValueError('invalid transform:' - ' Spherical barbs are not supported - ' - ' consider using PlateCarree/RotatedPole.') - if isinstance(t, ccrs.Projection): - kwargs['transform'] = t._as_mpl_transform(self) - else: - kwargs['transform'] = t + t = kwargs['transform'] regrid_shape = kwargs.pop('regrid_shape', None) target_extent = kwargs.pop('target_extent', self.get_extent(self.projection)) @@ -1833,6 +1913,7 @@ u, v = self.projection.transform_vectors(t, x, y, u, v) return matplotlib.axes.Axes.barbs(self, x, y, u, v, *args, **kwargs) + @_add_transform def streamplot(self, x, y, u, v, **kwargs): """ Plot streamlines of a vector flow. @@ -1863,13 +1944,7 @@ grid northward. """ - t = kwargs.pop('transform', None) - if t is None: - t = self.projection - if isinstance(t, ccrs.CRS) and not isinstance(t, ccrs.Projection): - raise ValueError('invalid transform:' - ' Spherical streamplot is not supported - ' - ' consider using PlateCarree/RotatedPole.') + t = kwargs.pop('transform') # Regridding is required for streamplot, it must have an evenly spaced # grid to work correctly. Choose our destination grid based on the # density keyword. The grid need not be bigger than the grid used by @@ -1978,11 +2053,11 @@ def _trigger_patch_reclip(event): """ - Define an event callback for a GeoAxes which forces the outline and - background patches to be re-clipped next time they are drawn. + Define an event callback for a GeoAxes which forces the background patch to + be re-clipped next time it is drawn. """ axes = event.axes # trigger the outline and background patches to be re-clipped - axes.outline_patch.reclip = True - axes.background_patch.reclip = True + axes.spines['geo'].stale = True + axes.patch.stale = True diff -Nru python-cartopy-0.17.0+dfsg/lib/cartopy/mpl/gridliner.py python-cartopy-0.18.0+dfsg/lib/cartopy/mpl/gridliner.py --- python-cartopy-0.17.0+dfsg/lib/cartopy/mpl/gridliner.py 2018-11-17 07:25:32.000000000 +0000 +++ python-cartopy-0.18.0+dfsg/lib/cartopy/mpl/gridliner.py 2020-05-03 08:12:47.000000000 +0000 @@ -1,36 +1,45 @@ -# (C) British Crown Copyright 2011 - 2018, Met Office +# Copyright Cartopy Contributors # -# 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 . +# 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. from __future__ import (absolute_import, division, print_function) +import operator +import warnings + import matplotlib import matplotlib.collections as mcollections -import matplotlib.text as mtext import matplotlib.ticker as mticker import matplotlib.transforms as mtrans +import matplotlib.path as mpath import numpy as np +import shapely.geometry as sgeom import cartopy from cartopy.crs import Projection, _RectangularProjection - +from cartopy.mpl.ticker import ( + LongitudeLocator, LatitudeLocator, + LongitudeFormatter, LatitudeFormatter) degree_locator = mticker.MaxNLocator(nbins=9, steps=[1, 1.5, 1.8, 2, 3, 6, 10]) +classic_locator = mticker.MaxNLocator(nbins=9) +classic_formatter = mticker.ScalarFormatter _DEGREE_SYMBOL = u'\u00B0' +_X_INLINE_PROJS = ( + cartopy.crs.InterruptedGoodeHomolosine, + cartopy.crs.LambertConformal, + cartopy.crs.Mollweide, + cartopy.crs.Sinusoidal, + cartopy.crs.RotatedPole, +) +_POLAR_PROJS = ( + cartopy.crs.NorthPolarStereo, + cartopy.crs.SouthPolarStereo, + cartopy.crs.Stereographic +) def _fix_lons(lons): @@ -45,7 +54,7 @@ return fixed_lons -def _lon_heimisphere(longitude): +def _lon_hemisphere(longitude): """Return the hemisphere (E, W or '' for 0) for the given longitude.""" longitude = _fix_lons(longitude) if longitude > 0: @@ -57,7 +66,7 @@ return hemisphere -def _lat_heimisphere(latitude): +def _lat_hemisphere(latitude): """Return the hemisphere (N, S or '' for 0) for the given latitude.""" if latitude > 0: hemisphere = 'N' @@ -71,14 +80,14 @@ def _east_west_formatted(longitude, num_format='g'): fmt_string = u'{longitude:{num_format}}{degree}{hemisphere}' return fmt_string.format(longitude=abs(longitude), num_format=num_format, - hemisphere=_lon_heimisphere(longitude), + hemisphere=_lon_hemisphere(longitude), degree=_DEGREE_SYMBOL) def _north_south_formatted(latitude, num_format='g'): fmt_string = u'{latitude:{num_format}}{degree}{hemisphere}' return fmt_string.format(latitude=abs(latitude), num_format=num_format, - hemisphere=_lat_heimisphere(latitude), + hemisphere=_lat_hemisphere(latitude), degree=_DEGREE_SYMBOL) @@ -96,7 +105,9 @@ # method on draw. This will enable automatic gridline resolution # determination on zoom/pan. def __init__(self, axes, crs, draw_labels=False, xlocator=None, - ylocator=None, collection_kwargs=None): + ylocator=None, collection_kwargs=None, + xformatter=None, yformatter=None, dms=False, + x_inline=None, y_inline=None, auto_inline=True): """ Object used by :meth:`cartopy.mpl.geoaxes.GeoAxes.gridlines` to add gridlines and tick labels to a map. @@ -121,40 +132,116 @@ to determine the locations of the gridlines in the y-coordinate of the given CRS. Defaults to None, which implies automatic locating of the gridlines. + xformatter: optional + A :class:`matplotlib.ticker.Formatter` instance to format labels + for x-coordinate gridlines. It defaults to None, which implies the + use of a :class:`cartopy.mpl.ticker.LongitudeFormatter` initiated + with the ``dms`` argument, if the crs is of + :class:`~cartopy.crs.PlateCarree` type. + yformatter: optional + A :class:`matplotlib.ticker.Formatter` instance to format labels + for y-coordinate gridlines. It defaults to None, which implies the + use of a :class:`cartopy.mpl.ticker.LatitudeFormatter` initiated + with the ``dms`` argument, if the crs is of + :class:`~cartopy.crs.PlateCarree` type. collection_kwargs: optional Dictionary controlling line properties, passed to :class:`matplotlib.collections.Collection`. Defaults to None. - + dms: bool + When default locators and formatters are used, + ticks are able to stop on minutes and seconds if minutes + is set to True, and not fraction of degrees. + x_inline: optional + Toggle whether the x labels drawn should be inline. + y_inline: optional + Toggle whether the y labels drawn should be inline. + auto_inline: optional + Set x_inline and y_inline automatically based on projection. + + Notes + ----- + The "x" and "y" labels for locators and formatters do not necessarily + correspond to X and Y, but to the first and second coordinates of the + specified CRS. For the common case of PlateCarree gridlines, these + correspond to longitudes and latitudes. Depending on the projection + used for the map, meridians and parallels can cross both the X axis and + the Y axis. """ self.axes = axes #: The :class:`~matplotlib.ticker.Locator` to use for the x #: gridlines and labels. - self.xlocator = xlocator or degree_locator + if xlocator is not None: + if not isinstance(xlocator, mticker.Locator): + xlocator = mticker.FixedLocator(xlocator) + self.xlocator = xlocator + elif isinstance(crs, cartopy.crs.PlateCarree): + self.xlocator = LongitudeLocator(dms=dms) + else: + self.xlocator = classic_locator #: The :class:`~matplotlib.ticker.Locator` to use for the y #: gridlines and labels. - self.ylocator = ylocator or degree_locator + if ylocator is not None: + if not isinstance(ylocator, mticker.Locator): + ylocator = mticker.FixedLocator(ylocator) + self.ylocator = ylocator + elif isinstance(crs, cartopy.crs.PlateCarree): + self.ylocator = LatitudeLocator(dms=dms) + else: + self.ylocator = classic_locator - #: The :class:`~matplotlib.ticker.Formatter` to use for the x labels. - self.xformatter = mticker.ScalarFormatter() - self.xformatter.create_dummy_axis() - - #: The :class:`~matplotlib.ticker.Formatter` to use for the y labels. - self.yformatter = mticker.ScalarFormatter() - self.yformatter.create_dummy_axis() + if xformatter is None: + if isinstance(crs, cartopy.crs.PlateCarree): + xformatter = LongitudeFormatter(dms=dms) + else: + xformatter = classic_formatter() + #: The :class:`~matplotlib.ticker.Formatter` to use for the lon labels. + self.xformatter = xformatter + + if yformatter is None: + if isinstance(crs, cartopy.crs.PlateCarree): + yformatter = LatitudeFormatter(dms=dms) + else: + yformatter = classic_formatter() + #: The :class:`~matplotlib.ticker.Formatter` to use for the lat labels. + self.yformatter = yformatter #: Whether to draw labels on the top of the map. - self.xlabels_top = draw_labels + self.top_labels = draw_labels #: Whether to draw labels on the bottom of the map. - self.xlabels_bottom = draw_labels + self.bottom_labels = draw_labels #: Whether to draw labels on the left hand side of the map. - self.ylabels_left = draw_labels + self.left_labels = draw_labels #: Whether to draw labels on the right hand side of the map. - self.ylabels_right = draw_labels + self.right_labels = draw_labels + + if auto_inline: + if isinstance(self.axes.projection, _X_INLINE_PROJS): + self.x_inline = True + self.y_inline = False + elif isinstance(self.axes.projection, _POLAR_PROJS): + self.x_inline = False + self.y_inline = True + else: + self.x_inline = False + self.y_inline = False + + # overwrite auto_inline if necessary + if x_inline is not None: + #: Whether to draw x labels inline + self.x_inline = x_inline + elif not auto_inline: + self.x_inline = False + + if y_inline is not None: + #: Whether to draw y labels inline + self.y_inline = y_inline + elif not auto_inline: + self.y_inline = False #: Whether to draw the x gridlines. self.xlines = True @@ -176,17 +263,21 @@ #: The padding from the map edge to the y labels in points. self.ypadding = 5 + #: Allow the rotation of labels. + self.rotate_labels = True + + # Current transform self.crs = crs # if the user specifies tick labels at this point, check if they can # be drawn. The same check will take place at draw time in case # public attributes are changed after instantiation. - if draw_labels: + if draw_labels and not (x_inline or y_inline or auto_inline): self._assert_can_draw_ticks() #: The number of interpolation points which are used to draw the #: gridlines. - self.n_steps = 30 + self.n_steps = 100 #: A dictionary passed through to #: ``matplotlib.collections.LineCollection`` on grid line creation. @@ -198,11 +289,73 @@ #: The y gridlines which were created at draw time. self.yline_artists = [] - #: The x labels which were created at draw time. - self.xlabel_artists = [] + # Plotted status + self._plotted = False - #: The y labels which were created at draw time. - self.ylabel_artists = [] + # Check visibility of labels at each draw event + # (or once drawn, only at resize event ?) + self.axes.figure.canvas.mpl_connect('draw_event', self._draw_event) + + @property + def xlabels_top(self): + warnings.warn('The .xlabels_top attribute is deprecated. Please ' + 'use .top_labels to toggle visibility instead.') + return self.top_labels + + @xlabels_top.setter + def xlabels_top(self, value): + warnings.warn('The .xlabels_top attribute is deprecated. Please ' + 'use .top_labels to toggle visibility instead.') + self.top_labels = value + + @property + def xlabels_bottom(self): + warnings.warn('The .xlabels_bottom attribute is deprecated. Please ' + 'use .bottom_labels to toggle visibility instead.') + return self.bottom_labels + + @xlabels_bottom.setter + def xlabels_bottom(self, value): + warnings.warn('The .xlabels_bottom attribute is deprecated. Please ' + 'use .bottom_labels to toggle visibility instead.') + self.bottom_labels = value + + @property + def ylabels_left(self): + warnings.warn('The .ylabels_left attribute is deprecated. Please ' + 'use .left_labels to toggle visibility instead.') + return self.left_labels + + @ylabels_left.setter + def ylabels_left(self, value): + warnings.warn('The .ylabels_left attribute is deprecated. Please ' + 'use .left_labels to toggle visibility instead.') + self.left_labels = value + + @property + def ylabels_right(self): + warnings.warn('The .ylabels_right attribute is deprecated. Please ' + 'use .right_labels to toggle visibility instead.') + return self.right_labels + + @ylabels_right.setter + def ylabels_right(self, value): + warnings.warn('The .ylabels_right attribute is deprecated. Please ' + 'use .right_labels to toggle visibility instead.') + self.right_labels = value + + def _draw_event(self, event): + if self.has_labels(): + self._update_labels_visibility(event.renderer) + + def has_labels(self): + return hasattr(self, '_labels') and self._labels + + @property + def label_artists(self): + if self.has_labels(): + return self._labels + return [] def _crs_transform(self): """ @@ -219,103 +372,56 @@ transform = transform._as_mpl_transform(self.axes) return transform - def _add_gridline_label(self, value, axis, upper_end): - """ - Create a Text artist on our axes for a gridline label. - - Parameters - ---------- - value - Coordinate value of this gridline. The text contains this - value, and is positioned centred at that point. - axis - Which axis the label is on: 'x' or 'y'. - upper_end: bool - If True, place at the maximum of the "other" coordinate (Axes - coordinate == 1.0). Else 'lower' end (Axes coord = 0.0). - - """ - transform = self._crs_transform() - if upper_end: - shift_scale = 1 + @staticmethod + def _round(x, base=5): + if np.isnan(base): + base = 5 + return int(base * round(float(x) / base)) + + def _find_midpoints(self, lim, ticks): + # Find the center point between each lat gridline. + if len(ticks) > 1: + cent = np.diff(ticks).mean() / 2 else: - shift_scale = -1 - if axis == 'x': - x = value - y = 1.0 if upper_end else 0.0 - h_align = 'center' - v_align = 'bottom' if upper_end else 'top' - tr_x = transform - tr_y = self.axes.transAxes + \ - mtrans.ScaledTranslation( - 0.0, - shift_scale * self.xpadding * (1.0 / 72), - self.axes.figure.dpi_scale_trans) - str_value = self.xformatter(value) - user_label_style = self.xlabel_style - elif axis == 'y': - y = value - x = 1.0 if upper_end else 0.0 - if matplotlib.__version__ > '2.0': - v_align = 'center_baseline' - else: - v_align = 'center' - h_align = 'left' if upper_end else 'right' - tr_y = transform - tr_x = self.axes.transAxes + \ - mtrans.ScaledTranslation( - shift_scale * self.ypadding * (1.0 / 72), - 0.0, - self.axes.figure.dpi_scale_trans) - str_value = self.yformatter(value) - user_label_style = self.ylabel_style + cent = np.nan + if isinstance(self.axes.projection, _POLAR_PROJS): + lq = 90 + uq = 90 else: - raise ValueError( - "Unknown axis, {!r}, must be either 'x' or 'y'".format(axis)) - - # Make a 'blended' transform for label text positioning. - # One coord is geographic, and the other a plain Axes - # coordinate with an appropriate offset. - label_transform = mtrans.blended_transform_factory( - x_transform=tr_x, y_transform=tr_y) - - label_style = {'verticalalignment': v_align, - 'horizontalalignment': h_align, - } - label_style.update(user_label_style) - - # Create and add a Text artist with these properties - text_artist = mtext.Text(x, y, str_value, - clip_on=False, - transform=label_transform, **label_style) - if axis == 'x': - self.xlabel_artists.append(text_artist) - elif axis == 'y': - self.ylabel_artists.append(text_artist) - self.axes.add_artist(text_artist) + lq = 25 + uq = 75 + midpoints = (self._round(np.percentile(lim, lq), cent), + self._round(np.percentile(lim, uq), cent)) + return midpoints - def _draw_gridliner(self, nx=None, ny=None, background_patch=None): + def _draw_gridliner(self, nx=None, ny=None, renderer=None): """Create Artists for all visible elements and add to our Axes.""" - x_lim, y_lim = self._axes_domain(nx=nx, ny=ny, - background_patch=background_patch) + # Check status + if self._plotted: + return + self._plotted = True - transform = self._crs_transform() + # Inits + lon_lim, lat_lim = self._axes_domain(nx=nx, ny=ny) + transform = self._crs_transform() rc_params = matplotlib.rcParams - n_steps = self.n_steps - - x_ticks = self.xlocator.tick_values(x_lim[0], x_lim[1]) - y_ticks = self.ylocator.tick_values(y_lim[0], y_lim[1]) - - # XXX this bit is cartopy specific. (for circular longitudes) - # Purpose: omit plotting the last x line, as it may overlap the first. - x_gridline_points = x_ticks[:] crs = self.crs - if (isinstance(crs, Projection) and - isinstance(crs, _RectangularProjection) and - abs(np.diff(x_lim)) == abs(np.diff(crs.x_limits))): - x_gridline_points = x_gridline_points[:-1] + + # Get nice ticks within crs domain + lon_ticks = self.xlocator.tick_values(lon_lim[0], lon_lim[1]) + lat_ticks = self.ylocator.tick_values(lat_lim[0], lat_lim[1]) + lon_ticks = [value for value in lon_ticks + if value >= max(lon_lim[0], crs.x_limits[0]) and + value <= min(lon_lim[1], crs.x_limits[1])] + lat_ticks = [value for value in lat_ticks + if value >= max(lat_lim[0], crs.y_limits[0]) and + value <= min(lat_lim[1], crs.y_limits[1])] + + ##################### + # Gridlines drawing # + ##################### collection_kwargs = self.collection_kwargs if collection_kwargs is None: @@ -327,58 +433,383 @@ collection_kwargs.setdefault('linestyle', rc_params['grid.linestyle']) collection_kwargs.setdefault('linewidth', rc_params['grid.linewidth']) - if self.xlines: - lines = [] - for x in x_gridline_points: - ticks = list(zip( - np.zeros(n_steps) + x, - np.linspace(min(y_ticks), max(y_ticks), n_steps))) - lines.append(ticks) - - x_lc = mcollections.LineCollection(lines, **collection_kwargs) - self.xline_artists.append(x_lc) - self.axes.add_collection(x_lc, autolim=False) + # Meridians + lat_min, lat_max = lat_lim + if lat_ticks: + lat_min = min(lat_min, min(lat_ticks)) + lat_max = max(lat_max, max(lat_ticks)) + lon_lines = np.empty((len(lon_ticks), n_steps, 2)) + lon_lines[:, :, 0] = np.array(lon_ticks)[:, np.newaxis] + lon_lines[:, :, 1] = np.linspace(lat_min, lat_max, + n_steps)[np.newaxis, :] + if self.xlines: + nx = len(lon_lines) + 1 + # XXX this bit is cartopy specific. (for circular longitudes) + # Purpose: omit plotting the last x line, + # as it may overlap the first. + if (isinstance(crs, Projection) and + isinstance(crs, _RectangularProjection) and + abs(np.diff(lon_lim)) == abs(np.diff(crs.x_limits))): + nx -= 1 + lon_lc = mcollections.LineCollection(lon_lines, + **collection_kwargs) + self.xline_artists.append(lon_lc) + self.axes.add_collection(lon_lc, autolim=False) + + # Parallels + lon_min, lon_max = lon_lim + if lon_ticks: + lon_min = min(lon_min, min(lon_ticks)) + lon_max = max(lon_max, max(lon_ticks)) + lat_lines = np.empty((len(lat_ticks), n_steps, 2)) + lat_lines[:, :, 0] = np.linspace(lon_min, lon_max, + n_steps)[np.newaxis, :] + lat_lines[:, :, 1] = np.array(lat_ticks)[:, np.newaxis] if self.ylines: - lines = [] - for y in y_ticks: - ticks = list(zip( - np.linspace(min(x_ticks), max(x_ticks), n_steps), - np.zeros(n_steps) + y)) - lines.append(ticks) - - y_lc = mcollections.LineCollection(lines, **collection_kwargs) - self.yline_artists.append(y_lc) - self.axes.add_collection(y_lc, autolim=False) + lat_lc = mcollections.LineCollection(lat_lines, + **collection_kwargs) + self.yline_artists.append(lat_lc) + self.axes.add_collection(lat_lc, autolim=False) ################# # Label drawing # ################# - # Trim outside-area points from the label coords. - # Tickers may round *up* the desired range to something tidy, not - # all of which is necessarily visible. We must be stricter with - # our texts, as they are drawn *without clipping*. - x_label_points = [x for x in x_ticks if x_lim[0] <= x <= x_lim[1]] - y_label_points = [y for y in y_ticks if y_lim[0] <= y <= y_lim[1]] + self.bottom_label_artists = [] + self.top_label_artists = [] + self.left_label_artists = [] + self.right_label_artists = [] + if not (self.left_labels or self.right_labels or + self.bottom_labels or self.top_labels): + return + self._assert_can_draw_ticks() + + # Get the real map boundaries + map_boundary_vertices = self.axes.patch.get_path().vertices + map_boundary = sgeom.Polygon(map_boundary_vertices) + + self._labels = [] + + if self.x_inline: + y_midpoints = self._find_midpoints(lat_lim, lat_ticks) + if self.y_inline: + x_midpoints = self._find_midpoints(lon_lim, lon_ticks) + + for lonlat, lines, line_ticks, formatter, label_style in ( + ('lon', lon_lines, lon_ticks, + self.xformatter, self.xlabel_style), + ('lat', lat_lines, lat_ticks, + self.yformatter, self.ylabel_style)): + + formatter.set_locs(line_ticks) + + for line, tick_value in zip(lines, line_ticks): + # Intersection of line with map boundary + line = self.axes.projection.transform_points( + crs, line[:, 0], line[:, 1])[:, :2] + infs = np.isinf(line).any(axis=1) + line = line.compress(~infs, axis=0) + if line.size == 0: + continue + line = sgeom.LineString(line) + if line.intersects(map_boundary): + intersection = line.intersection(map_boundary) + del line + if intersection.is_empty: + continue + if isinstance(intersection, sgeom.MultiPoint): + if len(intersection) < 2: + continue + tails = [[(pt.x, pt.y) for pt in intersection[:2]]] + heads = [[(pt.x, pt.y) + for pt in intersection[-1:-3:-1]]] + elif isinstance(intersection, (sgeom.LineString, + sgeom.MultiLineString)): + if isinstance(intersection, sgeom.LineString): + intersection = [intersection] + elif len(intersection) > 4: + # Gridline and map boundary are parallel + # and they intersect themselves too much + # it results in a multiline string + # that must be converted to a single linestring. + # This is an empirical workaround for a problem + # that can probably be solved in a cleaner way. + xy = np.append(intersection[0], intersection[-1], + axis=0) + intersection = [sgeom.LineString(xy)] + tails = [] + heads = [] + for inter in intersection: + if len(inter.coords) < 2: + continue + tails.append(inter.coords[:2]) + heads.append(inter.coords[-1:-3:-1]) + if not tails: + continue + elif isinstance(intersection, + sgeom.collection.GeometryCollection): + # This is a collection of Point and LineString that + # represent the same gridline. + # We only consider the first geometries, merge their + # coordinates and keep first two points to get only one + # tail ... + xy = [] + for geom in intersection.geoms: + for coord in geom.coords: + xy.append(coord) + if len(xy) == 2: + break + if len(xy) == 2: + break + tails = [xy] + # ... and the last geometries, merge their coordinates + # and keep last two points to get only one head. + xy = [] + for geom in reversed(intersection.geoms): + for coord in reversed(geom.coords): + xy.append(coord) + if len(xy) == 2: + break + if len(xy) == 2: + break + heads = [xy] + else: + warnings.warn( + 'Unsupported intersection geometry for gridline ' + 'labels: ' + intersection.__class__.__name__) + continue + del intersection + + # Loop on head and tail and plot label by extrapolation + for tail, head in zip(tails, heads): + for i, (pt0, pt1) in enumerate([tail, head]): + kw, angle, loc = self._segment_to_text_specs( + pt0, pt1, lonlat) + if not getattr(self, loc+'_labels'): + continue + kw.update(label_style, + bbox={'pad': 0, 'visible': False}) + text = formatter(tick_value) + + if self.y_inline and lonlat == 'lat': + # 180 degrees isn't formatted with a + # suffix and adds confusion if it's inline + if abs(tick_value) == 180: + continue + x = x_midpoints[i] + y = tick_value + kw.update(clip_on=True) + y_set = True + else: + x = pt0[0] + y_set = False + + if self.x_inline and lonlat == 'lon': + if abs(tick_value) == 180: + continue + x = tick_value + y = y_midpoints[i] + kw.update(clip_on=True) + elif not y_set: + y = pt0[1] + + tt = self.axes.text(x, y, text, **kw) + tt._angle = angle + priority = (((lonlat == 'lon') and + loc in ('bottom', 'top')) or + ((lonlat == 'lat') and + loc in ('left', 'right'))) + self._labels.append((lonlat, priority, tt)) + getattr(self, loc + '_label_artists').append(tt) + + # Sort labels + if self._labels: + self._labels.sort(key=operator.itemgetter(0), reverse=True) + self._update_labels_visibility(renderer) + + def _segment_to_text_specs(self, pt0, pt1, lonlat): + """Get appropriate kwargs for a label from lon or lat line segment""" + x0, y0 = pt0 + x1, y1 = pt1 + angle = np.arctan2(y0-y1, x0-x1) * 180 / np.pi + kw, loc = self._segment_angle_to_text_specs(angle, lonlat) + return kw, angle, loc + + def _text_angle_to_specs_(self, angle, lonlat): + """Get specs for a rotated label from its angle in degrees""" + + angle %= 360 + if angle > 180: + angle -= 360 + + if ((self.x_inline and lonlat == 'lon') or + (self.y_inline and lonlat == 'lat')): + kw = {'rotation': 0, 'rotation_mode': 'anchor', + 'ha': 'center', 'va': 'center'} + loc = 'bottom' + return kw, loc + + # Default options + kw = {'rotation': angle, 'rotation_mode': 'anchor'} + + # Options that depend in which quarter the angle falls + if abs(angle) <= 45: + loc = 'right' + kw.update(ha='left', va='center') + + elif abs(angle) >= 135: + loc = 'left' + kw.update(ha='right', va='center') + kw['rotation'] -= np.sign(angle) * 180 + + elif angle > 45: + loc = 'top' + kw.update(ha='center', va='bottom', rotation=angle-90) - if self.xlabels_bottom or self.xlabels_top: - self._assert_can_draw_ticks() - self.xformatter.set_locs(x_label_points) - for x in x_label_points: - if self.xlabels_bottom: - self._add_gridline_label(x, axis='x', upper_end=False) - if self.xlabels_top: - self._add_gridline_label(x, axis='x', upper_end=True) + else: + loc = 'bottom' + kw.update(ha='center', va='top', rotation=angle+90) - if self.ylabels_left or self.ylabels_right: - self._assert_can_draw_ticks() - self.yformatter.set_locs(y_label_points) - for y in y_label_points: - if self.ylabels_left: - self._add_gridline_label(y, axis='y', upper_end=False) - if self.ylabels_right: - self._add_gridline_label(y, axis='y', upper_end=True) + return kw, loc + + def _segment_angle_to_text_specs(self, angle, lonlat): + """Get appropriate kwargs for a given text angle""" + kw, loc = self._text_angle_to_specs_(angle, lonlat) + if not self.rotate_labels: + angle = {'top': 90., 'right': 0., + 'bottom': -90., 'left': 180.}[loc] + del kw['rotation'] + + if ((self.x_inline and lonlat == 'lon') or + (self.y_inline and lonlat == 'lat')): + kw.update(transform=cartopy.crs.PlateCarree()) + else: + xpadding = (self.xpadding if self.xpadding is not None + else matplotlib.rc_params['xtick.major.pad']) + ypadding = (self.ypadding if self.ypadding is not None + else matplotlib.rc_params['ytick.major.pad']) + dx = ypadding * np.cos(angle * np.pi / 180) + dy = xpadding * np.sin(angle * np.pi / 180) + transform = mtrans.offset_copy( + self.axes.transData, self.axes.figure, + x=dx, y=dy, units='dots') + kw.update(transform=transform) + + return kw, loc + + def _update_labels_visibility(self, renderer): + """Update the visibility of each plotted label + + The following rules apply: + + - Labels are plotted and checked by order of priority, + with a high priority for longitude labels at the bottom and + top of the map, and the reverse for latitude labels. + - A label must not overlap another label marked as visible. + - A label must not overlap the map boundary. + - When a label is about to be hidden, other angles are tried in the + absolute given limit of max_delta_angle by increments of delta_angle + of difference from the original angle. + """ + if renderer is None or not self._labels: + return + paths = [] + outline_path = None + delta_angle = 22.5 + max_delta_angle = 45 + axes_children = self.axes.get_children() + + def remove_path_dupes(path): + """ + Remove duplicate points in a path (zero-length segments). + + This is necessary only for Matplotlib 3.1.0 -- 3.1.2, because + Path.intersects_path incorrectly returns True for any paths with + such segments. + """ + segment_length = np.diff(path.vertices, axis=0) + mask = np.logical_or.reduce(segment_length != 0, axis=1) + mask = np.append(mask, True) + path = mpath.Path(np.compress(mask, path.vertices, axis=0), + np.compress(mask, path.codes, axis=0)) + return path + + for lonlat, priority, artist in self._labels: + + if artist not in axes_children: + warnings.warn('The labels of this gridliner do not belong to ' + 'the gridliner axes') + + orig_specs = {'rotation': artist.get_rotation(), + 'ha': artist.get_ha(), + 'va': artist.get_va()} + # Compute angles to try + angles = [None] + for abs_delta_angle in np.arange(delta_angle, max_delta_angle+1, + delta_angle): + angles.append(artist._angle + abs_delta_angle) + angles.append(artist._angle - abs_delta_angle) + + # Loop on angles until it works + for angle in angles: + if ((self.x_inline and lonlat == 'lon') or + (self.y_inline and lonlat == 'lat')): + angle = 0 + + if angle is not None: + specs, _ = self._segment_angle_to_text_specs(angle, lonlat) + artist.update(specs) + + artist.update_bbox_position_size(renderer) + this_patch = artist.get_bbox_patch() + this_path = this_patch.get_path().transformed( + this_patch.get_transform()) + if '3.1.0' <= matplotlib.__version__ <= '3.1.2': + this_path = remove_path_dupes(this_path) + center = artist.get_transform().transform_point( + artist.get_position()) + visible = False + + for path in paths: + + # Check it does not overlap another label + if this_path.intersects_path(path): + break + + else: + + # Finally check that it does not overlap the map + if outline_path is None: + outline_path = (self.axes.patch.get_path() + .transformed(self.axes.transData)) + if '3.1.0' <= matplotlib.__version__ <= '3.1.2': + outline_path = remove_path_dupes(outline_path) + # Inline must be within the map. + if ((lonlat == 'lon' and self.x_inline) or + (lonlat == 'lat' and self.y_inline)): + # TODO: When Matplotlib clip path works on text, this + # clipping can be left to it. + if outline_path.contains_point(center): + visible = True + # Non-inline must not run through the outline. + elif not outline_path.intersects_path(this_path): + visible = True + + # Good + if visible: + break + + if ((self.x_inline and lonlat == 'lon') or + (self.y_inline and lonlat == 'lat')): + break + + # Action + artist.set_visible(visible) + if not visible: + artist.update(orig_specs) + else: + paths.append(this_path) def _assert_can_draw_ticks(self): """ @@ -391,16 +822,10 @@ raise TypeError('Cannot label {crs.__class__.__name__} gridlines.' ' Only PlateCarree gridlines are currently ' 'supported.'.format(crs=self.crs)) - if not isinstance(self.axes.projection, - (cartopy.crs.PlateCarree, cartopy.crs.Mercator)): - raise TypeError('Cannot label gridlines on a ' - '{prj.__class__.__name__} plot. Only PlateCarree' - ' and Mercator plots are currently ' - 'supported.'.format(prj=self.axes.projection)) return True - def _axes_domain(self, nx=None, ny=None, background_patch=None): - """Return x_range, y_range""" + def _axes_domain(self, nx=None, ny=None): + """Return lon_range, lat_range""" DEBUG = False transform = self._crs_transform() @@ -408,8 +833,8 @@ ax_transform = self.axes.transAxes desired_trans = ax_transform - transform - nx = nx or 30 - ny = ny or 30 + nx = nx or 100 + ny = ny or 100 x = np.linspace(1e-9, 1 - 1e-9, nx) y = np.linspace(1e-9, 1 - 1e-9, ny) x, y = np.meshgrid(x, y) @@ -418,13 +843,12 @@ in_data = desired_trans.transform(coords) - ax_to_bkg_patch = self.axes.transAxes - \ - background_patch.get_transform() + ax_to_bkg_patch = self.axes.transAxes - self.axes.patch.get_transform() # convert the coordinates of the data to the background patches # coordinates background_coord = ax_to_bkg_patch.transform(coords) - ok = background_patch.get_path().contains_points(background_coord) + ok = self.axes.patch.get_path().contains_points(background_coord) if DEBUG: import matplotlib.pyplot as plt @@ -438,23 +862,32 @@ # If there were no data points in the axes we just use the x and y # range of the projection. if inside.size == 0: - x_range = self.crs.x_limits - y_range = self.crs.y_limits + lon_range = self.crs.x_limits + lat_range = self.crs.y_limits else: - x_range = np.nanmin(inside[:, 0]), np.nanmax(inside[:, 0]) - y_range = np.nanmin(inside[:, 1]), np.nanmax(inside[:, 1]) + # np.isfinite must be used to prevent np.inf values that + # not filtered by np.nanmax for some projections + lat_max = np.compress(np.isfinite(inside[:, 1]), + inside[:, 1]) + if lat_max.size == 0: + lon_range = self.crs.x_limits + lat_range = self.crs.y_limits + else: + lat_max = lat_max.max() + lon_range = np.nanmin(inside[:, 0]), np.nanmax(inside[:, 0]) + lat_range = np.nanmin(inside[:, 1]), lat_max # XXX Cartopy specific thing. Perhaps make this bit a specialisation # in a subclass... crs = self.crs if isinstance(crs, Projection): - x_range = np.clip(x_range, *crs.x_limits) - y_range = np.clip(y_range, *crs.y_limits) + lon_range = np.clip(lon_range, *crs.x_limits) + lat_range = np.clip(lat_range, *crs.y_limits) # if the limit is >90% of the full x limit, then just use the full # x limit (this makes circular handling better) - prct = np.abs(np.diff(x_range) / np.diff(crs.x_limits)) + prct = np.abs(np.diff(lon_range) / np.diff(crs.x_limits)) if prct > 0.9: - x_range = crs.x_limits + lon_range = crs.x_limits - return x_range, y_range + return lon_range, lat_range diff -Nru python-cartopy-0.17.0+dfsg/lib/cartopy/mpl/patch.py python-cartopy-0.18.0+dfsg/lib/cartopy/mpl/patch.py --- python-cartopy-0.17.0+dfsg/lib/cartopy/mpl/patch.py 2018-11-17 07:25:32.000000000 +0000 +++ python-cartopy-0.18.0+dfsg/lib/cartopy/mpl/patch.py 2020-05-03 08:12:47.000000000 +0000 @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2011 - 2018, Met Office +# (C) British Crown Copyright 2011 - 2020, Met Office # # This file is part of cartopy. # @@ -19,10 +19,10 @@ See also `Shapely Geometric Objects `_ -and `Matplotlib Path API `_. +and `Matplotlib Path API `_. .. see_also_shapely: - http://toblerity.org/shapely/manual.html#geometric-objects + https://shapely.readthedocs.io/en/latest/manual.html#geometric-objects """ @@ -45,6 +45,7 @@ A list, tuple or single instance of any of the following types: :class:`shapely.geometry.point.Point`, :class:`shapely.geometry.linestring.LineString`, + :class:`shapely.geometry.linestring.LinearRing`, :class:`shapely.geometry.polygon.Polygon`, :class:`shapely.geometry.multipoint.MultiPoint`, :class:`shapely.geometry.multipolygon.MultiPolygon`, @@ -64,7 +65,9 @@ paths.extend(geos_to_path(shp)) return paths - if isinstance(shape, (sgeom.LineString, sgeom.Point)): + if isinstance(shape, sgeom.LinearRing): + return [Path(np.column_stack(shape.xy), closed=True)] + elif isinstance(shape, (sgeom.LineString, sgeom.Point)): return [Path(np.column_stack(shape.xy))] elif isinstance(shape, sgeom.Polygon): def poly_codes(poly): @@ -164,8 +167,14 @@ if len(path_verts) == 0: continue - verts_same_as_first = np.all(path_verts[0, :] == path_verts[1:, :], - axis=1) + if path_codes[-1] == Path.CLOSEPOLY: + path_verts[-1, :] = path_verts[0, :] + + verts_same_as_first = np.isclose(path_verts[0, :], path_verts[1:, :], + rtol=1e-10, atol=1e-13) + verts_same_as_first = np.logical_and.reduce(verts_same_as_first, + axis=1) + if all(verts_same_as_first): geom = sgeom.Point(path_verts[0, :]) elif path_verts.shape[0] > 4 and path_codes[-1] == Path.CLOSEPOLY: diff -Nru python-cartopy-0.17.0+dfsg/lib/cartopy/mpl/slippy_image_artist.py python-cartopy-0.18.0+dfsg/lib/cartopy/mpl/slippy_image_artist.py --- python-cartopy-0.17.0+dfsg/lib/cartopy/mpl/slippy_image_artist.py 2018-11-17 07:25:32.000000000 +0000 +++ python-cartopy-0.18.0+dfsg/lib/cartopy/mpl/slippy_image_artist.py 2020-05-03 08:12:47.000000000 +0000 @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2014 - 2018, Met Office +# (C) British Crown Copyright 2014 - 2020, Met Office # # This file is part of cartopy. # @@ -39,8 +39,10 @@ """ def __init__(self, ax, raster_source, **kwargs): self.raster_source = raster_source + if matplotlib.__version__ >= '3': + # This artist fills the Axes, so should not influence layout. + kwargs.setdefault('in_layout', False) super(SlippyImageArtist, self).__init__(ax, **kwargs) - self.set_clip_path(ax.outline_patch) self.cache = [] ax.figure.canvas.mpl_connect('button_press_event', self.on_press) @@ -55,6 +57,9 @@ self.user_is_interacting = False self.stale = True + def get_window_extent(self, renderer=None): + return self.axes.get_window_extent(renderer=renderer) + @matplotlib.artist.allow_rasterization def draw(self, renderer, *args, **kwargs): if not self.get_visible(): @@ -74,3 +79,8 @@ with ax.hold_limits(): self.set_extent(extent) super(SlippyImageArtist, self).draw(renderer, *args, **kwargs) + + def can_composite(self): + # As per https://github.com/SciTools/cartopy/issues/689, disable + # compositing multiple raster sources. + return False diff -Nru python-cartopy-0.17.0+dfsg/lib/cartopy/mpl/style.py python-cartopy-0.18.0+dfsg/lib/cartopy/mpl/style.py --- python-cartopy-0.17.0+dfsg/lib/cartopy/mpl/style.py 1970-01-01 00:00:00.000000000 +0000 +++ python-cartopy-0.18.0+dfsg/lib/cartopy/mpl/style.py 2020-05-03 08:12:47.000000000 +0000 @@ -0,0 +1,114 @@ +# (C) British Crown Copyright 2018 - 2019, 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 . + +from __future__ import (absolute_import, division, print_function) + +""" +Handles matplotlib styling in a single consistent place. + +""" +import warnings + +import six + + +# Define the matplotlib style aliases that cartopy can expand. +# Note: This should not contain the plural aliases +# (e.g. linewidths -> linewidth). +# This is an intended duplication of +# https://github.com/matplotlib/matplotlib/blob/\ +# 2d2dab511d22b6cc9c812cfbcca6df3f9bf3094a/lib/matplotlib/patches.py#L20-L26 +# Duplication intended to simplify readability, given the small number of +# aliases. +_ALIASES = { + 'lw': 'linewidth', + 'ls': 'linestyle', + 'fc': 'facecolor', + 'ec': 'edgecolor', +} + + +def merge(*style_dicts): + """ + Merge together multiple matplotlib style dictionaries in a predictable way + + The approach taken is: + + For each style: + * Expand aliases, such as "lw" -> "linewidth", but always prefer + the full form if over-specified (i.e. lw AND linewidth + are both set) + * "color" overwrites "facecolor" and "edgecolor" (as per + matplotlib), UNLESS facecolor == "never", which will be expanded + at finalization to 'none' + + >>> style = merge({"lw": 1, "edgecolor": "black", "facecolor": "never"}, + ... {"linewidth": 2, "color": "gray"}) + >>> sorted(style.items()) + [('edgecolor', 'gray'), ('facecolor', 'never'), ('linewidth', 2)] + + """ + style = {} + facecolor = None + + for orig_style in style_dicts: + this_style = orig_style.copy() + + for alias_from, alias_to in _ALIASES.items(): + alias = this_style.pop(alias_from, None) + if alias_from in orig_style: + # n.b. alias_from doesn't trump alias_to + # (e.g. 'lw' doesn't trump 'linewidth'). + this_style.setdefault(alias_to, alias) + + color = this_style.pop('color', None) + if 'color' in orig_style: + this_style['edgecolor'] = color + this_style['facecolor'] = color + + if isinstance(facecolor, six.string_types) and facecolor == 'never': + requested_color = this_style.pop('facecolor', None) + setting_color = not ( + isinstance(requested_color, six.string_types) and + requested_color.lower() == 'none') + if (('fc' in orig_style or 'facecolor' in orig_style) and + setting_color): + warnings.warn('facecolor will have no effect as it has been ' + 'defined as "never".') + else: + facecolor = this_style.get('facecolor', facecolor) + + # Push the remainder of the style into the merged style. + style.update(this_style) + + return style + + +def finalize(style): + """ + Update the given matplotlib style according to cartopy's style rules. + + Rules: + + 1. A facecolor of 'never' is replaced with 'none'. + + """ + # Expand 'never' to 'none' if we have it. + facecolor = style.get('facecolor', None) + if facecolor == 'never': + style['facecolor'] = 'none' + return style diff -Nru python-cartopy-0.17.0+dfsg/lib/cartopy/mpl/ticker.py python-cartopy-0.18.0+dfsg/lib/cartopy/mpl/ticker.py --- python-cartopy-0.17.0+dfsg/lib/cartopy/mpl/ticker.py 2018-11-17 07:25:32.000000000 +0000 +++ python-cartopy-0.18.0+dfsg/lib/cartopy/mpl/ticker.py 2020-05-03 08:12:47.000000000 +0000 @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2014 - 2018, Met Office +# (C) British Crown Copyright 2014 - 2020, Met Office # # This file is part of cartopy. # @@ -18,7 +18,8 @@ from __future__ import (absolute_import, division, print_function) -from matplotlib.ticker import Formatter +import numpy as np +from matplotlib.ticker import Formatter, MaxNLocator import cartopy.crs as ccrs from cartopy.mpl.geoaxes import GeoAxes @@ -34,46 +35,149 @@ _target_projection = ccrs.PlateCarree() def __init__(self, degree_symbol=u'\u00B0', number_format='g', - transform_precision=1e-8): + transform_precision=1e-8, dms=False, + minute_symbol=u"'", second_symbol=u"''", + seconds_number_format='g', + auto_hide=True): """ Base class for simpler implementation of specialised formatters for latitude and longitude axes. """ self._degree_symbol = degree_symbol - self._number_format = number_format + self._degrees_number_format = number_format self._transform_precision = transform_precision + self._dms = dms + self._minute_symbol = minute_symbol + self._second_symbol = second_symbol + self._seconds_num_format = seconds_number_format + self._auto_hide = auto_hide + self._auto_hide_degrees = False + self._auto_hide_minutes = False + self._precision = 5 # locator precision def __call__(self, value, pos=None): - if not isinstance(self.axis.axes, GeoAxes): - raise TypeError("This formatter can only be " - "used with cartopy axes.") - # We want to produce labels for values in the familiar Plate Carree - # projection, so convert the tick values from their own projection - # before formatting them. - source = self.axis.axes.projection - if not isinstance(source, (ccrs._RectangularProjection, - ccrs.Mercator)): - raise TypeError("This formatter cannot be used with " - "non-rectangular projections.") - projected_value = self._apply_transform(value, self._target_projection, - source) - # Round the transformed value using a given precision for display - # purposes. Transforms can introduce minor rounding errors that make - # the tick values look bad, these need to be accounted for. - f = 1. / self._transform_precision - projected_value = round(f * projected_value) / f + if self.axis is not None and isinstance(self.axis.axes, GeoAxes): + + # We want to produce labels for values in the familiar Plate Carree + # projection, so convert the tick values from their own projection + # before formatting them. + source = self.axis.axes.projection + if not isinstance(source, (ccrs._RectangularProjection, + ccrs.Mercator)): + raise TypeError("This formatter cannot be used with " + "non-rectangular projections.") + projected_value = self._apply_transform(value, + self._target_projection, + source) + + # Round the transformed value using a given precision for display + # purposes. Transforms can introduce minor rounding errors that + # make the tick values look bad, these need to be accounted for. + f = 1. / self._transform_precision + projected_value = round(f * projected_value) / f + + else: + + # There is no projection so we assume it is already PlateCarree + projected_value = value + # Return the formatted values, the formatter has both the re-projected # tick value and the original axis value available to it. return self._format_value(projected_value, value) def _format_value(self, value, original_value): hemisphere = self._hemisphere(value, original_value) - fmt_string = u'{value:{number_format}}{degree}{hemisphere}' - return fmt_string.format(value=abs(value), - number_format=self._number_format, - degree=self._degree_symbol, - hemisphere=hemisphere) + + if not self._dms: + return (self._format_degrees(abs(value)) + + hemisphere) + + value, deg, mn, sec = self._get_dms(abs(value)) + + # Format + label = u'' + if sec: + label = self._format_seconds(sec) + + if mn or (not self._auto_hide_minutes and label): + label = self._format_minutes(mn) + label + + if not self._auto_hide_degrees or not label: + label = self._format_degrees(deg) + hemisphere + label + + return label + + def _get_dms(self, x): + """Convert to degrees, minutes, seconds + + Parameters + ---------- + x: float or array of floats + Degrees + + Return + ------ + x: degrees rounded to the requested precision + degs: degrees + mins: minutes + secs: seconds + """ + self._precision = 6 + x = np.asarray(x, 'd') + degs = np.round(x, self._precision).astype('i') + y = (x - degs) * 60 + mins = np.round(y, self._precision).astype('i') + secs = np.round((y - mins)*60, self._precision - 3) + return x, degs, mins, secs + + def set_locs(self, locs): + Formatter.set_locs(self, locs) + if not self._auto_hide: + return + self.locs, degs, mins, secs = self._get_dms(self.locs) + secs = np.round(secs, self._precision-3).astype('i') + secs0 = secs == 0 + mins0 = mins == 0 + + def auto_hide(valid, values): + """Should I switch on auto_hide?""" + if not valid.any(): + return False + if valid.sum() == 1: + return True + return np.diff(values.compress(valid)).max() == 1 + + # Potentially hide minutes labels when pure minutes are all displayed + self._auto_hide_minutes = auto_hide(secs0, mins) + + # Potentially hide degrees labels when pure degrees are all displayed + self._auto_hide_degrees = auto_hide(secs0 & mins0, degs) + + def _format_degrees(self, deg): + """Format degrees as an integer""" + if self._dms: + deg = int(deg) + number_format = 'd' + else: + number_format = self._degrees_number_format + return u'{value:{number_format}}{symbol}'.format( + value=abs(deg), + number_format=number_format, + symbol=self._degree_symbol) + + def _format_minutes(self, mn): + """Format minutes as an integer""" + return u'{value:d}{symbol}'.format( + value=int(mn), + symbol=self._minute_symbol) + + def _format_seconds(self, sec): + """Format seconds as an float""" + return u'{value:{fmt}}{symbol}'.format( + value=sec, + fmt=self._seconds_num_format, + symbol=self._second_symbol) def _apply_transform(self, value, target_proj, source_crs): """ @@ -99,12 +203,15 @@ class LatitudeFormatter(_PlateCarreeFormatter): """Tick formatter for latitude axes.""" def __init__(self, degree_symbol=u'\u00B0', number_format='g', - transform_precision=1e-8): + transform_precision=1e-8, dms=False, + minute_symbol=u"'", second_symbol=u"''", + seconds_number_format='g', auto_hide=True, + ): """ - Tick formatter for a latitude axis. + Tick formatter for latitudes. - The axis must be part of an axes defined on a rectangular - projection (e.g. Plate Carree, Mercator). + When bound to an axis, the axis must be part of an axes defined + on a rectangular projection (e.g. Plate Carree, Mercator). Parameters @@ -115,12 +222,25 @@ degree symbol. Can be an empty string if no degree symbol is desired. number_format: optional - Format string to represent the tick values. Defaults to 'g'. + Format string to represent the longitude values when `dms` + is set to False. Defaults to 'g'. transform_precision: optional Sets the precision (in degrees) to which transformed tick values are rounded. The default is 1e-7, and should be suitable for most use cases. To control the appearance of tick labels use the *number_format* keyword. + dms: bool, optional + Wether or not formatting as degrees-minutes-seconds and not + as decimal degrees. + minute_symbol: str, optional + The character(s) used to represent the minute symbol. + second_symbol: str, optional + The character(s) used to represent the second symbol. + seconds_number_format: optional + Format string to represent the "seconds" component of the longitude + values. Defaults to 'g'. + auto_hide: bool, optional + Auto-hide degrees or minutes when redundant. Note ---- @@ -148,11 +268,24 @@ lat_formatter = LatitudeFormatter(degree_symbol='') ax.yaxis.set_major_formatter(lat_formatter) + When not bound to an axis:: + + lat_formatter = LatitudeFormatter() + ticks = [-90, -60, -30, 0, 30, 60, 90] + lat_formatter.set_locs(ticks) + labels = [lat_formatter(value) for value in ticks] + """ super(LatitudeFormatter, self).__init__( degree_symbol=degree_symbol, number_format=number_format, - transform_precision=transform_precision) + transform_precision=transform_precision, + dms=dms, + minute_symbol=minute_symbol, + second_symbol=second_symbol, + seconds_number_format=seconds_number_format, + auto_hide=auto_hide, + ) def _apply_transform(self, value, target_proj, source_crs): return target_proj.transform_point(0, value, source_crs)[1] @@ -175,12 +308,18 @@ dateline_direction_label=False, degree_symbol=u'\u00B0', number_format='g', - transform_precision=1e-8): + transform_precision=1e-8, + dms=False, + minute_symbol=u"'", + second_symbol=u"''", + seconds_number_format='g', + auto_hide=True, + ): """ - Create a formatter for longitude values. + Create a formatter for longitudes. - The axis must be part of an axes defined on a rectangular - projection (e.g. Plate Carree, Mercator). + When bound to an axis, the axis must be part of an axes defined + on a rectangular projection (e.g. Plate Carree, Mercator). Parameters ---------- @@ -198,13 +337,25 @@ The symbol used to represent degrees. Defaults to u'\u00B0' which is the unicode degree symbol. number_format: optional - Format string to represent the longitude values. Defaults to - 'g'. + Format string to represent the latitude values when `dms` + is set to False. Defaults to 'g'. transform_precision: optional Sets the precision (in degrees) to which transformed tick values are rounded. The default is 1e-7, and should be suitable for most use cases. To control the appearance of tick labels use the *number_format* keyword. + dms: bool, optional + Wether or not formatting as degrees-minutes-seconds and not + as decimal degrees. + minute_symbol: str, optional + The character(s) used to represent the minute symbol. + second_symbol: str, optional + The character(s) used to represent the second symbol. + seconds_number_format: optional + Format string to represent the "seconds" component of the latitude + values. Defaults to 'g'. + auto_hide: bool, optional + Auto-hide degrees or minutes when redundant. Note ---- @@ -233,18 +384,58 @@ lon_formatter = LongitudeFormatter() ax.xaxis.set_major_formatter(lon_formatter) + + When not bound to an axis:: + + lon_formatter = LongitudeFormatter() + ticks = [0, 60, 120, 180, 240, 300, 360] + lon_formatter.set_locs(ticks) + labels = [lon_formatter(value) for value in ticks] """ super(LongitudeFormatter, self).__init__( degree_symbol=degree_symbol, number_format=number_format, - transform_precision=transform_precision) + transform_precision=transform_precision, + dms=dms, + minute_symbol=minute_symbol, + second_symbol=second_symbol, + seconds_number_format=seconds_number_format, + auto_hide=auto_hide, + ) self._zero_direction_labels = zero_direction_label self._dateline_direction_labels = dateline_direction_label def _apply_transform(self, value, target_proj, source_crs): return target_proj.transform_point(value, 0, source_crs)[0] + @classmethod + def _fix_lons(cls, lons): + if isinstance(lons, list): + return [cls._fix_lons(lon) for lon in lons] + p180 = lons == 180 + m180 = lons == -180 + + # Wrap + lons = ((lons + 180) % 360) - 180 + + # Keep -180 and 180 when requested + for mp180, value in [(m180, -180), (p180, 180)]: + if np.any(mp180): + if isinstance(lons, np.ndarray): + lons = np.where(mp180, value, lons) + else: + lons = value + + return lons + + def set_locs(self, locs): + _PlateCarreeFormatter.set_locs(self, self._fix_lons(locs)) + + def _format_degrees(self, deg): + return _PlateCarreeFormatter._format_degrees(self, self._fix_lons(deg)) + def _hemisphere(self, value, value_source_crs): + value = self._fix_lons(value) # Perform basic hemisphere detection. if value < 0: hemisphere = 'W' @@ -262,3 +453,78 @@ if value in (-180, 180) and not self._dateline_direction_labels: hemisphere = '' return hemisphere + + +class LongitudeLocator(MaxNLocator): + """ + A locator for longitudes that works even at very small scale. + + Parameters + ---------- + dms: bool + Allow the locator to stop on minutes and seconds (False by default) + """ + + default_params = MaxNLocator.default_params.copy() + default_params.update(nbins=8, dms=False) + + def set_params(self, **kwargs): + """Set parameters within this locator.""" + if 'dms' in kwargs: + self._dms = kwargs.pop('dms') + MaxNLocator.set_params(self, **kwargs) + + def _guess_steps(self, vmin, vmax): + + dv = abs(vmax - vmin) + if dv > 180: + dv -= 180 + + if dv > 50.: + + steps = np.array([1, 2, 3, 6, 10]) + + elif not self._dms or dv > 3.: + + steps = np.array([1, 1.5, 2, 2.5, 3, 5, 10]) + + else: + steps = np.array([1, 10/6., 15/6., 20/6., 30/6., 10]) + + self.set_params(steps=np.array(steps)) + + def _raw_ticks(self, vmin, vmax): + self._guess_steps(vmin, vmax) + return MaxNLocator._raw_ticks(self, vmin, vmax) + + def bin_boundaries(self, vmin, vmax): + self._guess_steps(vmin, vmax) + return MaxNLocator.bin_boundaries(self, vmin, vmax) + + +class LatitudeLocator(LongitudeLocator): + """ + A locator for latitudes that works even at very small scale. + + Parameters + ---------- + dms: bool + Allow the locator to stop on minutes and seconds (False by default) + """ + def tick_values(self, vmin, vmax): + vmin = max(vmin, -90.) + vmax = min(vmax, 90.) + return LongitudeLocator.tick_values(self, vmin, vmax) + + def _guess_steps(self, vmin, vmax): + vmin = max(vmin, -90.) + vmax = min(vmax, 90.) + LongitudeLocator._guess_steps(self, vmin, vmax) + + def _raw_ticks(self, vmin, vmax): + ticks = LongitudeLocator._raw_ticks(self, vmin, vmax) + return [t for t in ticks if -90 <= t <= 90] + + def bin_boundaries(self, vmin, vmax): + ticks = LongitudeLocator.bin_boundaries(self, vmin, vmax) + return [t for t in ticks if -90 <= t <= 90] diff -Nru python-cartopy-0.17.0+dfsg/lib/cartopy/_proj4.pxd python-cartopy-0.18.0+dfsg/lib/cartopy/_proj4.pxd --- python-cartopy-0.17.0+dfsg/lib/cartopy/_proj4.pxd 1970-01-01 00:00:00.000000000 +0000 +++ python-cartopy-0.18.0+dfsg/lib/cartopy/_proj4.pxd 2020-05-03 08:12:47.000000000 +0000 @@ -0,0 +1,39 @@ +# (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 . + +""" +This file declares the Proj API, version 4. + +""" + + +cdef extern from "proj_api.h": + ctypedef void *projPJ + ctypedef struct projLP: + double u + double v + + projPJ pj_init_plus(char *) nogil + void pj_free(projPJ) nogil + void pj_get_spheroid_defn(projPJ, double *, double *) nogil + int pj_transform(projPJ, projPJ, long, int, double *, double *, double *) nogil + int pj_is_latlong(projPJ) nogil + char *pj_strerrno(int) nogil + int *pj_get_errno_ref() nogil + char *pj_get_release() nogil + cdef double DEG_TO_RAD + cdef double RAD_TO_DEG diff -Nru python-cartopy-0.17.0+dfsg/lib/cartopy/tests/conftest.py python-cartopy-0.18.0+dfsg/lib/cartopy/tests/conftest.py --- python-cartopy-0.17.0+dfsg/lib/cartopy/tests/conftest.py 1970-01-01 00:00:00.000000000 +0000 +++ python-cartopy-0.18.0+dfsg/lib/cartopy/tests/conftest.py 2020-05-03 08:12:47.000000000 +0000 @@ -0,0 +1,27 @@ +# (C) British Crown Copyright 2020, 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 . + +from __future__ import (absolute_import, division, print_function) + + +def pytest_configure(config): + # Register additional markers. + config.addinivalue_line('markers', + 'natural_earth: mark tests that use Natural Earth ' + 'data, and the network, if not cached.') + config.addinivalue_line('markers', + 'network: mark tests that use the network.') diff -Nru python-cartopy-0.17.0+dfsg/lib/cartopy/tests/crs/helpers.py python-cartopy-0.18.0+dfsg/lib/cartopy/tests/crs/helpers.py --- python-cartopy-0.17.0+dfsg/lib/cartopy/tests/crs/helpers.py 1970-01-01 00:00:00.000000000 +0000 +++ python-cartopy-0.18.0+dfsg/lib/cartopy/tests/crs/helpers.py 2020-05-03 08:12:47.000000000 +0000 @@ -0,0 +1,28 @@ +# (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 . +""" +Helpers for Cartopy CRS subclass tests. + +""" + +from __future__ import (absolute_import, division, print_function) + + +def check_proj_params(name, crs, other_args): + expected = other_args | {'proj=' + name, 'no_defs'} + proj_params = set(crs.proj4_init.lstrip('+').split(' +')) + assert expected == proj_params diff -Nru python-cartopy-0.17.0+dfsg/lib/cartopy/tests/crs/__init__.py python-cartopy-0.18.0+dfsg/lib/cartopy/tests/crs/__init__.py --- python-cartopy-0.17.0+dfsg/lib/cartopy/tests/crs/__init__.py 2018-11-17 07:25:32.000000000 +0000 +++ python-cartopy-0.18.0+dfsg/lib/cartopy/tests/crs/__init__.py 2020-05-03 08:12:47.000000000 +0000 @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2013 - 2016, Met Office +# (C) British Crown Copyright 2013 - 2018, Met Office # # This file is part of cartopy. # @@ -20,3 +20,8 @@ """ from __future__ import (absolute_import, division, print_function) + +import pytest + + +pytest.register_assert_rewrite('cartopy.tests.crs.helpers') diff -Nru python-cartopy-0.17.0+dfsg/lib/cartopy/tests/crs/test_albers_equal_area.py python-cartopy-0.18.0+dfsg/lib/cartopy/tests/crs/test_albers_equal_area.py --- python-cartopy-0.17.0+dfsg/lib/cartopy/tests/crs/test_albers_equal_area.py 2018-11-17 07:25:32.000000000 +0000 +++ python-cartopy-0.18.0+dfsg/lib/cartopy/tests/crs/test_albers_equal_area.py 2020-05-03 08:12:47.000000000 +0000 @@ -26,12 +26,7 @@ import pytest import cartopy.crs as ccrs - - -def check_proj4_params(crs, other_args): - expected = other_args | {'proj=aea', 'no_defs'} - pro4_params = set(crs.proj4_init.lstrip('+').split(' +')) - assert expected == pro4_params +from .helpers import check_proj_params class TestAlbersEqualArea(object): @@ -39,7 +34,7 @@ aea = ccrs.AlbersEqualArea() other_args = {'ellps=WGS84', 'lon_0=0.0', 'lat_0=0.0', 'x_0=0.0', 'y_0=0.0', 'lat_1=20.0', 'lat_2=50.0'} - check_proj4_params(aea, other_args) + check_proj_params('aea', aea, other_args) assert_almost_equal(np.array(aea.x_limits), [-17702759.799178038, 17702759.799178038], @@ -54,7 +49,7 @@ aea = ccrs.AlbersEqualArea(globe=globe) other_args = {'a=1000', 'b=500', 'lon_0=0.0', 'lat_0=0.0', 'x_0=0.0', 'y_0=0.0', 'lat_1=20.0', 'lat_2=50.0'} - check_proj4_params(aea, other_args) + check_proj_params('aea', aea, other_args) assert_almost_equal(np.array(aea.x_limits), [-2323.47073363411, 2323.47073363411], @@ -69,7 +64,7 @@ other_args = {'ellps=WGS84', 'lon_0=0.0', 'lat_0=0.0', 'x_0=1234', 'y_0=-4321', 'lat_1=20.0', 'lat_2=50.0'} - check_proj4_params(aea_offset, other_args) + check_proj_params('aea', aea_offset, other_args) @pytest.mark.parametrize('lon', [-10.0, 10.0]) def test_central_longitude(self, lon): @@ -77,7 +72,7 @@ aea_offset = ccrs.AlbersEqualArea(central_longitude=lon) other_args = {'ellps=WGS84', 'lon_0={}'.format(lon), 'lat_0=0.0', 'x_0=0.0', 'y_0=0.0', 'lat_1=20.0', 'lat_2=50.0'} - check_proj4_params(aea_offset, other_args) + check_proj_params('aea', aea_offset, other_args) assert_array_almost_equal(aea_offset.boundary, aea.boundary, decimal=0) @@ -86,17 +81,17 @@ aea = ccrs.AlbersEqualArea(standard_parallels=(13, 37)) other_args = {'ellps=WGS84', 'lon_0=0.0', 'lat_0=0.0', 'x_0=0.0', 'y_0=0.0', 'lat_1=13', 'lat_2=37'} - check_proj4_params(aea, other_args) + check_proj_params('aea', aea, other_args) aea = ccrs.AlbersEqualArea(standard_parallels=(13, )) other_args = {'ellps=WGS84', 'lon_0=0.0', 'lat_0=0.0', 'x_0=0.0', 'y_0=0.0', 'lat_1=13'} - check_proj4_params(aea, other_args) + check_proj_params('aea', aea, other_args) aea = ccrs.AlbersEqualArea(standard_parallels=13) other_args = {'ellps=WGS84', 'lon_0=0.0', 'lat_0=0.0', 'x_0=0.0', 'y_0=0.0', 'lat_1=13'} - check_proj4_params(aea, other_args) + check_proj_params('aea', aea, other_args) def test_sphere_transform(self): # USGS Professional Paper 1395, pg 291 @@ -112,7 +107,7 @@ other_args = {'a=1.0', 'b=1.0', 'lon_0=-96.0', 'lat_0=23.0', 'x_0=0.0', 'y_0=0.0', 'lat_1=29.5', 'lat_2=45.5'} - check_proj4_params(aea, other_args) + check_proj_params('aea', aea, other_args) assert_almost_equal(np.array(aea.x_limits), [-2.6525072042232, 2.6525072042232], @@ -141,7 +136,7 @@ other_args = {'a=6378206.4', 'f=0.003390076308689371', 'lon_0=-96.0', 'lat_0=23.0', 'x_0=0.0', 'y_0=0.0', 'lat_1=29.5', 'lat_2=45.5'} - check_proj4_params(aea, other_args) + check_proj_params('aea', aea, other_args) assert_almost_equal(np.array(aea.x_limits), [-16900972.674607, 16900972.674607], diff -Nru python-cartopy-0.17.0+dfsg/lib/cartopy/tests/crs/test_azimuthal_equidistant.py python-cartopy-0.18.0+dfsg/lib/cartopy/tests/crs/test_azimuthal_equidistant.py --- python-cartopy-0.17.0+dfsg/lib/cartopy/tests/crs/test_azimuthal_equidistant.py 2018-11-17 07:25:32.000000000 +0000 +++ python-cartopy-0.18.0+dfsg/lib/cartopy/tests/crs/test_azimuthal_equidistant.py 2020-05-03 08:12:47.000000000 +0000 @@ -21,12 +21,7 @@ from numpy.testing import assert_almost_equal, assert_array_almost_equal import cartopy.crs as ccrs - - -def check_proj4_params(crs, other_args): - expected = other_args | {'proj=aeqd', 'no_defs'} - pro4_params = set(crs.proj4_init.lstrip('+').split(' +')) - assert expected == pro4_params +from .helpers import check_proj_params class TestAzimuthalEquidistant(object): @@ -34,7 +29,7 @@ aeqd = ccrs.AzimuthalEquidistant() other_args = {'ellps=WGS84', 'lon_0=0.0', 'lat_0=0.0', 'x_0=0.0', 'y_0=0.0'} - check_proj4_params(aeqd, other_args) + check_proj_params('aeqd', aeqd, other_args) assert_almost_equal(np.array(aeqd.x_limits), [-20037508.34278924, 20037508.34278924], decimal=6) @@ -47,7 +42,7 @@ aeqd = ccrs.AzimuthalEquidistant(globe=globe) other_args = {'a=1000', 'b=500', 'lon_0=0.0', 'lat_0=0.0', 'x_0=0.0', 'y_0=0.0'} - check_proj4_params(aeqd, other_args) + check_proj_params('aeqd', aeqd, other_args) assert_almost_equal(np.array(aeqd.x_limits), [-3141.59265359, 3141.59265359], decimal=6) @@ -60,7 +55,7 @@ other_args = {'ellps=WGS84', 'lon_0=0.0', 'lat_0=0.0', 'x_0=1234', 'y_0=-4321'} - check_proj4_params(aeqd_offset, other_args) + check_proj_params('aeqd', aeqd_offset, other_args) assert_almost_equal(np.array(aeqd_offset.x_limits), [-20036274.34278924, 20038742.34278924], decimal=6) @@ -78,7 +73,7 @@ other_args = {'a=1.0', 'b=1.0', 'lon_0=0.0', 'lat_0=0.0', 'x_0=0.0', 'y_0=0.0'} - check_proj4_params(aeqd, other_args) + check_proj_params('aeqd', aeqd, other_args) assert_almost_equal(np.array(aeqd.x_limits), [-3.14159265, 3.14159265], decimal=6) @@ -147,7 +142,7 @@ other_args = {'a=3.0', 'b=3.0', 'lon_0=-100.0', 'lat_0=40.0', 'x_0=0.0', 'y_0=0.0'} - check_proj4_params(aeqd, other_args) + check_proj_params('aeqd', aeqd, other_args) assert_almost_equal(np.array(aeqd.x_limits), [-9.42477796, 9.42477796], decimal=6) @@ -169,7 +164,7 @@ other_args = {'a=6378388.0', 'f=0.003367003355798981', 'lon_0=-100.0', 'lat_0=90.0', 'x_0=0.0', 'y_0=0.0'} - check_proj4_params(aeqd, other_args) + check_proj_params('aeqd', aeqd, other_args) assert_almost_equal(np.array(aeqd.x_limits), [-20038296.88254529, 20038296.88254529], decimal=6) @@ -199,7 +194,7 @@ other_args = {'a=6378206.4', 'f=0.003390076308689371', 'lon_0=144.7487507055556', 'lat_0=13.47246635277778', 'x_0=50000.0', 'y_0=50000.0'} - check_proj4_params(aeqd, other_args) + check_proj_params('aeqd', aeqd, other_args) assert_almost_equal(np.array(aeqd.x_limits), [-19987726.36931940, 20087726.36931940], decimal=6) @@ -233,7 +228,7 @@ other_args = {'a=6378206.4', 'f=0.003390076308689371', 'lon_0=145.7416588888889', 'lat_0=15.18491194444444', 'x_0=28657.52', 'y_0=67199.99000000001'} - check_proj4_params(aeqd, other_args) + check_proj_params('aeqd', aeqd, other_args) assert_almost_equal(np.array(aeqd.x_limits), [-20009068.84931940, 20066383.88931940], decimal=6) diff -Nru python-cartopy-0.17.0+dfsg/lib/cartopy/tests/crs/test_eckert.py python-cartopy-0.18.0+dfsg/lib/cartopy/tests/crs/test_eckert.py --- python-cartopy-0.17.0+dfsg/lib/cartopy/tests/crs/test_eckert.py 2018-11-17 07:25:32.000000000 +0000 +++ python-cartopy-0.18.0+dfsg/lib/cartopy/tests/crs/test_eckert.py 2020-05-03 08:12:47.000000000 +0000 @@ -26,12 +26,7 @@ import pytest import cartopy.crs as ccrs - - -def check_proj4_params(name, crs, other_args): - expected = other_args | {'proj=' + name, 'no_defs'} - pro4_params = set(crs.proj4_init.lstrip('+').split(' +')) - assert expected == pro4_params +from .helpers import check_proj_params @pytest.mark.parametrize('name, proj, lim', [ @@ -45,7 +40,72 @@ def test_default(name, proj, lim): eck = proj() other_args = {'a=6378137.0', 'lon_0=0'} - check_proj4_params(name, eck, other_args) + check_proj_params(name, eck, other_args) + + assert_almost_equal(eck.x_limits, [-lim, lim]) + assert_almost_equal(eck.y_limits, [-lim / 2, lim / 2]) + + +@pytest.mark.parametrize('name, proj, lim', [ + pytest.param('eck1', ccrs.EckertI, 2894.4050182, id='EckertI'), + pytest.param('eck2', ccrs.EckertII, 2894.4050182, id='EckertII'), + pytest.param('eck3', ccrs.EckertIII, 2653.0008564, id='EckertIII'), + pytest.param('eck4', ccrs.EckertIV, 2653.0008564, id='EckertIV'), + pytest.param('eck5', ccrs.EckertV, 2770.9649676, id='EckertV'), + pytest.param('eck6', ccrs.EckertVI, 2770.9649676, id='EckertVI'), +]) +def test_sphere_globe(name, proj, lim): + globe = ccrs.Globe(semimajor_axis=1000, ellipse=None) + eck = proj(globe=globe) + other_args = {'a=1000', 'lon_0=0'} + check_proj_params(name, eck, other_args) + + assert_almost_equal(eck.x_limits, [-lim, lim]) + assert_almost_equal(eck.y_limits, [-lim / 2, lim / 2]) + + +@pytest.mark.parametrize('name, proj, lim', [ + # Limits are the same as default since ellipses are not supported. + pytest.param('eck1', ccrs.EckertI, 18460911.739778, id='EckertI'), + pytest.param('eck2', ccrs.EckertII, 18460911.739778, id='EckertII'), + pytest.param('eck3', ccrs.EckertIII, 16921202.9229432, id='EckertIII'), + pytest.param('eck4', ccrs.EckertIV, 16921202.9229432, id='EckertIV'), + pytest.param('eck5', ccrs.EckertV, 17673594.1854146, id='EckertV'), + pytest.param('eck6', ccrs.EckertVI, 17673594.1854146, id='EckertVI'), +]) +def test_ellipse_globe(name, proj, lim): + globe = ccrs.Globe(ellipse='WGS84') + with pytest.warns(UserWarning, + match='does not handle elliptical globes.') as w: + eck = proj(globe=globe) + assert len(w) == 1 + + other_args = {'ellps=WGS84', 'lon_0=0'} + check_proj_params(name, eck, other_args) + + assert_almost_equal(eck.x_limits, [-lim, lim]) + assert_almost_equal(eck.y_limits, [-lim / 2, lim / 2]) + + +@pytest.mark.parametrize('name, proj, lim', [ + # Limits are the same as spheres since ellipses are not supported. + pytest.param('eck1', ccrs.EckertI, 2894.4050182, id='EckertI'), + pytest.param('eck2', ccrs.EckertII, 2894.4050182, id='EckertII'), + pytest.param('eck3', ccrs.EckertIII, 2653.0008564, id='EckertIII'), + pytest.param('eck4', ccrs.EckertIV, 2653.0008564, id='EckertIV'), + pytest.param('eck5', ccrs.EckertV, 2770.9649676, id='EckertV'), + pytest.param('eck6', ccrs.EckertVI, 2770.9649676, id='EckertVI'), +]) +def test_eccentric_globe(name, proj, lim): + globe = ccrs.Globe(semimajor_axis=1000, semiminor_axis=500, + ellipse=None) + with pytest.warns(UserWarning, + match='does not handle elliptical globes.') as w: + eck = proj(globe=globe) + assert len(w) == 1 + + other_args = {'a=1000', 'b=500', 'lon_0=0'} + check_proj_params(name, eck, other_args) assert_almost_equal(eck.x_limits, [-lim, lim]) assert_almost_equal(eck.y_limits, [-lim / 2, lim / 2]) @@ -63,7 +123,7 @@ crs = proj() crs_offset = proj(false_easting=1234, false_northing=-4321) other_args = {'a=6378137.0', 'lon_0=0', 'x_0=1234', 'y_0=-4321'} - check_proj4_params(name, crs_offset, other_args) + check_proj_params(name, 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 @@ -80,7 +140,7 @@ def test_central_longitude(name, proj, lim, lon): eck = proj(central_longitude=lon) other_args = {'a=6378137.0', 'lon_0={}'.format(lon)} - check_proj4_params(name, eck, other_args) + check_proj_params(name, eck, other_args) assert_almost_equal(eck.x_limits, [-lim, lim], decimal=5) assert_almost_equal(eck.y_limits, [-lim / 2, lim / 2]) @@ -114,7 +174,7 @@ geodetic = eck.as_geodetic() other_args = {'a={}'.format(radius), 'lon_0=0'} - check_proj4_params(name, eck, other_args) + check_proj_params(name, eck, other_args) assert_almost_equal(eck.x_limits, [-2, 2], decimal=5) assert_almost_equal(eck.y_limits, [-1, 1], decimal=5) @@ -141,7 +201,7 @@ geodetic = eck.as_geodetic() other_args = {'a=1.0', 'lon_0=-90.0'} - check_proj4_params(name, eck, other_args) + check_proj_params(name, eck, other_args) assert_almost_equal(eck.x_limits, [-lim, lim], decimal=2) assert_almost_equal(eck.y_limits, [-lim / 2, lim / 2]) diff -Nru python-cartopy-0.17.0+dfsg/lib/cartopy/tests/crs/test_equal_earth.py python-cartopy-0.18.0+dfsg/lib/cartopy/tests/crs/test_equal_earth.py --- python-cartopy-0.17.0+dfsg/lib/cartopy/tests/crs/test_equal_earth.py 2018-11-17 07:25:32.000000000 +0000 +++ python-cartopy-0.18.0+dfsg/lib/cartopy/tests/crs/test_equal_earth.py 2020-05-03 08:12:47.000000000 +0000 @@ -26,22 +26,17 @@ import pytest import cartopy.crs as ccrs +from .helpers import check_proj_params 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) + check_proj_params('eqearth', eqearth, other_args) assert_almost_equal(eqearth.x_limits, [-17243959.0622169, 17243959.0622169]) @@ -56,7 +51,7 @@ 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) + check_proj_params('eqearth', 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 @@ -66,7 +61,7 @@ ellipse=None) eqearth = ccrs.EqualEarth(globe=globe) other_args = {'a=1000', 'b=500', 'lon_0=0'} - check_proj_params(eqearth, other_args) + check_proj_params('eqearth', eqearth, other_args) assert_almost_equal(eqearth.x_limits, [-2248.43664092550, 2248.43664092550]) @@ -81,7 +76,7 @@ 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) + check_proj_params('eqearth', eqearth, other_args) assert_almost_equal(eqearth.x_limits, [-17243959.0622169, 17243959.0622169], decimal=5) diff -Nru python-cartopy-0.17.0+dfsg/lib/cartopy/tests/crs/test_equidistant_conic.py python-cartopy-0.18.0+dfsg/lib/cartopy/tests/crs/test_equidistant_conic.py --- python-cartopy-0.17.0+dfsg/lib/cartopy/tests/crs/test_equidistant_conic.py 2018-11-17 07:25:32.000000000 +0000 +++ python-cartopy-0.18.0+dfsg/lib/cartopy/tests/crs/test_equidistant_conic.py 2020-05-03 08:12:47.000000000 +0000 @@ -26,12 +26,7 @@ import pytest import cartopy.crs as ccrs - - -def check_proj4_params(crs, other_args): - expected = other_args | {'proj=eqdc', 'no_defs'} - pro4_params = set(crs.proj4_init.lstrip('+').split(' +')) - assert expected == pro4_params +from .helpers import check_proj_params class TestEquidistantConic(object): @@ -39,7 +34,7 @@ eqdc = ccrs.EquidistantConic() other_args = {'ellps=WGS84', 'lon_0=0.0', 'lat_0=0.0', 'x_0=0.0', 'y_0=0.0', 'lat_1=20.0', 'lat_2=50.0'} - check_proj4_params(eqdc, other_args) + check_proj_params('eqdc', eqdc, other_args) assert_almost_equal(np.array(eqdc.x_limits), (-22784919.35600352, 22784919.35600352), @@ -54,7 +49,7 @@ eqdc = ccrs.EquidistantConic(globe=globe) other_args = {'a=1000', 'b=500', 'lon_0=0.0', 'lat_0=0.0', 'x_0=0.0', 'y_0=0.0', 'lat_1=20.0', 'lat_2=50.0'} - check_proj4_params(eqdc, other_args) + check_proj_params('eqdc', eqdc, other_args) assert_almost_equal(np.array(eqdc.x_limits), (-3016.869847713461, 3016.869847713461), @@ -69,7 +64,7 @@ other_args = {'ellps=WGS84', 'lon_0=0.0', 'lat_0=0.0', 'x_0=1234', 'y_0=-4321', 'lat_1=20.0', 'lat_2=50.0'} - check_proj4_params(eqdc_offset, other_args) + check_proj_params('eqdc', eqdc_offset, other_args) @pytest.mark.parametrize('lon', [-10.0, 10.0]) def test_central_longitude(self, lon): @@ -77,7 +72,7 @@ eqdc_offset = ccrs.EquidistantConic(central_longitude=lon) other_args = {'ellps=WGS84', 'lon_0={}'.format(lon), 'lat_0=0.0', 'x_0=0.0', 'y_0=0.0', 'lat_1=20.0', 'lat_2=50.0'} - check_proj4_params(eqdc_offset, other_args) + check_proj_params('eqdc', eqdc_offset, other_args) assert_array_almost_equal(eqdc_offset.boundary, eqdc.boundary, decimal=0) @@ -86,17 +81,17 @@ eqdc = ccrs.EquidistantConic(standard_parallels=(13, 37)) other_args = {'ellps=WGS84', 'lon_0=0.0', 'lat_0=0.0', 'x_0=0.0', 'y_0=0.0', 'lat_1=13', 'lat_2=37'} - check_proj4_params(eqdc, other_args) + check_proj_params('eqdc', eqdc, other_args) eqdc = ccrs.EquidistantConic(standard_parallels=(13, )) other_args = {'ellps=WGS84', 'lon_0=0.0', 'lat_0=0.0', 'x_0=0.0', 'y_0=0.0', 'lat_1=13'} - check_proj4_params(eqdc, other_args) + check_proj_params('eqdc', eqdc, other_args) eqdc = ccrs.EquidistantConic(standard_parallels=13) other_args = {'ellps=WGS84', 'lon_0=0.0', 'lat_0=0.0', 'x_0=0.0', 'y_0=0.0', 'lat_1=13'} - check_proj4_params(eqdc, other_args) + check_proj_params('eqdc', eqdc, other_args) def test_sphere_transform(self): # USGS Professional Paper 1395, pg 298 @@ -112,7 +107,7 @@ other_args = {'a=1.0', 'b=1.0', 'lon_0=-96.0', 'lat_0=23.0', 'x_0=0.0', 'y_0=0.0', 'lat_1=29.5', 'lat_2=45.5'} - check_proj4_params(eqdc, other_args) + check_proj_params('eqdc', eqdc, other_args) assert_almost_equal(np.array(eqdc.x_limits), (-3.520038619089038, 3.520038619089038), @@ -141,7 +136,7 @@ other_args = {'a=6378206.4', 'f=0.003390076308689371', 'lon_0=-96.0', 'lat_0=23.0', 'x_0=0.0', 'y_0=0.0', 'lat_1=29.5', 'lat_2=45.5'} - check_proj4_params(eqdc, other_args) + check_proj_params('eqdc', eqdc, other_args) assert_almost_equal(np.array(eqdc.x_limits), (-22421870.719894886, 22421870.719894886), diff -Nru python-cartopy-0.17.0+dfsg/lib/cartopy/tests/crs/test_geostationary.py python-cartopy-0.18.0+dfsg/lib/cartopy/tests/crs/test_geostationary.py --- python-cartopy-0.17.0+dfsg/lib/cartopy/tests/crs/test_geostationary.py 2018-11-17 07:25:32.000000000 +0000 +++ python-cartopy-0.18.0+dfsg/lib/cartopy/tests/crs/test_geostationary.py 2020-05-03 08:12:47.000000000 +0000 @@ -24,12 +24,7 @@ from numpy.testing import assert_almost_equal import cartopy.crs as ccrs - - -def check_proj4_params(name, crs, other_args): - expected = other_args | {'proj={}'.format(name), 'units=m', 'no_defs'} - pro4_params = set(crs.proj4_init.lstrip('+').split(' +')) - assert expected == pro4_params +from .helpers import check_proj_params class TestGeostationary(object): @@ -44,39 +39,42 @@ def test_default(self): geos = self.test_class() other_args = {'ellps=WGS84', 'h=35785831', 'lat_0=0.0', 'lon_0=0.0', - 'x_0=0', 'y_0=0'} + 'units=m', 'x_0=0', 'y_0=0'} self.adjust_expected_params(other_args) - check_proj4_params(self.expected_proj_name, geos, other_args) + check_proj_params(self.expected_proj_name, geos, other_args) assert_almost_equal(geos.boundary.bounds, (-5434177.81588539, -5434177.81588539, 5434177.81588539, 5434177.81588539), decimal=4) - def test_eccentric_globe(self): - globe = ccrs.Globe(semimajor_axis=10000, semiminor_axis=5000, - ellipse=None) - geos = self.test_class(satellite_height=50000, - globe=globe) - other_args = {'a=10000', 'b=5000', 'h=50000', 'lat_0=0.0', 'lon_0=0.0', - 'x_0=0', 'y_0=0'} + def test_low_orbit(self): + geos = self.test_class(satellite_height=700000) + other_args = {'ellps=WGS84', 'h=700000', 'lat_0=0.0', 'lon_0=0.0', + 'units=m', 'x_0=0', 'y_0=0'} self.adjust_expected_params(other_args) - check_proj4_params(self.expected_proj_name, geos, other_args) + check_proj_params(self.expected_proj_name, geos, other_args) assert_almost_equal(geos.boundary.bounds, - (-8372.4040, -4171.5043, 8372.4040, 4171.5043), + (-785616.1189, -785616.1189, + 785616.1189, 785616.1189), + decimal=4) + + # Checking that this isn't just a simple elliptical border + assert_almost_equal(geos.boundary.coords[7], + (697323.205, -453041.0626), decimal=4) def test_eastings(self): geos = self.test_class(false_easting=5000000, false_northing=-125000,) other_args = {'ellps=WGS84', 'h=35785831', 'lat_0=0.0', 'lon_0=0.0', - 'x_0=5000000', 'y_0=-125000'} + 'units=m', 'x_0=5000000', 'y_0=-125000'} self.adjust_expected_params(other_args) - check_proj4_params(self.expected_proj_name, geos, other_args) + check_proj_params(self.expected_proj_name, geos, other_args) assert_almost_equal(geos.boundary.bounds, (-434177.81588539, -5559177.81588539, @@ -86,9 +84,9 @@ def test_sweep(self): geos = ccrs.Geostationary(sweep_axis='x') other_args = {'ellps=WGS84', 'h=35785831', 'lat_0=0.0', 'lon_0=0.0', - 'sweep=x', 'x_0=0', 'y_0=0'} + 'sweep=x', 'units=m', 'x_0=0', 'y_0=0'} - check_proj4_params(self.expected_proj_name, geos, other_args) + check_proj_params(self.expected_proj_name, geos, other_args) pt = geos.transform_point(-60, 25, ccrs.PlateCarree()) diff -Nru python-cartopy-0.17.0+dfsg/lib/cartopy/tests/crs/test_gnomonic.py python-cartopy-0.18.0+dfsg/lib/cartopy/tests/crs/test_gnomonic.py --- python-cartopy-0.17.0+dfsg/lib/cartopy/tests/crs/test_gnomonic.py 2018-11-17 07:25:32.000000000 +0000 +++ python-cartopy-0.18.0+dfsg/lib/cartopy/tests/crs/test_gnomonic.py 2020-05-03 08:12:47.000000000 +0000 @@ -26,18 +26,13 @@ import pytest import cartopy.crs as ccrs - - -def check_proj4_params(crs, other_args): - expected = other_args | {'proj=gnom', 'no_defs'} - pro4_params = set(crs.proj4_init.lstrip('+').split(' +')) - assert expected == pro4_params +from .helpers import check_proj_params def test_default(): gnom = ccrs.Gnomonic() - other_args = {'ellps=WGS84', 'lon_0=0.0', 'lat_0=0.0'} - check_proj4_params(gnom, other_args) + other_args = {'a=6378137.0', 'lon_0=0.0', 'lat_0=0.0'} + check_proj_params('gnom', gnom, other_args) assert_almost_equal(np.array(gnom.x_limits), [-5e7, 5e7]) @@ -45,13 +40,54 @@ [-5e7, 5e7]) +def test_sphere_globe(): + globe = ccrs.Globe(semimajor_axis=1000, ellipse=None) + gnom = ccrs.Gnomonic(globe=globe) + other_args = {'a=1000', 'lon_0=0.0', 'lat_0=0.0'} + check_proj_params('gnom', gnom, other_args) + + assert_almost_equal(gnom.x_limits, [-5e7, 5e7]) + assert_almost_equal(gnom.y_limits, [-5e7, 5e7]) + + +def test_ellipse_globe(): + globe = ccrs.Globe(ellipse='WGS84') + with pytest.warns(UserWarning, + match='does not handle elliptical globes.') as w: + gnom = ccrs.Gnomonic(globe=globe) + assert len(w) == 1 + + other_args = {'ellps=WGS84', 'lon_0=0.0', 'lat_0=0.0'} + check_proj_params('gnom', gnom, other_args) + + # Limits are the same as default since ellipses are not supported. + assert_almost_equal(gnom.x_limits, [-5e7, 5e7]) + assert_almost_equal(gnom.y_limits, [-5e7, 5e7]) + + +def test_eccentric_globe(): + globe = ccrs.Globe(semimajor_axis=1000, semiminor_axis=500, + ellipse=None) + with pytest.warns(UserWarning, + match='does not handle elliptical globes.') as w: + gnom = ccrs.Gnomonic(globe=globe) + assert len(w) == 1 + + other_args = {'a=1000', 'b=500', 'lon_0=0.0', 'lat_0=0.0'} + check_proj_params('gnom', gnom, other_args) + + # Limits are the same as spheres since ellipses are not supported. + assert_almost_equal(gnom.x_limits, [-5e7, 5e7]) + assert_almost_equal(gnom.y_limits, [-5e7, 5e7]) + + @pytest.mark.parametrize('lat', [-10, 0, 10]) @pytest.mark.parametrize('lon', [-10, 0, 10]) def test_central_params(lat, lon): gnom = ccrs.Gnomonic(central_latitude=lat, central_longitude=lon) other_args = {'lat_0={}'.format(lat), 'lon_0={}'.format(lon), - 'ellps=WGS84'} - check_proj4_params(gnom, other_args) + 'a=6378137.0'} + check_proj_params('gnom', gnom, other_args) assert_almost_equal(np.array(gnom.x_limits), [-5e7, 5e7]) @@ -67,7 +103,7 @@ geodetic = gnom.as_geodetic() other_args = {'a=1.0', 'b=1.0', 'lon_0=0.0', 'lat_0=0.0'} - check_proj4_params(gnom, other_args) + check_proj_params('gnom', gnom, other_args) assert_almost_equal(np.array(gnom.x_limits), [-5e7, 5e7]) @@ -109,7 +145,7 @@ geodetic = gnom.as_geodetic() other_args = {'a=1.0', 'b=1.0', 'lon_0=-100.0', 'lat_0=40.0'} - check_proj4_params(gnom, other_args) + check_proj_params('gnom', gnom, other_args) assert_almost_equal(np.array(gnom.x_limits), [-5e7, 5e7]) diff -Nru python-cartopy-0.17.0+dfsg/lib/cartopy/tests/crs/test_interrupted_goode_homolosine.py python-cartopy-0.18.0+dfsg/lib/cartopy/tests/crs/test_interrupted_goode_homolosine.py --- python-cartopy-0.17.0+dfsg/lib/cartopy/tests/crs/test_interrupted_goode_homolosine.py 2018-11-17 07:25:32.000000000 +0000 +++ python-cartopy-0.18.0+dfsg/lib/cartopy/tests/crs/test_interrupted_goode_homolosine.py 2020-05-03 08:12:47.000000000 +0000 @@ -26,18 +26,13 @@ import pytest import cartopy.crs as ccrs - - -def check_proj4_params(crs, other_args): - expected = other_args | {'proj=igh', 'no_defs'} - pro4_params = set(crs.proj4_init.lstrip('+').split(' +')) - assert expected == pro4_params +from .helpers import check_proj_params def test_default(): igh = ccrs.InterruptedGoodeHomolosine() other_args = {'ellps=WGS84', 'lon_0=0'} - check_proj4_params(igh, other_args) + check_proj_params('igh', igh, other_args) assert_almost_equal(np.array(igh.x_limits), [-20037508.3427892, 20037508.3427892]) @@ -50,7 +45,7 @@ ellipse=None) igh = ccrs.InterruptedGoodeHomolosine(globe=globe) other_args = {'a=1000', 'b=500', 'lon_0=0'} - check_proj4_params(igh, other_args) + check_proj_params('igh', igh, other_args) assert_almost_equal(np.array(igh.x_limits), [-3141.5926536, 3141.5926536]) @@ -62,7 +57,7 @@ def test_central_longitude(lon): igh = ccrs.InterruptedGoodeHomolosine(central_longitude=lon) other_args = {'ellps=WGS84', 'lon_0={}'.format(lon)} - check_proj4_params(igh, other_args) + check_proj_params('igh', igh, other_args) assert_almost_equal(np.array(igh.x_limits), [-20037508.3427892, 20037508.3427892], diff -Nru python-cartopy-0.17.0+dfsg/lib/cartopy/tests/crs/test_lambert_azimuthal_equal_area.py python-cartopy-0.18.0+dfsg/lib/cartopy/tests/crs/test_lambert_azimuthal_equal_area.py --- python-cartopy-0.17.0+dfsg/lib/cartopy/tests/crs/test_lambert_azimuthal_equal_area.py 2018-11-17 07:25:32.000000000 +0000 +++ python-cartopy-0.18.0+dfsg/lib/cartopy/tests/crs/test_lambert_azimuthal_equal_area.py 2020-05-03 08:12:47.000000000 +0000 @@ -22,12 +22,7 @@ import pytest import cartopy.crs as ccrs - - -def check_proj4_params(crs, other_args): - expected = other_args | {'proj=laea', 'no_defs'} - pro4_params = set(crs.proj4_init.lstrip('+').split(' +')) - assert expected == pro4_params +from .helpers import check_proj_params class TestLambertAzimuthalEqualArea(object): @@ -35,7 +30,7 @@ crs = ccrs.LambertAzimuthalEqualArea() other_args = {'ellps=WGS84', 'lon_0=0.0', 'lat_0=0.0', 'x_0=0.0', 'y_0=0.0'} - check_proj4_params(crs, other_args) + check_proj_params('laea', crs, other_args) assert_almost_equal(np.array(crs.x_limits), [-12755636.1863, 12755636.1863], @@ -50,7 +45,7 @@ crs = ccrs.LambertAzimuthalEqualArea(globe=globe) other_args = {'a=1000', 'b=500', 'lon_0=0.0', 'lat_0=0.0', 'x_0=0.0', 'y_0=0.0'} - check_proj4_params(crs, other_args) + check_proj_params('laea', crs, other_args) assert_almost_equal(np.array(crs.x_limits), [-1999.9, 1999.9], decimal=1) @@ -63,7 +58,7 @@ false_northing=-4321) other_args = {'ellps=WGS84', 'lon_0=0.0', 'lat_0=0.0', 'x_0=1234', 'y_0=-4321'} - check_proj4_params(crs_offset, other_args) + check_proj_params('laea', 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 @@ -72,4 +67,4 @@ crs = ccrs.LambertAzimuthalEqualArea(central_latitude=latitude) other_args = {'ellps=WGS84', 'lon_0=0.0', 'lat_0={}'.format(latitude), 'x_0=0.0', 'y_0=0.0'} - check_proj4_params(crs, other_args) + check_proj_params('laea', crs, other_args) diff -Nru python-cartopy-0.17.0+dfsg/lib/cartopy/tests/crs/test_lambert_conformal.py python-cartopy-0.18.0+dfsg/lib/cartopy/tests/crs/test_lambert_conformal.py --- python-cartopy-0.17.0+dfsg/lib/cartopy/tests/crs/test_lambert_conformal.py 2018-11-17 07:25:32.000000000 +0000 +++ python-cartopy-0.18.0+dfsg/lib/cartopy/tests/crs/test_lambert_conformal.py 2020-05-03 08:12:47.000000000 +0000 @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2011 - 2018, Met Office +# (C) British Crown Copyright 2011 - 2019, Met Office # # This file is part of cartopy. # @@ -21,19 +21,14 @@ import pytest import cartopy.crs as ccrs - - -def check_proj4_params(crs, other_args): - expected = other_args | {'proj=lcc', 'no_defs'} - pro4_params = set(crs.proj4_init.lstrip('+').split(' +')) - assert expected == pro4_params +from .helpers import check_proj_params def test_defaults(): crs = ccrs.LambertConformal() other_args = {'ellps=WGS84', 'lon_0=-96.0', 'lat_0=39.0', 'x_0=0.0', 'y_0=0.0', 'lat_1=33', 'lat_2=45'} - check_proj4_params(crs, other_args) + check_proj_params('lcc', crs, other_args) def test_default_with_cutoff(): @@ -43,7 +38,7 @@ other_args = {'ellps=WGS84', 'lon_0=-96.0', 'lat_0=39.0', 'x_0=0.0', 'y_0=0.0', 'lat_1=33', 'lat_2=45'} - check_proj4_params(crs, other_args) + check_proj_params('lcc', crs, other_args) # Check the behaviour of !=, == and (not ==) for the different cutoffs. assert crs == crs2 @@ -66,7 +61,7 @@ globe=ccrs.Globe(ellipse='GRS80')) other_args = {'ellps=GRS80', 'lon_0=10', 'lat_0=52', 'x_0=4000000', 'y_0=2800000', 'lat_1=35', 'lat_2=65'} - check_proj4_params(crs, other_args) + check_proj_params('lcc', crs, other_args) class Test_LambertConformal_standard_parallels(object): @@ -74,14 +69,14 @@ crs = ccrs.LambertConformal(standard_parallels=[1.]) other_args = {'ellps=WGS84', 'lon_0=-96.0', 'lat_0=39.0', 'x_0=0.0', 'y_0=0.0', 'lat_1=1.0'} - check_proj4_params(crs, other_args) + check_proj_params('lcc', crs, other_args) def test_no_parallel(self): - with pytest.raises(ValueError, message='1 or 2 standard parallels'): + with pytest.raises(ValueError, match='1 or 2 standard parallels'): ccrs.LambertConformal(standard_parallels=[]) def test_too_many_parallel(self): - with pytest.raises(ValueError, message='1 or 2 standard parallels'): + with pytest.raises(ValueError, match='1 or 2 standard parallels'): ccrs.LambertConformal(standard_parallels=[1, 2, 3]) def test_single_spole(self): diff -Nru python-cartopy-0.17.0+dfsg/lib/cartopy/tests/crs/test_mercator.py python-cartopy-0.18.0+dfsg/lib/cartopy/tests/crs/test_mercator.py --- python-cartopy-0.17.0+dfsg/lib/cartopy/tests/crs/test_mercator.py 2018-11-17 07:25:32.000000000 +0000 +++ python-cartopy-0.18.0+dfsg/lib/cartopy/tests/crs/test_mercator.py 2020-05-03 08:12:47.000000000 +0000 @@ -21,19 +21,14 @@ import pytest import cartopy.crs as ccrs - - -def check_proj4_params(crs, other_args): - expected = other_args | {'proj=merc', 'units=m', 'no_defs'} - pro4_params = set(crs.proj4_init.lstrip('+').split(' +')) - assert expected == pro4_params +from .helpers import check_proj_params def test_default(): crs = ccrs.Mercator() - other_args = {'ellps=WGS84', 'lon_0=0.0', 'x_0=0.0', 'y_0=0.0'} - check_proj4_params(crs, other_args) + other_args = {'ellps=WGS84', 'lon_0=0.0', 'x_0=0.0', 'y_0=0.0', 'units=m'} + check_proj_params('merc', crs, other_args) assert_almost_equal(crs.boundary.bounds, [-20037508, -15496571, 20037508, 18764656], decimal=0) @@ -42,8 +37,9 @@ globe = ccrs.Globe(semimajor_axis=10000, semiminor_axis=5000, ellipse=None) crs = ccrs.Mercator(globe=globe, min_latitude=-40, max_latitude=40) - other_args = {'a=10000', 'b=5000', 'lon_0=0.0', 'x_0=0.0', 'y_0=0.0'} - check_proj4_params(crs, other_args) + other_args = {'a=10000', 'b=5000', 'lon_0=0.0', 'x_0=0.0', 'y_0=0.0', + 'units=m'} + check_proj_params('merc', crs, other_args) assert_almost_equal(crs.boundary.bounds, [-31415.93, -2190.5, 31415.93, 2190.5], decimal=2) @@ -67,8 +63,9 @@ @pytest.mark.parametrize('lon', [-10.0, 10.0]) def test_central_longitude(lon): crs = ccrs.Mercator(central_longitude=lon) - other_args = {'ellps=WGS84', 'lon_0={}'.format(lon), 'x_0=0.0', 'y_0=0.0'} - check_proj4_params(crs, other_args) + other_args = {'ellps=WGS84', 'lon_0={}'.format(lon), 'x_0=0.0', 'y_0=0.0', + 'units=m'} + check_proj_params('merc', crs, other_args) assert_almost_equal(crs.boundary.bounds, [-20037508, -15496570, 20037508, 18764656], decimal=0) @@ -77,9 +74,9 @@ def test_latitude_true_scale(): lat_ts = 20.0 crs = ccrs.Mercator(latitude_true_scale=lat_ts) - other_args = {'ellps=WGS84', 'lon_0=0.0', 'x_0=0.0', 'y_0=0.0', + other_args = {'ellps=WGS84', 'lon_0=0.0', 'x_0=0.0', 'y_0=0.0', 'units=m', 'lat_ts={}'.format(lat_ts)} - check_proj4_params(crs, other_args) + check_proj_params('merc', crs, other_args) assert_almost_equal(crs.boundary.bounds, [-18836475, -14567718, 18836475, 17639917], decimal=0) @@ -91,8 +88,8 @@ crs = ccrs.Mercator(false_easting=false_easting, false_northing=false_northing) other_args = {'ellps=WGS84', 'lon_0=0.0', 'x_0={}'.format(false_easting), - 'y_0={}'.format(false_northing)} - check_proj4_params(crs, other_args) + 'y_0={}'.format(false_northing), 'units=m'} + check_proj_params('merc', crs, other_args) assert_almost_equal(crs.boundary.bounds, [-19037508, -17496571, 21037508, 16764656], decimal=0) @@ -103,9 +100,9 @@ scale_factor = 0.939692620786 crs = ccrs.Mercator(scale_factor=scale_factor, globe=ccrs.Globe(ellipse='sphere')) - other_args = {'ellps=sphere', 'lon_0=0.0', 'x_0=0.0', 'y_0=0.0', + other_args = {'ellps=sphere', 'lon_0=0.0', 'x_0=0.0', 'y_0=0.0', 'units=m', 'k_0={:.12f}'.format(scale_factor)} - check_proj4_params(crs, other_args) + check_proj_params('merc', crs, other_args) assert_almost_equal(crs.boundary.bounds, [-18808021, -14585266, 18808021, 17653216], decimal=0) diff -Nru python-cartopy-0.17.0+dfsg/lib/cartopy/tests/crs/test_miller.py python-cartopy-0.18.0+dfsg/lib/cartopy/tests/crs/test_miller.py --- python-cartopy-0.17.0+dfsg/lib/cartopy/tests/crs/test_miller.py 2018-11-17 07:25:32.000000000 +0000 +++ python-cartopy-0.18.0+dfsg/lib/cartopy/tests/crs/test_miller.py 2020-05-03 08:12:47.000000000 +0000 @@ -26,18 +26,13 @@ import pytest import cartopy.crs as ccrs - - -def check_proj4_params(crs, other_args): - expected = other_args | {'proj=mill', 'no_defs'} - pro4_params = set(crs.proj4_init.lstrip('+').split(' +')) - assert expected == pro4_params +from .helpers import check_proj_params def test_default(): mill = ccrs.Miller() other_args = {'a=57.29577951308232', 'lon_0=0.0'} - check_proj4_params(mill, other_args) + check_proj_params('mill', mill, other_args) assert_almost_equal(np.array(mill.x_limits), [-180, 180]) @@ -45,11 +40,56 @@ [-131.9758172, 131.9758172]) +def test_sphere_globe(): + globe = ccrs.Globe(semimajor_axis=1000, ellipse=None) + mill = ccrs.Miller(globe=globe) + other_args = {'a=1000', 'lon_0=0.0'} + check_proj_params('mill', mill, other_args) + + assert_almost_equal(mill.x_limits, [-3141.5926536, 3141.5926536]) + assert_almost_equal(mill.y_limits, [-2303.4125434, 2303.4125434]) + + +def test_ellipse_globe(): + globe = ccrs.Globe(ellipse='WGS84') + with pytest.warns(UserWarning, + match='does not handle elliptical globes.') as w: + mill = ccrs.Miller(globe=globe) + assert len(w) == 1 + + other_args = {'ellps=WGS84', 'lon_0=0.0'} + check_proj_params('mill', mill, other_args) + + # Limits are the same as spheres (but not the default radius) since + # ellipses are not supported. + mill_sph = ccrs.Miller( + globe=ccrs.Globe(semimajor_axis=ccrs.WGS84_SEMIMAJOR_AXIS, + ellipse=None)) + assert_almost_equal(mill.x_limits, mill_sph.x_limits) + assert_almost_equal(mill.y_limits, mill_sph.y_limits) + + +def test_eccentric_globe(): + globe = ccrs.Globe(semimajor_axis=1000, semiminor_axis=500, + ellipse=None) + with pytest.warns(UserWarning, + match='does not handle elliptical globes.') as w: + mill = ccrs.Miller(globe=globe) + assert len(w) == 1 + + other_args = {'a=1000', 'b=500', 'lon_0=0.0'} + check_proj_params('mill', mill, other_args) + + # Limits are the same as spheres since ellipses are not supported. + assert_almost_equal(mill.x_limits, [-3141.5926536, 3141.5926536]) + assert_almost_equal(mill.y_limits, [-2303.4125434, 2303.4125434]) + + @pytest.mark.parametrize('lon', [-10.0, 10.0]) def test_central_longitude(lon): mill = ccrs.Miller(central_longitude=lon) other_args = {'a=57.29577951308232', 'lon_0={}'.format(lon)} - check_proj4_params(mill, other_args) + check_proj_params('mill', mill, other_args) assert_almost_equal(np.array(mill.x_limits), [-180, 180]) @@ -64,7 +104,7 @@ geodetic = mill.as_geodetic() other_args = {'a=1.0', 'lon_0=0.0'} - check_proj4_params(mill, other_args) + check_proj_params('mill', mill, other_args) assert_almost_equal(np.array(mill.x_limits), [-3.14159265, 3.14159265]) @@ -91,7 +131,7 @@ geodetic = mill.as_geodetic() other_args = {'a=1.0', 'lon_0=0.0'} - check_proj4_params(mill, other_args) + check_proj_params('mill', mill, other_args) assert_almost_equal(np.array(mill.x_limits), [-3.14159265, 3.14159265]) diff -Nru python-cartopy-0.17.0+dfsg/lib/cartopy/tests/crs/test_mollweide.py python-cartopy-0.18.0+dfsg/lib/cartopy/tests/crs/test_mollweide.py --- python-cartopy-0.17.0+dfsg/lib/cartopy/tests/crs/test_mollweide.py 2018-11-17 07:25:32.000000000 +0000 +++ python-cartopy-0.18.0+dfsg/lib/cartopy/tests/crs/test_mollweide.py 2020-05-03 08:12:47.000000000 +0000 @@ -26,18 +26,13 @@ import pytest import cartopy.crs as ccrs - - -def check_proj4_params(crs, other_args): - expected = other_args | {'proj=moll', 'no_defs'} - pro4_params = set(crs.proj4_init.lstrip('+').split(' +')) - assert expected == pro4_params +from .helpers import check_proj_params def test_default(): moll = ccrs.Mollweide() other_args = {'a=6378137.0', 'lon_0=0'} - check_proj4_params(moll, other_args) + check_proj_params('moll', moll, other_args) assert_almost_equal(np.array(moll.x_limits), [-18040095.6961473, 18040095.6961473]) @@ -45,11 +40,52 @@ [-9020047.8480736, 9020047.8480736]) +def test_sphere_globe(): + globe = ccrs.Globe(semimajor_axis=1000, ellipse=None) + moll = ccrs.Mollweide(globe=globe) + other_args = {'a=1000', 'lon_0=0'} + check_proj_params('moll', moll, other_args) + + assert_almost_equal(moll.x_limits, [-2828.4271247, 2828.4271247]) + assert_almost_equal(moll.y_limits, [-1414.2135624, 1414.2135624]) + + +def test_ellipse_globe(): + globe = ccrs.Globe(ellipse='WGS84') + with pytest.warns(UserWarning, + match='does not handle elliptical globes.') as w: + moll = ccrs.Mollweide(globe=globe) + assert len(w) == 1 + + other_args = {'ellps=WGS84', 'lon_0=0'} + check_proj_params('moll', moll, other_args) + + # Limits are the same as default since ellipses are not supported. + assert_almost_equal(moll.x_limits, [-18040095.6961473, 18040095.6961473]) + assert_almost_equal(moll.y_limits, [-9020047.8480736, 9020047.8480736]) + + +def test_eccentric_globe(): + globe = ccrs.Globe(semimajor_axis=1000, semiminor_axis=500, + ellipse=None) + with pytest.warns(UserWarning, + match='does not handle elliptical globes.') as w: + moll = ccrs.Mollweide(globe=globe) + assert len(w) == 1 + + other_args = {'a=1000', 'b=500', 'lon_0=0'} + check_proj_params('moll', moll, other_args) + + # Limits are the same as spheres since ellipses are not supported. + assert_almost_equal(moll.x_limits, [-2828.4271247, 2828.4271247]) + assert_almost_equal(moll.y_limits, [-1414.2135624, 1414.2135624]) + + def test_offset(): crs = ccrs.Mollweide() crs_offset = ccrs.Mollweide(false_easting=1234, false_northing=-4321) other_args = {'a=6378137.0', 'lon_0=0', 'x_0=1234', 'y_0=-4321'} - check_proj4_params(crs_offset, other_args) + check_proj_params('moll', 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 @@ -58,7 +94,7 @@ def test_central_longitude(lon): moll = ccrs.Mollweide(central_longitude=lon) other_args = {'a=6378137.0', 'lon_0={}'.format(lon)} - check_proj4_params(moll, other_args) + check_proj_params('moll', moll, other_args) assert_almost_equal(np.array(moll.x_limits), [-18040095.6961473, 18040095.6961473], @@ -75,7 +111,7 @@ geodetic = moll.as_geodetic() other_args = {'a=0.7071067811865476', 'b=0.7071067811865476', 'lon_0=0'} - check_proj4_params(moll, other_args) + check_proj_params('moll', moll, other_args) assert_almost_equal(np.array(moll.x_limits), [-2, 2]) @@ -110,7 +146,7 @@ geodetic = moll.as_geodetic() other_args = {'a=1.0', 'b=1.0', 'lon_0=-90.0'} - check_proj4_params(moll, other_args) + check_proj_params('moll', moll, other_args) assert_almost_equal(np.array(moll.x_limits), [-2.8284271247461903, 2.8284271247461903], diff -Nru python-cartopy-0.17.0+dfsg/lib/cartopy/tests/crs/test_nearside_perspective.py python-cartopy-0.18.0+dfsg/lib/cartopy/tests/crs/test_nearside_perspective.py --- python-cartopy-0.17.0+dfsg/lib/cartopy/tests/crs/test_nearside_perspective.py 2018-11-17 07:25:32.000000000 +0000 +++ python-cartopy-0.18.0+dfsg/lib/cartopy/tests/crs/test_nearside_perspective.py 2020-05-03 08:12:47.000000000 +0000 @@ -23,17 +23,16 @@ from numpy.testing import assert_almost_equal -from cartopy.tests.crs.test_geostationary import check_proj4_params - import cartopy.crs as ccrs +from .helpers import check_proj_params def test_default(): geos = ccrs.NearsidePerspective() other_args = {'a=6378137.0', 'h=35785831', 'lat_0=0.0', 'lon_0=0.0', - 'x_0=0', 'y_0=0'} + 'units=m', 'x_0=0', 'y_0=0'} - check_proj4_params('nsper', geos, other_args) + check_proj_params('nsper', geos, other_args) assert_almost_equal(geos.boundary.bounds, (-5476336.098, -5476336.098, @@ -45,9 +44,9 @@ geos = ccrs.NearsidePerspective(false_easting=5000000, false_northing=-123000,) other_args = {'a=6378137.0', 'h=35785831', 'lat_0=0.0', 'lon_0=0.0', - 'x_0=5000000', 'y_0=-123000'} + 'units=m', 'x_0=5000000', 'y_0=-123000'} - check_proj4_params('nsper', geos, other_args) + check_proj_params('nsper', geos, other_args) assert_almost_equal(geos.boundary.bounds, (-476336.098, -5599336.098, @@ -59,8 +58,8 @@ # Check the effect of the added 'central_latitude' key. geos = ccrs.NearsidePerspective(central_latitude=53.7) other_args = {'a=6378137.0', 'h=35785831', 'lat_0=53.7', 'lon_0=0.0', - 'x_0=0', 'y_0=0'} - check_proj4_params('nsper', geos, other_args) + 'units=m', 'x_0=0', 'y_0=0'} + check_proj_params('nsper', geos, other_args) assert_almost_equal(geos.boundary.bounds, (-5476336.098, -5476336.098, diff -Nru python-cartopy-0.17.0+dfsg/lib/cartopy/tests/crs/test_orthographic.py python-cartopy-0.18.0+dfsg/lib/cartopy/tests/crs/test_orthographic.py --- python-cartopy-0.17.0+dfsg/lib/cartopy/tests/crs/test_orthographic.py 2018-11-17 07:25:32.000000000 +0000 +++ python-cartopy-0.18.0+dfsg/lib/cartopy/tests/crs/test_orthographic.py 2020-05-03 08:12:47.000000000 +0000 @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2018, Met Office +# (C) British Crown Copyright 2018 - 2019, Met Office # # This file is part of cartopy. # @@ -26,18 +26,13 @@ import pytest import cartopy.crs as ccrs - - -def check_proj4_params(crs, other_args): - expected = other_args | {'proj=ortho', 'no_defs'} - pro4_params = set(crs.proj4_init.lstrip('+').split(' +')) - assert expected == pro4_params +from .helpers import check_proj_params def test_default(): ortho = ccrs.Orthographic() - other_args = {'ellps=WGS84', 'lon_0=0.0', 'lat_0=0.0'} - check_proj4_params(ortho, other_args) + other_args = {'a=6378137.0', 'lon_0=0.0', 'lat_0=0.0'} + check_proj_params('ortho', ortho, other_args) # WGS84 radius * 0.99999 assert_almost_equal(np.array(ortho.x_limits), @@ -46,13 +41,58 @@ [-6378073.21863, 6378073.21863]) +def test_sphere_globe(): + globe = ccrs.Globe(semimajor_axis=1000, ellipse=None) + ortho = ccrs.Orthographic(globe=globe) + other_args = {'a=1000', 'lon_0=0.0', 'lat_0=0.0'} + check_proj_params('ortho', ortho, other_args) + + assert_almost_equal(ortho.x_limits, [-999.99, 999.99]) + assert_almost_equal(ortho.y_limits, [-999.99, 999.99]) + + +def test_ellipse_globe(): + globe = ccrs.Globe(ellipse='WGS84') + with pytest.warns(UserWarning, + match='does not handle elliptical globes.') as w: + ortho = ccrs.Orthographic(globe=globe) + assert len(w) == (2 + if (5, 0, 0) <= ccrs.PROJ4_VERSION < (5, 1, 0) + else 1) + + other_args = {'ellps=WGS84', 'lon_0=0.0', 'lat_0=0.0'} + check_proj_params('ortho', ortho, other_args) + + # Limits are the same as default since ellipses are not supported. + assert_almost_equal(ortho.x_limits, [-6378073.21863, 6378073.21863]) + assert_almost_equal(ortho.y_limits, [-6378073.21863, 6378073.21863]) + + +def test_eccentric_globe(): + globe = ccrs.Globe(semimajor_axis=1000, semiminor_axis=500, + ellipse=None) + with pytest.warns(UserWarning, + match='does not handle elliptical globes.') as w: + ortho = ccrs.Orthographic(globe=globe) + assert len(w) == (2 + if (5, 0, 0) <= ccrs.PROJ4_VERSION < (5, 1, 0) + else 1) + + other_args = {'a=1000', 'b=500', 'lon_0=0.0', 'lat_0=0.0'} + check_proj_params('ortho', ortho, other_args) + + # Limits are the same as spheres since ellipses are not supported. + assert_almost_equal(ortho.x_limits, [-999.99, 999.99]) + assert_almost_equal(ortho.y_limits, [-999.99, 999.99]) + + @pytest.mark.parametrize('lat', [-10, 0, 10]) @pytest.mark.parametrize('lon', [-10, 0, 10]) def test_central_params(lat, lon): ortho = ccrs.Orthographic(central_latitude=lat, central_longitude=lon) other_args = {'lat_0={}'.format(lat), 'lon_0={}'.format(lon), - 'ellps=WGS84'} - check_proj4_params(ortho, other_args) + 'a=6378137.0'} + check_proj_params('ortho', ortho, other_args) # WGS84 radius * 0.99999 assert_almost_equal(np.array(ortho.x_limits), @@ -69,7 +109,7 @@ geodetic = ortho.as_geodetic() other_args = {'a=1.0', 'b=1.0', 'lon_0=0.0', 'lat_0=0.0'} - check_proj4_params(ortho, other_args) + check_proj_params('ortho', ortho, other_args) assert_almost_equal(np.array(ortho.x_limits), [-0.99999, 0.99999]) @@ -122,7 +162,7 @@ geodetic = ortho.as_geodetic() other_args = {'a=1.0', 'b=1.0', 'lon_0=-100.0', 'lat_0=40.0'} - check_proj4_params(ortho, other_args) + check_proj_params('ortho', ortho, other_args) assert_almost_equal(np.array(ortho.x_limits), [-0.99999, 0.99999]) diff -Nru python-cartopy-0.17.0+dfsg/lib/cartopy/tests/crs/test_robinson.py python-cartopy-0.18.0+dfsg/lib/cartopy/tests/crs/test_robinson.py --- python-cartopy-0.17.0+dfsg/lib/cartopy/tests/crs/test_robinson.py 2018-11-17 07:25:32.000000000 +0000 +++ python-cartopy-0.18.0+dfsg/lib/cartopy/tests/crs/test_robinson.py 2020-05-03 08:12:47.000000000 +0000 @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2013 - 2018, Met Office +# (C) British Crown Copyright 2013 - 2020, Met Office # # This file is part of cartopy. # @@ -26,26 +26,26 @@ import pytest import cartopy.crs as ccrs +from .helpers import check_proj_params _CRS_PC = ccrs.PlateCarree() _CRS_ROB = ccrs.Robinson() # Increase tolerance if using older proj releases -_TOL = -1 if ccrs.PROJ4_VERSION < (4, 9) else 7 +if ccrs.PROJ4_VERSION >= (6, 3, 1): + _TRANSFORM_TOL = 7 +elif ccrs.PROJ4_VERSION >= (4, 9): + _TRANSFORM_TOL = 0 +else: + _TRANSFORM_TOL = -1 _LIMIT_TOL = -1 # if ccrs.PROJ4_VERSION < (5, 2, 0) else 7 -def check_proj_params(crs, other_args): - expected = other_args | {'proj=robin', 'no_defs'} - proj_params = set(crs.proj4_init.lstrip('+').split(' +')) - assert expected == proj_params - - def test_default(): robin = ccrs.Robinson() other_args = {'a=6378137.0', 'lon_0=0'} - check_proj_params(robin, other_args) + check_proj_params('robin', robin, other_args) assert_almost_equal(robin.x_limits, [-17005833.3305252, 17005833.3305252]) @@ -53,11 +53,55 @@ [-8625154.6651000, 8625154.6651000], _LIMIT_TOL) +def test_sphere_globe(): + globe = ccrs.Globe(semimajor_axis=1000, ellipse=None) + robin = ccrs.Robinson(globe=globe) + other_args = {'a=1000', 'lon_0=0'} + check_proj_params('robin', robin, other_args) + + assert_almost_equal(robin.x_limits, [-2666.2696851, 2666.2696851]) + assert_almost_equal(robin.y_limits, [-1352.3000000, 1352.3000000], + _LIMIT_TOL) + + +def test_ellipse_globe(): + globe = ccrs.Globe(ellipse='WGS84') + with pytest.warns(UserWarning, + match='does not handle elliptical globes.') as w: + robin = ccrs.Robinson(globe=globe) + assert len(w) == 1 + + other_args = {'ellps=WGS84', 'lon_0=0'} + check_proj_params('robin', robin, other_args) + + # Limits are the same as default since ellipses are not supported. + assert_almost_equal(robin.x_limits, [-17005833.3305252, 17005833.3305252]) + assert_almost_equal(robin.y_limits, [-8625154.6651000, 8625154.6651000], + _LIMIT_TOL) + + +def test_eccentric_globe(): + globe = ccrs.Globe(semimajor_axis=1000, semiminor_axis=500, + ellipse=None) + with pytest.warns(UserWarning, + match='does not handle elliptical globes.') as w: + robin = ccrs.Robinson(globe=globe) + assert len(w) == 1 + + other_args = {'a=1000', 'b=500', 'lon_0=0'} + check_proj_params('robin', robin, other_args) + + # Limits are the same as spheres since ellipses are not supported. + assert_almost_equal(robin.x_limits, [-2666.2696851, 2666.2696851]) + assert_almost_equal(robin.y_limits, [-1352.3000000, 1352.3000000], + _LIMIT_TOL) + + def test_offset(): crs = ccrs.Robinson() crs_offset = ccrs.Robinson(false_easting=1234, false_northing=-4321) other_args = {'a=6378137.0', 'lon_0=0', 'x_0=1234', 'y_0=-4321'} - check_proj_params(crs_offset, other_args) + check_proj_params('robin', 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 @@ -66,7 +110,7 @@ def test_central_longitude(lon): robin = ccrs.Robinson(central_longitude=lon) other_args = {'a=6378137.0', 'lon_0={}'.format(lon)} - check_proj_params(robin, other_args) + check_proj_params('robin', robin, other_args) assert_almost_equal(robin.x_limits, [-17005833.3305252, 17005833.3305252], @@ -78,14 +122,14 @@ def test_transform_point(): """ Mostly tests the workaround for a specific problem. - Problem report in: https://github.com/SciTools/cartopy/issues/23 + Problem report in: https://github.com/SciTools/cartopy/issues/232 Fix covered in: https://github.com/SciTools/cartopy/pull/277 """ # this way has always worked result = _CRS_ROB.transform_point(35.0, 70.0, _CRS_PC) - assert_array_almost_equal(result, (2376187.27182751, 7275317.81573085), - _TOL) + assert_array_almost_equal(result, (2376187.2182271, 7275318.1162980), + _TRANSFORM_TOL) # this always did something, but result has altered result = _CRS_ROB.transform_point(np.nan, 70.0, _CRS_PC) @@ -99,7 +143,7 @@ def test_transform_points(): """ Mostly tests the workaround for a specific problem. - Problem report in: https://github.com/SciTools/cartopy/issues/23 + Problem report in: https://github.com/SciTools/cartopy/issues/232 Fix covered in: https://github.com/SciTools/cartopy/pull/277 """ @@ -108,14 +152,16 @@ np.array([35.0]), np.array([70.0])) assert_array_almost_equal(result, - [[2376187.27182751, 7275317.81573085, 0]], _TOL) + [[2376187.2182271, 7275318.1162980, 0]], + _TRANSFORM_TOL) result = _CRS_ROB.transform_points(_CRS_PC, np.array([35.0]), np.array([70.0]), np.array([0.0])) assert_array_almost_equal(result, - [[2376187.27182751, 7275317.81573085, 0]], _TOL) + [[2376187.2182271, 7275318.1162980, 0]], + _TRANSFORM_TOL) # this always did something, but result has altered result = _CRS_ROB.transform_points(_CRS_PC, diff -Nru python-cartopy-0.17.0+dfsg/lib/cartopy/tests/crs/test_rotated_geodetic.py python-cartopy-0.18.0+dfsg/lib/cartopy/tests/crs/test_rotated_geodetic.py --- python-cartopy-0.17.0+dfsg/lib/cartopy/tests/crs/test_rotated_geodetic.py 2018-11-17 07:25:32.000000000 +0000 +++ python-cartopy-0.18.0+dfsg/lib/cartopy/tests/crs/test_rotated_geodetic.py 2020-05-03 08:12:47.000000000 +0000 @@ -22,16 +22,14 @@ from __future__ import (absolute_import, division, print_function) import cartopy.crs as ccrs +from .helpers import check_proj_params -def check_proj4_params(crs, other_args): - expected = other_args | {'proj=ob_tran', 'o_proj=latlon', - 'to_meter=0.0174532925199433', 'no_defs'} - pro4_params = set(crs.proj4_init.lstrip('+').split(' +')) - assert expected == pro4_params +common_other_args = {'o_proj=latlon', 'to_meter=0.0174532925199433'} def test_default(): geos = ccrs.RotatedPole(60, 50, 80) - other_args = {'ellps=WGS84', 'lon_0=240', 'o_lat_p=50', 'o_lon_p=80'} - check_proj4_params(geos, other_args) + other_args = common_other_args | {'ellps=WGS84', 'lon_0=240', 'o_lat_p=50', + 'o_lon_p=80'} + check_proj_params('ob_tran', geos, other_args) diff -Nru python-cartopy-0.17.0+dfsg/lib/cartopy/tests/crs/test_rotated_pole.py python-cartopy-0.18.0+dfsg/lib/cartopy/tests/crs/test_rotated_pole.py --- python-cartopy-0.17.0+dfsg/lib/cartopy/tests/crs/test_rotated_pole.py 2018-11-17 07:25:32.000000000 +0000 +++ python-cartopy-0.18.0+dfsg/lib/cartopy/tests/crs/test_rotated_pole.py 2020-05-03 08:12:47.000000000 +0000 @@ -22,17 +22,14 @@ from __future__ import (absolute_import, division, print_function) import cartopy.crs as ccrs +from .helpers import check_proj_params -def check_proj4_params(crs, other_args): - expected = other_args | {'proj=ob_tran', 'o_proj=latlon', - 'to_meter=0.0174532925199433', 'no_defs'} - pro4_params = set(crs.proj4_init.lstrip('+').split(' +')) - assert expected == pro4_params +common_other_args = {'o_proj=latlon', 'to_meter=0.0174532925199433'} def test_default(): geos = ccrs.RotatedGeodetic(30, 15, 27) other_args = {'datum=WGS84', 'ellps=WGS84', 'lon_0=210', 'o_lat_p=15', - 'o_lon_p=27'} - check_proj4_params(geos, other_args) + 'o_lon_p=27'} | common_other_args + check_proj_params('ob_tran', geos, other_args) diff -Nru python-cartopy-0.17.0+dfsg/lib/cartopy/tests/crs/test_sinusoidal.py python-cartopy-0.18.0+dfsg/lib/cartopy/tests/crs/test_sinusoidal.py --- python-cartopy-0.17.0+dfsg/lib/cartopy/tests/crs/test_sinusoidal.py 2018-11-17 07:25:32.000000000 +0000 +++ python-cartopy-0.18.0+dfsg/lib/cartopy/tests/crs/test_sinusoidal.py 2020-05-03 08:12:47.000000000 +0000 @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2016 - 2018, Met Office +# (C) British Crown Copyright 2016 - 2019, Met Office # # This file is part of cartopy. # @@ -22,19 +22,14 @@ import pytest import cartopy.crs as ccrs - - -def check_proj4_params(crs, other_args): - expected = other_args | {'proj=sinu', 'no_defs'} - pro4_params = set(crs.proj4_init.lstrip('+').split(' +')) - assert expected == pro4_params +from .helpers import check_proj_params class TestSinusoidal(object): def test_default(self): crs = ccrs.Sinusoidal() other_args = {'ellps=WGS84', 'lon_0=0.0', 'x_0=0.0', 'y_0=0.0'} - check_proj4_params(crs, other_args) + check_proj_params('sinu', crs, other_args) assert_almost_equal(np.array(crs.x_limits), [-20037508.3428, 20037508.3428], @@ -48,7 +43,7 @@ ellipse=None) crs = ccrs.Sinusoidal(globe=globe) other_args = {'a=1000', 'b=500', 'lon_0=0.0', 'x_0=0.0', 'y_0=0.0'} - check_proj4_params(crs, other_args) + check_proj_params('sinu', crs, other_args) assert_almost_equal(np.array(crs.x_limits), [-3141.59, 3141.59], decimal=2) @@ -60,7 +55,7 @@ crs_offset = ccrs.Sinusoidal(false_easting=1234, false_northing=-4321) other_args = {'ellps=WGS84', 'lon_0=0.0', 'x_0=1234', 'y_0=-4321'} - check_proj4_params(crs_offset, other_args) + check_proj_params('sinu', 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 @@ -69,7 +64,7 @@ crs = ccrs.Sinusoidal(central_longitude=lon) other_args = {'ellps=WGS84', 'lon_0={}'.format(lon), 'x_0=0.0', 'y_0=0.0'} - check_proj4_params(crs, other_args) + check_proj_params('sinu', crs, other_args) assert_almost_equal(np.array(crs.x_limits), [-20037508.3428, 20037508.3428], @@ -80,7 +75,7 @@ def test_MODIS(self): # Testpoints verified with MODLAND Tile Calculator - # http://landweb.nascom.nasa.gov/cgi-bin/developer/tilemap.cgi + # https://landweb.nascom.nasa.gov/cgi-bin/developer/tilemap.cgi # Settings: Sinusoidal, Global map coordinates, Forward mapping crs = ccrs.Sinusoidal.MODIS lons = np.array([-180, -50, 40, 180]) diff -Nru python-cartopy-0.17.0+dfsg/lib/cartopy/tests/crs/test_stereographic.py python-cartopy-0.18.0+dfsg/lib/cartopy/tests/crs/test_stereographic.py --- python-cartopy-0.17.0+dfsg/lib/cartopy/tests/crs/test_stereographic.py 2018-11-17 07:25:32.000000000 +0000 +++ python-cartopy-0.18.0+dfsg/lib/cartopy/tests/crs/test_stereographic.py 2020-05-03 08:12:47.000000000 +0000 @@ -21,12 +21,7 @@ from numpy.testing import assert_almost_equal import cartopy.crs as ccrs - - -def check_proj4_params(crs, other_args): - expected = other_args | {'proj=stere', 'no_defs'} - pro4_params = set(crs.proj4_init.lstrip('+').split(' +')) - assert expected == pro4_params +from .helpers import check_proj_params class TestStereographic(object): @@ -34,7 +29,7 @@ stereo = ccrs.Stereographic() other_args = {'ellps=WGS84', 'lat_0=0.0', 'lon_0=0.0', 'x_0=0.0', 'y_0=0.0'} - check_proj4_params(stereo, other_args) + check_proj_params('stere', stereo, other_args) assert_almost_equal(np.array(stereo.x_limits), [-5e7, 5e7], decimal=4) @@ -47,7 +42,7 @@ stereo = ccrs.Stereographic(globe=globe) other_args = {'a=1000', 'b=500', 'lat_0=0.0', 'lon_0=0.0', 'x_0=0.0', 'y_0=0.0'} - check_proj4_params(stereo, other_args) + check_proj_params('stere', stereo, other_args) # The limits in this test are sensible values, but are by no means # a "correct" answer - they mean that plotting the crs results in a @@ -66,7 +61,7 @@ stereo = ccrs.NorthPolarStereo(true_scale_latitude=30, globe=globe) other_args = {'ellps=sphere', 'lat_0=90', 'lon_0=0.0', 'lat_ts=30', 'x_0=0.0', 'y_0=0.0'} - check_proj4_params(stereo, other_args) + check_proj_params('stere', stereo, other_args) def test_scale_factor(self): # See #455 @@ -79,7 +74,7 @@ globe=globe) other_args = {'ellps=sphere', 'lat_0=90.0', 'lon_0=0.0', 'k_0=0.75', 'x_0=0.0', 'y_0=0.0'} - check_proj4_params(stereo, other_args) + check_proj_params('stere', stereo, other_args) # Now test projections lon, lat = 10, 10 @@ -101,6 +96,6 @@ other_args = {'ellps=WGS84', 'lat_0=0.0', 'lon_0=0.0', 'x_0=1234', 'y_0=-4321'} - check_proj4_params(stereo_offset, other_args) + check_proj_params('stere', stereo_offset, other_args) assert (tuple(np.array(stereo.x_limits) + 1234) == stereo_offset.x_limits) diff -Nru python-cartopy-0.17.0+dfsg/lib/cartopy/tests/crs/test_transverse_mercator.py python-cartopy-0.18.0+dfsg/lib/cartopy/tests/crs/test_transverse_mercator.py --- python-cartopy-0.17.0+dfsg/lib/cartopy/tests/crs/test_transverse_mercator.py 2018-11-17 07:25:32.000000000 +0000 +++ python-cartopy-0.18.0+dfsg/lib/cartopy/tests/crs/test_transverse_mercator.py 2020-05-03 08:12:47.000000000 +0000 @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2013 - 2018, Met Office +# (C) British Crown Copyright 2013 - 2019, Met Office # # This file is part of cartopy. # @@ -22,42 +22,48 @@ from __future__ import (absolute_import, division, print_function) import numpy as np +import pytest import cartopy.crs as ccrs +@pytest.mark.parametrize('approx', [True, False]) class TestTransverseMercator(object): def setup_class(self): self.point_a = (-3.474083, 50.727301) self.point_b = (0.5, 50.5) self.src_crs = ccrs.PlateCarree() - def test_default(self): - proj = ccrs.TransverseMercator() + def test_default(self, approx): + proj = ccrs.TransverseMercator(approx=approx) res = proj.transform_point(*self.point_a, src_crs=self.src_crs) - np.testing.assert_array_almost_equal(res, (-245269.53180633, - 5627508.74354959)) + np.testing.assert_array_almost_equal(res, + (-245269.53181, 5627508.74355), + decimal=5) res = proj.transform_point(*self.point_b, src_crs=self.src_crs) np.testing.assert_array_almost_equal(res, (35474.63566645, 5596583.41949901)) - def test_osgb_vals(self): + def test_osgb_vals(self, approx): proj = ccrs.TransverseMercator(central_longitude=-2, central_latitude=49, scale_factor=0.9996012717, false_easting=400000, false_northing=-100000, globe=ccrs.Globe(datum='OSGB36', - ellipse='airy')) + ellipse='airy'), + approx=approx) res = proj.transform_point(*self.point_a, src_crs=self.src_crs) - np.testing.assert_array_almost_equal(res, (295971.28667707, - 93064.27666368)) + np.testing.assert_array_almost_equal(res, (295971.28668, 93064.27666), + decimal=5) res = proj.transform_point(*self.point_b, src_crs=self.src_crs) - np.testing.assert_array_almost_equal(res, (577274.98380140, - 69740.49227181)) + np.testing.assert_array_almost_equal(res, (577274.98380, 69740.49227), + decimal=5) - def test_nan(self): - proj = ccrs.TransverseMercator() + def test_nan(self, approx): + if not approx: + pytest.xfail('Proj does not return NaN correctly with etmerc.') + proj = ccrs.TransverseMercator(approx=approx) res = proj.transform_point(0.0, float('nan'), src_crs=self.src_crs) assert np.all(np.isnan(res)) res = proj.transform_point(float('nan'), 0.0, src_crs=self.src_crs) @@ -71,17 +77,18 @@ self.src_crs = ccrs.PlateCarree() self.nan = float('nan') - def test_default(self): - proj = ccrs.OSGB() + @pytest.mark.parametrize('approx', [True, False]) + def test_default(self, approx): + proj = ccrs.OSGB(approx=approx) res = proj.transform_point(*self.point_a, src_crs=self.src_crs) - np.testing.assert_array_almost_equal(res, (295971.28667707, - 93064.27666368)) + np.testing.assert_array_almost_equal(res, (295971.28668, 93064.27666), + decimal=5) res = proj.transform_point(*self.point_b, src_crs=self.src_crs) - np.testing.assert_array_almost_equal(res, (577274.98380140, - 69740.49227181)) + np.testing.assert_array_almost_equal(res, (577274.98380, 69740.49227), + decimal=5) def test_nan(self): - proj = ccrs.OSGB() + proj = ccrs.OSGB(approx=True) res = proj.transform_point(0.0, float('nan'), src_crs=self.src_crs) assert np.all(np.isnan(res)) res = proj.transform_point(float('nan'), 0.0, src_crs=self.src_crs) @@ -94,15 +101,16 @@ self.src_crs = ccrs.PlateCarree() self.nan = float('nan') - def test_default(self): - proj = ccrs.OSNI() + @pytest.mark.parametrize('approx', [True, False]) + def test_default(self, approx): + proj = ccrs.OSNI(approx=approx) res = proj.transform_point(*self.point_a, src_crs=self.src_crs) np.testing.assert_array_almost_equal( res, (275614.26762651594, 386984.206429612), decimal=0 if ccrs.PROJ4_VERSION < (5, 0, 0) else 6) def test_nan(self): - proj = ccrs.OSNI() + proj = ccrs.OSNI(approx=True) res = proj.transform_point(0.0, float('nan'), src_crs=self.src_crs) assert np.all(np.isnan(res)) res = proj.transform_point(float('nan'), 0.0, src_crs=self.src_crs) diff -Nru python-cartopy-0.17.0+dfsg/lib/cartopy/tests/crs/test_utm.py python-cartopy-0.18.0+dfsg/lib/cartopy/tests/crs/test_utm.py --- python-cartopy-0.17.0+dfsg/lib/cartopy/tests/crs/test_utm.py 2018-11-17 07:25:32.000000000 +0000 +++ python-cartopy-0.18.0+dfsg/lib/cartopy/tests/crs/test_utm.py 2020-05-03 08:12:47.000000000 +0000 @@ -26,22 +26,17 @@ import pytest import cartopy.crs as ccrs - - -def check_proj4_params(crs, other_args): - expected = other_args | {'proj=utm', 'no_defs', 'units=m'} - pro4_params = set(crs.proj4_init.lstrip('+').split(' +')) - assert expected == pro4_params +from .helpers import check_proj_params @pytest.mark.parametrize('south', [False, True]) def test_default(south): zone = 1 # Limits are fixed, so don't bother checking other zones. utm = ccrs.UTM(zone, southern_hemisphere=south) - other_args = {'ellps=WGS84', 'zone={}'.format(zone)} + other_args = {'ellps=WGS84', 'units=m', 'zone={}'.format(zone)} if south: other_args |= {'south'} - check_proj4_params(utm, other_args) + check_proj_params('utm', utm, other_args) assert_almost_equal(np.array(utm.x_limits), [-250000, 1250000]) @@ -55,8 +50,8 @@ utm = ccrs.UTM(zone=18, globe=globe) geodetic = utm.as_geodetic() - other_args = {'ellps=clrk66', 'zone=18'} - check_proj4_params(utm, other_args) + other_args = {'ellps=clrk66', 'units=m', 'zone=18'} + check_proj_params('utm', utm, other_args) assert_almost_equal(np.array(utm.x_limits), [-250000, 1250000]) diff -Nru python-cartopy-0.17.0+dfsg/lib/cartopy/tests/feature/test_nightshade.py python-cartopy-0.18.0+dfsg/lib/cartopy/tests/feature/test_nightshade.py --- python-cartopy-0.17.0+dfsg/lib/cartopy/tests/feature/test_nightshade.py 2018-11-17 07:25:32.000000000 +0000 +++ python-cartopy-0.18.0+dfsg/lib/cartopy/tests/feature/test_nightshade.py 2020-05-03 08:12:47.000000000 +0000 @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2018, Met Office +# (C) British Crown Copyright 2018 - 2019, Met Office # # This file is part of cartopy. # @@ -52,7 +52,7 @@ (datetime(2018, 9, 29, 14, 0), -(2 + 32/60), -(32 + 25/60)), (datetime(1992, 2, 14, 0, 0), -(13 + 20/60), -(176 + 26/60)), (datetime(2030, 6, 21, 0, 0), (23 + 26/60), -(179 + 34/60)) - ]) +]) def test_solar_position(dt, true_lat, true_lon): lat, lon = _solar_position(dt) assert pytest.approx(true_lat, 0.1) == lat diff -Nru python-cartopy-0.17.0+dfsg/lib/cartopy/tests/io/test_ogc_clients.py python-cartopy-0.18.0+dfsg/lib/cartopy/tests/io/test_ogc_clients.py --- python-cartopy-0.17.0+dfsg/lib/cartopy/tests/io/test_ogc_clients.py 2018-11-17 07:25:32.000000000 +0000 +++ python-cartopy-0.18.0+dfsg/lib/cartopy/tests/io/test_ogc_clients.py 2020-05-03 08:12:47.000000000 +0000 @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2011 - 2018, Met Office +# (C) British Crown Copyright 2011 - 2019, Met Office # # This file is part of cartopy. # @@ -74,8 +74,8 @@ assert source.layers == self.layers def test_no_layers(self): - msg = 'One or more layers must be defined.' - with pytest.raises(ValueError, message=msg): + match = r'One or more layers must be defined\.' + with pytest.raises(ValueError, match=match): ogc.WMSRasterSource(self.URI, []) def test_extra_kwargs_empty(self): @@ -103,10 +103,10 @@ # Patch dict of known Proj->SRS mappings so that it does # not include any of the available SRSs from the WMS. with mock.patch.dict('cartopy.io.ogc_clients._CRS_TO_OGC_SRS', - {ccrs.OSNI(): 'EPSG:29901'}, + {ccrs.OSNI(approx=True): 'EPSG:29901'}, clear=True): msg = 'not available' - with pytest.raises(ValueError, message=msg): + with pytest.raises(ValueError, match=msg): source.validate_projection(ccrs.Miller()) def test_fetch_img(self): @@ -147,6 +147,7 @@ @pytest.mark.network @pytest.mark.skipif(not _OWSLIB_AVAILABLE, reason='OWSLib is unavailable.') +@pytest.mark.xfail(raises=KeyError, reason='OWSLib WMTS support is broken.') class TestWMTSRasterSource(object): URI = 'https://map1c.vis.earthdata.nasa.gov/wmts-geo/wmts.cgi' layer_name = 'VIIRS_CityLights_2012' @@ -174,8 +175,8 @@ def test_unsupported_projection(self): source = ogc.WMTSRasterSource(self.URI, self.layer_name) with mock.patch('cartopy.io.ogc_clients._URN_TO_CRS', {}): - msg = 'Unable to find tile matrix for projection.' - with pytest.raises(ValueError, message=msg): + match = r'Unable to find tile matrix for projection\.' + with pytest.raises(ValueError, match=match): source.validate_projection(ccrs.Miller()) def test_fetch_img(self): @@ -246,7 +247,7 @@ def test_unsupported_projection(self): source = ogc.WFSGeometrySource(self.URI, self.typename) msg = 'Geometries are only available in projection' - with pytest.raises(ValueError, message=msg): + with pytest.raises(ValueError, match=msg): source.fetch_geometries(ccrs.PlateCarree(), [-180, 180, -90, 90]) def test_fetch_geometries(self): diff -Nru python-cartopy-0.17.0+dfsg/lib/cartopy/tests/io/test_srtm.py python-cartopy-0.18.0+dfsg/lib/cartopy/tests/io/test_srtm.py --- python-cartopy-0.17.0+dfsg/lib/cartopy/tests/io/test_srtm.py 2018-11-17 07:25:32.000000000 +0000 +++ python-cartopy-0.18.0+dfsg/lib/cartopy/tests/io/test_srtm.py 2020-05-03 08:12:47.000000000 +0000 @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2011 - 2018, Met Office +# (C) British Crown Copyright 2011 - 2019, Met Office # # This file is part of cartopy. # @@ -76,7 +76,7 @@ 'srtm1', ]) def test_srtm_retrieve(self, Source, read_SRTM, max_, min_, pt, - download_to_temp): + download_to_temp): # noqa: F811 # test that the download mechanism for SRTM works with warnings.catch_warnings(record=True) as w: r = Source().srtm_fname(-4, 50) @@ -116,8 +116,8 @@ class TestSRTMSource__single_tile(object): def test_out_of_range(self, Source): source = Source() - msg = 'No srtm tile found for those coordinates.' - with pytest.raises(ValueError, message=msg): + match = r'No srtm tile found for those coordinates\.' + with pytest.raises(ValueError, match=match): source.single_tile(-25, 50) def test_in_range(self, Source): Binary files /tmp/tmpxFyELb/7OZ0Yt5HUS/python-cartopy-0.17.0+dfsg/lib/cartopy/tests/mpl/baseline_images/mpl/test_crs/lambert_conformal_south.png and /tmp/tmpxFyELb/YczkQEvnSU/python-cartopy-0.18.0+dfsg/lib/cartopy/tests/mpl/baseline_images/mpl/test_crs/lambert_conformal_south.png differ Binary files /tmp/tmpxFyELb/7OZ0Yt5HUS/python-cartopy-0.17.0+dfsg/lib/cartopy/tests/mpl/baseline_images/mpl/test_examples/contour_label.png and /tmp/tmpxFyELb/YczkQEvnSU/python-cartopy-0.18.0+dfsg/lib/cartopy/tests/mpl/baseline_images/mpl/test_examples/contour_label.png differ Binary files /tmp/tmpxFyELb/7OZ0Yt5HUS/python-cartopy-0.17.0+dfsg/lib/cartopy/tests/mpl/baseline_images/mpl/test_examples/global_map.png and /tmp/tmpxFyELb/YczkQEvnSU/python-cartopy-0.18.0+dfsg/lib/cartopy/tests/mpl/baseline_images/mpl/test_examples/global_map.png differ Binary files /tmp/tmpxFyELb/7OZ0Yt5HUS/python-cartopy-0.17.0+dfsg/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/gridliner1.png and /tmp/tmpxFyELb/YczkQEvnSU/python-cartopy-0.18.0+dfsg/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/gridliner1.png differ Binary files /tmp/tmpxFyELb/7OZ0Yt5HUS/python-cartopy-0.17.0+dfsg/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/gridliner_labels_1.5.png and /tmp/tmpxFyELb/YczkQEvnSU/python-cartopy-0.18.0+dfsg/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/gridliner_labels_1.5.png differ Binary files /tmp/tmpxFyELb/7OZ0Yt5HUS/python-cartopy-0.17.0+dfsg/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/gridliner_labels_inline_1.5.png and /tmp/tmpxFyELb/YczkQEvnSU/python-cartopy-0.18.0+dfsg/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/gridliner_labels_inline_1.5.png differ Binary files /tmp/tmpxFyELb/7OZ0Yt5HUS/python-cartopy-0.17.0+dfsg/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/gridliner_labels_inline.png and /tmp/tmpxFyELb/YczkQEvnSU/python-cartopy-0.18.0+dfsg/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/gridliner_labels_inline.png differ Binary files /tmp/tmpxFyELb/7OZ0Yt5HUS/python-cartopy-0.17.0+dfsg/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/gridliner_labels_inline_usa_1.5.png and /tmp/tmpxFyELb/YczkQEvnSU/python-cartopy-0.18.0+dfsg/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/gridliner_labels_inline_usa_1.5.png differ Binary files /tmp/tmpxFyELb/7OZ0Yt5HUS/python-cartopy-0.17.0+dfsg/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/gridliner_labels_inline_usa.png and /tmp/tmpxFyELb/YczkQEvnSU/python-cartopy-0.18.0+dfsg/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/gridliner_labels_inline_usa.png differ Binary files /tmp/tmpxFyELb/7OZ0Yt5HUS/python-cartopy-0.17.0+dfsg/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/gridliner_labels.png and /tmp/tmpxFyELb/YczkQEvnSU/python-cartopy-0.18.0+dfsg/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/gridliner_labels.png differ Binary files /tmp/tmpxFyELb/7OZ0Yt5HUS/python-cartopy-0.17.0+dfsg/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/global_contour_wrap.png and /tmp/tmpxFyELb/YczkQEvnSU/python-cartopy-0.18.0+dfsg/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/global_contour_wrap.png differ Binary files /tmp/tmpxFyELb/7OZ0Yt5HUS/python-cartopy-0.17.0+dfsg/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/global_map.png and /tmp/tmpxFyELb/YczkQEvnSU/python-cartopy-0.18.0+dfsg/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/global_map.png differ Binary files /tmp/tmpxFyELb/7OZ0Yt5HUS/python-cartopy-0.17.0+dfsg/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/pcolormesh_goode_wrap.png and /tmp/tmpxFyELb/YczkQEvnSU/python-cartopy-0.18.0+dfsg/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/pcolormesh_goode_wrap.png differ Binary files /tmp/tmpxFyELb/7OZ0Yt5HUS/python-cartopy-0.17.0+dfsg/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/streamplot_mpl_3.0.0.png and /tmp/tmpxFyELb/YczkQEvnSU/python-cartopy-0.18.0+dfsg/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/streamplot_mpl_3.0.0.png differ Binary files /tmp/tmpxFyELb/7OZ0Yt5HUS/python-cartopy-0.17.0+dfsg/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/streamplot_mpl_3.2.0.png and /tmp/tmpxFyELb/YczkQEvnSU/python-cartopy-0.18.0+dfsg/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/streamplot_mpl_3.2.0.png differ Binary files /tmp/tmpxFyELb/7OZ0Yt5HUS/python-cartopy-0.17.0+dfsg/lib/cartopy/tests/mpl/baseline_images/mpl/test_nightshade/nightshade_platecarree.png and /tmp/tmpxFyELb/YczkQEvnSU/python-cartopy-0.18.0+dfsg/lib/cartopy/tests/mpl/baseline_images/mpl/test_nightshade/nightshade_platecarree.png differ diff -Nru python-cartopy-0.17.0+dfsg/lib/cartopy/tests/mpl/__init__.py python-cartopy-0.18.0+dfsg/lib/cartopy/tests/mpl/__init__.py --- python-cartopy-0.17.0+dfsg/lib/cartopy/tests/mpl/__init__.py 2018-11-17 07:25:32.000000000 +0000 +++ python-cartopy-0.18.0+dfsg/lib/cartopy/tests/mpl/__init__.py 2020-05-03 08:12:47.000000000 +0000 @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2011 - 2018, Met Office +# (C) British Crown Copyright 2011 - 2019, Met Office # # This file is part of cartopy. # @@ -31,7 +31,6 @@ import matplotlib.patches as mpatches from matplotlib.testing import setup as mpl_setup import matplotlib.testing.compare as mcompare -import matplotlib._pylab_helpers as pyplot_helpers MPL_VERSION = distutils.version.LooseVersion(mpl.__version__) @@ -211,12 +210,12 @@ plt.switch_backend('agg') mpl_setup() - if pyplot_helpers.Gcf.figs: + if plt.get_fignums(): warnings.warn('Figures existed before running the %s %s test.' ' All figures should be closed after they run. ' 'They will be closed automatically now.' % (mod_name, test_name)) - pyplot_helpers.Gcf.destroy_all() + plt.close('all') if MPL_VERSION >= '2': style_context = mpl.style.context @@ -226,16 +225,18 @@ yield with style_context(self.style): + if MPL_VERSION >= '3.2.0': + mpl.rcParams['text.kerning_factor'] = 6 + r = test_func(*args, **kwargs) - fig_managers = pyplot_helpers.Gcf._activeQue - figures = [manager.canvas.figure for manager in fig_managers] + figures = [plt.figure(num) for num in plt.get_fignums()] try: self.run_figure_comparisons(figures, test_name=mod_name) finally: for figure in figures: - pyplot_helpers.Gcf.destroy_fig(figure) + plt.close(figure) plt.switch_backend(orig_backend) return r diff -Nru python-cartopy-0.17.0+dfsg/lib/cartopy/tests/mpl/test_caching.py python-cartopy-0.18.0+dfsg/lib/cartopy/tests/mpl/test_caching.py --- python-cartopy-0.17.0+dfsg/lib/cartopy/tests/mpl/test_caching.py 2018-11-17 07:25:32.000000000 +0000 +++ python-cartopy-0.18.0+dfsg/lib/cartopy/tests/mpl/test_caching.py 2020-05-03 08:12:47.000000000 +0000 @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2011 - 2018, Met Office +# (C) British Crown Copyright 2011 - 2020, Met Office # # This file is part of cartopy. # @@ -23,7 +23,7 @@ try: from owslib.wmts import WebMapTileService -except ImportError as e: +except ImportError: WebMapTileService = None import matplotlib.pyplot as plt import pytest @@ -151,8 +151,9 @@ def test_contourf_transform_path_counting(): + fig = plt.figure() ax = plt.axes(projection=ccrs.Robinson()) - ax.figure.canvas.draw() + fig.canvas.draw() # Capture the size of the cache before our test. gc.collect() @@ -189,6 +190,7 @@ @pytest.mark.network @pytest.mark.skipif(not _OWSLIB_AVAILABLE, reason='OWSLib is unavailable.') +@pytest.mark.xfail(raises=KeyError, reason='OWSLib WMTS support is broken.') def test_wmts_tile_caching(): image_cache = WMTSRasterSource._shared_image_cache image_cache.clear() diff -Nru python-cartopy-0.17.0+dfsg/lib/cartopy/tests/mpl/test_contour.py python-cartopy-0.18.0+dfsg/lib/cartopy/tests/mpl/test_contour.py --- python-cartopy-0.17.0+dfsg/lib/cartopy/tests/mpl/test_contour.py 2018-11-17 07:25:32.000000000 +0000 +++ python-cartopy-0.18.0+dfsg/lib/cartopy/tests/mpl/test_contour.py 2020-05-03 08:12:47.000000000 +0000 @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2016 - 2018, Met Office +# (C) British Crown Copyright 2016 - 2020, Met Office # # This file is part of cartopy. # @@ -41,6 +41,11 @@ assert_array_almost_equal(ax.get_extent(), np.array([x[0], x[-1], y[0], y[-1]])) + # Levels that don't include data should not fail. + plt.figure() + ax = plt.axes(projection=proj_lcc) + ax.contourf(x, y, data, levels=np.max(data) + np.arange(1, 3)) + @cleanup def test_contour_linear_ring(): diff -Nru python-cartopy-0.17.0+dfsg/lib/cartopy/tests/mpl/test_crs.py python-cartopy-0.18.0+dfsg/lib/cartopy/tests/mpl/test_crs.py --- python-cartopy-0.17.0+dfsg/lib/cartopy/tests/mpl/test_crs.py 2018-11-17 07:25:32.000000000 +0000 +++ python-cartopy-0.18.0+dfsg/lib/cartopy/tests/mpl/test_crs.py 2020-05-03 08:12:47.000000000 +0000 @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2013 - 2018, Met Office +# (C) British Crown Copyright 2013 - 2020, Met Office # # This file is part of cartopy. # @@ -18,6 +18,7 @@ from __future__ import (absolute_import, division, print_function) import matplotlib.pyplot as plt +from matplotlib.testing.decorators import cleanup import pytest import cartopy.crs as ccrs @@ -27,7 +28,7 @@ @pytest.mark.natural_earth @ImageTesting(['lambert_conformal_south']) def test_lambert_south(): - # Reference image: http://www.icsm.gov.au/mapping/map_projections.html + # Reference image: https://www.icsm.gov.au/mapping/map_projections.html crs = ccrs.LambertConformal(central_longitude=140, cutoff=65, standard_parallels=(-30, -60)) ax = plt.axes(projection=crs) @@ -44,3 +45,14 @@ ax = plt.axes(projection=crs) ax.coastlines() ax.gridlines() + + +@pytest.mark.natural_earth +@cleanup +def test_repr_html(): + pc = ccrs.PlateCarree() + html = pc._repr_html_() + + assert html is not None + assert '<cartopy.crs.PlateCarree object at ' in html diff -Nru python-cartopy-0.17.0+dfsg/lib/cartopy/tests/mpl/test_examples.py python-cartopy-0.18.0+dfsg/lib/cartopy/tests/mpl/test_examples.py --- python-cartopy-0.17.0+dfsg/lib/cartopy/tests/mpl/test_examples.py 2018-11-17 07:25:32.000000000 +0000 +++ python-cartopy-0.18.0+dfsg/lib/cartopy/tests/mpl/test_examples.py 2020-05-03 08:12:47.000000000 +0000 @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2011 - 2018, Met Office +# (C) British Crown Copyright 2011 - 2020, Met Office # # This file is part of cartopy. # @@ -43,7 +43,24 @@ @pytest.mark.natural_earth @ExampleImageTesting(['global_map'], - tolerance=4 if MPL_VERSION < '2' else 0) + tolerance=4.5 if MPL_VERSION < '2' else 0.5) def test_global_map(): - import cartopy.examples.global_map as c - c.main() + import cartopy.examples.global_map as example + example.main() + + +if MPL_VERSION < '2': + contour_labels_tolerance = 7.5 +elif MPL_VERSION <= '2.0.2': + contour_labels_tolerance = 1.24 +elif MPL_VERSION <= '2.1.2': + contour_labels_tolerance = 0.63 +else: + contour_labels_tolerance = 0 + + +@pytest.mark.natural_earth +@ExampleImageTesting(['contour_label'], tolerance=contour_labels_tolerance) +def test_contour_label(): + import cartopy.examples.contour_labels as example + example.main() diff -Nru python-cartopy-0.17.0+dfsg/lib/cartopy/tests/mpl/test_feature_artist.py python-cartopy-0.18.0+dfsg/lib/cartopy/tests/mpl/test_feature_artist.py --- python-cartopy-0.17.0+dfsg/lib/cartopy/tests/mpl/test_feature_artist.py 2018-11-17 07:25:32.000000000 +0000 +++ python-cartopy-0.18.0+dfsg/lib/cartopy/tests/mpl/test_feature_artist.py 2020-05-03 08:12:47.000000000 +0000 @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2018, Met Office +# (C) British Crown Copyright 2018 - 2019, Met Office # # This file is part of cartopy. # @@ -30,6 +30,7 @@ import cartopy.mpl.geoaxes as geoaxes from cartopy.feature import ShapelyFeature from cartopy.mpl.feature_artist import FeatureArtist, _freeze, _GeomKey +from cartopy.mpl import style @pytest.mark.parametrize("source, expected", [ @@ -108,6 +109,7 @@ geoms = list(feature.geometries()) style1 = {'facecolor': 'blue', 'edgecolor': 'white'} style2 = {'color': 'black', 'linewidth': 1} + style2_finalized = style.finalize(style.merge(style2)) def styler(geom): if geom == geoms[0]: @@ -126,7 +128,7 @@ calls = [{'paths': (cached_paths(geoms[0], prj_crs), ), 'style': dict(linewidth=2, **style1)}, {'paths': (cached_paths(geoms[1], prj_crs), ), - 'style': style2}] + 'style': style2_finalized}] assert path_collection_cls.call_count == 2 for expected_call, (actual_args, actual_kwargs) in \ diff -Nru python-cartopy-0.17.0+dfsg/lib/cartopy/tests/mpl/test_features.py python-cartopy-0.18.0+dfsg/lib/cartopy/tests/mpl/test_features.py --- python-cartopy-0.17.0+dfsg/lib/cartopy/tests/mpl/test_features.py 2018-11-17 07:25:32.000000000 +0000 +++ python-cartopy-0.18.0+dfsg/lib/cartopy/tests/mpl/test_features.py 2020-05-03 08:12:47.000000000 +0000 @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2011 - 2018, Met Office +# (C) British Crown Copyright 2011 - 2020, Met Office # # This file is part of cartopy. # @@ -54,7 +54,7 @@ @ImageTesting(['gshhs_coastlines'], - tolerance=1.7 if MPL_VERSION < '2' else 0) + tolerance=3.3 if MPL_VERSION < '2' else 0.95) def test_gshhs(): ax = plt.axes(projection=ccrs.Mollweide()) ax.set_extent([138, 142, 32, 42], ccrs.Geodetic()) @@ -71,7 +71,7 @@ @pytest.mark.skipif(not _OWSLIB_AVAILABLE, reason='OWSLib is unavailable.') @ImageTesting(['wfs']) def test_wfs(): - ax = plt.axes(projection=ccrs.OSGB()) + ax = plt.axes(projection=ccrs.OSGB(approx=True)) url = 'https://nsidc.org/cgi-bin/atlas_south?service=WFS' typename = 'land_excluding_antarctica' feature = cfeature.WFSFeature(url, typename, diff -Nru python-cartopy-0.17.0+dfsg/lib/cartopy/tests/mpl/test_gridliner.py python-cartopy-0.18.0+dfsg/lib/cartopy/tests/mpl/test_gridliner.py --- python-cartopy-0.17.0+dfsg/lib/cartopy/tests/mpl/test_gridliner.py 2018-11-17 07:25:32.000000000 +0000 +++ python-cartopy-0.18.0+dfsg/lib/cartopy/tests/mpl/test_gridliner.py 2020-05-03 08:12:47.000000000 +0000 @@ -1,40 +1,62 @@ -# (C) British Crown Copyright 2011 - 2018, Met Office +# Copyright Cartopy Contributors # -# 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 . +# 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. from __future__ import (absolute_import, division, print_function) -from matplotlib.backends.backend_agg import FigureCanvasAgg import matplotlib.pyplot as plt import matplotlib.ticker as mticker -try: - from unittest import mock -except ImportError: - import mock import pytest import cartopy.crs as ccrs from cartopy.mpl.geoaxes import GeoAxes -from cartopy.mpl.gridliner import LATITUDE_FORMATTER, LONGITUDE_FORMATTER +from cartopy.mpl.ticker import LongitudeLocator, LongitudeFormatter +from cartopy.mpl.gridliner import ( + LATITUDE_FORMATTER, LONGITUDE_FORMATTER, + classic_locator, classic_formatter) + from cartopy.tests.mpl import MPL_VERSION, ImageTesting +TEST_PROJS = [ + ccrs.PlateCarree, + ccrs.AlbersEqualArea, + ccrs.AzimuthalEquidistant, + ccrs.LambertConformal, + ccrs.LambertCylindrical, + ccrs.Mercator, + ccrs.Miller, + ccrs.Mollweide, + ccrs.Orthographic, + ccrs.Robinson, + ccrs.Sinusoidal, + ccrs.Stereographic, + ccrs.InterruptedGoodeHomolosine, + (ccrs.RotatedPole, + dict(pole_longitude=180.0, + pole_latitude=36.0, + central_rotated_longitude=-106.0, + globe=ccrs.Globe(semimajor_axis=6370000, + semiminor_axis=6370000))), + (ccrs.OSGB, dict(approx=False)), + ccrs.EuroPP, + ccrs.Geostationary, + ccrs.NearsidePerspective, + ccrs.Gnomonic, + ccrs.LambertAzimuthalEqualArea, + ccrs.NorthPolarStereo, + (ccrs.OSNI, dict(approx=False)), + ccrs.SouthPolarStereo, +] + + @pytest.mark.natural_earth -@ImageTesting(['gridliner1']) +@ImageTesting(['gridliner1'], + # Robinson projection is slightly better in Proj 6+. + tolerance=0.7 if ccrs.PROJ4_VERSION >= (6, 0, 0) else 0.5) def test_gridliner(): ny, nx = 2, 4 @@ -42,49 +64,49 @@ ax = plt.subplot(nx, ny, 1, projection=ccrs.PlateCarree()) ax.set_global() - ax.coastlines() - ax.gridlines() + ax.coastlines(resolution="110m") + ax.gridlines(linestyle=':') - ax = plt.subplot(nx, ny, 2, projection=ccrs.OSGB()) + ax = plt.subplot(nx, ny, 2, projection=ccrs.OSGB(approx=False)) ax.set_global() - ax.coastlines() - ax.gridlines() + ax.coastlines(resolution="110m") + ax.gridlines(linestyle=':') - ax = plt.subplot(nx, ny, 3, projection=ccrs.OSGB()) + ax = plt.subplot(nx, ny, 3, projection=ccrs.OSGB(approx=False)) ax.set_global() - ax.coastlines() + ax.coastlines(resolution="110m") ax.gridlines(ccrs.PlateCarree(), color='blue', linestyle='-') - ax.gridlines(ccrs.OSGB()) + ax.gridlines(ccrs.OSGB(approx=False), linestyle=':') ax = plt.subplot(nx, ny, 4, projection=ccrs.PlateCarree()) ax.set_global() - ax.coastlines() + ax.coastlines(resolution="110m") ax.gridlines(ccrs.NorthPolarStereo(), alpha=0.5, linewidth=1.5, linestyle='-') ax = plt.subplot(nx, ny, 5, projection=ccrs.PlateCarree()) ax.set_global() - ax.coastlines() - osgb = ccrs.OSGB() + ax.coastlines(resolution="110m") + osgb = ccrs.OSGB(approx=False) ax.set_extent(tuple(osgb.x_limits) + tuple(osgb.y_limits), crs=osgb) - ax.gridlines(osgb) + ax.gridlines(osgb, linestyle=':') ax = plt.subplot(nx, ny, 6, projection=ccrs.NorthPolarStereo()) ax.set_global() - ax.coastlines() + ax.coastlines(resolution="110m") ax.gridlines(alpha=0.5, linewidth=1.5, linestyle='-') ax = plt.subplot(nx, ny, 7, projection=ccrs.NorthPolarStereo()) ax.set_global() - ax.coastlines() - osgb = ccrs.OSGB() + ax.coastlines(resolution="110m") + osgb = ccrs.OSGB(approx=False) ax.set_extent(tuple(osgb.x_limits) + tuple(osgb.y_limits), crs=osgb) - ax.gridlines(osgb) + ax.gridlines(osgb, linestyle=':') ax = plt.subplot(nx, ny, 8, projection=ccrs.Robinson(central_longitude=135)) ax.set_global() - ax.coastlines() + ax.coastlines(resolution="110m") ax.gridlines(ccrs.PlateCarree(), alpha=0.5, linewidth=1.5, linestyle='-') delta = 1.5e-2 @@ -93,68 +115,92 @@ def test_gridliner_specified_lines(): - xs = [0, 60, 120, 180, 240, 360] - ys = [-90, -60, -30, 0, 30, 60, 90] - ax = mock.Mock(_gridliners=[], spec=GeoAxes) - gl = GeoAxes.gridlines(ax, xlocs=xs, ylocs=ys) + meridians = [0, 60, 120, 180, 240, 360] + parallels = [-90, -60, -30, 0, 30, 60, 90] + + ax = plt.subplot(1, 1, 1, projection=ccrs.PlateCarree()) + gl = GeoAxes.gridlines(ax, xlocs=meridians, ylocs=parallels) assert isinstance(gl.xlocator, mticker.FixedLocator) assert isinstance(gl.ylocator, mticker.FixedLocator) - assert gl.xlocator.tick_values(None, None).tolist() == xs - assert gl.ylocator.tick_values(None, None).tolist() == ys + assert gl.xlocator.tick_values(None, None).tolist() == meridians + assert gl.ylocator.tick_values(None, None).tolist() == parallels -# The tolerance on this test is particularly high because of the high number +# The tolerance on these tests are particularly high because of the high number # of text objects. A new testing strategy is needed for this kind of test. +grid_label_tol = grid_label_inline_tol = grid_label_inline_usa_tol = 0.5 if MPL_VERSION >= '2.0': grid_label_image = 'gridliner_labels' + if ccrs.PROJ4_VERSION < (4, 9, 3): + # A 0-longitude label is missing on older Proj versions. + grid_label_tol = 1.8 + grid_label_inline_image = 'gridliner_labels_inline' + grid_label_inline_usa_image = 'gridliner_labels_inline_usa' + if ccrs.PROJ4_VERSION == (4, 9, 1): + # AzimuthalEquidistant was previously broken. + grid_label_inline_tol = 7.9 + grid_label_inline_usa_tol = 7.7 + elif ccrs.PROJ4_VERSION < (5, 0, 0): + # Stereographic was previously broken. + grid_label_inline_tol = 6.4 + grid_label_inline_usa_tol = 4.0 else: grid_label_image = 'gridliner_labels_1.5' + grid_label_tol = 1.8 + grid_label_inline_image = 'gridliner_labels_inline_1.5' + grid_label_inline_usa_image = 'gridliner_labels_inline_usa_1.5' + if ccrs.PROJ4_VERSION >= (5, 0, 0): + # Stereographic was fixed, but test image was not updated. + grid_label_inline_tol = 7.9 + grid_label_inline_usa_tol = 7.9 + elif ccrs.PROJ4_VERSION >= (4, 9, 2): + # AzimuthalEquidistant was fixed, but test image was not updated. + grid_label_inline_tol = 5.4 + grid_label_inline_usa_tol = 7.2 +if (5, 0, 0) <= ccrs.PROJ4_VERSION < (5, 1, 0): + # Several projections are broken in these versions, so not plotted. + grid_label_inline_tol += 5.1 + grid_label_inline_usa_tol += 5.5 +elif (6, 0, 0) <= ccrs.PROJ4_VERSION: + # Better Robinson projection causes some text movement. + grid_label_inline_tol += 1.2 @pytest.mark.natural_earth -@ImageTesting([grid_label_image]) +@ImageTesting([grid_label_image], tolerance=grid_label_tol) def test_grid_labels(): - plt.figure(figsize=(8, 10)) + fig = plt.figure(figsize=(10, 10)) crs_pc = ccrs.PlateCarree() crs_merc = ccrs.Mercator() - crs_osgb = ccrs.OSGB() - ax = plt.subplot(3, 2, 1, projection=crs_pc) - ax.coastlines() + ax = fig.add_subplot(3, 2, 1, projection=crs_pc) + ax.coastlines(resolution="110m") ax.gridlines(draw_labels=True) # Check that adding labels to Mercator gridlines gives an error. # (Currently can only label PlateCarree gridlines.) - ax = plt.subplot(3, 2, 2, - projection=ccrs.PlateCarree(central_longitude=180)) - ax.coastlines() - with pytest.raises(TypeError): - ax.gridlines(crs=crs_merc, draw_labels=True) + ax = fig.add_subplot(3, 2, 2, + projection=ccrs.PlateCarree(central_longitude=180)) + ax.coastlines(resolution="110m") ax.set_title('Known bug') gl = ax.gridlines(crs=crs_pc, draw_labels=True) - gl.xlabels_top = False - gl.ylabels_left = False + gl.top_labels = False + gl.left_labels = False gl.xlines = False - ax = plt.subplot(3, 2, 3, projection=crs_merc) - ax.coastlines() - ax.gridlines(draw_labels=True) - - # Check that labelling the gridlines on an OSGB plot gives an error. - # (Currently can only draw these on PlateCarree or Mercator plots.) - ax = plt.subplot(3, 2, 4, projection=crs_osgb) - ax.coastlines() - with pytest.raises(TypeError): - ax.gridlines(draw_labels=True) + ax = fig.add_subplot(3, 2, 3, projection=crs_merc) + ax.coastlines(resolution="110m") + gl = ax.gridlines(draw_labels=True) + gl.xlabel_style = gl.ylabel_style = {'size': 9} ax = plt.subplot(3, 2, 4, projection=crs_pc) - ax.coastlines() + ax.coastlines(resolution="110m") gl = ax.gridlines( - crs=crs_pc, linewidth=2, color='gray', alpha=0.5, linestyle='--') - gl.xlabels_bottom = True - gl.ylabels_right = True + crs=crs_pc, linewidth=2, color='gray', alpha=0.5, linestyle=':') + gl.bottom_labels = True + gl.right_labels = True gl.xlines = False gl.xlocator = mticker.FixedLocator([-180, -45, 45, 180]) gl.xformatter = LONGITUDE_FORMATTER @@ -166,22 +212,112 @@ # trigger a draw at this point and check the appropriate artists are # populated on the gridliner instance - FigureCanvasAgg(plt.gcf()).draw() + fig.canvas.draw() - assert len(gl.xlabel_artists) == 4 - assert len(gl.ylabel_artists) == 5 - assert len(gl.ylabel_artists) == 5 + assert len(gl.bottom_label_artists) == 4 + assert len(gl.top_label_artists) == 0 + assert len(gl.left_label_artists) == 0 + assert len(gl.right_label_artists) != 0 assert len(gl.xline_artists) == 0 - ax = plt.subplot(3, 2, 5, projection=crs_pc) + ax = fig.add_subplot(3, 2, 5, projection=crs_pc) ax.set_extent([-20, 10.0, 45.0, 70.0]) - ax.coastlines() + ax.coastlines(resolution="110m") ax.gridlines(draw_labels=True) - ax = plt.subplot(3, 2, 6, projection=crs_merc) + ax = fig.add_subplot(3, 2, 6, projection=crs_merc) ax.set_extent([-20, 10.0, 45.0, 70.0], crs=crs_pc) - ax.coastlines() - ax.gridlines(draw_labels=True) + ax.coastlines(resolution="110m") + gl = ax.gridlines(draw_labels=True) + gl.rotate_labels = False + gl.xlabel_style = gl.ylabel_style = {'size': 9} # Increase margins between plots to stop them bumping into one another. plt.subplots_adjust(wspace=0.25, hspace=0.25) + + +@pytest.mark.natural_earth +@ImageTesting([grid_label_inline_image], tolerance=grid_label_inline_tol) +def test_grid_labels_inline(): + plt.figure(figsize=(35, 35)) + for i, proj in enumerate(TEST_PROJS, 1): + if isinstance(proj, tuple): + proj, kwargs = proj + else: + kwargs = {} + ax = plt.subplot(7, 4, i, projection=proj(**kwargs)) + if (ccrs.PROJ4_VERSION[:2] == (5, 0) and + proj in (ccrs.Orthographic, ccrs.AlbersEqualArea, + ccrs.Geostationary, ccrs.NearsidePerspective)): + # Above projections are broken, so skip labels. + # Add gridlines anyway to minimize image differences. + ax.gridlines() + else: + ax.gridlines(draw_labels=True, auto_inline=True) + ax.coastlines(resolution="110m") + ax.set_title(proj, y=1.075) + plt.subplots_adjust(wspace=0.35, hspace=0.35) + + +@pytest.mark.natural_earth +@ImageTesting([grid_label_inline_usa_image], + tolerance=grid_label_inline_usa_tol) +def test_grid_labels_inline_usa(): + top = 49.3457868 # north lat + left = -124.7844079 # west long + right = -66.9513812 # east long + bottom = 24.7433195 # south lat + plt.figure(figsize=(35, 35)) + for i, proj in enumerate(TEST_PROJS, 1): + if isinstance(proj, tuple): + proj, kwargs = proj + else: + kwargs = {} + ax = plt.subplot(7, 4, i, projection=proj(**kwargs)) + try: + ax.set_extent([left, right, bottom, top], + crs=ccrs.PlateCarree()) + except Exception: + pass + ax.set_title(proj, y=1.075) + if (ccrs.PROJ4_VERSION[:2] == (5, 0) and + proj in (ccrs.Orthographic, ccrs.AlbersEqualArea, + ccrs.Geostationary, ccrs.NearsidePerspective)): + # Above projections are broken, so skip labels. + # Add gridlines anyway to minimize image differences. + ax.gridlines() + else: + ax.gridlines(draw_labels=True, auto_inline=True, clip_on=True) + ax.coastlines(resolution="110m") + plt.subplots_adjust(wspace=0.35, hspace=0.35) + + +@pytest.mark.parametrize( + "proj,gcrs,xloc,xfmt,xloc_expected,xfmt_expected", + [ + (ccrs.PlateCarree(), ccrs.PlateCarree(), + [10, 20], None, mticker.FixedLocator, LongitudeFormatter), + (ccrs.PlateCarree(), ccrs.Mercator(), + [10, 20], None, mticker.FixedLocator, classic_formatter), + (ccrs.PlateCarree(), ccrs.PlateCarree(), + mticker.MaxNLocator(nbins=9), None, + mticker.MaxNLocator, LongitudeFormatter), + (ccrs.PlateCarree(), ccrs.Mercator(), + mticker.MaxNLocator(nbins=9), None, + mticker.MaxNLocator, classic_formatter), + (ccrs.PlateCarree(), ccrs.PlateCarree(), + None, None, LongitudeLocator, LongitudeFormatter), + (ccrs.PlateCarree(), ccrs.Mercator(), + None, None, classic_locator.__class__, classic_formatter), + (ccrs.PlateCarree(), ccrs.PlateCarree(), + None, mticker.StrMethodFormatter('{x}'), + LongitudeLocator, mticker.StrMethodFormatter), + ]) +def test_gridliner_default_fmtloc( + proj, gcrs, xloc, xfmt, xloc_expected, xfmt_expected): + plt.figure() + ax = plt.subplot(111, projection=proj) + gl = ax.gridlines(crs=gcrs, draw_labels=False, xlocs=xloc, xformatter=xfmt) + plt.close() + assert isinstance(gl.xlocator, xloc_expected) + assert isinstance(gl.xformatter, xfmt_expected) diff -Nru python-cartopy-0.17.0+dfsg/lib/cartopy/tests/mpl/test_images.py python-cartopy-0.18.0+dfsg/lib/cartopy/tests/mpl/test_images.py --- python-cartopy-0.17.0+dfsg/lib/cartopy/tests/mpl/test_images.py 2018-11-17 07:25:32.000000000 +0000 +++ python-cartopy-0.18.0+dfsg/lib/cartopy/tests/mpl/test_images.py 2020-05-03 08:12:47.000000000 +0000 @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2011 - 2018, Met Office +# (C) British Crown Copyright 2011 - 2020, Met Office # # This file is part of cartopy. # @@ -44,13 +44,20 @@ # We have an exceptionally large tolerance for the web_tiles test. # The basemap changes on a regular basis (for seasons) and we really only # care that it is putting images onto the map which are roughly correct. +if MPL_VERSION < '2': + web_tiles_tolerance = 12 +elif MPL_VERSION < '2.1.0': + web_tiles_tolerance = 4.6 +else: + web_tiles_tolerance = 5.4 + + @pytest.mark.natural_earth @pytest.mark.network @pytest.mark.xfail(ccrs.PROJ4_VERSION == (5, 0, 0), reason='Proj returns slightly different bounds.', strict=True) -@ImageTesting(['web_tiles'], - tolerance=12 if MPL_VERSION < '2' else 2.9) +@ImageTesting(['web_tiles'], tolerance=web_tiles_tolerance) def test_web_tiles(): extent = [-15, 0.1, 50, 60] target_domain = sgeom.Polygon([[extent[0], extent[1]], @@ -90,7 +97,7 @@ reason='Proj returns slightly different bounds.', strict=True) @ImageTesting(['image_merge'], - tolerance=3.6 if MPL_VERSION < '2' else 0.01) + tolerance=3.9 if MPL_VERSION < '2' else 0.01) def test_image_merge(): # tests the basic image merging functionality tiles = [] @@ -120,7 +127,7 @@ reason='Proj Orthographic projection is buggy.', strict=True) @ImageTesting(['imshow_natural_earth_ortho'], - tolerance=3.96 if MPL_VERSION < '2' else 0.7) + tolerance=3.99 if MPL_VERSION < '2' else 0.7) def test_imshow(): source_proj = ccrs.PlateCarree() img = plt.imread(NATURAL_EARTH_IMG) @@ -128,13 +135,13 @@ # form that JPG images would be loaded with imread. img = (img * 255).astype('uint8') ax = plt.axes(projection=ccrs.Orthographic()) - ax.imshow(img, origin='upper', transform=source_proj, + ax.imshow(img, transform=source_proj, extent=[-180, 180, -90, 90]) @pytest.mark.natural_earth @ImageTesting(['imshow_regional_projected'], - tolerance=10.4 if MPL_VERSION < '2' else 0) + tolerance=10.4 if MPL_VERSION < '2' else 0.8) def test_imshow_projected(): source_proj = ccrs.PlateCarree() img_extent = (-120.67660000000001, -106.32104523100001, @@ -143,14 +150,24 @@ ax = plt.axes(projection=ccrs.LambertConformal()) ax.set_extent(img_extent, crs=source_proj) ax.coastlines(resolution='50m') - ax.imshow(img, extent=img_extent, origin='upper', transform=source_proj) + ax.imshow(img, extent=img_extent, transform=source_proj) + + +def test_imshow_wrapping(): + ax = plt.axes(projection=ccrs.PlateCarree(central_longitude=0.0)) + # Set the extent outside of the current projection domain to ensure + # it is wrapped back to the (-180, 180) extent of the projection + ax.imshow(np.random.random((10, 10)), transform=ccrs.PlateCarree(), + extent=(0, 360, -90, 90)) + + assert ax.get_xlim() == (-180, 180) @pytest.mark.xfail((5, 0, 0) <= ccrs.PROJ4_VERSION < (5, 1, 0), reason='Proj Orthographic projection is buggy.', strict=True) @ImageTesting(['imshow_natural_earth_ortho'], - tolerance=4.15 if MPL_VERSION < '2' else 0.7) + tolerance=4.19 if MPL_VERSION < '2' else 0.7) def test_stock_img(): ax = plt.axes(projection=ccrs.Orthographic()) ax.stock_img() @@ -160,12 +177,12 @@ reason='Proj Orthographic projection is buggy.', strict=True) @ImageTesting(['imshow_natural_earth_ortho'], - tolerance=3.96 if MPL_VERSION < '2' else 0.7) + tolerance=3.99 if MPL_VERSION < '2' else 0.7) def test_pil_Image(): img = Image.open(NATURAL_EARTH_IMG) source_proj = ccrs.PlateCarree() ax = plt.axes(projection=ccrs.Orthographic()) - ax.imshow(img, origin='upper', transform=source_proj, + ax.imshow(img, transform=source_proj, extent=[-180, 180, -90, 90]) @@ -173,7 +190,7 @@ reason='Proj Orthographic projection is buggy.', strict=True) @ImageTesting(['imshow_natural_earth_ortho'], - tolerance=4.2 if MPL_VERSION < '2' else 0) + tolerance=4.2 if MPL_VERSION < '2' else 0.5) def test_background_img(): ax = plt.axes(projection=ccrs.Orthographic()) ax.background_img(name='ne_shaded', resolution='low') diff -Nru python-cartopy-0.17.0+dfsg/lib/cartopy/tests/mpl/test_img_transform.py python-cartopy-0.18.0+dfsg/lib/cartopy/tests/mpl/test_img_transform.py --- python-cartopy-0.17.0+dfsg/lib/cartopy/tests/mpl/test_img_transform.py 2018-11-17 07:25:32.000000000 +0000 +++ python-cartopy-0.18.0+dfsg/lib/cartopy/tests/mpl/test_img_transform.py 2020-05-03 08:12:47.000000000 +0000 @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2011 - 2018, Met Office +# (C) British Crown Copyright 2011 - 2020, Met Office # # This file is part of cartopy. # @@ -94,11 +94,14 @@ if MPL_VERSION < '2': # Changes in zooming in old versions. regrid_tolerance = 2.5 -elif '2.0.1' <= MPL_VERSION: +elif MPL_VERSION < '2.0.1': + regrid_tolerance = 0.5 +elif MPL_VERSION < '2.1.0': # Bug in latest Matplotlib that we don't consider correct. - regrid_tolerance = 4.75 + regrid_tolerance = 4.78 else: - regrid_tolerance = 0 + # Bug in latest Matplotlib that we don't consider correct. + regrid_tolerance = 5.55 @pytest.mark.natural_earth @@ -133,13 +136,13 @@ gs = mpl.gridspec.GridSpec(nrows=4, ncols=1, hspace=1.5, wspace=0.5) # Set up axes and title - ax = plt.subplot(gs[0], frameon=False, projection=target_proj) + ax = plt.subplot(gs[0], projection=target_proj) plt.imshow(new_array, origin='lower', extent=target_extent) ax.coastlines() # Plot each color slice (tests masking) cmaps = {'red': 'Reds', 'green': 'Greens', 'blue': 'Blues'} for i, color in enumerate(['red', 'green', 'blue']): - ax = plt.subplot(gs[i + 1], frameon=False, projection=target_proj) + ax = plt.subplot(gs[i + 1], projection=target_proj) plt.imshow(new_array[:, :, i], extent=target_extent, origin='lower', cmap=cmaps[color]) ax.coastlines() diff -Nru python-cartopy-0.17.0+dfsg/lib/cartopy/tests/mpl/test_mpl_integration.py python-cartopy-0.18.0+dfsg/lib/cartopy/tests/mpl/test_mpl_integration.py --- python-cartopy-0.17.0+dfsg/lib/cartopy/tests/mpl/test_mpl_integration.py 2018-11-17 07:25:32.000000000 +0000 +++ python-cartopy-0.18.0+dfsg/lib/cartopy/tests/mpl/test_mpl_integration.py 2020-05-03 08:12:47.000000000 +0000 @@ -1,19 +1,8 @@ -# (C) British Crown Copyright 2011 - 2018, Met Office +# Copyright Cartopy Contributors # -# 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 . +# 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. from __future__ import (absolute_import, division, print_function) @@ -33,10 +22,16 @@ _ROB_TOL = 0.5 if ccrs.PROJ4_VERSION < (4, 9) else 0.111 _CONTOUR_STYLE = _STREAMPLOT_STYLE = 'classic' +_CONTOUR_TOL = 0.5 if MPL_VERSION >= '3.0.0': _CONTOUR_IMAGE = 'global_contour_wrap' _CONTOUR_STYLE = 'mpl20' - _STREAMPLOT_IMAGE = 'streamplot_mpl_3.0.0' + if MPL_VERSION < '3.2.0': + _CONTOUR_TOL = 0.74 + if MPL_VERSION >= '3.2.0': + _STREAMPLOT_IMAGE = 'streamplot_mpl_3.2.0' + else: + _STREAMPLOT_IMAGE = 'streamplot_mpl_3.0.0' # Should have been the case for anything but _1.4.3, but we don't want to # regenerate those images again. _STREAMPLOT_STYLE = 'mpl20' @@ -45,13 +40,15 @@ if MPL_VERSION >= '2.1.0': _STREAMPLOT_IMAGE = 'streamplot_mpl_2.1.0' elif MPL_VERSION >= '2': + _CONTOUR_TOL = 11.4 _STREAMPLOT_IMAGE = 'streamplot_mpl_2.0.0' else: + _CONTOUR_TOL = 11.4 _STREAMPLOT_IMAGE = 'streamplot_mpl_1.4.3' @pytest.mark.natural_earth -@ImageTesting([_CONTOUR_IMAGE], style=_CONTOUR_STYLE) +@ImageTesting([_CONTOUR_IMAGE], style=_CONTOUR_STYLE, tolerance=_CONTOUR_TOL) def test_global_contour_wrap_new_transform(): ax = plt.axes(projection=ccrs.PlateCarree()) ax.coastlines() @@ -61,7 +58,7 @@ @pytest.mark.natural_earth -@ImageTesting([_CONTOUR_IMAGE], style=_CONTOUR_STYLE) +@ImageTesting([_CONTOUR_IMAGE], style=_CONTOUR_STYLE, tolerance=_CONTOUR_TOL) def test_global_contour_wrap_no_transform(): ax = plt.axes(projection=ccrs.PlateCarree()) ax.coastlines() @@ -96,7 +93,7 @@ ax = plt.axes(projection=ccrs.PlateCarree()) ax.coastlines() x, y = np.meshgrid(np.linspace(0, 360), np.linspace(-90, 90)) - data = np.sin(np.sqrt(x ** 2 + y ** 2)) + data = np.sin(np.sqrt(x ** 2 + y ** 2))[:-1, :-1] plt.pcolor(x, y, data, transform=ccrs.PlateCarree()) @@ -106,12 +103,13 @@ ax = plt.axes(projection=ccrs.PlateCarree()) ax.coastlines() x, y = np.meshgrid(np.linspace(0, 360), np.linspace(-90, 90)) - data = np.sin(np.sqrt(x ** 2 + y ** 2)) + data = np.sin(np.sqrt(x ** 2 + y ** 2))[:-1, :-1] plt.pcolor(x, y, data) @pytest.mark.natural_earth -@ImageTesting(['global_scatter_wrap']) +@ImageTesting(['global_scatter_wrap'], + tolerance=12.4 if MPL_VERSION < '2.1.0' else 0.5) def test_global_scatter_wrap_new_transform(): ax = plt.axes(projection=ccrs.PlateCarree()) # By default the coastline feature will be drawn after patches. @@ -124,7 +122,8 @@ @pytest.mark.natural_earth -@ImageTesting(['global_scatter_wrap']) +@ImageTesting(['global_scatter_wrap'], + tolerance=12.4 if MPL_VERSION < '2.1.0' else 0.5) def test_global_scatter_wrap_no_transform(): ax = plt.axes(projection=ccrs.PlateCarree()) ax.coastlines(zorder=0) @@ -134,7 +133,7 @@ @ImageTesting(['global_map'], - tolerance=16 if ccrs.PROJ4_VERSION < (4, 9) else 0.1) + tolerance=1.93 if MPL_VERSION < '2.1.0' else 0.55) def test_global_map(): plt.axes(projection=ccrs.Robinson()) # ax.coastlines() @@ -149,6 +148,7 @@ transform=ccrs.Geodetic()) +@pytest.mark.filterwarnings("ignore:Unable to determine extent") @pytest.mark.natural_earth @ImageTesting(['simple_global']) def test_simple_global(): @@ -157,16 +157,18 @@ # produces a global map, despite not having needed to set the limits +@pytest.mark.filterwarnings("ignore:Unable to determine extent") @pytest.mark.natural_earth @ImageTesting(['multiple_projections4' if ccrs.PROJ4_VERSION < (5, 0, 0) - else 'multiple_projections5']) + else 'multiple_projections5'], + tolerance=0.81) def test_multiple_projections(): projections = [ccrs.PlateCarree(), ccrs.Robinson(), ccrs.RotatedPole(pole_latitude=45, pole_longitude=180), - ccrs.OSGB(), - ccrs.TransverseMercator(), + ccrs.OSGB(approx=True), + ccrs.TransverseMercator(approx=True), ccrs.Mercator( globe=ccrs.Globe(semimajor_axis=math.degrees(1)), min_latitude=-85., max_latitude=85.), @@ -187,7 +189,7 @@ ccrs.EckertVI(), ] - rows = np.ceil(len(projections) / 5) + rows = np.ceil(len(projections) / 5).astype(int) fig = plt.figure(figsize=(10, 2 * rows)) for i, prj in enumerate(projections, 1): @@ -195,7 +197,7 @@ ax.set_global() - ax.coastlines() + ax.coastlines(resolution="110m") plt.plot(-0.08, 51.53, 'o', transform=ccrs.PlateCarree()) @@ -274,7 +276,8 @@ @pytest.mark.natural_earth -@ImageTesting(['pcolormesh_global_wrap1']) +@ImageTesting(['pcolormesh_global_wrap1'], + tolerance=6.3 if MPL_VERSION < '2.1.0' else 1.27) def test_pcolormesh_global_with_wrap1(): # make up some realistic data with bounds (such as data from the UM) nx, ny = 36, 18 @@ -296,10 +299,17 @@ ax.set_global() # make sure everything is visible +tolerance = 1.61 +if MPL_VERSION < '2.1.0': + tolerance = 6.4 +if (5, 0, 0) <= ccrs.PROJ4_VERSION < (5, 1, 0): + tolerance += 0.8 + + @pytest.mark.natural_earth @ImageTesting( ['pcolormesh_global_wrap2'], - tolerance=1.8 if (5, 0, 0) <= ccrs.PROJ4_VERSION < (5, 1, 0) else 0.5) + tolerance=tolerance) def test_pcolormesh_global_with_wrap2(): # make up some realistic data with bounds (such as data from the UM) nx, ny = 36, 18 @@ -325,10 +335,17 @@ ax.set_global() # make sure everything is visible +tolerance = 1.39 +if MPL_VERSION < '2.1.0': + tolerance = 2.5 +if (5, 0, 0) <= ccrs.PROJ4_VERSION < (5, 1, 0): + tolerance += 1.4 + + @pytest.mark.natural_earth @ImageTesting( ['pcolormesh_global_wrap3'], - tolerance=2.4 if (5, 0, 0) <= ccrs.PROJ4_VERSION < (5, 1, 0) else _ROB_TOL) + tolerance=tolerance) def test_pcolormesh_global_with_wrap3(): nx, ny = 33, 17 xbnds = np.linspace(-1.875, 358.125, nx, endpoint=True) @@ -365,9 +382,10 @@ ax.set_global() # make sure everything is visible +@pytest.mark.xfail(MPL_VERSION < '2.1.0', reason='Matplotlib is broken.') @pytest.mark.natural_earth @ImageTesting(['pcolormesh_limited_area_wrap'], - tolerance=1.41 if MPL_VERSION >= '2.1.0' else 0.7) + tolerance=1.82 if MPL_VERSION >= '2.1.0' else 0.7) def test_pcolormesh_limited_area_wrap(): # make up some realistic data with bounds (such as data from the UM's North # Atlantic Europe model) @@ -427,6 +445,7 @@ ax.set_global() +@pytest.mark.xfail(MPL_VERSION < '2.1.0', reason='Matplotlib is broken.') @pytest.mark.natural_earth @ImageTesting(['pcolormesh_goode_wrap']) def test_pcolormesh_goode_wrap(): @@ -442,8 +461,9 @@ ax.pcolormesh(x, y, Z, transform=ccrs.PlateCarree()) +@pytest.mark.xfail(MPL_VERSION < '2.1.0', reason='Matplotlib is broken.') @pytest.mark.natural_earth -@ImageTesting(['pcolormesh_mercator_wrap']) +@ImageTesting(['pcolormesh_mercator_wrap'], tolerance=0.93) def test_pcolormesh_mercator_wrap(): x = np.linspace(0, 360, 73) y = np.linspace(-87.5, 87.5, 36) @@ -455,6 +475,7 @@ ax.pcolormesh(x, y, Z, transform=ccrs.PlateCarree()) +@pytest.mark.xfail(MPL_VERSION < '2.1.0', reason='Matplotlib is broken.') @pytest.mark.natural_earth @ImageTesting(['quiver_plate_carree']) def test_quiver_plate_carree(): @@ -469,7 +490,7 @@ # plot on native projection ax = plt.subplot(211, projection=ccrs.PlateCarree()) ax.set_extent(plot_extent, crs=ccrs.PlateCarree()) - ax.coastlines() + ax.coastlines(resolution="110m") ax.quiver(x, y, u, v, mag) # plot on a different projection ax = plt.subplot(212, projection=ccrs.NorthPolarStereo()) @@ -478,6 +499,7 @@ ax.quiver(x, y, u, v, mag, transform=ccrs.PlateCarree()) +@pytest.mark.xfail(MPL_VERSION < '2.1.0', reason='Matplotlib is broken.') @pytest.mark.natural_earth @ImageTesting(['quiver_rotated_pole']) def test_quiver_rotated_pole(): @@ -522,7 +544,7 @@ @pytest.mark.natural_earth -@ImageTesting(['quiver_regrid_with_extent']) +@ImageTesting(['quiver_regrid_with_extent'], tolerance=0.51) def test_quiver_regrid_with_extent(): x = np.arange(-60, 42.5, 2.5) y = np.arange(30, 72.5, 2.5) @@ -541,7 +563,8 @@ @pytest.mark.natural_earth -@ImageTesting(['barbs_plate_carree']) +@ImageTesting(['barbs_plate_carree'], + tolerance=8 if MPL_VERSION < '2.1.0' else 0.5) def test_barbs(): x = np.arange(-60, 45, 5) y = np.arange(30, 75, 5) @@ -553,17 +576,18 @@ # plot on native projection ax = plt.subplot(211, projection=ccrs.PlateCarree()) ax.set_extent(plot_extent, crs=ccrs.PlateCarree()) - ax.coastlines() + ax.coastlines(resolution="110m") ax.barbs(x, y, u, v, length=4, linewidth=.25) # plot on a different projection ax = plt.subplot(212, projection=ccrs.NorthPolarStereo()) ax.set_extent(plot_extent, crs=ccrs.PlateCarree()) - ax.coastlines() + ax.coastlines(resolution="110m") ax.barbs(x, y, u, v, transform=ccrs.PlateCarree(), length=4, linewidth=.25) @pytest.mark.natural_earth -@ImageTesting(['barbs_regrid']) +@ImageTesting(['barbs_regrid'], + tolerance=2.9 if MPL_VERSION < '2.1.0' else 0.5) def test_barbs_regrid(): x = np.arange(-60, 42.5, 2.5) y = np.arange(30, 72.5, 2.5) @@ -581,7 +605,7 @@ @pytest.mark.natural_earth -@ImageTesting(['barbs_regrid_with_extent']) +@ImageTesting(['barbs_regrid_with_extent'], tolerance=0.54) def test_barbs_regrid_with_extent(): x = np.arange(-60, 42.5, 2.5) y = np.arange(30, 72.5, 2.5) @@ -611,7 +635,7 @@ plt.figure(figsize=(6, 5)) ax = plt.axes(projection=ccrs.PlateCarree()) ax.set_extent(plot_extent, crs=ccrs.PlateCarree()) - ax.coastlines() + ax.coastlines(resolution="110m") ax.barbs(x, y, u, v, transform=ccrs.PlateCarree(), length=8, linewidth=1, color='#7f7f7f') @@ -633,7 +657,7 @@ @pytest.mark.natural_earth -@ImageTesting([_STREAMPLOT_IMAGE], style=_STREAMPLOT_STYLE) +@ImageTesting([_STREAMPLOT_IMAGE], style=_STREAMPLOT_STYLE, tolerance=0.54) def test_streamplot(): x = np.arange(-60, 42.5, 2.5) y = np.arange(30, 72.5, 2.5) diff -Nru python-cartopy-0.17.0+dfsg/lib/cartopy/tests/mpl/test_nightshade.py python-cartopy-0.18.0+dfsg/lib/cartopy/tests/mpl/test_nightshade.py --- python-cartopy-0.17.0+dfsg/lib/cartopy/tests/mpl/test_nightshade.py 2018-11-17 07:25:32.000000000 +0000 +++ python-cartopy-0.18.0+dfsg/lib/cartopy/tests/mpl/test_nightshade.py 2020-05-03 08:12:47.000000000 +0000 @@ -27,6 +27,7 @@ from cartopy.tests.mpl import ImageTesting +@pytest.mark.natural_earth @ImageTesting(['nightshade_platecarree']) def test_nightshade_image(): # Test the actual creation of the image diff -Nru python-cartopy-0.17.0+dfsg/lib/cartopy/tests/mpl/test_patch.py python-cartopy-0.18.0+dfsg/lib/cartopy/tests/mpl/test_patch.py --- python-cartopy-0.17.0+dfsg/lib/cartopy/tests/mpl/test_patch.py 2018-11-17 07:25:32.000000000 +0000 +++ python-cartopy-0.18.0+dfsg/lib/cartopy/tests/mpl/test_patch.py 2020-05-03 08:12:47.000000000 +0000 @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2015 - 2018, Met Office +# (C) British Crown Copyright 2015 - 2020, Met Office # # This file is part of cartopy. # @@ -26,14 +26,21 @@ class Test_path_to_geos(object): - def test_empty_polyon(self): - p = Path([[0, 0], [0, 0], [0, 0], [0, 0], - [1, 2], [1, 2], [1, 2], [1, 2]], - codes=[1, 2, 2, 79, - 1, 2, 2, 79]) + def test_empty_polygon(self): + p = Path( + [ + [0, 0], [0, 0], [0, 0], [0, 0], + [1, 2], [1, 2], [1, 2], [1, 2], + # The vertex for CLOSEPOLY should be ignored. + [2, 3], [2, 3], [2, 3], [42, 42], + # Very close points should be treated the same. + [193.75, -14.166664123535156], [193.75, -14.166664123535158], + [193.75, -14.166664123535156], [193.75, -14.166664123535156], + ], + codes=[1, 2, 2, 79] * 4) geoms = cpatch.path_to_geos(p) - assert [type(geom) for geom in geoms] == [sgeom.Point, sgeom.Point] - assert len(geoms) == 2 + assert [type(geom) for geom in geoms] == [sgeom.Point] * 4 + assert len(geoms) == 4 @pytest.mark.skipif(matplotlib.__version__ < '2.2.0', reason='Paths may not be closed with old Matplotlib.') diff -Nru python-cartopy-0.17.0+dfsg/lib/cartopy/tests/mpl/test_plots.py python-cartopy-0.18.0+dfsg/lib/cartopy/tests/mpl/test_plots.py --- python-cartopy-0.17.0+dfsg/lib/cartopy/tests/mpl/test_plots.py 2018-11-17 07:25:32.000000000 +0000 +++ python-cartopy-0.18.0+dfsg/lib/cartopy/tests/mpl/test_plots.py 2020-05-03 08:12:47.000000000 +0000 @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2018, Met Office +# (C) British Crown Copyright 2018 - 2019, Met Office # # This file is part of cartopy. # @@ -40,6 +40,6 @@ triangles = np.asarray([[0, 1, 2]]) fig = plt.figure() - ax = plt.axes(projection=ccrs.OSGB()) + ax = plt.axes(projection=ccrs.OSGB(approx=False)) ax.triplot(x, y, triangles, transform=ccrs.Geodetic()) fig.savefig(BytesIO(), bbox_inches='tight') diff -Nru python-cartopy-0.17.0+dfsg/lib/cartopy/tests/mpl/test_pseudo_color.py python-cartopy-0.18.0+dfsg/lib/cartopy/tests/mpl/test_pseudo_color.py --- python-cartopy-0.17.0+dfsg/lib/cartopy/tests/mpl/test_pseudo_color.py 2018-11-17 07:25:32.000000000 +0000 +++ python-cartopy-0.18.0+dfsg/lib/cartopy/tests/mpl/test_pseudo_color.py 2020-05-03 08:12:47.000000000 +0000 @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2013 - 2017, Met Office +# (C) British Crown Copyright 2013 - 2020, Met Office # # This file is part of cartopy. # @@ -24,8 +24,10 @@ except ImportError: import mock import numpy as np +import pytest import cartopy.crs as ccrs +from cartopy.tests.mpl import MPL_VERSION def test_pcolormesh_fully_masked(): @@ -66,6 +68,7 @@ plt.close() +@pytest.mark.skipif(MPL_VERSION < '2.1.0', reason='Matplotlib is broken.') def test_savefig_tight(): nx, ny = 36, 18 xbnds = np.linspace(0, 360, nx, endpoint=True) diff -Nru python-cartopy-0.17.0+dfsg/lib/cartopy/tests/mpl/test_set_extent.py python-cartopy-0.18.0+dfsg/lib/cartopy/tests/mpl/test_set_extent.py --- python-cartopy-0.17.0+dfsg/lib/cartopy/tests/mpl/test_set_extent.py 2018-11-17 07:25:32.000000000 +0000 +++ python-cartopy-0.18.0+dfsg/lib/cartopy/tests/mpl/test_set_extent.py 2020-05-03 08:12:47.000000000 +0000 @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2011 - 2018, Met Office +# (C) British Crown Copyright 2011 - 2019, Met Office # # This file is part of cartopy. # @@ -96,8 +96,8 @@ ax.set_extent((-180, 180, -90, 90), ccrs.PlateCarree(90)) assert_array_equal(ax.viewLim.get_points(), [[-180, -90], [180, 90]]) - ax = plt.axes(projection=ccrs.OSGB()) - ax.set_extent((0, 7e5, 0, 13e5), ccrs.OSGB()) + ax = plt.axes(projection=ccrs.OSGB(approx=False)) + ax.set_extent((0, 7e5, 0, 13e5), ccrs.OSGB(approx=False)) assert_array_equal(ax.viewLim.get_points(), [[0, 0], [7e5, 13e5]]) diff -Nru python-cartopy-0.17.0+dfsg/lib/cartopy/tests/mpl/test_shapely_to_mpl.py python-cartopy-0.18.0+dfsg/lib/cartopy/tests/mpl/test_shapely_to_mpl.py --- python-cartopy-0.17.0+dfsg/lib/cartopy/tests/mpl/test_shapely_to_mpl.py 2018-11-17 07:25:32.000000000 +0000 +++ python-cartopy-0.18.0+dfsg/lib/cartopy/tests/mpl/test_shapely_to_mpl.py 2020-05-03 08:12:47.000000000 +0000 @@ -1,19 +1,8 @@ -# (C) British Crown Copyright 2011 - 2018, Met Office +# Copyright Cartopy Contributors # -# 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 . +# 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. from __future__ import (absolute_import, division, print_function) @@ -31,8 +20,10 @@ from cartopy.tests.mpl import ImageTesting +# Note: Matplotlib is broken here +# https://github.com/matplotlib/matplotlib/issues/15946 @pytest.mark.natural_earth -@ImageTesting(['poly_interiors']) +@ImageTesting(['poly_interiors'], tolerance=3.1) def test_polygon_interiors(): ax = plt.subplot(211, projection=ccrs.PlateCarree()) @@ -67,7 +58,7 @@ # test multiple interior polygons ax = plt.subplot(212, projection=ccrs.PlateCarree(), xlim=[-5, 15], ylim=[-5, 15]) - ax.coastlines() + ax.coastlines(resolution="110m") exterior = np.array(sgeom.box(0, 0, 12, 12).exterior.coords) interiors = [np.array(sgeom.box(1, 1, 2, 2, ccw=False).exterior.coords), diff -Nru python-cartopy-0.17.0+dfsg/lib/cartopy/tests/mpl/test_style.py python-cartopy-0.18.0+dfsg/lib/cartopy/tests/mpl/test_style.py --- python-cartopy-0.17.0+dfsg/lib/cartopy/tests/mpl/test_style.py 1970-01-01 00:00:00.000000000 +0000 +++ python-cartopy-0.18.0+dfsg/lib/cartopy/tests/mpl/test_style.py 2020-05-03 08:12:47.000000000 +0000 @@ -0,0 +1,80 @@ +# (C) British Crown Copyright 2018 - 2019, 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 . + +from __future__ import (absolute_import, division, print_function) + +import pytest + +from cartopy.mpl import style + + +@pytest.mark.parametrize( + ('styles', 'expected'), + [([], {}), + ([{}, {}, {}], {}), + ([{}, dict(a=2), dict(a=1)], dict(a=1)), + ([dict(fc='red')], dict(facecolor='red')), + ([dict(fc='red', color='blue')], + dict(facecolor='blue', edgecolor='blue')), + ([dict(fc='red', facecolor='blue')], dict(facecolor='blue')), + ([dict(color='red')], + dict(edgecolor='red', facecolor='red')), + ([dict(edgecolor='blue'), dict(color='red')], + dict(edgecolor='red', facecolor='red')), + ([dict(edgecolor='blue'), dict(color='red')], + dict(edgecolor='red', facecolor='red')), + ([dict(color='blue'), dict(edgecolor='red')], + dict(edgecolor='red', facecolor='blue')), + # Even if you set an edgecolor, color should trump it. + ([dict(color='blue'), dict(edgecolor='red', color='yellow')], + dict(edgecolor='yellow', facecolor='yellow')), + # Support for 'never' being honoured. + ([dict(facecolor='never'), dict(color='yellow')], + dict(edgecolor='yellow', facecolor='never')), + ([dict(lw=1, linewidth=2)], dict(linewidth=2)), + ([dict(lw=1, linewidth=2), dict(lw=3)], dict(linewidth=3)), + ([dict(color=None), dict(facecolor='red')], + dict(facecolor='red', edgecolor=None)), + ([dict(linewidth=1), dict(lw=None)], dict(linewidth=None)), + ] +) +def test_merge(styles, expected): + merged_style = style.merge(*styles) + assert merged_style == expected + + +@pytest.mark.parametrize( + ('case', 'should_warn'), + [[{'fc': 'red'}, True], [{'fc': 'NoNe'}, False], [{'fc': 1}, True]]) +def test_merge_warning(case, should_warn): + warn_type = UserWarning if should_warn else None + with pytest.warns(warn_type, match=r'defined as \"never\"') as record: + style.merge({'facecolor': 'never'}, case) + assert len(record) == (1 if should_warn else 0) + + +@pytest.mark.parametrize( + ('style_d', 'expected'), + [ + # Support for 'never' being honoured. + (dict(facecolor='never', edgecolor='yellow'), + dict(edgecolor='yellow', facecolor='none')), + ]) +def test_finalize(style_d, expected): + assert style.finalize(style_d) == expected + # Double check we are updating in-place + assert style_d == expected diff -Nru python-cartopy-0.17.0+dfsg/lib/cartopy/tests/mpl/test_ticker.py python-cartopy-0.18.0+dfsg/lib/cartopy/tests/mpl/test_ticker.py --- python-cartopy-0.17.0+dfsg/lib/cartopy/tests/mpl/test_ticker.py 2018-11-17 07:25:32.000000000 +0000 +++ python-cartopy-0.18.0+dfsg/lib/cartopy/tests/mpl/test_ticker.py 2020-05-03 08:12:47.000000000 +0000 @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2014 - 2017, Met Office +# (C) British Crown Copyright 2014 - 2020, Met Office # # This file is part of cartopy. # @@ -21,43 +21,32 @@ from unittest.mock import Mock except ImportError: from mock import Mock -from matplotlib.axes import Axes +import matplotlib.pyplot as plt import pytest +import numpy as np import cartopy.crs as ccrs from cartopy.mpl.geoaxes import GeoAxes -from cartopy.mpl.ticker import LatitudeFormatter, LongitudeFormatter +from cartopy.mpl.ticker import (LatitudeFormatter, LongitudeFormatter, + LatitudeLocator, LongitudeLocator) - -def test_LatitudeFormatter_bad_axes(): - formatter = LatitudeFormatter() - formatter.axis = Mock(axes=Mock(Axes, projection=ccrs.PlateCarree())) - message = 'This formatter can only be used with cartopy axes.' - with pytest.raises(TypeError, message=message): - formatter(0) +ONE_MIN = 1 / 60. +ONE_SEC = 1 / 3600. def test_LatitudeFormatter_bad_projection(): formatter = LatitudeFormatter() formatter.axis = Mock(axes=Mock(GeoAxes, projection=ccrs.Orthographic())) - message = 'This formatter cannot be used with non-rectangular projections.' - with pytest.raises(TypeError, message=message): - formatter(0) - - -def test_LongitudeFormatter_bad_axes(): - formatter = LongitudeFormatter() - formatter.axis = Mock(axes=Mock(Axes, projection=ccrs.PlateCarree())) - message = 'This formatter can only be used with cartopy axes.' - with pytest.raises(TypeError, message=message): + match = r'This formatter cannot be used with non-rectangular projections\.' + with pytest.raises(TypeError, match=match): formatter(0) def test_LongitudeFormatter_bad_projection(): formatter = LongitudeFormatter() formatter.axis = Mock(axes=Mock(GeoAxes, projection=ccrs.Orthographic())) - message = 'This formatter cannot be used with non-rectangular projections.' - with pytest.raises(TypeError, message=message): + match = r'This formatter cannot be used with non-rectangular projections\.' + with pytest.raises(TypeError, match=match): formatter(0) @@ -84,7 +73,7 @@ def test_LatitudeFormatter_number_format(): - formatter = LatitudeFormatter(number_format='.2f') + formatter = LatitudeFormatter(number_format='.2f', dms=False) p = ccrs.PlateCarree() formatter.axis = Mock(axes=Mock(GeoAxes, projection=p)) test_ticks = [-90, -60, -30, 0, 30, 60, 90] @@ -109,7 +98,7 @@ def test_LatitudeFormatter_small_numbers(): - formatter = LatitudeFormatter(number_format='.7f') + formatter = LatitudeFormatter(number_format='.7f', dms=False) p = ccrs.PlateCarree() formatter.axis = Mock(axes=Mock(GeoAxes, projection=p)) test_ticks = [40.1275150, 40.1275152, 40.1275154] @@ -164,7 +153,7 @@ def test_LongitudeFormatter_number_format(): - formatter = LongitudeFormatter(number_format='.2f', + formatter = LongitudeFormatter(number_format='.2f', dms=False, dateline_direction_label=True) p = ccrs.PlateCarree() formatter.axis = Mock(axes=Mock(GeoAxes, projection=p)) @@ -190,7 +179,7 @@ def test_LongitudeFormatter_small_numbers_0(): - formatter = LongitudeFormatter(number_format='.7f') + formatter = LongitudeFormatter(number_format='.7f', dms=False) p = ccrs.PlateCarree(central_longitude=0) formatter.axis = Mock(axes=Mock(GeoAxes, projection=p)) test_ticks = [-17.1142343, -17.1142340, -17.1142337] @@ -201,7 +190,7 @@ def test_LongitudeFormatter_small_numbers_180(): - formatter = LongitudeFormatter(zero_direction_label=True, + formatter = LongitudeFormatter(zero_direction_label=True, dms=False, number_format='.7f') p = ccrs.PlateCarree(central_longitude=180) formatter.axis = Mock(axes=Mock(GeoAxes, projection=p)) @@ -210,3 +199,77 @@ expected = [u'162.8857657\u00B0E', u'162.8857660\u00B0E', u'162.8857663\u00B0E'] assert result == expected + + +@pytest.mark.parametrize("test_ticks,expected", + [pytest.param([-3.75, -3.5], + [u"3\u00B0W45'", u"3\u00B0W30'"], + id='minutes_no_hide'), + pytest.param([-3.5, -3.], + [u"30'", u"3\u00B0W"], + id='minutes_hide'), + pytest.param([-3. - 2 * ONE_MIN - 30 * ONE_SEC], + [u"3\u00B0W2'30''"], + id='seconds'), + ]) +def test_LongitudeFormatter_minutes_seconds(test_ticks, expected): + formatter = LongitudeFormatter(dms=True, auto_hide=True) + formatter.set_locs(test_ticks) + result = [formatter(tick) for tick in test_ticks] + assert result == expected + + +@pytest.mark.parametrize("test_ticks,expected", + [pytest.param([-3.75, -3.5], + [u"3\u00B0S45'", u"3\u00B0S30'"], + id='minutes_no_hide'), + ]) +def test_LatitudeFormatter_minutes_seconds(test_ticks, expected): + formatter = LatitudeFormatter(dms=True, auto_hide=True) + formatter.set_locs(test_ticks) + result = [formatter(tick) for tick in test_ticks] + assert result == expected + + +@pytest.mark.parametrize("cls,letter", + [(LongitudeFormatter, 'E'), + (LatitudeFormatter, 'N')]) +def test_lonlatformatter_non_geoaxes(cls, letter): + ticks = [2, 2.5] + fig = plt.figure() + ax = plt.subplot(111) + ax.plot([0, 10], [0, 1]) + ax.set_xticks(ticks) + ax.xaxis.set_major_formatter(cls(degree_symbol='', dms=False)) + fig.canvas.draw() + ticklabels = [t.get_text() for t in ax.get_xticklabels()] + assert ticklabels == ['{:g}{}'.format(v, letter) for v in ticks] + plt.close() + + +@pytest.mark.parametrize("cls,vmin,vmax,expected", + [pytest.param(LongitudeLocator, -180, 180, + [-180., -120., -60., 0., + 60., 120., 180.], + id='lon_large'), + pytest.param(LatitudeLocator, -180, 180, + [-90.0, -60.0, -30.0, 0.0, + 30.0, 60.0, 90.0], + id='lat_large'), + pytest.param(LongitudeLocator, -10, 0, + [-10.5, -9., -7.5, -6., -4.5, + -3., -1.5, 0.], + id='lon_medium'), + pytest.param(LongitudeLocator, -1, 0, + np.array([-60., -50., -40., -30., + -20., -10., 0.]) / 60, + id='lon_small'), + pytest.param(LongitudeLocator, 0, 2 * ONE_MIN, + np.array([0., 18., 36., 54., 72., 90., + 108., 126.]) / 3600, + id='lon_tiny'), + ]) +def test_LongitudeLocator(cls, vmin, vmax, expected): + locator = cls(dms=True) + result = locator.tick_values(vmin, vmax) + np.testing.assert_allclose(result, expected) diff -Nru python-cartopy-0.17.0+dfsg/lib/cartopy/tests/mpl/test_ticks.py python-cartopy-0.18.0+dfsg/lib/cartopy/tests/mpl/test_ticks.py --- python-cartopy-0.17.0+dfsg/lib/cartopy/tests/mpl/test_ticks.py 2018-11-17 07:25:32.000000000 +0000 +++ python-cartopy-0.18.0+dfsg/lib/cartopy/tests/mpl/test_ticks.py 2020-05-03 08:12:47.000000000 +0000 @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2011 - 2018, Met Office +# (C) British Crown Copyright 2011 - 2019, Met Office # # This file is part of cartopy. # @@ -139,7 +139,7 @@ projections = (ccrs.PlateCarree(), ccrs.Mercator(globe=ccrs.Globe( semimajor_axis=math.degrees(1))), - ccrs.TransverseMercator()) + ccrs.TransverseMercator(approx=False)) x = -3.275024 y = 50.753998 for i, prj in enumerate(projections, 1): diff -Nru python-cartopy-0.17.0+dfsg/lib/cartopy/tests/mpl/test_web_services.py python-cartopy-0.18.0+dfsg/lib/cartopy/tests/mpl/test_web_services.py --- python-cartopy-0.17.0+dfsg/lib/cartopy/tests/mpl/test_web_services.py 2018-11-17 07:25:32.000000000 +0000 +++ python-cartopy-0.18.0+dfsg/lib/cartopy/tests/mpl/test_web_services.py 2020-05-03 08:12:47.000000000 +0000 @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2014 - 2018, Met Office +# (C) British Crown Copyright 2014 - 2020, Met Office # # This file is part of cartopy. # @@ -18,6 +18,7 @@ from __future__ import (absolute_import, division, print_function) import matplotlib.pyplot as plt +from matplotlib.testing.decorators import cleanup import pytest from cartopy.tests.mpl import MPL_VERSION, ImageTesting @@ -27,6 +28,7 @@ @pytest.mark.network @pytest.mark.skipif(not _OWSLIB_AVAILABLE, reason='OWSLib is unavailable.') +@pytest.mark.xfail(raises=KeyError, reason='OWSLib WMTS support is broken.') @ImageTesting(['wmts'], tolerance=7.56 if MPL_VERSION < '2' else 0) def test_wmts(): ax = plt.axes(projection=ccrs.PlateCarree()) @@ -36,11 +38,22 @@ @pytest.mark.network +@pytest.mark.skipif(not _OWSLIB_AVAILABLE, reason='OWSLib is unavailable.') +@cleanup +def test_wms_tight_layout(): + ax = plt.axes(projection=ccrs.PlateCarree()) + url = 'http://vmap0.tiles.osgeo.org/wms/vmap0' + layer = 'basic' + ax.add_wms(url, layer) + ax.figure.tight_layout() + + +@pytest.mark.network @pytest.mark.xfail((5, 0, 0) <= ccrs.PROJ4_VERSION < (5, 1, 0), reason='Proj Orthographic projection is buggy.', strict=True) @pytest.mark.skipif(not _OWSLIB_AVAILABLE, reason='OWSLib is unavailable.') -@ImageTesting(['wms'], tolerance=7.76 if MPL_VERSION < '2' else 0) +@ImageTesting(['wms'], tolerance=7.79 if MPL_VERSION < '2' else 0.02) def test_wms(): ax = plt.axes(projection=ccrs.Orthographic()) url = 'http://vmap0.tiles.osgeo.org/wms/vmap0' diff -Nru python-cartopy-0.17.0+dfsg/lib/cartopy/tests/test_coastline.py python-cartopy-0.18.0+dfsg/lib/cartopy/tests/test_coastline.py --- python-cartopy-0.17.0+dfsg/lib/cartopy/tests/test_coastline.py 2018-11-17 07:25:32.000000000 +0000 +++ python-cartopy-0.18.0+dfsg/lib/cartopy/tests/test_coastline.py 2020-05-03 08:12:47.000000000 +0000 @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2011 - 2018, Met Office +# (C) British Crown Copyright 2011 - 2019, Met Office # # This file is part of cartopy. # @@ -30,7 +30,8 @@ # Make sure all the coastlines can be projected without raising any # exceptions. - projection = cartopy.crs.TransverseMercator(central_longitude=-90) + projection = cartopy.crs.TransverseMercator(central_longitude=-90, + approx=False) reader = shp.Reader(COASTLINE_PATH) all_geometries = list(reader.geometries()) geometries = [] @@ -38,6 +39,5 @@ # geometries += all_geometries[48:52] # Aus & Taz # geometries += all_geometries[72:73] # GB # for geometry in geometries: - for i, geometry in enumerate(geometries[93:]): - for line_string in geometry: - projection.project_geometry(line_string) + for geometry in geometries[93:]: + projection.project_geometry(geometry) diff -Nru python-cartopy-0.17.0+dfsg/lib/cartopy/tests/test_coding_standards.py python-cartopy-0.18.0+dfsg/lib/cartopy/tests/test_coding_standards.py --- python-cartopy-0.17.0+dfsg/lib/cartopy/tests/test_coding_standards.py 2018-11-17 07:25:32.000000000 +0000 +++ python-cartopy-0.18.0+dfsg/lib/cartopy/tests/test_coding_standards.py 2020-05-03 08:12:47.000000000 +0000 @@ -1,24 +1,14 @@ -# (C) British Crown Copyright 2012 - 2018, Met Office +# Copyright Cartopy Contributors # -# 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 . +# 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. from __future__ import (absolute_import, division, print_function) from datetime import datetime from fnmatch import fnmatch +import io from itertools import chain import os import re @@ -50,8 +40,20 @@ LICENSE_RE_PATTERN = re.escape(LICENSE_TEMPLATE).replace(r'\{YEARS\}', '(.*?)') # Add shebang possibility or C comment starter to the LICENSE_RE_PATTERN -LICENSE_RE_PATTERN = r'((\#\!.*|\/\*)\n)?' + LICENSE_RE_PATTERN -LICENSE_RE = re.compile(LICENSE_RE_PATTERN, re.MULTILINE) +SHEBANG_PATTERN = r'((\#\!.*|\/\*)\n)?' +LICENSE_RE = re.compile(SHEBANG_PATTERN + LICENSE_RE_PATTERN, re.MULTILINE) + + +LICENSE_TEMPLATE_v2 = """ +# 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. +""".strip() +LICENSE_RE_PATTERN_v2 = re.escape(LICENSE_TEMPLATE_v2) +LICENSE_RE_v2 = re.compile(SHEBANG_PATTERN + LICENSE_RE_PATTERN_v2, + re.MULTILINE) # Guess cartopy repo directory of cartopy - realpath is used to mitigate @@ -63,16 +65,16 @@ class TestLicenseHeaders(object): @staticmethod - def years_of_license_in_file(fh): + def years_of_license_in_file(content, fname): """ Using :data:`LICENSE_RE` look for the years defined in the license header of the given file handle. - If the license cannot be found in the given fh, None will be returned, - else a tuple of (start_year, end_year) will be returned. + If the license cannot be found in the given content, None will be + returned, else a tuple of (start_year, end_year) will be returned. """ - license_matches = LICENSE_RE.match(fh.read()) + license_matches = LICENSE_RE.match(content) if not license_matches: # no license found in file. return None @@ -83,7 +85,6 @@ elif len(years) == 11: start_year, end_year = int(years[:4]), int(years[7:]) else: - fname = getattr(fh, 'name', 'unknown filename') raise ValueError("Unexpected year(s) string in {}'s copyright " "notice: {!r}".format(fname, years)) return (start_year, end_year) @@ -146,19 +147,35 @@ if ext in ('.py', '.pyx', '.c', '.cpp', '.h') and \ os.path.isfile(full_fname) and \ not any(fnmatch(fname, pat) for pat in exclude_patterns): - with open(full_fname) as fh: - years = TestLicenseHeaders.years_of_license_in_file(fh) - if years is None: - print('The file {} has no valid header license and ' - 'has not been excluded from the license header ' - 'test.'.format(fname)) - failed = True - elif last_change.year > years[1]: - print('The file header at {} is out of date. The last' - ' commit was in {}, but the copyright states it' - ' was {}.'.format(fname, last_change.year, - years[1])) - failed = True + + is_empty = os.path.getsize(full_fname) == 0 + + with io.open(full_fname, encoding='utf-8') as fh: + content = fh.read() + + is_yearless_license = bool(LICENSE_RE_v2.match(content)) + years = TestLicenseHeaders.years_of_license_in_file( + content, full_fname) + + if is_empty: + # Allow completely empty files (e.g. ``__init__.py``) + pass + elif is_yearless_license: + # Allow new style license (v2). + pass + + # What is left is the old-style (pre 2019) header. + elif years is None: + print('The file {} has no valid header license and ' + 'has not been excluded from the license header ' + 'test.'.format(fname)) + failed = True + elif last_change.year > years[1]: + print('The file header at {} is out of date. The last' + ' commit was in {}, but the copyright states it' + ' was {}.'.format(fname, last_change.year, + years[1])) + failed = True if failed: raise ValueError('There were license header failures. See stdout.') @@ -196,14 +213,21 @@ if any(fnmatch(full_fname, pat) for pat in self.excluded): continue - with open(full_fname, "r") as fh: + is_empty = os.path.getsize(full_fname) == 0 + + with io.open(full_fname, "r", encoding='utf-8') as fh: content = fh.read() - if re.search(self.future_imports_pattern, content) is None: - print('The file {} has no valid __future__ imports ' - 'and has not been excluded from the imports ' - 'test.'.format(full_fname)) - failed = True + has_future_import = re.search( + self.future_imports_pattern, content) is not None + + if is_empty: + pass + elif not has_future_import: + print('The file {} has no valid __future__ imports ' + 'and has not been excluded from the imports ' + 'test.'.format(full_fname)) + failed = True if failed: raise ValueError('There were __future__ import check failures. ' diff -Nru python-cartopy-0.17.0+dfsg/lib/cartopy/tests/test_crs.py python-cartopy-0.18.0+dfsg/lib/cartopy/tests/test_crs.py --- python-cartopy-0.17.0+dfsg/lib/cartopy/tests/test_crs.py 2018-11-17 07:25:32.000000000 +0000 +++ python-cartopy-0.18.0+dfsg/lib/cartopy/tests/test_crs.py 2020-05-03 08:12:47.000000000 +0000 @@ -1,22 +1,12 @@ -# (C) British Crown Copyright 2011 - 2018, Met Office +# Copyright Cartopy Contributors # -# 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 . +# 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. from __future__ import (absolute_import, division, print_function) +import copy from io import BytesIO import pickle @@ -43,8 +33,9 @@ assert ccrs.Geodetic() == ccrs.Geodetic() - def test_osni(self): - osni = ccrs.OSNI() + @pytest.mark.parametrize('approx', [True, False]) + def test_osni(self, approx): + osni = ccrs.OSNI(approx=approx) ll = ccrs.Geodetic() # results obtained by nearby.org.uk. @@ -82,21 +73,28 @@ r_inverted = np.array(ll.transform_point(r_east, r_north, osgb)) assert_arr_almost_eq(r_inverted, [lon, lat]) - def test_osgb(self): - self._check_osgb(ccrs.OSGB()) + @pytest.mark.parametrize('approx', [True, False]) + def test_osgb(self, approx): + self._check_osgb(ccrs.OSGB(approx=approx)) @pytest.mark.network @pytest.mark.skipif(pyepsg is None, reason='requires pyepsg') def test_epsg(self): uk = ccrs.epsg(27700) assert uk.epsg_code == 27700 - assert_almost_equal( - uk.x_limits, (-118365.7408171, 751581.564796)) - assert_almost_equal( - uk.y_limits, (-5268.1797977, 1272227.798124)) + assert_almost_equal(uk.x_limits, (-118365.7406176, 751581.5647514), + decimal=3) + assert_almost_equal(uk.y_limits, (-5268.1704980, 1272227.7987656), + decimal=2) assert_almost_equal(uk.threshold, 8699.47, decimal=2) self._check_osgb(uk) + @pytest.mark.network + @pytest.mark.skipif(pyepsg is None, reason='requires pyepsg') + def test_epsg_compound_crs(self): + projection = ccrs.epsg(5973) + assert projection.epsg_code == 5973 + def test_europp(self): europp = ccrs.EuroPP() proj4_init = europp.proj4_init @@ -228,13 +226,32 @@ decimal=1) -def test_pickle(): +@pytest.fixture(params=[ + [ccrs.PlateCarree, {}], + [ccrs.PlateCarree, dict( + central_longitude=1.23)], + [ccrs.NorthPolarStereo, dict( + central_longitude=42.5, + globe=ccrs.Globe(ellipse="helmert"))], +]) +def proj_to_copy(request): + cls, kwargs = request.param + return cls(**kwargs) + + +def test_pickle(proj_to_copy): # check that we can pickle a simple CRS fh = BytesIO() - pickle.dump(ccrs.PlateCarree(), fh) + pickle.dump(proj_to_copy, fh) fh.seek(0) - pc = pickle.load(fh) - assert pc == ccrs.PlateCarree() + pickled_prj = pickle.load(fh) + assert proj_to_copy == pickled_prj + + +def test_deepcopy(proj_to_copy): + prj_cp = copy.deepcopy(proj_to_copy) + assert proj_to_copy.proj4_params == prj_cp.proj4_params + assert proj_to_copy == prj_cp def test_PlateCarree_shortcut(): diff -Nru python-cartopy-0.17.0+dfsg/lib/cartopy/tests/test_features.py python-cartopy-0.18.0+dfsg/lib/cartopy/tests/test_features.py --- python-cartopy-0.17.0+dfsg/lib/cartopy/tests/test_features.py 2018-11-17 07:25:32.000000000 +0000 +++ python-cartopy-0.18.0+dfsg/lib/cartopy/tests/test_features.py 2020-05-03 08:12:47.000000000 +0000 @@ -1,23 +1,13 @@ -# (C) British Crown Copyright 2017 - 2018, Met Office +# Copyright Cartopy Contributors # -# 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 . +# 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. from __future__ import (absolute_import, division, print_function) import cartopy.feature as cfeature +import pytest small_extent = (-6, -8, 56, 59) medium_extent = (-20, 20, 20, 60) @@ -71,3 +61,8 @@ # '110m' when the extent is large and autoscale is True. auto_land.intersecting_geometries(large_extent) assert auto_land.scale == '110m' + + +def test_bad_ne_scale(): + with pytest.raises(ValueError, match='not a valid Natural Earth scale'): + cfeature.NaturalEarthFeature('physical', 'land', '30m') diff -Nru python-cartopy-0.17.0+dfsg/lib/cartopy/tests/test_geodesic.py python-cartopy-0.18.0+dfsg/lib/cartopy/tests/test_geodesic.py --- python-cartopy-0.17.0+dfsg/lib/cartopy/tests/test_geodesic.py 2018-11-17 07:25:32.000000000 +0000 +++ python-cartopy-0.18.0+dfsg/lib/cartopy/tests/test_geodesic.py 2020-05-03 08:12:47.000000000 +0000 @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2015 - 2018, Met Office +# (C) British Crown Copyright 2015 - 2020, Met Office # # This file is part of cartopy. # @@ -29,7 +29,7 @@ def setup_class(self): """ Data sampled from the GeographicLib Test Data for Geodesics at: - http://geographiclib.sourceforge.net/html/geodesic.html#testgeod + https://geographiclib.sourceforge.io/html/geodesic.html#testgeod """ self.geod = geodesic.Geodesic() @@ -151,7 +151,7 @@ def test_geometry_length_linestring(): geod = geodesic.Geodesic() - geom = sgeom.LineString(np.array([lhr, jfk, lhr]).T) + geom = sgeom.LineString(np.array([lhr, jfk, lhr])) expected = pytest.approx(lhr_to_jfk * 2, abs=1) assert geod.geometry_length(geom) == expected @@ -159,8 +159,8 @@ def test_geometry_length_multilinestring(): geod = geodesic.Geodesic() geom = sgeom.MultiLineString( - [sgeom.LineString(np.array([lhr, jfk]).T), - sgeom.LineString(np.array([tul, jfk]).T)]) + [sgeom.LineString(np.array([lhr, jfk])), + sgeom.LineString(np.array([tul, jfk]))]) expected = pytest.approx(lhr_to_jfk + jfk_to_tul, abs=1) assert geod.geometry_length(geom) == expected @@ -182,5 +182,5 @@ def test_geometry_length_point(): geod = geodesic.Geodesic() geom = sgeom.Point(lhr) - with pytest.raises(ValueError): + with pytest.raises(TypeError): geod.geometry_length(geom) diff -Nru python-cartopy-0.17.0+dfsg/lib/cartopy/tests/test_img_nest.py python-cartopy-0.18.0+dfsg/lib/cartopy/tests/test_img_nest.py --- python-cartopy-0.17.0+dfsg/lib/cartopy/tests/test_img_nest.py 2018-11-17 07:25:32.000000000 +0000 +++ python-cartopy-0.18.0+dfsg/lib/cartopy/tests/test_img_nest.py 2020-05-03 08:12:47.000000000 +0000 @@ -20,6 +20,7 @@ import io import os import shutil +import sys import warnings import numpy as np @@ -41,35 +42,31 @@ 'wmts', 'aerial') -def test_world_files(): - func = cimg_nest.Img.world_files - fname = 'one' - expected = ['one.w', 'one.W', 'ONE.w', 'ONE.W'] - assert func(fname) == expected - - fname = 'one.png' - expected = ['one.pngw', 'one.pgw', 'one.PNGW', 'one.PGW', - 'ONE.pngw', 'ONE.pgw', 'ONE.PNGW', 'ONE.PGW'] - assert func(fname) == expected - - fname = '/one.png' - expected = ['/one.pngw', '/one.pgw', '/one.PNGW', '/one.PGW', - '/ONE.pngw', '/ONE.pgw', '/ONE.PNGW', '/ONE.PGW'] - assert func(fname) == expected - - fname = '/one/two.png' - expected = ['/one/two.pngw', '/one/two.pgw', - '/one/two.PNGW', '/one/two.PGW', - '/one/TWO.pngw', '/one/TWO.pgw', - '/one/TWO.PNGW', '/one/TWO.PGW'] - assert func(fname) == expected - - fname = '/one/two/THREE.png' - expected = ['/one/two/THREE.pngw', '/one/two/THREE.pgw', - '/one/two/THREE.PNGW', '/one/two/THREE.PGW', - '/one/two/three.pngw', '/one/two/three.pgw', - '/one/two/three.PNGW', '/one/two/three.PGW'] - assert func(fname) == expected +@pytest.mark.parametrize('fname, expected', [ + ('one', ['one.w', 'one.W', 'ONE.w', 'ONE.W']), + ('one.png', + ['one.pngw', 'one.pgw', 'one.PNGW', 'one.PGW', 'ONE.pngw', 'ONE.pgw', + 'ONE.PNGW', 'ONE.PGW']), + ('/one.png', + ['/one.pngw', '/one.pgw', '/one.PNGW', '/one.PGW', '/ONE.pngw', + '/ONE.pgw', '/ONE.PNGW', '/ONE.PGW']), + ('/one/two.png', + ['/one/two.pngw', '/one/two.pgw', '/one/two.PNGW', '/one/two.PGW', + '/one/TWO.pngw', '/one/TWO.pgw', '/one/TWO.PNGW', '/one/TWO.PGW']), + ('/one/two/THREE.png', + ['/one/two/THREE.pngw', '/one/two/THREE.pgw', '/one/two/THREE.PNGW', + '/one/two/THREE.PGW', '/one/two/three.pngw', '/one/two/three.pgw', + '/one/two/three.PNGW', '/one/two/three.PGW']), +]) +def test_world_files(fname, expected): + if sys.platform == 'win32': + fname = fname.replace('/', '\\') + expected = [f.replace('/', '\\') for f in expected] + if fname.startswith('\\'): + fname = 'c:' + fname + expected = ['c:' + f for f in expected] + + assert cimg_nest.Img.world_files(fname) == expected def _save_world(fname, args): diff -Nru python-cartopy-0.17.0+dfsg/lib/cartopy/tests/test_img_tiles.py python-cartopy-0.18.0+dfsg/lib/cartopy/tests/test_img_tiles.py --- python-cartopy-0.17.0+dfsg/lib/cartopy/tests/test_img_tiles.py 2018-11-17 07:25:32.000000000 +0000 +++ python-cartopy-0.18.0+dfsg/lib/cartopy/tests/test_img_tiles.py 2020-05-03 08:12:47.000000000 +0000 @@ -17,6 +17,7 @@ from __future__ import (absolute_import, division, print_function) +import os import types import numpy as np @@ -27,7 +28,6 @@ import cartopy.crs as ccrs import cartopy.io.img_tiles as cimgt - #: Maps Google tile coordinates to native mercator coordinates as defined #: by https://goo.gl/pgJi. KNOWN_EXTENTS = {(0, 0, 0): (-20037508.342789244, 20037508.342789244, @@ -240,3 +240,61 @@ mapbox_sample = cimgt.MapboxStyleTiles(token, username, map_id) url_str = mapbox_sample._image_url(tile) assert url_str == exp_url + + +def test_ordnance_survey_tile_styles(): + """ + Tests that setting the Ordnance Survey tile style works as expected. + + This is essentially just assures information is properly propagated through + the class structure. + """ + dummy_apikey = "None" + + ref_url = ('https://api2.ordnancesurvey.co.uk/' + 'mapping_api/v1/service/wmts?' + 'key=None&height=256&width=256&tilematrixSet=EPSG%3A3857&' + 'version=1.0.0&style=true&layer={layer}%203857&' + 'SERVICE=WMTS&REQUEST=GetTile&format=image%2Fpng&' + 'TileMatrix=EPSG%3A3857%3A{z}&TileRow={y}&TileCol={x}') + tile = ["1", "2", "3"] + + # Default is Road. + os = cimgt.OrdnanceSurvey(dummy_apikey) + url = os._image_url(tile) + assert url == ref_url.format(layer="Road", + z=tile[2], y=tile[1], x=tile[0]) + + for layer in ['Outdoor', 'Light', 'Night', 'Leisure']: + os = cimgt.OrdnanceSurvey(dummy_apikey, layer=layer) + url = os._image_url(tile) + assert url == ref_url.format(layer=layer, + z=tile[2], y=tile[1], x=tile[0]) + + # Exception is raised if unknown style is passed. + with pytest.raises(ValueError): + cimgt.OrdnanceSurvey(dummy_apikey, layer="random_style") + + +@pytest.mark.network +def test_ordnance_survey_get_image(): + # In order to test fetching map images from OS + # an API key needs to be provided + try: + api_key = os.environ['ORDNANCE_SURVEY_API_KEY'] + except KeyError: + pytest.skip('ORDNANCE_SURVEY_API_KEY environment variable is unset.') + + os1 = cimgt.OrdnanceSurvey(api_key, layer="Outdoor") + os2 = cimgt.OrdnanceSurvey(api_key, layer="Night") + + tile = (500, 300, 10) + + img1, extent1, _ = os1.get_image(tile) + img2, extent2, _ = os2.get_image(tile) + + # Different images for different layers + assert img1 != img2 + + # The extent is the same though + assert extent1 == extent2 diff -Nru python-cartopy-0.17.0+dfsg/lib/cartopy/tests/test_img_transform.py python-cartopy-0.18.0+dfsg/lib/cartopy/tests/test_img_transform.py --- python-cartopy-0.17.0+dfsg/lib/cartopy/tests/test_img_transform.py 2018-11-17 07:25:32.000000000 +0000 +++ python-cartopy-0.18.0+dfsg/lib/cartopy/tests/test_img_transform.py 2020-05-03 08:12:47.000000000 +0000 @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2014 - 2017, Met Office +# (C) British Crown Copyright 2014 - 2020, Met Office # # This file is part of cartopy. # @@ -19,11 +19,43 @@ import numpy as np from numpy.testing import assert_array_equal +import pytest import cartopy.img_transform as img_trans import cartopy.crs as ccrs +@pytest.mark.parametrize('xmin, xmax', [ + (-90, 0), (-90, 90), (-90, None), + (0, 90), (0, None), + (None, 0), (None, 90), (None, None)]) +@pytest.mark.parametrize('ymin, ymax', [ + (-45, 0), (-45, 45), (-45, None), + (0, 45), (0, None), + (None, 0), (None, 45), (None, None)]) +def test_mesh_projection_extent(xmin, xmax, ymin, ymax): + proj = ccrs.PlateCarree() + nx = 4 + ny = 2 + + target_x, target_y, extent = img_trans.mesh_projection( + proj, nx, ny, + x_extents=(xmin, xmax), + y_extents=(ymin, ymax)) + + if xmin is None: + xmin = proj.x_limits[0] + if xmax is None: + xmax = proj.x_limits[1] + if ymin is None: + ymin = proj.y_limits[0] + if ymax is None: + ymax = proj.y_limits[1] + assert_array_equal(extent, [xmin, xmax, ymin, ymax]) + assert_array_equal(np.diff(target_x, axis=1), (xmax - xmin) / nx) + assert_array_equal(np.diff(target_y, axis=0), (ymax - ymin) / ny) + + def test_griding_data_std_range(): # Data which exists inside the standard projection bounds i.e. # [-180, 180]. diff -Nru python-cartopy-0.17.0+dfsg/lib/cartopy/tests/test_linear_ring.py python-cartopy-0.18.0+dfsg/lib/cartopy/tests/test_linear_ring.py --- python-cartopy-0.17.0+dfsg/lib/cartopy/tests/test_linear_ring.py 2018-11-17 07:25:32.000000000 +0000 +++ python-cartopy-0.18.0+dfsg/lib/cartopy/tests/test_linear_ring.py 2020-05-03 08:12:47.000000000 +0000 @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2011 - 2017, Met Office +# (C) British Crown Copyright 2011 - 2019, Met Office # # This file is part of cartopy. # @@ -60,7 +60,7 @@ # Check that a ring that is completely out of the map boundary # produces an empty result. # XXX Check efficiency? - projection = ccrs.TransverseMercator(central_longitude=0) + projection = ccrs.TransverseMercator(central_longitude=0, approx=True) rings = [ # All valid diff -Nru python-cartopy-0.17.0+dfsg/lib/cartopy/tests/test_line_string.py python-cartopy-0.18.0+dfsg/lib/cartopy/tests/test_line_string.py --- python-cartopy-0.17.0+dfsg/lib/cartopy/tests/test_line_string.py 2018-11-17 07:25:32.000000000 +0000 +++ python-cartopy-0.18.0+dfsg/lib/cartopy/tests/test_line_string.py 2020-05-03 08:12:47.000000000 +0000 @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2011 - 2017, Met Office +# (C) British Crown Copyright 2011 - 2019, Met Office # # This file is part of cartopy. # @@ -31,7 +31,7 @@ def test_out_of_bounds(self): # Check that a line that is completely out of the map boundary produces # a valid LineString - projection = ccrs.TransverseMercator(central_longitude=0) + projection = ccrs.TransverseMercator(central_longitude=0, approx=True) # For both start & end, define a point that results in well-defined # projection coordinates and one that results in NaN. @@ -184,7 +184,8 @@ assert len(multi_line_string[0].coords) == 2 def test_nan_start(self): - projection = ccrs.TransverseMercator(central_longitude=-90) + projection = ccrs.TransverseMercator(central_longitude=-90, + approx=False) line_string = sgeom.LineString([(10, 50), (-10, 30)]) multi_line_string = projection.project_geometry(line_string) assert len(multi_line_string) == 1 @@ -194,7 +195,8 @@ 'Unexpected NaN in projected coords.' def test_nan_end(self): - projection = ccrs.TransverseMercator(central_longitude=-90) + projection = ccrs.TransverseMercator(central_longitude=-90, + approx=False) line_string = sgeom.LineString([(-10, 30), (10, 50)]) multi_line_string = projection.project_geometry(line_string) # from cartopy.tests.mpl import show @@ -208,7 +210,8 @@ class TestMisc(object): def test_misc(self): - projection = ccrs.TransverseMercator(central_longitude=-90) + projection = ccrs.TransverseMercator(central_longitude=-90, + approx=False) line_string = sgeom.LineString([(10, 50), (-10, 30)]) multi_line_string = projection.project_geometry(line_string) # from cartopy.tests.mpl import show diff -Nru python-cartopy-0.17.0+dfsg/lib/cartopy/tests/test_polygon.py python-cartopy-0.18.0+dfsg/lib/cartopy/tests/test_polygon.py --- python-cartopy-0.17.0+dfsg/lib/cartopy/tests/test_polygon.py 2018-11-17 07:25:32.000000000 +0000 +++ python-cartopy-0.18.0+dfsg/lib/cartopy/tests/test_polygon.py 2020-05-03 08:12:47.000000000 +0000 @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2011 - 2018, Met Office +# (C) British Crown Copyright 2011 - 2019, Met Office # # This file is part of cartopy. # @@ -51,7 +51,7 @@ def test_out_of_bounds(self): # Check that a polygon that is completely out of the map boundary # doesn't produce an empty result. - projection = ccrs.TransverseMercator(central_longitude=0) + projection = ccrs.TransverseMercator(central_longitude=0, approx=True) polys = [ # All valid @@ -73,7 +73,8 @@ class TestMisc(object): def test_misc(self): - projection = ccrs.TransverseMercator(central_longitude=-90) + projection = ccrs.TransverseMercator(central_longitude=-90, + approx=False) polygon = sgeom.Polygon([(-10, 30), (10, 60), (10, 50)]) projection.project_geometry(polygon) @@ -181,11 +182,12 @@ rel=target.threshold) def test_3pt_poly(self): - projection = ccrs.OSGB() + projection = ccrs.OSGB(approx=True) polygon = sgeom.Polygon([(-1000, -1000), (-1000, 200000), (200000, -1000)]) - multi_polygon = projection.project_geometry(polygon, ccrs.OSGB()) + multi_polygon = projection.project_geometry(polygon, + ccrs.OSGB(approx=True)) assert len(multi_polygon) == 1 assert len(multi_polygon[0].exterior.coords) == 4 diff -Nru python-cartopy-0.17.0+dfsg/lib/cartopy/tests/test_shapereader.py python-cartopy-0.18.0+dfsg/lib/cartopy/tests/test_shapereader.py --- python-cartopy-0.17.0+dfsg/lib/cartopy/tests/test_shapereader.py 2018-11-17 07:25:32.000000000 +0000 +++ python-cartopy-0.18.0+dfsg/lib/cartopy/tests/test_shapereader.py 2020-05-03 08:12:47.000000000 +0000 @@ -26,7 +26,6 @@ import cartopy.io.shapereader as shp -@pytest.mark.natural_earth class TestLakes(object): def setup_class(self): LAKES_PATH = os.path.join(os.path.dirname(__file__), @@ -43,10 +42,9 @@ def test_geometry(self): lake_geometry = self.test_lake_geometry - assert lake_geometry.type == 'MultiPolygon' - assert len(lake_geometry) == 1 + assert lake_geometry.type == 'Polygon' - polygon = lake_geometry[0] + polygon = lake_geometry expected = np.array([(-84.85548682324658, 11.147898667846633), (-85.29013729525353, 11.176165676310276), @@ -70,6 +68,8 @@ assert actual == expected assert lake_record.geometry == self.test_lake_geometry + @pytest.mark.skipif(shp._HAS_FIONA, + reason="Fiona reader doesn't support lazy loading.") def test_bounds(self): # tests that a file which has a record with a bbox can # use the bbox without first creating the geometry @@ -99,10 +99,9 @@ def test_geometry(self): geometry = self.test_river_geometry - assert geometry.type == 'MultiLineString' - assert len(geometry) == 1 + assert geometry.type == 'LineString' - linestring = geometry[0] + linestring = geometry coords = linestring.coords assert round(abs(coords[0][0] - -124.83563045947423), 7) == 0 assert round(abs(coords[0][1] - 56.75692352968272), 7) == 0 diff -Nru python-cartopy-0.17.0+dfsg/lib/cartopy/_trace.cpp python-cartopy-0.18.0+dfsg/lib/cartopy/_trace.cpp --- python-cartopy-0.17.0+dfsg/lib/cartopy/_trace.cpp 2018-11-17 07:25:32.000000000 +0000 +++ python-cartopy-0.18.0+dfsg/lib/cartopy/_trace.cpp 1970-01-01 00:00:00.000000000 +0000 @@ -1,658 +0,0 @@ -/* -# (C) British Crown Copyright 2010 - 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 . -*/ - -#include -#include -#include -#include - -#include "_trace.h" - -#ifdef _MSC_VER -#include -#define isnan _isnan -#define isfinite _finite -#endif - - -Interpolator::~Interpolator(){} - -void Interpolator::set_line(const Point &start, const Point &end) -{ - m_start = start; - m_end = end; -} - - -CartesianInterpolator::CartesianInterpolator(projPJ src_proj, projPJ dest_proj) -{ - m_src_proj = src_proj; - m_dest_proj = dest_proj; -} - -Point CartesianInterpolator::interpolate(double t) -{ - Point xy; - xy.x = m_start.x + (m_end.x - m_start.x) * t; - xy.y = m_start.y + (m_end.y - m_start.y) * t; - return project(xy); -} - -Point CartesianInterpolator::project(const Point &src_xy) -{ - Point dest_xy; - projLP xy; - - xy.u = src_xy.x; - xy.v = src_xy.y; - - int status = pj_transform(m_src_proj, m_dest_proj, 1, 1, &xy.u, &xy.v, NULL); - if (status == -14 || status == -20) - { - // -14 => "latitude or longitude exceeded limits" - // -20 => "tolerance condition error" - xy.u = xy.v = HUGE_VAL; - } - else if (status != 0) - { - // TODO: Raise a Python exception instead - std::cerr << "*******************" << std::endl; - std::cerr << status << std::endl; - std::cerr << pj_strerrno(status) << std::endl; - exit(2); - } - - dest_xy.x = xy.u; - dest_xy.y = xy.v; - return dest_xy; -} - - -SphericalInterpolator::SphericalInterpolator(projPJ src_proj, projPJ dest_proj) -{ - m_src_proj = src_proj; - m_dest_proj = dest_proj; -} - -void SphericalInterpolator::set_line(const Point &start, const Point &end) -{ - m_start = start; - m_end = end; - - if (start.x != end.x || start.y != end.y) - { - double lon, lat; - double t, x, y; - Vec3 end3, axis3; - - // Convert lon/lat to unit vectors - lon = start.x * DEG_TO_RAD; - lat = start.y * DEG_TO_RAD; - t = cos(lat); - m_start3.x = t * sin(lon); - m_start3.y = sin(lat); - m_start3.z = t * cos(lon); - - lon = end.x * DEG_TO_RAD; - lat = end.y * DEG_TO_RAD; - t = cos(lat); - end3.x = t * sin(lon); - end3.y = sin(lat); - end3.z = t * cos(lon); - - // Determine the rotation axis for the great circle. - // axis = ||start x end|| - axis3.x = m_start3.y * end3.z - m_start3.z * end3.y; - axis3.y = m_start3.z * end3.x - m_start3.x * end3.z; - axis3.z = m_start3.x * end3.y - m_start3.y * end3.x; - t = sqrt(axis3.x * axis3.x + axis3.y * axis3.y + axis3.z * axis3.z); - axis3.x /= t; - axis3.y /= t; - axis3.z /= t; - - // Figure out the remaining basis vector. - // perp = axis x start - m_perp3.x = axis3.y * m_start3.z - axis3.z * m_start3.y; - m_perp3.y = axis3.z * m_start3.x - axis3.x * m_start3.z; - m_perp3.z = axis3.x * m_start3.y - axis3.y * m_start3.x; - - // Derive the rotation angle around the rotation axis. - x = m_start3.x * end3.x + m_start3.y * end3.y + m_start3.z * end3.z; - y = m_perp3.x * end3.x + m_perp3.y * end3.y + m_perp3.z * end3.z; - m_angle = atan2(y, x); - } - else - { - m_angle = 0.0; - } -} - -Point SphericalInterpolator::interpolate(double t) -{ - Point lonlat; - - if (m_angle == 0.0) - { - lonlat = m_start; - } - else - { - double angle; - double c, s; - double x, y, z; - double lon, lat; - - angle = t * m_angle; - c = cos(angle); - s = sin(angle); - x = m_start3.x * c + m_perp3.x * s; - y = m_start3.y * c + m_perp3.y * s; - z = m_start3.z * c + m_perp3.z * s; - - lat = asin(y); - if(isnan(lat)) - { - lat = y > 0.0 ? 90.0 : -90.0; - } - else - { - lat = lat * RAD_TO_DEG; - } - lon = atan2(x, z) * RAD_TO_DEG; - - lonlat.x = lon; - lonlat.y = lat; - } - - return project(lonlat); -} - -Point SphericalInterpolator::project(const Point &lonlat) -{ - Point xy; - projLP dest; - - //std::cerr << "lon/lat: " << lonlat.x << ", " << lonlat.y; - - dest.u = lonlat.x * DEG_TO_RAD; - dest.v = lonlat.y * DEG_TO_RAD; - - //std::cerr << " => " << dest.u << ", " << dest.v; - - int status = pj_transform(m_src_proj, m_dest_proj, 1, 1, &dest.u, &dest.v, NULL); - if (status == -14 || status == -20) - { - // -14 => "latitude or longitude exceeded limits" - // -20 => "tolerance condition error" - dest.u = dest.v = HUGE_VAL; - } - else if (status != 0) - { - // TODO: Raise a Python exception instead - std::cerr << "*******************" << std::endl; - std::cerr << status << std::endl; - std::cerr << pj_strerrno(status) << std::endl; - exit(2); - } - - //std::cerr << " -> " << dest.u << ", " << dest.v; - - xy.x = dest.u; - xy.y = dest.v; - //std::cerr << "xy: " << xy.x << ", " << xy.y << std::endl; - return xy; -} - - -typedef std::list Line; - -class LineAccumulator -{ - public: - LineAccumulator(); - void new_line(); - void add_point(const Point &point); - void add_point_if_empty(const Point &point); - GEOSGeometry *as_geom(GEOSContextHandle_t handle); - - std::list::size_type size() const - { - return m_lines.size(); - } - - private: - std::list m_lines; -}; - -LineAccumulator::LineAccumulator() -{ - new_line(); -} - -void LineAccumulator::new_line() -{ - //std::cerr << "NEW LINE" << std::endl; - Line line; - m_lines.push_back(line); -} - -void LineAccumulator::add_point(const Point &point) -{ - //std::cerr << "ADD POINT: " << point.x << ", " << point.y << std::endl; - m_lines.back().push_back(point); -} - -void LineAccumulator::add_point_if_empty(const Point &point) -{ - //std::cerr << "ADD POINT IF EMPTY " << m_lines.back().size() << std::endl; - if (m_lines.back().empty()) - { - add_point(point); - } - //std::cerr << " FROM EMPTY " << std::endl; -} - -bool degenerate_line(const Line &value) -{ - return value.size() < 2; -} - -bool close(double a, double b) -{ - return fabs(a - b) <= (1e-8 + 1e-5 * fabs(b)); -} - -GEOSGeometry *LineAccumulator::as_geom(GEOSContextHandle_t handle) -{ - m_lines.remove_if(degenerate_line); - - if(m_lines.size() > 1) - { - //std::cerr << "Checking first & last" << std::endl; - Point first, last; - first = m_lines.front().front(); - last = m_lines.back().back(); - //std::cerr << "first: " << first.x << ", " << first.y << std::endl; - //std::cerr << "last: " << last.x << ", " << last.y << std::endl; - if(close(first.x, last.x) && close(first.y, last.y)) - { - m_lines.front().pop_front(); - m_lines.back().splice(m_lines.back().end(), m_lines.front()); - m_lines.pop_front(); - } - } - - std::vector geoms; - std::list::const_iterator ilines; - for(ilines = m_lines.begin(); ilines != m_lines.end(); ++ilines) - { - std::list::const_iterator ipoints; - int i; - - GEOSCoordSequence *coords = GEOSCoordSeq_create_r(handle, (*ilines).size(), 2); - for(ipoints = (*ilines).begin(), i = 0; ipoints != (*ilines).end(); ++ipoints, ++i) - { - GEOSCoordSeq_setX_r(handle, coords, i, ipoints->x); - GEOSCoordSeq_setY_r(handle, coords, i, ipoints->y); - } - geoms.push_back(GEOSGeom_createLineString_r(handle, coords)); - } - - GEOSGeometry *geom; - if(geoms.empty()) - { - geom = GEOSGeom_createEmptyCollection_r(handle, GEOS_MULTILINESTRING); - } - else - { - geom = GEOSGeom_createCollection_r(handle, GEOS_MULTILINESTRING, - &geoms[0], geoms.size()); - } - return geom; -} - -enum State { - POINT_IN=1, - POINT_OUT, - POINT_NAN -}; - -State get_state(const Point &point, const GEOSPreparedGeometry *gp_domain, - GEOSContextHandle_t handle) -{ - State state; - - if (isfinite(point.x) && isfinite(point.y)) - { - // TODO: Avoid create-destroy - GEOSCoordSequence *coords = GEOSCoordSeq_create_r(handle, 1, 2); - GEOSCoordSeq_setX_r(handle, coords, 0, point.x); - GEOSCoordSeq_setY_r(handle, coords, 0, point.y); - GEOSGeometry *g_point = GEOSGeom_createPoint_r(handle, coords); - state = GEOSPreparedCovers_r(handle, gp_domain, g_point) ? POINT_IN : POINT_OUT; - GEOSGeom_destroy_r(handle, g_point); - } - else - { - state = POINT_NAN; - } - return state; -} - -/* - * Return whether the given line segment is suitable as an - * approximation of the projection of the source line. - * - * t_start: Interpolation parameter for the start point. - * p_start: Projected start point. - * t_end: Interpolation parameter for the end point. - * p_start: Projected end point. - * interpolator: Interpolator for current source line. - * threshold: Lateral tolerance in target projection coordinates. - * handle: Thread-local context handle for GEOS. - * gp_domain: Prepared polygon of target map domain. - * inside: Whether the start point is within the map domain. - */ -bool straightAndDomain(double t_start, const Point &p_start, - double t_end, const Point &p_end, - Interpolator *interpolator, double threshold, - GEOSContextHandle_t handle, - const GEOSPreparedGeometry *gp_domain, - bool inside) -{ - // Straight and in-domain (de9im[7] == 'F') - - bool valid; - - // This could be optimised out of the loop. - if (!(isfinite(p_start.x) && isfinite(p_start.y))) - { - valid = false; - } - else if (!(isfinite(p_end.x) && isfinite(p_end.y))) - { - valid = false; - } - else - { - // TODO: Re-use geometries, instead of create-destroy! - - // Create a LineString for the current end-point. - GEOSCoordSequence *coords = GEOSCoordSeq_create_r(handle, 2, 2); - GEOSCoordSeq_setX_r(handle, coords, 0, p_start.x); - GEOSCoordSeq_setY_r(handle, coords, 0, p_start.y); - GEOSCoordSeq_setX_r(handle, coords, 1, p_end.x); - GEOSCoordSeq_setY_r(handle, coords, 1, p_end.y); - GEOSGeometry *g_segment = GEOSGeom_createLineString_r(handle, coords); - - // Find the projected mid-point - double t_mid = (t_start + t_end) * 0.5; - Point p_mid = interpolator->interpolate(t_mid); - - // Make it into a GEOS geometry - coords = GEOSCoordSeq_create_r(handle, 1, 2); - GEOSCoordSeq_setX_r(handle, coords, 0, p_mid.x); - GEOSCoordSeq_setY_r(handle, coords, 0, p_mid.y); - GEOSGeometry *g_mid = GEOSGeom_createPoint_r(handle, coords); - - double along = GEOSProjectNormalized_r(handle, g_segment, g_mid); - if(isnan(along)) - { - valid = true; - } - else - { - valid = 0.0 < along && along < 1.0; - if (valid) - { - double separation; - GEOSDistance_r(handle, g_segment, g_mid, &separation); - if (inside) - { - // Scale the lateral threshold by the distance from - // the nearest end. I.e. Near the ends the lateral - // threshold is much smaller; it only has its full - // value in the middle. - valid = separation <= threshold * 2.0 * - (0.5 - fabs(0.5 - along)); - } - else - { - // Check if the mid-point makes less than ~11 degree - // angle with the straight line. - // sin(11') => 0.2 - // To save the square-root we just use the square of - // the lengths, hence: - // 0.2 ^ 2 => 0.04 - double hypot_dx = p_mid.x - p_start.x; - double hypot_dy = p_mid.y - p_start.y; - double hypot = hypot_dx * hypot_dx + hypot_dy * hypot_dy; - valid = ((separation * separation) / hypot) < 0.04; - } - } - } - - if (valid) - { - if(inside) - valid = GEOSPreparedCovers_r(handle, gp_domain, g_segment); - else - valid = GEOSPreparedDisjoint_r(handle, gp_domain, g_segment); - } - - GEOSGeom_destroy_r(handle, g_segment); - GEOSGeom_destroy_r(handle, g_mid); - } - - return valid; -} - -void bisect(double t_start, const Point &p_start, const Point &p_end, - GEOSContextHandle_t handle, - const GEOSPreparedGeometry *gp_domain, - State &state, Interpolator *interpolator, double threshold, - double &t_min, Point &p_min, double &t_max, Point &p_max) -{ - double t_current; - Point p_current; - - // Initialise our bisection range to the start and end points. - t_min = t_start; - p_min = p_start; - t_max = 1.0; - p_max = p_end; - - // Start the search at the end. - t_current = t_max; - p_current = p_max; - - // TODO: See if we can convert the 't' threshold into one based on the - // projected coordinates - e.g. the resulting line length. - // - - while (fabs(t_max - t_min) > 1.0e-6) - { -#ifdef DEBUG - std::cerr << "t: " << t_current << std::endl; -#endif - bool valid; - if (state == POINT_IN) - { - // Straight and entirely-inside-domain - valid = straightAndDomain(t_start, p_start, t_current, p_current, - interpolator, threshold, - handle, gp_domain, true); - } - else if(state == POINT_OUT) - { - // Straight and entirely-outside-domain - valid = straightAndDomain(t_start, p_start, t_current, p_current, - interpolator, threshold, - handle, gp_domain, false); - } - else - { - valid = (!isfinite(p_current.x)) || (!isfinite(p_current.y)); - } -#ifdef DEBUG - std::cerr << " => valid: " << valid << std::endl; -#endif - - if (valid) - { - t_min = t_current; - p_min = p_current; - } - else - { - t_max = t_current; - p_max = p_current; - } - - t_current = (t_min + t_max) * 0.5; - p_current = interpolator->interpolate(t_current); - } -} - -void _project_segment(GEOSContextHandle_t handle, - const GEOSCoordSequence *src_coords, - unsigned int src_idx_from, unsigned int src_idx_to, - Interpolator *interpolator, - const GEOSPreparedGeometry *gp_domain, - double threshold, - LineAccumulator &lines) -{ - Point p_current, p_min, p_max, p_end; - double t_current, t_min, t_max; - State state; - - GEOSCoordSeq_getX_r(handle, src_coords, src_idx_from, &p_current.x); - GEOSCoordSeq_getY_r(handle, src_coords, src_idx_from, &p_current.y); - GEOSCoordSeq_getX_r(handle, src_coords, src_idx_to, &p_end.x); - GEOSCoordSeq_getY_r(handle, src_coords, src_idx_to, &p_end.y); -#ifdef DEBUG - std::cerr << "Setting line:" << std::endl; - std::cerr << " " << p_current.x << ", " << p_current.y << std::endl; - std::cerr << " " << p_end.x << ", " << p_end.y << std::endl; -#endif - interpolator->set_line(p_current, p_end); - p_current = interpolator->project(p_current); - p_end = interpolator->project(p_end); -#ifdef DEBUG - std::cerr << "Projected as:" << std::endl; - std::cerr << " " << p_current.x << ", " << p_current.y << std::endl; - std::cerr << " " << p_end.x << ", " << p_end.y << std::endl; -#endif - - t_current = 0.0; - state = get_state(p_current, gp_domain, handle); - - size_t old_lines_size = lines.size(); - while(t_current < 1.0 && (lines.size() - old_lines_size) < 100) - { - //std::cerr << "Bisecting" << std::endl; -#ifdef DEBUG - std::cerr << "Working from: " << t_current << " ("; - if (state == POINT_IN) - std::cerr << "IN"; - else if (state == POINT_OUT) - std::cerr << "OUT"; - else - std::cerr << "NAN"; - std::cerr << ")" << std::endl; - std::cerr << " " << p_current.x << ", " << p_current.y << std::endl; - std::cerr << " " << p_end.x << ", " << p_end.y << std::endl; -#endif - bisect(t_current, p_current, p_end, handle, gp_domain, state, - interpolator, threshold, - t_min, p_min, t_max, p_max); -#ifdef DEBUG - std::cerr << " => " << t_min << " to " << t_max << std::endl; - std::cerr << " => (" << p_min.x << ", " << p_min.y << ") to (" << p_max.x << ", " << p_max.y << ")" << std::endl; -#endif - if (state == POINT_IN) - { - lines.add_point_if_empty(p_current); - if (t_min != t_current) - { - lines.add_point(p_min); - t_current = t_min; - p_current = p_min; - } - else - { - t_current = t_max; - p_current = p_max; - state = get_state(p_current, gp_domain, handle); - if(state == POINT_IN) - lines.new_line(); - } - } - else if(state == POINT_OUT) - { - if (t_min != t_current) - { - t_current = t_min; - p_current = p_min; - } - else - { - t_current = t_max; - p_current = p_max; - state = get_state(p_current, gp_domain, handle); - if(state == POINT_IN) - lines.new_line(); - } - } - else - { - t_current = t_max; - p_current = p_max; - state = get_state(p_current, gp_domain, handle); - if(state == POINT_IN) - lines.new_line(); - } - } -} - -GEOSGeometry *_project_line_string(GEOSContextHandle_t handle, - GEOSGeometry *g_line_string, - Interpolator *interpolator, - GEOSGeometry *g_domain, double threshold) -{ - const GEOSCoordSequence *src_coords = GEOSGeom_getCoordSeq_r(handle, g_line_string); - unsigned int src_size, src_idx; - - - const GEOSPreparedGeometry *gp_domain = GEOSPrepare_r(handle, g_domain); - - GEOSCoordSeq_getSize_r(handle, src_coords, &src_size); // check exceptions - - LineAccumulator lines; - - for(src_idx = 1; src_idx < src_size; src_idx++) - { - _project_segment(handle, src_coords, src_idx - 1, src_idx, - interpolator, gp_domain, threshold, lines); - } - - GEOSPreparedGeom_destroy_r(handle, gp_domain); - - return lines.as_geom(handle); -} diff -Nru python-cartopy-0.17.0+dfsg/lib/cartopy/_trace.h python-cartopy-0.18.0+dfsg/lib/cartopy/_trace.h --- python-cartopy-0.17.0+dfsg/lib/cartopy/_trace.h 2018-11-17 07:25:32.000000000 +0000 +++ python-cartopy-0.18.0+dfsg/lib/cartopy/_trace.h 1970-01-01 00:00:00.000000000 +0000 @@ -1,88 +0,0 @@ -/* -# (C) British Crown Copyright 2010 - 2016, 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 . -*/ - - -#ifndef _TRACE_H -#define _TRACE_H - -#include - -#include -#include - - -typedef struct { - double x; - double y; -} Point; - -typedef struct { - double x; - double y; - double z; -} Vec3; - - -class Interpolator -{ - public: - virtual ~Interpolator(); - virtual void set_line(const Point &start, const Point &end); - virtual Point interpolate(double t)=0; - virtual Point project(const Point &point)=0; - - protected: - Point m_start, m_end; -}; - - -class CartesianInterpolator : public Interpolator -{ - public: - CartesianInterpolator(projPJ src_proj, projPJ dest_proj); - Point interpolate(double t); - Point project(const Point &point); - - private: - projPJ m_src_proj, m_dest_proj; -}; - - -class SphericalInterpolator : public Interpolator -{ - public: - // XXX Move the constructor and members up to the superclass? - SphericalInterpolator(projPJ src_proj, projPJ dest_proj); - void set_line(const Point &start, const Point &end); - Point interpolate(double t); - Point project(const Point &point); - - private: - projPJ m_src_proj, m_dest_proj; - Vec3 m_start3, m_perp3; - double m_angle; -}; - - -GEOSGeometry *_project_line_string(GEOSContextHandle_t handle, - GEOSGeometry *g_line_string, - Interpolator *interpolator, - GEOSGeometry *g_domain, double threshold); - -#endif // _TRACE_H diff -Nru python-cartopy-0.17.0+dfsg/lib/cartopy/trace.pyx python-cartopy-0.18.0+dfsg/lib/cartopy/trace.pyx --- python-cartopy-0.17.0+dfsg/lib/cartopy/trace.pyx 2018-11-17 07:25:32.000000000 +0000 +++ python-cartopy-0.18.0+dfsg/lib/cartopy/trace.pyx 2020-05-03 08:12:47.000000000 +0000 @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2011 - 2018, Met Office +# (C) British Crown Copyright 2011 - 2019, Met Office # # This file is part of cartopy. # @@ -18,50 +18,60 @@ # cython: embedsignature=True """ -This module pulls together ``_trace.cpp``, proj, GEOS and ``_crs.pyx`` to -implement a function to project a `~shapely.geometry.LinearRing` / -`~shapely.geometry.LineString`. In general, this should never be called -manually, instead leaving the processing to be done by the -:class:`cartopy.crs.Projection` subclasses. +This module pulls together proj, GEOS and ``_crs.pyx`` to implement a function +to project a `~shapely.geometry.LinearRing` / `~shapely.geometry.LineString`. +In general, this should never be called manually, instead leaving the +processing to be done by the :class:`cartopy.crs.Projection` subclasses. """ +cimport cython +from libc.math cimport HUGE_VAL, sqrt +from numpy.math cimport isfinite, isnan from libc.stdint cimport uintptr_t as ptr +from libcpp cimport bool +from libcpp.list cimport list +from libcpp.vector cimport vector +cdef bool DEBUG = False cdef extern from "geos_c.h": ctypedef void *GEOSContextHandle_t ctypedef struct GEOSGeometry: pass + ctypedef struct GEOSCoordSequence + ctypedef struct GEOSPreparedGeometry + GEOSCoordSequence *GEOSCoordSeq_create_r(GEOSContextHandle_t, unsigned int, unsigned int) nogil + GEOSGeometry *GEOSGeom_createPoint_r(GEOSContextHandle_t, GEOSCoordSequence *) nogil + GEOSGeometry *GEOSGeom_createLineString_r(GEOSContextHandle_t, GEOSCoordSequence *) nogil + GEOSGeometry *GEOSGeom_createCollection_r(GEOSContextHandle_t, int, GEOSGeometry **, unsigned int) nogil + GEOSGeometry *GEOSGeom_createEmptyCollection_r(GEOSContextHandle_t, int) nogil + void GEOSGeom_destroy_r(GEOSContextHandle_t, GEOSGeometry *) nogil + GEOSCoordSequence *GEOSGeom_getCoordSeq_r(GEOSContextHandle_t, GEOSGeometry *) nogil + int GEOSCoordSeq_getSize_r(GEOSContextHandle_t handle, const GEOSCoordSequence* s, unsigned int *size) nogil + int GEOSCoordSeq_getX_r(GEOSContextHandle_t, GEOSCoordSequence *, int, double *) nogil + int GEOSCoordSeq_getY_r(GEOSContextHandle_t, GEOSCoordSequence *, int, double *) nogil + int GEOSCoordSeq_setX_r(GEOSContextHandle_t, GEOSCoordSequence *, int, double) nogil + int GEOSCoordSeq_setY_r(GEOSContextHandle_t, GEOSCoordSequence *, int, double) nogil + const GEOSPreparedGeometry *GEOSPrepare_r(GEOSContextHandle_t handle, const GEOSGeometry* g) nogil + char GEOSPreparedCovers_r(GEOSContextHandle_t, const GEOSPreparedGeometry*, const GEOSGeometry*) nogil + char GEOSPreparedDisjoint_r(GEOSContextHandle_t, const GEOSPreparedGeometry*, const GEOSGeometry*) nogil + void GEOSPreparedGeom_destroy_r(GEOSContextHandle_t handle, const GEOSPreparedGeometry* g) nogil + cdef int GEOS_MULTILINESTRING from cartopy._crs cimport CRS +from cartopy._crs import PROJ4_VERSION +from ._proj4 cimport (projPJ, projLP, pj_get_spheroid_defn, pj_transform, + pj_strerrno, DEG_TO_RAD) +from .geodesic._geodesic cimport (geod_geodesic, geod_geodesicline, + geod_init, geod_geninverse, + geod_lineinit, geod_genposition, + GEOD_ARCMODE, GEOD_LATITUDE, GEOD_LONGITUDE) import shapely.geometry as sgeom from shapely.geos import lgeos -cdef extern from "proj_api.h": - ctypedef void *projPJ - - -cdef extern from "_trace.h": - cdef cppclass Interpolator: - pass - - cdef cppclass SphericalInterpolator: - SphericalInterpolator(projPJ src_proj, projPJ dest_proj) - - cdef cppclass CartesianInterpolator: - CartesianInterpolator(projPJ src_proj, projPJ dest_proj) - - # XXX Rename? It handles LinearRings too. - GEOSGeometry *_project_line_string(GEOSContextHandle_t handle, - GEOSGeometry *g_line_string, - Interpolator *interpolator, - GEOSGeometry *g_domain, - double threshold) - - cdef GEOSContextHandle_t get_geos_context_handle(): cdef ptr handle = lgeos.geos_handle return handle @@ -75,15 +85,491 @@ cdef shapely_from_geos(GEOSGeometry *geom): """Turn the given GEOS geometry pointer into a shapely geometry.""" - # This is the "correct" way to do it... - # return geom_factory(geom) - # ... but it's quite slow, so we do it by hand. - multi_line_string = sgeom.base.BaseGeometry() - multi_line_string.__class__ = sgeom.MultiLineString - multi_line_string.__geom__ = geom - multi_line_string.__parent__ = None - multi_line_string._ndim = 2 - return multi_line_string + return sgeom.base.geom_factory(geom) + + +ctypedef struct Point: + double x + double y + +ctypedef list[Point] Line + + +cdef bool degenerate_line(const Line &value): + return value.size() < 2 + + +cdef bool close(double a, double b): + return abs(a - b) <= (1e-8 + 1e-5 * abs(b)) + + +@cython.final +cdef class LineAccumulator: + cdef list[Line] lines + + def __init__(self): + self.new_line() + + cdef void new_line(self): + cdef Line line + self.lines.push_back(line) + + cdef void add_point(self, const Point &point): + self.lines.back().push_back(point) + + cdef void add_point_if_empty(self, const Point &point): + if self.lines.back().empty(): + self.add_point(point) + + cdef GEOSGeometry *as_geom(self, GEOSContextHandle_t handle): + from cython.operator cimport dereference, preincrement + # self.lines.remove_if(degenerate_line) is not available in Cython. + cdef list[Line].iterator it = self.lines.begin() + while it != self.lines.end(): + if degenerate_line(dereference(it)): + it = self.lines.erase(it) + else: + preincrement(it) + + cdef Point first, last + if self.lines.size() > 1: + first = self.lines.front().front() + last = self.lines.back().back() + if close(first.x, last.x) and close(first.y, last.y): + self.lines.front().pop_front() + self.lines.back().splice(self.lines.back().end(), + self.lines.front()) + self.lines.pop_front() + + cdef Line ilines + cdef Point ipoints + cdef vector[GEOSGeometry *] geoms + cdef int i + cdef GEOSCoordSequence *coords + for ilines in self.lines: + coords = GEOSCoordSeq_create_r(handle, ilines.size(), 2) + for i, ipoints in enumerate(ilines): + GEOSCoordSeq_setX_r(handle, coords, i, ipoints.x) + GEOSCoordSeq_setY_r(handle, coords, i, ipoints.y) + + geoms.push_back(GEOSGeom_createLineString_r(handle, coords)) + + cdef GEOSGeometry *geom + if geoms.empty(): + geom = GEOSGeom_createEmptyCollection_r(handle, + GEOS_MULTILINESTRING) + else: + geom = GEOSGeom_createCollection_r(handle, GEOS_MULTILINESTRING, + &geoms[0], geoms.size()) + return geom + + cdef size_t size(self): + return self.lines.size() + + +cdef class Interpolator: + cdef Point start + cdef Point end + cdef projPJ src_proj + cdef projPJ dest_proj + cdef double src_scale + cdef double dest_scale + + def __cinit__(self): + self.src_scale = 1 + self.dest_scale = 1 + + cdef void init(self, projPJ src_proj, projPJ dest_proj): + self.src_proj = src_proj + self.dest_proj = dest_proj + + cdef void set_line(self, const Point &start, const Point &end): + self.start = start + self.end = end + + cdef Point interpolate(self, double t): + raise NotImplementedError + + cdef Point project(self, const Point &point): + raise NotImplementedError + + +cdef class CartesianInterpolator(Interpolator): + cdef Point interpolate(self, double t): + cdef Point xy + xy.x = self.start.x + (self.end.x - self.start.x) * t + xy.y = self.start.y + (self.end.y - self.start.y) * t + return self.project(xy) + + cdef Point project(self, const Point &src_xy): + cdef Point dest_xy + cdef projLP xy + + xy.u = src_xy.x * self.src_scale + xy.v = src_xy.y * self.src_scale + + cdef int status = pj_transform(self.src_proj, self.dest_proj, + 1, 1, &xy.u, &xy.v, NULL) + if status in (-14, -20): + # -14 => "latitude or longitude exceeded limits" + # -20 => "tolerance condition error" + xy.u = xy.v = HUGE_VAL + elif status != 0: + raise Exception('pj_transform failed: %d\n%s' % ( + status, + pj_strerrno(status))) + + dest_xy.x = xy.u * self.dest_scale + dest_xy.y = xy.v * self.dest_scale + return dest_xy + + +cdef class SphericalInterpolator(Interpolator): + cdef geod_geodesic geod + cdef geod_geodesicline geod_line + cdef double a13 + + cdef void init(self, projPJ src_proj, projPJ dest_proj): + self.src_proj = src_proj + self.dest_proj = dest_proj + + cdef double major_axis + cdef double eccentricity_squared + pj_get_spheroid_defn(self.src_proj, &major_axis, &eccentricity_squared) + geod_init(&self.geod, major_axis, 1 - sqrt(1 - eccentricity_squared)) + + cdef void set_line(self, const Point &start, const Point &end): + cdef double azi1 + self.a13 = geod_geninverse(&self.geod, + start.y, start.x, end.y, end.x, + NULL, &azi1, NULL, NULL, NULL, NULL, NULL) + geod_lineinit(&self.geod_line, &self.geod, start.y, start.x, azi1, + GEOD_LATITUDE | GEOD_LONGITUDE); + + cdef Point interpolate(self, double t): + cdef Point lonlat + + geod_genposition(&self.geod_line, GEOD_ARCMODE, self.a13 * t, + &lonlat.y, &lonlat.x, NULL, NULL, NULL, NULL, NULL, + NULL) + + return self.project(lonlat) + + cdef Point project(self, const Point &lonlat): + cdef Point xy + cdef projLP dest + + dest.u = (lonlat.x * DEG_TO_RAD) * self.src_scale + dest.v = (lonlat.y * DEG_TO_RAD) * self.src_scale + + cdef int status = pj_transform(self.src_proj, self.dest_proj, + 1, 1, &dest.u, &dest.v, NULL) + if status in (-14, -20): + # -14 => "latitude or longitude exceeded limits" + # -20 => "tolerance condition error" + dest.u = dest.v = HUGE_VAL + elif status != 0: + raise Exception('pj_transform failed: %d\n%s' % ( + status, + pj_strerrno(status))) + + xy.x = dest.u * self.dest_scale + xy.y = dest.v * self.dest_scale + return xy + + +cdef enum State: + POINT_IN = 1, + POINT_OUT, + POINT_NAN + + +cdef State get_state(const Point &point, const GEOSPreparedGeometry *gp_domain, + GEOSContextHandle_t handle): + cdef State state + cdef GEOSCoordSequence *coords + cdef GEOSGeometry *g_point + + if isfinite(point.x) and isfinite(point.y): + # TODO: Avoid create-destroy + coords = GEOSCoordSeq_create_r(handle, 1, 2) + GEOSCoordSeq_setX_r(handle, coords, 0, point.x) + GEOSCoordSeq_setY_r(handle, coords, 0, point.y) + g_point = GEOSGeom_createPoint_r(handle, coords) + state = (POINT_IN + if GEOSPreparedCovers_r(handle, gp_domain, g_point) + else POINT_OUT) + GEOSGeom_destroy_r(handle, g_point) + else: + state = POINT_NAN + return state + + +@cython.cdivision(True) # Want divide-by-zero to produce NaN. +cdef bool straightAndDomain(double t_start, const Point &p_start, + double t_end, const Point &p_end, + Interpolator interpolator, double threshold, + GEOSContextHandle_t handle, + const GEOSPreparedGeometry *gp_domain, + bool inside): + """ + Return whether the given line segment is suitable as an + approximation of the projection of the source line. + + t_start: Interpolation parameter for the start point. + p_start: Projected start point. + t_end: Interpolation parameter for the end point. + p_start: Projected end point. + interpolator: Interpolator for current source line. + threshold: Lateral tolerance in target projection coordinates. + handle: Thread-local context handle for GEOS. + gp_domain: Prepared polygon of target map domain. + inside: Whether the start point is within the map domain. + + """ + # Straight and in-domain (de9im[7] == 'F') + cdef bool valid + cdef double t_mid + cdef Point p_mid + cdef double seg_dx, seg_dy + cdef double mid_dx, mid_dy + cdef double seg_hypot_sq + cdef double along + cdef double separation + cdef double hypot + cdef GEOSCoordSequence *coords + cdef GEOSGeometry *g_segment + + # This could be optimised out of the loop. + if not (isfinite(p_start.x) and isfinite(p_start.y)): + valid = False + elif not (isfinite(p_end.x) and isfinite(p_end.y)): + valid = False + else: + # Find the projected mid-point + t_mid = (t_start + t_end) * 0.5 + p_mid = interpolator.interpolate(t_mid) + + # Determine the closest point on the segment to the midpoint, in + # normalized coordinates. We could use GEOSProjectNormalized_r + # here, but since it's a single line segment, it's much easier to + # just do the math ourselves: + # ○̩ (x1, y1) (assume that this is not necessarily vertical) + # │ + # │ D + # ╭├───────○ (x, y) + # ┊│┘ ╱ + # ┊│ ╱ + # ┊│ ╱ + # L│ ╱ + # ┊│ ╱ + # ┊│θ╱ + # ┊│╱ + # ╰̍○̍ + # (x0, y0) + # The angle θ can be found by arctan2: + # θ = arctan2(y1 - y0, x1 - x0) - arctan2(y - y0, x - x0) + # and the projection onto the line is simply: + # L = hypot(x - x0, y - y0) * cos(θ) + # with the normalized form being: + # along = L / hypot(x1 - x0, y1 - y0) + # + # Plugging those into SymPy and .expand().simplify(), we get the + # following equations (with a slight refactoring to reuse some + # intermediate values): + seg_dx = p_end.x - p_start.x + seg_dy = p_end.y - p_start.y + mid_dx = p_mid.x - p_start.x + mid_dy = p_mid.y - p_start.y + seg_hypot_sq = seg_dx*seg_dx + seg_dy*seg_dy + + along = (seg_dx*mid_dx + seg_dy*mid_dy) / seg_hypot_sq + + if isnan(along): + valid = True + else: + valid = 0.0 < along < 1.0 + if valid: + # For the distance of the point from the line segment, using + # the same geometry above, use sin instead of cos: + # D = hypot(x - x0, y - y0) * sin(θ) + # and then simplify with SymPy again: + separation = (abs(mid_dx*seg_dy - mid_dy*seg_dx) / + sqrt(seg_hypot_sq)) + if inside: + # Scale the lateral threshold by the distance from + # the nearest end. I.e. Near the ends the lateral + # threshold is much smaller; it only has its full + # value in the middle. + valid = (separation <= + threshold * 2.0 * (0.5 - abs(0.5 - along))) + else: + # Check if the mid-point makes less than ~11 degree + # angle with the straight line. + # sin(11') => 0.2 + # To save the square-root we just use the square of + # the lengths, hence: + # 0.2 ^ 2 => 0.04 + hypot = mid_dx*mid_dx + mid_dy*mid_dy + valid = ((separation * separation) / hypot) < 0.04 + + if valid: + # TODO: Re-use geometries, instead of create-destroy! + + # Create a LineString for the current end-point. + coords = GEOSCoordSeq_create_r(handle, 2, 2) + GEOSCoordSeq_setX_r(handle, coords, 0, p_start.x) + GEOSCoordSeq_setY_r(handle, coords, 0, p_start.y) + GEOSCoordSeq_setX_r(handle, coords, 1, p_end.x) + GEOSCoordSeq_setY_r(handle, coords, 1, p_end.y) + g_segment = GEOSGeom_createLineString_r(handle, coords) + + if inside: + valid = GEOSPreparedCovers_r(handle, gp_domain, g_segment) + else: + valid = GEOSPreparedDisjoint_r(handle, gp_domain, g_segment) + + GEOSGeom_destroy_r(handle, g_segment) + + return valid + + +cdef void bisect(double t_start, const Point &p_start, const Point &p_end, + GEOSContextHandle_t handle, + const GEOSPreparedGeometry *gp_domain, const State &state, + Interpolator interpolator, double threshold, + double &t_min, Point &p_min, double &t_max, Point &p_max): + cdef double t_current + cdef Point p_current + cdef bool valid + + # Initialise our bisection range to the start and end points. + (&t_min)[0] = t_start + (&p_min)[0] = p_start + (&t_max)[0] = 1.0 + (&p_max)[0] = p_end + + # Start the search at the end. + t_current = t_max + p_current = p_max + + # TODO: See if we can convert the 't' threshold into one based on the + # projected coordinates - e.g. the resulting line length. + + while abs(t_max - t_min) > 1.0e-6: + if DEBUG: + print("t: ", t_current) + + if state == POINT_IN: + # Straight and entirely-inside-domain + valid = straightAndDomain(t_start, p_start, t_current, p_current, + interpolator, threshold, + handle, gp_domain, True) + + elif state == POINT_OUT: + # Straight and entirely-outside-domain + valid = straightAndDomain(t_start, p_start, t_current, p_current, + interpolator, threshold, + handle, gp_domain, False) + else: + valid = not isfinite(p_current.x) or not isfinite(p_current.y) + + if DEBUG: + print(" => valid: ", valid) + + if valid: + (&t_min)[0] = t_current + (&p_min)[0] = p_current + else: + (&t_max)[0] = t_current + (&p_max)[0] = p_current + + t_current = (t_min + t_max) * 0.5 + p_current = interpolator.interpolate(t_current) + + +cdef void _project_segment(GEOSContextHandle_t handle, + const GEOSCoordSequence *src_coords, + unsigned int src_idx_from, unsigned int src_idx_to, + Interpolator interpolator, + const GEOSPreparedGeometry *gp_domain, + double threshold, LineAccumulator lines): + cdef Point p_current, p_min, p_max, p_end + cdef double t_current, t_min, t_max + cdef State state + + GEOSCoordSeq_getX_r(handle, src_coords, src_idx_from, &p_current.x) + GEOSCoordSeq_getY_r(handle, src_coords, src_idx_from, &p_current.y) + GEOSCoordSeq_getX_r(handle, src_coords, src_idx_to, &p_end.x) + GEOSCoordSeq_getY_r(handle, src_coords, src_idx_to, &p_end.y) + if DEBUG: + print("Setting line:") + print(" ", p_current.x, ", ", p_current.y) + print(" ", p_end.x, ", ", p_end.y) + + interpolator.set_line(p_current, p_end) + p_current = interpolator.project(p_current) + p_end = interpolator.project(p_end) + if DEBUG: + print("Projected as:") + print(" ", p_current.x, ", ", p_current.y) + print(" ", p_end.x, ", ", p_end.y) + + t_current = 0.0 + state = get_state(p_current, gp_domain, handle) + + cdef size_t old_lines_size = lines.size() + while t_current < 1.0 and (lines.size() - old_lines_size) < 100: + if DEBUG: + print("Bisecting from: ", t_current, " (") + if state == POINT_IN: + print("IN") + elif state == POINT_OUT: + print("OUT") + else: + print("NAN") + print(")") + print(" ", p_current.x, ", ", p_current.y) + print(" ", p_end.x, ", ", p_end.y) + + bisect(t_current, p_current, p_end, handle, gp_domain, state, + interpolator, threshold, + t_min, p_min, t_max, p_max) + if DEBUG: + print(" => ", t_min, "to", t_max) + print(" => (", p_min.x, ", ", p_min.y, ") to (", + p_max.x, ", ", p_max.y, ")") + + if state == POINT_IN: + lines.add_point_if_empty(p_current) + if t_min != t_current: + lines.add_point(p_min) + t_current = t_min + p_current = p_min + else: + t_current = t_max + p_current = p_max + state = get_state(p_current, gp_domain, handle) + if state == POINT_IN: + lines.new_line() + + elif state == POINT_OUT: + if t_min != t_current: + t_current = t_min + p_current = p_min + else: + t_current = t_max + p_current = p_max + state = get_state(p_current, gp_domain, handle) + if state == POINT_IN: + lines.new_line() + + else: + t_current = t_max + p_current = p_max + state = get_state(p_current, gp_domain, handle) + if state == POINT_IN: + lines.new_line() def project_linear(geometry not None, CRS src_crs not None, @@ -111,21 +597,48 @@ double threshold = dest_projection.threshold GEOSContextHandle_t handle = get_geos_context_handle() GEOSGeometry *g_linear = geos_from_shapely(geometry) - Interpolator *interpolator + Interpolator interpolator GEOSGeometry *g_domain + const GEOSCoordSequence *src_coords + unsigned int src_size, src_idx + const GEOSPreparedGeometry *gp_domain + LineAccumulator lines GEOSGeometry *g_multi_line_string g_domain = geos_from_shapely(dest_projection.domain) if src_crs.is_geodetic(): - interpolator = new SphericalInterpolator( - src_crs.proj4, (dest_projection).proj4) + interpolator = SphericalInterpolator() else: - interpolator = new CartesianInterpolator( - src_crs.proj4, (dest_projection).proj4) + interpolator = CartesianInterpolator() + interpolator.init(src_crs.proj4, (dest_projection).proj4) + if (6, 1, 1) <= PROJ4_VERSION < (6, 3, 0): + # Workaround bug in Proj 6.1.1+ with +to_meter on +proj=ob_tran. + # See https://github.com/OSGeo/proj#1782. + lonlat = ('latlon', 'latlong', 'lonlat', 'longlat') + if (src_crs.proj4_params.get('proj', '') == 'ob_tran' and + src_crs.proj4_params.get('o_proj', '') in lonlat and + 'to_meter' in src_crs.proj4_params): + interpolator.src_scale = src_crs.proj4_params['to_meter'] + if (dest_projection.proj4_params.get('proj', '') == 'ob_tran' and + dest_projection.proj4_params.get('o_proj', '') in lonlat and + 'to_meter' in dest_projection.proj4_params): + interpolator.dest_scale = 1 / dest_projection.proj4_params['to_meter'] + + src_coords = GEOSGeom_getCoordSeq_r(handle, g_linear) + gp_domain = GEOSPrepare_r(handle, g_domain) + + GEOSCoordSeq_getSize_r(handle, src_coords, &src_size) # check exceptions + + lines = LineAccumulator() + for src_idx in range(1, src_size): + _project_segment(handle, src_coords, src_idx - 1, src_idx, + interpolator, gp_domain, threshold, lines); + + GEOSPreparedGeom_destroy_r(handle, gp_domain) + + g_multi_line_string = lines.as_geom(handle) - g_multi_line_string = _project_line_string(handle, g_linear, - interpolator, g_domain, threshold) - del interpolator + del lines, interpolator multi_line_string = shapely_from_geos(g_multi_line_string) return multi_line_string diff -Nru python-cartopy-0.17.0+dfsg/lib/cartopy/_version.py python-cartopy-0.18.0+dfsg/lib/cartopy/_version.py --- python-cartopy-0.17.0+dfsg/lib/cartopy/_version.py 2018-11-17 07:25:32.000000000 +0000 +++ python-cartopy-0.18.0+dfsg/lib/cartopy/_version.py 2020-05-03 08:12:47.000000000 +0000 @@ -23,8 +23,8 @@ # setup.py/versioneer.py will grep for the variable names, so they must # each be defined on a line of their own. _version.py will just call # get_keywords(). - git_refnames = " (tag: v0.17.0)" - git_full = "15cb9437dc92b2a7319cd9ae7876e4505a2bf7cb" + git_refnames = " (tag: v0.18.0, refs/pull/1548/head, v0.18.x)" + git_full = "178a15e39d085758832d30aa9f77f34f708a7d1e" keywords = {"refnames": git_refnames, "full": git_full} return keywords diff -Nru python-cartopy-0.17.0+dfsg/MANIFEST.in python-cartopy-0.18.0+dfsg/MANIFEST.in --- python-cartopy-0.17.0+dfsg/MANIFEST.in 2018-11-17 07:25:32.000000000 +0000 +++ python-cartopy-0.18.0+dfsg/MANIFEST.in 2020-05-03 08:12:47.000000000 +0000 @@ -3,11 +3,10 @@ include COPYING* include INSTALL include README.rst -include pyproject.toml include requirements/*.txt include lib/cartopy/data/* include lib/cartopy/io/srtm.npz include lib/cartopy/tests/lakes_shapefile/* recursive-include lib *.py -recursive-include lib *.pyx *.pxd *.h +recursive-include lib *.pyx *.pxd *.h *.c *.cpp include versioneer.py diff -Nru python-cartopy-0.17.0+dfsg/pyproject.toml python-cartopy-0.18.0+dfsg/pyproject.toml --- python-cartopy-0.17.0+dfsg/pyproject.toml 2018-11-17 07:25:32.000000000 +0000 +++ python-cartopy-0.18.0+dfsg/pyproject.toml 1970-01-01 00:00:00.000000000 +0000 @@ -1,2 +0,0 @@ -[build-system] -requires = ["setuptools", "wheel", "numpy>=1.10", "Cython>=0.15.1"] diff -Nru python-cartopy-0.17.0+dfsg/README.md python-cartopy-0.18.0+dfsg/README.md --- python-cartopy-0.17.0+dfsg/README.md 2018-11-17 07:25:32.000000000 +0000 +++ python-cartopy-0.18.0+dfsg/README.md 2020-05-03 08:12:47.000000000 +0000 @@ -1,6 +1,6 @@

- - + Cartopy

@@ -68,7 +68,7 @@ - powerful vector data handling by integrating shapefile reading with Shapely capabilities -Documentation can be found at . +Documentation can be found at . ## Get in touch @@ -82,12 +82,21 @@ - To chat with developers and other users you can use the [Gitter Chat](https://gitter.im/SciTools/cartopy) -## License and copyright -Cartopy is licensed under GNU Lesser General Public License (LGPLv3). +## Credits, copyright and license -Development occurs on GitHub at , with a -contributor's license agreement (CLA) that can be found at -. +Cartopy is developed collaboratively under the SciTools umberella. -(C) British Crown Copyright, Met Office +A full list of codecontributors ("Cartopy contributors") can be found at +https://github.com/SciTools/cartopy/graphs/contributors. + +Code is just one of many ways of positively contributing to Cartopy, please see +our [contributing guide](.github/CONTRIBUTING.md) for more details on how +you can get involved. + +Cartopy is released under a LGPL license with a shared copyright model. +See [COPYING](COPYING) and [COPYING.LESSER](COPYING.LESSER) for full terms. + +The [Met Office](https://metoffice.gov.uk) has made a significant +contribution to the development, maintenance and support of this library. +All Met Office contributions are copyright on behalf of the British Crown. diff -Nru python-cartopy-0.17.0+dfsg/requirements/default.txt python-cartopy-0.18.0+dfsg/requirements/default.txt --- python-cartopy-0.17.0+dfsg/requirements/default.txt 2018-11-17 07:25:32.000000000 +0000 +++ python-cartopy-0.18.0+dfsg/requirements/default.txt 2020-05-03 08:12:47.000000000 +0000 @@ -3,3 +3,4 @@ pyshp>=1.1.4 six>=1.3.0 setuptools>=0.7.2 +futures; python_version == "2.7" diff -Nru python-cartopy-0.17.0+dfsg/requirements/epsg.txt python-cartopy-0.18.0+dfsg/requirements/epsg.txt --- python-cartopy-0.17.0+dfsg/requirements/epsg.txt 2018-11-17 07:25:32.000000000 +0000 +++ python-cartopy-0.18.0+dfsg/requirements/epsg.txt 2020-05-03 08:12:47.000000000 +0000 @@ -1 +1 @@ -pyepsg>=0.2.0 +pyepsg>=0.4.0 diff -Nru python-cartopy-0.17.0+dfsg/setup.cfg python-cartopy-0.18.0+dfsg/setup.cfg --- python-cartopy-0.17.0+dfsg/setup.cfg 2018-11-17 07:25:32.000000000 +0000 +++ python-cartopy-0.18.0+dfsg/setup.cfg 2020-05-03 08:12:47.000000000 +0000 @@ -1,21 +1,15 @@ [flake8] ignore = E402,\ # Due to conditional imports E226,\ # Due to whitespace around operators (e.g. 2*x + 3) - E241 # Due to multiple spaces after comma + E241,\ # Due to multiple spaces after comma + W504 # Line break after binary operator exclude = \ build, \ setup.py, \ docs/source/conf.py, \ - docs/source/sphinxext/plot_directive.py, \ versioneer.py -[tool:pytest] -markers = - natural_earth: mark tests that use Natural Earth data, and the network, if not cached. - network: mark tests that use the network. -doctest_optionflags = ELLIPSIS NORMALIZE_WHITESPACE - [versioneer] # See the docstring in versioneer.py for instructions. Note that you must # re-run 'versioneer.py setup' after changing this section, and commit the diff -Nru python-cartopy-0.17.0+dfsg/setup.py python-cartopy-0.18.0+dfsg/setup.py --- python-cartopy-0.17.0+dfsg/setup.py 2018-11-17 07:25:32.000000000 +0000 +++ python-cartopy-0.18.0+dfsg/setup.py 2020-05-03 08:12:47.000000000 +0000 @@ -1,45 +1,44 @@ -# (C) British Crown Copyright 2011 - 2018, Met Office +# Copyright Cartopy Contributors # -# 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 . -from __future__ import print_function - -""" -Distribution definition for Cartopy. +# 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. -""" +from __future__ import print_function -import setuptools -from setuptools import setup, Extension -from setuptools import Command -from setuptools import convert_path -from distutils.spawn import find_executable -from distutils.sysconfig import get_config_var import fnmatch import os import subprocess import sys import warnings +from collections import defaultdict +from distutils.spawn import find_executable +from distutils.sysconfig import get_config_var + +from setuptools import Command, Extension, convert_path, setup import versioneer +""" +Distribution definition for Cartopy. + +""" + +# The existence of a PKG-INFO directory is enough to tell us whether this is a +# source installation or not (sdist). +HERE = os.path.dirname(__file__) +IS_SDIST = os.path.exists(os.path.join(HERE, 'PKG-INFO')) +FORCE_CYTHON = os.environ.get('FORCE_CYTHON', False) + +if not IS_SDIST or FORCE_CYTHON: + import Cython + if Cython.__version__ < '0.28': + raise ImportError( + "Cython 0.28+ is required to install cartopy from source.") + + from Cython.Distutils import build_ext as cy_build_ext + -try: - from Cython.Distutils import build_ext -except ImportError: - raise ImportError('Cython 0.15.1+ is required to install cartopy.') try: import numpy as np except ImportError: @@ -52,8 +51,6 @@ GEOS_MIN_VERSION = (3, 3, 3) PROJ_MIN_VERSION = (4, 9, 0) -HERE = os.path.dirname(__file__) - def file_walk_relative(top, remove=''): """ @@ -93,75 +90,14 @@ return packages -class MissingHeaderError(Exception): - """ - Raised when one or more files do not have the required copyright - and licence header. - - """ - pass - - -class HeaderCheck(Command): - """ - Checks that all the necessary files have the copyright and licence - header. - - """ - - description = "check for copyright/licence headers" - user_options = [] - - exclude_patterns = ('./setup.py', - './build/*', - './docs/build/*', - './dist/*', - './lib/cartopy/examples/*.py') - - def initialize_options(self): - pass - - def finalize_options(self): - pass - - def run(self): - check_paths = [] - for root, dirs, files in os.walk('.'): - for file in files: - if file.endswith('.py') or file.endswith('.c'): - path = os.path.join(root, file) - check_paths.append(path) - - for pattern in self.exclude_patterns: - exclude = lambda path: not fnmatch.fnmatch(path, pattern) - check_paths = list(filter(exclude, check_paths)) - - bad_paths = list(filter(self._header_bad, check_paths)) - if bad_paths: - raise MissingHeaderError(bad_paths) - - def _header_bad(self, path): - target = '(C) British Crown Copyright 2011 - 2012, Met Office' - with open(path, 'rt') as text_file: - # Check for the header on the first line. - line = text_file.readline().rstrip() - bad = target not in line - - # Check if it was an executable script, with the header - # starting on the second line. - if bad and line == '#!/usr/bin/env python': - line = text_file.readline().rstrip() - bad = target not in line - return bad - - # Dependency checks # ================= # GEOS try: geos_version = subprocess.check_output(['geos-config', '--version']) - geos_version = tuple(int(v) for v in geos_version.split(b'.')) + geos_version = tuple(int(v) for v in geos_version.split(b'.') + if 'dev' not in str(v)) geos_includes = subprocess.check_output(['geos-config', '--includes']) geos_clibs = subprocess.check_output(['geos-config', '--clibs']) except (OSError, ValueError, subprocess.CalledProcessError): @@ -172,10 +108,7 @@ geos_includes = [] geos_library_dirs = [] - if sys.platform.startswith('win'): - geos_libraries = ['geos'] - else: - geos_libraries = ['geos_c'] + geos_libraries = ['geos_c'] else: if geos_version < GEOS_MIN_VERSION: print('GEOS version %s is installed, but cartopy requires at least ' @@ -230,6 +163,18 @@ return proj_version +def get_proj_libraries(): + """ + This function gets the PROJ libraries to cythonize with + """ + proj_libraries = ["proj"] + if os.name == "nt" and (6, 0, 0) <= proj_version < (6, 3, 0): + proj_libraries = [ + "proj_{}_{}".format(proj_version[0], proj_version[1]) + ] + return proj_libraries + + conda = os.getenv('CONDA_DEFAULT_ENV') if conda is not None and conda in sys.prefix: # Conda does not provide pkg-config compatibility, but the search paths @@ -245,7 +190,7 @@ exit(1) proj_includes = [] - proj_libraries = ['proj'] + proj_libraries = get_proj_libraries() proj_library_dirs = [] else: @@ -268,7 +213,7 @@ exit(1) proj_includes = [] - proj_libraries = ['proj'] + proj_libraries = get_proj_libraries() proj_library_dirs = [] else: if proj_version < PROJ_MIN_VERSION: @@ -283,8 +228,9 @@ proj_includes = proj_includes.decode() proj_clibs = proj_clibs.decode() - proj_includes = [proj_include[2:] if proj_include.startswith('-I') else - proj_include for proj_include in proj_includes.split()] + proj_includes = [ + proj_include[2:] if proj_include.startswith('-I') else + proj_include for proj_include in proj_includes.split()] proj_libraries = [] proj_library_dirs = [] @@ -308,7 +254,7 @@ else: extras_require[section].append(line.strip()) install_requires = extras_require.pop('default') -tests_require = extras_require.pop('tests', []) +tests_require = extras_require.get('tests', []) # General extension paths if sys.platform.startswith('win'): @@ -316,21 +262,90 @@ return '.' include_dir = get_config_var('INCLUDEDIR') library_dir = get_config_var('LIBDIR') -if sys.platform.startswith('win'): - extra_extension_args = {} -else: - extra_extension_args = dict( - runtime_library_dirs=[get_config_var('LIBDIR')]) +extra_extension_args = defaultdict(list) +if not sys.platform.startswith('win'): + extra_extension_args["runtime_library_dirs"].append( + get_config_var('LIBDIR') + ) # Description # =========== - with open(os.path.join(HERE, 'README.md'), 'r') as fh: description = ''.join(fh.readlines()) +cython_coverage_enabled = os.environ.get('CYTHON_COVERAGE', None) +if proj_version >= (6, 0, 0): + extra_extension_args["define_macros"].append( + ('ACCEPT_USE_OF_DEPRECATED_PROJ_API_H', '1') + ) +if cython_coverage_enabled: + extra_extension_args["define_macros"].append( + ('CYTHON_TRACE_NOGIL', '1') + ) + +extensions = [ + Extension( + 'cartopy.trace', + ['lib/cartopy/trace.pyx'], + include_dirs=([include_dir, './lib/cartopy', np.get_include()] + + proj_includes + geos_includes), + libraries=proj_libraries + geos_libraries, + library_dirs=[library_dir] + proj_library_dirs + geos_library_dirs, + language='c++', + **extra_extension_args), + Extension( + 'cartopy._crs', + ['lib/cartopy/_crs.pyx'], + include_dirs=[include_dir, np.get_include()] + proj_includes, + libraries=proj_libraries, + library_dirs=[library_dir] + proj_library_dirs, + **extra_extension_args), + # Requires proj v4.9 + Extension( + 'cartopy.geodesic._geodesic', + ['lib/cartopy/geodesic/_geodesic.pyx'], + include_dirs=[include_dir, np.get_include()] + proj_includes, + libraries=proj_libraries, + library_dirs=[library_dir] + proj_library_dirs, + **extra_extension_args), +] + + +if cython_coverage_enabled: + # We need to explicitly cythonize the extension in order + # to control the Cython compiler_directives. + from Cython.Build import cythonize + + directives = {'linetrace': True, + 'binding': True} + extensions = cythonize(extensions, compiler_directives=directives) + + +def decythonize(extensions, **_ignore): + # Remove pyx sources from extensions. + # Note: even if there are changes to the pyx files, they will be ignored. + for extension in extensions: + sources = [] + for sfile in extension.sources: + path, ext = os.path.splitext(sfile) + if ext in ('.pyx',): + if extension.language == 'c++': + ext = '.cpp' + else: + ext = '.c' + sfile = path + ext + sources.append(sfile) + extension.sources[:] = sources + return extensions + + cmdclass = versioneer.get_cmdclass() -cmdclass.update({'build_ext': build_ext}) + +if IS_SDIST and not FORCE_CYTHON: + extensions = decythonize(extensions) +else: + cmdclass.update({'build_ext': cy_build_ext}) # Main setup @@ -338,7 +353,7 @@ setup( name='Cartopy', version=versioneer.get_version(), - url='http://scitools.org.uk/cartopy/docs/latest/', + url='https://scitools.org.uk/cartopy/docs/latest/', download_url='https://github.com/SciTools/cartopy', author='UK Met Office', description='A cartographic python library with Matplotlib support for ' @@ -371,39 +386,12 @@ # requires proj headers - ext_modules=[ - Extension( - 'cartopy.trace', - ['lib/cartopy/trace.pyx', 'lib/cartopy/_trace.cpp'], - include_dirs=[include_dir, - './lib/cartopy'] + proj_includes + geos_includes, - libraries=proj_libraries + geos_libraries, - library_dirs=[library_dir] + proj_library_dirs + geos_library_dirs, - language='c++', - **extra_extension_args - ), - Extension( - 'cartopy._crs', - ['lib/cartopy/_crs.pyx'], - include_dirs=[include_dir, np.get_include()] + proj_includes, - libraries=proj_libraries, - library_dirs=[library_dir] + proj_library_dirs, - **extra_extension_args - ), - # Requires proj v4.9 - Extension( - 'cartopy.geodesic._geodesic', - ['lib/cartopy/geodesic/_geodesic.pyx'], - include_dirs=[include_dir, np.get_include()] + proj_includes, - libraries=proj_libraries, - library_dirs=[library_dir] + proj_library_dirs, - **extra_extension_args - ), - ], - + ext_modules=extensions, cmdclass=cmdclass, + python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*', classifiers=[ 'Development Status :: 4 - Beta', + 'Framework :: Matplotlib', 'License :: OSI Approved :: GNU Lesser General Public License v3 ' 'or later (LGPLv3+)', 'Operating System :: MacOS :: MacOS X', @@ -416,9 +404,10 @@ 'Programming Language :: Python :: 2', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.3', - 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8', 'Topic :: Scientific/Engineering', 'Topic :: Scientific/Engineering :: GIS', 'Topic :: Scientific/Engineering :: Visualization', diff -Nru python-cartopy-0.17.0+dfsg/.travis.yml python-cartopy-0.18.0+dfsg/.travis.yml --- python-cartopy-0.17.0+dfsg/.travis.yml 2018-11-17 07:25:32.000000000 +0000 +++ python-cartopy-0.18.0+dfsg/.travis.yml 2020-05-03 08:12:47.000000000 +0000 @@ -5,16 +5,16 @@ # These ancient versions are linked to an old libgfortran, but that version # isn't pinned in the package metadata - PYTHON_VERSION=2.7 - PACKAGES="numpy=1.10.0 matplotlib=1.5.1 nose proj4=4.9.1 scipy=0.16.0 libgfortran=1 mock" + PACKAGES="cython=0.28 numpy=1.10.0 matplotlib=1.5.1 nose proj4=4.9.1 scipy=0.16.0 libgfortran=1 mock futures" - PYTHON_VERSION=3.5 - PACKAGES="numpy=1.10.0 matplotlib=1.5.1 nose proj4=4.9.1 scipy=0.16.0 libgfortran=1" + PACKAGES="cython=0.28 numpy=1.10.0 matplotlib=1.5.1 nose proj4=4.9.1 scipy=0.16.0 libgfortran=1 owslib=0.19.1" PYTHONHASHSEED=0 # So pytest-xdist works. - NAME="Latest everything." PYTHON_VERSION=3.6 - PACKAGES="numpy matplotlib-base proj4 pykdtree scipy" - - NAME="Latest everything (py2k)." + PACKAGES="cython numpy matplotlib-base proj4 pykdtree scipy fiona" + - NAME="Latest everything (py2)." PYTHON_VERSION=2 - PACKAGES="numpy matplotlib proj4 scipy mock" + PACKAGES="cython=0.29 numpy matplotlib-base proj4 scipy mock futures" sudo: false @@ -38,16 +38,32 @@ # Create the basic testing environment # ------------------------------------ - conda config --set always_yes yes --set changeps1 no --set show_channel_urls yes + - conda config --add channels conda-forge + - conda config --add channels conda-forge/label/testing + - if [[ "$NAME" != "Latest*" ]]; then + conda config --set restore_free_channel true; + fi - ENV_NAME="test-environment" - - conda create -n $ENV_NAME python=$PYTHON_VERSION - - source activate $ENV_NAME # Customise the testing environment # --------------------------------- - - conda config --add channels conda-forge - - conda config --add channels conda-forge/label/testing - - PACKAGES="$PACKAGES cython pillow pytest pytest-xdist filelock pep8 pyshp shapely six requests pyepsg owslib" - - conda install --quiet $PACKAGES + - PACKAGES="$PACKAGES pillow pytest pytest-xdist filelock pep8 pyshp shapely six requests pyepsg owslib" + - | + if [[ "$NAME" == "Latest everything"* ]]; then + PACKAGES="$PACKAGES pytest-cov coveralls"; + export CYTHON_COVERAGE=1; + if [[ "$PYTHON_VERSION" == 2* ]]; then + # Latest available pytest-forked requires a newer pytest than available. + PACKAGES="$PACKAGES pytest-forked<1.1.0" + fi + fi + - conda create -n $ENV_NAME python=$PYTHON_VERSION $PACKAGES + - source activate $ENV_NAME + # Re-install so it's using FreeType 2.6; conda-forge/label/testing does not + # include the latest Python 2-compatible Matplotlib. + - if [[ "$NAME" == "Latest everything (py2)." ]]; then + pip install --force matplotlib; + fi # Conda debug # ----------- @@ -65,18 +81,24 @@ - python -c "import cartopy; print('Version ', cartopy.__version__)" && python setup.py version script: - - mkdir ../test_folder - - cd ../test_folder - # Check that the downloader tool at least knows where to get the data from (but don't actually download it) - python $TRAVIS_BUILD_DIR/tools/feature_download.py gshhs physical --dry-run - if [[ "$NAME" == "Latest everything"* ]]; then - CARTOPY_GIT_DIR=$TRAVIS_BUILD_DIR pytest -n 4 --doctest-modules --pyargs cartopy; + CARTOPY_GIT_DIR=$TRAVIS_BUILD_DIR pytest -n 4 --doctest-modules --pyargs cartopy --cov=cartopy -ra; else CARTOPY_GIT_DIR=$TRAVIS_BUILD_DIR pytest -n 4 --pyargs cartopy; fi + +after_success: + - if [[ "$NAME" == "Latest everything"* ]]; then + if [[ "$PYTHON_VERSION" == 2* ]]; then + coverage combine; + fi; + coveralls; + fi + after_failure: - source activate $ENV_NAME - python -c "from __future__ import print_function; import cartopy.tests.mpl; print(cartopy.tests.mpl.failed_images_html())" @@ -91,5 +113,5 @@ upload_docs: false on: repo: SciTools/cartopy - condition: $NAME == "Latest everything"* + condition: $NAME == "Latest everything." tags: true diff -Nru python-cartopy-0.17.0+dfsg/versioneer.py python-cartopy-0.18.0+dfsg/versioneer.py --- python-cartopy-0.17.0+dfsg/versioneer.py 2018-11-17 07:25:32.000000000 +0000 +++ python-cartopy-0.18.0+dfsg/versioneer.py 2020-05-03 08:12:47.000000000 +0000 @@ -1907,7 +1907,7 @@ except EnvironmentError: pass # That doesn't cover everything MANIFEST.in can do - # (http://docs.python.org/2/distutils/sourcedist.html#commands), so + # (https://docs.python.org/2/distutils/sourcedist.html#commands), so # it might give some false negatives. Appending redundant 'include' # lines is safe, though. if "versioneer.py" not in simple_includes: