Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Future Spherical Class] Add SArc #478

Open
wants to merge 14 commits into
base: main
Choose a base branch
from
1 change: 1 addition & 0 deletions pyresample/future/spherical/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,5 @@
# with this program. If not, see <http://www.gnu.org/licenses/>.
"""Future features that are backwards incompatible with current functionality."""

from .arc import SArc # noqa
from .point import SMultiPoint, SPoint # noqa
209 changes: 209 additions & 0 deletions pyresample/future/spherical/arc.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# Copyright (c) 2022 Pyresample developers
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""Define a great-circle arc between two points on the sphere through the SArc class."""

import numpy as np
import pyproj
from shapely.geometry import LineString

from pyresample.spherical import Arc

from .point import SPoint

EPSILON = 0.0000001


def _check_valid_arc(start_point, end_point):
"""Check arc validity."""
if start_point == end_point:
raise ValueError("An SArc can not be represented by the same start and end SPoint.")
if start_point.is_pole() and end_point.is_pole():
raise ValueError("An SArc can not be uniquely defined between the two poles.")
if start_point.is_on_equator and end_point.is_on_equator and abs(start_point.lon - end_point.lon) == np.pi:
raise ValueError(
"An SArc can not be uniquely defined on the equator if start and end points are 180 degrees apart.")


class SArc(Arc):
"""Object representing a great-circle arc between two points on a sphere.

The ``start_point`` and ``end_point`` must be SPoint objects.
The great-circle arc is defined as the shortest track(s) between the two points.
Between the north and south pole there are an infinite number of great-circle arcs.
"""

def __init__(self, start_point, end_point):
_check_valid_arc(start_point, end_point)
super().__init__(start_point, end_point)

def __hash__(self):
"""Define SArc hash."""
return hash((float(self.start.lon), float(self.start.lat),
float(self.end.lon), float(self.end.lat)))

def is_on_equator(self):
ghiggi marked this conversation as resolved.
Show resolved Hide resolved
"""Check if the SArc lies on the equator."""
if self.start.lat == 0 and self.end.lat == 0:
return True
return False

def __eq__(self, other):
"""Check equality."""
if self.start == other.start and self.end == other.end:
return True
return False

def reverse_direction(self):
"""Reverse SArc direction."""
return SArc(self.end, self.start)

@property
def vertices(self):
"""Get start SPoint and end SPoint (radians) vertices array."""
return self.start.vertices, self.end.vertices

@property
def vertices_in_degrees(self):
"""Get start SPoint and end SPoint (degrees) vertices array."""
return self.start.vertices_in_degrees, self.end.vertices_in_degrees

def get_next_intersection(self, other_arc):
"""Overwrite a Arc deprecated function. Inherited from Arc class."""
raise ValueError("This function is deprecated.")

def intersections(self, other_arc):
"""Overwritea Arc deprecated function. Inherited from Arc class."""
raise ValueError("'SArc.intersections' is deprecated. Use '_great_circle_intersections' instead.")

def _great_circle_intersections(self, other_arc):
"""Compute the intersections points of the greats circles over which the arcs lies.

A great circle divides the sphere in two equal hemispheres.
"""
end_lon = self.end.lon
other_end_lon = other_arc.end.lon

if self.end.lon - self.start.lon > np.pi:
end_lon -= 2 * np.pi
if other_arc.end.lon - other_arc.start.lon > np.pi:
other_end_lon -= 2 * np.pi
if self.end.lon - self.start.lon < -np.pi:
end_lon += 2 * np.pi
if other_arc.end.lon - other_arc.start.lon < -np.pi:
other_end_lon += 2 * np.pi

end_point = SPoint(end_lon, self.end.lat)
other_end_point = SPoint(other_end_lon, other_arc.end.lat)

ea_ = self.start.cross2cart(end_point).normalize()
eb_ = other_arc.start.cross2cart(other_end_point).normalize()

cross = ea_.cross(eb_)
lat = np.arctan2(cross.cart[2],
np.sqrt(cross.cart[0] ** 2 + cross.cart[1] ** 2))
lon = np.arctan2(cross.cart[1], cross.cart[0])

return (SPoint(lon, lat), SPoint(lon + np.pi, -lat))

def intersection(self, other_arc):
"""Overwrite a Arc deprecated function. Inherited from Arc class."""
raise ValueError("'SArc.intersection' is deprecated. Use 'intersection_point' instead.")

def intersection_point(self, other_arc):
"""Compute the intersection point between two arcs.

