werthmuller.org

a dash of

A basemap example

10 September 2014

In this first entry of the aftershock series I show a basemap-example. Not that there would not be enough basemap-examples out in the wild; a quick search on the internet reveals many beautiful ones. But you know how it is: there is always something that you want to do differently than the examples that you find. So you go and look at many different ones, start to mix and match, and end up with your own version that you are happy with. I post my final version here for this very reason, it might be one piece in your own jigsaw. (I deliberately call it an example, not a tutorial, as I do not provide enough information nor do I break it down neatly into simple steps to deserve the tag tutorial.)

But before I can go on to the example we have to install the basemap package. After I had already some troubles installing numpy, scipy, matplotlib, and ipython in a virtual environment, I came across more difficulties when I tried to install basemap in a virtual environment. (If you want to install it system-wide it should not be a problem, just use apt-get install python-mpltoolkits.basemap, or your system’s equivalent for apt-get. However, in a virtual environment it is a bit more complicated.)

During my Ph.D. I used the Enthought Python Distribution (EPD) (now called Enthought Canopy) with an academic license, and I was fairly happy with it. However, after I finished I had no need for a distribution, and just used apt-get for the few packages I needed. But recently I started to have dedicated virtual environments for specific tasks. And there it can be painful at times to set up everything; not the least numpy, scipy, matplotlib, ipython, and, what I need here, basemap. It was just now that I came across Anaconda, a completely free Python distribution, that installs into its own virtual environment. And it is dead easy! I won’t delve into a discussion Canopy vs. Anaconda vs. pick your distribution. I just got to know Anaconda, gave it a shot, and so far I am very pleased with it.

To install Anaconda, download the bash script from Continuum Analytics, and then run it, in my case

$ bash Anaconda3-2.0.1-Linux-x86_64.sh

and follow the instructions (the default install-directory is ~/anaconda3; I changed that to ipynb during installation, to be consistent with my previous article). You can then activate and deactivate the environment with

$ source ~/ipynb/bin/activate ~/ipynb
$ source deactivate

and you are good to go with numpy and friends! The download takes a few minutes, but that is the biggest hurdle in the whole process. What we are still missing for this example is the basemap toolkit. With Anaconda this is, again, very easy to install:

(ipynb)$ conda install basemap

For some shapefiles the basemap-function readshapefile had encoding issues that I could not figure out. I used therefore another module, shapefile, that had no such issues. This module is not in the conda-repo, but you can install it with pip in your anaconda environment:

(ipynb) pip install pyshp

Now back to the purpose of this article: a basemap example.

Plotting the location of the North Sea Harding Oil and Gas field

This example is taken out of my Ph.D. thesis. It shows the North Sea bathymetry and the topography of the surrounding countries, the locations of Edinburgh, Bergen, and the Harding Oil and Gas field, the outline of Block 9 (the hydrocarbon exploration areas are divided into blocks, which are made up of the 1° longitudes and latitudes). It also includes the median line, which defines the economic sectors of the countries adjacent to the North Sea.

All data I use here are freely available online:
  • etopo1_bedrock.asc: You can download the etopo-data from the U.S. National Geophysical Data Center (NGDC). For this example, I downloaded a data set with the following parameters:
    1. ETOPO1 (bedrock)
    2. -8 West to 10 East, 64 North to 54 South
    3. ArcGIS ASCII Grid
  • DECC_OFF_Median_Line: The median line is downloaded from the UK Department of Energy & Climate Change (DECC). The coordinates of Harding are from Well 9/23b-7, which are also available at the DECC.
  • The coordinates of Edinburgh and Bergen are taken from Wikipedia.
In [1]:
import shapefile
import numpy as np
from matplotlib import cm, rcParams
import matplotlib.pyplot as plt
from mpl_toolkits.basemap import Basemap
rcParams.update({'font.size': 16}) # Increase font-size

Load data

In [2]:
# Load the topo file to get header information
etopo1name = 'data/basemap/etopo1_bedrock.asc'
topo_file = open(etopo1name, 'r')

# Read header (number of columns and rows, cell-size, and lower left coordinates)
ncols = int(topo_file.readline().split()[1])
nrows = int(topo_file.readline().split()[1])
xllcorner = float(topo_file.readline().split()[1])
yllcorner = float(topo_file.readline().split()[1])
cellsize = float(topo_file.readline().split()[1])
topo_file.close()

