Skip to content

Commit 6118f5e

Browse files
committed
experimental UnitSpherical module that can express geometry on unit sphere
1 parent 1f5fba2 commit 6118f5e

File tree

1 file changed

+214
-0
lines changed

1 file changed

+214
-0
lines changed
Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
# module UnitSpherical
2+
3+
using CoordinateTransformations
4+
using StaticArrays
5+
import GeoInterface as GI, GeoFormatTypes as GFT
6+
using LinearAlgebra
7+
8+
"""
9+
UnitSphericalPoint(v)
10+
11+
A unit spherical point, i.e., point living on the 2-sphere (𝕊²),
12+
represented as Cartesian coordinates in ℝ³.
13+
14+
This currently has no support for heights, only going from lat long to spherical
15+
and back again.
16+
"""
17+
struct UnitSphericalPoint{T} <: StaticArrays.StaticVector{3, T}
18+
data::SVector{3, T}
19+
UnitSphericalPoint{T}(v::SVector{3, T}) where T = new{T}(v)
20+
UnitSphericalPoint(x::T, y::T, z::T) where T = new{T}(SVector{3, T}((x, y, z)))
21+
end
22+
23+
24+
function UnitSphericalPoint(v::AbstractVector{T}) where T
25+
if length(v) == 3
26+
UnitSphericalPoint{T}(SVector(v[1], v[2], v[3]))
27+
elseif length(v) == 2
28+
UnitSphereFromGeographic()(v)
29+
else
30+
throw(ArgumentError("Passed a vector of length `$(length(v))` to `UnitSphericalPoint` constructor, which only accepts vectors of length 3 (assumed to be on the unit sphere) or 2 (assumed to be geographic lat/long)."))
31+
end
32+
end
33+
34+
UnitSphericalPoint(v) = UnitSphericalPoint(GI.trait(v), v)
35+
UnitSphericalPoint(::GI.PointTrait, v) = UnitSphereFromGeographic()(v) # since it works on any GI compatible point
36+
37+
# StaticArraysCore.jl interface implementation
38+
Base.Tuple(p::UnitSphericalPoint) = Base.Tuple(p.data)
39+
Base.@propagate_inbounds @inline Base.getindex(p::UnitSphericalPoint, i::Int64) = p.data[i]
40+
Base.setindex(p::UnitSphericalPoint, args...) = throw(ArgumentError("`setindex!` on a UnitSphericalPoint is not permitted as it is static."))
41+
@generated function StaticArrays.similar_type(::Type{SV}, ::Type{T},
42+
s::StaticArrays.Size{S}) where {SV <: UnitSphericalPoint,T,S}
43+
return if length(S) === 1
44+
UnitSphericalPoint{T}
45+
else
46+
StaticArrays.default_similar_type(T, s(), Val{length(S)})
47+
end
48+
end
49+
50+
# Base math implementation (really just forcing re-wrapping)
51+
Base.:(*)(a::UnitSphericalPoint, b::UnitSphericalPoint) = a .* b
52+
function Base.broadcasted(f, a::AbstractArray{T}, b::UnitSphericalPoint) where {T <: UnitSphericalPoint}
53+
return Base.broadcasted(f, a, (b,))
54+
end
55+
56+
# GeoInterface implementation: traits
57+
GI.trait(::UnitSphericalPoint) = GI.PointTrait()
58+
GI.geomtrait(::UnitSphericalPoint) = GI.PointTrait()
59+
# GeoInterface implementation: coordinate traits
60+
GI.is3d(::GI.PointTrait, ::UnitSphericalPoint) = true
61+
GI.ismeasured(::GI.PointTrait, ::UnitSphericalPoint) = false
62+
# GeoInterface implementation: accessors
63+
GI.ncoord(::GI.PointTrait, ::UnitSphericalPoint) = 3
64+
GI.getcoord(::GI.PointTrait, p::UnitSphericalPoint) = p[i]
65+
# GeoInterface implementation: metadata (CRS, extent, etc)
66+
GI.crs(::UnitSphericalPoint) = GFT.ProjString("+proj=cart +R=1 +type=crs") # TODO: make this a full WKT definition
67+
68+
# several useful LinearAlgebra functions, forwarded to the static arrays
69+
LinearAlgebra.cross(p1::UnitSphericalPoint, p2::UnitSphericalPoint) = UnitSphericalPoint(LinearAlgebra.cross(p1.data, p2.data))
70+
LinearAlgebra.dot(p1::UnitSphericalPoint, p2::UnitSphericalPoint) = LinearAlgebra.dot(p1.data, p2.data)
71+
LinearAlgebra.normalize(p1::UnitSphericalPoint) = UnitSphericalPoint(LinearAlgebra.normalize(p1.data))
72+
73+
# Spherical cap implementation
74+
struct SphericalCap{T}
75+
point::UnitSphericalPoint{T}
76+
radius::T
77+
end
78+
79+
SphericalCap(point::UnitSphericalPoint{T}, radius::Number) where T = SphericalCap{T}(point, convert(T, radius))
80+
SphericalCap(point, radius::Number) = SphericalCap(GI.trait(point), point, radius)
81+
function SphericalCap(::GI.PointTrait, point, radius::Number)
82+
return SphericalCap(UnitSphereFromGeographic()(point), radius)
83+
end
84+
85+
SphericalCap(geom) = SphericalCap(GI.trait(geom), geom)
86+
SphericalCap(t::GI.PointTrait, geom) = SphericalCap(t, geom, 0)
87+
# TODO: add implementations for line string and polygon traits
88+
# TODO: add implementations to merge two spherical caps
89+
# TODO: add implementations for multitraits based on this
90+
91+
# TODO: this returns an approximately antipodal point...
92+
93+
spherical_distance(x::UnitSphericalPoint, y::UnitSphericalPoint) = acos(clamp(x y, -1.0, 1.0))
94+
95+
# TODO: exact-predicate intersection
96+
# This is all inexact and thus subject to floating point error
97+
function _intersects(x::SphericalCap, y::SphericalCap)
98+
spherical_distance(x.point, y.point) <= max(x.radius, y.radius)
99+
end
100+
101+
_disjoint(x::SphericalCap, y::SphericalCap) = !_intersects(x, y)
102+
103+
function _contains(big::SphericalCap, small::SphericalCap)
104+
dist = spherical_distance(big.point, small.point)
105+
return dist < big.radius #=small.point in big=# && dist + small.radius < big.radius
106+
end
107+
108+
function slerp(a::UnitSphericalPoint, b::UnitSphericalPoint, i01::Number)
109+
Ω = spherical_distance(a, b)
110+
sinΩ = sin(Ω)
111+
return (sin((1-i01)*Ω) / sinΩ) * a + (sin(i01*Ω)/sinΩ) * b
112+
end
113+
114+
function slerp(a::UnitSphericalPoint, b::UnitSphericalPoint, i01s::AbstractVector{<: Number})
115+
Ω = spherical_distance(a, b)
116+
sinΩ = sin(Ω)
117+
return (sin((1-i01)*Ω) / sinΩ) .* a .+ (sin(i01*Ω)/sinΩ) .* b
118+
end
119+
120+
121+
122+
123+
function circumcenter_on_unit_sphere(a::UnitSphericalPoint, b::UnitSphericalPoint, c::UnitSphericalPoint)
124+
LinearAlgebra.normalize(a × b + b × c + c × a)
125+
end
126+
127+
"Get the circumcenter of the triangle (a, b, c) on the unit sphere. Returns a normalized 3-vector."
128+
function SphericalCap(a::UnitSphericalPoint, b::UnitSphericalPoint, c::UnitSphericalPoint)
129+
circumcenter = circumcenter_on_unit_sphere(a, b, c)
130+
circumradius = spherical_distance(a, circumcenter)
131+
return SphericalCap(circumcenter, circumradius)
132+
end
133+
134+
function _is_ccw_unit_sphere(v_0,v_c,v_i)
135+
# checks if the smaller interior angle for the great circles connecting u-v and v-w is CCW
136+
return(LinearAlgebra.dot(LinearAlgebra.cross(v_c - v_0,v_i - v_c), v_i) < 0)
137+
end
138+
139+
function angle_between(a, b, c)
140+
ab = b - a
141+
bc = c - b
142+
norm_dot = (ab bc) / (LinearAlgebra.norm(ab) * LinearAlgebra.norm(bc))
143+
angle = acos(clamp(norm_dot, -1.0, 1.0))
144+
if _is_ccw_unit_sphere(a, b, c)
145+
return angle
146+
else
147+
return 2π - angle
148+
end
149+
end
150+
151+
152+
# Coordinate transformations from lat/long to geographic and back
153+
struct UnitSphereFromGeographic <: CoordinateTransformations.Transformation
154+
end
155+
156+
function (::UnitSphereFromGeographic)(geographic_point)
157+
# Asssume that geographic_point is GeoInterface compatible
158+
# Longitude is directly translatable to a spherical coordinate
159+
# θ (azimuth)
160+
θ = GI.x(geographic_point)
161+
# The polar angle is 90 degrees minus the latitude
162+
# ϕ (polar angle)
163+
ϕ = 90 - GI.y(geographic_point)
164+
# Since this is the unit sphere, the radius is assumed to be 1,
165+
# and we don't need to multiply by it.
166+
sinϕ, cosϕ = sincosd(ϕ)
167+
sinθ, cosθ = sincosd(θ)
168+
169+
return UnitSphericalPoint(
170+
sinϕ * cosθ,
171+
sinϕ * sinθ,
172+
cosϕ
173+
)
174+
end
175+
176+
struct GeographicFromUnitSphere <: CoordinateTransformations.Transformation
177+
end
178+
179+
function (::GeographicFromUnitSphere)(xyz::AbstractVector)
180+
@assert length(xyz) == 3 "GeographicFromUnitCartesian expects a 3D Cartesian vector"
181+
x, y, z = xyz.data
182+
return (
183+
atan(y, x) |> rad2deg,
184+
90 - (atan(hypot(x, y), z) |> rad2deg),
185+
)
186+
end
187+
188+
function randsphericalangles(n)
189+
θ = 2π .* rand(n)
190+
ϕ = acos.(2 .* rand(n) .- 1)
191+
return tuple.(θ, ϕ)
192+
end
193+
194+
"""
195+
randsphere(n)
196+
197+
Return `n` random [`UnitSphericalPoint`](@ref)s spanning the whole sphere 𝕊².
198+
"""
199+
function randsphere(n)
200+
θϕs = randsphericalangles(n)
201+
return map(θϕs) do θϕ
202+
θ, ϕ = θϕ
203+
sinθ, cosθ = sincos(θ)
204+
sinϕ, cosϕ = sincos(ϕ)
205+
UnitSphericalPoint(
206+
sinϕ * cosθ,
207+
sinϕ * sinθ,
208+
cosϕ
209+
)
210+
end
211+
end
212+
213+
214+
# end

0 commit comments

Comments
 (0)