Cartopy
lastmod 2025-02-14created 2019-11-2220 edits
Cartopy is a Python package designed for geospatial data processing in order to produce maps and other geospatial data analyses.
Cartopy makes use of the powerful PROJ.4, NumPy and Shapely libraries and includes a programmatic interface built on top of Matplotlib for the creation of publication quality maps.
Key features of cartopy are its object oriented projection definitions, and its ability to transform points, lines, vectors, polygons and images between those projections.
You will find cartopy especially useful for large area / small scale data, where Cartesian 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!
Prerequisites
Beautifully simple maps
Cartopy has exposed an interface to enable easy map creation using matplotlib. Creating a basic map is as simple as telling matplotlib to use a specific map projection, and then adding some coastlines to the axes:
import cartopy.crs as ccrs
import matplotlib.pyplot as plt
ax = plt.axes(projection=ccrs.PlateCarree())
ax.coastlines()
plt.show()
Offline cache example
import os
import types
import cartopy.io.img_tiles as img_tiles
import requests
import PIL
class CachedTiler(object):
def __init__(self, tiler):
self.tiler = tiler
# Python2:
# def __getattr__(self, name):
# # Mimic the tiler interface, but for methods, ensure that the "self"
# # that is passed through continues to be CachedTiler, and not the
# # contained tiler instance.
# attr = getattr(self.tiler, name, None)
# if isinstance(attr, types.MethodType):
# attr = types.MethodType(attr.im_func, self)
# return attr
def __getattr__(self, name):
"""
Python3 version:
"""
attr = getattr(self.tiler, name, None)
if isinstance(attr, types.MethodType):
attr = types.MethodType(attr.__func__, self)
return attr
def get_image(self, tile):
tileset_name = '{}'.format(self.tiler.__class__.__name__.lower())
cache_dir = os.path.expanduser(os.path.join('~/', 'image_tiles', tileset_name))
if not os.path.exists(cache_dir):
os.makedirs(cache_dir)
tile_fname = os.path.join(cache_dir, '_'.join(str(v) for v in tile) + '.png')
if not os.path.exists(tile_fname):
response = requests.get(self._image_url(tile),
stream=True)
with open(tile_fname, "wb") as fh:
for chunk in response:
fh.write(chunk)
with open(tile_fname, 'rb') as fh:
img = PIL.Image.open(fh)
img = img.convert(self.desired_tile_form)
return img, self.tileextent(tile), 'lower'
from cartopy.io.img_tiles import StamenTerrain
import matplotlib.pyplot as plt
actual_tiler = StamenTerrain()
tiler = CachedTiler(actual_tiler)
mercator = tiler.crs
ax = plt.axes(projection=mercator)
ax.set_extent([-90, -73, 22, 34])
ax.add_image(tiler, 6)
ax.coastlines('10m')
plt.show()
OSM Preview
import cartopy.crs as ccrs
from cartopy.io.img_tiles import OSM
import matplotlib.pyplot as plt
osm_tiles = OSM()
plt.figure(figsize=(16, 16))
# Use the tile's projection for the underlying map.
ax = plt.axes(projection=osm_tiles.crs)
# Specify a region of interest, in this case, Cardiff.
ax.set_extent([-3.3849, -3.0424, 51.4260, 51.5842],
ccrs.PlateCarree())
# Add the tiles at zoom level 12.
ax.add_image(osm_tiles, 12)
ax.coastlines('10m')
plt.show()
Longitude and Latitude guidline
from cartopy.mpl.gridliner import LONGITUDE_FORMATTER, LATITUDE_FORMATTER
ax = plt.axes(projection=request.crs)
gl = ax.gridlines(draw_labels=True, alpha=0.2)
gl.xlabels_top = gl.ylabels_right = False
gl.xformatter = LONGITUDE_FORMATTER
gl.yformatter = LATITUDE_FORMATTER
Draw point and label
Demonstrates cartopy’s ability to draw map tiles which are downloaded on demand from the MapQuest tile server. Internally these tiles are then combined into a single image and displayed in the cartopy GeoAxes.
import matplotlib.pyplot as plt
from matplotlib.transforms import offset_copy
import cartopy.crs as ccrs
import cartopy.io.img_tiles as cimgt
# ...
# Add a marker for the Eyjafjallajökull volcano.
plt.plot(-19.613333, 63.62, marker='o', color='yellow', markersize=12, alpha=0.7, transform=ccrs.Geodetic())
# Use the cartopy interface to create a matplotlib transform object
# for the Geodetic coordinate system. We will use this along with
# matplotlib's offset_copy function to define a coordinate system which
# translates the text by 25 pixels to the left.
geodetic_transform = ccrs.Geodetic()._as_mpl_transform(ax)
text_transform = offset_copy(geodetic_transform, units='dots', x=-25)
# Add text 25 pixels to the left of the volcano.
plt.text(-19.613333, 63.62, u'Eyjafjallajökull',
verticalalignment='center', horizontalalignment='right',
transform=text_transform,
bbox=dict(facecolor='wheat', alpha=0.5, boxstyle='round'))
GoogleMap Cache example
Renders Osong, Korea.
import os
import types
import six
import requests
import numpy as np
import matplotlib as mpl
import matplotlib.pyplot as plt
import cartopy.io.img_tiles as img_tiles
import cartopy.crs as ccrs
from multiprocessing import Process, Lock
from PIL import Image
from cartopy.mpl.gridliner import LONGITUDE_FORMATTER, LATITUDE_FORMATTER
mutex = Lock()
class CachedTiler(object):
def __init__(self, tiler):
self.tiler = tiler
# Python2:
# def __getattr__(self, name):
# # Mimic the tiler interface, but for methods, ensure that the "self"
# # that is passed through continues to be CachedTiler, and not the
# # contained tiler instance.
# attr = getattr(self.tiler, name, None)
# if isinstance(attr, types.MethodType):
# attr = types.MethodType(attr.im_func, self)
# return attr
def __getattr__(self, name):
"""
Python3 version:
"""
attr = getattr(self.tiler, name, None)
if isinstance(attr, types.MethodType):
attr = types.MethodType(attr.__func__, self)
return attr
def get_image(self, tile):
tileset_name = '{}'.format(self.tiler.__class__.__name__.lower())
cache_dir = os.path.expanduser(os.path.join(os.getcwd(), 'image_tiles', tileset_name))
if not os.path.exists(cache_dir):
os.makedirs(cache_dir)
tile_fname = os.path.join(cache_dir, '_'.join(str(v) for v in tile) + '.png')
if not os.path.exists(tile_fname):
#with mutex:
url = self._image_url(tile)
print('Download Image URL: {}'.format(url))
response = requests.get(url, stream=True)
print('Result: {}'.format(response))
with open(tile_fname, "wb") as fh:
for chunk in response:
fh.write(chunk)
with open(tile_fname, 'rb') as fh:
img = Image.open(fh)
img = img.convert(self.desired_tile_form)
return img, self.tileextent(tile), 'lower'
def new_get_image(self, tile):
if six.PY3:
from urllib.request import urlopen, Request
else:
from urllib2 import urlopen
url = self._image_url(tile) # added by H.C. Winsemius
req = Request(url) # added by H.C. Winsemius
# req.add_header('User-agent', 'your bot 0.1')
# fh = urlopen(url) # removed by H.C. Winsemius
fh = urlopen(req)
im_data = six.BytesIO(fh.read())
fh.close()
img = Image.open(im_data)
img = img.convert(self.desired_tile_form)
return img, self.tileextent(tile), 'lower'
#img_tiles.GoogleWTS.get_image = new_get_image
#request = img_tiles.GoogleTiles()
#extent = [-89, -88, 41, 42]
#ax = plt.axes(projection=request.crs)
#ax.set_extent(extent)
#ax.add_image(request, 8)
#plt.show()
import cartopy.crs as ccrs
from cartopy.io.img_tiles import OSM
import matplotlib.pyplot as plt
OSM.get_image = new_get_image
#actual_tiler = OSM()
actual_tiler = img_tiles.GoogleTiles()
osm_tiles = CachedTiler(actual_tiler)
plt.figure(figsize=(16, 16))
# fig = plt.figure(figsize=(9, 13))
# Use the tile's projection for the underlying map.
ax = plt.axes(projection=osm_tiles.crs)
gl = ax.gridlines(draw_labels=True, alpha=0.2)
gl.xlabels_top = gl.ylabels_right = False
gl.xformatter = LONGITUDE_FORMATTER
gl.yformatter = LATITUDE_FORMATTER
#extent = [-39, -38.25, -13.25, -12.5]
#extent = [36.6412, 127.2951, 36.6258, 127.3273]
extent = [127.2951, 127.3273, 36.6412, 36.6258]
#extent = [36.38, 127.17, 36.37, 127.19]
# Specify a region of interest, in this case, Cardiff.
# ax.set_extent([-3.3849, -3.0424, 51.4260, 51.5842], ccrs.PlateCarree())
# ax.set_extent([36.6412, 127.2951, 36.6258, 127.3273], ccrs.PlateCarree())
#ax.set_extent(extent, osm_tiles.crs)
ax.set_extent(extent, ccrs.PlateCarree())
#print(ax.get_extent())
#ax.set_xlim([extent[0], extent[2]])
#ax.set_ylim([extent[1], extent[3]])
# Add the tiles at zoom level 12.
ax.add_image(osm_tiles, 16)
ax.coastlines('10m')
plt.show()
Troubleshooting
Unable to determine GEOS version. Ensure you have 3.3.3 or later installed, or installation may fail
Installing with pip may produce an error like the following.
$ pip install cartopy
Collecting cartopy
Downloading https://files.pythonhosted.org/packages/e5/92/fe8838fa8158931906dfc4f16c5c1436b3dd2daf83592645b179581403ad/Cartopy-0.17.0.tar.gz (8.9MB)
|████████████████████████████████| 8.9MB 848kB/s
Installing build dependencies ... done
Getting requirements to build wheel ... error
ERROR: Complete output from command /Users/your/.pyenv/versions/3.6.8/envs/opy-your-3.6.8/bin/python /Users/your/.pyenv/versions/3.6.8/envs/opy-your-3.6.8/lib/python3.6/site-packages/pip/_vendor/pep517/_in_process.py get_requires_for_build_wheel /var/folders/6v/598t63jj15lbcn_nq9nm12ww0000gn/T/tmprp39fedd:
ERROR: setup.py:171: UserWarning: Unable to determine GEOS version. Ensure you have 3.3.3 or later installed, or installation may fail.
'.'.join(str(v) for v in GEOS_MIN_VERSION), ))
Proj 4.9.0 must be installed.
----------------------------------------
ERROR: Command "/Users/your/.pyenv/versions/3.6.8/envs/opy-your-3.6.8/bin/python /Users/your/.pyenv/versions/3.6.8/envs/opy-your-3.6.8/lib/python3.6/site-packages/pip/_vendor/pep517/_in_process.py get_requires_for_build_wheel /var/folders/6v/598t63jj15lbcn_nq9nm12ww0000gn/T/tmprp39fedd" failed with error code 1 in /private/var/folders/6v/598t63jj15lbcn_nq9nm12ww0000gn/T/pip-install-y2vdys54/cartopy
You need to install geos and proj.
On Ubuntu, install them as follows.
$ apt-get install libproj-dev proj-data proj-bin
$ apt-get install libgeos-dev
$ pip install cython
$ pip install cartopy
ValueError: A non-empty list of tiles should be provided to merge
import numpy as np
import matplotlib as mpl
import matplotlib.pyplot as plt
import cartopy.crs as ccrs
import cartopy.io.img_tiles as cimgt
import six
from PIL import Image
def new_get_image(self, tile):
if six.PY3:
from urllib.request import urlopen, Request
else:
from urllib2 import urlopen
url = self._image_url(tile) # added by H.C. Winsemius
req = Request(url) # added by H.C. Winsemius
req.add_header('User-agent', 'your bot 0.1')
# fh = urlopen(url) # removed by H.C. Winsemius
fh = urlopen(req)
im_data = six.BytesIO(fh.read())
fh.close()
img = Image.open(im_data)
img = img.convert(self.desired_tile_form)
return img, self.tileextent(tile), 'lower'
cimgt.GoogleWTS.get_image = new_get_image
request = cimgt.GoogleTiles()
extent = [-89, -88, 41, 42]
ax = plt.axes(projection=request.crs)
ax.set_extent(extent)
ax.add_image(request, 8)
plt.show()
OSM Example:
Nothing is drawn after GeoAxes.clear()
When you redraw an image after GeoAxes.clear(), the cartopy.mpl.GeoAxes._done_img_factory variable gets set to True, so the self.imshow() call in cartopy.mpl.GeoAxes.draw() is SKIPPED. The fix is to force that flag back to False.
class PlotCanvasPanel(wx.Panel):
def __init__(self, *args, **kwds):
wx.Panel.__init__(self, *args, **kwds)
self.figure = Figure()
self.figure.subplots_adjust(left=0.0, bottom=0.0, right=1.0, top=1.0, wspace=0.0, hspace=0.0)
self.axes = self.figure.add_subplot(111, projection=GoogleTiles().crs)
# ...
self.canvas = FigureCanvas(self, -1, self.figure)
self.sizer = wx.BoxSizer(wx.VERTICAL)
self.sizer.Add(self.canvas, proportion=1, border=0, flag=wx.ALL | wx.GROW)
self.SetSizer(self.sizer)
self.Fit()
def draw(self):
self.axes: GeoAxes
self.axes.clear() # CLEAR !!
self.axes._done_img_factory = False # Force flag setting.
self.axes.add_image(CachedTiler(GoogleTiles()), self.zoom_level)
self.axes.set_extent(self.extent, ccrs.PlateCarree())
self.axes.coastlines('10m')
# ...
self.canvas.draw()
See also
Favorite site
- Cartopy web site
- High resolution coastline
- Background image in cartopy
- Basics: Quick + Simple maps with cartopy
- Plotting Actual Things: geopandas and cartopy