# Read in topography as a whole, disregarding first five rows (header)
etopo = np.loadtxt(etopo1name, skiprows=5)

# Data resolution is quite high. I decrease the data resolution 
# to decrease the size of the final figure
dres = 2

# Swap the rows
etopo[:nrows+1, :] = etopo[nrows+1::-1, :]
etopo = etopo[::dres, ::dres]

# Create longitude and latitude vectors for etopo
lons = np.arange(xllcorner, xllcorner+cellsize*ncols, cellsize)[::dres]
lats = np.arange(yllcorner, yllcorner+cellsize*nrows, cellsize)[::dres]

Create the figure

In [3]:
fig = plt.figure(figsize=(8, 6))

# Create basemap, 870 km east-west, 659 km north-south,
# intermediate resolution, Transverse Mercator projection,
# centred around lon/lat 1°/58.5°
m = Basemap(width=870000, height=659000,
            resolution='i', projection='tmerc',
            lon_0=1, lat_0=58.5)

# Draw coast line
m.drawcoastlines(color='k')

# Draw continents and lakes
m.fillcontinents(lake_color='b', color='none')

# Draw a think border around the whole map
m.drawmapboundary(linewidth=3)

# Convert etopo1 coordinates lon/lat in ° to x/y in m
# (From the basemap help: Calling a Basemap class instance with the arguments
# lon, lat will convert lon/lat (in degrees) to x/y map projection coordinates
# (in meters).)
rlons, rlats = m(*np.meshgrid(lons,lats))

# Draw etopo1, first for land and then for the ocean, with different colormaps
llevels = np.arange(-500,2251,100) # check etopo.ravel().max()
lcs = m.contourf(rlons, rlats, etopo, llevels, cmap=cm.terrain)
olevels = np.arange(-3500,1,100) # check etopo.ravel().min()
cso = m.contourf(rlons, rlats, etopo, olevels, cmap=cm.ocean)

# Draw parallels and meridians
m.drawparallels(np.arange(-56,63.,2.), color='.2', labels=[1,0,0,0])
m.drawparallels(np.arange(-55,63.,2.), color='.2', labels=[0,0,0,0])
m.drawmeridians(np.arange(-6.,12.,2.), color='.2', labels=[0,0,0,1])
m.drawmeridians(np.arange(-7.,12.,2.), color='.2', labels=[0,0,0,0])

# Draw Block 9 boundaries
m.plot([1, 2, 2, 1, 1], [59, 59, 60, 60, 59], 'b', linewidth=2, latlon=True)
plt.annotate('9', m(1.1, 59.7), color='b')

# Draw maritime boundaries
m.readshapefile('data/basemap/DECC_OFF_Median_Line', 'medline', linewidth=2)

# Add Harding, Edinburgh, Bergen
# 1. Convert coordinates
EDIx, EDIy = m(-3.188889, 55.953056)
BERx, BERy = m(5.33, 60.389444)
HARx, HARy = m(1.5, 59.29)
# 2. Plot symbol
plt.plot(HARx, HARy, mfc='r', mec='k', marker='s', markersize=10)
plt.plot(EDIx, EDIy, mfc='r', mec='k', marker='o', markersize=10)
plt.plot(BERx, BERy, mfc='r', mec='k', marker='o', markersize=10)
# 3. Plot name
plt.text(EDIx+50000, EDIy+10000,'Edinburgh', color='r')
plt.text(BERx-140000, BERy, 'Bergen', color='r')
plt.text(HARx-160000, HARy, 'Harding', color='r')

plt.show()

The original intent was to just post the above example a few days ago. However, playing around with basemap got me distracted (it always happens with basemap, there are so many interesting things to do). My imminent move from Scotland to Mexico was a good excuse to play around a bit more with maps.

Plotting the outline of Mexico and Switzerland on top of each other

I wanted to plot Mexico and Switzerland on top of each other, with the same scale, to compare the size and relative distances. (If you try that in Google maps you run into troubles, as the scale is not the same; try it!)

