Source code for cartopy.geodesic

# Copyright Crown and Cartopy Contributors
#
# This file is part of Cartopy and is released under the BSD 3-clause license.
# See LICENSE in the root of the repository for full licensing details.
#
# cython: embedsignature=True

"""
This module defines the Geodesic class which can interface with the Proj
geodesic functions. See the `Proj geodesic page <https://proj.org/geodesic.html>`_
for more background information.

"""
import numpy as np
import pyproj
import shapely.geometry as sgeom


[docs] class Geodesic: """ Define an ellipsoid on which to solve geodesic problems. """ def __init__(self, radius=6378137.0, flattening=1 / 298.257223563): """ Parameters ---------- radius: float, optional Equatorial radius (metres). Defaults to the WGS84 semimajor axis (6378137.0 metres). flattening: float, optional Flattening of ellipsoid. Setting flattening = 0 gives a sphere. Negative flattening gives a prolate ellipsoid. If flattening > 1, set flattening to 1/flattening. Defaults to the WGS84 flattening (1/298.257223563). """ if flattening > 1: flattening = 1 / flattening self.geod = pyproj.Geod(a=radius, f=flattening) self.radius = radius self.flattening = flattening def __str__(self): return (f'<Geodesic: radius={self.radius:0.3f}, ' f'flattening=1/{1/self.flattening:0.3f}>')
[docs] def direct(self, points, azimuths, distances): """ Solve the direct geodesic problem where the length of the geodesic is specified in terms of distance. Can accept and broadcast length 1 arguments. For example, given a single start point and distance, an array of different azimuths can be supplied to locate multiple endpoints. Parameters ---------- points: array_like, shape=(n *or* 1, 2) The starting longitude-latitude point(s) from which to travel. azimuths: float or array_like with shape=(n, ) List of azimuth values (degrees). distances : float or array_like with shape(n, ) List of distances values (metres). Returns ------- `numpy.ndarray`, shape=(n, 3) The longitudes, latitudes, and forward azimuths of the located endpoint(s). """ # Create numpy arrays from inputs, and ensure correct shape. Note: # reshape(-1) returns a 1D array from a 0 dimensional array as required # for broadcasting. pts = np.array(points, dtype=np.float64).reshape((-1, 2)) azims = np.array(azimuths, dtype=np.float64).reshape(-1) dists = np.array(distances, dtype=np.float64).reshape(-1) sizes = [pts.shape[0], azims.size, dists.size] n_points = max(sizes) if not all(size in [1, n_points] for size in sizes): raise ValueError("Inputs must have common length n or length one.") # Broadcast any length 1 arrays to the correct size. if pts.shape[0] == 1: orig_pts = pts pts = np.empty([n_points, 2], dtype=np.float64) pts[:, :] = orig_pts if azims.size == 1: azims = np.repeat(azims, n_points) if dists.size == 1: dists = np.repeat(dists, n_points) lons, lats, azims = self.geod.fwd(pts[:, 0], pts[:, 1], azims, dists) # Convert back azimuth to forward azimuth. azims += np.where(azims > 0, -180, 180) return np.column_stack([lons, lats, azims])
[docs] def inverse(self, points, endpoints): """ Solve the inverse geodesic problem. Can accept and broadcast length 1 arguments. For example, given a single start point, an array of different endpoints can be supplied to find multiple distances. Parameters ---------- points: array_like, shape=(n *or* 1, 2) The starting longitude-latitude point(s) from which to travel. endpoints: array_like, shape=(n *or* 1, 2) The longitude-latitude point(s) to travel to. Returns ------- `numpy.ndarray`, shape=(n, 3) The distances, and the (forward) azimuths of the start and end points. """ # Create numpy arrays from inputs, and ensure correct shape. points = np.array(points, dtype=np.float64) endpoints = np.array(endpoints, dtype=np.float64) if points.ndim > 2 or (points.ndim == 2 and points.shape[1] != 2): raise ValueError( f'Expecting input points to be (N, 2), got {points.shape}') pts = points.reshape((-1, 2)) epts = endpoints.reshape((-1, 2)) sizes = [pts.shape[0], epts.shape[0]] n_points = max(sizes) if not all(size in [1, n_points] for size in sizes): raise ValueError("Inputs must have common length n or length one.") # Broadcast any length 1 arrays to the correct size. if pts.shape[0] == 1: orig_pts = pts pts = np.empty([n_points, 2], dtype=np.float64) pts[:, :] = orig_pts if epts.shape[0] == 1: orig_pts = epts epts = np.empty([n_points, 2], dtype=np.float64) epts[:, :] = orig_pts start_azims, end_azims, dists = self.geod.inv(pts[:, 0], pts[:, 1], epts[:, 0], epts[:, 1]) # Convert back azimuth to forward azimuth. end_azims += np.where(end_azims > 0, -180, 180) return np.column_stack([dists, start_azims, end_azims])
[docs] def circle(self, lon, lat, radius, n_samples=180, endpoint=False): """ Find a geodesic circle of given radius at a given point. Parameters ---------- lon : float Longitude coordinate of the centre. lat : float Latitude coordinate of the centre. radius : float The radius of the circle (metres). n_samples: int, optional Integer number of sample points of circle. endpoint: bool, optional Whether to repeat endpoint at the end of returned array. Returns ------- `numpy.ndarray`, shape=(n_samples, 2) The evenly spaced longitude-latitude points on the circle. """ # Put the input arguments into c-typed values. center = np.array([lon, lat]).reshape((1, 2)) radius_m = np.asarray(radius).reshape(1) azimuths = np.linspace(360., 0., n_samples, endpoint=endpoint).astype(np.double) return self.direct(center, azimuths, radius_m)[:, 0:2]
[docs] def geometry_length(self, geometry): """ Return the distance (in physical meters) of the given Shapely geometry. The geometry is assumed to be in spherical (lon, lat) coordinates. Parameters ---------- geometry : `shapely.geometry.BaseGeometry` The Shapely geometry to compute the length of. For polygons, the exterior length will be calculated. For multi-part geometries, the sum of the parts will be computed. """ result = None if hasattr(geometry, 'geoms'): # Multi-geometry. result = sum(self.geometry_length(geom) for geom in geometry.geoms) elif hasattr(geometry, 'exterior'): # Polygon. result = self.geometry_length(geometry.exterior) elif (hasattr(geometry, 'coords') and not isinstance(geometry, sgeom.Point)): coords = np.array(geometry.coords) result = self.geometry_length(coords) elif isinstance(geometry, np.ndarray): coords = geometry distances, _, _ = np.array( self.inverse(coords[:-1, :], coords[1:, :]).T) result = distances.sum() else: raise TypeError(f'Unhandled type {geometry.__class__}') return result