Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ll/polygon intersects #53

Merged
merged 94 commits into from
Jan 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
94 commits
Select commit Hold shift + click to select a range
aba090f
Fix up intersection point base calculation
skygering Oct 2, 2023
ab16785
Update intersects and add line tests
skygering Oct 4, 2023
7e75e86
Add more tests and debug intersects
skygering Oct 4, 2023
a7a7367
Add comments to point_in_poly
skygering Oct 4, 2023
e86bfef
adds mutable struct for node
LanaLubecke Oct 4, 2023
9842146
Merge branch 'main' of github.com:Caltech-OCTO/GeometryOps.jl into ll…
LanaLubecke Oct 4, 2023
b99e37d
Remove CairoMakie
skygering Oct 4, 2023
d65b6eb
implements 1st draft of intersection for polygons
LanaLubecke Oct 4, 2023
a369c07
correctly inserts intersection points into b_list
LanaLubecke Oct 6, 2023
a2a7ca8
takes into account alpha vaks when adding intr pts
LanaLubecke Oct 12, 2023
f448736
returns intersection of polygons
LanaLubecke Oct 15, 2023
319bd88
Update equals and overlaps
skygering Oct 18, 2023
3f28df9
simplifies loops, & tracking of unprocessed pts
LanaLubecke Oct 18, 2023
0b9799d
Remove use of findfirst for 1.6 compat
skygering Oct 18, 2023
90fff1c
Updated geom, multi-geom equality
skygering Oct 20, 2023
ab211f3
Merge branch 'asinghvi17:main' into sg/update_intersects
skygering Oct 20, 2023
6184971
Merge pull request #1 from Caltech-OCTO/sg/update_intersects
skygering Oct 20, 2023
529837a
adds latest changes before pulling equals func
LanaLubecke Oct 23, 2023
b9c1e59
Merge branch 'main' into ll/polygon_intersects
LanaLubecke Oct 23, 2023
c135df1
adds compareGO_LG
LanaLubecke Oct 23, 2023
a8e0ba1
expands tests for intersections
LanaLubecke Oct 23, 2023
025b813
adds tolerance when comparing polygons
LanaLubecke Oct 26, 2023
37e0fc7
Add files for new functions
skygering Oct 26, 2023
a224c87
Merge branch 'main' of github.com:Caltech-OCTO/GeometryOps.jl
LanaLubecke Oct 27, 2023
22e8616
Merge branch 'main' into ll/polygon_intersects
LanaLubecke Oct 27, 2023
413b587
deals with merge on 10_27
LanaLubecke Oct 27, 2023
92778b0
Start cleaning up geom on and in geom
skygering Oct 27, 2023
bce6381
Uncomment polygon_in_polygon
skygering Oct 27, 2023
98af38e
Add more geom in geom
skygering Oct 28, 2023
09fd1fb
Merge branch 'sg/in_on_updates' into ll/polygon_intersects
LanaLubecke Oct 30, 2023
086e9fd
adds more polygon to tests & checks if inter empty
LanaLubecke Oct 30, 2023
e83a030
Merge branch 'sg/in_on_updates' into ll/polygon_intersects
LanaLubecke Oct 30, 2023
9696296
adjusts intersects for new point_in_polygon
LanaLubecke Oct 30, 2023
6933aa6
Add more comments
LanaLubecke Nov 6, 2023
24e29fc
adds union and intersection
LanaLubecke Nov 13, 2023
301b82b
fixes difference
LanaLubecke Nov 13, 2023
fae7026
inter/diff/union working for polys with holes
LanaLubecke Nov 26, 2023
8fb2e16
Merge branch 'asinghvi17:main' into ll/polygon_intersects
LanaLubecke Dec 1, 2023
35c0d53
Moves polygon clipping functions into own folder
LanaLubecke Dec 4, 2023
98d060f
Merge branch 'main' of github.com:Caltech-OCTO/GeometryOps.jl
LanaLubecke Dec 4, 2023
f698f73
Merge branch 'main' into ll/polygon_intersects
LanaLubecke Dec 4, 2023
0cfb9f5
Merge branch 'll/polygon_intersects' of github.com:Caltech-OCTO/Geome…
LanaLubecke Dec 4, 2023
02831e7
cleans up difference and intersection tests
LanaLubecke Dec 4, 2023
c1b3dbe
fix to_points and add repeat_last argument
LanaLubecke Dec 6, 2023
e9f5a13
Changes k from 30 to 4
LanaLubecke Dec 6, 2023
6938f9b
cleans up more functions
LanaLubecke Dec 13, 2023
07ea2c8
Merge branch 'main' into ll/polygon_intersects
skygering Jan 5, 2024
6ebc453
Fix merge issues
skygering Jan 5, 2024
e8994aa
gets rid of edges_a and edges_a in build_a_list
LanaLubecke Jan 14, 2024
b1f6163
removes all edges_a and edges_b variables
LanaLubecke Jan 14, 2024
368c388
better spacing
LanaLubecke Jan 16, 2024
ef74ae3
fixes out-of-memory error
LanaLubecke Jan 22, 2024
8b00020
fixes/cleans up _flag_ent_exit helper func
LanaLubecke Jan 22, 2024
fa07923
changes alpha values to tuples
LanaLubecke Jan 22, 2024
4a02b2d
trying to fix _build_b_list2
LanaLubecke Jan 22, 2024
7720066
makes _build_b_list_2 works
LanaLubecke Jan 22, 2024
fba79ad
Remove inter_list from tracing functions
skygering Jan 23, 2024
418a1ed
Remove unneeded vectors
skygering Jan 23, 2024
5123aa2
Stylistic fixes
skygering Jan 23, 2024
8238303
Remove use of idx field for starting point
skygering Jan 23, 2024
5edbe6e
Finish updating clipping_processor
skygering Jan 24, 2024
b263870
Cleanup exterior clipping functions
skygering Jan 24, 2024
a466425
Clean up trace intersection
skygering Jan 24, 2024
c046e18
Finish swapping out all traces for helper functions
skygering Jan 24, 2024
b526d57
Cleaned up intersection hole code
skygering Jan 25, 2024
c95bb08
Major clipping cleanup done
skygering Jan 26, 2024
5055a0f
Switch return type
skygering Jan 26, 2024
49572b6
Finish documenting and changing typing
skygering Jan 26, 2024
1044280
Merge branch 'main' into ll/polygon_intersects
skygering Jan 26, 2024
d0c0e5d
Update doc tests
skygering Jan 26, 2024
e03777b
Merge branch 'main' into ll/polygon_intersects
skygering Jan 26, 2024
a468aff
Update clipping doc tests
skygering Jan 26, 2024
dd49518
Update doc test output types
skygering Jan 26, 2024
895593a
Update doc test spacing
skygering Jan 26, 2024
f69465f
Small cleanups
skygering Jan 26, 2024
2e8aa65
Remove clipping test utils
skygering Jan 26, 2024
771f9ad
commit so that I can merge Skylar's changes
LanaLubecke Jan 28, 2024
ee9e9d5
Merge branch 'll/polygon_intersects' of github.com:Caltech-OCTO/Geome…
LanaLubecke Jan 28, 2024
82a09ed
wrote nested for-loop in one line in intersection.jl
LanaLubecke Jan 28, 2024
c19814e
fixes "exists" typo
LanaLubecke Jan 28, 2024
0823d9e
changes assert to throw(ArgumentError)
LanaLubecke Jan 28, 2024
d3b9e9f
fixes doc string for intersection
LanaLubecke Jan 28, 2024
4bc4253
fixes docstring for difference
LanaLubecke Jan 29, 2024
e7257a9
Update src/methods/clipping/clipping_processor.jl
LanaLubecke Jan 29, 2024
05f866d
uses continue to avoid deep nesting
LanaLubecke Jan 29, 2024
3c13a12
uses continue to avoid deep nesting
LanaLubecke Jan 29, 2024
538daf5
adds type 'T' to union docstring
LanaLubecke Jan 29, 2024
f2dda17
fixes line 5 union docstring
LanaLubecke Jan 29, 2024
646a387
fixes and error with the continues
LanaLubecke Jan 29, 2024
b795038
makes PolyNode Immutable
LanaLubecke Jan 29, 2024
be8a819
makes _tuple_points earlier
LanaLubecke Jan 29, 2024
f0988b0
Update types
skygering Jan 30, 2024
28f7ee6
Merge branch 'll/polygon_intersects' of github.com:Caltech-OCTO/Geome…
skygering Jan 30, 2024
10bd3b5
Remove extra .gitignore files
skygering Jan 30, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/GeometryOps.jl
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ include("methods/bools.jl")
include("methods/centroid.jl")
include("methods/distance.jl")
include("methods/equals.jl")
include("methods/clipping/clipping_processor.jl")
include("methods/clipping/intersection.jl")
include("methods/clipping/difference.jl")
include("methods/clipping/union.jl")
include("methods/geom_relations/contains.jl")
include("methods/geom_relations/coveredby.jl")
include("methods/geom_relations/covers.jl")
Expand Down
291 changes: 291 additions & 0 deletions src/methods/clipping/clipping_processor.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,291 @@
# # This file contains the shared helper functions forlyNode the polygon clipping functionalities.