If arc and *other_arc* intersect, it returns the intersection SPoint.
If arc and *other_arc* does not intersect, it returns None.
If same arc (also same direction), it returns None.
"""
# If same arc (same direction), return None
if self == other_arc:
return None

great_circles_intersection_spoints = self._great_circle_intersections(other_arc)

for spoint in great_circles_intersection_spoints:
a = self.start
b = self.end
c = other_arc.start
d = other_arc.end

ab_dist = a.hdistance(b)
cd_dist = c.hdistance(d)
ap_dist = a.hdistance(spoint)
bp_dist = b.hdistance(spoint)
cp_dist = c.hdistance(spoint)
dp_dist = d.hdistance(spoint)

if (((spoint in (a, b)) or (abs(ap_dist + bp_dist - ab_dist) < EPSILON)) and
((spoint in (c, d)) or (abs(cp_dist + dp_dist - cd_dist) < EPSILON))):
return spoint
ghiggi marked this conversation as resolved.
Show resolved Hide resolved
return None

def intersects(self, other_arc):
"""Check if the current Sarc and another SArc intersect."""
return bool(self.intersection_point(other_arc))

def midpoint(self, ellips='sphere'):
"""Return the SArc midpoint SPoint."""
geod = pyproj.Geod(ellps=ellips)
lon_start = self.start.lon
lon_end = self.end.lon
lat_start = self.start.lat
lat_end = self.end.lat
lon_mid, lat_mid = geod.npts(lon_start, lat_start, lon_end, lat_end, npts=1, radians=True)[0]
return SPoint(lon_mid, lat_mid)

def to_shapely(self):
"""Convert to Shapely LineString."""
start_coord, end_coord = self.vertices_in_degrees
return LineString((start_coord.tolist()[0], end_coord.tolist()[0]))

# def segmentize(self, npts=0, max_distance=0, ellips='sphere'):
# """Segmentize the great-circle arc.

# It returns an SLine.
# npts or max_distance are mutually exclusively. Specify one of them.
# max_distance must be provided in kilometers.
# """
# if npts != 0:
# npts = npts + 2 # + 2 to account for initial and terminus
# geod = pyproj.Geod(ellps=ellips)
# lon_start = self.start.lon
# lon_end = self.end.lon
# lat_start = self.start.lat
# lat_end = self.end.lat
# points = geod.inv_intermediate(lon_start, lat_start, lon_end, lat_end,
# del_s=max_distance,
# npts=npts,
# radians=True,
# initial_idx=0, terminus_idx=0)
# lons, lats = (points.lons, points.lats)
# lons = np.asarray(lons)
# lats = np.asarray(lats)
# vertices = np.stack((lons, lats)).T
# return SLine(vertices)

# def to_line(self):
# """Convert to SLine."""
# vertices = np.vstack(self.vertices)
# return SLine(vertices)

# def plot(self, *args, **kwargs):
# """Convert to SLine."""
# self.to_line.plot(*args, **kwargs)
32 changes: 29 additions & 3 deletions pyresample/future/spherical/point.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,32 @@ def from_degrees(cls, lon, lat):
"""Create SPoint from lon/lat coordinates in degrees."""
return cls(np.deg2rad(lon), np.deg2rad(lat))

def is_pole(self):
"""Test if the point is on a pole."""
if self.lat in [-np.pi / 2, np.pi / 2]:
return True
return False
ghiggi marked this conversation as resolved.
Show resolved Hide resolved

def is_on_equator(self):
"""Test if the point is on the equator."""
if self.lat == 0:
return True
return False
ghiggi marked this conversation as resolved.
Show resolved Hide resolved

def __str__(self):
"""Get simplified representation of lon/lat arrays in radians."""
return str((float(self.lon), float(self.lat)))

