-
Notifications
You must be signed in to change notification settings - Fork 21
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
DM-12924: SpherePoint.offset should work at the poles and for negative offsets #302
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -62,7 +62,8 @@ class SpherePoint final { | |
/** | ||
* Construct a SpherePoint from a longitude and latitude. | ||
* | ||
* @param longitude The longitude of the point. | ||
* @param longitude The longitude of the point. `+/-inf` is treated as `nan` | ||
* (because longitude is wrapped to a standard range and infinity cannot be wrapped) | ||
* @param latitude The latitude of the point. Must be in the | ||
* interval [-π/2, π/2] radians. | ||
* | ||
|
@@ -76,7 +77,8 @@ class SpherePoint final { | |
/** | ||
* Construct a SpherePoint from double longitude and latitude. | ||
* | ||
* @param longitude The longitude of the point. | ||
* @param longitude The longitude of the point. `+/-inf` is treated as `nan` | ||
* (because longitude is wrapped to a standard range and infinity cannot be wrapped) | ||
* @param latitude The latitude of the point. Must be in the | ||
* interval [-π/2, π/2] radians. | ||
* @param units The units of longitude and latitude | ||
|
@@ -99,6 +101,9 @@ class SpherePoint final { | |
* Thrown if `vector` is the zero vector. | ||
* | ||
* @exceptsafe Provides strong exception guarantee. | ||
* | ||
* @note If the SpherePoint is at a pole then longitude is set to 0. | ||
* That provides predictable behavior for @ref bearingTo and @ref offset. | ||
*/ | ||
explicit SpherePoint(Point3D const& vector); | ||
|
||
|
@@ -148,11 +153,6 @@ class SpherePoint final { | |
/** | ||
* The longitude of this point. | ||
* | ||
* If this point is at a coordinate pole, the longitude is undefined, and | ||
* this method may return any value. If the SpherePoint implementation | ||
* allows multiple values of longitude from a pole, they shall all be | ||
* treated as valid representations of the same point. | ||
* | ||
* @returns the longitude, in the interval [0, 2π) radians. | ||
* | ||
* @exceptsafe Shall not throw exceptions. | ||
|
@@ -202,10 +202,8 @@ class SpherePoint final { | |
* @exceptsafe Shall not throw exceptions. | ||
*/ | ||
bool atPole() const noexcept { | ||
// Unit tests indicate we don't need to worry about epsilon-errors | ||
// Objects constructed from lat=90*degrees, <0,0,1>, etc. all have | ||
// atPole() = true. More complex operations like bearingTo have also | ||
// been tested near the poles with no ill effects | ||
// Unit tests indicate we don't need to worry about epsilon-errors, in that | ||
// Objects constructed from lat=90*degrees, <0,0,1>, etc. all have atPole() = true. | ||
return fabs(_latitude) >= HALFPI; | ||
} | ||
|
||
|
@@ -225,17 +223,18 @@ class SpherePoint final { | |
*/ | ||
|
||
/** | ||
* `true` if two points represent the same position. | ||
* | ||
* Points are considered equal if and only if they represent the same | ||
* location, regardless of how they were constructed. In particular, | ||
* representations of the north or south pole with different longitudes | ||
* are considered equal. | ||
* `true` if two points have the same longitude and latitude. | ||
* | ||
* @param other the point to test for equality | ||
* @returns true if this point matches `other` exactly, false otherwise | ||
* @returns true if this point has exactly the same values of getLongitude() and getLatitude() | ||
* as `other`, false otherwise | ||
* | ||
* @exceptsafe Shall not throw exceptions. | ||
* | ||
* @note Two points at the same pole will compare unequal if they have different longitudes, | ||
* despite representing the same point on the unit sphere. | ||
* This is important because the behavior of @ref offset and @ref bearingTo | ||
* depend on longitude, even at the pole. | ||
* | ||
* @warning Points whose @ref getLongitude(), @ref getLatitude(), or | ||
* @ref getVector() methods return `NaN` shall not compare | ||
|
@@ -254,20 +253,19 @@ class SpherePoint final { | |
bool operator!=(SpherePoint const& other) const noexcept; | ||
|
||
/** | ||
* Direction from one point to another. | ||
* Orientation at this point of the great circle arc to another point. | ||
* | ||
* The orientation is measured in a plane tangent to the sphere at this point, with 0 degrees along | ||
* the direction of increasing longitude and 90 degrees along the direction of increasing latitude. | ||
* Thus for any point except the pole, the arc is due east at 0 degrees and due north at 90 degrees. | ||
* To understand the behavior at the poles it may help to imagine the point has been shifted slightly | ||
* by decreasing the absolute value of its latitude. | ||
* | ||
* This method finds the shortest (great-circle) arc between two | ||
* points, and characterizes its direction by the angle between | ||
* it and a line of latitude passing through this point. 0 represents | ||
* due east, π/2 represents due north. If the points are on | ||
* opposite sides of the sphere, the bearing may be any value. | ||
* If the points are on opposite sides of the sphere then the returned bearing may be any value. | ||
* | ||
* @param other the point to which to measure the bearing | ||
* @returns the direction, as defined above, in the interval [0, 2π). | ||
* | ||
* @throws pex::exceptions::DomainError | ||
* Thrown if `this.atPole()`. | ||
* | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please add a paragraph documenting the behavior at the poles. The current description implies that the bearing must always be -π/2 at the north pole and π/2 at the south pole, which I suspect is no longer what happens. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. OK. I rewrote the description to one whose behavior is clearer at the poles. |
||
* @exceptsafe Provides strong exception guarantee. | ||
* | ||
* @note For two points `A` and `B`, `A.bearingTo(B)` will in | ||
|
@@ -296,7 +294,8 @@ class SpherePoint final { | |
* @param axis a point defining the north pole of the rotation axis. | ||
* @param amount the amount of rotation, where positive values | ||
* represent right-handed rotations around `axis`. | ||
* @returns a new point created by rotating this point | ||
* @returns a new point created by rotating this point. If the new point is at the pole | ||
* then its longitude will be 0. | ||
* | ||
* @exceptsafe Shall not throw exceptions. | ||
*/ | ||
|
@@ -305,20 +304,16 @@ class SpherePoint final { | |
/** | ||
* Return a point offset from this one along a great circle. | ||
* | ||
* For any point `A` not at a coordinate pole, and any two angles `b` | ||
* and `delta`, `A.bearingTo(A.offset(b, delta))` = `b` and | ||
* `A.separationTo(A.offset(b, delta))` = `delta`. | ||
* For any point `A`, any angle `bearing` and any non-negative angle `amount`, | ||
* if `B` = A.offset(bearing, amount)` then `A.bearingTo(B) = amount` and `A.separationTo(B) = amount`. | ||
* Negative values of `amount` are supported in the obvious manner: | ||
* `A.offset(bearing, delta) = A.offset(bearing + 180*degrees, -delta)` | ||
* | ||
* @param bearing the direction in which to move this point, following | ||
* the conventions described in @ref bearingTo. | ||
* @param amount the distance by which to move along the great | ||
* circle defined by `bearing` | ||
* @returns a new point created by shifting this point | ||
* | ||
* @throws pex::exceptions::DomainError | ||
* Thrown if `this.atPole()`. | ||
* @throws pex::exceptions::InvalidParameterError | ||
* Thrown if `amount` is negative. | ||
* @param amount the distance by which to move along the great circle defined by `bearing` | ||
* @returns a new point created by rotating this point. If the new point is at the pole | ||
* then its longitude will be 0. | ||
* | ||
* @exceptsafe Provides strong exception guarantee. | ||
*/ | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -90,10 +90,14 @@ SpherePoint::SpherePoint(Point3D const& vector) { | |
double const y = vector.getY() / norm; | ||
double const z = vector.getZ() / norm; | ||
|
||
// Need to convert to Angle, Angle::wrap, and convert back to radians | ||
// to handle _longitude = -1e-16 without code duplication | ||
_longitude = (atan2(y, x) * radians).wrap().asRadians(); | ||
_latitude = asin(z); | ||
if (!atPole()) { | ||
// Need to convert to Angle, Angle::wrap, and convert back to radians | ||
// to handle _longitude = -1e-16 without code duplication | ||
_longitude = (atan2(y, x) * radians).wrap().asRadians(); | ||
} else { | ||
_longitude = 0; | ||
} | ||
} | ||
|
||
SpherePoint::SpherePoint(SpherePoint const& other) noexcept = default; | ||
|
@@ -124,22 +128,12 @@ bool SpherePoint::isFinite() const noexcept { return isfinite(_longitude) && isf | |
bool SpherePoint::operator==(SpherePoint const& other) const noexcept { | ||
// Deliberate override of Style Guide 5-12 | ||
// Approximate FP comparison would make object equality intransitive | ||
if (atPole()) { | ||
return _latitude == other._latitude; | ||
} else { | ||
return _longitude == other._longitude && _latitude == other._latitude; | ||
} | ||
return _longitude == other._longitude && _latitude == other._latitude; | ||
} | ||
|
||
bool SpherePoint::operator!=(SpherePoint const& other) const noexcept { return !(*this == other); } | ||
|
||
Angle SpherePoint::bearingTo(SpherePoint const& other) const { | ||
if (atPole()) { | ||
stringstream buffer; | ||
buffer << "Cannot calculate offset from pole " << *this << "."; | ||
throw pexExcept::DomainError(buffer.str()); | ||
} | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As I understand it, There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think the documentation is sufficiently clear, now that I have reworked it. |
||
Angle const deltaLon = other.getLongitude() - this->getLongitude(); | ||
|
||
double const sinDelta1 = sin(getLatitude().asRadians()); | ||
|
@@ -167,16 +161,6 @@ SpherePoint SpherePoint::rotated(SpherePoint const& axis, Angle const& amount) c | |
|
||
SpherePoint SpherePoint::offset(Angle const& bearing, Angle const& amount) const { | ||
double const phi = bearing.asRadians(); | ||
if (atPole()) { | ||
stringstream buffer; | ||
buffer << "Cannot define offset direction from pole " << *this << "."; | ||
throw pexExcept::DomainError(buffer.str()); | ||
} | ||
if (amount < 0.0) { | ||
stringstream buffer; | ||
buffer << "Negative offset of " << amount.asDegrees() << " degrees is not allowed."; | ||
throw pexExcept::InvalidParameterError(buffer.str()); | ||
} | ||
|
||
// let v = vector in the direction bearing points (tangent to surface of sphere) | ||
// To do the rotation, use rotate() method. | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this makes it pretty clear how the pole is handled. 👍