Lat/long transformed to OSGB has an offset when plotted - cartopy

If I take a set of lat/long points, convert them to OSGB crs using cartopy's "transform_points" method and then plot them, they are offset compared to plotting the lat/long directly. If I transform them back to lat/long, they plot ok. If I convert them to UTM coords they plot ok. Am I missing something?
Example code:
import numpy as np
import matplotlib.pyplot as plt
import cartopy.crs as ccrs
from cartopy.io.img_tiles import OSM
# set up figure and background map tile
fig = plt.figure()
ax = plt.axes(projection=OSM().crs)
ax.set_extent((-2.473911, -2.410147, 50.567006, 50.605896))
ax.add_image(imagery, 14)
# Plot some test points using lat/long (PlateCarree) crs
test_lonlat = np.array([[-2.464482, -2.432523, -2.437892], [50.593243, 50.596390, 50.573177]])
plt.plot(test_lonlat[0], test_lonlat[1], 'r:+', transform=ccrs.PlateCarree())
# Transform to OSGB coords and plot
test_OS = ccrs.OSGB().transform_points(ccrs.PlateCarree(), test_lonlat[0], test_lonlat[1])
plt.plot(test_OS[:, 0], test_OS[:, 1], 'kx-', transform=ccrs.OSGB())
plt.show()
Thanks for any advice.
Simpler example using single point
fig = plt.figure(figsize=[7, 7])
ax = plt.axes(projection=OSM().crs)
ax.set_extent((-2.435, -2.405, 50.575, 50.595))
ax.add_image(OSM(), 14)
# Simpler test of OSGB crs
# Take point at end of outer breakwater: OS (370783, 76226) or lat/long (-2.414343, 50.584978)
point_os = [370774.0, 76221.0]
point_lonlat = [-2.414278, 50.584971]
# Plot each on OSM tile - both look ok but small error
plt.plot(point_os[0], point_os[1], 'r+', transform=ccrs.OSGB(), markersize=15)
plt.plot(point_lonlat[0], point_lonlat[1], 'kx', transform=ccrs.PlateCarree(), markersize=10)
# Convert lat/long to OSGB and plot - now offset by ~50 m to NW
point_os_new = ccrs.OSGB().transform_point(point_lonlat[0], point_lonlat[1], ccrs.PlateCarree())
plt.plot(point_os_new[0], point_os_new[1], 'm^', transform=ccrs.OSGB(), markersize=10)
# Print both sets of OS coords
print(f'Original point: {point_os}')
print(f'Transformed point: {point_os_new}')

When you create an axes to plot with this line of code
ax = plt.axes(projection=OSM().crs)
you are expected to use cartopy.crs.Mercator projection coordinates to plot on it as OSM().crs is Mercator.
This part of your code:
# Transform to OSGB coords and plot
test_OS = ccrs.OSGB().transform_points(ccrs.PlateCarree(), \
test_lonlat[0], test_lonlat[1])
plt.plot(test_OS[:, 0], test_OS[:, 1], 'kx-', transform=ccrs.OSGB())
uses OSGB coordinates to plot on Mercator projection. That's the wrong part.
The correct lines of code should be
test2_OS = OSM().crs.transform_points(ccrs.PlateCarree(), \
test_lonlat[0], test_lonlat[1])
ax.plot(test2_OS[:, 0], test2_OS[:, 1], 'kx-', transform=OSM().crs)
Edit1
The tile images in my plot above is OSM(), via ax.add_image(OSM(), 14).
The problems the OP faces is not using matching coordinate systems between the data and the axes, or wrong values.

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()

PyPlot ConnectionPatch between CartoPy GeoAxes