def __repr__(self):
"""Get simplified representation of lon/lat arrays in radians."""
return str((float(self.lon), float(self.lat)))
coords = str((float(self.lon), float(self.lat)))
return f"{self.__class__.__name__}: {coords}"

def __eq__(self, other):
"""Check equality."""
if self.lat == other.lat and self.is_pole():
return True
return np.allclose((self.lon, self.lat), (other.lon, other.lat))
ghiggi marked this conversation as resolved.
Show resolved Hide resolved

def to_shapely(self):
"""Convert the SPoint to a shapely Point (in lon/lat degrees)."""
Expand All @@ -71,15 +90,22 @@ def from_degrees(cls, lon, lat):

def __eq__(self, other):
"""Check equality."""
return np.allclose(self.lon, other.lon) and np.allclose(self.lat, other.lat)
lon1 = self.lon.copy()
lon2 = other.lon.copy()
lat1 = self.lat
lat2 = other.lat
# Set longitude 0 at the pole for array comparison
lon1[np.isin(lat1, [-np.pi / 2, np.pi / 2])] = 0
lon2[np.isin(lat2, [-np.pi / 2, np.pi / 2])] = 0
return np.allclose(lon1, lon2) and np.allclose(lat1, lat2)

def __str__(self):
"""Get simplified representation of lon/lat arrays in radians."""
return str(self.vertices)

def __repr__(self):
"""Get simplified representation of lon/lat arrays in radians."""
return str(self.vertices)
return f"{self.__class__.__name__}:\n {self.vertices}"

def to_shapely(self):
"""Convert the SMultiPoint to a shapely MultiPoint (in lon/lat degrees)."""
Expand Down
24 changes: 14 additions & 10 deletions pyresample/spherical.py
Original file line number Diff line number Diff line change
Expand Up @@ -366,19 +366,19 @@ def __init__(self, start, end):
def __eq__(self, other):
"""Check equality."""
if self.start == other.start and self.end == other.end:
ghiggi marked this conversation as resolved.
Show resolved Hide resolved
return 1
return 0
return True
return False
ghiggi marked this conversation as resolved.
Show resolved Hide resolved

def __ne__(self, other):
"""Check not equal comparison."""
return not self.__eq__(other)

def __str__(self):
"""Get simplified representation."""
"""Get simplified str representation."""
ghiggi marked this conversation as resolved.
Show resolved Hide resolved
return str(self.start) + " -> " + str(self.end)

def __repr__(self):
"""Get simplified representation."""
"""Get simplified repr representation."""
return str(self.start) + " -> " + str(self.end)

def angle(self, other_arc):
Expand Down Expand Up @@ -427,9 +427,9 @@ def angle(self, other_arc):
return angle

def intersections(self, other_arc):
"""Give the two intersections of the greats circles defined by the current arc and *other_arc*.
"""Compute the intersections points of the greats circles over which the arcs lies.
ghiggi marked this conversation as resolved.
Show resolved Hide resolved

From http://williams.best.vwh.net/intersect.htm
A great circle divides the sphere in two equal hemispheres.
"""
end_lon = self.end.lon
other_end_lon = other_arc.end.lon
Expand Down Expand Up @@ -465,10 +465,10 @@ def intersects(self, other_arc):
return bool(self.intersection(other_arc))

def intersection(self, other_arc):
"""Return where, if the current arc and the *other_arc* intersect.
"""Compute the intersection point between two arcs.

None is returned if there is not intersection. An arc is defined
as the shortest tracks between two points.
If arc and *other_arc* intersect, it returns the intersection SPoint.
If arc and *other_arc* does not intersect, it returns None.
"""
if self == other_arc:
return None
Expand All @@ -490,7 +490,11 @@ def intersection(self, other_arc):
return None

def get_next_intersection(self, arcs, known_inter=None):
"""Get the next intersection between the current arc and *arcs*."""
"""Get the next intersection between the current arc and *arcs*.

It return a tuple with the intersecting point and the arc within *arcs*
that intersect the self arc.
"""
res = []
for arc in arcs:
inter = self.intersection(arc)
Expand Down
Loading