1
+ import GeoInterface as GI
2
+ import GeometryOps as GO
3
+ import LinearAlgebra
4
+ import LinearAlgebra: dot, cross
5
+
6
+ # # Get the area of a LinearRing with coordinates in radians
7
+ struct SphericalPoint{T <: Real }
8
+ data:: NTuple{3, T}
9
+ end
10
+ SphericalPoint (x, y, z) = SphericalPoint ((x, y, z))
11
+
12
+ # define the 4 basic mathematical operators elementwise on the data tuple
13
+ Base.:+ (p:: SphericalPoint , q:: SphericalPoint ) = SphericalPoint (p. data .+ q. data)
14
+ Base.:- (p:: SphericalPoint , q:: SphericalPoint ) = SphericalPoint (p. data .- q. data)
15
+ Base.:* (p:: SphericalPoint , q:: SphericalPoint ) = SphericalPoint (p. data .* q. data)
16
+ Base.:/ (p:: SphericalPoint , q:: SphericalPoint ) = SphericalPoint (p. data ./ q. data)
17
+ # Define sum on a SphericalPoint to sum across its data
18
+ Base. sum (p:: SphericalPoint ) = sum (p. data)
19
+
20
+ # define dot and cross products
21
+ LinearAlgebra. dot (p:: SphericalPoint , q:: SphericalPoint ) = sum (p * q)
22
+ function LinearAlgebra. cross (a:: SphericalPoint , b:: SphericalPoint )
23
+ a1, a2, a3 = a. data
24
+ b1, b2, b3 = b. data
25
+ SphericalPoint ((a2* b3- a3* b2, a3* b1- a1* b3, a1* b2- a2* b1))
26
+ end
27
+
28
+ # Using Eriksson's formula for the area of spherical triangles: https://www.jstor.org/stable/2691141
29
+ # This melts down only when two points are antipodal, which in our case will not happen.
30
+ function _unit_spherical_triangle_area (a, b, c)
31
+ # t = abs(dot(a, cross(b, c)))
32
+ # t /= 1 + dot(b,c) + dot(c, a) + dot(a, b)
33
+ t = abs (dot (a, (cross (b - a, c - a))) / dot (b + a, c + a))
34
+ 2 * atan (t)
35
+ end
36
+
37
+ _lonlat_to_sphericalpoint (p) = _lonlat_to_sphericalpoint (GI. x (p), GI. y (p))
38
+ function _lonlat_to_sphericalpoint (lon, lat)
39
+ lonsin, loncos = sincosd (lon)
40
+ latsin, latcos = sincosd (lat)
41
+ x = latcos * loncos
42
+ y = latcos * lonsin
43
+ z = latsin
44
+ return SphericalPoint (x,y,z)
45
+ end
46
+
47
+
48
+
49
+ # Extend area to spherical
50
+
51
+ # TODO : make this the other way around, but that can wait.
52
+ GO. area (m:: GO.Planar , geoms) = GO. area (geoms)
53
+
54
+ function GO. area (m:: GO.Spherical , geoms)
55
+ return GO. applyreduce (+ , GI. PolygonTrait (), geoms) do poly
56
+ GO. area (m, GI. PolygonTrait (), poly)
57
+ end * ((- m. radius^ 2 )/ 2 ) # do this after the sum, to increase accuracy and minimize calculations.
58
+ end
59
+
60
+ function GO. area (m:: GO.Spherical , :: GI.PolygonTrait , poly)
61
+ area = abs (_ring_area (m, GI. getexterior (poly)))
62
+ for interior in GI. gethole (poly)
63
+ area -= abs (_ring_area (m, interior))
64
+ end
65
+ return area
66
+ end
67
+
68
+ function _ring_area (m:: GO.Spherical , ring)
69
+ # convert ring to a sequence of SphericalPoints
70
+ points = _lonlat_to_sphericalpoint .(GI. getpoint (ring))[1 : end - 1 ] # deliberately drop the closing point
71
+ p1, p2, p3 = points[1 ], points[2 ], points[3 ]
72
+ # For a spherical polygon, we can compute the area by splitting it into triangles
73
+ # and summing their areas. We use the first point as a common vertex for all triangles.
74
+ area = 0.0
75
+ # Sum areas of triangles formed by first point and consecutive pairs of points
76
+ np = length (points)
77
+ for i in 1 : np
78
+ p1, p2, p3 = p2, p3, points[i]
79
+ area += _unit_spherical_triangle_area (p1, p2, p3)
80
+ end
81
+ return area
82
+ end
83
+
84
+ function _ring_area (m:: GO.Spherical , ring)
85
+ # Convert ring points to spherical coordinates
86
+ points = GO. tuples (ring). geom
87
+
88
+ # Remove last point if it's the same as first (closed ring)
89
+ if points[end ] == points[1 ]
90
+ points = points[1 : end - 1 ]
91
+ end
92
+
93
+ n = length (points)
94
+ if n < 3
95
+ return 0.0
96
+ end
97
+
98
+ area = 0.0
99
+
100
+ # Use L'Huilier's formula to sum the areas of spherical triangles
101
+ # formed by first point and consecutive pairs of points
102
+ for i in 1 : n
103
+ p1, p2, p3 = points[mod1 (i- 1 , n)], points[mod1 (i, n)], points[mod1 (i+ 1 , n)]
104
+ area += sind (GI. y (p2)) * (GI. x (p3) - GI. x (p1))
105
+ end
106
+
107
+ return area
108
+ end
109
+
110
+
111
+
112
+
113
+ # Test the area calculation
114
+ p1 = GI. Polygon ([GI. LinearRing (Point2f[(0 , 0 ), (1 , 0 ), (0 , 1 ), (0 , 0 )] .- (Point2f (0.5 , 0.5 ),))])
115
+
116
+ GO. area (GO. Spherical (), p1)
0 commit comments