Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bug fixes to arcgisimage #598

Merged
merged 7 commits into from
Feb 14, 2024
168 changes: 90 additions & 78 deletions packages/basemap/src/mpl_toolkits/basemap/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4257,45 +4257,56 @@ def pil_to_array(*args, **kwargs):
im,c = self._cliplimb(ax,im)
return im

def arcgisimage(self,server='http://server.arcgisonline.com/ArcGIS',\
service='World_Imagery',xpixels=400,ypixels=None,\
dpi=96,cachedir=None,verbose=False,**kwargs):
"""
Retrieve an image using the ArcGIS Server REST API and display it on
the map. In order to use this method, the Basemap instance must be
created using the ``epsg`` keyword to define the map projection, unless
the ``cyl`` projection is used (in which case the epsg code 4326 is
assumed).
def arcgisimage(self, server="http://server.arcgisonline.com/ArcGIS",
service="World_Imagery", xpixels=400, ypixels=None,
dpi=96, cachedir=None, verbose=False, **kwargs):
r"""Display background image using ArcGIS Server REST API.

.. tabularcolumns:: |l|L|
In order to use this method, the :class:`Basemap` instance
must be created using the ``epsg`` keyword to define the
map projection, unless the "cyl" projection is used (in
which case the EPSG code 4326 is assumed).

