Union Polygon Clipping
julia
export union
"""
union(geom_a, geom_b, [::Type{T}]; target::Type, fix_multipoly = UnionIntersectingPolygons())
Return the union as a list of geometries, empty when no result exists. The list type is
constrained by the inputs; `target` selects output geometry types and `T` sets coordinate
precision.
`fix_multipoly` corrects intersecting multipolygon components before clipping. The default
union correction inherits the caller’s algorithm, manifold, and numeric type. Set
`fix_multipoly = nothing` only when the input multipolygons are valid.
# Example
```jldoctest
import GeoInterface as GI, GeometryOps as GO
p1 = GI.Polygon([[(0.0, 0.0), (5.0, 5.0), (10.0, 0.0), (5.0, -5.0), (0.0, 0.0)]])
p2 = GI.Polygon([[(3.0, 0.0), (8.0, 5.0), (13.0, 0.0), (8.0, -5.0), (3.0, 0.0)]])
union_poly = GO.union(p1, p2; target = GI.PolygonTrait())
GI.coordinates.(union_poly)output
julia
1-element Vector{Vector{Vector{Vector{Float64}}}}:
[[[6.5, 3.5], [5.0, 5.0], [0.0, 0.0], [5.0, -5.0], [6.5, -3.5], [8.0, -5.0], [13.0, 0.0], [8.0, 5.0], [6.5, 3.5]]]
```
"""
function union(
alg::FosterHormannClipping, geom_a, geom_b, ::Type{T}=Float64; target=nothing, kwargs...
) where {T<:AbstractFloat}
return _union(
alg, TraitTarget(target), T, GI.trait(geom_a), geom_a, GI.trait(geom_b), geom_b;
exact = True(), kwargs...,
)
endfallback definitions if no manifold - assume planar (until we have best_manifold)
julia
function union(
geom_a, geom_b, ::Type{T}=Float64; target=nothing, kwargs...
) where {T<:AbstractFloat}
return union(FosterHormannClipping(Planar()), geom_a, geom_b, T; target, kwargs...)
endif manifold but no algorithm - assume FosterHormannClipping with provided manifold.
julia
function union(m::Manifold, geom_a, geom_b, ::Type{T}=Float64; target=nothing, kwargs...) where {T<:AbstractFloat}
return union(FosterHormannClipping(m), geom_a, geom_b, T; target, kwargs...)
end
#= This 'union' implementation returns the union of two polygons. The algorithm to determine
the union was adapted from "Efficient clipping of efficient polygons," by Greiner and
Hormann (1998). DOI: https://doi.org/10.1145/274363.274364 =#
function _union(
alg::FosterHormannClipping, ::TraitTarget{GI.PolygonTrait}, ::Type{T},
::GI.PolygonTrait, poly_a,
::GI.PolygonTrait, poly_b;
exact, kwargs...,
) where TFirst, I get the exteriors of the two polygons
julia
ext_a = GI.getexterior(poly_a)
ext_b = GI.getexterior(poly_b)Then, I get the union of the exteriors
julia
a_list, b_list, a_idx_list = _build_ab_list(alg, T, ext_a, ext_b, _union_delay_cross_f, _union_delay_bounce_f; exact)
polys = _trace_polynodes(alg, T, a_list, b_list, a_idx_list, _union_step, poly_a, poly_b)
n_pieces = length(polys)Check if one polygon totally within other and if so, return the larger polygon
julia
a_in_b, b_in_a = false, false
if n_pieces == 0 # no crossing points, determine if either poly is inside the other
#= Every branch here returns a piece of the input, so each is rebuilt in the
representation `polys` is committed to rather than plain tuples. =#
P = _fh_out_point_type(alg.manifold, poly_a, T)
a_in_b, b_in_a = _find_non_cross_orientation(alg, a_list, b_list, ext_a, ext_b; exact)
if a_in_b
push!(polys, GI.Polygon([_fh_as_ring(P, ext_b, T)]))
elseif b_in_a
push!(polys, GI.Polygon([_fh_as_ring(P, ext_a, T)]))
else
push!(polys, _fh_as_poly(P, poly_a, T))
push!(polys, _fh_as_poly(P, poly_b, T))
return polys
end
elseif n_pieces > 1
#= extra polygons are holes (n_pieces == 1 is the desired state) and since
holes are formed by regions exterior to both poly_a and poly_b, they can't interact
with pre-existing holes =#
sort!(polys, by = p -> area(alg.manifold, p, T), rev = true) # sort by area so first element is the exteriorthe first element is the exterior, the rest are holes
julia
@views append!(polys[1].geom, (GI.getexterior(p) for p in polys[2:end]))
keepat!(polys, 1)
endAdd in holes
julia
if GI.nhole(poly_a) != 0 || GI.nhole(poly_b) != 0
_add_union_holes!(alg, polys, a_in_b, b_in_a, poly_a, poly_b; exact)
endRemove unneeded collinear points on same edge
julia
_remove_collinear_points!(alg, polys, [false], poly_a, poly_b)
return polys
endHelper functions for Unions with Greiner and Hormann Polygon Clipping
julia
#= When marking the crossing status of a delayed crossing, the chain start point is crossing
when the start point is a entry point and is a bouncing point when the start point is an
exit point. The end of the chain has the opposite crossing / bouncing status. =#
_union_delay_cross_f(x) = (x, !x)
#= Delayed-bounce endpoints bounce if adjacent edges lie inside the other polygon
(`x`); otherwise they cross. =#
_union_delay_bounce_f(x, _) = !x
#= When tracing polygons, step backwards if the most recent intersection point was an entry
point, else step forwards where x is the entry/exit status. =#
_union_step(x, _) = x ? (-1) : 1
#= Add holes from two polygons to the exterior polygon formed by their union. If adding the
the holes reveals that the polygons aren't actually intersecting, return the original
polygons. =#
function _add_union_holes!(alg::FosterHormannClipping, polys, a_in_b, b_in_a, poly_a, poly_b; exact)
P = _fh_poly_point_type(eltype(polys))
T = _fh_float_type(P)
if a_in_b
_add_union_holes_contained_polys!(alg, polys, poly_a, poly_b; exact)
elseif b_in_a
_add_union_holes_contained_polys!(alg, polys, poly_b, poly_a; exact)
else # Polygons intersect, but neither is contained in the other
n_a_holes = GI.nhole(poly_a)
ext_poly_a = GI.Polygon(StaticArrays.SVector(GI.getexterior(poly_a)))
ext_poly_b = GI.Polygon(StaticArrays.SVector(GI.getexterior(poly_b)))
#= Start with poly_b when comparing with holes from poly_a and then switch to poly_a
to compare with holes from poly_b. For current_poly, use ext_poly_b to avoid
repeating overlapping holes in poly_a and poly_b =#
curr_exterior_poly = n_a_holes > 0 ? ext_poly_b : ext_poly_a
current_poly = n_a_holes > 0 ? ext_poly_b : poly_aLoop over all holes in both original polygons
julia
for (i, ih) in enumerate(Iterators.flatten((GI.gethole(poly_a), GI.gethole(poly_b))))
ih = _fh_as_ring(P, ih, T)
in_ext, _, _ = _line_polygon_interactions(alg.manifold, ih, curr_exterior_poly; exact, closed_line = true)
if !in_ext
#= if the hole isn't in the overlapping region between the two polygons, add
the hole to the resulting polygon as we know it can't interact with any
other holes =#
push!(polys[1].geom, ih)
else
#= Subtract the other polygon from holes that intersect the overlap.
Include its holes so shared holes within the overlap are retained. =#
h_poly = GI.Polygon(StaticArrays.SVector(ih))
new_holes = difference(alg, h_poly, current_poly, T; target = GI.PolygonTrait())
append!(polys[1].geom, (GI.getexterior(new_h) for new_h in new_holes))
end
if i == n_a_holes
curr_exterior_poly = ext_poly_a
current_poly = poly_a
end
end
end
return
end
#= Add holes holes to the union of two polygons where one of the original polygons was
inside of the other. If adding the the holes reveal that the polygons aren't actually
intersecting, return the original polygons.=#
function _add_union_holes_contained_polys!(alg::FosterHormannClipping, polys, interior_poly, exterior_poly; exact)
P = _fh_poly_point_type(eltype(polys))
T = _fh_float_type(P)
union_poly = polys[1]
interior_poly = _fh_as_poly(P, interior_poly, T)
exterior_poly = _fh_as_poly(P, exterior_poly, T)
ext_int_ring = GI.getexterior(interior_poly)
for (i, ih) in enumerate(GI.gethole(exterior_poly))
poly_ih = GI.Polygon(StaticArrays.SVector(ih))
in_ih, on_ih, out_ih = _line_polygon_interactions(alg.manifold, ext_int_ring, poly_ih; exact, closed_line = true)
if in_ih # at least part of interior polygon exterior is within the ith hole
if !on_ih && !out_ih
#= interior polygon is completely within the ith hole - polygons aren't
touching and do not actually form a union =#
P = _fh_poly_point_type(eltype(polys))
polys[1] = _fh_as_poly(P, interior_poly)
push!(polys, _fh_as_poly(P, exterior_poly))
return polys
else
#= interior polygon is partially within the ith hole - area of interior
polygon reduces the size of the hole =#
new_holes = difference(alg, poly_ih, interior_poly, T; target = GI.PolygonTrait())
append!(union_poly.geom, (GI.getexterior(new_h) for new_h in new_holes))
end
else # none of interior polygon exterior is within the ith hole
if !out_ih
#= interior polygon's exterior is the same as the ith hole - polygons do
form a union, but do not overlap so all holes stay in final polygon =#
append!(union_poly.geom, Iterators.drop(GI.gethole(exterior_poly), i))
append!(union_poly.geom, GI.gethole(interior_poly))
return polys
else
#= interior polygon's exterior is outside of the ith hole - the interior
polygon could either be disjoint from the hole, or contain the hole =#
ext_int_poly = GI.Polygon(StaticArrays.SVector(ext_int_ring))
in_int, _, _ = _line_polygon_interactions(alg.manifold, ih, ext_int_poly; exact, closed_line = true)
if in_int
#= interior polygon contains the hole - overlapping holes between the
interior and exterior polygons will be added =#
for jh in GI.gethole(interior_poly)
poly_jh = GI.Polygon(StaticArrays.SVector(jh))
if intersects(alg.manifold, poly_ih, poly_jh)
new_holes = intersection(alg, poly_ih, poly_jh, T; target = GI.PolygonTrait())
append!(union_poly.geom, (GI.getexterior(new_h) for new_h in new_holes))
end
end
else
#= interior polygon and the exterior polygon are disjoint - add the ith
hole as it is not covered by the interior polygon =#
push!(union_poly.geom, ih)
end
end
end
end
return
end
#= Include all components of `multipoly_b`, merging those that intersect `poly_a`.
Correct the multipolygon unless `fix_multipoly = nothing`. =#
function _union(
alg::FosterHormannClipping, target::TraitTarget{GI.PolygonTrait}, ::Type{T},
::GI.PolygonTrait, poly_a,
::GI.MultiPolygonTrait, multipoly_b;
fix_multipoly = UnionIntersectingPolygons(alg, T), kwargs...,
) where T
if !isnothing(fix_multipoly) # Fix multipoly_b to prevent repeated regions in the output
multipoly_b = fix_multipoly(multipoly_b)
end
P = _fh_out_point_type(alg.manifold, poly_a, T)
polys = [_fh_as_poly(P, poly_a, T)]
for poly_b in GI.getpolygon(multipoly_b)
if intersects(alg.manifold, polys[1], poly_b)If polygons intersect and form a new polygon, swap out polygon
julia
new_polys = union(alg, polys[1], poly_b, T; target)
if length(new_polys) > 1 # case where they intersect by just one point
push!(polys, _fh_as_poly(P, poly_b, T)) # add poly_b to list
else
polys[1] = new_polys[1]
end
elseIf they don't intersect, poly_b is now a part of the union as its own polygon
julia
push!(polys, _fh_as_poly(P, poly_b, T))
end
end
return polys
end
#= Preserve operand order so mixed representations do not round-trip passthrough vertices.
The multipolygon implementation accumulates its original components in their own chart. =#
_union(
alg::FosterHormannClipping, target::TraitTarget{GI.PolygonTrait}, ::Type{T},
::GI.MultiPolygonTrait, multipoly_a,
::GI.PolygonTrait, poly_b;
kwargs...,
) where T = union(alg, multipoly_a, GI.MultiPolygon([poly_b]), T; target, kwargs...)
#= Merge intersecting components of both multipolygons. Apply the supplied correction
unless `fix_multipoly = nothing`. =#
function _union(
alg::FosterHormannClipping, target::TraitTarget{GI.PolygonTrait}, ::Type{T},
::GI.MultiPolygonTrait, multipoly_a,
::GI.MultiPolygonTrait, multipoly_b;
fix_multipoly = UnionIntersectingPolygons(alg, T), kwargs...,
) where T
if !isnothing(fix_multipoly) # Fix multipoly_b to prevent repeated regions in the output
multipoly_a = fix_multipoly(multipoly_a)
multipoly_b = fix_multipoly(multipoly_b)
fix_multipoly = nothing
end
multipolys = multipoly_b
polys = [_fh_as_poly(_fh_out_point_type(alg.manifold, multipoly_a, T), p, T) for p in GI.getpolygon(multipoly_b)]
for poly_a in GI.getpolygon(multipoly_a)
polys = union(alg, poly_a, multipolys, T; target, fix_multipoly)
multipolys = _fh_multipolygon(polys)
end
return polys
end
function _union(
alg::FosterHormannClipping, target::TraitTarget{GI.MultiPolygonTrait}, ::Type{T},
::GI.PolygonTrait, poly_a,
::GI.PolygonTrait, poly_b;
kwargs...,
) where T
return _fh_multipolygon(union(alg, poly_a, poly_b, T; target = GI.PolygonTrait(), kwargs...))
end
function _union(
alg::FosterHormannClipping, target::TraitTarget{GI.MultiPolygonTrait}, ::Type{T},
::GI.PolygonTrait, poly_a,
::GI.MultiPolygonTrait, multipoly_b;
fix_multipoly = UnionIntersectingPolygons(alg, T),
kwargs...,
) where T
res = union(alg, poly_a, multipoly_b, T; target = GI.PolygonTrait(), fix_multipoly, kwargs...)
if !isnothing(fix_multipoly)
return fix_multipoly(_fh_multipolygon(res))
else
return _fh_multipolygon(res)
end
endthis is the opposite of the above
julia
function _union(
alg::FosterHormannClipping, target::TraitTarget{GI.MultiPolygonTrait}, ::Type{T},
::GI.MultiPolygonTrait, multipoly_a,
::GI.PolygonTrait, poly_b;
fix_multipoly = UnionIntersectingPolygons(alg, T),
kwargs...,
) where T
return _fh_multipolygon(union(alg, multipoly_a, poly_b, T;
target = GI.PolygonTrait(), fix_multipoly, kwargs...))
end
function _union(
alg::FosterHormannClipping, target::TraitTarget{GI.MultiPolygonTrait}, ::Type{T},
trait_a::GI.MultiPolygonTrait, multipoly_a,
trait_b::GI.MultiPolygonTrait, multipoly_b;
fix_multipoly = UnionIntersectingPolygons(alg, T), kwargs...,
) where T
return _fh_multipolygon(_union(alg, TraitTarget{GI.PolygonTrait}(), T, trait_a, multipoly_a, trait_b, multipoly_b; fix_multipoly, kwargs...))
endMany type and target combos aren't implemented
julia
function _union(
alg::GeometryOpsCore.Algorithm, target::TraitTarget{Target}, ::Type{T},
trait_a::GI.AbstractTrait, geom_a,
trait_b::GI.AbstractTrait, geom_b;
kwargs...
) where {Target,T}
throw(ArgumentError("Union between $trait_a and $trait_b with target $Target isn't implemented yet."))
return nothing
endThis page was generated using Literate.jl.