Skip to content

Tree based acceleration for polygon clipping / boolean ops #274

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

Open
wants to merge 18 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 1 addition & 2 deletions Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,7 @@ GeometryOpsCore = "=0.1.6"
LibGEOS = "0.9.2"
LinearAlgebra = "1"
Proj = "1"
SortTileRecursiveTree = "0.1"
StaticArrays = "1"
SortTileRecursiveTree = "0.1.2"
Statistics = "1"
TGGeometry = "0.1"
Tables = "1"
Expand Down
10 changes: 10 additions & 0 deletions src/GeometryOps.jl
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,16 @@ export TraitTarget, Manifold, Planar, Spherical, Geodesic, apply, applyreduce, f
using GeoInterface
using LinearAlgebra, Statistics

using StaticArrays

import Tables, DataAPI
import StaticArrays
import DelaunayTriangulation # for convex hull and triangulation
import ExactPredicates
import Base.@kwdef
import GeoInterface.Extents: Extents
import SortTileRecursiveTree
import SortTileRecursiveTree: STRtree

const GI = GeoInterface

Expand All @@ -43,6 +47,12 @@ include("utils/SpatialTreeInterface/SpatialTreeInterface.jl")

using .LoopStateMachine, .SpatialTreeInterface

include("utils/NaturalIndexing.jl")
using .NaturalIndexing


# Load utility modules in
using .NaturalIndexing, .SpatialTreeInterface, .LoopStateMachine

include("methods/angles.jl")
include("methods/area.jl")
Expand Down
424 changes: 354 additions & 70 deletions src/methods/clipping/clipping_processor.jl

Large diffs are not rendered by default.

