Repository: SciTools/cartopy

Issue:
Google tiles vertical offset bug
Following on from #313, fix the vertical offset error shown by this code:

```
ax = plt.axes(projection=ccrs.Mercator())
ax.set_extent([-4.2, -3, 50.3, 50.7])
tiles = img_tiles.MapQuestOpenAerial()
ax.add_image(tiles, 8)
ax.coastlines("10m")
plt.show()
```



Discussion:

Resolving patch:
diff --git a/lib/cartopy/crs.py b/lib/cartopy/crs.py
--- a/lib/cartopy/crs.py
+++ b/lib/cartopy/crs.py
@@ -803,7 +803,7 @@ def __init__(self, central_longitude=0.0,
         super(Mercator, self).__init__(proj4_params, globe=globe)
 
         # Calculate limits.
-        limits = self.transform_points(PlateCarree(),
+        limits = self.transform_points(Geodetic(),
                                        np.array([-180., 180.]),
                                        np.array([min_latitude, max_latitude]))
         self._xlimits = tuple(limits[..., 0])
diff --git a/lib/cartopy/io/img_tiles.py b/lib/cartopy/io/img_tiles.py
--- a/lib/cartopy/io/img_tiles.py
+++ b/lib/cartopy/io/img_tiles.py
@@ -22,8 +22,7 @@
 Tile generation is explicitly not yet implemented.
 
 """
-
-import math
+from __future__ import division
 
 import PIL.Image as Image
 import shapely.geometry as sgeom
@@ -40,11 +39,13 @@ class GoogleTiles(object):
 
     """
     def __init__(self, desired_tile_form='RGB'):
-        # XXX consider fixing the CRS???
         self.imgs = []
-        self.crs = ccrs.Mercator(
-            min_latitude=-85., max_latitude=85.,
-            globe=ccrs.Globe(semimajor_axis=math.degrees(1)))
+        self.crs = ccrs.Mercator(min_latitude=-85.0511287798066,
+                                 max_latitude=85.0511287798066,
+                                 globe=ccrs.Globe(ellipse=None,
+                                                  semimajor_axis=6378137,
+                                                  semiminor_axis=6378137,
+                                                  nadgrids='@null'))
         self.desired_tile_form = desired_tile_form
 
     def image_for_domain(self, target_domain, target_z):
@@ -69,7 +70,7 @@ def _find_images(self, target_domain, target_z, start_tile=(0, 0, 0)):
                                                              'be an integer '
                                                              '>=0.')
 
-        # recursively drill down to the images at the target zoom
+        # Recursively drill down to the images at the target zoom.
         x0, x1, y0, y1 = self._tileextent(start_tile)
         domain = sgeom.box(x0, y0, x1, y1)
         if domain.intersects(target_domain):
@@ -85,21 +86,30 @@ def _find_images(self, target_domain, target_z, start_tile=(0, 0, 0)):
 
     def subtiles(self, x_y_z):
         x, y, z = x_y_z
-        # google tile specific (i.e. up->down)
+        # Google tile specific (i.e. up->down).
         for xi in range(0, 2):
             for yi in range(0, 2):
                 yield x * 2 + xi, y * 2 + yi, z + 1
 
     _subtiles = subtiles
 
-    @staticmethod
-    def tile_bbox(prj, x, y, z, lat_extent_at_z0=(-85., 85.),
-                  bottom_up=True):
+    def tile_bbox(self, x, y, z, y0_at_north_pole=True):
         """
         Returns the ``(x0, x1), (y0, y1)`` bounding box for the given x, y, z
         tile position.
 
-        NOTE: This interface is highly liable to change in the future.
+        Parameters
+        ----------
+        x, y, z : int
+            The x, y, z tile coordinates in the Google tile numbering system
+            (with y=0 being at the north pole), unless `y0_at_north_pole` is
+            set to ``False``, in which case `y` is in the TMS numbering system
+            (with y=0 being at the south pole).
+        y0_at_north_pole : bool
+            Whether the numbering of the y coordinate starts at the north
+            pole (as is the convention for Google tiles), or the south
+            pole (as is the convention for TMS).
+
         """
         n = 2 ** z
         assert 0 <= x <= (n - 1), ("Tile's x index is out of range. Upper "
@@ -107,46 +117,34 @@ def tile_bbox(prj, x, y, z, lat_extent_at_z0=(-85., 85.),
         assert 0 <= y <= (n - 1), ("Tile's y index is out of range. Upper "
                                    "limit %s. Got %s" % (n, y))
 
-        x0 = -180.
-        # compute the box height in native coordinates for this zoom level
-        box_w = 360. / n
+        x0, x1 = self.crs.x_limits
+        y0, y1 = self.crs.y_limits
 
-        result = prj.transform_points(ccrs.PlateCarree(),
-                                      np.array([0., 0]),
-                                      np.array(lat_extent_at_z0))
-        y1, y0 = result[:, 1]
-        # compute the box height in native coordinates for this zoom level
+        # Compute the box height and width in native coordinates
+        # for this zoom level.
         box_h = (y1 - y0) / n
+        box_w = (x1 - x0) / n
 
-        # compute the native x & native y extents of the box
-    #    x_lower, x_upper = x0 + x*box_w, x0 + (x+1)*box_w
-    #    y_lower, y_upper = y0 + y*box_h, y0 + (y+1)*box_h
+        # Compute the native x & y extents of the tile.
         n_xs = x0 + (x + np.arange(0, 2, dtype=np.float64)) * box_w
         n_ys = y0 + (y + np.arange(0, 2, dtype=np.float64)) * box_h
 
-        if not bottom_up:
-            # n.b. assumes that the projection is symmetric
+        if y0_at_north_pole:
             n_ys = -1 * n_ys[::-1]
 
         return n_xs, n_ys
 
     def tileextent(self, x_y_z):
-        """Returns extent tuple in MercatorCoords ``(x0, x1, y0, y1)``."""
+        """Returns extent tuple ``(x0,x1,y0,y1)`` in Mercator coordinates."""
         x, y, z = x_y_z
-        x_lim, y_lim = self.tile_bbox(self.crs, x, y, z, bottom_up=True)
+        x_lim, y_lim = self.tile_bbox(x, y, z, y0_at_north_pole=True)
         return tuple(x_lim) + tuple(y_lim)
 
     _tileextent = tileextent
 
     def _image_url(self, tile):
-        url = ('http://chart.apis.google.com/chart?chst=d_text_outline&'
-               'chs=256x256&chf=bg,s,00000055&chld=FFFFFF|16|h|000000|b||||'
-               'Google:%20%20(' + str(tile[0]) + ',' + str(tile[1]) + ')'
-               '|Zoom%20' + str(tile[2]) + '||||||______________________'
-               '______')
-#        print url
-#        url = ('http://mts0.google.com/vt/lyrs=m@177000000&hl=en&src=api&'
-#               'x=%s&y=%s&z=%s&s=G' % tile)
+        url = ('http://mts0.google.com/vt/lyrs=m@177000000&hl=en&src=api&'
+               'x=%s&y=%s&z=%s&s=G' % tile)
         return url
 
     def get_image(self, tile):
@@ -205,13 +203,8 @@ class QuadtreeTiles(GoogleTiles):
     """
     def _image_url(self, tile):
         url = ('http://ecn.dynamic.t1.tiles.virtualearth.net/comp/'
-               'CompositionHandler/' + str(tile) + '?mkt=en-'
-               'gb&it=A,G,L&shading=hill&n=z')
-        url = ('http://chart.apis.google.com/chart?chst=d_text_outline&'
-               'chs=256x256&chf=bg,s,00000055&chld=FFFFFF|16|h|000000|b'
-               '||||Quadkey:%20%20(' + str(tile) + ')||||||____________'
-               '________________')
-
+               'CompositionHandler/{tile}?mkt=en-'
+               'gb&it=A,G,L&shading=hill&n=z'.format(tile=tile))
         return url
 
     def tms_to_quadkey(self, tms, google=False):


Test patch:
diff --git a/lib/cartopy/tests/mpl/baseline_images/mpl/test_images/image_merge.png b/lib/cartopy/tests/mpl/baseline_images/mpl/test_images/image_merge.png
Binary files a/lib/cartopy/tests/mpl/baseline_images/mpl/test_images/image_merge.png and b/lib/cartopy/tests/mpl/baseline_images/mpl/test_images/image_merge.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
@@ -15,9 +15,8 @@
 # You should have received a copy of the GNU Lesser General Public License
 # along with cartopy.  If not, see <http://www.gnu.org/licenses/>.
 
-import math
+import types
 
-from nose.tools import assert_equal, assert_raises
 import numpy as np
 import matplotlib.pyplot as plt
 import shapely.geometry
@@ -27,6 +26,7 @@
 
 from cartopy.tests.mpl import ImageTesting
 import cartopy.tests.test_img_nest as ctest_nest
+import cartopy.tests.test_img_tiles as ctest_tiles
 
 
 @ImageTesting(['web_tiles'], tolerance=12)
@@ -40,6 +40,8 @@ def test_web_tiles():
 
     ax = plt.subplot(3, 2, 1, projection=ccrs.Mercator())
     gt = cimgt.GoogleTiles()
+    gt._image_url = types.MethodType(ctest_tiles.GOOGLE_IMAGE_URL_REPLACEMENT,
+                                     gt)
     img, extent, origin = gt.image_for_domain(target_domain, 1)
     ax.imshow(np.array(img), extent=extent, transform=ccrs.Mercator(),
               interpolation='bilinear', origin=origin)
@@ -93,20 +95,19 @@ def test_image_merge():
         for j in range(0, 3):
             tiles.append((i, j, 2))
 
-    tiler = cimgt.GoogleTiles()
+    gt = cimgt.GoogleTiles()
+    gt._image_url = types.MethodType(ctest_tiles.GOOGLE_IMAGE_URL_REPLACEMENT,
+                                     gt)
     images_to_merge = []
     for tile in tiles:
-        img, extent, origin = tiler.get_image(tile)
+        img, extent, origin = gt.get_image(tile)
         img = np.array(img)
         x = np.linspace(extent[0], extent[1], img.shape[1], endpoint=False)
         y = np.linspace(extent[2], extent[3], img.shape[0], endpoint=False)
         images_to_merge.append([img, x, y, origin])
 
     img, extent, origin = cimgt._merge_tiles(images_to_merge)
-    ax = plt.axes(projection=ccrs.Mercator(
-        min_latitude=-85.,
-        max_latitude=85.,
-        globe=ccrs.Globe(semimajor_axis=math.degrees(1))))
+    ax = plt.axes(projection=gt.crs)
     ax.set_global()
     ax.coastlines()
     plt.imshow(img, origin=origin, extent=extent, alpha=0.5)
diff --git a/lib/cartopy/tests/test_img_nest.py b/lib/cartopy/tests/test_img_nest.py
--- a/lib/cartopy/tests/test_img_nest.py
+++ b/lib/cartopy/tests/test_img_nest.py
@@ -19,10 +19,12 @@
 import io
 import cPickle as pickle
 import os
+import shutil
+import warnings
 
 from nose.tools import assert_equal, assert_in, assert_true
 import numpy as np
-from numpy.testing import assert_array_equal
+from numpy.testing import assert_array_equal, assert_array_almost_equal
 import PIL.Image
 import shapely.geometry as sgeom
 
@@ -33,8 +35,14 @@
 import cartopy.tests as tests
 
 
-_TEST_DATA_DIR = os.path.join(config["repo_data_dir"],
+#: An integer version which should be increased if the test data needs
+#: to change in some way.
+_TEST_DATA_VERSION = 1
+_TEST_DATA_DIR = os.path.join(config["data_dir"],
                               'wmts', 'aerial')
+#: A global to determine whether the test data has already been made available
+#: in this session.
+_TEST_DATA_AVAILABLE = False
 
 
 def _save_world(fname, args):
@@ -184,52 +192,22 @@ def world_file_extent(*args, **kwargs):
         """
         extent, pix_size = cimg_nest.Img.world_file_extent(*args, **kwargs)
         # round the extent
-        extent = tuple(round(v, 8) for v in extent)
-        pix_size = tuple(round(v, 8) for v in pix_size)
+        extent = tuple(round(v, 4) for v in extent)
+        pix_size = tuple(round(v, 4) for v in pix_size)
         return extent, pix_size
 
 
-def test_find_images():
-    gen_test_data()
-    z2_dir = os.path.join(_TEST_DATA_DIR, 'z_2')
-    img_fname = os.path.join(z2_dir, 'x_2_y_1.png')
-    world_file_fname = os.path.join(z2_dir, 'x_2_y_1.pgw')
-    img = RoundedImg.from_world_file(img_fname, world_file_fname)
-
-    assert_equal(img.filename, img_fname)
-    assert_equal(img.extent, (0.0, 90.0, 89.51370048, -0.0))
-    assert_equal(img.origin, 'lower')
-    assert_array_equal(img, np.array(PIL.Image.open(img.filename)))
-    assert_equal(img.pixel_size, (0.3515625, -0.34966289))
-
-
-def gen_nest():
-    gen_test_data()
-    from_config = cimg_nest.NestedImageCollection.from_configuration
-
-    files = [['aerial z0 test', os.path.join(_TEST_DATA_DIR, 'z_0')],
-             ['aerial z1 test', os.path.join(_TEST_DATA_DIR, 'z_1')],
-             ]
-
-    nest_z0_z1 = from_config('aerial test',
-                             ccrs.Mercator(),
-                             files,
-                             glob_pattern='*.png',
-                             img_class=RoundedImg,
-                             )
-    return nest_z0_z1
-
-
 def test_nest():
-    z0 = cimg_nest.ImageCollection('aerial z0 test', ccrs.Mercator())
+    crs = cimgt.GoogleTiles().crs
+    z0 = cimg_nest.ImageCollection('aerial z0 test', crs)
     z0.scan_dir_for_imgs(os.path.join(_TEST_DATA_DIR, 'z_0'),
                          glob_pattern='*.png', img_class=RoundedImg)
 
-    z1 = cimg_nest.ImageCollection('aerial z1 test', ccrs.Mercator())
+    z1 = cimg_nest.ImageCollection('aerial z1 test', crs)
     z1.scan_dir_for_imgs(os.path.join(_TEST_DATA_DIR, 'z_1'),
                          glob_pattern='*.png', img_class=RoundedImg)
 
-    z2 = cimg_nest.ImageCollection('aerial z2 test', ccrs.Mercator())
+    z2 = cimg_nest.ImageCollection('aerial z2 test', crs)
     z2.scan_dir_for_imgs(os.path.join(_TEST_DATA_DIR, 'z_2'),
                          glob_pattern='*.png', img_class=RoundedImg)
 
@@ -244,12 +222,10 @@ def test_nest():
                           '{!s}'.format(img, z0.images[0], img.extent,
                                         z0.images[0].extent))
     nest_z0_z1 = cimg_nest.NestedImageCollection('aerial test',
-                                                 ccrs.Mercator(),
+                                                 crs,
                                                  [z0, z1])
 
-    nest = cimg_nest.NestedImageCollection('aerial test',
-                                           ccrs.Mercator(),
-                                           [z0, z1, z2])
+    nest = cimg_nest.NestedImageCollection('aerial test', crs, [z0, z1, z2])
 
     z0_key = ('aerial z0 test', z0.images[0])
 
@@ -291,7 +267,12 @@ def test_nest():
                  nest_z0_z1_from_pickle._ancestry)
 
 
-def gen_test_data():
+def requires_wmts_data(function):
+    """
+    A decorator which ensures that the WMTS data is available for
+    use in testing.
+
+    """
     aerial = cimgt.MapQuestOpenAerial()
 
     # get web tiles upto 3 zoom levels deep
@@ -302,12 +283,32 @@ def gen_test_data():
         for sub_tile in aerial.subtiles(tile):
             tiles.append(sub_tile)
 
-    data_dir = os.path.join(_TEST_DATA_DIR, 'z_{}', 'x_{}_y_{}.png')
-
-    # download the tiles
+    fname_template = os.path.join(_TEST_DATA_DIR, 'z_{}', 'x_{}_y_{}.png')
+
+    if not os.path.isdir(_TEST_DATA_DIR):
+        os.makedirs(_TEST_DATA_DIR)
+
+    data_version_fname = os.path.join(_TEST_DATA_DIR, 'version.txt')
+
+    test_data_version = None
+    try:
+        with open(data_version_fname, 'r') as fh:
+            test_data_version = int(fh.read().strip())
+    except IOError:
+        pass
+    finally:
+        if test_data_version != _TEST_DATA_VERSION:
+            warnings.warn('WMTS test data is out of date, regenerating at '
+                          '{}.'.format(_TEST_DATA_DIR))
+            shutil.rmtree(_TEST_DATA_DIR)
+            os.makedirs(_TEST_DATA_DIR)
+            with open(data_version_fname, 'w') as fh:
+                fh.write(str(_TEST_DATA_VERSION))
+
+    # Download the tiles.
     for tile in tiles:
         x, y, z = tile
-        fname = data_dir.format(z, x, y)
+        fname = fname_template.format(z, x, y)
         if not os.path.exists(fname):
             if not os.path.isdir(os.path.dirname(fname)):
                 os.makedirs(os.path.dirname(fname))
@@ -334,6 +335,44 @@ def gen_test_data():
             _save_world(pgw_fname, pgw_keys)
             img.save(fname)
 
+    global _TEST_DATA_AVAILABLE
+    _TEST_DATA_AVAILABLE = True
+
+    return function
+
+
+@requires_wmts_data
+def test_find_images():
+    z2_dir = os.path.join(_TEST_DATA_DIR, 'z_2')
+    img_fname = os.path.join(z2_dir, 'x_2_y_0.png')
+    world_file_fname = os.path.join(z2_dir, 'x_2_y_0.pgw')
+    img = RoundedImg.from_world_file(img_fname, world_file_fname)
+
+    assert_equal(img.filename, img_fname)
+    assert_array_almost_equal(img.extent,
+                              (0., 10018754.17139462,
+                               10018754.17139462, 20037508.342789244),
+                              decimal=4)
+    assert_equal(img.origin, 'lower')
+    assert_array_equal(img, np.array(PIL.Image.open(img.filename)))
+    assert_equal(img.pixel_size, (39135.7585, 39135.7585))
+
+
+@requires_wmts_data
+def gen_nest():
+    from_config = cimg_nest.NestedImageCollection.from_configuration
+
+    files = [['aerial z0 test', os.path.join(_TEST_DATA_DIR, 'z_0')],
+             ['aerial z1 test', os.path.join(_TEST_DATA_DIR, 'z_1')],
+             ]
+
+    crs = cimgt.GoogleTiles().crs
+
+    nest_z0_z1 = from_config('aerial test',
+                             crs, files, glob_pattern='*.png',
+                             img_class=RoundedImg)
+    return nest_z0_z1
+
 
 if __name__ == '__main__':
     import nose
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
@@ -15,22 +15,47 @@
 # You should have received a copy of the GNU Lesser General Public License
 # along with cartopy.  If not, see <http://www.gnu.org/licenses/>.
 
+import types
+
 from nose.tools import assert_equal, assert_raises
+import numpy as np
 from numpy.testing import assert_array_almost_equal as assert_arr_almost
 import shapely.geometry as sgeom
 
+import cartopy.crs as ccrs
 import cartopy.io.img_tiles as cimgt
 
 
+#: Maps Google tile coordinates to native mercator coordinates as defined
+#: by http://goo.gl/pgJi.
+KNOWN_EXTENTS = {(0, 0, 0): (-20037508.342789244, 20037508.342789244,
+                             -20037508.342789244, 20037508.342789244),
+                 (2, 0, 2): (0., 10018754.17139462,
+                             10018754.17139462, 20037508.342789244),
+                 (0, 2, 2): (-20037508.342789244, -10018754.171394622,
+                             -10018754.171394622, 0),
+                 (2, 2, 2): (0, 10018754.17139462,
+                             -10018754.171394622, 0),
+                 (8, 9, 4): (0, 2504688.542848654,
+                             -5009377.085697312, -2504688.542848654),
+                 }
+
+
+def GOOGLE_IMAGE_URL_REPLACEMENT(self, tile):
+    url = ('http://chart.apis.google.com/chart?chst=d_text_outline&'
+           'chs=256x256&chf=bg,s,00000055&chld=FFFFFF|16|h|000000|b||||'
+           'Google:%20%20(' + str(tile[0]) + ',' + str(tile[1]) + ')'
+           '|Zoom%20' + str(tile[2]) + '||||||______________________'
+           '______')
+    return url
+
+
 def test_google_wts():
     gt = cimgt.GoogleTiles()
 
-    extent = [-15, 0.1, 50, 60]
-    target_domain = sgeom.Polygon([[extent[0], extent[1]],
-                                   [extent[2], extent[1]],
-                                   [extent[2], extent[3]],
-                                   [extent[0], extent[3]],
-                                   [extent[0], extent[1]]])
+    ll_target_domain = sgeom.box(-15, 50, 0, 60)
+    multi_poly = gt.crs.project_geometry(ll_target_domain, ccrs.PlateCarree())
+    target_domain = multi_poly.geoms[0]
 
     with assert_raises(AssertionError):
         list(gt.find_images(target_domain, -1))
@@ -47,39 +72,56 @@ def test_google_wts():
     with assert_raises(AssertionError):
         gt.tileextent((0, 1, 0))
 
-    assert_arr_almost(gt.tileextent((0, 0, 0)),
-                      (-180.0, 180.0,
-                       179.02740096396502, -179.02740096396491))
-    assert_arr_almost(gt.tileextent((2, 0, 2)),
-                      (0.0, 90.0, 179.02740096396502, 89.513700481982539))
-    assert_arr_almost(gt.tileextent((0, 2, 2)),
-                      (-180.0, -90.0,
-                       5.6843418860808015e-14, -89.513700481982426))
-    assert_arr_almost(gt.tileextent((2, 2, 2)),
-                      (0.0, 90.0,
-                       5.6843418860808015e-14, -89.513700481982426))
-    assert_arr_almost(gt.tileextent((8, 9, 4)), (0.0, 22.5, -22.37842512,
-                      -44.75685024))  # <- zoom 4, contains cape town.
+    assert_arr_almost(gt.tileextent((0, 0, 0)), KNOWN_EXTENTS[(0, 0, 0)])
+    assert_arr_almost(gt.tileextent((2, 0, 2)), KNOWN_EXTENTS[(2, 0, 2)])
+    assert_arr_almost(gt.tileextent((0, 2, 2)), KNOWN_EXTENTS[(0, 2, 2)])
+    assert_arr_almost(gt.tileextent((2, 2, 2)), KNOWN_EXTENTS[(2, 2, 2)])
+    assert_arr_almost(gt.tileextent((8, 9, 4)), KNOWN_EXTENTS[(8, 9, 4)])
+
+
+def test_tile_bbox_y0_at_south_pole():
+    tms = cimgt.MapQuestOpenAerial()
+
+    # Check the y0_at_north_pole keywords returns the appropriate bounds.
+    assert_arr_almost(tms.tile_bbox(8, 6, 4, y0_at_north_pole=False),
+                      np.array(KNOWN_EXTENTS[(8, 9, 4)]).reshape([2, 2]))
 
 
 def test_tile_find_images():
     gt = cimgt.GoogleTiles()
-    # test the find_images method on a Tile instance.
-    target_domain = sgeom.box(-10, 60, 10, 70)
+    # Test the find_images method on a GoogleTiles instance.
+    ll_target_domain = sgeom.box(-10, 50, 10, 60)
+    multi_poly = gt.crs.project_geometry(ll_target_domain, ccrs.PlateCarree())
+    target_domain = multi_poly.geoms[0]
 
     assert_equal([(7, 4, 4), (7, 5, 4), (8, 4, 4), (8, 5, 4)],
                  list(gt.find_images(target_domain, 4)))
 
 
+def test_image_for_domain():
+    gt = cimgt.GoogleTiles()
+    gt._image_url = types.MethodType(GOOGLE_IMAGE_URL_REPLACEMENT, gt)
+
+    ll_target_domain = sgeom.box(-10, 50, 10, 60)
+    multi_poly = gt.crs.project_geometry(ll_target_domain, ccrs.PlateCarree())
+    target_domain = multi_poly.geoms[0]
+
+    _, extent, _ = gt.image_for_domain(target_domain, 6)
+
+    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]])
+
+
 def test_quadtree_wts():
     qt = cimgt.QuadtreeTiles()
 
