How to plot a tissot with cartopy and matplotlib? - matplotlib

For plotting skymaps I just switched from Basemap to cartopy, I like it a lot more
.
(The main reason was segfaulting of Basemap on some computers, which I could not fix).
The only thing I struggle with, is getting a tissot circle (used to show the view cone of our telescope.)
This is some example code plotting random stars (I use a catalogue for the real thing):
import matplotlib.pyplot as plt
from cartopy import crs
import numpy as np
# create some random stars:
n_stars = 100
azimuth = np.random.uniform(0, 360, n_stars)
altitude = np.random.uniform(75, 90, n_stars)
brightness = np.random.normal(8, 2, n_stars)
fig = plt.figure()
ax = fig.add_subplot(1,1,1, projection=crs.NorthPolarStereo())
ax.background_patch.set_facecolor('black')
ax.set_extent([-180, 180, 75, 90], crs.PlateCarree())
plot = ax.scatter(
azimuth,
altitude,
c=brightness,
s=0.5*(-brightness + brightness.max())**2,
transform=crs.PlateCarree(),
cmap='gray_r',
)
plt.show()
How would I add a tissot circle with a certain radius in degrees to that image?
https://en.wikipedia.org/wiki/Tissot%27s_indicatrix

I keep meaning to go back and add the two functions from GeographicLib which provide the forward and inverse geodesic calculations, with this it is simply a matter of computing a geodetic circle by sampling at appropriate azimuths for a given lat/lon/radius. Alas, I haven't yet done that, but there is a fairly primitive (but effective) wrapper in pyproj for the functionality.
To implement a tissot indicatrix then, the code might look something like:
import matplotlib.pyplot as plt
import cartopy.crs as ccrs
import numpy as np
from pyproj import Geod
import shapely.geometry as sgeom
def circle(geod, lon, lat, radius, n_samples=360):
"""
Return the coordinates of a geodetic circle of a given
radius about a lon/lat point.
Radius is in meters in the geodetic's coordinate system.
"""
lons, lats, back_azim = geod.fwd(np.repeat(lon, n_samples),
np.repeat(lat, n_samples),
np.linspace(360, 0, n_samples),
np.repeat(radius, n_samples),
radians=False,
)
return lons, lats
def main():
ax = plt.axes(projection=ccrs.Robinson())
ax.coastlines()
geod = Geod(ellps='WGS84')
radius_km = 500
n_samples = 80
geoms = []
for lat in np.linspace(-80, 80, 10):
for lon in np.linspace(-180, 180, 7, endpoint=False):
lons, lats = circle(geod, lon, lat, radius_km * 1e3, n_samples)
geoms.append(sgeom.Polygon(zip(lons, lats)))
ax.add_geometries(geoms, ccrs.Geodetic(), facecolor='blue', alpha=0.7)
plt.show()
if __name__ == '__main__':
main()

Related

How to plot a map of a semi-sphere (eg northern hemisphere) using matplotlib cartopy

How to plot a map of a semi-sphere (eg northern hemisphere) using cartopy.
I'm trying to plot a map of the northern hemisphere using cartopy. But I don't understand how should I define the extent of the map so that only this region of interest is plotted. I would like the map to be cut off at 0° latitude. I would like to have code where I could easily define any subset of the glob using the ccrs.NearsidePerspective projection, or the ccrs.Orthographic projection.
Below I leave a code for reproduction.
import numpy as np
import cartopy.crs as ccrs
import matplotlib.pyplot as plt
# Creating fake data
x = np.linspace(-180, 180, 361)
y = np.linspace(-90, 90, 181)
lon, lat = np.meshgrid(x, y)
values = np.random.random(lon.shape)*20
fig = plt.figure(figsize=(15, 10))
proj = ccrs.NearsidePerspective(central_longitude=-45, central_latitude=21)
ax = fig.add_subplot(121, projection=proj)
ax.set_extent([-120, 40, 0, 60])
ax.pcolormesh(lon, lat, values, transform=ccrs.PlateCarree())
ax.coastlines(linewidth=2)
gl = ax.gridlines(draw_labels=True, linestyle='--')
The code generates the following figure:
Thank you very much in advance.
Robson
To plot only the upper hemisphere part of the map projection, a polygon of that part is needed to use as the projection boundary.
That polygon is created as a matplotlib-path object. It vertices' coordinates are data coordinates in my code, so that, no transformation is required when applied to the final plot.
This is a complete code:-
import matplotlib.pyplot as plt
import cartopy.crs as ccrs
import matplotlib.path as mpath
import numpy as np
from geographiclib.geodesic import Geodesic
fig = plt.figure(figsize=[12, 12])
proj = ccrs.NearsidePerspective(central_longitude=-45, central_latitude=21, satellite_height=35785831)
ax = plt.subplot(projection=proj)
# The value of r is obtained by previous run of this code ...
# with the line .. #print(ax.get_xlim()) uncommented
r = 5476336.098
ax.set_xlim(-r, r)
ax.set_ylim(-r, r)
ax.stock_img()
ax.coastlines(lw=1, color="darkblue")
# Find the locations of points along the equatorial arc
# start location
lon_fr, lat_fr = 30, 0
# end location
lon_to, lat_to = -120, 0
# This gets geodesic between the two points, WGS84 ellipsoid is used
geodl = Geodesic.WGS84.InverseLine(lat_fr, lon_fr, lat_to, lon_to)
lonlist, latlist = [], []
num_points = 32 #for series of points on geodesic/equator
for ea in np.linspace(0, geodl.s13, num_points):
g = geodl.Position(ea, Geodesic.STANDARD | Geodesic.LONG_UNROLL)
#print("{:.0f} {:.5f} {:.5f} {:.5f}".format(g['s12'], g['lat2'], g['lon2'], g['azi2']))
lon2, lat2 = g['lon2'], g['lat2']
lonlist.append( g['lon2'] )
latlist.append( g['lat2'] )
# Get data-coords from (lonlist, latlist)
# .. as points along equatorial arc
dataxy = proj.transform_points(ccrs.PlateCarree(), np.array(lonlist), np.array(latlist))
# (Uncomment to) Plot equator line
#ax.plot(dataxy[:, 0:1], dataxy[:, 1:2], "go-", linewidth=2, markersize=5, zorder=10)
# Top semi-circle arc for map extent
theta = np.linspace(-0.5*np.pi, 0.5*np.pi, 64)
center, radius = [0, 0], r
verts = np.vstack([np.sin(theta), np.cos(theta)]).T
# Combine vertices of the semi-circle and equatorial arcs
# These points are in data coordinates, ready to plot on the axes.
verts = np.vstack([verts*r, dataxy[:, 0:2]])
polygon = mpath.Path(verts + center)
ax.set_boundary(polygon) #This masks-out unwanted part of the plot
gl = ax.gridlines(draw_labels=True, xlocs=range(-150,180,30), ylocs=range(0, 90, 15),
y_inline=True, linestyle='--', lw= 5, color= "w", )
# Get limits, the values are the radius of the circular map extent
# The values is then used as r = 5476336.09797 on top of the code
#print(ax.get_xlim())
#print(ax.get_ylim())
plt.show()

is there a way to use matplotlib.patches.Wedge's radius in km instead of degrees?

I currently want to draw a sector-like wedge in Cartopy, so I look up the matplotlib.patches.Wedge method. It's almost the function I need, but the unit of the parameter radius it needs is in degrees, rather than kilometers.
Is there a way to use matplotlib.patches.Wedge method in kilometers rather than degrees?
Thanks.
my idea image here
I assume you saying "rather than degrees" means you're working with a position in lon/lat. The easiest way is going to be to convert your lon/lat center point to a location on the map. We'll also actually be using the radius in meters when plotting:
import matplotlib.pyplot as plt
from matplotlib.patches import Wedge
import cartopy.crs as ccrs
proj = ccrs.LambertConformal()
latlon_proj = ccrs.PlateCarree()
lat = 18
lon = 140
radius = 500 # In km
x, y = proj.transform_point(lon, lat, src_crs=latlon_proj)
fig = plt.figure()
ax = fig.add_subplot(1, 1, 1, projection=proj)
ax.plot(x, y, 'ro')
# Here we need to convert radius to meters
ax.add_patch(Wedge((x, y), r=radius * 1000, theta1=0, theta2=90))
ax.coastlines()

Limit extent of orthograpic projection (zooming)

I would like to produce orthographic (polar) plots of Antarctica that are 'zoomed' with respect to the default settings. By default I get this:
Antarctica polar
The following script produced this.
import cartopy.crs as ccrs
import matplotlib.pyplot as plt
ax = plt.axes(projection=ccrs.Orthographic(central_longitude=0.0, central_latitude=-90.))
ax.stock_img()
plt.show()
My best attempt to tell Cartopy 'limit the latitude to 60S to 90S' was:
ax.set_extent([-180,180,-60,-90], ccrs.PlateCarree())
unfortunately it does not give the desired result. Any ideas? Thanks in advance.
I'm not sure I fully understand what you're trying to do. Your example looks like a bounding box that was defined, but you'd like it rounded like your first example?
cartopy documentation has an example of this http://scitools.org.uk/cartopy/docs/latest/examples/always_circular_stereo.html:
import matplotlib.path as mpath
import matplotlib.pyplot as plt
import numpy as np
import cartopy.crs as ccrs
import cartopy.feature
def main():
fig = plt.figure(figsize=[10, 5])
ax1 = plt.subplot(1, 2, 1, projection=ccrs.SouthPolarStereo())
ax2 = plt.subplot(1, 2, 2, projection=ccrs.SouthPolarStereo(),
sharex=ax1, sharey=ax1)
fig.subplots_adjust(bottom=0.05, top=0.95,
left=0.04, right=0.95, wspace=0.02)
# Limit the map to -60 degrees latitude and below.
ax1.set_extent([-180, 180, -90, -60], ccrs.PlateCarree())
ax1.add_feature(cartopy.feature.LAND)
ax1.add_feature(cartopy.feature.OCEAN)
ax1.gridlines()
ax2.gridlines()
ax2.add_feature(cartopy.feature.LAND)
ax2.add_feature(cartopy.feature.OCEAN)
# Compute a circle in axes coordinates, which we can use as a boundary
# for the map. We can pan/zoom as much as we like - the boundary will be
# permanently circular.
theta = np.linspace(0, 2*np.pi, 100)
center, radius = [0.5, 0.5], 0.5
verts = np.vstack([np.sin(theta), np.cos(theta)]).T
circle = mpath.Path(verts * radius + center)
ax2.set_boundary(circle, transform=ax2.transAxes)
plt.show()
if __name__ == '__main__':
main()

