Skip to content

Commit f5f9b0a

Browse files
authored
Multipolygon Clipping (#102)
* Add minimal multipolygon correction * Multipolygon clipping working * Update multipolygon clipping documentation * Explain fix_multipoly * Make fix_multipoly more general * Fix multipolygon type stability * Remove GO ref * Update fix_multipoly default comment
1 parent 45315ee commit f5f9b0a

File tree

8 files changed

+338
-18
lines changed

8 files changed

+338
-18
lines changed

src/GeometryOps.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ include("transformations/tuples.jl")
5454
include("transformations/transform.jl")
5555
include("transformations/correction/geometry_correction.jl")
5656
include("transformations/correction/closed_ring.jl")
57+
include("transformations/correction/intersecting_polygons.jl")
5758

5859
# Import all names from GeoInterface and Extents, so users can do `GO.extent` or `GO.trait`.
5960
for name in names(GeoInterface)

src/methods/clipping/difference.jl

Lines changed: 71 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,19 @@ export difference
33

44

55
"""
6-
difference(geom_a, geom_b, [T::Type]; target::Type)
6+
difference(geom_a, geom_b, [T::Type]; target::Type, fix_multipoly = UnionIntersectingPolygons())
77
88
Return the difference between two geometries as a list of geometries. Return an empty list
99
if none are found. The type of the list will be constrained as much as possible given the
1010
input geometries. Furthermore, the user can provide a `taget` type as a keyword argument and
1111
a list of target geometries found in the difference will be returned. The user can also
12-
provide a float type that they would like the points of returned geometries to be.
12+
provide a float type that they would like the points of returned geometries to be. If the
13+
user is taking a intersection involving one or more multipolygons, and the multipolygon
14+
might be comprised of polygons that intersect, if `fix_multipoly` is set to an
15+
`IntersectingPolygons` correction (the default is `UnionIntersectingPolygons()`), then the
16+
needed multipolygons will be fixed to be valid before performing the intersection to ensure
17+
a correct answer. Only set `fix_multipoly` to false if you know that the multipolygons are
18+
valid, as it will avoid unneeded computation.
1319
1420
## Example
1521
@@ -27,9 +33,9 @@ GI.coordinates.(diff_poly)
2733
```
2834
"""
2935
function difference(
30-
geom_a, geom_b, ::Type{T} = Float64; target=nothing,
36+
geom_a, geom_b, ::Type{T} = Float64; target=nothing, kwargs...,
3137
) where {T<:AbstractFloat}
32-
return _difference(TraitTarget(target), T, GI.trait(geom_a), geom_a, GI.trait(geom_b), geom_b)
38+
return _difference(TraitTarget(target), T, GI.trait(geom_a), geom_a, GI.trait(geom_b), geom_b; kwargs...)
3339
end
3440

3541
#= The 'difference' function returns the difference of two polygons as a list of polygons.
@@ -38,7 +44,8 @@ polygons," by Greiner and Hormann (1998). DOI: https://doi.org/10.1145/274363.27
3844
function _difference(
3945
::TraitTarget{GI.PolygonTrait}, ::Type{T},
4046
::GI.PolygonTrait, poly_a,
41-
::GI.PolygonTrait, poly_b,
47+
::GI.PolygonTrait, poly_b;
48+
kwargs...
4249
) where T
4350
# Get the exterior of the polygons
4451
ext_a = GI.getexterior(poly_a)
@@ -99,6 +106,65 @@ tracing a_list, else step backwards, where x is the entry/exit status and y is a
99106
that is true if we are on a_list and false if we are on b_list. =#
100107
_diff_step(x, y) = (x y) ? 1 : (-1)
101108

109+
#= Polygon with multipolygon difference - note that all intersection regions between
110+
`poly_a` and any of the sub-polygons of `multipoly_b` are removed from `poly_a`. =#
111+
function _difference(
112+
target::TraitTarget{GI.PolygonTrait}, ::Type{T},
113+
::GI.PolygonTrait, poly_a,
114+
::GI.MultiPolygonTrait, multipoly_b;
115+
kwargs...,
116+
) where T
117+
polys = [tuples(poly_a, T)]
118+
for poly_b in GI.getpolygon(multipoly_b)
119+
isempty(polys) && break
120+
polys = mapreduce(p -> difference(p, poly_b; target), append!, polys)
121+
end
122+
return polys
123+
end
124+
125+
#= Multipolygon with polygon difference - note that all intersection regions between
126+
sub-polygons of `multipoly_a` and `poly_b` will be removed from the corresponding
127+
sub-polygon. Unless specified with `fix_multipoly = nothing`, `multipolygon_a` will be
128+
validated using the given (default is `UnionIntersectingPolygons()`) correction. =#
129+
function _difference(
130+
target::TraitTarget{GI.PolygonTrait}, ::Type{T},
131+
::GI.MultiPolygonTrait, multipoly_a,
132+
::GI.PolygonTrait, poly_b;
133+
fix_multipoly = UnionIntersectingPolygons(), kwargs...,
134+
) where T
135+
if !isnothing(fix_multipoly) # Fix multipoly_a to prevent returning an invalid multipolygon
136+
multipoly_a = fix_multipoly(multipoly_a)
137+
end
138+
polys = Vector{_get_poly_type(T)}()
139+
sizehint!(polys, GI.npolygon(multipoly_a))
140+
for poly_a in GI.getpolygon(multipoly_a)
141+
append!(polys, difference(poly_a, poly_b; target))
142+
end
143+
return polys
144+
end
145+
146+
#= Multipolygon with multipolygon difference - note that all intersection regions between
147+
sub-polygons of `multipoly_a` and sub-polygons of `multipoly_b` will be removed from the
148+
corresponding sub-polygon of `multipoly_a`. Unless specified with `fix_multipoly = nothing`,
149+
`multipolygon_a` will be validated using the given (defauly is `UnionIntersectingPolygons()`)
150+
correction. =#
151+
function _difference(
152+
target::TraitTarget{GI.PolygonTrait}, ::Type{T},
153+
::GI.MultiPolygonTrait, multipoly_a,
154+
::GI.MultiPolygonTrait, multipoly_b;
155+
fix_multipoly = UnionIntersectingPolygons(), kwargs...,
156+
) where T
157+
if !isnothing(fix_multipoly) # Fix multipoly_a to prevent returning an invalid multipolygon
158+
multipoly_a = fix_multipoly(multipoly_a)
159+
fix_multipoly = nothing
160+
end
161+
local polys
162+
for (i, poly_b) in enumerate(GI.getpolygon(multipoly_b))
163+
polys = difference(i == 1 ? multipoly_a : GI.MultiPolygon(polys), poly_b; target, fix_multipoly)
164+
end
165+
return polys
166+
end
167+
102168
# Many type and target combos aren't implemented
103169
function _difference(
104170
::TraitTarget{Target}, ::Type{T},

src/methods/clipping/intersection.jl

Lines changed: 69 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,19 @@ Enum for the orientation of a line with respect to a curve. A line can be
1010
@enum LineOrientation line_cross=1 line_hinge=2 line_over=3 line_out=4
1111

1212
"""
13-
intersection(geom_a, geom_b, [T::Type]; target::Type)
13+
intersection(geom_a, geom_b, [T::Type]; target::Type, fix_multipoly = UnionIntersectingPolygons())
1414
1515
Return the intersection between two geometries as a list of geometries. Return an empty list
1616
if none are found. The type of the list will be constrained as much as possible given the
1717
input geometries. Furthermore, the user can provide a `target` type as a keyword argument and
1818
a list of target geometries found in the intersection will be returned. The user can also
19-
provide a float type that they would like the points of returned geometries to be.
19+
provide a float type that they would like the points of returned geometries to be. If the
20+
user is taking a intersection involving one or more multipolygons, and the multipolygon
21+
might be comprised of polygons that intersect, if `fix_multipoly` is set to an
22+
`IntersectingPolygons` correction (the default is `UnionIntersectingPolygons()`), then the
23+
needed multipolygons will be fixed to be valid before performing the intersection to ensure
24+
a correct answer. Only set `fix_multipoly` to nothing if you know that the multipolygons are
25+
valid, as it will avoid unneeded computation.
2026
2127
## Example
2228
@@ -34,16 +40,17 @@ GI.coordinates.(inter_points)
3440
```
3541
"""
3642
function intersection(
37-
geom_a, geom_b, ::Type{T}=Float64; target=nothing,
43+
geom_a, geom_b, ::Type{T}=Float64; target=nothing, kwargs...,
3844
) where {T<:AbstractFloat}
39-
return _intersection(TraitTarget(target), T, GI.trait(geom_a), geom_a, GI.trait(geom_b), geom_b)
45+
return _intersection(TraitTarget(target), T, GI.trait(geom_a), geom_a, GI.trait(geom_b), geom_b; kwargs...)
4046
end
4147

4248
# Curve-Curve Intersections with target Point
4349
_intersection(
4450
::TraitTarget{GI.PointTrait}, ::Type{T},
4551
trait_a::Union{GI.LineTrait, GI.LineStringTrait, GI.LinearRingTrait}, geom_a,
46-
trait_b::Union{GI.LineTrait, GI.LineStringTrait, GI.LinearRingTrait}, geom_b,
52+
trait_b::Union{GI.LineTrait, GI.LineStringTrait, GI.LinearRingTrait}, geom_b;
53+
kwargs...,
4754
) where T = _intersection_points(T, trait_a, geom_a, trait_b, geom_b)
4855

4956
#= Polygon-Polygon Intersections with target Polygon
@@ -53,7 +60,8 @@ DOI: https://doi.org/10.1145/274363.274364 =#
5360
function _intersection(
5461
::TraitTarget{GI.PolygonTrait}, ::Type{T},
5562
::GI.PolygonTrait, poly_a,
56-
::GI.PolygonTrait, poly_b,
63+
::GI.PolygonTrait, poly_b;
64+
kwargs...,
5765
) where {T}
5866
# First we get the exteriors of 'poly_a' and 'poly_b'
5967
ext_a = GI.getexterior(poly_a)
@@ -98,11 +106,65 @@ _inter_delay_bounce_f(x, _) = x
98106
point, else step backwards where x is the entry/exit status. =#
99107
_inter_step(x, _) = x ? 1 : (-1)
100108

109+
#= Polygon with multipolygon intersection - note that all intersection regions between
110+
`poly_a` and any of the sub-polygons of `multipoly_b` are counted as intersection polygons.
111+
Unless specified with `fix_multipoly = nothing`, `multipolygon_b` will be validated using
112+
the given (default is `UnionIntersectingPolygons()`) correction. =#
113+
function _intersection(
114+
target::TraitTarget{GI.PolygonTrait}, ::Type{T},
115+
::GI.PolygonTrait, poly_a,
116+
::GI.MultiPolygonTrait, multipoly_b;
117+
fix_multipoly = UnionIntersectingPolygons(), kwargs...,
118+
) where T
119+
if !isnothing(fix_multipoly) # Fix multipoly_b to prevent duplicated intersection regions
120+
multipoly_b = fix_multipoly(multipoly_b)
121+
end
122+
polys = Vector{_get_poly_type(T)}()
123+
for poly_b in GI.getpolygon(multipoly_b)
124+
append!(polys, intersection(poly_a, poly_b; target))
125+
end
126+
return polys
127+
end
128+
129+
#= Multipolygon with polygon intersection is equivalent to taking the intersection of the
130+
poylgon with the multipolygon and thus simply switches the order of operations and calls the
131+
above method. =#
132+
_intersection(
133+
target::TraitTarget{GI.PolygonTrait}, ::Type{T},
134+
::GI.MultiPolygonTrait, multipoly_a,
135+
::GI.PolygonTrait, poly_b;
136+
kwargs...,
137+
) where T = intersection(poly_b, multipoly_a; target , kwargs...)
138+
139+
#= Multipolygon with multipolygon intersection - note that all intersection regions between
140+
any sub-polygons of `multipoly_a` and any of the sub-polygons of `multipoly_b` are counted
141+
as intersection polygons. Unless specified with `fix_multipoly = nothing`, both
142+
`multipolygon_a` and `multipolygon_b` will be validated using the given (default is
143+
`UnionIntersectingPolygons()`) correction. =#
144+
function _intersection(
145+
target::TraitTarget{GI.PolygonTrait}, ::Type{T},
146+
::GI.MultiPolygonTrait, multipoly_a,
147+
::GI.MultiPolygonTrait, multipoly_b;
148+
fix_multipoly = UnionIntersectingPolygons(), kwargs...,
149+
) where T
150+
if !isnothing(fix_multipoly) # Fix both multipolygons to prevent duplicated regions
151+
multipoly_a = fix_multipoly(multipoly_a)
152+
multipoly_b = fix_multipoly(multipoly_b)
153+
fix_multipoly = nothing
154+
end
155+
polys = Vector{_get_poly_type(T)}()
156+
for poly_a in GI.getpolygon(multipoly_a)
157+
append!(polys, intersection(poly_a, multipoly_b; target, fix_multipoly))
158+
end
159+
return polys
160+
end
161+
101162
# Many type and target combos aren't implemented
102163
function _intersection(
103164
::TraitTarget{Target}, ::Type{T},
104165
trait_a::GI.AbstractTrait, geom_a,
105-
trait_b::GI.AbstractTrait, geom_b,
166+
trait_b::GI.AbstractTrait, geom_b;
167+
kwargs...,
106168
) where {Target, T}
107169
@assert(
108170
false,

src/methods/clipping/union.jl

Lines changed: 76 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,19 @@
22
export union
33

44
"""
5-
union(geom_a, geom_b, [::Type{T}]; target::Type)
5+
union(geom_a, geom_b, [::Type{T}]; target::Type, fix_multipoly = UnionIntersectingPolygons())
66
77
Return the union between two geometries as a list of geometries. Return an empty list if
88
none are found. The type of the list will be constrained as much as possible given the input
99
geometries. Furthermore, the user can provide a `taget` type as a keyword argument and a
1010
list of target geometries found in the difference will be returned. The user can also
11-
provide a float type 'T' that they would like the points of returned geometries to be.
11+
provide a float type 'T' that they would like the points of returned geometries to be. If
12+
the user is taking a intersection involving one or more multipolygons, and the multipolygon
13+
might be comprised of polygons that intersect, if `fix_multipoly` is set to an
14+
`IntersectingPolygons` correction (the default is `UnionIntersectingPolygons()`), then the
15+
needed multipolygons will be fixed to be valid before performing the intersection to ensure
16+
a correct answer. Only set `fix_multipoly` to false if you know that the multipolygons are
17+
valid, as it will avoid unneeded computation.
1218
1319
Calculates the union between two polygons.
1420
## Example
@@ -27,9 +33,9 @@ GI.coordinates.(union_poly)
2733
```
2834
"""
2935
function union(
30-
geom_a, geom_b, ::Type{T}=Float64; target=nothing,
36+
geom_a, geom_b, ::Type{T}=Float64; target=nothing, kwargs...
3137
) where {T<:AbstractFloat}
32-
_union(TraitTarget(target), T, GI.trait(geom_a), geom_a, GI.trait(geom_b), geom_b)
38+
_union(TraitTarget(target), T, GI.trait(geom_a), geom_a, GI.trait(geom_b), geom_b; kwargs...)
3339
end
3440

3541
#= This 'union' implementation returns the union of two polygons. The algorithm to determine
@@ -38,7 +44,8 @@ Hormann (1998). DOI: https://doi.org/10.1145/274363.274364 =#
3844
function _union(
3945
::TraitTarget{GI.PolygonTrait}, ::Type{T},
4046
::GI.PolygonTrait, poly_a,
41-
::GI.PolygonTrait, poly_b,
47+
::GI.PolygonTrait, poly_b;
48+
kwargs...,
4249
) where T
4350
# First, I get the exteriors of the two polygons
4451
ext_a = GI.getexterior(poly_a)
@@ -196,12 +203,75 @@ function _add_union_holes_contained_polys!(polys, interior_poly, exterior_poly)
196203
return
197204
end
198205

206+
#= Polygon with multipolygon union - note that all sub-polygons of `multipoly_b` will be
207+
included, unioning these sub-polygons with `poly_a` where they intersect. Unless specified
208+
with `fix_multipoly = nothing`, `multipolygon_b` will be validated using the given (default
209+
is `UnionIntersectingPolygons()`) correction. =#
210+
function _union(
211+
target::TraitTarget{GI.PolygonTrait}, ::Type{T},
212+
::GI.PolygonTrait, poly_a,
213+
::GI.MultiPolygonTrait, multipoly_b;
214+
fix_multipoly = UnionIntersectingPolygons(), kwargs...,
215+
) where T
216+
if !isnothing(fix_multipoly) # Fix multipoly_b to prevent repeated regions in the output
217+
multipoly_b = fix_multipoly(multipoly_b)
218+
end
219+
polys = [tuples(poly_a, T)]
220+
for poly_b in GI.getpolygon(multipoly_b)
221+
if intersects(polys[1], poly_b)
222+
# If polygons intersect and form a new polygon, swap out polygon
223+
new_polys = union(polys[1], poly_b; target)
224+
if length(new_polys) > 1 # case where they intersect by just one point
225+
push!(polys, tuples(poly_b, T)) # add poly_b to list
226+
else
227+
polys[1] = new_polys[1]
228+
end
229+
else
230+
# If they don't intersect, poly_b is now a part of the union as its own polygon
231+
push!(polys, tuples(poly_b, T))
232+
end
233+
end
234+
return polys
235+
end
236+
237+
#= Multipolygon with polygon union is equivalent to taking the union of the poylgon with the
238+
multipolygon and thus simply switches the order of operations and calls the above method. =#
239+
_union(
240+
target::TraitTarget{GI.PolygonTrait}, ::Type{T},
241+
::GI.MultiPolygonTrait, multipoly_a,
242+
::GI.PolygonTrait, poly_b;
243+
kwargs...,
244+
) where T = union(poly_b, multipoly_a; target, kwargs...)
245+
246+
#= Multipolygon with multipolygon union - note that all of the sub-polygons of `multipoly_a`
247+
and the sub-polygons of `multipoly_b` are included and combined together where there are
248+
intersections. Unless specified with `fix_multipoly = nothing`, `multipolygon_b` will be
249+
validated using the given (default is `UnionIntersectingPolygons()`) correction. =#
250+
function _union(
251+
target::TraitTarget{GI.PolygonTrait}, ::Type{T},
252+
::GI.MultiPolygonTrait, multipoly_a,
253+
::GI.MultiPolygonTrait, multipoly_b;
254+
fix_multipoly = UnionIntersectingPolygons(), kwargs...,
255+
) where T
256+
if !isnothing(fix_multipoly) # Fix multipoly_b to prevent repeated regions in the output
257+
multipoly_b = fix_multipoly(multipoly_b)
258+
fix_multipoly = nothing
259+
end
260+
multipolys = multipoly_b
261+
local polys
262+
for poly_a in GI.getpolygon(multipoly_a)
263+
polys = union(poly_a, multipolys; target, fix_multipoly)
264+
multipolys = GI.MultiPolygon(polys)
265+
end
266+
return polys
267+
end
199268

200269
# Many type and target combos aren't implemented
201270
function _union(
202271
::TraitTarget{Target}, ::Type{T},
203272
trait_a::GI.AbstractTrait, geom_a,
204-
trait_b::GI.AbstractTrait, geom_b,
273+
trait_b::GI.AbstractTrait, geom_b;
274+
kwargs...
205275
) where {Target,T}
206276
throw(ArgumentError("Union between $trait_a and $trait_b with target $Target isn't implemented yet."))
207277
return nothing

0 commit comments

Comments
 (0)