#= This is the struct that makes up a_list and b_list. Many values are only used if point is
an intersection point (ipt). =#
struct PolyNode{T <: AbstractFloat}
idx::Int # If ipt, index of point in a_idx_list, else 0
point::Tuple{T,T} # (x, y) values of given point
inter::Bool # If ipt, true, else 0
neighbor::Int # If ipt, index of equivalent point in a_list or b_list, else 0
ent_exit::Bool # If ipt, true if enter and false if exit, else false
fracs::Tuple{T,T} # If ipt, fractions along edges to ipt (a_frac, b_frac), else (0, 0)
end

#=
_build_ab_list(::Type{T}, poly_a, poly_b) -> (a_list, b_list, a_idx_list)

This function takes in two polygon rings and calls '_build_a_list', '_build_b_list', and
'_flag_ent_exit' in order to fully form a_list and b_list. The 'a_list' and 'b_list' that it
returns are the fully updated vectors of PolyNodes that represent the rings 'poly_a' and
'poly_b', respectively. This function also returns 'a_idx_list', which at its "ith" index
stores the index in 'a_list' at which the "ith" intersection point lies.
=#
function _build_ab_list(::Type{T}, poly_a, poly_b) where T
# Make a list for nodes of each polygon
a_list, a_idx_list = _build_a_list(T, poly_a, poly_b)
b_list = _build_b_list(T, a_idx_list, a_list, poly_b)

# Flag the entry and exits
_flag_ent_exit!(poly_b, a_list)
_flag_ent_exit!(poly_a, b_list)

return a_list, b_list, a_idx_list
end

#=
_build_a_list(::Type{T}, poly_a, poly_b) -> (a_list, a_idx_list)

This function take in two polygon rings and creates a vector of PolyNodes to represent
poly_a, including its intersection points with poly_b. The information stored in each
PolyNode is needed for clipping using the Greiner-Hormann clipping algorithm.

Note: After calling this function, a_list is not fully formed because the neighboring
indicies of the intersection points in b_list still need to be updated. Also we still have
not update the entry and exit flags for a_list.

The a_idx_list is a list of the indicies of intersection points in a_list. The value at
index i of a_idx_list is the location in a_list where the ith intersection point lies.
=#
function _build_a_list(::Type{T}, poly_a, poly_b) where T
n_a_edges = _nedge(poly_a)
a_list = Vector{PolyNode{T}}(undef, n_a_edges) # list of points in poly_a
LanaLubecke marked this conversation as resolved.
Show resolved Hide resolved
a_idx_list = Vector{Int}() # finds indices of intersection points in a_list
intr_count = 0 # number of intersection points found
a_count = 0 # number of points added to a_list
# Loop through points of poly_a
local a_pt1
for (i, a_p2) in enumerate(GI.getpoint(poly_a))
a_pt2 = (T(GI.x(a_p2)), T(GI.y(a_p2)))
if i <= 1
a_pt1 = a_pt2
continue
end
# Add the first point of the edge to the list of points in a_list
new_point = PolyNode(0, a_pt1, false, 0, false, (zero(T), zero(T)))
a_count += 1
_add!(a_list, a_count, new_point, n_a_edges)
# Find intersections with edges of poly_b
local b_pt1
prev_counter = intr_count
for (j, b_p2) in enumerate(GI.getpoint(poly_b))
b_pt2 = _tuple_point(b_p2)
if j <=1
b_pt1 = b_pt2
continue
end
int_pt, fracs = _intersection_point(T, (a_pt1, a_pt2), (b_pt1, b_pt2))
# if no intersection point, skip this edge
if !isnothing(int_pt) && all(0 .≤ fracs .≤ 1)
# Set neighbor field to b edge (j-1) to keep track of intersection
new_intr = PolyNode(intr_count, int_pt, true, j - 1, false, fracs)
a_count += 1
intr_count += 1
_add!(a_list, a_count, new_intr, n_a_edges)
push!(a_idx_list, a_count)
end
b_pt1 = b_pt2
end

