I'm having trouble using cartopy ...
I have some locations (mainly changing in lat) and I want to draw some circles along the this great circle path. Here's the code
import cartopy.crs as ccrs
import cartopy.feature as cfeature
import cartopy
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
points = np.array([[-145.624, 14.8853],
[-145.636, 10.6289],
[-145.647, 6.3713]])
proj2 = ccrs.Orthographic(central_longitude= points[1,0], central_latitude= points[1,1]) # Spherical map
pad_radius = compute_radius(proj2, points[1,0],points[1,1], 35)
resolution = '50m'
fig = plt.figure(figsize=(112,6), dpi=96)
ax = fig.add_subplot(1, 1, 1, projection=proj2)
ax.set_xlim([-pad_radius, pad_radius])
ax.set_ylim([-pad_radius, pad_radius])
ax.imshow(np.tile(np.array([[cfeature.COLORS['water'] * 255]], dtype=np.uint8), [2, 2, 1]), origin='upper', transform=ccrs.PlateCarree(), extent=[-180, 180, -180, 180])
ax.add_feature(cfeature.NaturalEarthFeature('physical', 'land', resolution, edgecolor='black', facecolor=cfeature.COLORS['land']))
ax.add_feature(cfeature.NaturalEarthFeature('cultural', 'admin_0_countries', resolution, edgecolor='black', facecolor='none'))
# Loop over the points
# Compute the projected circle at that point
# Plot it!
for i in range(len(points)):
thePt = points[i,0], points[i,1]
r_or = compute_radius(proj2, points[i,0], points[i,1], 10)
print(thePt, r_or)
c= mpatches.Circle(xy=thePt, radius=r_or, color='red', alpha=0.3, transform=proj2, zorder=30)
# print(c.contains_point(points[i,0], points[i,1]))
ax.add_patch(c)
fig.tight_layout()
plt.show()
Compute radius is:
def compute_radius(ortho, lon, lat, radius_degrees):
'''
Compute a earth central angle around lat, lon
Return phi in terms of projection desired
This only seems to work for non-PlateCaree projections
'''
phi1 = lat + radius_degrees if lat <= 0 else lat - radius_degrees
_, y1 = ortho.transform_point(lon, phi1, ccrs.PlateCarree()) # From lon/lat in PlateCaree to ortho
return abs(y1)
And what I get for output:
(-145.624, 14.8853) 638304.2929446043 (-145.636, 10.6289)
1107551.8669600221 (-145.647, 6.3713) 1570819.3871025692
You can see the interpolated points going down in lat (lon is almost constant), but the radius is growing smaller with lat and the location isn't changing at all???
Thanks for rewriting the example, that clears things up!
I think the key point is that you need to convert the x/y coordinates that you use for the Circle as well, or vice versa keep the radius also in lat/lon (probably close but not identical). Now you mix and match, where the radius is based on the Orthographic projection, but the x/y are lat/lon. Because of that, the points do move along the path you want, but it's just incredibly close to the origin of the plot due to the incorrect units.
Something like this might help you along:
points = np.array([
[-145.624, 14.8853],
[-145.636, 10.6289],
[-145.647, 6.3713]],
)
proj2 = ccrs.Orthographic(
central_longitude= points[1,0],
central_latitude= points[1,1],
)
pad_radius = compute_radius(proj2, points[1,0],points[1,1], 35)
resolution = '50m'
fig, ax = plt.subplots(
figsize=(12,6), dpi=96, subplot_kw=dict(projection=map_proj, facecolor=cfeature.COLORS['water']),
)
ax.add_feature(cfeature.NaturalEarthFeature('physical', 'land', resolution, edgecolor='black', facecolor=cfeature.COLORS['land']))
ax.add_feature(cfeature.NaturalEarthFeature('cultural', 'admin_0_countries', resolution, edgecolor='black', facecolor='none'))
ax.set_extent((-pad_radius, pad_radius, -pad_radius, pad_radius), crs=proj2)
for lon, lat in points:
r_or = compute_radius(proj2, lon, lat, 10)
### I think this is what you intended!
mapx, mapy = proj2.transform_point(lon, lat, ccrs.PlateCarree())
###
c= mpatches.Circle(xy=(mapx, mapy), radius=r_or, color='red', alpha=0.3, transform=proj2, zorder=30)
ax.add_patch(c)
Here's the complete answer. I was not converting the lat/lon into the orthographic projection coords... and apparently it wants the size of the circle to be in meters -- hence I use a function to change degrees (which I know for my circle) into meters:
def deg2m(val_degree):
"""
Compute surface distance in meters for a given angular value in degrees
Uses the definition of a degree on the equator...
"""
geod84 = Geod(ellps='WGS84')
lat0, lon0 = 0, 90
_, _, dist_m = geod84.inv(lon0, lat0, lon0+val_degree, lat0)
return dist_m
# Data points where I wish to draw circles
points = np.array([[-111.624, 30.0],
[-111.636, 35.0],
[-111.647, 40.0]])
proj2 = ccrs.Orthographic(central_longitude= points[1,0], central_latitude= points[1,1]) # Spherical map
pad_radius = compute_radius(proj2, points[1,0],points[1,1], 45) # Generate a region bigger than our circles
resolution = '50m'
fig = plt.figure(figsize=(112,6), dpi=96)
ax = fig.add_subplot(1, 1, 1, projection=proj2)
# Bound our plot/map
ax.set_xlim([-pad_radius, pad_radius])
ax.set_ylim([-pad_radius, pad_radius])
ax.imshow(np.tile(np.array([[cfeature.COLORS['water'] * 255]], dtype=np.uint8), [2, 2, 1]), origin='upper', transform=ccrs.PlateCarree(), extent=[-180, 180, -180, 180])
ax.add_feature(cfeature.NaturalEarthFeature('physical', 'land', resolution, edgecolor='black', facecolor=cfeature.COLORS['land']))
ax.add_feature(cfeature.NaturalEarthFeature('cultural', 'admin_0_countries', resolution, edgecolor='black', facecolor='none'))
r_or = deg2m(17) # Compute the size of circle with a radius of 17 deg
# Loop over the points
# Compute the projected circle at that point
# Plot it!
for i in range(len(points)):
thePt = points[i,0], points[i,1]
# Goes from lat/lon to coordinates in our Orthographic projection
mapx, mapy = proj2.transform_point(points[i,0], points[i,1], ccrs.PlateCarree())
c= mpatches.Circle(xy=(mapx, mapy), radius=r_or, color='red', alpha=0.3, transform=proj2, zorder=30)
ax.add_patch(c)
fig.tight_layout()
plt.show()
I've verified that the deg2m routine works since it's about 17 deg of lat from Phoenix AZ to the Canadian border (approx) which is nearby these test points.
Related
To simplify, as much as possible, a question I already asked, how would you OVERLAY or PROJECT a polar plot onto a cartopy map.
phis = np.linspace(1e-5,10,10) # SV half cone ang, measured up from nadir
thetas = np.linspace(0,2*np.pi,361)# SV azimuth, 0 coincides with the vel vector
X,Y = np.meshgrid(thetas,phis)
Z = np.sin(X)**10 + np.cos(10 + Y*X) * np.cos(X)
fig, ax = plt.subplots(figsize=(4,4),subplot_kw=dict(projection='polar'))
im = ax.pcolormesh(X,Y,Z, cmap=mpl.cm.jet_r,shading='auto')
ax.set_theta_direction(-1)
ax.set_theta_offset(np.pi / 2.0)
ax.grid(True)
that results in
Over a cartopy map like this...
flatMap = ccrs.PlateCarree()
resolution = '110m'
fig = plt.figure(figsize=(12,6), dpi=96)
ax = fig.add_subplot(111, projection=flatMap)
ax.imshow(np.tile(np.array([[cfeature.COLORS['water'] * 255]], dtype=np.uint8), [2, 2, 1]), origin='upper', transform=ccrs.PlateCarree(), extent=[-180, 180, -180, 180])
ax.add_feature(cfeature.NaturalEarthFeature('physical', 'land', resolution, edgecolor='black', facecolor=cfeature.COLORS['land']))
ax.pcolormesh(X,Y,Z, cmap=mpl.cm.jet_r,shading='auto')
gc.collect()
I'd like to project this polar plot over an arbitrary lon/lat... I can convert the polar theta/phi into lon/lat, but lon/lat coords (used on the map) are more 'cartesian like' than polar, hence you cannot just substitute lon/lat for theta/phi ... This is a conceptual problem. How would you tackle it?
Firstly, the data must be prepared/transformed into certain projection coordinates for use as input. And the instruction/option of the data's CRS must be specified correctly when used in the plot statement.
In your specific case, you need to transform your data into (long,lat) values.
XX = X/np.pi*180 # wrap around data in EW direction
YY = Y*9 # spread across N hemisphere
And plot it with an instruction transform=ccrs.PlateCarree().
ax.pcolormesh(XX,YY,Z, cmap=mpl.cm.jet_r,shading='auto',
transform=ccrs.PlateCarree())
The same (XX,YY,Z) data set can be plotted on orthographic projection.
Edit1
Update of the code and plots.
Part 1 (Data)
import matplotlib.colors
import matplotlib.pyplot as plt
import cartopy.crs as ccrs
import numpy as np
import matplotlib.pyplot as mpl
import cartopy.feature as cfeature
#
# Part 1
#
phis = np.linspace(1e-5,10,10) # SV half cone ang, measured up from nadir
thetas = np.linspace(0,2*np.pi,361)# SV azimuth, 0 coincides with the vel vector
X,Y = np.meshgrid(thetas,phis)
Z = np.sin(X)**10 + np.cos(10 + Y*X) * np.cos(X)
fig, ax = plt.subplots(figsize=(4,4),subplot_kw=dict(projection='polar'))
im = ax.pcolormesh(X,Y,Z, cmap=mpl.cm.jet_r,shading='auto')
ax.set_theta_direction(-1)
ax.set_theta_offset(np.pi / 2.0)
ax.grid(True)
Part 2 The required code and output.
#
# Part 2
#
flatMap = ccrs.PlateCarree()
resolution = '110m'
fig = plt.figure(figsize=(12,6), dpi=96)
ax = fig.add_subplot(111, projection=flatMap)
ax.add_feature(cfeature.NaturalEarthFeature('physical', 'land',
resolution, edgecolor='black', alpha=0.7,
facecolor=cfeature.COLORS['land']))
ax.set_extent([-180, 180, -90, 90], crs=ccrs.PlateCarree())
def scale_position(lat_deg, lon_deg, rad_deg):
# Two operations:
# 1. manipulates X,Y data and get (XX,YY)
# 2. create proper projection of (XX,YY), `rotpole_proj`
# Returns: XX,YY,rotpole_proj
# For X data
XX = X/np.pi*180 #always wrap around EW direction
# For Y data
# The cone data: min=0, max=10 --> (90-rad),90
# rad_deg = radius of the display area
top = 90
btm = top-rad_deg
YY = btm + (Y/Y.max())*rad_deg
# The proper coordinate system
rotpole_proj = ccrs.RotatedPole(pole_latitude=lat_deg, pole_longitude=lon_deg)
# Finally,
return XX,YY,rotpole_proj
# Location 1 (Asia)
XX1, YY1, rotpole_proj1 = scale_position(20, 100, 20)
ax.pcolormesh(XX1, YY1, Z, cmap=mpl.cm.jet_r,
transform=rotpole_proj1)
# Location 2 (Europe)
XX2, YY2, rotpole_proj2 = scale_position(62, -6, 8)
ax.pcolormesh(XX2, YY2, Z, cmap=mpl.cm.jet_r,
transform=rotpole_proj2)
# Location 3 (N America)
XX3, YY3, rotpole_proj3 = scale_position(29, -75, 30)
ax.pcolormesh(XX3, YY3, Z, cmap=mpl.cm.jet_r,shading='auto',
transform=rotpole_proj3)
#gc.collect()
plt.show()
This solution does NOT account for the projection point being at some altitude above the globe... I can do that part, so I really have trouble mapping the meshgrid to lon/lat so the work with the PREVIOUSLY GENERATES values of Z.
Here's a simple mapping directly from polar to cart:
X_cart = np.array([[p*np.sin(t) for p in phis] for t in thetas]).T
Y_cart = np.array([[p*np.cos(t) for p in phis] for t in thetas]).T
# Need to map cartesian XY to Z that is compatbile with above...
Z_cart = np.sin(X)**10 + np.cos(10 + Y*X) * np.cos(X) # This Z does NOT map to cartesian X,Y
print(X_cart.shape,Y_cart.shape,Z_cart.shape)
flatMap = ccrs.PlateCarree()
resolution = '110m'
fig = plt.figure(figsize=(12,6), dpi=96)
ax = fig.add_subplot(111, projection=flatMap)
ax.imshow(np.tile(np.array([[cfeature.COLORS['water'] * 255]], dtype=np.uint8), [2, 2, 1]), origin='upper', transform=ccrs.PlateCarree(), extent=[-180, 180, -180, 180])
ax.add_feature(cfeature.NaturalEarthFeature('physical', 'land', resolution, edgecolor='black', facecolor=cfeature.COLORS['land']))
im = ax.pcolormesh(X_cart*2,Y_cart*2, Z_cart, cmap=mpl.cm.jet_r, shading='auto') # c=mapper.to_rgba(Z_cart), cmap=mpl.cm.jet_r)
gc.collect()
Which maps the polar plot center to lon/lat (0,0):
I'm close... I somehow need to move my cartesian coords to the proper lon/lat (the satellite sub-point) and then scale it appropriately. Have the set of lon/lat but I'm screwing up the meshgrid somehow... ???
The sphere_intersect() routine returns lon/lat for projection of theta/phi on the globe (that works)... The bit that doesn't work is building the meshgrid that replaces X,Y:
lons = np.array([orbits.sphere_intersect(SV_pos_vec, SV_vel_vec, az << u.deg, el << u.deg,
lonlat=True)[0] for az in thetas for el in phis], dtype='object')
lats = np.array([orbits.sphere_intersect(SV_pos_vec, SV_vel_vec, az << u.deg, el << u.deg,
lonlat=True)[1] for az in thetas for el in phis], dtype='object')
long, latg = np.meshgrid(lons,lats) # THIS IS A PROBLEM I BELIEVE...
and the pcolormesh makes a mess...
I have an algorithm problem. I would like to do the following.
I have a polar plot in theta, r coords as below:
phis = np.linspace(0.01,63,100) # SV half cone ang, measured up from nadir
thetas = np.linspace(0,2*np.pi,361)# SV azimuth, 0 coincides with the vel vector
X,Y = np.meshgrid(thetas,phis)
rangeMap = orbits.range(orbits.h_mission, Y * u.deg)[0].value
fig, ax = plt.subplots(figsize=(8,7),subplot_kw=dict(projection='polar'))
X, Y = np.meshgrid(thetas, phis) # Create a grid over the range of bins for the plot
im = (ax.pcolormesh(thetas,phis, rangeMap, cmap=mpl.cm.jet_r, alpha=0.95,shading='auto') )
ax.set_theta_direction(-1)
ax.set_theta_offset(np.pi / 2.0)
plt.thetagrids([theta * 15 for theta in range(360//15)])
ax.grid(True)
# ax.set_xlabel("")
# ax.set_ylabel("")
# ax.set_xticklabels([])
# ax.set_yticklabels([])
plt.xticks(fontsize=14)
plt.yticks(fontsize=14)
## Add colorbar
gc.collect()
Notice that I plot with theta and phis as opposed to X,Y ... this seems to work (I don't have to use X,Y).
The theta and r correspond to pointing vectors from a spacecraft that intersect the earth. As such, I can transform those coordinates, (theta,r), into lon/lat on the globe for cartopy.
However, pcolormesh, obviously uses polar coordinates. And although I can translate each PAIR of theta,r into lon/lat, it doesn't help. I thought I could just substitute the theta, phi for lon,lat but that doesn't seem to work (keeping my Z = rangeMap values unchanged). i.e. - This doesn't work
resolution = '110m'
lls = [orbits.sphere_intersect(SV_pos_vec, SV_vel_vec, az << u.deg, el << u.deg, lonlat=True)[:2] for az in thetas for el in phis] # This returns a long/lat array for az/el
gd = Geodesic() # from cartopy
fig = plt.figure(figsize=(12,6), dpi=96)
ax = fig.add_subplot(111, projection=flatMap)
ax.imshow(np.tile(np.array([[cfeature.COLORS['water'] * 255]], dtype=np.uint8), [2, 2, 1]), origin='upper', transform=ccrs.PlateCarree(), extent=[-180, 180, -180, 180])
ax.add_feature(cfeature.NaturalEarthFeature('physical', 'land', resolution, edgecolor='black', facecolor=cfeature.COLORS['land']))
ax.add_feature(cfeature.NaturalEarthFeature('cultural', 'admin_0_countries', resolution, edgecolor='black', facecolor='none'))
ax.add_feature(cfeature.NaturalEarthFeature('physical', 'lakes', resolution, edgecolor='none', facecolor=cfeature.COLORS['water']), alpha=0.5)
im = (ax.pcolormesh(thetas, phis, rangeMap, cmap=mpl.cm.jet_r, alpha=0.95,shading='auto') )
fig.tight_layout()
# plt.savefig('PlotBeamInfo.pdf', dpi=96)
gc.collect()
The approach gives me something like:
Here's my goal: project a theta,r polar plot onto a cartopy map. Anyone have ideas on how to do this? How do I project a polar plot onto the globe?
You can make up any data you like for the rangeMap ... it doesn't matter... its the x,y for pcolormesh that I can't figure out.
I am trying to draw the maximum (theoretical) field of view of a satellite along its orbit. I am using Basemap, on which I want to plot different positions along the orbit (with scatter), and I would like to add the whole field of view using the tissot method (or equivalent).
The code below works fine until the latitude reaches about 75 degrees North, on a 300km altitude orbit. Beyond which the code outputs a ValueError:
"ValueError: undefined inverse geodesic (may be an antipodal point)"
import matplotlib.pyplot as plt
from mpl_toolkits.basemap import Basemap
import math
earth_radius = 6371000. # m
fig = plt.figure(figsize=(8, 6), edgecolor='w')
m = Basemap(projection='cyl', resolution='l',
llcrnrlat=-90, urcrnrlat=90,
llcrnrlon=-180, urcrnrlon=180)
# draw the coastlines on the empty map
m.drawcoastlines(color='k')
# define the position of the satellite
position = [300000., 75., 0.] # altitude, latitude, longitude
# radius needed by the tissot method
radius = math.degrees(math.acos(earth_radius / (earth_radius + position[0])))
m.tissot(position[2], position[1], radius, 100, facecolor='tab:blue', alpha=0.3)
m.scatter(position[2], position[1], marker='*', c='tab:red')
plt.show()
To be noted that the code works fine at the south pole (latitude lower than -75). I know it's a known bug, is there a known workaround for this issue?
Thanks for your help!
What you found is some of Basemap's limitations. Let's switch to Cartopy for now. The working code will be different but not much.
import matplotlib.pyplot as plt
import cartopy.crs as ccrs
import math
earth_radius = 6371000.
position = [300000., 75., 0.] # altitude (m), lat, long
radius = math.degrees(math.acos(earth_radius / (earth_radius + position[0])))
print(radius) # in subtended degrees??
fig = plt.figure(figsize=(12,8))
img_extent = [-180, 180, -90, 90]
# here, cartopy's' `PlateCarree` is equivalent with Basemap's `cyl` you use
ax = fig.add_subplot(1, 1, 1, projection = ccrs.PlateCarree(), extent = img_extent)
# for demo purposes, ...
# let's take 1 subtended degree = 112 km on earth surface (*** you set the value as needed ***)
ax.tissot(rad_km=radius*112, lons=position[2], lats=position[1], n_samples=64, \
facecolor='red', edgecolor='black', linewidth=0.15, alpha = 0.3)
ax.coastlines(linewidth=0.15)
ax.gridlines(draw_labels=False, linewidth=1, color='blue', alpha=0.3, linestyle='--')
plt.show()
With the code above, the resulting plot is:
Now, if we use Orthographic projection, (replace relevant line of code with this)
ax = fig.add_subplot(1, 1, 1, projection = ccrs.Orthographic(central_longitude=0.0, central_latitude=60.0))
you get this plot:
I am trying to draw circles at a given geographical coordinate with a certain radius using cartopy. I want to draw using an orthographic projection, which is centred at the centre of the circle.
I use the following python code for testing:
import numpy as np
import cartopy.crs as ccrs
import cartopy.feature as cfeature
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
# example: draw circle with 45 degree radius around the North pole
lon = 0
lat = 90
r = 45
# find map ranges (with 5 degree margin)
minLon = lon - r - 5
maxLon = lon + r + 5
minLat = lat - r - 5
maxLat = lat + r + 5
# define image properties
width = 800
height = 800
dpi = 96
resolution = '50m'
# create figure
fig = plt.figure(figsize=(width / dpi, height / dpi), dpi=dpi)
ax = fig.add_subplot(1, 1, 1, projection=ccrs.Orthographic(central_longitude=lon, central_latitude=lat))
ax.set_extent([minLon, maxLon, minLat, maxLat])
ax.imshow(np.tile(np.array([[cfeature.COLORS['water'] * 255]], dtype=np.uint8), [2, 2, 1]), origin='upper', transform=ccrs.PlateCarree(), extent=[-180, 180, -180, 180])
ax.add_feature(cfeature.NaturalEarthFeature('physical', 'land', resolution, edgecolor='black', facecolor=cfeature.COLORS['land']))
ax.add_feature(cfeature.NaturalEarthFeature('cultural', 'admin_0_countries', resolution, edgecolor='black', facecolor='none'))
ax.add_feature(cfeature.NaturalEarthFeature('physical', 'lakes', resolution, edgecolor='none', facecolor=cfeature.COLORS['water']), alpha=0.5)
ax.add_feature(cfeature.NaturalEarthFeature('physical', 'rivers_lake_centerlines', resolution, edgecolor=cfeature.COLORS['water'], facecolor='none'))
ax.add_feature(cfeature.NaturalEarthFeature('cultural', 'admin_1_states_provinces_lines', resolution, edgecolor='gray', facecolor='none'))
ax.add_patch(mpatches.Circle(xy=[lon, lat], radius=r, color='red', alpha=0.3, transform=ccrs.PlateCarree(), zorder=30))
fig.tight_layout()
plt.savefig('CircleTest.png', dpi=dpi)
plt.show()
I get a correct result at the equator (set lat to 0 in example above):
But when I move towards a pole the shape is distorted (lat = 45):
At the pole I only see one quarter of the circle:
I would expect to always see a perfect circle in orthographic projection, if the view is centred correctly. I also tried to use a different transform in the add_patch method, but then the circle completely vanishes!
You approach of defining the circle in PlateCarree coordinates is not going to work, because this is a cartesian projection and a circle drawn on it is not necessarily circular in spherical geometry (unless the circle is at (0, 0) as you saw).
Since you want the result to be circular in the Orthographic projection, you could draw the circle in native coordinates. This requires first defining your Orthographic projection centred on the centre of your circle, then computing what the radius of the circle (which you specify in degrees) would be in projection coordinates (distance from the centre of the projection). Doing it this way is convenient because it also gives you a neat way of determining the correct map extents. The example below computes the radius in orthographic coordinates by transforming a point 45 degrees north (or south if more convenient) away from the centre of the projection and gives the following:
The full code is below:
import numpy as np
import cartopy.crs as ccrs
import cartopy.feature as cfeature
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
# example: draw circle with 45 degree radius around the North pole
lat = 51.4198101
lon = -0.950854653584
r = 45
# Define the projection used to display the circle:
proj = ccrs.Orthographic(central_longitude=lon, central_latitude=lat)
def compute_radius(ortho, radius_degrees):
phi1 = lat + radius_degrees if lat <= 0 else lat - radius_degrees
_, y1 = ortho.transform_point(lon, phi1, ccrs.PlateCarree())
return abs(y1)
# Compute the required radius in projection native coordinates:
r_ortho = compute_radius(proj, r)
# We can now compute the correct plot extents to have padding in degrees:
pad_radius = compute_radius(proj, r + 5)
# define image properties
width = 800
height = 800
dpi = 96
resolution = '50m'
# create figure
fig = plt.figure(figsize=(width / dpi, height / dpi), dpi=dpi)
ax = fig.add_subplot(1, 1, 1, projection=proj)
# Deliberately avoiding set_extent because it has some odd behaviour that causes
# errors for this case. However, since we already know our extents in native
# coordinates we can just use the lower-level set_xlim/set_ylim safely.
ax.set_xlim([-pad_radius, pad_radius])
ax.set_ylim([-pad_radius, pad_radius])
ax.imshow(np.tile(np.array([[cfeature.COLORS['water'] * 255]], dtype=np.uint8), [2, 2, 1]), origin='upper', transform=ccrs.PlateCarree(), extent=[-180, 180, -180, 180])
ax.add_feature(cfeature.NaturalEarthFeature('physical', 'land', resolution, edgecolor='black', facecolor=cfeature.COLORS['land']))
ax.add_feature(cfeature.NaturalEarthFeature('cultural', 'admin_0_countries', resolution, edgecolor='black', facecolor='none'))
ax.add_feature(cfeature.NaturalEarthFeature('physical', 'lakes', resolution, edgecolor='none', facecolor=cfeature.COLORS['water']), alpha=0.5)
ax.add_feature(cfeature.NaturalEarthFeature('physical', 'rivers_lake_centerlines', resolution, edgecolor=cfeature.COLORS['water'], facecolor='none'))
ax.add_feature(cfeature.NaturalEarthFeature('cultural', 'admin_1_states_provinces_lines', resolution, edgecolor='gray', facecolor='none'))
ax.add_patch(mpatches.Circle(xy=[lon, lat], radius=r_ortho, color='red', alpha=0.3, transform=proj, zorder=30))
fig.tight_layout()
plt.savefig('CircleTest.png', dpi=dpi)
plt.show()
This might be a little late, but there is a convient function in Cartopy for this.
We can use Cartopy's .circle function (documentation) to generate a ring of points with a specified radius from a particular (longitude & latitude) in the Geodesic coordinate frame and then plot a polygon with those points using Shapely.
This would look something like the following
circle_points = cartopy.geodesic.Geodesic().circle(lon=lon, lat=lat, radius=radius_in_meters, n_samples=n_points, endpoint=False)
geom = shapely.geometry.Polygon(circle_points)
ax.add_geometries((geom,), crs=cartopy.crs.PlateCarree(), facecolor='red', edgecolor='none', linewidth=0)
Specifying the crs as PlateCarree does not matter and merely avoids a warning with Shapely. You will keep your desired projection. However, if you are plotting directly with the circle center on the pole, you might still have an issue and may need to do some fancy transformations (Haven't tested it recently, but recall from a few months ago it being a little wonky).
You could also manually compute these points using the pyproj library Cartopy makes use of, specifically the Geod class. Pick a point with a radius and loop through the azmoths for however fine you want your circle to be with the .inv or .fwd function similar to the suggestion in https://stackoverflow.com/a/57002776/2430454. I don't recommend this method, but used it a long while back to accomplish the same thing.
There are two points on the stereographic projection as shown in the figure:
These points are supposed to be on the end points of a dimeter of a circle. How to draw a circle passing through these two points?
Code for the above plot:
import matplotlib.pylab as plt
from mpl_toolkits.basemap import Basemap
import numpy as np
from scipy.interpolate import splev, splrep
# create instance of basemap, note we want a south polar projection to 90 = E
myMap = Basemap(projection='spstere',boundinglat=0,lon_0=180,resolution='l',round=True,suppress_ticks=True)
# set the grid up
gridX, gridY = 10.0, 15.0
parallelGrid = np.arange(-90.0,90.0,gridX)
meridianGrid = np.arange(-180.0,180.0,gridY)
# draw parallel and meridian grid, not labels are off. We have to manually create these.
myMap.drawparallels(parallelGrid,labels=[False,False,False,False])
myMap.drawmeridians(meridianGrid,labels=[False,False,False,False],labelstyle='+/-',fmt='%i')
# plot azimuth labels, with a North label.
ax = plt.gca()
ax.text(0.5,1.025,'N',transform=ax.transAxes,horizontalalignment='center',verticalalignment='bottom',size=25)
for para in np.arange(gridY,360,gridY):
x= (1.1*0.5*np.sin(np.deg2rad(para)))+0.5
y= (1.1*0.5*np.cos(np.deg2rad(para)))+0.5
ax.text(x,y,u'%i\N{DEGREE SIGN}'%para,transform=ax.transAxes,horizontalalignment='center',verticalalignment='center')
summerAzi = np.array([0, 360])
summerAlt = np.array([40, 4])
summerX, summerY = myMap(summerAzi, -summerAlt)
summerX_new = np.linspace(summerX.min(), summerX.max(),30)
summerY_smooth = splev(summerX_new, splrep(summerX, summerY, k=1))
myMap.plot(summerX_new, summerY_smooth, 'g')
myMap.plot(summerX, summerY, 'go')
plt.show()
Inbuilt tissot() function is good enough to plot circles on a conformal projections (as in this case). On non-conformal projections, it plots ellipses.
Here the mid point of the tissot indicatrix is (0, -22) in degrees.
Its radius = (40-4)/2 = 18 in degrees.
Number of points = 36 is fine.
The relevant code is:
myMap.tissot(0, -22, 18, 36, \
facecolor='none', \
edgecolor='#ff0000', \
linewidth=1, \
alpha=1)
The circle in this polar representation will not look like a circle on a rectangular grid (i.e. "round"). Apart from that you can draw a circle just as you would on the cartesian plane, starting in polar coordinates, transforming to cartesian coordinates, offset the center and use the plot function.
summerAzi = np.array([0, 360])
summerAlt = -np.array([40, 4])
summerX, summerY = myMap(summerAzi, summerAlt)
phi = np.linspace(0,2.*np.pi)
r = np.abs(np.diff(summerAlt))/2.
x = r*np.cos(phi)
y = -r*np.sin(phi)+summerAlt.mean()
X,Y= myMap(x,y)
myMap.plot(X,Y, color="crimson")
myMap.plot(summerX, summerY, color="gold", marker="o")