24 changes: 20 additions & 4 deletions src/methods/clipping/intersection.jl
Original file line number Diff line number Diff line change
Expand Up @@ -236,18 +236,34 @@ function _intersection_points(manifold::M, accelerator::A, ::Type{T}, ::GI.Abstr
# Check if the geometries extents even overlap
Extents.intersects(GI.extent(a), GI.extent(b)) || return result
# Create a list of edges from the two input geometries
edges_a, edges_b = map(sort! ∘ to_edges, (a, b))
# edges_a, edges_b = map(sort! ∘ to_edges, (a, b))
# Loop over pairs of edges and add any unique intersection points to results
for a_edge in edges_a, b_edge in edges_b
line_orient, intr1, intr2 = _intersection_point(T, a_edge, b_edge; exact)
line_orient == line_out && continue # no intersection points
# TODO: add intersection acceleration here.

function f_on_each_maybe_intersect((a_edge, a_idx), (b_edge, b_idx))
line_orient, intr1, intr2 = _intersection_point(manifold, T, a_edge, b_edge; exact)
line_orient == line_out && return LoopStateMachine.Action(:continue) # use LoopStateMachine.Continue() to skip this edge - in this case it doesn't matter but you could use it to e.g. break once you found the first intersecting point.
pt1, _ = intr1
push!(result, pt1) # if not line_out, there is at least one intersection point
if line_orient == line_over # if line_over, there are two intersection points
pt2, _ = intr2
push!(result, pt2)
end
end

# iterate over each pair of intersecting edges only,
# calling `f_on_each_maybe_intersect` for each pair
# that may intersect.
foreach_pair_of_maybe_intersecting_edges_in_order(
manifold, accelerator,
nothing, # f_on_each_a
nothing, # f_after_each_a
f_on_each_maybe_intersect, # f_on_each_maybe_intersect
a,
b,
T
)

#= TODO: We might be able to just add unique points with checks on the α and β values
returned from `_intersection_point`, but this would be different for curves vs polygons
vs multipolygons depending on if the shape is closed. This then wouldn't allow using the
Expand Down
245 changes: 245 additions & 0 deletions src/utils/NaturalIndexing.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,245 @@
module NaturalIndexing

import GeoInterface as GI
import Extents

using ..SpatialTreeInterface

import ..GeometryOps as GO # TODO: only needed for NaturallyIndexedRing, remove when that is removed.

export NaturalIndex, NaturallyIndexedRing, prepare_naturally

"""
NaturalLevel{E <: Extents.Extent}

A level in the natural tree. Stored in a vector in [`NaturalIndex`](@ref).

- `extents` is a vector of extents of the children of the node
"""
struct NaturalLevel{E <: Extents.Extent}
extents::Vector{E} # child extents
end

Base.show(io::IO, level::NaturalLevel) = print(io, "NaturalLevel($(length(level.extents)) extents)")
Base.show(io::IO, ::MIME"text/plain", level::NaturalLevel) = Base.show(io, level)

Check warning on line 24 in src/utils/NaturalIndexing.jl

View check run for this annotation

Codecov / codecov/patch

src/utils/NaturalIndexing.jl#L23-L24

Added lines #L23 - L24 were not covered by tests

"""
NaturalIndex{E <: Extents.Extent}

A natural tree index. Stored in a vector in [`NaturalIndex`](@ref).

- `nodecapacity` is the "spread", number of children per node
- `extent` is the extent of the tree
- `levels` is a vector of [`NaturalLevel`](@ref)s
"""
struct NaturalIndex{E <: Extents.Extent}
nodecapacity::Int # "spread", number of children per node
extent::E
levels::Vector{NaturalLevel{E}}
end

Extents.extent(idx::NaturalIndex) = idx.extent

Check warning on line 41 in src/utils/NaturalIndexing.jl

View check run for this annotation

Codecov / codecov/patch

src/utils/NaturalIndexing.jl#L41

Added line #L41 was not covered by tests

function Base.show(io::IO, ::MIME"text/plain", idx::NaturalIndex)
println(io, "NaturalIndex with $(length(idx.levels)) levels and $(idx.nodecapacity) children per node")
println(io, "extent: $(idx.extent)")

Check warning on line 45 in src/utils/NaturalIndexing.jl

View check run for this annotation

Codecov / codecov/patch

src/utils/NaturalIndexing.jl#L43-L45

Added lines #L43 - L45 were not covered by tests
end
function Base.show(io::IO, idx::NaturalIndex)
println(io, "NaturalIndex($(length(idx.levels)) levels, $(idx.extent))")

Check warning on line 48 in src/utils/NaturalIndexing.jl

View check run for this annotation

Codecov / codecov/patch

src/utils/NaturalIndexing.jl#L47-L48

Added lines #L47 - L48 were not covered by tests
end

function NaturalIndex(geoms; nodecapacity = 32)
# Get the extent type initially (coord order, coord type, etc.)
# so that the construction is type stable.
e1 = GI.extent(first(geoms))
E = typeof(e1)
return NaturalIndex{E}(geoms; nodecapacity = nodecapacity)
end
function NaturalIndex(last_level_extents::Vector{E}; nodecapacity = 32) where E <: Extents.Extent
# If we are passed a vector of extents - inflate immediately!
return NaturalIndex{E}(last_level_extents; nodecapacity = nodecapacity)
end

function NaturalIndex{E}(geoms; nodecapacity = 32) where E <: Extents.Extent
# If passed a vector of geometries, and we know the type of the extent,
# then simply retrieve the extents so they can serve as the "last-level"
# extents.
# Finally, call the lowest level method that performs inflation.
last_level_extents = GI.extent.(geoms)
return NaturalIndex{E}(last_level_extents; nodecapacity = nodecapacity)
end
# This is the main constructor that performs inflation.
function NaturalIndex{E}(last_level_extents::Vector{E}; nodecapacity = 32) where E <: Extents.Extent
ngeoms = length(last_level_extents)
last_level = NaturalLevel(last_level_extents)

nlevels = _number_of_levels(nodecapacity, ngeoms)

levels = Vector{NaturalLevel{E}}(undef, nlevels)
levels[end] = last_level
# Iterate backwards, from bottom to top level,
# and build up the level extent vectors.
for level_index in (nlevels-1):(-1):1
prev_level = levels[level_index+1] # this is always instantiated, since we are iterating backwards
nrects = _number_of_keys(nodecapacity, nlevels - (level_index), ngeoms)
extents = [
begin
start = (rect_index - 1) * nodecapacity + 1
stop = min(start + nodecapacity - 1, length(prev_level.extents))
reduce(Extents.union, view(prev_level.extents, start:stop))
end
for rect_index in 1:nrects
]
levels[level_index] = NaturalLevel(extents)
end

return NaturalIndex(nodecapacity, reduce(Extents.union, levels[1].extents), levels)

end

function _number_of_keys(nodecapacity::Int, level::Int, ngeoms::Int)
return ceil(Int, ngeoms / (nodecapacity ^ (level)))
end

"""
_number_of_levels(nodecapacity::Int, ngeoms::Int)

Calculate the number of levels in a natural tree for a given number of geometries and node capacity.

## How this works

The number of keys in a level is given by `ngeoms / nodecapacity ^ level`.

The number of levels is the smallest integer such that the number of keys in the last level is 1.
So it goes - if that makes sense.
"""
function _number_of_levels(nodecapacity::Int, ngeoms::Int)
level = 1
while _number_of_keys(nodecapacity, level, ngeoms) > 1
level += 1
end
return level
end


# This is like a pointer to a node in the tree.
"""
NaturalIndexNode{E <: Extents.Extent}

A reference to a node in the natural tree. Kind of like a tree cursor.

- `parent_index` is a pointer to the parent index
- `level` is the level of the node in the tree
- `index` is the index of the node in the level
- `extent` is the extent of the node
"""
struct NaturalIndexNode{E <: Extents.Extent}
parent_index::NaturalIndex{E}
level::Int
index::Int
extent::E
end

Extents.extent(node::NaturalIndexNode) = node.extent

# What does SpatialTreeInterface require of trees?
# - Parents completely cover their children
# - `GI.extent(node)` returns `Extent`
# - can mean that `Extents.extent(node)` returns the extent of the node
# - `nchild(node)` returns the number of children of the node
# - `getchild(node)` returns an iterator over all children of the node
# - `getchild(node, i)` returns the i-th child of the node
# - `isleaf(node)` returns a boolean indicating whether the node is a leaf
# - `child_indices_extents(node)` returns an iterator over the indices and extents of the children of the node

SpatialTreeInterface.isspatialtree(::Type{<: NaturalIndex}) = true
SpatialTreeInterface.isspatialtree(::Type{<: NaturalIndexNode}) = true

Check warning on line 156 in src/utils/NaturalIndexing.jl

View check run for this annotation

Codecov / codecov/patch

src/utils/NaturalIndexing.jl#L156

Added line #L156 was not covered by tests

function SpatialTreeInterface.nchild(node::NaturalIndexNode)
start_idx = (node.index - 1) * node.parent_index.nodecapacity + 1
stop_idx = min(start_idx + node.parent_index.nodecapacity - 1, length(node.parent_index.levels[node.level+1].extents))
return stop_idx - start_idx + 1
end

function SpatialTreeInterface.getchild(node::NaturalIndexNode, i::Int)
child_index = (node.index - 1) * node.parent_index.nodecapacity + i
return NaturalIndexNode(
node.parent_index,
node.level + 1, # increment level by 1
child_index, # index of this particular child
node.parent_index.levels[node.level+1].extents[child_index] # the extent of this child
)
end

# Get all children of a node
function SpatialTreeInterface.getchild(node::NaturalIndexNode)
return (SpatialTreeInterface.getchild(node, i) for i in 1:SpatialTreeInterface.nchild(node))
end

SpatialTreeInterface.isleaf(node::NaturalIndexNode) = node.level == length(node.parent_index.levels) - 1

function SpatialTreeInterface.child_indices_extents(node::NaturalIndexNode)
start_idx = (node.index - 1) * node.parent_index.nodecapacity + 1
stop_idx = min(start_idx + node.parent_index.nodecapacity - 1, length(node.parent_index.levels[node.level+1].extents))
return ((i, node.parent_index.levels[node.level+1].extents[i]) for i in start_idx:stop_idx)
end

# implementation for "root node" / top level tree

SpatialTreeInterface.isleaf(node::NaturalIndex) = length(node.levels) == 1

SpatialTreeInterface.nchild(node::NaturalIndex) = length(node.levels[1].extents)

Check warning on line 191 in src/utils/NaturalIndexing.jl

View check run for this annotation

Codecov / codecov/patch

src/utils/NaturalIndexing.jl#L191

Added line #L191 was not covered by tests

SpatialTreeInterface.getchild(node::NaturalIndex) = SpatialTreeInterface.getchild(NaturalIndexNode(node, 0, 1, node.extent))
SpatialTreeInterface.getchild(node::NaturalIndex, i) = SpatialTreeInterface.getchild(NaturalIndexNode(node, 0, 1, node.extent), i)

Check warning on line 194 in src/utils/NaturalIndexing.jl

View check run for this annotation

Codecov / codecov/patch

src/utils/NaturalIndexing.jl#L194

Added line #L194 was not covered by tests

SpatialTreeInterface.child_indices_extents(node::NaturalIndex) = (i_ext for i_ext in enumerate(node.levels[1].extents))

"""
NaturallyIndexedRing(points; nodecapacity = 32)

A linear ring that contains a natural index.

!!! warning
This will be removed in favour of prepared geometry - the idea here
is just to test what interface works best to store things in.
"""
struct NaturallyIndexedRing
points::Vector{Tuple{Float64, Float64}}
index::NaturalIndex{Extents.Extent{(:X, :Y), NTuple{2, NTuple{2, Float64}}}}
end

function NaturallyIndexedRing(points::Vector{Tuple{Float64, Float64}}; nodecapacity = 32)
index = NaturalIndex(GO.edge_extents(GI.LinearRing(points)); nodecapacity)
return NaturallyIndexedRing(points, index)

Check warning on line 214 in src/utils/NaturalIndexing.jl

View check run for this annotation

Codecov / codecov/patch

src/utils/NaturalIndexing.jl#L212-L214

Added lines #L212 - L214 were not covered by tests
end
NaturallyIndexedRing(ring::NaturallyIndexedRing) = ring

Check warning on line 216 in src/utils/NaturalIndexing.jl

View check run for this annotation

Codecov / codecov/patch

src/utils/NaturalIndexing.jl#L216

Added line #L216 was not covered by tests

function GI.convert(::Type{NaturallyIndexedRing}, ::GI.LinearRingTrait, geom)
points = GO.tuples(geom).geom
return NaturallyIndexedRing(points)

Check warning on line 220 in src/utils/NaturalIndexing.jl

View check run for this annotation

Codecov / codecov/patch

src/utils/NaturalIndexing.jl#L218-L220

Added lines #L218 - L220 were not covered by tests
end

Base.show(io::IO, ::MIME"text/plain", ring::NaturallyIndexedRing) = Base.show(io, ring)
Base.show(io::IO, ring::NaturallyIndexedRing) = print(io, "NaturallyIndexedRing($(length(ring.points)) points) with index $(sprint(show, ring.index))")

Check warning on line 224 in src/utils/NaturalIndexing.jl

View check run for this annotation

Codecov / codecov/patch

src/utils/NaturalIndexing.jl#L223-L224

Added lines #L223 - L224 were not covered by tests

GI.ncoord(::GI.LinearRingTrait, ring::NaturallyIndexedRing) = 2
GI.is3d(::GI.LinearRingTrait, ring::NaturallyIndexedRing) = false
GI.ismeasured(::GI.LinearRingTrait, ring::NaturallyIndexedRing) = false

Check warning on line 228 in src/utils/NaturalIndexing.jl

View check run for this annotation

Codecov / codecov/patch

src/utils/NaturalIndexing.jl#L226-L228

Added lines #L226 - L228 were not covered by tests

GI.ngeom(::GI.LinearRingTrait, ring::NaturallyIndexedRing) = length(ring.points)
GI.getgeom(::GI.LinearRingTrait, ring::NaturallyIndexedRing) = ring.points
GI.getgeom(::GI.LinearRingTrait, ring::NaturallyIndexedRing, i::Int) = ring.points[i]

Check warning on line 232 in src/utils/NaturalIndexing.jl

View check run for this annotation

Codecov / codecov/patch

src/utils/NaturalIndexing.jl#L230-L232

Added lines #L230 - L232 were not covered by tests

Extents.extent(ring::NaturallyIndexedRing) = ring.index.extent

Check warning on line 234 in src/utils/NaturalIndexing.jl

View check run for this annotation

Codecov / codecov/patch

src/utils/NaturalIndexing.jl#L234

Added line #L234 was not covered by tests

GI.isgeometry(::Type{<: NaturallyIndexedRing}) = true
GI.geomtrait(::NaturallyIndexedRing) = GI.LinearRingTrait()

Check warning on line 237 in src/utils/NaturalIndexing.jl

View check run for this annotation

Codecov / codecov/patch

src/utils/NaturalIndexing.jl#L236-L237

Added lines #L236 - L237 were not covered by tests

function prepare_naturally(geom)
return GO.apply(GI.PolygonTrait(), geom) do poly
return GI.Polygon([GI.convert(NaturallyIndexedRing, GI.LinearRingTrait(), ring) for ring in GI.getring(poly)])

Check warning on line 241 in src/utils/NaturalIndexing.jl

View check run for this annotation

Codecov / codecov/patch

src/utils/NaturalIndexing.jl#L239-L241

Added lines #L239 - L241 were not covered by tests
end
end

end # module NaturalIndexing
2 changes: 1 addition & 1 deletion src/utils/SpatialTreeInterface/SpatialTreeInterface.jl
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import GeoInterface as GI
import AbstractTrees

# public isspatialtree, isleaf, getchild, nchild, child_indices_extents, node_extent
export query, do_query
export query
export FlatNoTree

# The spatial tree interface and its implementations are defined here.
Expand Down
Loading
Loading