The ConnectionPatch is a useful way to draw a line between two points on two different axes (demo). Is it possible to use this class when one (or both) of the axes is of Cartopy GeoAxes type? A related answer suggests a work-around but I would prefer to avoid this.
I can not answer your question about the use of that class thing. But, if you are interested in plotting the lines between 2 different Cartopy geoaxes, or between matplotlib axes and a geoaxe, that can be achieved with some coordinate transformation. Here is a runnable code and the output plot. I have written some comments within the code to help explain the important steps.
For further information about coordinate system and tranformation:
Cartopy https://scitools.org.uk/cartopy/docs/latest/tutorials/understanding_transform.html
Since Cartopy is built on top of Matplotlib, you need to look into the related subject in Matplotlib.
Matplotlib https://matplotlib.org/3.2.1/tutorials/advanced/transforms_tutorial.html
import cartopy
import cartopy.mpl.geoaxes
import matplotlib.pyplot as plt
from mpl_toolkits.axes_grid1.inset_locator import inset_axes
fig, ax = plt.subplots()
fig.set_size_inches([8,8]) # 9,6; 8,9; 8,3 all OK
# Plot simple line on main axes
ax.plot([4,5,3,1,2])
p1 = [0.5,3.0] # Bangkok text location
p2 = [0.5,2.75] # Himalaya text location
# Plot texts (Bangkok, Himalaya) on the main axes
ax.text(*p1, "Bangkok", ha='right')
ax.text(*p2, "Himalaya", ha='right')
# Ploting on UR inset map (cartopy) on the main axes (ax)
bkk_lon, bkk_lat = 100, 13 # Bangkok
hml_lon, hml_lat = 83.32, 29.22 # Everest peak
# Create cartopy geoaxes inset axes as part of the main axes 'ax'
axins = inset_axes(ax, width="40%", height="30%", loc="upper right",
axes_class = cartopy.mpl.geoaxes.GeoAxes,
axes_kwargs = dict(map_projection = cartopy.crs.PlateCarree()))
# Set map limits on that axes (for Thailand)
llx, lly = 95, 0
urx, ury = 110, 25
axins.set_xlim((llx, urx))
axins.set_ylim((lly, ury))
# Plot coastlines
axins.add_feature(cartopy.feature.COASTLINE)
# Plot line across the inset mao, LL to UR; OK
#ll_p, ur_p = [llx,urx], [lly,ury]
#axins.plot(ll_p, ur_p, "r--")
axins.plot(bkk_lon, bkk_lat, 'ro', transform=cartopy.crs.PlateCarree()) # OK!
# Create another inset map on the main axes (ax)
axins2 = inset_axes(ax, width="40%", height="30%", loc="lower left",
axes_class = cartopy.mpl.geoaxes.GeoAxes,
axes_kwargs = dict(map_projection = cartopy.crs.PlateCarree()))
# Set map limits on that axes (second inset map)
llx2, lly2 = -60, -20
urx2, ury2 = 120, 90
axins2.set_xlim((llx2, urx2))
axins2.set_ylim((lly2, ury2))
axins2.add_feature(cartopy.feature.COASTLINE)
# Plot line from UK to BKK, OK
#p21, p22 = [0, 100], [40, 13]
#axins2.plot(p21, p22, "r--")
# Plot blue dot at Himalaya
axins2.plot(hml_lon, hml_lat, "bo")
plt.draw() # Do this to get updated position
# Do coordinate transformation to get BKK, HML locations in display coordinates
# from axins_data_xy to dp_xy
dpxy_bkk_axins = axins.transData.transform((bkk_lon, bkk_lat)) # get display coordinates
# from axins2_data_xy to dp_xy
dpxy_bkk_axins2 = axins2.transData.transform((hml_lon, hml_lat)) # get display coordinates
# Do coordinate transformation to get BKK, HML locations in data coordinates of the main axes 'ax'
# from both dp_xy to main_ax_data
ur_bkk = ax.transData.inverted().transform( dpxy_bkk_axins )
ll_hml = ax.transData.inverted().transform( dpxy_bkk_axins2 )
# Prep coordinates for line connecting BKK to HML
xs = ur_bkk[0], ll_hml[0]
ys = ur_bkk[1], ll_hml[1]
xs = ur_bkk[0], ll_hml[0]
ys = ur_bkk[1], ll_hml[1]
ax.plot(xs, ys, 'g--') # from Bkk to Himalaya of different inset maps
# Plot lines from texts (on main axes) to locations on maps
ax.plot([p1[0], ur_bkk[0]], [p1[1], ur_bkk[1]], 'y--')
ax.plot([p2[0], ll_hml[0]], [p2[1], ll_hml[1]], 'y--')
# Set cartopy inset background invisible
axins.background_patch.set_visible(False)
axins2.background_patch.set_visible(False)
plt.show()
The output plot:-

Cartopy non-zero central longitude distorted with contourf

