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 %}
+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, 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 '