Mask cube with features - cartopy

I want to plot data from a global cube, but only for a list of countries. So I select a subcube according to the countries' "bounding box".
So far so good. What I'm looking for is an easy way to mask out all points of a cube which do not fall in any of my countries (which are represented as features), so that only those points of the cube which lie within any of my features are plotted.
Any idea is greatly appreciated =)

You can achieve this directly at the plotting stage rather than masking the cube within iris. I've approached this by setting the clip path of the artist returned by pcolor. The method is to create a list of geometries from features (in this case countries from Natural Earth, they could be from a shapefile) then transform these geometries into a matplotlib path which the image can be clipped to. I'll detail this method, and hopefully this will be enough to get you started:
I first defined a function to retrieve the Shapely geometries corresponding to given country names, the geometries come from the Natural Earth 110m administrative boundaries shapefile, access through the cartopy interface.
I then defined a second function which is a wrapper around the iris.plot.pcolor function which makes the plot and clips it to the given geometries.
Now all I need to do is set up the plot as normal, but use the plotting wrapper instead of directly calling the iris.plot.pcolor function.
Here is a complete example:
import cartopy.crs as ccrs
from cartopy.io.shapereader import natural_earth, Reader
from cartopy.mpl.patch import geos_to_path
import iris
import iris.plot as iplt
import matplotlib.pyplot as plt
from matplotlib.path import Path
def get_geometries(country_names):
"""
Get an iterable of Shapely geometries corrresponding to given countries.
"""
# Using the Natural Earth feature interface provided by cartopy.
# You could use a different source, all you need is the geometries.
shape_records = Reader(natural_earth(resolution='110m',
category='cultural',
name='admin_0_countries')).records()
geoms = []
for country in shape_records:
if country.attributes['name_long'] in country_names:
try:
geoms += country.geometry
except TypeError:
geoms.append(country.geometry)
return geoms, ccrs.PlateCarree()._as_mpl_transform
def pcolor_mask_geoms(cube, geoms, transform):
path = Path.make_compound_path(*geos_to_path(geoms))
im = iplt.pcolor(cube)
im.set_clip_path(path, transform=transform)
# First plot the full map:
cube = iris.load_cube(iris.sample_data_path('air_temp.pp'))
plt.figure(figsize=(12, 6))
ax1 = plt.axes(projection=ccrs.PlateCarree())
ax1.coastlines()
iplt.pcolor(cube)
# Now plot just the required countries:
plt.figure(figsize=(12, 6))
ax2 = plt.axes(projection=ccrs.PlateCarree())
ax2.coastlines()
countries = [
'United States',
'United Kingdom',
'Saudi Arabia',
'South Africa',
'Nigeria']
geoms, transform = get_geometries(countries)
pcolor_mask_geoms(cube, geoms, transform(ax2))
plt.show()
The results of which look like this:
If you want to use iris.plot.pcolormesh instead you will need to modify the plotting function a little bit. This is dues to a workaround for a matplotlib issue that is currently included in cartopy. The modified version would look like this:
def pcolor_mask_geoms(cube, geoms, transform):
path = Path.make_compound_path(*geos_to_path(geoms))
im = iplt.pcolormesh(cube)
im.set_clip_path(path, transform=transform)
try:
im._wrapped_collection_fix.set_clip_path(path, transform)
except AttributeError:
pass

Related

How to overlay hatches on shapefile with condition?

