Skip to content

Commit

Permalink
Add a user/site specified background image directory (#815)
Browse files Browse the repository at this point in the history
* Added code to include user defined background image library.
  • Loading branch information
Malcolm Brooks authored and marqh committed Jan 10, 2017
1 parent 8c3d7d7 commit bf37d53
Show file tree
Hide file tree
Showing 3 changed files with 202 additions and 1 deletion.
7 changes: 7 additions & 0 deletions lib/cartopy/data/raster/natural_earth/images.json
Original file line number Diff line number Diff line change
@@ -0,0 +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/",
"__projection__": "PlateCarree",
"low": "50-natural-earth-1-downsampled.png" }
}
190 changes: 189 additions & 1 deletion lib/cartopy/mpl/geoaxes.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# (C) British Crown Copyright 2011 - 2016, Met Office
# (C) British Crown Copyright 2011 - 2017, Met Office
#
# This file is part of cartopy.
#
Expand Down Expand Up @@ -68,6 +68,18 @@
"""

_BACKG_IMG_CACHE = {}
"""
A dictionary of pre-loaded images for large background images, kept as a
dictionary so that large images are loaded only once.
"""

_USER_BG_IMGS = {}
"""
A dictionary of background images in the directory specified by the
CARTOPY_USER_BACKGROUNDS environment variable.
"""


# XXX call this InterCRSTransform
class InterProjectionTransform(mtransforms.Transform):
Expand Down Expand Up @@ -784,6 +796,182 @@ def stock_img(self, name='ne_shaded'):
else:
raise ValueError('Unknown stock image %r.' % name)

def background_img(self, name='ne_shaded', resolution='low', extent=None,
cache=False):
"""
Adds a background image to the map, from a selection of pre-prepared
images held in a directory specified by the CARTOPY_USER_BACKGROUNDS
environment variable. That directory is checked with
func:`self.read_user_background_images` and needs to contain a JSON
file which defines for the image metadata.
Kwargs:
* name - the name of the image to read according to the contents
of the JSON file. A typical file might have, for instance:
'ne_shaded' : Natural Earth Shaded Relief
'ne_grey' : Natural Earth Grey Earth
* resolution - the resolution of the image to read, according to
the contents of the JSON file. A typical file might
have the following for each name of the image:
'low', 'med', 'high', 'vhigh', 'full'.
* extent - using a high resolution background image, zoomed into
a small area, will take a very long time to render as
the image is prepared globally, even though only a small
area is used. Adding the extent will only render a
particular geographic region. Specified as
[longitude start, longitude end,
latitude start, latitude end].
e.g. [-11, 3, 48, 60] for the UK
or [167.0, 193.0, 47.0, 68.0] to cross the date line.
* cache - logical flag as to whether or not to cache the loaded
images into memory. The images are stored before the
extent is used.
"""
# read in the user's background image directory:
if len(_USER_BG_IMGS) == 0:
self.read_user_background_images()
import os
bgdir = os.getenv('CARTOPY_USER_BACKGROUNDS')
if bgdir is None:
bgdir = os.path.join(config["repo_data_dir"],
'raster', 'natural_earth')
# now get the filename we want to use:
try:
fname = _USER_BG_IMGS[name][resolution]
except KeyError:
msg = ('Image "{}" and resolution "{}" are not present in '
'the user background image metadata in directory "{}"')
raise ValueError(msg.format(name, resolution, bgdir))
# Now obtain the image data from file or cache:
fpath = os.path.join(bgdir, fname)
if cache:
if fname in _BACKG_IMG_CACHE:
img = _BACKG_IMG_CACHE[fname]
else:
img = imread(fpath)
_BACKG_IMG_CACHE[fname] = img
else:
img = imread(fpath)
if len(img.shape) == 2:
# greyscale images are only 2-dimensional, so need replicating
# to 3 colour channels:
img = np.repeat(img[:, :, np.newaxis], 3, axis=2)
# now get the projection from the metadata:
if _USER_BG_IMGS[name]['__projection__'] == 'PlateCarree':
# currently only PlateCarree is defined:
source_proj = ccrs.PlateCarree()
else:
raise NotImplementedError('Background image projection undefined')

if extent is None:
# not specifying an extent, so return all of it:
return self.imshow(img, origin='upper',
transform=source_proj,
extent=[-180, 180, -90, 90])
else:
# return only a subset of the image:
# set up coordinate arrays:
d_lat = 180.0 / img.shape[0]
d_lon = 360.0 / img.shape[1]
# latitude starts at 90N for this image:
lat_pts = (np.arange(img.shape[0]) * -d_lat - (d_lat / 2.0)) + 90.0
lon_pts = (np.arange(img.shape[1]) * d_lon + (d_lon / 2.0)) - 180.0

# which points are in range:
lat_in_range = np.logical_and(lat_pts >= extent[2],
lat_pts <= extent[3])
if extent[0] < 180 and extent[1] > 180:
# we have a region crossing the dateline
# this is the westerly side of the input image:
lon_in_range1 = np.logical_and(lon_pts >= extent[0],
lon_pts <= 180.0)
img_subset1 = img[lat_in_range, :, :][:, lon_in_range1, :]
# and the eastward half:
lon_in_range2 = lon_pts + 360. <= extent[1]
img_subset2 = img[lat_in_range, :, :][:, lon_in_range2, :]
# now join them up:
img_subset = np.concatenate((img_subset1, img_subset2), axis=1)
# now define the extent for output that matches those points:
ret_extent = [lon_pts[lon_in_range1][0] - d_lon / 2.0,
lon_pts[lon_in_range2][-1] + d_lon / 2.0 + 360,
lat_pts[lat_in_range][-1] - d_lat / 2.0,
lat_pts[lat_in_range][0] + d_lat / 2.0]
else:
# not crossing the dateline, so just find the region:
lon_in_range = np.logical_and(lon_pts >= extent[0],
lon_pts <= extent[1])
img_subset = img[lat_in_range, :, :][:, lon_in_range, :]
# now define the extent for output that matches those points:
ret_extent = [lon_pts[lon_in_range][0] - d_lon / 2.0,
lon_pts[lon_in_range][-1] + d_lon / 2.0,
lat_pts[lat_in_range][-1] - d_lat / 2.0,
lat_pts[lat_in_range][0] + d_lat / 2.0]

return self.imshow(img_subset, origin='upper',
transform=source_proj,
extent=ret_extent)

def read_user_background_images(self, verify=True):
"""
Reads the metadata in the specified CARTOPY_USER_BACKGROUNDS
environment variable to populate the dictionaries for background_img.
If CARTOPY_USER_BACKGROUNDS is not set then by default the image in
lib/cartopy/data/raster/natural_earth/ will be made available.
The metadata should be a standard JSON file which specifies a two
level dictionary. The first level is the image type.
For each image type there must be the fields:
__comment__, __source__ and __projection__
and then an element giving the filename for each resolution.
An example JSON file can be found at:
lib/cartopy/data/raster/natural_earth/images.json
"""
import os
import json

bgdir = os.getenv('CARTOPY_USER_BACKGROUNDS')
if bgdir is None:
bgdir = os.path.join(config["repo_data_dir"],
'raster', 'natural_earth')
json_file = os.path.join(bgdir, 'images.json')

with open(json_file, 'r') as js_obj:
dict_in = json.load(js_obj)
for img_type in dict_in:
_USER_BG_IMGS[img_type] = dict_in[img_type]

if verify:
required_info = ['__comment__', '__source__', '__projection__']
for img_type in _USER_BG_IMGS:
if img_type == '__comment__':
# the top level comment doesn't need verifying:
pass
else:
# check that this image type has the required info:
for required in required_info:
if required not in _USER_BG_IMGS[img_type]:
msg = ('User background metadata file "{}", '
'image type "{}", does not specify '
'metadata item "{}"')
raise ValueError(msg.format(json_file, img_type,
required))
for resln in _USER_BG_IMGS[img_type]:
# the required_info items are not resolutions:
if resln not in required_info:
img_it_r = _USER_BG_IMGS[img_type][resln]
test_file = os.path.join(bgdir, img_it_r)
if not os.path.isfile(test_file):
msg = 'File "{}" not found'
raise ValueError(msg.format(test_file))

def add_raster(self, raster_source, **slippy_image_kwargs):
"""
Add the given raster source to the GeoAxes.
Expand Down
6 changes: 6 additions & 0 deletions lib/cartopy/tests/mpl/test_images.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,12 @@ def test_pil_Image():
extent=[-180, 180, -90, 90])


@ImageTesting(['imshow_natural_earth_ortho'], tolerance=0.7)
def test_background_img():
ax = plt.axes(projection=ccrs.Orthographic())
ax.background_img(name='ne_shaded', resolution='low')


if __name__ == '__main__':
import nose
nose.runmodule(argv=['-sv', '--with-doctest'], exit=False)

0 comments on commit bf37d53

Please sign in to comment.