Skip to content

Commit 879c812

Browse files
authored
experimental UnitSpherical module that can express geometry on unit sphere (#285)
2 parents c9c7904 + e94ad07 commit 879c812

File tree

17 files changed

+1918
-7
lines changed

17 files changed

+1918
-7
lines changed

Project.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ GeoFormatTypes = "68eda718-8dee-11e9-39e7-89f7f65f511f"
1515
GeoInterface = "cf35fbd7-0cd7-5166-be24-54bfbe79505f"
1616
GeometryOpsCore = "05efe853-fabf-41c8-927e-7063c8b9f013"
1717
LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
18+
Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
1819
SortTileRecursiveTree = "746ee33f-1797-42c2-866d-db2fce69d14d"
1920
StaticArrays = "90137ffa-7385-5640-81b9-e52037218182"
2021
Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2"
@@ -51,7 +52,8 @@ GeometryOpsCore = "=0.1.7"
5152
LibGEOS = "0.9.2"
5253
LinearAlgebra = "1"
5354
Proj = "1"
54-
SortTileRecursiveTree = "0.1"
55+
Random = "1"
56+
SortTileRecursiveTree = "0.1.2"
5557
StaticArrays = "1"
5658
Statistics = "1"
5759
TGGeometry = "0.1"

docs/make.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ CairoMakie.activate!(px_per_unit = 2, type = "svg", inline = true) # TODO: make
77
# import packages that activate extensions
88
import FlexiJoins, LibGEOS, Proj, TGGeometry
99

10-
DocMeta.setdocmeta!(GeometryOps, :DocTestSetup, :(using GeometryOps; using GeometryBasics); recursive=true)
10+
DocMeta.setdocmeta!(GeometryOps, :DocTestSetup, :(using GeometryOps; using GeometryBasics; using GeometryOps.GeometryOpsCore); recursive=true)
1111

1212
using GeoInterfaceMakie
1313

src/GeometryOps.jl

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,10 @@ include("not_implemented_yet.jl")
4444
include("utils/utils.jl")
4545
include("utils/LoopStateMachine/LoopStateMachine.jl")
4646
include("utils/SpatialTreeInterface/SpatialTreeInterface.jl")
47+
include("utils/UnitSpherical/UnitSpherical.jl")
4748

48-
using .LoopStateMachine, .SpatialTreeInterface
49+
# Load utility modules in
50+
using .LoopStateMachine, .SpatialTreeInterface, .UnitSpherical
4951

5052
include("utils/NaturalIndexing.jl")
5153
using .NaturalIndexing

src/methods/clipping/clipping_processor.jl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,7 @@ checks in the inner loop.
213213
"""
214214
function foreach_pair_of_maybe_intersecting_edges_in_order(
215215
manifold::M, accelerator::AutoAccelerator, f_on_each_a::FA, f_after_each_a::FAAfter, f_on_each_maybe_intersect::FI, poly_a, poly_b, _t::Type{T} = Float64
216-
) where {FA, FAAfter, FI, T, M <: Manifold, A <: IntersectionAccelerator}
216+
) where {FA, FAAfter, FI, T, M <: Manifold}
217217
# this is suitable for planar
218218
# but spherical / geodesic will need s2 support at some point,
219219
# or -- even now -- just buffering
@@ -247,7 +247,7 @@ function foreach_pair_of_maybe_intersecting_edges_in_order(
247247
# First, loop over "each edge" in poly_a
248248
for (i, (a1t, a2t)) in enumerate(eachedge(poly_a, T))
249249
a1t == a2t && continue
250-
isnothing(f_on_each_a) ||f_on_each_a(a1t, i)
250+
isnothing(f_on_each_a) || f_on_each_a(a1t, i)
251251
for (j, (b1t, b2t)) in enumerate(eachedge(poly_b, T))
252252
b1t == b2t && continue
253253
LoopStateMachine.@controlflow f_on_each_maybe_intersect(((a1t, a2t), i), ((b1t, b2t), j)) # this should be aware of manifold by construction.

src/methods/clipping/predicates.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ module Predicates
1212
Return 0 if c is on (a, b) or if a == b. =#
1313
orient(a, b, c; exact) = _orient(booltype(exact), _tuple_point(a, Float64), _tuple_point(b, Float64), _tuple_point(c, Float64))
1414

15-
# If `exact` is `true`, use `ExactPredicates` to calculate the orientation.
15+
# If `exact` is `true`, use `AdaptivePredicates` to calculate the orientation.
1616
_orient(::True, a, b, c) = AdaptivePredicates.orient2p(_tuple_point(a, Float64), _tuple_point(b, Float64), _tuple_point(c, Float64))
1717
# _orient(::True, a, b, c) = ExactPredicates.orient(_tuple_point(a, Float64), _tuple_point(b, Float64), _tuple_point(c, Float64))
1818
# If `exact` is `false`, calculate the orientation without using `ExactPredicates`.
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
module UnitSpherical
2+
3+
using CoordinateTransformations
4+
using StaticArrays, LinearAlgebra
5+
import GeoInterface as GI, GeoFormatTypes as GFT
6+
7+
import Random
8+
9+
# using TestItems # this is a thin package that allows TestItems.@testitem to be parsed.
10+
11+
include("point.jl")
12+
13+
include("robustcrossproduct/RobustCrossProduct.jl")
14+
# Re-export from RobustCrossProduct
15+
using .RobustCrossProduct: robust_cross_product
16+
export robust_cross_product
17+
18+
include("coordinate_transforms.jl")
19+
include("slerp.jl")
20+
include("cap.jl")
21+
22+
export UnitSphericalPoint, UnitSphereFromGeographic, GeographicFromUnitSphere,
23+
slerp, SphericalCap, spherical_distance
24+
25+
end

src/utils/UnitSpherical/cap.jl

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
# # Spherical Caps
2+
3+
#=
4+
```@meta
5+
CollapsedDocStrings = true
6+
```
7+
8+
```@docs; canonical=false
9+
SphericalCap
10+
circumcenter_on_unit_sphere
11+
```
12+
13+
## What is SphericalCap?
14+
15+
A spherical cap represents a section of a unit sphere about some point, bounded by a radius.
16+
It is defined by a center point on the unit sphere and a radius (in radians).
17+
18+
Spherical caps are used in:
19+
- Representing circular regions on a spherical surface
20+
- Approximating and bounding spherical geometries
21+
- Spatial indexing and filtering on the unit sphere
22+
- Implementing containment, intersection, and disjoint predicates
23+
24+
The `SphericalCap` type offers multiple constructors to create caps from:
25+
- UnitSphericalPoint and radius
26+
- Geographic coordinates and radius
27+
- Three points on the unit sphere (circumcircle)
28+
29+
## Examples
30+
31+
```@example sphericalcap
32+
using GeometryOps
33+
using GeoInterface
34+
35+
# Create a spherical cap from a point and radius
36+
point = UnitSphericalPoint(1.0, 0.0, 0.0) # Point on the unit sphere
37+
cap = SphericalCap(point, 0.5) # Cap with radius 0.5 radians
38+
```
39+
40+
```@example sphericalcap
41+
# Create a spherical cap from geographic coordinates
42+
lat, lon = 40.0, -74.0 # New York City (approximate)
43+
point = GeoInterface.Point(lon, lat)
44+
cap = SphericalCap(point, 0.1) # Cap with radius ~0.1 radians
45+
```
46+
47+
```@example sphericalcap
48+
# Create a spherical cap from three points (circumcircle)
49+
p1 = UnitSphericalPoint(1.0, 0.0, 0.0)
50+
p2 = UnitSphericalPoint(0.0, 1.0, 0.0)
51+
p3 = UnitSphericalPoint(0.0, 0.0, 1.0)
52+
cap = SphericalCap(p1, p2, p3)
53+
```
54+
55+
=#
56+
57+
# Spherical cap implementation
58+
struct SphericalCap{T}
59+
point::UnitSphericalPoint{T}
60+
radius::T
61+
# cartesian_radius::T # TODO: compute using cosine(radius)
62+
end
63+
64+
SphericalCap(point::UnitSphericalPoint{T}, radius::Number) where T = SphericalCap{T}(point, convert(T, radius))
65+
SphericalCap(point, radius::Number) = SphericalCap(GI.trait(point), point, radius)
66+
67+
SphericalCap(geom) = SphericalCap(GI.trait(geom), geom)
68+
SphericalCap(t::GI.AbstractGeometryTrait, geom) = SphericalCap(t, geom, 0)
69+
70+
function SphericalCap(::GI.PointTrait, point, radius::Number)
71+
return SphericalCap(UnitSphereFromGeographic()(point), radius)
72+
end
73+
# TODO: add implementations for line string and polygon traits
74+
# That will require a minimum bounding circle implementation.
75+
# TODO: add implementations for multitraits based on this
76+
77+
# TODO: this returns an approximately antipodal point...
78+
79+
# TODO: exact-predicate intersection
80+
# This is all inexact and thus subject to floating point error
81+
function _intersects(x::SphericalCap, y::SphericalCap)
82+
spherical_distance(x.point, y.point) <= x.radius + y.radius
83+
end
84+
85+
_disjoint(x::SphericalCap, y::SphericalCap) = !_intersects(x, y)
86+
87+
function _contains(big::SphericalCap, small::SphericalCap)
88+
dist = spherical_distance(big.point, small.point)
89+
# small circle fits in big circle
90+
return dist + small.radius < big.radius
91+
end
92+
function _contains(cap::SphericalCap, point::UnitSphericalPoint)
93+
spherical_distance(cap.point, point) <= cap.radius
94+
end
95+
96+
#Comment by asinghvi: this could be transformed to GO.union
97+
function _merge(x::SphericalCap, y::SphericalCap)
98+
99+
d = spherical_distance(x.point, y.point)
100+
newradius = (x.radius + y.radius + d) / 2
101+
if newradius < x.radius
102+
#x contains y
103+
x
104+
elseif newradius < y.radius
105+
#y contains x
106+
y
107+
else
108+
excenter = 0.5 * (1 - (x.radius - y.radius) / d)
109+
newcenter = slerp(x.point, y.point, excenter)
110+
SphericalCap(newcenter, newradius)
111+
end
112+
end
113+
114+
function circumcenter_on_unit_sphere(a::UnitSphericalPoint, b::UnitSphericalPoint, c::UnitSphericalPoint)
115+
LinearAlgebra.normalize(
116+
LinearAlgebra.cross(a, b) +
117+
LinearAlgebra.cross(b, c) +
118+
LinearAlgebra.cross(c, a)
119+
)
120+
end
121+
122+
"Get the circumcenter of the triangle (a, b, c) on the unit sphere. Returns a normalized 3-vector."
123+
function SphericalCap(a::UnitSphericalPoint, b::UnitSphericalPoint, c::UnitSphericalPoint)
124+
circumcenter = circumcenter_on_unit_sphere(a, b, c)
125+
circumradius = spherical_distance(a, circumcenter)
126+
return SphericalCap(circumcenter, circumradius)
127+
end
128+
129+
function _is_ccw_unit_sphere(v_0::S, v_c::S, v_i::S) where S <: UnitSphericalPoint
130+
# checks if the smaller interior angle for the great circles connecting u-v and v-w is CCW
131+
return(LinearAlgebra.dot(LinearAlgebra.cross(v_c - v_0, v_i - v_c), v_i) < 0)
132+
end
133+
134+
function angle_between(a::S, b::S, c::S) where S <: UnitSphericalPoint
135+
ab = b - a
136+
bc = c - b
137+
norm_dot = (ab bc) / (LinearAlgebra.norm(ab) * LinearAlgebra.norm(bc))
138+
angle = acos(clamp(norm_dot, -1.0, 1.0))
139+
if _is_ccw_unit_sphere(a, b, c)
140+
return angle
141+
else
142+
return 2π - angle
143+
end
144+
end
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
#=
2+
# Coordinate transformations
3+
=#
4+
# Coordinate transformations from lat/long to geographic and back
5+
"""
6+
UnitSphereFromGeographic()
7+
8+
A transformation that converts a geographic point (latitude, longitude) to a
9+
[`UnitSphericalPoint`] in ℝ³.
10+
11+
Accepts any [GeoInterface-compatible](https://github.com/JuliaGeo/GeoInterface.jl) point.
12+
13+
## Examples
14+
15+
```jldoctest
16+
julia> import GeoInterface as GI; using GeometryOps.UnitSpherical
17+
18+
julia> UnitSphereFromGeographic()(GI.Point(45, 45))
19+
3-element UnitSphericalPoint{Float64} with indices SOneTo(3):
20+
0.5000000000000001
21+
0.5000000000000001
22+
0.7071067811865476
23+
```
24+
25+
```jldoctest
26+
julia> using GeometryOps.UnitSpherical
27+
28+
julia> UnitSphereFromGeographic()((45, 45))
29+
3-element UnitSphericalPoint{Float64} with indices SOneTo(3):
30+
0.5000000000000001
31+
0.5000000000000001
32+
0.7071067811865476
33+
```
34+
"""
35+
struct UnitSphereFromGeographic <: CoordinateTransformations.Transformation
36+
end
37+
38+
function (::UnitSphereFromGeographic)(geographic_point)
39+
# Asssume that geographic_point is GeoInterface compatible
40+
# Longitude is directly translatable to a spherical coordinate
41+
# θ (azimuth)
42+
θ = GI.x(geographic_point)
43+
# The polar angle is 90 degrees minus the latitude
44+
# ϕ (polar angle)
45+
ϕ = 90 - GI.y(geographic_point)
46+
# Since this is the unit sphere, the radius is assumed to be 1,
47+
# and we don't need to multiply by it.
48+
sinϕ, cosϕ = sincosd(ϕ)
49+
sinθ, cosθ = sincosd(θ)
50+
51+
return UnitSphericalPoint(
52+
sinϕ * cosθ,
53+
sinϕ * sinθ,
54+
cosϕ
55+
)
56+
end
57+
58+
"""
59+
GeographicFromUnitSphere()
60+
61+
A transformation that converts a [`UnitSphericalPoint`](@ref) in ℝ³ to a
62+
2-tuple geographic point (longitude, latitude), in degrees.
63+
64+
Accepts any 3-element vector, but the input is assumed to be on the unit sphere.
65+
66+
## Examples
67+
68+
```jldoctest
69+
julia> using GeometryOps.UnitSpherical
70+
71+
julia> GeographicFromUnitSphere()(UnitSphericalPoint(0.5, 0.5, 1/√(2)))
72+
(45.0, 44.99999999999999)
73+
```
74+
(the inaccuracy is due to the precision of the `atan` function)
75+
76+
"""
77+
struct GeographicFromUnitSphere <: CoordinateTransformations.Transformation
78+
end
79+
80+
function (::GeographicFromUnitSphere)(xyz::AbstractVector)
81+
@assert length(xyz) == 3 "GeographicFromUnitSphere expects a 3D Cartesian vector"
82+
x, y, z = xyz
83+
return (
84+
atand(y, x),
85+
asind(z),
86+
)
87+
end
88+
89+
90+
Base.inv(::UnitSphereFromGeographic) = GeographicFromUnitSphere()
91+
Base.inv(::GeographicFromUnitSphere) = UnitSphereFromGeographic()

0 commit comments

Comments
 (0)