I've been trying to plot hatches (like this pattern, "//") on polygons of a shapefile, based on a condition. The condition is that whichever polygon values ("Sig") are greater than equal to 0.05, there should be a hatch pattern for them. Unfortunately the resulting map doesn't meet my requirements.
So I first plot the "AMOTL" variable and then wanted to plot the hatches (variable Sig) on top of them (if the values are greater than equal to 0.05). I have used the following code:
import contextily as ctx
from mpl_toolkits.basemap import Basemap
import matplotlib.pyplot as plt
import numpy as np
import matplotlib.ticker as ticker
from matplotlib.patches import Ellipse, Polygon
data = gpd.read_file("mapsignif.shp")
Sig = data.loc[data["Sig"].ge(0.05)]
data.loc[data["AMOTL"].eq(0), "AMOTL"] = np.nan
ax = data.plot(
figsize=(12, 10),
column="AMOTL",
legend=True,
cmap="bwr",
vmin = -1,
vmax= 1,
missing_kwds={"color":"white"},
)
Sig.plot(
ax=ax,
hatch='//'
)
map = Basemap(
llcrnrlon=-50,
llcrnrlat=30,
urcrnrlon=50.0,
urcrnrlat=85.0,
resolution="i",
lat_0=39.5,
lon_0=1,
)
map.fillcontinents(color="lightgreen")
map.drawcoastlines()
map.drawparallels(np.arange(10,90,20),labels=[1,1,1,1])
map.drawmeridians(np.arange(-180,180,30),labels=[1,1,0,1])
Now the problem is that my original image (on which I want to plot the hatches) is different from the image resulting from the above code:
Original Image -
Resultant image from above code:
I basically want to plot hatches on that first image. This topic is similar to correlation plots where you have places with hatches (if the p-value is greater than 0.05). The first image plots the correlation variable and some of them are significant (defined by Sig). So I want to plot the Sig variable on top of the AMOTL. I've tried variations of the code, but still can't get through.
Would be grateful for some assistance... Here's my file - https://drive.google.com/file/d/10LPNjBtQMdQMw6XmXdJEg6Uq4icx_LD6/view?usp=sharing
I’d bet this is the culprit:
data.loc[data["Sig"].ge(0.05), "Sig"].plot(
column="Sig", hatch='//'
)
In this line, you’re selecting only the 'Sig' column, eliminating all spatial data in the 'geometry' column and returning a pandas.Series instead of a geopandas.GeoDataFrame. In order to plot a data column using the geometries column for your shapes you must maintain at least both of those columns in the object you call .plot on.
So instead, don’t select the column:
data.loc[data["Sig"].ge(0.05)].plot(
column="Sig", hatch='//'
)
You are already telling geopandas to plot the "Sig" column by using the column argument to .plot - no need to limit the actual data too.
Also, when overlaying a plot on an existing axis, be sure to pass in the axis object:
data.loc[data["Sig"].ge(0.05)].plot(
column="Sig", hatch='//', ax=ax
)

Is Cartopy capable of plotting georeferenced data from another planet (e.g., Mars, the Moon)?

I'm working with several data sets from the Moon and Mars (topography, crustal thickness) and was wondering if Cartopy can manipulate these data given the reference ellipsoids are different. Do custom ellipsoids need to be created, or are they built in to Cartopy?
I figured out how to do this on my own. Here's the solution I came up with...
Step 1
Import Cartopy...
import cartopy.crs as ccrs
After importing Cartopy and loading your data set, you need to change Cartopy's Globe class such that it does not use the WGS84 ellipse. Simply define new semi-major and semi-minor axes and tell Cartopy to refrain from using a terrestrial ellipse.
img_globe = ccrs.Globe(semimajor_axis = semimajor, semiminor_axis = semiminor, ellipse = None)
Step 2
Next, choose a map projection for plotting and identify your data's format. I decided to plot my data using a Mollweide coordinate system and found my data is defined in the Plate Carree coordinate system. Now we can define the map projection and coordinate system for the data using the new Globe class defined above.
projection = ccrs.Mollweide(globe = img_globe)
data_crs = ccrs.PlateCarree(globe = img_globe)
Step 3
Lastly, plot your data using standard Matplotlib syntax with two important caveats. First create axes that implement the map projection.
fig = plt.figure(figsize = (6,6))
ax = plt.axes(projection = projection)
When plotting the data, you have to inform Matplotlib how your data are formatted using the transform argument.
ax.imshow(data, extent = extent, cmap = 'viridis', transform = data_crs)
The end result looks like this...

Cartopy AzimuthalEquidistant projection: zooming into a region and coastlines

I am trying to plot some data on an AzimuthalEquidistant projection using cartopy. However, it gives me a couple of problems. First the coastlines no longer show for this type of projection. Not sure if this is my code or a Cartopy problem. I also notice that if I use a ccrs.PlateCarree() transform in the pcolormesh command the coastlines do show but then, presumably, my data is on the wrong type of prejection?
Second I would prefer if the axis boarder was circular after plotting the data, is it possible to use set_extent or some similar function to do this?
The code below should reproduce the problems, the circle shows how I would like the boarder to look.
import matplotlib.pyplot as plt
import cartopy.crs as ccrs
import matplotlib.patches as mpatches
clat = 55.0
clon = -8.0
lons = np.arange(clon-15,clon+16,0.5)
lats = np.arange(clat-15,clat+16,0.5)
d = np.random.rand(lons.shape[0],lats.shape[0])
trans = ccrs.AzimuthalEquidistant(central_latitude=clat, central_longitude=clon)
ax = plt.axes(projection=trans)
ax.coastlines(resolution='10m')
CB=ax.pcolormesh(lons-0.25, lats-0.25, d.T,
cmap=plt.cm.viridis, alpha=0.5,
transform=trans)#ccrs.PlateCarree())
p1 = mpatches.Circle((clon,clat), radius=15, color='k', lw=5, fill=False,
transform=trans)
ax.add_patch(p1)
If the data you are plotting is in latitude/longitude coordinates then the correct value for the transform keyword is indeed ccrs.PlateCarree(). This is common gotcha for new users. The transform argument tells cartopy what coordinates your data are in, and is completely independent of the projection you want to plot onto.
To make the plot circular you'll need to set the boundary yourself. The Cartopy documentation have a couple of examples of this: http://scitools.org.uk/cartopy/docs/latest/examples/always_circular_stereo.html and http://scitools.org.uk/cartopy/docs/latest/examples/star_shaped_boundary.html.