# Order intersection points by placement along edge using fracs value
if prev_counter < intr_count
Δintrs = intr_count - prev_counter
inter_points = @view a_list[(a_count - Δintrs + 1):a_count]
sort!(inter_points, by = x -> x.fracs[1])
for (i, p) in enumerate(inter_points)
inter_points[i] = PolyNode(prev_counter + i, p.point, p.inter, p.neighbor, p.ent_exit, p.fracs)
end
end

a_pt1 = a_pt2
end
return a_list, a_idx_list
end

# Add value x at index i to given array - if list isn't long enough, push value to array
function _add!(arr, i, x, l = length(arr))
if i <= l
arr[i] = x
else
push!(arr, x)
end
return
end

#=
_build_b_list(::Type{T}, a_idx_list, a_list, poly_b) -> b_list

This function takes in the a_list and a_idx_list build in _build_a_list and poly_b and
creates a vector of PolyNodes to represent poly_b. The information stored in each PolyNode
is needed for clipping using the Greiner-Hormann clipping algorithm.

Note: after calling this function, b_list is not fully updated. The entry/exit flags still
need to be updated. However, the neightbor value in a_list is now updated.
=#
function _build_b_list(::Type{T}, a_idx_list, a_list, poly_b) where T
# Sort intersection points by insertion order in b_list
sort!(a_idx_list, by = x-> a_list[x].neighbor + a_list[x].fracs[2])
# Initialize needed values and lists
n_b_edges = _nedge(poly_b)
n_intr_pts = length(a_idx_list)
b_list = Vector{PolyNode{T}}(undef, n_b_edges + n_intr_pts)
intr_curr = 1
b_count = 0
# Loop over points in poly_b and add each point and intersection point
for (i, p) in enumerate(GI.getpoint(poly_b))
(i == n_b_edges + 1) && break
b_count += 1
pt = (T(GI.x(p)), T(GI.y(p)))
b_list[b_count] = PolyNode(0, pt, false, 0, false, (zero(T), zero(T)))
if intr_curr ≤ n_intr_pts
curr_idx = a_idx_list[intr_curr]
curr_node = a_list[curr_idx]
while curr_node.neighbor == i # Add all intersection points in current edge
b_count += 1
b_list[b_count] = PolyNode(curr_node.idx, curr_node.point, true, curr_idx, false, curr_node.fracs)
a_list[curr_idx] = PolyNode(curr_node.idx, curr_node.point, curr_node.inter, b_count, curr_node.ent_exit, curr_node.fracs)
curr_node = a_list[curr_idx]
intr_curr += 1
intr_curr > n_intr_pts && break
curr_idx = a_idx_list[intr_curr]
curr_node = a_list[curr_idx]
end
end
end
sort!(a_idx_list) # return a_idx_list to order of points in a_list
return b_list
end

#=
_flag_ent_exit(poly_b, a_list)

This function flags all the intersection points as either an 'entry' or 'exit' point in
relation to the given polygon.
=#
function _flag_ent_exit!(poly, pt_list)
local status
for ii in eachindex(pt_list)
if ii == 1
status = !_point_filled_curve_orientation(
pt_list[ii].point, poly;
in = true, on = false, out = false
)
elseif pt_list[ii].inter
pt_list[ii] = PolyNode(pt_list[ii].idx, pt_list[ii].point, pt_list[ii].inter, pt_list[ii].neighbor, status, pt_list[ii].fracs)
status = !status
end
end
return
end

#=
_trace_polynodes(::Type{T}, a_list, b_list, a_idx_list, f_step)::Vector{GI.Polygon}

This function takes the outputs of _build_ab_list and traces the lists to determine which
polygons are formed as described in Greiner and Hormann. The function f_step determines in
which direction the lists are traced. This function is different for intersection,
difference, and union. f_step must take in two arguments: the most recent intersection
node's entry/exit status and a boolean that is true if we are currently tracing a_list and
false if we are tracing b_list. The functions used for each clipping operation are follows:
- Intersection: (x, y) -> x ? 1 : (-1)
- Difference: (x, y) -> (x ⊻ y) ? 1 : (-1)
- Union: (x, y) -> (x ⊻ y) ? 1 : (-1)

A list of GeoInterface polygons is returned from this function.
=#
function _trace_polynodes(::Type{T}, a_list, b_list, a_idx_list, f_step) where T
n_a_pts, n_b_pts = length(a_list), length(b_list)
n_intr_pts = length(a_idx_list)
return_polys = Vector{_get_poly_type(T)}(undef, 0)
# Keep track of number of processed intersection points
processed_pts = 0
while processed_pts < n_intr_pts
curr_list, curr_npoints = a_list, n_a_pts
on_a_list = true
# Find first unprocessed intersecting point in subject polygon
processed_pts += 1
first_idx = findnext(x -> x != 0, a_idx_list, processed_pts)
idx = a_idx_list[first_idx]
a_idx_list[first_idx] = 0
start_pt = a_list[idx]

# Set first point in polygon
curr = curr_list[idx]
pt_list = [curr.point]

curr_not_start = true
while curr_not_start
step = f_step(curr.ent_exit, on_a_list)
curr_not_intr = true
while curr_not_intr
# Traverse polygon either forwards or backwards
idx += step
idx = (idx > curr_npoints) ? mod(idx, curr_npoints) : idx
idx = (idx == 0) ? curr_npoints : idx

# Get current node and add to pt_list
curr = curr_list[idx]
push!(pt_list, curr.point)
if curr.inter
# Keep track of processed intersection points
curr_not_start = curr != start_pt && curr != b_list[start_pt.neighbor]
if curr_not_start
processed_pts += 1
a_idx_list[curr.idx] = 0
end
curr_not_intr = false
end
end

# Switch to next list and next point
curr_list, curr_npoints = on_a_list ? (b_list, n_b_pts) : (a_list, n_a_pts)
on_a_list = !on_a_list
idx = curr.neighbor
curr = curr_list[idx]
end
push!(return_polys, GI.Polygon([pt_list]))
end
return return_polys
end

# Get type of polygons that will be made
# TODO: Increase type options
_get_poly_type(::Type{T}) where T =
GI.Polygon{false, false, Vector{GI.LinearRing{false, false, Vector{Tuple{T, T}}, Nothing, Nothing}}, Nothing, Nothing}

#=
_add_holes_to_polys!(::Type{T}, return_polys, hole_iterator)

The holes specified by the hole iterator are added to the polygons in the return_polys list.
If this creates more polygon, they are added to the end of the list. If this removes
polygons, they are removed from the list
=#
function _add_holes_to_polys!(::Type{T}, return_polys, hole_iterator) where T
n_polys = length(return_polys)
# Remove set of holes from all polygons
for i in 1:n_polys
n_new_per_poly = 0
for hole in hole_iterator # loop through all holes
hole_poly = GI.Polygon([hole])
# loop through all pieces of original polygon (new pieces added to end of list)
for j in Iterators.flatten((i:i, (n_polys + 1):(n_polys + n_new_per_poly)))
if !isnothing(return_polys[j])
new_polys = difference(return_polys[j], hole_poly, T; target = GI.PolygonTrait)
n_new_polys = length(new_polys)
if n_new_polys == 0 # hole covered whole polygon
return_polys[j] = nothing
else
return_polys[j] = new_polys[1] # replace original
if n_new_polys > 1 # add any extra pieces
append!(return_polys, @view new_polys[2:end])
n_new_per_poly += n_new_polys - 1
end
end
end
end
end
n_polys += n_new_per_poly
end
# Remove all polygon that were marked for removal
filter!(!isnothing, return_polys)
return
end
83 changes: 83 additions & 0 deletions src/methods/clipping/difference.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# # Difference Polygon Clipping
export difference