There is nowadays a wealth of data in the Open Domain when it comes to administrative borders. For this example I use the following data:
  • General country boundaries: In-built from the basemap module.
  • MEX_adm0, CHE_adm0, CHE_adm1: Borders of Mexico, Switzerland, and the Swiss district Aargau are taken from Global Administrative Areas [GADM]. I think the data from Switzerland are the detailed data from Swisstopo.
  • ne_10m_admin_1_states_provinces: I took the border of Mexico City from Natural Earth (the one from GADM was not as detailed as this one).

More resources can be found on the wiki of the Open Street Map, the U.S. Humanitarian Information Unit [HIU], and an internet search will provide many more.

In [1]:
import shapefile
import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.basemap import Basemap

Settings for both figures

I want that Mexico City and my home village Thalheim overlay (I took the coordinates from Wikipedia). The easiest way to achieve this is to use them as the centre of the map. However, I shift them subsequently by 2° to the south, as Mexico City is not in the centre of Mexico.

In [2]:
# Coordinates (from Wikipedia) and shift
DF = [-99.1333, 19.4333]
TH = [  8.1042, 47.4375]
shift = [0, 2]

# Colours
cMX = '#0000cc' # Blue tone
cCH = '#006600' # Green tone

Figure 1.A: Transverse Mercator

In [3]:
# 4000 km east-west, 3000 km north-south, low resolution, Transverse Mercator
width = 4000000
height = 3000000
res = 'l'
proj = 'tmerc'

fig1a = plt.figure(figsize=(8, 6))

# Create basemaps for Mexico and Switzerland
m_mx = Basemap(width=width, height=height, resolution=res, projection=proj,
            lon_0=DF[0]+shift[0], lat_0=DF[1]+shift[1])
m_ch = Basemap(width=width, height=height, resolution=res, projection=proj,
            lon_0=TH[0]+shift[0], lat_0=TH[1]+shift[1])

# Draw coast lines
m_mx.drawcoastlines(color=cMX, linewidth=1)
m_ch.drawcoastlines(color=cCH, linewidth=1)

# Draw all country borders
m_mx.drawcountries(color=cMX, linewidth=1)
m_ch.drawcountries(color=cCH, linewidth=1)

# Fill continents and lakes
m_mx.fillcontinents(lake_color='none', color=cMX, alpha=.4)
m_ch.fillcontinents(lake_color='none', color=cCH, alpha=.4)

# Draw extra points for the Swiss and Mexican countries to highlight
CHE_adm0 = shapefile.Reader('data/basemap/CHE_adm0')
MEX_adm0 = shapefile.Reader('data/basemap/MEX_adm0')

chlonlat = np.array(CHE_adm0.shape().points)
mxlonlat = np.array(MEX_adm0.shape().points)

m_ch.plot(chlonlat[:, 0], chlonlat[:, 1] , '.', c=cCH, latlon=True, ms=1)
m_mx.plot(mxlonlat[:, 0], mxlonlat[:, 1], '.', c=cMX, latlon=True, ms=1)

# Draw scales to cross-check that the scales are the same
# (shift scale 1600 km to the west from centre, and a bit north/south)
smx = np.array(m_mx(DF[0], DF[1])) - np.array([1600000,  50000])
sch = np.array(m_ch(TH[0], TH[1])) - np.array([1600000, -350000])
ismx = m_mx(smx[0], smx[1], inverse='True')
isch = m_ch(sch[0], sch[1], inverse='True')

m_mx.drawmapscale(ismx[0], ismx[1], DF[0]+shift[0], DF[1]+shift[1],
                  500, barstyle='fancy', fillcolor2=cMX, fontsize=12)
m_ch.drawmapscale(isch[0], isch[1], TH[0]+shift[0], TH[1]+shift[1],
                  500, barstyle='fancy',  fillcolor2=cCH, fontsize=12)

# Add Mexico City and Thalheim AG
m_mx.plot(DF[0], DF[1], 'x', c='r', ms=10, mew=2, latlon=True, label='Thalheim')
m_ch.plot(TH[0], TH[1], '+', c='r', ms=14, mew=2, latlon=True, label='Mexico City')
lg = plt.legend(loc='lower left', fontsize=12, numpoints=1)
lg.get_frame().set_alpha(.8) # A little transparency

# Draw a thick border around the whole
m_mx.drawmapboundary(color='k', linewidth=3)

plt.show()

Figure 1.B: Orthographic

In [4]:
fig1b = plt.figure(figsize=(8, 8))

# Create basemaps for Mexico and Switzerland
m_mx = Basemap(resolution='c', projection='ortho', lon_0=DF[0], lat_0=DF[1])
m_ch = Basemap(resolution='c', projection='ortho', lon_0=TH[0], lat_0=TH[1])

# Draw coast lines
m_mx.drawcoastlines(color=cMX, linewidth=1)
m_ch.drawcoastlines(color=cCH, linewidth=1)

# Draw all country borders
m_mx.drawcountries(color=cMX, linewidth=1)
m_ch.drawcountries(color=cCH, linewidth=1)

# Fill continents and lakes
m_mx.fillcontinents(lake_color='none', color=cMX, alpha=.4)
m_ch.fillcontinents(lake_color='none', color=cCH, alpha=.4)

plt.show()

B. Districts

Figure 2

In [5]:
# 70 km by 70 km, Transverse Mercator, resolution does not
# matter, as we use other data for the borders
width = 70000
height = 70000
dshift = [-.133, -.05]

fig2 = plt.figure(figsize=(8,8))

# Create basemaps for Mexico and Switzerland
m_mx = Basemap(width=width, height=height, projection=proj,
            lon_0=DF[0], lat_0=DF[1]+dshift[0])
m_ch = Basemap(width=width, height=height, projection=proj,
            lon_0=TH[0], lat_0=TH[1]+dshift[1])

# Draw the district of Aargau
CHE_adm1 = shapefile.Reader('data/basemap/CHE_adm1')
iag = [i for i, s in enumerate(CHE_adm1.records()) if 'Aargau' in s][0]
aglonlat = np.array(CHE_adm1.shapes()[iag].points)
m_ch.plot(aglonlat[:, 0], aglonlat[:, 1], '-', c=cCH, latlon=True, lw=2)
agx, agy = m_ch(aglonlat[:, 0], aglonlat[:, 1])
plt.fill(agx, agy, cCH, ec='none', alpha=.4)

# Draw Mexico City DF
MEX_adm1 = shapefile.Reader('data/basemap/ne_10m_admin_1_states_provinces')
idf = [i for i, s in enumerate(MEX_adm1.records()) if 'MX.DF' in s][0]
dflonlat = np.array(MEX_adm1.shape(idf).points)
m_mx.plot(dflonlat[:, 0], dflonlat[:, 1], '-', c=cMX, latlon=True, lw=2)
dfx, dfy = m_mx(dflonlat[:, 0], dflonlat[:, 1])
plt.fill(dfx, dfy, cMX, ec='none', alpha=.4)

# Draw scales to cross-check that the scales are the same
sdf = np.array(m_mx(DF[0], DF[1])) - np.array([28000, 10000])
sth = np.array(m_ch(TH[0], TH[1])) - np.array([28000, 10000])
isdf = m_mx(sdf[0], sdf[1], inverse='True')
isth = m_ch(sth[0], sth[1], inverse='True')
m_mx.drawmapscale(isdf[0], isdf[1], DF[0], DF[1]+dshift[0],
                  4, barstyle='fancy', fillcolor2=cMX, fontsize=12)
m_ch.drawmapscale(isth[0], isth[1], TH[0], TH[1]+dshift[1],
                  4, barstyle='fancy',  fillcolor2=cCH, fontsize=12)

# Draw locations
hPT = [-99.12, 19.47]
pPT = [-99.11, 19.33]
m_mx.plot(hPT[0], hPT[1], '*', c='k', ms=14, mew=1, latlon=True)
m_mx.plot(pPT[0], pPT[1], 'o', c='k', ms=10, mew=1, latlon=True)

# Remove border around the whole
m_mx.drawmapboundary(color='none')

plt.show()

Mexico City (blue) and my home district Aargau (green) are approximately the same size (Wikipedia: DF ~1485 km2, AG ~1404 km2). True, roughly a third to the south and the south-west of the above outline of Mexico City is mountainous area. But then the city extends to the east and the north outside the official DF-boundaries, so the area-comparison roughly holds.

In Mexico, when we go from home (*) to my parents in law (o), we make about the same distance as if we would go from Thalheim to the German border, just without ever leaving the city. Interesting indeed.

As usual, the notebooks with the above code, Basemap.ipynb and MXCH.ipynb, can be found on my GitHub page in the blog-notebooks-repo.

Update: You can find more basemap examples in the Travel Map post.