How do I match the projection of my cartopy map with that of a shapefile?

I am trying to synthesise the projections of a coastlines() map with that of a shapefile, whose .prj file says:
GEOGCS["GCS_WGS_1984",DATUM["D_WGS_1984",
SPHEROID["WGS_1984",6378137.0,298.257223563]],
PRIMEM["Greenwich",0.0],UNIT["Degree",0.0174532925199433]]
My attempt is:
import matplotlib.pyplot as plt
import cartopy.crs as ccrs
from cartopy.io import shapereader
# set up a map with coastlines around Auckland:
plt.figure(figsize=(10, 10))
platecarree = ccrs.PlateCarree(globe=ccrs.Globe(datum='WGS84'))
ax = plt.axes(projection=platecarree)
extent = [174.25, 175.25, -37.5, -36.5]
ax.set_extent(extent)
ax.coastlines('10m',color='red')
# read in shapefile and plot the polygons:
shp2 = shapereader.Reader('auckland_geology_wgs84gcs.shp')
formations = shp2.records()
for formation in formations:
# plot water blue, and all other rocks yellow
if formation.attributes['MAIN_ROCK'] == b' ':
ax.add_geometries(formation.geometry, ccrs.PlateCarree(),facecolor='blue',alpha=.1)
else:
ax.add_geometries(formation.geometry, ccrs.PlateCarree(), facecolor='yellow',alpha=.1)
plt.show()
I tried giving the globe parameter in my platecarree definition the radius and inverse flattening from the prj file, but I didn't see any change to the output if I set or even varied those numbers.
In addition, with the defined "platecarree" projection (with the call to the globe with WGS84) as the crs in the add_geometries calls, my output is blank.
As is, the result looks to me like a projection mismatch
I've tried to reproduce your problem using QGIS and data downloaded from Natural Earth (10m coastlines) and from GADM (NZ adm0 level). It looks like the NE10m coastlines are the culprit ! The GADM aligns perfectly with your geology layer, while the NE10m is off (and deformed). screenshot of QGIS with Geological map & coastlines

How to add a point-feature shapefile to map using cartopy

I have two shapefiles. One is a point feature shapefile, named "point.shp", the other is a polygon shapefile named "polygon.shp". Both I want to add to a map using cartopy.
I managed to add the "polygon.shp", but failed with the "point.shp".
Here's my code:
import matplotlib.pyplot as plt
from cartopy import crs
from cartopy.io.shapereader import Reader
from cartopy.feature import ShapelyFeature
ax = plt.axes(projection=crs.PlateCarree())
# add the polygon file, worked
ax.add_geometries(Reader("polygon.shp").geometries(), crs.PlateCarree(), facecolor='w')
# or(also worked):
ax.add_feature(ShapelyFeature(Reader("polygon.shp").geometries(), crs.PlateCarree(), facecolor='r'))
# but these two ways both failed with the "point.shp"
ax.add_geometries(Reader("point.shp").geometries(), crs.PlateCarree())
# or, this doesn't work neither:
ax.add_feature(ShapelyFeature(Reader("polygon.shp").geometries(), crs.PlateCarree(), facecolor='r'))
Does any one know how to do this, or why, without retrieving all the points' x, y coords and then plotting them?
And with coordinates(x, y values), ax.plot() works, but ax.scatter() fails, why?
Thanks
add_geometries currently turns a geometry into a polygon and then colours it appropriately, which of course means that when you pass points the add_geometries, the polygons are not visible. Potentially cartopy could do a better job of this in the future, but in the meantime, it sounds like you just want to use something like scatter to visualize your data.
You can achieve this by getting the x and y coordinate values out of the geometry and passing these straight on to scatter with the appropriate transform:
import cartopy.crs as ccrs
import cartopy.io
import matplotlib.pyplot as plt
fname = cartopy.io.shapereader.natural_earth(resolution='10m',
category='cultural',
name='populated_places_simple')
plt.figure(figsize=(12, 6))
ax = plt.axes(projection=ccrs.Robinson())
ax.set_title('Populated places of the world.')
ax.coastlines()
points = list(cartopy.io.shapereader.Reader(fname).geometries())
ax.scatter([point.x for point in points],
[point.y for point in points],
transform=ccrs.Geodetic())
plt.show()
HTH