I am trying to plot the surface temperature from a NetCDF file using Cartopy and contourf. The domain of my plot is 30S to 60N and 90.044495E to 89.95552E (so all the way around the Earth centered on 90W). Here is a section of my code:
import numpy as np
import wrf as wrf
import matplotlib.pyplot as plt
import cartopy.crs as ccrs
cart_proj = wrf.get_cartopy(skintemp)
lats, lons = wrf.latlon_coords(skintemp)
ax = plt.axes(projection=cart_proj)
ax.coastlines('50m', linewidth=0.8)
clevels = np.linspace(230,300,8)
cmap = plt.cm.YlOrRd
contours_fill = plt.contourf(wrf.to_np(lons), wrf.to_np(lats), skintemp, cmap=cmap, levels = clevels, transform=ccrs.PlateCarree(),extend="both")
cbar = plt.colorbar(contours_fill, shrink = .65, orientation='horizontal', pad=.05)
plt.show()
skintemp, lats and lons are all 2D arrays with dimensions (454, 1483), ordered (lat,lon), and cart_proj = wrf.projection.MercatorWithLatTS.
When I show the plot, it's distorted and incorrect:
I have determined that the issue has to do with the non-zero central longitude. The problem appears to be when the longitude changes from 179.90082 to -179.85632. lons.values[0,370]=179.90082, so I changed contourf to the following:
contours_fill = plt.contourf(wrf.to_np(lons[:,0:371]), wrf.to_np(lats[:,0:371]), skintemp[:,0:371], cmap=cmap, levels = clevels, transform=ccrs.PlateCarree(),extend="both")
which produces the following correct figure:
And when I change contourf to:
contours_fill = plt.contourf(wrf.to_np(lons[:,371:-1]), wrf.to_np(lats[:,371:-1]), skintemp[:,371:-1], cmap=cmap, levels = clevels, transform=ccrs.PlateCarree(),extend="both")
I get the other part of the map:
I cannot seem to get both parts of the map to display correctly together. I tried using contourf twice in the same plot, one for each section of the map, but only the last contourf line plots. Any help would be much appreciated!

Get projected coordinates from geometric coordinates

I have a map figure rendered with Cartopy and Matplotlib. I have a specific geometric coordinate (in lat/lon) and I would like to know the pixel coordinate closest to this geometric coordinate's projection (if it is visible), for instance to draw a graphic over the coordinate on the map.
(Note I don't want to draw with Matplotlib; I'm exporting the figure as a bitmap image and drawing in a different part of the pipeline.)
This documentation suggests it might be something like this:
import cartopy, matplotlib.pyplot
fig = matplotlib.pyplot.figure()
ax = fig.add_axes([0, 0, 1, 1], projection=cartopy.crs.Orthographic())
ax.add_feature(cartopy.feature.LAND, facecolor='black')
# Print the location of New York City in display coordinates
lon, lat = -74.0060, 40.7128
trans = cartopy.crs.Geodetic()._as_mpl_transform(ax)
x, y = trans.transform((lon, lat))
print(x, y)
# Or this way
projx, projy = ax.projection.transform_point(lon, lat, cartopy.crs.Geodetic())
x, y = ax.transData.transform((projx, projy))
print(x, y)
Though interestingly, if I plot this point, the figure centers on and zooms into Manhattan, and then the output display coordinates are indeed in the center of the figure at (640, 480).
matplotlib.pyplot.plot(lon, lat, marker='o', color='red', markersize=12,
alpha=0.7, transform=cartopy.crs.Geodetic())
I just found that the transforms are not properly set until the figure is in its final state. So the key is to first draw the figure,
fig.canvas.draw()
or at least apply the aspect properly.
ax.apply_aspect()
Then you will get the correct pixel coordinates out,
import matplotlib.pyplot as plt
import cartopy
import cartopy.crs as ccrs
fig = plt.figure()
ax = fig.add_axes([0, 0, 1, 1], projection=ccrs.PlateCarree())
ax.add_feature(cartopy.feature.LAND, facecolor='black')
ax.set_global()
# before being able to call any of the transforms, the figure needs to be drawn
fig.canvas.draw()
# or
# ax.apply_aspect()
# Print the location of New York City in display coordinates
lon, lat = -74.0060, 40.7128
trans = ccrs.PlateCarree()._as_mpl_transform(ax)
x, y = trans.transform_point((lon, lat))
print(x,y)
plt.show()
This prints:
188.43377777777778 312.3783111111111
Note that those coordinates refer to the pixels from the lower left corner.
In my sample code I failed to specify the extent of the map. If I add
ax.set_global()
then the transformed coordinates are sensible.
I presented two ways to compute the transformed coordinates, but the way with _as_mpl_transform() seems to return the center point when New York City is not visible. The way with ax.projection.transform_point() returns NaN when off-screen.

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: