Repository: SciTools/cartopy

Issue:
Error specifying center long for Mercator projection and extents on Python 3.6
### Description
<!-- Please provide a general introduction to the issue/proposal. -->
The following code works in Python 2 but fails in Python 3. The commented lines appear to work OK however, so it is really only a minor problem.


<!--
If you are reporting a bug, attach the *entire* traceback from Python.

If you are proposing an enhancement/new feature, provide links to related articles, reference examples, etc.

If you are asking a question, please ask on StackOverflow and use the cartopy tag. All cartopy
questions on StackOverflow can be found at https://stackoverflow.com/questions/tagged/cartopy
-->

#### Code to reproduce
```python
import cartopy.crs as ccrs
import cartopy
import matplotlib.pyplot as plt

cm_lon=51.15
maplimits = [-26., 128.3, -73.3, -31.1]

ax = plt.axes(projection=ccrs.Mercator(central_longitude=cm_lon))
#ax = plt.axes(projection=ccrs.PlateCarree(central_longitude=cm_lon))
#ax = plt.axes(projection=ccrs.Mercator())
ax.set_extent([maplimits[0], maplimits[1], maplimits[2], maplimits[3]])
plt.show()
```

#### Traceback 

```
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
~/miniconda2/envs/xesmf/lib/python3.6/site-packages/cartopy/mpl/geoaxes.py in set_extent(self, extents, crs)
    686             # the projection extents, so try and give a better error message.
--> 687             x1, y1, x2, y2 = projected.bounds
    688         except ValueError:

ValueError: not enough values to unpack (expected 4, got 0)

During handling of the above exception, another exception occurred:

ValueError                                Traceback (most recent call last)
<ipython-input-1-47cd62a8a14f> in <module>()
     12 ax.add_feature(cartopy.feature.LAND, color='gray')
     13 ax.coastlines(resolution='10m', linewidth=0.25)
---> 14 ax.set_extent([maplimits[0], maplimits[1], maplimits[2], maplimits[3]])
     15 plt.show()

~/miniconda2/envs/xesmf/lib/python3.6/site-packages/cartopy/mpl/geoaxes.py in set_extent(self, extents, crs)
    692                    'y_limits=[{ylim[0]}, {ylim[1]}]).')
    693             raise ValueError(msg.format(xlim=self.projection.x_limits,
--> 694                                         ylim=self.projection.y_limits))
    695         self.set_xlim([x1, x2])
    696         self.set_ylim([y1, y2])

ValueError: Failed to determine the required bounds in projection coordinates. Check that the values provided are within the valid range (x_limits=[-20037508.342789244, -20037508.34278924], y_limits=[-15496570.739723722, 18764656.23138057]).

```




Discussion:
What version of CartoPy are you using? Linux? I can't reproduce here on Python 3.6.5 and CartoPy 0.16.0 from conda-forge on MacOS. Instead (if I add `ax.coastlines()`), I get the following image:

![map](https://user-images.githubusercontent.com/221526/43937848-73d61cfa-9c1d-11e8-9d67-0091cb336049.png)


I have Python 3.6.6 miniconda running under Ubuntu 18.04 with the following (subset) of packages:
```
# Name                    Version                   Build  Channel
cartopy                   0.16.0           py36hfa13621_0 
proj4                     5.0.1                h14c3975_0
pyepsg                    0.3.2                    py36_0  
pyproj                    1.9.5.1          py36h7b21b82_1
shapely                   1.6.4            py36h7ef4460_0  
six                       1.11.0                   py36_1  
```

It sounds like there is some kind of issue with the non- conda-forge repository. I think we have been using `conda install -c scitools cartopy`, but I see in the documentation that this is no longer recommended/mentioned. Simply running `conda install -c conda-forge cartopy` does in fact solve the problem...
I can reproduce this problem now, it is due to using version 5 of proj (proj4 5.0.1). If you install cartopy from conda defaults you get a version built against proj4 5.0.1, but on conda-forge you get a cartopy built with proj4 4.9.3. You should pin proj4 to a version less than 5 when installing, for a new environment from defaults:

```
conda create -n my_env_name cartopy "proj4<5"
```

or to modify an existing install from defaults:

```
conda install "proj4<5"
```

Other proj4 5 problems are reported in #1071 and #1045.
Oh interesting, I'll have to double check that we are indeed having an issue on the Win install that seems to have a lower proj4 major version of 4.

EDIT: Yes it seems the windows installation is OK in this regard, had a different problem unrelated to Cartopy that impacted the test above.

Resolving patch:
diff --git a/lib/cartopy/crs.py b/lib/cartopy/crs.py
--- a/lib/cartopy/crs.py
+++ b/lib/cartopy/crs.py
@@ -52,7 +52,7 @@ class RotatedGeodetic(CRS):
 
     Coordinates are measured in degrees.
 
-    The class uses proj4 to perform an ob_tran operation, using the
+    The class uses proj to perform an ob_tran operation, using the
     pole_longitude to set a lon_0 then performing two rotations based on
     pole_latitude and central_rotated_longitude.
     This is equivalent to setting the new pole to a location defined by
@@ -994,10 +994,19 @@ def __init__(self, central_longitude=0.0,
 
         super(Mercator, self).__init__(proj4_params, globe=globe)
 
+        # In new proj, using exact limits will wrap-around, so subtract a
+        # small epsilon:
+        epsilon = 1e-10
+        minlon = -180 + central_longitude
+        maxlon = 180 + central_longitude
+        if central_longitude > 0:
+            maxlon -= epsilon
+        elif central_longitude < 0:
+            minlon += epsilon
+
         # Calculate limits.
         limits = self.transform_points(Geodetic(),
-                                       np.array([-180,
-                                                 180]) + central_longitude,
+                                       np.array([minlon, maxlon]),
                                        np.array([min_latitude, max_latitude]))
         self._xlimits = tuple(limits[..., 0])
         self._ylimits = tuple(limits[..., 1])
@@ -1267,7 +1276,7 @@ def __init__(self, central_longitude=0.0, globe=None):
         b = np.float(globe.semiminor_axis or a)
 
         if b != a or globe.ellipse is not None:
-            warnings.warn('The proj4 "mill" projection does not handle '
+            warnings.warn('The proj "mill" projection does not handle '
                           'elliptical globes.')
 
         proj4_params = [('proj', 'mill'), ('lon_0', central_longitude)]
@@ -1289,7 +1298,7 @@ class RotatedPole(_CylindricalProjection):
 
     Coordinates are measured in projection metres.
 
-    The class uses proj4 to perform an ob_tran operation, using the
+    The class uses proj to perform an ob_tran operation, using the
     pole_longitude to set a lon_0 then performing two rotations based on
     pole_latitude and central_rotated_longitude.
     This is equivalent to setting the new pole to a location defined by
@@ -1358,6 +1367,23 @@ def __init__(self, central_latitude=0.0, central_longitude=0.0,
                  false_easting=0.0, false_northing=0.0,
                  true_scale_latitude=None,
                  scale_factor=None, globe=None):
+        # Warn when using Stereographic with proj < 5.0.0 due to
+        # incorrect transformation with lon_0=0 (see
+        # https://github.com/OSGeo/proj.4/issues/194).
+        if central_latitude == 0:
+            if PROJ4_VERSION != ():
+                if PROJ4_VERSION < (5, 0, 0):
+                    warnings.warn(
+                        'The Stereographic projection in Proj older than '
+                        '5.0.0 incorrectly transforms points when '
+                        'central_latitude=0. Use this projection with '
+                        'caution.')
+            else:
+                warnings.warn(
+                    'Cannot determine Proj version. The Stereographic '
+                    'projection may be unreliable and should be used with '
+                    'caution.')
+
         proj4_params = [('proj', 'stere'), ('lat_0', central_latitude),
                         ('lon_0', central_longitude),
                         ('x_0', false_easting), ('y_0', false_northing)]
@@ -1441,6 +1467,17 @@ def __init__(self, central_longitude=0.0, true_scale_latitude=None,
 class Orthographic(Projection):
     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.')
+        else:
+            warnings.warn(
+                'Cannot determine Proj version. The Orthographic projection '
+                'may be unreliable and should be used with caution.')
+
         proj4_params = [('proj', 'ortho'), ('lon_0', central_longitude),
                         ('lat_0', central_latitude)]
         super(Orthographic, self).__init__(proj4_params, globe=globe)
@@ -1450,7 +1487,7 @@ def __init__(self, central_longitude=0.0, central_latitude=0.0,
         b = np.float(self.globe.semiminor_axis or a)
 
         if b != a:
-            warnings.warn('The proj4 "ortho" projection does not appear to '
+            warnings.warn('The proj "ortho" projection does not appear to '
                           'handle elliptical globes.')
 
         # To stabilise the projection of geometries, we reduce the boundary by
@@ -1483,22 +1520,25 @@ def __init__(self, proj4_params, central_longitude, globe=None):
         super(_WarpedRectangularProjection, self).__init__(proj4_params,
                                                            globe=globe)
 
+        # In new proj, using exact limits will wrap-around, so subtract a
+        # small epsilon:
+        epsilon = 1.e-10
+        minlon = -180 + central_longitude
+        maxlon = 180 + central_longitude
+        if central_longitude > 0:
+            maxlon -= epsilon
+        elif central_longitude < 0:
+            minlon += epsilon
+
         # Obtain boundary points
         points = []
         n = 91
         geodetic_crs = self.as_geodetic()
         for lat in np.linspace(-90, 90, n):
-            points.append(
-                self.transform_point(180 + central_longitude,
-                                     lat, geodetic_crs)
-            )
+            points.append(self.transform_point(maxlon, lat, geodetic_crs))
         for lat in np.linspace(90, -90, n):
-            points.append(
-                self.transform_point(-180 + central_longitude,
-                                     lat, geodetic_crs)
-            )
-        points.append(
-            self.transform_point(180 + central_longitude, -90, geodetic_crs))
+            points.append(self.transform_point(minlon, lat, geodetic_crs))
+        points.append(self.transform_point(maxlon, -90, geodetic_crs))
 
         self._boundary = sgeom.LineString(points[::-1])
 
@@ -1533,17 +1573,17 @@ def threshold(self):
 
 class Robinson(_WarpedRectangularProjection):
     def __init__(self, central_longitude=0, globe=None):
-        # Warn when using Robinson with proj4 4.8 due to discontinuity at
+        # Warn when using Robinson with proj 4.8 due to discontinuity at
         # 40 deg N introduced by incomplete fix to issue #113 (see
-        # https://trac.osgeo.org/proj/ticket/113).
+        # https://github.com/OSGeo/proj.4/issues/113).
         if PROJ4_VERSION != ():
             if (4, 8) <= PROJ4_VERSION < (4, 9):
                 warnings.warn('The Robinson projection in the v4.8.x series '
-                              'of Proj.4 contains a discontinuity at '
+                              'of Proj contains a discontinuity at '
                               '40 deg latitude. Use this projection with '
                               'caution.')
         else:
-            warnings.warn('Cannot determine Proj.4 version. The Robinson '
+            warnings.warn('Cannot determine Proj version. The Robinson '
                           'projection may be unreliable and should be used '
                           'with caution.')
 
@@ -1876,17 +1916,17 @@ def __init__(self, central_longitude=0.0, central_latitude=0.0,
             globe is created.
 
         """
-        # Warn when using Azimuthal Equidistant with proj4 < 4.9.2 due to
+        # Warn when using Azimuthal Equidistant with proj < 4.9.2 due to
         # incorrect transformation past 90 deg distance (see
         # https://github.com/OSGeo/proj.4/issues/246).
         if PROJ4_VERSION != ():
             if PROJ4_VERSION < (4, 9, 2):
-                warnings.warn('The Azimuthal Equidistant projection in Proj.4 '
+                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.')
         else:
-            warnings.warn('Cannot determine Proj.4 version. The Azimuthal '
+            warnings.warn('Cannot determine Proj version. The Azimuthal '
                           'Equidistant projection may be unreliable and '
                           'should be used with caution.')
 
diff --git a/setup.py b/setup.py
--- a/setup.py
+++ b/setup.py
@@ -205,19 +205,19 @@ def _header_bad(self, path):
             geos_libraries.append(entry[2:])
 
 
-# Proj4
+# Proj
 def find_proj_version_by_program(conda=None):
     proj = find_executable('proj')
     if proj is None:
         print(
-            'Proj4 %s must be installed.' % (
+            'Proj %s must be installed.' % (
                 '.'.join(str(v) for v in PROJ_MIN_VERSION), ),
             file=sys.stderr)
         exit(1)
 
     if conda is not None and conda not in proj:
         print(
-            'Proj4 %s must be installed in Conda environment "%s".' % (
+            'Proj %s must be installed in Conda environment "%s".' % (
                 '.'.join(str(v) for v in PROJ_MIN_VERSION), conda),
             file=sys.stderr)
         exit(1)
@@ -229,7 +229,7 @@ def find_proj_version_by_program(conda=None):
         proj_version = tuple(int(v.strip(b',')) for v in proj_version)
     except (OSError, IndexError, ValueError, subprocess.CalledProcessError):
         warnings.warn(
-            'Unable to determine Proj4 version. Ensure you have %s or later '
+            'Unable to determine Proj version. Ensure you have %s or later '
             'installed, or installation may fail.' % (
                 '.'.join(str(v) for v in PROJ_MIN_VERSION), ))
         proj_version = (0, 0, 0)
@@ -245,7 +245,7 @@ def find_proj_version_by_program(conda=None):
     proj_version = find_proj_version_by_program(conda)
     if proj_version < PROJ_MIN_VERSION:
         print(
-            'Proj4 version %s is installed, but cartopy requires at least '
+            'Proj version %s is installed, but cartopy requires at least '
             'version %s.' % ('.'.join(str(v) for v in proj_version),
                              '.'.join(str(v) for v in PROJ_MIN_VERSION)),
             file=sys.stderr)
@@ -268,7 +268,7 @@ def find_proj_version_by_program(conda=None):
         proj_version = find_proj_version_by_program()
         if proj_version < PROJ_MIN_VERSION:
             print(
-                'Proj4 version %s is installed, but cartopy requires at least '
+                'Proj version %s is installed, but cartopy requires at least '
                 'version %s.' % ('.'.join(str(v) for v in proj_version),
                                  '.'.join(str(v) for v in PROJ_MIN_VERSION)),
                 file=sys.stderr)
@@ -280,7 +280,7 @@ def find_proj_version_by_program(conda=None):
     else:
         if proj_version < PROJ_MIN_VERSION:
             print(
-                'Proj4 version %s is installed, but cartopy requires at least '
+                'Proj version %s is installed, but cartopy requires at least '
                 'version %s.' % ('.'.join(str(v) for v in proj_version),
                                  '.'.join(str(v) for v in PROJ_MIN_VERSION)),
                 file=sys.stderr)
@@ -353,7 +353,7 @@ def get_config_var(name):
     long_description=description,
     long_description_content_type='text/markdown',
     license="LGPLv3",
-    keywords="cartography map transform projection proj.4 geos shapely "
+    keywords="cartography map transform projection proj proj.4 geos shapely "
              "shapefile",
 
     install_requires=install_requires,
@@ -377,7 +377,7 @@ def get_config_var(name):
                   ['io/srtm.npz']},
 
 
-    # requires proj4 headers
+    # requires proj headers
     ext_modules=[
         Extension(
             'cartopy.trace',
@@ -397,7 +397,7 @@ def get_config_var(name):
             library_dirs=[library_dir] + proj_library_dirs,
             **extra_extension_args
         ),
-        # Requires proj4 v4.9
+        # Requires proj v4.9
         Extension(
             'cartopy.geodesic._geodesic',
             ['lib/cartopy/geodesic/_geodesic.pyx'],


Test patch:
diff --git a/lib/cartopy/tests/crs/test_mercator.py b/lib/cartopy/tests/crs/test_mercator.py
--- a/lib/cartopy/tests/crs/test_mercator.py
+++ b/lib/cartopy/tests/crs/test_mercator.py
@@ -18,6 +18,7 @@
 from __future__ import (absolute_import, division, print_function)
 
 from numpy.testing import assert_almost_equal
+import pytest
 
 import cartopy.crs as ccrs
 
@@ -63,10 +64,10 @@ def test_equality():
     assert hash(crs) == hash(crs2)
 
 
-def test_central_longitude():
-    cl = 10.0
-    crs = ccrs.Mercator(central_longitude=cl)
-    other_args = {'ellps=WGS84', 'lon_0={}'.format(cl), 'x_0=0.0', 'y_0=0.0'}
+@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)
 
     assert_almost_equal(crs.boundary.bounds,
diff --git a/lib/cartopy/tests/crs/test_miller.py b/lib/cartopy/tests/crs/test_miller.py
--- a/lib/cartopy/tests/crs/test_miller.py
+++ b/lib/cartopy/tests/crs/test_miller.py
@@ -23,6 +23,7 @@
 
 import numpy as np
 from numpy.testing import assert_almost_equal
+import pytest
 
 import cartopy.crs as ccrs
 
@@ -44,10 +45,10 @@ def test_default():
                         [-131.9758172, 131.9758172])
 
 
-def test_central_longitude():
-    cl = 10.0
-    mill = ccrs.Miller(central_longitude=cl)
-    other_args = {'a=57.29577951308232', 'lon_0={}'.format(cl)}
+@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)
 
     assert_almost_equal(np.array(mill.x_limits),
diff --git a/lib/cartopy/tests/crs/test_mollweide.py b/lib/cartopy/tests/crs/test_mollweide.py
--- a/lib/cartopy/tests/crs/test_mollweide.py
+++ b/lib/cartopy/tests/crs/test_mollweide.py
@@ -23,6 +23,7 @@
 
 import numpy as np
 from numpy.testing import assert_almost_equal
+import pytest
 
 import cartopy.crs as ccrs
 
@@ -57,14 +58,15 @@ def test_eccentric_globe():
                         [-1414.2135623731, 1414.2135623731])
 
 
-def test_central_longitude():
-    cl = 10.0
-    moll = ccrs.Mollweide(central_longitude=cl)
-    other_args = {'ellps=WGS84', 'lon_0={}'.format(cl)}
+@pytest.mark.parametrize('lon', [-10.0, 10.0])
+def test_central_longitude(lon):
+    moll = ccrs.Mollweide(central_longitude=lon)
+    other_args = {'ellps=WGS84', 'lon_0={}'.format(lon)}
     check_proj4_params(moll, other_args)
 
     assert_almost_equal(np.array(moll.x_limits),
-                        [-18040095.6961473, 18040095.6961473])
+                        [-18040095.6961473, 18040095.6961473],
+                        decimal=5)
     assert_almost_equal(np.array(moll.y_limits),
                         [-9020047.8480736, 9020047.8480736])
 
diff --git a/lib/cartopy/tests/crs/test_robinson.py b/lib/cartopy/tests/crs/test_robinson.py
--- a/lib/cartopy/tests/crs/test_robinson.py
+++ b/lib/cartopy/tests/crs/test_robinson.py
@@ -1,4 +1,4 @@
-# (C) British Crown Copyright 2013 - 2017, Met Office
+# (C) British Crown Copyright 2013 - 2018, Met Office
 #
 # This file is part of cartopy.
 #
@@ -35,7 +35,7 @@
 _CRS_PC = ccrs.PlateCarree()
 _CRS_ROB = ccrs.Robinson()
 
-# Increase tolerance if using older proj.4 releases
+# Increase tolerance if using older proj releases
 _TOL = -1 if ccrs.PROJ4_VERSION < (4, 9) else 7
 
 
diff --git a/lib/cartopy/tests/crs/test_stereographic.py b/lib/cartopy/tests/crs/test_stereographic.py
--- a/lib/cartopy/tests/crs/test_stereographic.py
+++ b/lib/cartopy/tests/crs/test_stereographic.py
@@ -60,7 +60,7 @@ def test_eccentric_globe(self):
     def test_true_scale(self):
         # The "true_scale_latitude" parameter only makes sense for
         # polar stereographic projections (#339 and #455).
-        # For now only the proj4 string creation is tested
+        # For now only the proj string creation is tested
         # See test_scale_factor for test on projection.
         globe = ccrs.Globe(ellipse='sphere')
         stereo = ccrs.NorthPolarStereo(true_scale_latitude=30, globe=globe)
diff --git a/lib/cartopy/tests/crs/test_transverse_mercator.py b/lib/cartopy/tests/crs/test_transverse_mercator.py
--- a/lib/cartopy/tests/crs/test_transverse_mercator.py
+++ b/lib/cartopy/tests/crs/test_transverse_mercator.py
@@ -1,4 +1,4 @@
-# (C) British Crown Copyright 2013 - 2017, Met Office
+# (C) British Crown Copyright 2013 - 2018, Met Office
 #
 # This file is part of cartopy.
 #
@@ -97,8 +97,9 @@ def setup_class(self):
     def test_default(self):
         proj = ccrs.OSNI()
         res = proj.transform_point(*self.point_a, src_crs=self.src_crs)
-        np.testing.assert_array_almost_equal(res, (275614.87105610,
-                                                   386984.15347340))
+        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()
diff --git a/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/multiple_projections5.png b/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/multiple_projections5.png
new file mode 100644
Binary files /dev/null and b/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/multiple_projections5.png differ
diff --git a/lib/cartopy/tests/mpl/test_images.py b/lib/cartopy/tests/mpl/test_images.py
--- a/lib/cartopy/tests/mpl/test_images.py
+++ b/lib/cartopy/tests/mpl/test_images.py
@@ -46,6 +46,9 @@
 # care that it is putting images onto the map which are roughly correct.
 @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)
 def test_web_tiles():
@@ -83,6 +86,9 @@ def test_web_tiles():
 
 @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(['image_merge'],
               tolerance=3.6 if MPL_VERSION < '2' else 0.01)
 def test_image_merge():
@@ -110,6 +116,9 @@ def test_image_merge():
     plt.imshow(img, origin=origin, extent=extent, alpha=0.5)
 
 
+@pytest.mark.xfail((5, 0, 0) <= ccrs.PROJ4_VERSION < (5, 1, 0),
+                   reason='Proj Orthographic projection is buggy.',
+                   strict=True)
 @ImageTesting(['imshow_natural_earth_ortho'],
               tolerance=3.96 if MPL_VERSION < '2' else 0.7)
 def test_imshow():
@@ -137,6 +146,9 @@ def test_imshow_projected():
     ax.imshow(img, extent=img_extent, origin='upper', transform=source_proj)
 
 
+@pytest.mark.xfail((5, 0, 0) <= ccrs.PROJ4_VERSION < (5, 1, 0),
+                   reason='Proj Orthographic projection is buggy.',
+                   strict=True)
 @ImageTesting(['imshow_natural_earth_ortho'],
               tolerance=4.15 if MPL_VERSION < '2' else 0.7)
 def test_stock_img():
@@ -144,6 +156,9 @@ def test_stock_img():
     ax.stock_img()
 
 
+@pytest.mark.xfail((5, 0, 0) <= ccrs.PROJ4_VERSION < (5, 1, 0),
+                   reason='Proj Orthographic projection is buggy.',
+                   strict=True)
 @ImageTesting(['imshow_natural_earth_ortho'],
               tolerance=3.96 if MPL_VERSION < '2' else 0.7)
 def test_pil_Image():
@@ -154,6 +169,9 @@ def test_pil_Image():
               extent=[-180, 180, -90, 90])
 
 
+@pytest.mark.xfail((5, 0, 0) <= ccrs.PROJ4_VERSION < (5, 1, 0),
+                   reason='Proj Orthographic projection is buggy.',
+                   strict=True)
 @ImageTesting(['imshow_natural_earth_ortho'],
               tolerance=4.2 if MPL_VERSION < '2' else 0)
 def test_background_img():
diff --git a/lib/cartopy/tests/mpl/test_mpl_integration.py b/lib/cartopy/tests/mpl/test_mpl_integration.py
--- a/lib/cartopy/tests/mpl/test_mpl_integration.py
+++ b/lib/cartopy/tests/mpl/test_mpl_integration.py
@@ -148,7 +148,8 @@ def test_simple_global():
 
 
 @pytest.mark.natural_earth
-@ImageTesting(['multiple_projections1'])
+@ImageTesting(['multiple_projections1' if ccrs.PROJ4_VERSION < (5, 0, 0)
+               else 'multiple_projections5'])
 def test_multiple_projections():
 
     projections = [ccrs.PlateCarree(),
@@ -170,9 +171,17 @@ def test_multiple_projections():
                    ccrs.InterruptedGoodeHomolosine(),
                    ]
 
-    fig = plt.figure(figsize=(10, 10))
+    if ccrs.PROJ4_VERSION < (5, 0, 0):
+        # Produce the same sized image for old proj, to avoid having to replace
+        # the image. If this figure is regenerated for both old and new proj,
+        # then drop this condition.
+        rows = 5
+    else:
+        rows = np.ceil(len(projections) / 5)
+
+    fig = plt.figure(figsize=(10, 2 * rows))
     for i, prj in enumerate(projections, 1):
-        ax = fig.add_subplot(5, 5, i, projection=prj)
+        ax = fig.add_subplot(rows, 5, i, projection=prj)
 
         ax.set_global()
 
@@ -255,7 +264,9 @@ def test_pcolormesh_global_with_wrap1():
 
 
 @pytest.mark.natural_earth
-@ImageTesting(['pcolormesh_global_wrap2'])
+@ImageTesting(
+    ['pcolormesh_global_wrap2'],
+    tolerance=1.8 if (5, 0, 0) <= ccrs.PROJ4_VERSION < (5, 1, 0) else 0.5)
 def test_pcolormesh_global_with_wrap2():
     # make up some realistic data with bounds (such as data from the UM)
     nx, ny = 36, 18
@@ -282,7 +293,9 @@ def test_pcolormesh_global_with_wrap2():
 
 
 @pytest.mark.natural_earth
-@ImageTesting(['pcolormesh_global_wrap3'], tolerance=_ROB_TOL)
+@ImageTesting(
+    ['pcolormesh_global_wrap3'],
+    tolerance=2.4 if (5, 0, 0) <= ccrs.PROJ4_VERSION < (5, 1, 0) else _ROB_TOL)
 def test_pcolormesh_global_with_wrap3():
     nx, ny = 33, 17
     xbnds = np.linspace(-1.875, 358.125, nx, endpoint=True)
diff --git a/lib/cartopy/tests/mpl/test_web_services.py b/lib/cartopy/tests/mpl/test_web_services.py
--- a/lib/cartopy/tests/mpl/test_web_services.py
+++ b/lib/cartopy/tests/mpl/test_web_services.py
@@ -36,6 +36,9 @@ def test_wmts():
 
 
 @pytest.mark.network
+@pytest.mark.xfail((5, 0, 0) <= ccrs.PROJ4_VERSION < (5, 1, 0),
+                   reason='Proj Orthographic projection is buggy.',
+                   strict=True)
 @pytest.mark.skipif(not _OWSLIB_AVAILABLE, reason='OWSLib is unavailable.')
 @ImageTesting(['wms'], tolerance=7.76 if MPL_VERSION < '2' else 0)
 def test_wms():
diff --git a/lib/cartopy/tests/test_crs.py b/lib/cartopy/tests/test_crs.py
--- a/lib/cartopy/tests/test_crs.py
+++ b/lib/cartopy/tests/test_crs.py
@@ -118,7 +118,7 @@ def test_transform_points_nD(self):
         unrotated_lon = res[..., 0]
         unrotated_lat = res[..., 1]
 
-        # Solutions derived by proj4 direct.
+        # Solutions derived by proj direct.
         solx = np.array([[-16.42176094, -14.85892262, -11.90627520],
                          [-16.71055023, -14.58434624, -11.68799988]])
         soly = np.array([[46.00724251, 51.29188893, 52.59101488],
@@ -138,7 +138,7 @@ def test_transform_points_1D(self):
         unrotated_lon = res[..., 0]
         unrotated_lat = res[..., 1]
 
-        # Solutions derived by proj4 direct.
+        # Solutions derived by proj direct.
         solx = np.array([-16.42176094, -14.85892262,
                          -12.88946157, -10.35078336])
         soly = np.array([46.00724251, 51.29188893,
diff --git a/lib/cartopy/tests/test_img_tiles.py b/lib/cartopy/tests/test_img_tiles.py
--- a/lib/cartopy/tests/test_img_tiles.py
+++ b/lib/cartopy/tests/test_img_tiles.py
@@ -41,6 +41,19 @@
                  (8, 9, 4): (0, 2504688.542848654,
                              -5009377.085697312, -2504688.542848654),
                  }
+if ccrs.PROJ4_VERSION == (5, 0, 0):
+    KNOWN_EXTENTS = {
+        (0, 0, 0): (-20037508.342789244, 20037508.342789244,
+                    -19994827.892149, 19994827.892149),
+        (2, 0, 2): (0, 10018754.171395,
+                    9997413.946075, 19994827.892149),
+        (0, 2, 2): (-20037508.342789244, -10018754.171394622,
+                    -9997413.946075, 0),
+        (2, 2, 2): (0, 10018754.171395,
+                    -9997413.946075, 0),
+        (8, 9, 4): (0, 2504688.542849,
+                    -4998706.973037, -2499353.486519),
+    }
 
 
 def GOOGLE_IMAGE_URL_REPLACEMENT(self, tile):
@@ -155,9 +168,14 @@ def test_image_for_domain():
     ll_extent = ccrs.Geodetic().transform_points(gt.crs,
                                                  np.array(extent[:2]),
                                                  np.array(extent[2:]))
-    assert_arr_almost(ll_extent[:, :2],
-                      [[-11.25, 48.92249926],
-                       [11.25, 61.60639637]])
+    if ccrs.PROJ4_VERSION == (5, 0, 0):
+        assert_arr_almost(ll_extent[:, :2],
+                          [[-11.25, 49.033955],
+                           [11.25, 61.687101]])
+    else:
+        assert_arr_almost(ll_extent[:, :2],
+                          [[-11.25, 48.92249926],
+                           [11.25, 61.60639637]])
 
 
 def test_quadtree_wts():