"""
difference(geom_a, geom_b, [T::Type]; target::Type)

Return the difference between two geometries as a list of geometries. Return an empty list
if none are found. The type of the list will be constrained as much as possible given the
input geometries. Furthermore, the user can provide a `taget` type as a keyword argument and
a list of target geometries found in the difference will be returned. The user can also
provide a float type that they would like the points of returned geometries to be.

## Example

```jldoctest
import GeoInterface as GI, GeometryOps as GO

poly1 = GI.Polygon([[[0.0, 0.0], [5.0, 5.0], [10.0, 0.0], [5.0, -5.0], [0.0, 0.0]]])
poly2 = GI.Polygon([[[3.0, 0.0], [8.0, 5.0], [13.0, 0.0], [8.0, -5.0], [3.0, 0.0]]])
diff_poly = GO.difference(poly1, poly2; target = GI.PolygonTrait)
GI.coordinates.(diff_poly)
skygering marked this conversation as resolved.
Show resolved Hide resolved

# output
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], [3.0, 0.0], [6.5, 3.5]]]
```
"""
function difference(
geom_a, geom_b, ::Type{T} = Float64; target::Type{Target} = Nothing,
) where {T <: AbstractFloat, Target <: GI.AbstractTrait}
return _difference(Target, T, GI.trait(geom_a), geom_a, GI.trait(geom_b), geom_b)
end

#= The 'difference' function returns the difference of two polygons as a list of polygons.
The algorithm to determine the difference was adapted from "Efficient clipping of efficient
polygons," by Greiner and Hormann (1998). DOI: https://doi.org/10.1145/274363.274364 =#
function _difference(
::Type{GI.PolygonTrait}, ::Type{T},
::GI.PolygonTrait, poly_a,
::GI.PolygonTrait, poly_b,
) where T
# Get the exterior of the polygons
ext_poly_a = GI.getexterior(poly_a)
ext_poly_b = GI.getexterior(poly_b)
# Find the difference of the exterior of the polygons
a_list, b_list, a_idx_list = _build_ab_list(T, ext_poly_a, ext_poly_b)
polys = _trace_polynodes(T, a_list, b_list, a_idx_list, (x, y) -> (x ⊻ y) ? 1 : (-1))
if isempty(polys)
if _point_filled_curve_orientation(b_list[1].point, ext_poly_a) == point_in
poly_a_b_hole = GI.Polygon([ext_poly_a, ext_poly_b])
push!(polys, poly_a_b_hole)
elseif _point_filled_curve_orientation(a_list[1].point, ext_poly_b) != point_in
# Two polygons don't intersect and are not contained in one another
push!(polys, GI.Polygon([ext_poly_a]))
end
end

# If the original polygons had holes, take that into account.
if GI.nhole(poly_a) != 0 || GI.nhole(poly_b) != 0
_add_holes_to_polys!(T, polys, GI.gethole(poly_a))
for hole in GI.gethole(poly_b)
new_polys = intersection(GI.Polygon([hole]), poly_a, T; target = GI.PolygonTrait)
if length(new_polys) > 0
append!(polys, new_polys)
end
end
end
return polys
end

# Many type and target combos aren't implemented
function _difference(
::Type{Target}, ::Type{T},
trait_a::GI.AbstractTrait, geom_a,
trait_b::GI.AbstractTrait, geom_b,
) where {Target, T}
@assert(
false,
"Intersection between $trait_a and $trait_b with target $Target isn't implemented yet.",
)
return nothing
end
Loading
Loading