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