Skip to content

Commit 1f2fe01

Browse files
committed
antimeridian cutting on any domain
1 parent 3b8794f commit 1f2fe01

File tree

1 file changed

+50
-47
lines changed

1 file changed

+50
-47
lines changed

src/transformations/correction/cut_at_antimeridian.jl

Lines changed: 50 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,11 @@ Base.@kwdef struct CutAtAntimeridianAndPoles <: GeometryCorrection
2626
"The value at the south pole, in your angular units"
2727
southpole::Float64 = -90.0 # TODO not used!!!
2828
"The value at the left edge of the antimeridian, in your angular units"
29-
left::Float64 = -180.0 # TODO not used!!!
29+
left::Float64 = -180.0
3030
"The value at the right edge of the antimeridian, in your angular units"
31-
right::Float64 = 180.0 # TODO not used!!!
31+
right::Float64 = 180.0
3232
"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!!!
33+
period::Float64 = right - left
3434
"If the polygon is known to enclose the north pole, set this to true"
3535
force_north_pole::Bool=false # TODO not used!!!
3636
"If the polygon is known to enclose the south pole, set this to true"
@@ -77,16 +77,16 @@ function spherical_degrees_to_cartesian(point::Tuple{Float64,Float64})::Tuple{Fl
7777
end
7878

7979
# Calculate crossing latitude using great circle method
80-
function crossing_latitude_great_circle(start::Tuple{Float64,Float64}, endpoint::Tuple{Float64,Float64})::Float64
80+
function crossing_latitude_great_circle(c::CutAtAntimeridianAndPoles, start::Tuple{Float64,Float64}, endpoint::Tuple{Float64,Float64})::Float64
8181
# Convert points to 3D vectors
8282
p1 = spherical_degrees_to_cartesian(start)
8383
p2 = spherical_degrees_to_cartesian(endpoint)
8484

8585
# Cross product defines plane through both points
8686
n1 = _cross(p1, p2)
8787

88-
# Unit vector -Y defines meridian plane
89-
n2 = (0.0, -1.0, 0.0)
88+
# Unit vector that defines the meridian plane
89+
n2 = spherical_degrees_to_cartesian(c.left, 0.0)
9090

9191
# Intersection of planes defined by cross product
9292
intersection = _cross(n1, n2)
@@ -98,52 +98,52 @@ function crossing_latitude_great_circle(start::Tuple{Float64,Float64}, endpoint:
9898
end
9999

100100
# 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
101+
function crossing_latitude_flat(c::CutAtAntimeridianAndPoles, start::Tuple{Float64,Float64}, stop::Tuple{Float64,Float64})::Float64
102+
lat_delta = stop[2] - start[2]
103+
if stop[1] > 0
104104
round(
105-
start[2] + (180.0 - start[1]) * lat_delta / (endpoint[1] + 360.0 - start[1]),
105+
start[2] + (c.right - start[1]) * lat_delta / (stop[1] + c.period - start[1]),
106106
digits=7
107107
)
108108
else
109109
round(
110-
start[2] + (start[1] + 180.0) * lat_delta / (start[1] + 360.0 - endpoint[1]),
110+
start[2] + (start[1] - c.left) * lat_delta / (start[1] + c.period - stop[1]),
111111
digits=7
112112
)
113113
end
114114
end
115115

116116
# Main crossing latitude calculation function
117-
function crossing_latitude(start::Tuple{Float64,Float64}, endpoint::Tuple{Float64,Float64}, great_circle::Bool)::Float64
117+
function crossing_latitude(c::CutAtAntimeridianAndPoles, start::Tuple{Float64,Float64}, endpoint::Tuple{Float64,Float64}, great_circle::Bool)::Float64
118118
abs(start[1]) == 180 && return start[2]
119119
abs(endpoint[1]) == 180 && return endpoint[2]
120120

121-
return great_circle ? crossing_latitude_great_circle(start, endpoint) : crossing_latitude_flat(start, endpoint)
121+
return great_circle ? crossing_latitude_great_circle(c, start, endpoint) : crossing_latitude_flat(c, start, endpoint)
122122
end
123123

124124
# Normalize coordinates to ensure longitudes are between -180 and 180
125-
function normalize_coords(coords::Vector{Tuple{Float64,Float64}})::Vector{Tuple{Float64,Float64}}
125+
function normalize_coords(c::CutAtAntimeridianAndPoles, coords::Vector{Tuple{Float64,Float64}})::Vector{Tuple{Float64,Float64}}
126126
normalized = deepcopy(coords)
127127
all_on_antimeridian = true
128128

129129
for i in eachindex(normalized)
130130
point = normalized[i]
131131
prev_point = normalized[mod1(i-1, length(normalized))]
132132

133-
if isapprox(point[1], 180)
134-
if abs(point[2]) != 90 && isapprox(prev_point[1], -180)
135-
normalized[i] = (-180.0, point[2])
133+
if isapprox(point[1], c.right)
134+
if abs(point[2]) != c.northpole && isapprox(prev_point[1], c.left)
135+
normalized[i] = (c.left, point[2])
136136
else
137-
normalized[i] = (180.0, point[2])
137+
normalized[i] = (c.right, point[2])
138138
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])
139+
elseif isapprox(point[1], c.left)
140+
if abs(point[2]) != c.northpole && isapprox(prev_point[1], c.right)
141+
normalized[i] = (c.right, point[2])
142142
else
143-
normalized[i] = (-180.0, point[2])
143+
normalized[i] = (c.left, point[2])
144144
end
145145
else
146-
normalized[i] = (((point[1] + 180) % 360) - 180, point[2])
146+
normalized[i] = (((point[1] - c.left) % c.period) + c.left, point[2])
147147
all_on_antimeridian = false
148148
end
149149
end
@@ -152,7 +152,7 @@ function normalize_coords(coords::Vector{Tuple{Float64,Float64}})::Vector{Tuple{
152152
end
153153

154154
# 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}}}
155+
function segment_ring(c::CutAtAntimeridianAndPoles, coords::Vector{Tuple{Float64,Float64}}, great_circle::Bool)::Vector{Vector{Tuple{Float64,Float64}}}
156156
segments = Vector{Vector{Tuple{Float64,Float64}}}()
157157
current_segment = Tuple{Float64,Float64}[]
158158

@@ -163,12 +163,12 @@ function segment_ring(coords::Vector{Tuple{Float64,Float64}}, great_circle::Bool
163163

164164
# Check for antimeridian crossing
165165
if (endpoint[1] - start[1] > 180) && (endpoint[1] - start[1] != 360) # left crossing
166-
lat = crossing_latitude(start, endpoint, great_circle)
166+
lat = crossing_latitude(c, start, endpoint, great_circle)
167167
push!(current_segment, (-180.0, lat))
168168
push!(segments, current_segment)
169169
current_segment = [(180.0, lat)]
170170
elseif (start[1] - endpoint[1] > 180) && (start[1] - endpoint[1] != 360) # right crossing
171-
lat = crossing_latitude(endpoint, start, great_circle)
171+
lat = crossing_latitude(c, endpoint, start, great_circle)
172172
push!(current_segment, (180.0, lat))
173173
push!(segments, current_segment)
174174
current_segment = [(-180.0, lat)]
@@ -190,32 +190,32 @@ function segment_ring(coords::Vector{Tuple{Float64,Float64}}, great_circle::Bool
190190
end
191191

192192
# 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
193+
function is_self_closing(c::CutAtAntimeridianAndPoles, segment::Vector{Tuple{Float64,Float64}})::Bool
194+
is_right = segment[end][1] == c.right
195195
return segment[1][1] == segment[end][1] && (
196196
(is_right && segment[1][2] > segment[end][2]) ||
197197
(!is_right && segment[1][2] < segment[end][2])
198198
)
199199
end
200200

201201
# Build polygons from segments
202-
function build_polygons(segments::Vector{Vector{Tuple{Float64,Float64}}})::Vector{GI.Polygon}
202+
function build_polygons(c::CutAtAntimeridianAndPoles, segments::Vector{Vector{Tuple{Float64,Float64}}})::Vector{GI.Polygon}
203203
isempty(segments) && return GI.Polygon[]
204204

205205
segment = pop!(segments)
206-
is_right = segment[end][1] == 180
206+
is_right = segment[end][1] == c.right
207207
candidates = Tuple{Union{Nothing,Int},Float64}[]
208208

209-
if is_self_closing(segment)
209+
if is_self_closing(c, segment)
210210
push!(candidates, (nothing, segment[1][2]))
211211
end
212212

213213
for (i, s) in enumerate(segments)
214214
if s[1][1] == segment[end][1]
215215
if (is_right && s[1][2] > segment[end][2] &&
216-
(!is_self_closing(s) || s[end][2] < segment[1][2])) ||
216+
(!is_self_closing(c, s) || s[end][2] < segment[1][2])) ||
217217
(!is_right && s[1][2] < segment[end][2] &&
218-
(!is_self_closing(s) || s[end][2] > segment[1][2]))
218+
(!is_self_closing(c, s) || s[end][2] > segment[1][2]))
219219
push!(candidates, (i, s[1][2]))
220220
end
221221
end
@@ -230,10 +230,10 @@ function build_polygons(segments::Vector{Vector{Tuple{Float64,Float64}}})::Vecto
230230
# Join segments and recurse
231231
segment = vcat(segment, segments[index])
232232
segments[index] = segment
233-
return build_polygons(segments)
233+
return build_polygons(c, segments)
234234
else
235235
# Handle self-joining segment
236-
polygons = build_polygons(segments)
236+
polygons = build_polygons(c, segments)
237237
if !all(p == segment[1] for p in segment)
238238
push!(polygons, GI.Polygon([segment]))
239239
end
@@ -245,9 +245,12 @@ function build_polygons(segments::Vector{Vector{Tuple{Float64,Float64}}})::Vecto
245245
end
246246

247247
# Main function to cut a polygon at the antimeridian
248-
cut_at_antimeridian(x) = cut_at_antimeridian(GI.trait(x), x)
248+
cut_at_antimeridian(x) = cut_at_antimeridian(CutAtAntimeridianAndPoles(), GI.trait(x), x)
249+
cut_at_antimeridian(c::CutAtAntimeridianAndPoles, x) = cut_at_antimeridian(c, GI.trait(x), x)
250+
cut_at_antimeridian(t::GI.AbstractTrait, x) = cut_at_antimeridian(CutAtAntimeridianAndPoles(), t, x)
249251

250252
function cut_at_antimeridian(
253+
c::CutAtAntimeridianAndPoles,
251254
::GI.PolygonTrait,
252255
polygon::T;
253256
force_north_pole::Bool=false,
@@ -257,16 +260,16 @@ function cut_at_antimeridian(
257260
) where {T}
258261
# Get exterior ring
259262
exterior = GO.tuples(GI.getexterior(polygon)).geom
260-
exterior = normalize_coords(exterior)
263+
exterior = normalize_coords(c, exterior)
261264

262265
# Segment the exterior ring
263-
segments = segment_ring(exterior, great_circle)
266+
segments = segment_ring(c, exterior, great_circle)
264267

265268
if isempty(segments)
266269
# No antimeridian crossing
267270
if fix_winding && GO.isclockwise(GI.LinearRing(exterior))
268271
coord_vecs = reverse.(getproperty.(GO.tuples.(GI.getring(polygon)), :geom))
269-
return GI.Polygon(normalize_coords.(coord_vecs))
272+
return GI.Polygon(normalize_coords.((c,), coord_vecs))
270273
end
271274
return polygon
272275
end
@@ -275,13 +278,13 @@ function cut_at_antimeridian(
275278
holes = Vector{Vector{Tuple{Float64,Float64}}}()
276279
for hole_idx in 1:GI.nhole(polygon)
277280
hole = GO.tuples(GI.gethole(polygon, hole_idx)).geom
278-
hole_segments = segment_ring(hole, great_circle)
281+
hole_segments = segment_ring(c, hole, great_circle)
279282

280283
if !isempty(hole_segments)
281284
if fix_winding
282285
unwrapped = [(x % 360, y) for (x, y) in hole]
283286
if !GO.isclockwise(GI.LineString(unwrapped))
284-
hole_segments = segment_ring(reverse(hole), great_circle)
287+
hole_segments = segment_ring(c, reverse(hole), great_circle)
285288
end
286289
end
287290
append!(segments, hole_segments)
@@ -310,9 +313,9 @@ function cut_at_antimeridian(
310313
return length(result_polygons) == 1 ? result_polygons[1] : GI.MultiPolygon(result_polygons)
311314
end
312315

313-
function cut_at_antimeridian(::GI.AbstractCurveTrait, line::T; great_circle::Bool=true) where {T}
316+
function cut_at_antimeridian(c::CutAtAntimeridianAndPoles, t::GI.AbstractCurveTrait, line::T; great_circle::Bool=true) where {T}
314317
coords = GO.tuples(line).geom
315-
segments = segment_ring(coords, great_circle)
318+
segments = segment_ring(c, coords, great_circle)
316319

317320
if isempty(segments)
318321
return line
@@ -322,10 +325,10 @@ function cut_at_antimeridian(::GI.AbstractCurveTrait, line::T; great_circle::Boo
322325
end
323326

324327

325-
function cut_at_antimeridian(::GI.MultiPolygonTrait, x; kwargs...)
328+
function cut_at_antimeridian(c::CutAtAntimeridianAndPoles, t::GI.MultiPolygonTrait, x; kwargs...)
326329
results = GI.Polygon[]
327330
for poly in GI.getgeom(x)
328-
result = cut_at_antimeridian(GI.PolygonTrait(), poly; kwargs...)
331+
result = cut_at_antimeridian(c, GI.PolygonTrait(), poly; kwargs...)
329332
if result isa GI.Polygon
330333
push!(results, result)
331334
elseif result isa GI.MultiPolygon
@@ -335,11 +338,11 @@ function cut_at_antimeridian(::GI.MultiPolygonTrait, x; kwargs...)
335338
return GI.MultiPolygon(results)
336339
end
337340

338-
function cut_at_antimeridian(::GI.MultiLineStringTrait, multiline::T; great_circle::Bool=true) where {T}
341+
function cut_at_antimeridian(c::CutAtAntimeridianAndPoles, t::GI.MultiLineStringTrait, multiline::T; great_circle::Bool=true) where {T}
339342
linestrings = Vector{Vector{Tuple{Float64,Float64}}}()
340343

341344
for line in GI.getgeom(multiline)
342-
fixed = cut_at_antimeridian(GI.LineStringTrait(), line; great_circle)
345+
fixed = cut_at_antimeridian(c, GI.LineStringTrait(), line; great_circle)
343346
if fixed isa GI.LineString
344347
push!(linestrings, GO.tuples(fixed).geom)
345348
else

0 commit comments

Comments
 (0)