How can I plot function values on a sphere?

I have a Nx2 matrix of lat lon coordinate pairs, spatial_data, and I have an array of measurements at these coordinates.
I would like to plot this data on a globe, and I understand that Basemap can do this. I found this link which shows how to plot data if you have cartesian coordinates. Does there exist functionality to convert lat,lon to cartesian coordinates? Alternatively, is there a way to plot this data with only the lat,lon information?
You can use cartopy:
import numpy as np
import matplotlib.pyplot as plt
from cartopy import crs
# a grid for the longitudes and latitudes
lats = np.linspace(-90, 90, 50)
longs = np.linspace(-180, 180, 50)
lats, longs = np.meshgrid(lats, longs)
# some data
data = lats[1:] ** 2 + longs[1:] ** 2
fig = plt.figure()
# create a new axes with a cartopy.crs projection instance
ax = fig.add_subplot(1, 1, 1, projection=crs.Mollweide())
# plot the date
ax.pcolormesh(
longs, lats, data,
cmap='hot',
transform=crs.PlateCarree(), # this means that x, y are given as longitude and latitude in degrees
)
fig.tight_layout()
fig.savefig('cartopy.png', dpi=300)
Result:

How to draw rectangles on a Basemap

I'm looking for a way to plot filled rectangles on a Basemap. I could easily draw the rectangle's edges using the drawgreatcircle method, but I cannot find a way to actually fill these rectangles (specifying color and alpha).
You can add a matplotlib.patches.Polygon() directly to your axes. The question is whether you want your rectangles defined the plot coordinates (straight lines on the plot) or in map coordinates (great circles on the plot). Either way, you specify vertices in map coordinates and then transform them to plot coordinates by calling the Basemap instance (m() in the below example), build a Polygon yourself, and add it manually to the axes to be rendered.
For rectangles defined in plot coordinates, here's an example:
from mpl_toolkits.basemap import Basemap
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.patches import Polygon
def draw_screen_poly( lats, lons, m):
x, y = m( lons, lats )
xy = zip(x,y)
poly = Polygon( xy, facecolor='red', alpha=0.4 )
plt.gca().add_patch(poly)
lats = [ -30, 30, 30, -30 ]
lons = [ -50, -50, 50, 50 ]
m = Basemap(projection='sinu',lon_0=0)
m.drawcoastlines()
m.drawmapboundary()
draw_screen_poly( lats, lons, m )
plt.show()
For rectangles defined in map coordinates, use the same approach, but interpolate your line in map space before transforming to plot coordinates. For each line segment, you'll have to do:
lats = np.linspace( lat0, lat1, resolution )
lons = np.linspace( lon0, lon1, resolution )
Then transform these map coordinates to plot coordinates (as above, with m()) and again create a Polygon with the plot coordinates.
Using Andrew's answer, I get the error
TypeError: len() of unsized object.
However, casting the zip to a list fixes this.
Complete code:
from mpl_toolkits.basemap import Basemap
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.patches import Polygon
def draw_screen_poly( lats, lons, m):
x, y = m( lons, lats )
xy = zip(x,y)
poly = Polygon( list(xy), facecolor='red', alpha=0.4 )
plt.gca().add_patch(poly)
lats = [ -30, 30, 30, -30 ]
lons = [ -50, -50, 50, 50 ]
m = Basemap(projection='sinu',lon_0=0)
m.drawcoastlines()
m.drawmapboundary()
draw_screen_poly( lats, lons, m )
plt.show()
Similar answer to above, but more basic code:
from mpl_toolkits.basemap import Basemap
import matplotlib.pyplot as plt
from matplotlib.patches import Polygon
map = Basemap(projection='cyl')
map.drawmapboundary(fill_color='aqua')
map.fillcontinents(color='coral',lake_color='aqua')
map.drawcoastlines()
x1,y1 = map(-25,-25)
x2,y2 = map(-25,25)
x3,y3 = map(25,25)
x4,y4 = map(25,-25)
poly = Polygon([(x1,y1),(x2,y2),(x3,y3),(x4,y4)],facecolor='red',edgecolor='green',linewidth=3)
plt.gca().add_patch(poly)
plt.show()