-    extent = [-15, 0.1, 50, 60]
-    target_domain = sgeom.Polygon([[extent[0], extent[1]],
-                                   [extent[2], extent[1]],
-                                   [extent[2], extent[3]],
-                                   [extent[0], extent[3]],
-                                   [extent[0], extent[1]]])
+    ll_target_domain = sgeom.box(-15, 50, 0, 60)
+    multi_poly = qt.crs.project_geometry(ll_target_domain, ccrs.PlateCarree())
+    target_domain = multi_poly.geoms[0]
 
     with assert_raises(ValueError):
         list(qt.find_images(target_domain, 0))
@@ -99,18 +141,17 @@ def test_quadtree_wts():
     with assert_raises(ValueError):
         qt.tileextent('4')
 
-    assert_arr_almost(qt.tileextent(''),
-                      (-180.0, 180.0, 179.02740096, -179.02740096))
+    assert_arr_almost(qt.tileextent(''), KNOWN_EXTENTS[(0, 0, 0)])
     assert_arr_almost(qt.tileextent(qt.tms_to_quadkey((2, 0, 2), google=True)),
-                      (0.0, 90.0, 179.02740096, 89.51370048))
+                      KNOWN_EXTENTS[(2, 0, 2)])
     assert_arr_almost(qt.tileextent(qt.tms_to_quadkey((0, 2, 2), google=True)),
-                      (-180.0, -90.0, 5.68434189e-14, -8.95137005e+01))
-    assert_arr_almost(qt.tileextent(qt.tms_to_quadkey((0, 1, 2), google=True)),
-                      (-180.0, -90.0, 8.95137005e+01, 5.68434189e-14))
+                      KNOWN_EXTENTS[(0, 2, 2)])
+    assert_arr_almost(qt.tileextent(qt.tms_to_quadkey((2, 0, 2), google=True)),
+                      KNOWN_EXTENTS[(2, 0, 2)])
     assert_arr_almost(qt.tileextent(qt.tms_to_quadkey((2, 2, 2), google=True)),
-                      (0.0, 90.0, 5.68434189e-14, -8.95137005e+01))
+                      KNOWN_EXTENTS[(2, 2, 2)])
     assert_arr_almost(qt.tileextent(qt.tms_to_quadkey((8, 9, 4), google=True)),
-                      (0.0, 22.5, -22.37842512, -44.75685024))
+                      KNOWN_EXTENTS[(8, 9, 4)])
 
 
 if __name__ == '__main__':