1
+ #=
2
+ # Antimeridian Cutting
3
+ =#
4
+ export CutAtAntimeridianAndPoles # TODO : too wordy?
5
+
6
+ #=
7
+ This correction cuts the geometry at the antimeridian and the poles, and returns a fixed geometry.
8
+
9
+ The implementation is translated from the implementation in https://github.com/gadomski/antimeridian,
10
+ which is a Python package. Several ports of that algorithm exist in e.g. Go, Rust, etc.
11
+
12
+ At some point we will have to go in and clean up the implementation, remove all the hardcoded code,
13
+ and make it more efficient by using raw geointerface, and not allocating so much (perhaps, by passing allocs around).
14
+
15
+ But for right now it works, that's all I need.
16
+ =#
17
+
18
+ """
19
+ CutAtAntimeridianAndPoles(; kwargs...) <: GeometryCorrection
20
+
21
+ This correction cuts the geometry at the antimeridian and the poles, and returns a fixed geometry.
22
+ """
23
+ Base. @kwdef struct CutAtAntimeridianAndPoles <: GeometryCorrection
24
+ " The value at the north pole, in your angular units"
25
+ northpole:: Float64 = 90.0 # TODO not used!!!
26
+ " The value at the south pole, in your angular units"
27
+ southpole:: Float64 = - 90.0 # TODO not used!!!
28
+ " The value at the left edge of the antimeridian, in your angular units"
29
+ left:: Float64 = - 180.0 # TODO not used!!!
30
+ " The value at the right edge of the antimeridian, in your angular units"
31
+ right:: Float64 = 180.0 # TODO not used!!!
32
+ " The period of the cyclic / cylindrical coordinate system's x value, usually computed automatically so you don't have to provide it."
33
+ period:: Float64 = right - left # TODO not used!!!
34
+ " If the polygon is known to enclose the north pole, set this to true"
35
+ force_north_pole:: Bool = false # TODO not used!!!
36
+ " If the polygon is known to enclose the south pole, set this to true"
37
+ force_south_pole:: Bool = false # TODO not used!!!
38
+ " If true, use the great circle method to find the antimeridian crossing, otherwise use the flat projection method. There is no reason to set this to be off."
39
+ great_circle = true
40
+ end
41
+
42
+ function Base. show (io:: IO , cut:: CutAtAntimeridian )
43
+ print (io, " CutAtAntimeridian(left=$(cut. left) , right=$(cut. right) )" )
44
+ end
45
+ Base. show (io:: IO , :: MIME"text/plain" , cut:: CutAtAntimeridian ) = Base. show (io, cut)
46
+
47
+ application_level (:: CutAtAntimeridian ) = TraitTarget (GI. PolygonTrait (), GI. LineStringTrait (), GI. MultiLineStringTrait (), GI. MultiPolygonTrait ())
48
+
49
+ function (c:: CutAtAntimeridianAndPoles )(trait:: GI.AbstractTrait , geom)
50
+ return _AntimeridianHelpers. cut_at_antimeridian (trait, geom)
51
+ end
52
+
53
+ module _AntimeridianHelpers
54
+
55
+ import GeoInterface as GI
56
+ import .. GeometryOps as GO
57
+
58
+ # Custom cross product for 3D tuples
59
+ function _cross (a:: Tuple{Float64,Float64,Float64} , b:: Tuple{Float64,Float64,Float64} )
60
+ return (
61
+ a[2 ]* b[3 ] - a[3 ]* b[2 ],
62
+ a[3 ]* b[1 ] - a[1 ]* b[3 ],
63
+ a[1 ]* b[2 ] - a[2 ]* b[1 ]
64
+ )
65
+ end
66
+
67
+ # Convert spherical degrees to cartesian coordinates
68
+ function spherical_degrees_to_cartesian (point:: Tuple{Float64,Float64} ):: Tuple{Float64,Float64,Float64}
69
+ lon, lat = point
70
+ slon, clon = sincosd (lon)
71
+ slat, clat = sincosd (lat)
72
+ return (
73
+ clon * clat,
74
+ slon * clat,
75
+ slat
76
+ )
77
+ end
78
+
79
+ # Calculate crossing latitude using great circle method
80
+ function crossing_latitude_great_circle (start:: Tuple{Float64,Float64} , endpoint:: Tuple{Float64,Float64} ):: Float64
81
+ # Convert points to 3D vectors
82
+ p1 = spherical_degrees_to_cartesian (start)
83
+ p2 = spherical_degrees_to_cartesian (endpoint)
84
+
85
+ # Cross product defines plane through both points
86
+ n1 = _cross (p1, p2)
87
+
88
+ # Unit vector -Y defines meridian plane
89
+ n2 = (0.0 , - 1.0 , 0.0 )
90
+
91
+ # Intersection of planes defined by cross product
92
+ intersection = _cross (n1, n2)
93
+ norm = sqrt (sum (intersection .^ 2 ))
94
+ intersection = intersection ./ norm
95
+
96
+ # Convert back to spherical coordinates (just need latitude)
97
+ round (asind (intersection[3 ]), digits= 7 )
98
+ end
99
+
100
+ # Calculate crossing latitude using flat projection method
101
+ function crossing_latitude_flat (start:: Tuple{Float64,Float64} , endpoint:: Tuple{Float64,Float64} ):: Float64
102
+ lat_delta = endpoint[2 ] - start[2 ]
103
+ if endpoint[1 ] > 0
104
+ round (
105
+ start[2 ] + (180.0 - start[1 ]) * lat_delta / (endpoint[1 ] + 360.0 - start[1 ]),
106
+ digits= 7
107
+ )
108
+ else
109
+ round (
110
+ start[2 ] + (start[1 ] + 180.0 ) * lat_delta / (start[1 ] + 360.0 - endpoint[1 ]),
111
+ digits= 7
112
+ )
113
+ end
114
+ end
115
+
116
+ # Main crossing latitude calculation function
117
+ function crossing_latitude (start:: Tuple{Float64,Float64} , endpoint:: Tuple{Float64,Float64} , great_circle:: Bool ):: Float64
118
+ abs (start[1 ]) == 180 && return start[2 ]
119
+ abs (endpoint[1 ]) == 180 && return endpoint[2 ]
120
+
121
+ return great_circle ? crossing_latitude_great_circle (start, endpoint) : crossing_latitude_flat (start, endpoint)
122
+ end
123
+
124
+ # Normalize coordinates to ensure longitudes are between -180 and 180
125
+ function normalize_coords (coords:: Vector{Tuple{Float64,Float64}} ):: Vector{Tuple{Float64,Float64}}
126
+ normalized = deepcopy (coords)
127
+ all_on_antimeridian = true
128
+
129
+ for i in eachindex (normalized)
130
+ point = normalized[i]
131
+ prev_point = normalized[mod1 (i- 1 , length (normalized))]
132
+
133
+ if isapprox (point[1 ], 180 )
134
+ if abs (point[2 ]) != 90 && isapprox (prev_point[1 ], - 180 )
135
+ normalized[i] = (- 180.0 , point[2 ])
136
+ else
137
+ normalized[i] = (180.0 , point[2 ])
138
+ end
139
+ elseif isapprox (point[1 ], - 180 )
140
+ if abs (point[2 ]) != 90 && isapprox (prev_point[1 ], 180 )
141
+ normalized[i] = (180.0 , point[2 ])
142
+ else
143
+ normalized[i] = (- 180.0 , point[2 ])
144
+ end
145
+ else
146
+ normalized[i] = (((point[1 ] + 180 ) % 360 ) - 180 , point[2 ])
147
+ all_on_antimeridian = false
148
+ end
149
+ end
150
+
151
+ return all_on_antimeridian ? coords : normalized
152
+ end
153
+
154
+ # Segment a ring of coordinates at antimeridian crossings
155
+ function segment_ring (coords:: Vector{Tuple{Float64,Float64}} , great_circle:: Bool ):: Vector{Vector{Tuple{Float64,Float64}}}
156
+ segments = Vector {Vector{Tuple{Float64,Float64}}} ()
157
+ current_segment = Tuple{Float64,Float64}[]
158
+
159
+ for i in 1 : length (coords)- 1
160
+ start = coords[i]
161
+ endpoint = coords[i+ 1 ]
162
+ push! (current_segment, start)
163
+
164
+ # Check for antimeridian crossing
165
+ if (endpoint[1 ] - start[1 ] > 180 ) && (endpoint[1 ] - start[1 ] != 360 ) # left crossing
166
+ lat = crossing_latitude (start, endpoint, great_circle)
167
+ push! (current_segment, (- 180.0 , lat))
168
+ push! (segments, current_segment)
169
+ current_segment = [(180.0 , lat)]
170
+ elseif (start[1 ] - endpoint[1 ] > 180 ) && (start[1 ] - endpoint[1 ] != 360 ) # right crossing
171
+ lat = crossing_latitude (endpoint, start, great_circle)
172
+ push! (current_segment, (180.0 , lat))
173
+ push! (segments, current_segment)
174
+ current_segment = [(- 180.0 , lat)]
175
+ end
176
+ end
177
+
178
+ # Handle last point and segment
179
+ if isempty (segments)
180
+ return Vector {Vector{Tuple{Float64,Float64}}} () # No crossings
181
+ elseif coords[end ] == segments[1 ][1 ]
182
+ # Join polygons
183
+ segments[1 ] = vcat (current_segment, segments[1 ])
184
+ else
185
+ push! (current_segment, coords[end ])
186
+ push! (segments, current_segment)
187
+ end
188
+
189
+ return normalize_coords .(segments)
190
+ end
191
+
192
+ # Check if a segment is self-closing
193
+ function is_self_closing (segment:: Vector{Tuple{Float64,Float64}} ):: Bool
194
+ is_right = segment[end ][1 ] == 180
195
+ return segment[1 ][1 ] == segment[end ][1 ] && (
196
+ (is_right && segment[1 ][2 ] > segment[end ][2 ]) ||
197
+ (! is_right && segment[1 ][2 ] < segment[end ][2 ])
198
+ )
199
+ end
200
+
201
+ # Build polygons from segments
202
+ function build_polygons (segments:: Vector{Vector{Tuple{Float64,Float64}}} ):: Vector{GI.Polygon}
203
+ isempty (segments) && return GI. Polygon[]
204
+
205
+ segment = pop! (segments)
206
+ is_right = segment[end ][1 ] == 180
207
+ candidates = Tuple{Union{Nothing,Int},Float64}[]
208
+
209
+ if is_self_closing (segment)
210
+ push! (candidates, (nothing , segment[1 ][2 ]))
211
+ end
212
+
213
+ for (i, s) in enumerate (segments)
214
+ if s[1 ][1 ] == segment[end ][1 ]
215
+ if (is_right && s[1 ][2 ] > segment[end ][2 ] &&
216
+ (! is_self_closing (s) || s[end ][2 ] < segment[1 ][2 ])) ||
217
+ (! is_right && s[1 ][2 ] < segment[end ][2 ] &&
218
+ (! is_self_closing (s) || s[end ][2 ] > segment[1 ][2 ]))
219
+ push! (candidates, (i, s[1 ][2 ]))
220
+ end
221
+ end
222
+ end
223
+
224
+ # Sort candidates
225
+ sort! (candidates, by= x-> x[2 ], rev= ! is_right)
226
+
227
+ index = isempty (candidates) ? nothing : candidates[1 ][1 ]
228
+
229
+ if ! isnothing (index)
230
+ # Join segments and recurse
231
+ segment = vcat (segment, segments[index])
232
+ segments[index] = segment
233
+ return build_polygons (segments)
234
+ else
235
+ # Handle self-joining segment
236
+ polygons = build_polygons (segments)
237
+ if ! all (p == segment[1 ] for p in segment)
238
+ push! (polygons, GI. Polygon ([segment]))
239
+ end
240
+ return polygons
241
+ end
242
+ end
243
+
244
+ # Main function to cut a polygon at the antimeridian
245
+ cut_at_antimeridian (x) = cut_at_antimeridian (GI. trait (x), x)
246
+
247
+ function cut_at_antimeridian (
248
+ :: GI.PolygonTrait ,
249
+ polygon:: T ;
250
+ force_north_pole:: Bool = false ,
251
+ force_south_pole:: Bool = false ,
252
+ fix_winding:: Bool = true ,
253
+ great_circle:: Bool = true
254
+ ) where {T}
255
+ # Get exterior ring
256
+ exterior = GO. tuples (GI. getexterior (polygon)). geom
257
+ exterior = normalize_coords (exterior)
258
+
259
+ # Segment the exterior ring
260
+ segments = segment_ring (exterior, great_circle)
261
+
262
+ if isempty (segments)
263
+ # No antimeridian crossing
264
+ if fix_winding && GO. isclockwise (GI. LinearRing (exterior))
265
+ coord_vecs = reverse .(getproperty .(GO. tuples .(GI. getring (polygon)), :geom ))
266
+ return GI. Polygon (normalize_coords .(coord_vecs))
267
+ end
268
+ return polygon
269
+ end
270
+
271
+ # Handle holes
272
+ holes = Vector {Vector{Tuple{Float64,Float64}}} ()
273
+ for hole_idx in 1 : GI. nhole (polygon)
274
+ hole = GO. tuples (GI. gethole (polygon, hole_idx)). geom
275
+ hole_segments = segment_ring (hole, great_circle)
276
+
277
+ if ! isempty (hole_segments)
278
+ if fix_winding
279
+ unwrapped = [(x % 360 , y) for (x, y) in hole]
280
+ if ! GO. isclockwise (GI. LineString (unwrapped))
281
+ hole_segments = segment_ring (reverse (hole), great_circle)
282
+ end
283
+ end
284
+ append! (segments, hole_segments)
285
+ else
286
+ push! (holes, hole)
287
+ end
288
+ end
289
+
290
+ # Build final polygons
291
+ result_polygons = build_polygons (segments)
292
+
293
+ # Add holes to appropriate polygons
294
+ for hole in holes
295
+ for (i, poly) in enumerate (result_polygons)
296
+ if GO. contains (poly, GI. Point (hole[1 ]))
297
+ rings = GI. getring (poly)
298
+ push! (rings, hole)
299
+ result_polygons[i] = GI. Polygon (rings)
300
+ break
301
+ end
302
+ end
303
+ end
304
+
305
+ return length (result_polygons) == 1 ? result_polygons[1 ] : GI. MultiPolygon (result_polygons)
306
+ end
307
+
308
+ function cut_at_antimeridian (:: GI.AbstractCurveTrait , line:: T ; great_circle:: Bool = true ) where {T}
309
+ coords = GO. tuples (line). geom
310
+ segments = segment_ring (coords, great_circle)
311
+
312
+ if isempty (segments)
313
+ return line
314
+ else
315
+ return GI. MultiLineString (segments)
316
+ end
317
+ end
318
+
319
+
320
+ function cut_at_antimeridian (:: GI.MultiPolygonTrait , x; kwargs... )
321
+ results = GI. Polygon[]
322
+ for poly in GI. getgeom (x)
323
+ result = cut_at_antimeridian (GI. PolygonTrait (), poly; kwargs... )
324
+ if result isa GI. Polygon
325
+ push! (results, result)
326
+ elseif result isa GI. MultiPolygon
327
+ append! (results, result. geom)
328
+ end
329
+ end
330
+ return GI. MultiPolygon (results)
331
+ end
332
+
333
+ function cut_at_antimeridian (:: GI.MultiLineStringTrait , multiline:: T ; great_circle:: Bool = true ) where {T}
334
+ linestrings = Vector {Vector{Tuple{Float64,Float64}}} ()
335
+
336
+ for line in GI. getgeom (multiline)
337
+ fixed = cut_at_antimeridian (GI. LineStringTrait (), line; great_circle)
338
+ if fixed isa GI. LineString
339
+ push! (linestrings, GO. tuples (fixed). geom)
340
+ else
341
+ append! (linestrings, GO. tuples .(GI. getgeom (fixed)). geom)
342
+ end
343
+ end
344
+
345
+ return GI. MultiLineString (linestrings)
346
+ end
347
+
348
+ end
0 commit comments