============== ====================================================
Keywords Description
============== ====================================================
server web map server URL (default
http://server.arcgisonline.com/ArcGIS).
service service (image type) hosted on server (default
'World_Imagery', which is NASA 'Blue Marble'
image).
xpixels requested number of image pixels in x-direction
(default 400).
ypixels requested number of image pixels in y-direction.
Default (None) is to infer the number from
from xpixels and the aspect ratio of the
map projection region.
dpi The device resolution of the exported image (dots per
inch, default 96).
cachedir An optional directory to use as cache folder for the retrieved images.
verbose if True, print URL used to retrieve image (default
False).
============== ====================================================
Parameters
----------

Extra keyword ``ax`` can be used to override the default axis instance.
server : str, optional
base URL of the web map server

returns a matplotlib.image.AxesImage instance.
service : str, optional
service (image type) hosted by the server

xpixels : int, optional
requested number of image pixels in the `x`-direction

ypixels : int, optional
requested number of image pixels in the `y`-direction;
if not given, it is inferred from ``xpixels`` and the
aspect ratio of the map projection region

dpi : int, optional
device resolution of the exported image

cachedir : str, optional
if given, directory to use as cache folder for the images
retrieved from the server

verbose : bool, optional
if True, print debugging information

\**kwargs : dict, optional
keyword-only arguments; currently, only ``ax`` is supported
to override the default :class:`matplotlib.axes.Axes`
instance

Returns
-------

aximg : matplotlib.image.AxesImage
image axes instance
"""

# fix PIL import on some versions of OSX and scipy
# Fix PIL import on some versions of OSX and scipy.
try:
from PIL import Image
except ImportError:
Expand All @@ -4305,70 +4316,71 @@ def arcgisimage(self,server='http://server.arcgisonline.com/ArcGIS',\
raise ImportError("arcgisimage method requires PIL "
"(http://pillow.readthedocs.io)")

if not hasattr(self,'epsg'):
if not hasattr(self, "epsg"):
raise ValueError("the Basemap instance must be created using "
"an EPSG code (http://spatialreference.org) "
"in order to use the wmsmap method")
ax = kwargs.pop('ax', None) or self._check_ax()
# find the x,y values at the corner points.

ax = kwargs.pop("ax", None) or self._check_ax()

# Find the `(x, y)` values at the corner points.
with warnings.catch_warnings():
warnings.simplefilter("ignore", category=FutureWarning)
p = pyproj.Proj(init="epsg:%s" % self.epsg, preserve_units=True)
xmin,ymin = p(self.llcrnrlon,self.llcrnrlat)
xmax,ymax = p(self.urcrnrlon,self.urcrnrlat)
xmin, ymin = p(self.llcrnrlon, self.llcrnrlat)
xmax, ymax = p(self.urcrnrlon, self.urcrnrlat)
if self.projection in _cylproj:
Dateline =\
_geoslib.Point(self(180.,0.5*(self.llcrnrlat+self.urcrnrlat)))
hasDateline = Dateline.within(self._boundarypolyxy)
if hasDateline:
dateline = _geoslib.Point(self(180., 0.5 * (self.llcrnrlat + self.urcrnrlat)))
if dateline.within(self._boundarypolyxy):
raise ValueError("arcgisimage cannot handle images that cross "
"the dateline for cylindrical projections")
# ypixels not given, find by scaling xpixels by the map aspect ratio.

# If ypixels is not given, compute it with xpixels and aspect ratio.
if ypixels is None:
ypixels = int(self.aspect*xpixels)
# construct a URL using the ArcGIS Server REST API.
basemap_url = \
"%s/rest/services/%s/MapServer/export?\
bbox=%s,%s,%s,%s&\
bboxSR=%s&\
imageSR=%s&\
size=%s,%s&\
dpi=%s&\
format=png32&\
transparent=true&\
f=image" %\
(server,service,xmin,ymin,xmax,ymax,self.epsg,self.epsg,xpixels,ypixels,dpi)
# print URL?
if verbose: print(basemap_url)

if cachedir != None:
ypixels = int(self.aspect * xpixels)

# Construct a URL using the ArcGIS Server REST API.
basemap_url = "".join([
"%s/rest/services/%s/MapServer/export?",
"bbox=%s,%s,%s,%s&",
"bboxSR=%s&",
"imageSR=%s&",
"size=%s,%s&",
"dpi=%s&",
"format=png32&",
"transparent=true&",
"f=image",
]) % (server, service, xmin, ymin, xmax, ymax, self.epsg, self.epsg, xpixels, ypixels, dpi)

# Print URL in verbose mode.
if verbose: # pragma: no cover
print(basemap_url)

# Try to return fast if cache is enabled.
if cachedir is not None:
# Generate a filename for the cached file.
filename = "%s-bbox-%s-%s-%s-%s-bboxsr%s-imagesr%s-size-%s-%s-dpi%s.png" %\
(service,xmin,ymin,xmax,ymax,self.epsg,self.epsg,xpixels,ypixels,dpi)

# Check if the cache directory exists, if not create it.
if not os.path.exists(cachedir):
os.makedirs(cachedir)

# Check if the image is already in the cachedir folder.
cache_path = cachedir + filename

if os.path.isfile(cache_path) and verbose:
print('Image already in cache')
filename = "%s-bbox-%s-%s-%s-%s-bboxsr%s-imagesr%s-size-%s-%s-dpi%s.png" % \
(service, xmin, ymin, xmax, ymax, self.epsg, self.epsg, xpixels, ypixels, dpi)
# Return fast if the image is already in the cache.
cache_path = os.path.join(cachedir, filename)
if os.path.isfile(cache_path):
if verbose: # pragma: no cover
print("Image already in cache")
img = Image.open(cache_path)
return basemap.imshow(img, ax=ax, origin='upper')
return self.imshow(img, ax=ax, origin="upper")

# Retrieve image from remote server.
# Retrieve image from the remote server.
import contextlib
conn = urlopen(basemap_url)
with contextlib.closing(conn):
img = Image.open(conn)
# Save to cache if requested.
if cachedir != None:
if cachedir is not None:
# Check if the cache directory exists, if not create it.
if not os.path.exists(cachedir):
os.makedirs(cachedir)
img.save(cache_path)

# Return AxesImage instance.
return self.imshow(img, ax=ax, origin='upper')
return self.imshow(img, ax=ax, origin="upper")

def wmsimage(self,server,\
xpixels=400,ypixels=None,\
Expand Down
46 changes: 46 additions & 0 deletions packages/basemap/test/mpl_toolkits/basemap/test_Basemap.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
"""Import test for the :mod:`mpl_toolkits.basemap.Basemap` class."""

import os
import shutil
import tempfile
import datetime as dt
try:
import unittest2 as unittest
Expand Down Expand Up @@ -145,6 +148,49 @@ def test_arcgisimage_with_cyl(self, axs=None, axslen0=10):
axs_children = axs_obj.get_children()
self.assertEqual(len(axs_children), axslen0 + 1)

@unittest.skipIf(PIL is None, reason="pillow unavailable")
def test_arcgisimage_with_cyl_using_cache(self, existing=False, axs=None, axslen0=10):
"""Test showing an ArcGIS image as background."""

axs_obj = plt.gca() if axs is None else axs
axs_children = axs_obj.get_children()
self.assertEqual(len(axs_children), axslen0)

bmap = Basemap(ax=axs, projection="cyl", resolution=None,
llcrnrlon=-90, llcrnrlat=30,
urcrnrlon=-60, urcrnrlat=60)

# Create cache directory string and check it is empty.
tmpdir = tempfile.mkdtemp(prefix="tmp-basemap-cachedir-")
cachedir = tmpdir if existing else os.path.join(tmpdir, "cachedir")
if os.path.isdir(cachedir):
self.assertEqual(len(os.listdir(cachedir)), 0)

try:
# Check that the first call populates the cache.
img = bmap.arcgisimage(verbose=False, cachedir=cachedir)
self.assertEqual(len(os.listdir(cachedir)), 1)
# Check output properties after the first call.
self.assertIsInstance(img, AxesImage)
axs_children = axs_obj.get_children()
self.assertEqual(len(axs_children), axslen0 + 1)
# Check that the second call does not update the cache.
img = bmap.arcgisimage(verbose=False, cachedir=cachedir)
self.assertEqual(len(os.listdir(cachedir)), 1)
# Check output properties after the second call.
self.assertIsInstance(img, AxesImage)
axs_children = axs_obj.get_children()
self.assertEqual(len(axs_children), axslen0 + 2)
finally:
if os.path.isdir(tmpdir):
shutil.rmtree(tmpdir)

@unittest.skipIf(PIL is None, reason="pillow unavailable")
def test_arcgisimage_with_cyl_using_cache_already_existing(self):
"""Test showing an ArcGIS image as background."""

self.test_arcgisimage_with_cyl_using_cache(existing=True)

def _test_basemap_data_warpimage(self, method, axs=None, axslen0=10):
"""Test drawing a map background from :mod:`basemap_data`."""

Expand Down