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