From 20057df22a21f66f68f3235ed0a6354549da4732 Mon Sep 17 00:00:00 2001 From: Anshul Singhvi Date: Sat, 3 Aug 2024 09:12:07 -0400 Subject: [PATCH 01/11] Use lazy iterator to ensure all rings in `create_[a/b]_list` are closed --- src/methods/clipping/clipping_processor.jl | 30 +++++++++++++++++-- test/methods/clipping/polygon_clipping.jl | 35 +++++++++++++++++++++- 2 files changed, 61 insertions(+), 4 deletions(-) diff --git a/src/methods/clipping/clipping_processor.jl b/src/methods/clipping/clipping_processor.jl index e0ea5b7c2..aba26e16f 100644 --- a/src/methods/clipping/clipping_processor.jl +++ b/src/methods/clipping/clipping_processor.jl @@ -38,6 +38,30 @@ PolyNode(node::PolyNode{T}; # Checks equality of two PolyNodes by backing point value, fractional value, and intersection status equals(pn1::PolyNode, pn2::PolyNode) = pn1.point == pn2.point && pn1.inter == pn2.inter && pn1.fracs == pn2.fracs +#= + _lazy_closed_ring_point_enumerator(ring) -> Iterator + +This function returns an enumerator over the points of a ring, and adds an extra point +to the end, if the ring is not closed. +=# +function _lazy_closed_ring_point_enumerator(ring) + @assert GI.geomtrait(ring) isa GI.AbstractCurveTrait "`ring` must be a curve, got $(GI.trait(ring))" + # Check if poly_a is closed. + # NOTE: `1` is defined as the starting index of a ring in GeoInterface. + if equals(GI.getpoint(ring, 1), GI.getpoint(ring, GI.npoint(ring))) + # If yes, pass the iterator as formed + Iterators.enumerate(GI.getpoint(ring)) + else + # If no, pass the iterator as lazy vcat + Iterators.enumerate( + Iterators.flatten( + (GI.getpoint(ring), + (GI.getpoint(ring, 1),),) + ) + ) + end +end + #= _build_ab_list(::Type{T}, poly_a, poly_b, delay_cross_f, delay_bounce_f; exact) -> (a_list, b_list, a_idx_list) @@ -89,7 +113,7 @@ function _build_a_list(::Type{T}, poly_a, poly_b; exact) where T n_b_intrs = 0 # Loop through points of poly_a local a_pt1 - for (i, a_p2) in enumerate(GI.getpoint(poly_a)) + for (i, a_p2) in _lazy_closed_ring_point_enumerator(poly_a) # basically `enumerate(GI.getpoint(poly_a))` but makes sure `poly_a` is closed a_pt2 = (T(GI.x(a_p2)), T(GI.y(a_p2))) if i <= 1 || (a_pt1 == a_pt2) # don't repeat points a_pt1 = a_pt2 @@ -102,7 +126,7 @@ function _build_a_list(::Type{T}, poly_a, poly_b; exact) where T # Find intersections with edges of poly_b local b_pt1 prev_counter = a_count - for (j, b_p2) in enumerate(GI.getpoint(poly_b)) + for (j, b_p2) in _lazy_closed_ring_point_enumerator(poly_b) # basically `enumerate(GI.getpoint(poly_b))` but makes sure `poly_b` is closed b_pt2 = _tuple_point(b_p2, T) if j <= 1 || (b_pt1 == b_pt2) # don't repeat points b_pt1 = b_pt2 @@ -193,7 +217,7 @@ function _build_b_list(::Type{T}, a_idx_list, a_list, n_b_intrs, poly_b) where T b_count = 0 # Loop over points in poly_b and add each point and intersection point local b_pt1 - for (i, b_p2) in enumerate(GI.getpoint(poly_b)) + for (i, b_p2) in _lazy_closed_ring_point_enumerator(poly_b) # basically `enumerate(GI.getpoint(poly_b))` but makes sure `poly_b` is closed b_pt2 = _tuple_point(b_p2, T) if i ≤ 1 || (b_pt1 == b_pt2) # don't repeat points b_pt1 = b_pt2 diff --git a/test/methods/clipping/polygon_clipping.jl b/test/methods/clipping/polygon_clipping.jl index ccb538218..5530b797d 100644 --- a/test/methods/clipping/polygon_clipping.jl +++ b/test/methods/clipping/polygon_clipping.jl @@ -103,6 +103,8 @@ p54 = GI.Polygon([[(2.5, 2.5), (2.5, 7.5), (7.5, 7.5), (7.5, 2.5), (2.5, 2.5)], p55 = GI.Polygon([[(5.0, 0.25), (5.0, 5.0), (9.5, 5.0), (9.5, 0.25), (5.0, 0.25)], [(6.0, 3.0), (6.0, 4.0), (7.0, 4.0), (7.0, 3.0), (6.0, 3.0)], [(7.5, 0.5), (7.5, 2.5), (9.25, 2.5), (9.25, 0.5), (7.5, 0.5)]]) +p56 = GI.Polygon([[(0.0, 0.0), (0.0, 10.0), (10.0, 10.0), (10.0, 0.0)]]) # polygons with unclosed rings +p57 = GI.Polygon([[(0.0, 0.0), (0.0, 11.0), (11.0, 11.0), (11.0, 0.0)]]) mp1 = GI.MultiPolygon([p1]) mp2 = GI.MultiPolygon([p2]) @@ -157,7 +159,8 @@ test_pairs = [ (p34, mp3, "p34", "mp3", "Polygon overlaps with multipolygon, where on of the sub-polygons is equivalent"), (mp3, p35, "mp3", "p35", "Mulitpolygon where just one sub-polygon touches other polygon"), (p35, mp3, "p35", "mp3", "Polygon that touches just one of the sub-polygons of multipolygon"), - (mp4, mp3, "mp4", "mp3", "Two multipolygons, which with two sub-polygons, where some of the sub-polygons intersect and some don't") + (mp4, mp3, "mp4", "mp3", "Two multipolygons, which with two sub-polygons, where some of the sub-polygons intersect and some don't"), + # (p56, p57, "p56", "p57", "Polygons with unclosed rings (#191)"), # TODO: `difference` doesn't work yet on p56 and p57 in that order (#193) ] const ϵ = 1e-10 @@ -212,3 +215,33 @@ end @testset "Intersection" begin test_clipping(GO.intersection, LG.intersection, "intersection") end @testset "Union" begin test_clipping(GO.union, LG.union, "union") end @testset "Difference" begin test_clipping(GO.difference, LG.difference, "difference") end + + +@testset "Lazy closed ring enumerator" begin + # first test an open ring + p = GI.Polygon([[(0.0, 0.0), (0.0, 10.0), (10.0, 10.0), (10.0, 0.0)]]) + cl = collect(GO._lazy_closed_ring_point_enumerator(GI.getring(p, 1))) + @test length(cl) == 5 + @test cl[end][2] == cl[1][2] + # then test a closed ring + p2 = GI.Polygon([[(0.0, 0.0), (0.0, 10.0), (10.0, 10.0), (10.0, 0.0), (0.0, 0.0)]]) + cl2 = collect(GO._lazy_closed_ring_point_enumerator(GI.getring(p2, 1))) + @test length(cl2) == 5 + @test cl2[end][2] == cl2[1][2] + @test all(cl .== cl2) + # TODO: `difference` doesn't work yet on p56 and p57 in that order, + # so we do some tests here + p_intersection = only(GO.intersection(p56, p57; target = GI.PolygonTrait())) + p_union = only(GO.union(p56, p57; target = GI.PolygonTrait())) + @test GI.extent(p_intersection) == GI.extent(p56) + @test GI.extent(p_union) == GI.extent(p57) + @test GO.equals(p_intersection, p56) + @test GO.equals(p_union, p57) + # Notice how the order of polygons is different here + p_diff = only(GO.difference(p57, p56; target = GI.PolygonTrait())) + @test GI.extent(p_diff) == GI.extent(p57) + p_lg_diff = LG.difference(GO.fix(p57), GO.fix(p56)) + # The point orders differ so we have to run GO intersection again. + # We could test area of difference also like `compare_GO_LG_clipping` does. + @test GO.equals(p_diff, only(GO.intersection(p_diff, p_lg_diff; target = GI.PolygonTrait()))) +end \ No newline at end of file From e04e23562af3e15461bdaffa3d122fd14fabb109 Mon Sep 17 00:00:00 2001 From: Anshul Singhvi Date: Sun, 4 Aug 2024 21:00:49 -0400 Subject: [PATCH 02/11] Revert "Use lazy iterator to ensure all rings in `create_[a/b]_list` are closed" This reverts commit 20057df22a21f66f68f3235ed0a6354549da4732. --- src/methods/clipping/clipping_processor.jl | 30 ++----------------- test/methods/clipping/polygon_clipping.jl | 35 +--------------------- 2 files changed, 4 insertions(+), 61 deletions(-) diff --git a/src/methods/clipping/clipping_processor.jl b/src/methods/clipping/clipping_processor.jl index aba26e16f..e0ea5b7c2 100644 --- a/src/methods/clipping/clipping_processor.jl +++ b/src/methods/clipping/clipping_processor.jl @@ -38,30 +38,6 @@ PolyNode(node::PolyNode{T}; # Checks equality of two PolyNodes by backing point value, fractional value, and intersection status equals(pn1::PolyNode, pn2::PolyNode) = pn1.point == pn2.point && pn1.inter == pn2.inter && pn1.fracs == pn2.fracs -#= - _lazy_closed_ring_point_enumerator(ring) -> Iterator - -This function returns an enumerator over the points of a ring, and adds an extra point -to the end, if the ring is not closed. -=# -function _lazy_closed_ring_point_enumerator(ring) - @assert GI.geomtrait(ring) isa GI.AbstractCurveTrait "`ring` must be a curve, got $(GI.trait(ring))" - # Check if poly_a is closed. - # NOTE: `1` is defined as the starting index of a ring in GeoInterface. - if equals(GI.getpoint(ring, 1), GI.getpoint(ring, GI.npoint(ring))) - # If yes, pass the iterator as formed - Iterators.enumerate(GI.getpoint(ring)) - else - # If no, pass the iterator as lazy vcat - Iterators.enumerate( - Iterators.flatten( - (GI.getpoint(ring), - (GI.getpoint(ring, 1),),) - ) - ) - end -end - #= _build_ab_list(::Type{T}, poly_a, poly_b, delay_cross_f, delay_bounce_f; exact) -> (a_list, b_list, a_idx_list) @@ -113,7 +89,7 @@ function _build_a_list(::Type{T}, poly_a, poly_b; exact) where T n_b_intrs = 0 # Loop through points of poly_a local a_pt1 - for (i, a_p2) in _lazy_closed_ring_point_enumerator(poly_a) # basically `enumerate(GI.getpoint(poly_a))` but makes sure `poly_a` is closed + 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) # don't repeat points a_pt1 = a_pt2 @@ -126,7 +102,7 @@ function _build_a_list(::Type{T}, poly_a, poly_b; exact) where T # Find intersections with edges of poly_b local b_pt1 prev_counter = a_count - for (j, b_p2) in _lazy_closed_ring_point_enumerator(poly_b) # basically `enumerate(GI.getpoint(poly_b))` but makes sure `poly_b` is closed + for (j, b_p2) in enumerate(GI.getpoint(poly_b)) b_pt2 = _tuple_point(b_p2, T) if j <= 1 || (b_pt1 == b_pt2) # don't repeat points b_pt1 = b_pt2 @@ -217,7 +193,7 @@ function _build_b_list(::Type{T}, a_idx_list, a_list, n_b_intrs, poly_b) where T b_count = 0 # Loop over points in poly_b and add each point and intersection point local b_pt1 - for (i, b_p2) in _lazy_closed_ring_point_enumerator(poly_b) # basically `enumerate(GI.getpoint(poly_b))` but makes sure `poly_b` is closed + for (i, b_p2) in enumerate(GI.getpoint(poly_b)) b_pt2 = _tuple_point(b_p2, T) if i ≤ 1 || (b_pt1 == b_pt2) # don't repeat points b_pt1 = b_pt2 diff --git a/test/methods/clipping/polygon_clipping.jl b/test/methods/clipping/polygon_clipping.jl index 5530b797d..ccb538218 100644 --- a/test/methods/clipping/polygon_clipping.jl +++ b/test/methods/clipping/polygon_clipping.jl @@ -103,8 +103,6 @@ p54 = GI.Polygon([[(2.5, 2.5), (2.5, 7.5), (7.5, 7.5), (7.5, 2.5), (2.5, 2.5)], p55 = GI.Polygon([[(5.0, 0.25), (5.0, 5.0), (9.5, 5.0), (9.5, 0.25), (5.0, 0.25)], [(6.0, 3.0), (6.0, 4.0), (7.0, 4.0), (7.0, 3.0), (6.0, 3.0)], [(7.5, 0.5), (7.5, 2.5), (9.25, 2.5), (9.25, 0.5), (7.5, 0.5)]]) -p56 = GI.Polygon([[(0.0, 0.0), (0.0, 10.0), (10.0, 10.0), (10.0, 0.0)]]) # polygons with unclosed rings -p57 = GI.Polygon([[(0.0, 0.0), (0.0, 11.0), (11.0, 11.0), (11.0, 0.0)]]) mp1 = GI.MultiPolygon([p1]) mp2 = GI.MultiPolygon([p2]) @@ -159,8 +157,7 @@ test_pairs = [ (p34, mp3, "p34", "mp3", "Polygon overlaps with multipolygon, where on of the sub-polygons is equivalent"), (mp3, p35, "mp3", "p35", "Mulitpolygon where just one sub-polygon touches other polygon"), (p35, mp3, "p35", "mp3", "Polygon that touches just one of the sub-polygons of multipolygon"), - (mp4, mp3, "mp4", "mp3", "Two multipolygons, which with two sub-polygons, where some of the sub-polygons intersect and some don't"), - # (p56, p57, "p56", "p57", "Polygons with unclosed rings (#191)"), # TODO: `difference` doesn't work yet on p56 and p57 in that order (#193) + (mp4, mp3, "mp4", "mp3", "Two multipolygons, which with two sub-polygons, where some of the sub-polygons intersect and some don't") ] const ϵ = 1e-10 @@ -215,33 +212,3 @@ end @testset "Intersection" begin test_clipping(GO.intersection, LG.intersection, "intersection") end @testset "Union" begin test_clipping(GO.union, LG.union, "union") end @testset "Difference" begin test_clipping(GO.difference, LG.difference, "difference") end - - -@testset "Lazy closed ring enumerator" begin - # first test an open ring - p = GI.Polygon([[(0.0, 0.0), (0.0, 10.0), (10.0, 10.0), (10.0, 0.0)]]) - cl = collect(GO._lazy_closed_ring_point_enumerator(GI.getring(p, 1))) - @test length(cl) == 5 - @test cl[end][2] == cl[1][2] - # then test a closed ring - p2 = GI.Polygon([[(0.0, 0.0), (0.0, 10.0), (10.0, 10.0), (10.0, 0.0), (0.0, 0.0)]]) - cl2 = collect(GO._lazy_closed_ring_point_enumerator(GI.getring(p2, 1))) - @test length(cl2) == 5 - @test cl2[end][2] == cl2[1][2] - @test all(cl .== cl2) - # TODO: `difference` doesn't work yet on p56 and p57 in that order, - # so we do some tests here - p_intersection = only(GO.intersection(p56, p57; target = GI.PolygonTrait())) - p_union = only(GO.union(p56, p57; target = GI.PolygonTrait())) - @test GI.extent(p_intersection) == GI.extent(p56) - @test GI.extent(p_union) == GI.extent(p57) - @test GO.equals(p_intersection, p56) - @test GO.equals(p_union, p57) - # Notice how the order of polygons is different here - p_diff = only(GO.difference(p57, p56; target = GI.PolygonTrait())) - @test GI.extent(p_diff) == GI.extent(p57) - p_lg_diff = LG.difference(GO.fix(p57), GO.fix(p56)) - # The point orders differ so we have to run GO intersection again. - # We could test area of difference also like `compare_GO_LG_clipping` does. - @test GO.equals(p_diff, only(GO.intersection(p_diff, p_lg_diff; target = GI.PolygonTrait()))) -end \ No newline at end of file From 4ec4505f01ad94e0f4d8fef2d90a4f90c9732ab0 Mon Sep 17 00:00:00 2001 From: Anshul Singhvi Date: Sun, 4 Aug 2024 21:45:24 -0400 Subject: [PATCH 03/11] Add and implement lazy wrappers --- src/GeometryOps.jl | 1 + src/lazy_wrappers.jl | 112 +++++++++++++++++++++++++++ src/methods/clipping/difference.jl | 4 +- src/methods/clipping/intersection.jl | 4 +- src/methods/clipping/union.jl | 4 +- test/lazy_wrappers.jl | 3 + 6 files changed, 122 insertions(+), 6 deletions(-) create mode 100644 src/lazy_wrappers.jl create mode 100644 test/lazy_wrappers.jl diff --git a/src/GeometryOps.jl b/src/GeometryOps.jl index af6628c70..8bd7f9f7f 100644 --- a/src/GeometryOps.jl +++ b/src/GeometryOps.jl @@ -21,6 +21,7 @@ const Edge{T} = Tuple{TuplePoint{T},TuplePoint{T}} where T include("types.jl") include("primitives.jl") +include("lazy_wrappers.jl") include("utils.jl") include("not_implemented_yet.jl") diff --git a/src/lazy_wrappers.jl b/src/lazy_wrappers.jl new file mode 100644 index 000000000..3e127870f --- /dev/null +++ b/src/lazy_wrappers.jl @@ -0,0 +1,112 @@ +#= +# Lazy wrappers + +These wrappers lazily apply some fixes like closing rings. +=# + +abstract type AbstractLazyWrapper{GeomType} end + +struct LazyClosedRing{GeomType} <: AbstractLazyWrapper{GeomType} + ring::GeomType + function LazyClosedRing(ring) + LazyClosedRing(GI.trait(ring), ring) + end + function LazyClosedRing{GeomType}(ring::GeomType) where GeomType + new{GeomType}(ring) + end +end + + + +function LazyClosedRing(::GI.AbstractCurveTrait, ring::GeomType) where GeomType + LazyClosedRing{GeomType}(ring) +end + +# GeoInterface implementation +GI.geomtrait(::LazyClosedRing) = GI.LinearRingTrait() +GI.is3d(wrapper::LazyClosedRing) = GI.is3d(wrapper.ring) +GI.ismeasured(wrapper::LazyClosedRing) = GI.ismeasured(wrapper.ring) +GI.isclosed(::LazyClosedRing) = true + +function GI.npoint(wrapper::LazyClosedRing) + ring_npoints = GI.npoint(wrapper.ring) + if GI.getpoint(wrapper.ring, 1) == GI.getpoint(wrapper.ring, ring_npoints) + return ring_npoints + else + return ring_npoints + 1 # account for closing + end +end + +function GI.getpoint(wrapper::LazyClosedRing) + return LazyClosedRingTuplePointIterator(wrapper) +end + +function GI.getpoint(wrapper::LazyClosedRing, i::Integer) + ring_npoint = GI.npoint(wrapper.ring) + if i ≤ ring_npoint + return GI.getpoint(wrapper.ring, i) + elseif i == ring_npoint + 1 + if GI.getpoint(wrapper.ring, 1) == GI.getpoint(wrapper.ring, ring_npoint) + return throw(BoundsError(wrapper.ring, i)) + else + return GI.getpoint(wrapper.ring, 1) + end + else + return throw(BoundsError(wrapper.ring, i)) + end +end + +function tuples(wrapper::LazyClosedRing) + return collect(LazyClosedRingTuplePointIterator(wrapper)) +end + +struct LazyClosedRingTuplePointIterator{hasZ, hasM, GeomType} + ring::GeomType + closed::Bool +end + +function LazyClosedRingTuplePointIterator(ring::LazyClosedRing) + geom = ring.ring + isclosed = GI.getpoint(geom, 1) == GI.getpoint(geom, GI.npoint(geom)) + return LazyClosedRingTuplePointIterator{GI.is3d(geom), GI.ismeasured(geom), typeof(geom)}(geom, isclosed) +end + +# Base iterator interface +Base.IteratorSize(::LazyClosedRingTuplePointIterator) = Base.HasLength() +Base.length(iter::LazyClosedRingTuplePointIterator) = GI.npoint(iter.ring) + iter.closed +Base.IteratorEltype(::LazyClosedRingTuplePointIterator) = Base.HasEltype() +function Base.eltype(::LazyClosedRingTuplePointIterator{hasZ, hasM}) where {hasZ, hasM} + if !hasZ && !hasM + Tuple{Float64, Float64} + elseif hasZ ⊻ hasM + Tuple{Float64, Float64, Float64} + else # hasZ && hasM + Tuple{Float64, Float64, Float64, Float64} + end +end + +function Base.iterate(iter::LazyClosedRingTuplePointIterator) + return (GI.getpoint(iter.ring, 1), 1) +end + +function Base.iterate(iter::LazyClosedRingTuplePointIterator, state) + ring_npoint = GI.npoint(iter.ring) + if iter.closed + if state == ring_npoint + return nothing + else + return (GI.getpoint(iter.ring, state + 1), state + 1) + end + else + if state < ring_npoint + return (GI.getpoint(iter.ring, state + 1), state + 1) + elseif state == ring_npoint + return (GI.getpoint(iter.ring, 1), state + 1) + elseif state == ring_npoint + 1 + return nothing + else + throw(BoundsError(iter.ring, state)) + end + end +end + diff --git a/src/methods/clipping/difference.jl b/src/methods/clipping/difference.jl index d121e56c5..87fec005c 100644 --- a/src/methods/clipping/difference.jl +++ b/src/methods/clipping/difference.jl @@ -51,8 +51,8 @@ function _difference( exact, kwargs... ) where T # Get the exterior of the polygons - ext_a = GI.getexterior(poly_a) - ext_b = GI.getexterior(poly_b) + ext_a = LazyClosedRing(GI.getexterior(poly_a)) + ext_b = LazyClosedRing(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_a, ext_b, _diff_delay_cross_f, _diff_delay_bounce_f; exact) polys = _trace_polynodes(T, a_list, b_list, a_idx_list, _diff_step, poly_a, poly_b) diff --git a/src/methods/clipping/intersection.jl b/src/methods/clipping/intersection.jl index 5892bd968..bedae24e8 100644 --- a/src/methods/clipping/intersection.jl +++ b/src/methods/clipping/intersection.jl @@ -67,8 +67,8 @@ function _intersection( exact, kwargs..., ) where {T} # First we get the exteriors of 'poly_a' and 'poly_b' - ext_a = GI.getexterior(poly_a) - ext_b = GI.getexterior(poly_b) + ext_a = LazyClosedRing(GI.getexterior(poly_a)) + ext_b = LazyClosedRing(GI.getexterior(poly_b)) # Then we find the intersection of the exteriors a_list, b_list, a_idx_list = _build_ab_list(T, ext_a, ext_b, _inter_delay_cross_f, _inter_delay_bounce_f; exact) polys = _trace_polynodes(T, a_list, b_list, a_idx_list, _inter_step, poly_a, poly_b) diff --git a/src/methods/clipping/union.jl b/src/methods/clipping/union.jl index cdde8c286..3f5702b9a 100644 --- a/src/methods/clipping/union.jl +++ b/src/methods/clipping/union.jl @@ -51,8 +51,8 @@ function _union( exact, kwargs..., ) where T # First, I get the exteriors of the two polygons - ext_a = GI.getexterior(poly_a) - ext_b = GI.getexterior(poly_b) + ext_a = LazyClosedRing(GI.getexterior(poly_a)) + ext_b = LazyClosedRing(GI.getexterior(poly_b)) # Then, I get the union of the exteriors a_list, b_list, a_idx_list = _build_ab_list(T, ext_a, ext_b, _union_delay_cross_f, _union_delay_bounce_f; exact) polys = _trace_polynodes(T, a_list, b_list, a_idx_list, _union_step, poly_a, poly_b) diff --git a/test/lazy_wrappers.jl b/test/lazy_wrappers.jl new file mode 100644 index 000000000..cf3648673 --- /dev/null +++ b/test/lazy_wrappers.jl @@ -0,0 +1,3 @@ +# Tests for lazy wrappers +# - Test that return type is inferred +# - Test that results are correct \ No newline at end of file From d3a54c423d16b8791a7e6bc72ca6e856c13675aa Mon Sep 17 00:00:00 2001 From: Anshul Singhvi Date: Sun, 4 Aug 2024 21:45:55 -0400 Subject: [PATCH 04/11] Test lazy polygon ring enumerator --- test/methods/clipping/polygon_clipping.jl | 35 ++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/test/methods/clipping/polygon_clipping.jl b/test/methods/clipping/polygon_clipping.jl index ccb538218..5530b797d 100644 --- a/test/methods/clipping/polygon_clipping.jl +++ b/test/methods/clipping/polygon_clipping.jl @@ -103,6 +103,8 @@ p54 = GI.Polygon([[(2.5, 2.5), (2.5, 7.5), (7.5, 7.5), (7.5, 2.5), (2.5, 2.5)], p55 = GI.Polygon([[(5.0, 0.25), (5.0, 5.0), (9.5, 5.0), (9.5, 0.25), (5.0, 0.25)], [(6.0, 3.0), (6.0, 4.0), (7.0, 4.0), (7.0, 3.0), (6.0, 3.0)], [(7.5, 0.5), (7.5, 2.5), (9.25, 2.5), (9.25, 0.5), (7.5, 0.5)]]) +p56 = GI.Polygon([[(0.0, 0.0), (0.0, 10.0), (10.0, 10.0), (10.0, 0.0)]]) # polygons with unclosed rings +p57 = GI.Polygon([[(0.0, 0.0), (0.0, 11.0), (11.0, 11.0), (11.0, 0.0)]]) mp1 = GI.MultiPolygon([p1]) mp2 = GI.MultiPolygon([p2]) @@ -157,7 +159,8 @@ test_pairs = [ (p34, mp3, "p34", "mp3", "Polygon overlaps with multipolygon, where on of the sub-polygons is equivalent"), (mp3, p35, "mp3", "p35", "Mulitpolygon where just one sub-polygon touches other polygon"), (p35, mp3, "p35", "mp3", "Polygon that touches just one of the sub-polygons of multipolygon"), - (mp4, mp3, "mp4", "mp3", "Two multipolygons, which with two sub-polygons, where some of the sub-polygons intersect and some don't") + (mp4, mp3, "mp4", "mp3", "Two multipolygons, which with two sub-polygons, where some of the sub-polygons intersect and some don't"), + # (p56, p57, "p56", "p57", "Polygons with unclosed rings (#191)"), # TODO: `difference` doesn't work yet on p56 and p57 in that order (#193) ] const ϵ = 1e-10 @@ -212,3 +215,33 @@ end @testset "Intersection" begin test_clipping(GO.intersection, LG.intersection, "intersection") end @testset "Union" begin test_clipping(GO.union, LG.union, "union") end @testset "Difference" begin test_clipping(GO.difference, LG.difference, "difference") end + + +@testset "Lazy closed ring enumerator" begin + # first test an open ring + p = GI.Polygon([[(0.0, 0.0), (0.0, 10.0), (10.0, 10.0), (10.0, 0.0)]]) + cl = collect(GO._lazy_closed_ring_point_enumerator(GI.getring(p, 1))) + @test length(cl) == 5 + @test cl[end][2] == cl[1][2] + # then test a closed ring + p2 = GI.Polygon([[(0.0, 0.0), (0.0, 10.0), (10.0, 10.0), (10.0, 0.0), (0.0, 0.0)]]) + cl2 = collect(GO._lazy_closed_ring_point_enumerator(GI.getring(p2, 1))) + @test length(cl2) == 5 + @test cl2[end][2] == cl2[1][2] + @test all(cl .== cl2) + # TODO: `difference` doesn't work yet on p56 and p57 in that order, + # so we do some tests here + p_intersection = only(GO.intersection(p56, p57; target = GI.PolygonTrait())) + p_union = only(GO.union(p56, p57; target = GI.PolygonTrait())) + @test GI.extent(p_intersection) == GI.extent(p56) + @test GI.extent(p_union) == GI.extent(p57) + @test GO.equals(p_intersection, p56) + @test GO.equals(p_union, p57) + # Notice how the order of polygons is different here + p_diff = only(GO.difference(p57, p56; target = GI.PolygonTrait())) + @test GI.extent(p_diff) == GI.extent(p57) + p_lg_diff = LG.difference(GO.fix(p57), GO.fix(p56)) + # The point orders differ so we have to run GO intersection again. + # We could test area of difference also like `compare_GO_LG_clipping` does. + @test GO.equals(p_diff, only(GO.intersection(p_diff, p_lg_diff; target = GI.PolygonTrait()))) +end \ No newline at end of file From 2a4537e2e7c6b9de7a0931b7d5c89c4f021560fa Mon Sep 17 00:00:00 2001 From: Anshul Singhvi Date: Sun, 4 Aug 2024 21:49:04 -0400 Subject: [PATCH 05/11] Fix tests --- test/methods/clipping/polygon_clipping.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/methods/clipping/polygon_clipping.jl b/test/methods/clipping/polygon_clipping.jl index 5530b797d..a3824e09c 100644 --- a/test/methods/clipping/polygon_clipping.jl +++ b/test/methods/clipping/polygon_clipping.jl @@ -220,12 +220,12 @@ end @testset "Lazy closed ring enumerator" begin # first test an open ring p = GI.Polygon([[(0.0, 0.0), (0.0, 10.0), (10.0, 10.0), (10.0, 0.0)]]) - cl = collect(GO._lazy_closed_ring_point_enumerator(GI.getring(p, 1))) + cl = collect(GO.LazyClosedRingTuplePointIterator(GO.LazyClosedRing(GI.getring(p, 1)))) @test length(cl) == 5 @test cl[end][2] == cl[1][2] # then test a closed ring p2 = GI.Polygon([[(0.0, 0.0), (0.0, 10.0), (10.0, 10.0), (10.0, 0.0), (0.0, 0.0)]]) - cl2 = collect(GO._lazy_closed_ring_point_enumerator(GI.getring(p2, 1))) + cl2 = collect(GO.LazyClosedRingTuplePointIterator(GO.LazyClosedRing(GI.getring(p2, 1)))) @test length(cl2) == 5 @test cl2[end][2] == cl2[1][2] @test all(cl .== cl2) From a4d01af1e8e67b84266064591b1fead75332d287 Mon Sep 17 00:00:00 2001 From: Anshul Singhvi Date: Sun, 4 Aug 2024 22:11:10 -0400 Subject: [PATCH 06/11] Add tests for lazy wrappers --- src/lazy_wrappers.jl | 2 +- test/lazy_wrappers.jl | 78 ++++++++++++++++++++++++++++++++++++++++++- test/runtests.jl | 1 + 3 files changed, 79 insertions(+), 2 deletions(-) diff --git a/src/lazy_wrappers.jl b/src/lazy_wrappers.jl index 3e127870f..a38d3848d 100644 --- a/src/lazy_wrappers.jl +++ b/src/lazy_wrappers.jl @@ -73,7 +73,7 @@ end # Base iterator interface Base.IteratorSize(::LazyClosedRingTuplePointIterator) = Base.HasLength() -Base.length(iter::LazyClosedRingTuplePointIterator) = GI.npoint(iter.ring) + iter.closed +Base.length(iter::LazyClosedRingTuplePointIterator) = GI.npoint(iter.ring) + !iter.closed Base.IteratorEltype(::LazyClosedRingTuplePointIterator) = Base.HasEltype() function Base.eltype(::LazyClosedRingTuplePointIterator{hasZ, hasM}) where {hasZ, hasM} if !hasZ && !hasM diff --git a/test/lazy_wrappers.jl b/test/lazy_wrappers.jl index cf3648673..26c38a2d0 100644 --- a/test/lazy_wrappers.jl +++ b/test/lazy_wrappers.jl @@ -1,3 +1,79 @@ # Tests for lazy wrappers + # - Test that return type is inferred -# - Test that results are correct \ No newline at end of file +# - Test that results are correct +# - Test that lazy evaluation is performed +# - Test for proper error handling +# - Test compatibility with different input types + +using Test +using GeometryOps +using GeoInterface as GI + +@testset "LazyClosedRing" begin + # Helper function to create a simple LineString + create_linestring(closed=false) = closed ? GI.LineString([ + (0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0), (0.0, 0.0), + + ]) : GI.LineString([ + (0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0) + ]) + + @testset "Type inference" begin + ls = create_linestring() + wrapped = GO.LazyClosedRing(ls) + @inferred GI.npoint(wrapped) + @inferred GI.getpoint(wrapped, 1) + @inferred collect(GI.getpoint(wrapped)) + end + + @testset "Correctness" begin + ls = create_linestring() + wrapped = GO.LazyClosedRing(ls) + + @test GI.npoint(wrapped) == 5 + @test GI.getpoint(wrapped, 1) == (0.0, 0.0) + @test GI.getpoint(wrapped, 5) == (0.0, 0.0) + @test collect(GI.getpoint(wrapped)) == [ + (0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0), (0.0, 0.0) + ] + end + + @testset "Lazy evaluation" begin + ls = create_linestring() + wrapped = GO.LazyClosedRing(ls) + + # Check that the original linestring is not modified + @test GI.npoint(ls) == 4 + @test GI.npoint(wrapped) == 5 + end + + @testset "Error handling" begin + ls = create_linestring() + wrapped = GO.LazyClosedRing(ls) + + @test_throws BoundsError GI.getpoint(wrapped, 0) + @test_throws BoundsError GI.getpoint(wrapped, 6) + end + + @testset "Compatibility with different input types" begin + # Test with a closed LineString + closed_ls = create_linestring(true) + wrapped_closed = GO.LazyClosedRing(closed_ls) + @test GI.npoint(wrapped_closed) == 5 + @test collect(GI.getpoint(wrapped_closed)) == GI.getpoint(closed_ls) + + # Test with a 3D LineString + ls_3d = GI.LineString([(x, y, 0.0) for (x, y) in GI.getpoint(create_linestring())]) + wrapped_3d = GO.LazyClosedRing(ls_3d) + @test GI.is3d(wrapped_3d) == true + @test GI.npoint(wrapped_3d) == 5 + @test GI.getpoint(wrapped_3d, 5) == (0.0, 0.0, 0.0) + + # Test with a measured LineString + ls_measured = GI.LineString([GI.Point{false, true}(x, y, 0.0) for (x, y) in GI.getpoint(create_linestring())]) + wrapped_measured = GO.LazyClosedRing(ls_measured) + @test GI.ismeasured(wrapped_measured) == true + @test GI.npoint(wrapped_measured) == 5 + end +end \ No newline at end of file diff --git a/test/runtests.jl b/test/runtests.jl index c671a87b7..00c91dcfb 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -16,6 +16,7 @@ const GO = GeometryOps @testset "GeometryOps.jl" begin @testset "Primitives" begin include("primitives.jl") end + @testset "Lazy Wrappers" begin include("lazy_wrappers.jl") end # Methods @testset "Angles" begin include("methods/angles.jl") end @testset "Area" begin include("methods/area.jl") end From 05476c195789c5d3ab344ea28e87cbd1207537ff Mon Sep 17 00:00:00 2001 From: Anshul Singhvi Date: Sun, 4 Aug 2024 22:12:52 -0400 Subject: [PATCH 07/11] lazy support for basic iterators --- src/lazy_wrappers.jl | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/lazy_wrappers.jl b/src/lazy_wrappers.jl index a38d3848d..7d9a7dfec 100644 --- a/src/lazy_wrappers.jl +++ b/src/lazy_wrappers.jl @@ -67,6 +67,9 @@ end function LazyClosedRingTuplePointIterator(ring::LazyClosedRing) geom = ring.ring + if GI.isempty(geom) + return LazyClosedRingTuplePointIterator{GI.is3d(geom), GI.ismeasured(geom), typeof(geom)}(geom, true) + end isclosed = GI.getpoint(geom, 1) == GI.getpoint(geom, GI.npoint(geom)) return LazyClosedRingTuplePointIterator{GI.is3d(geom), GI.ismeasured(geom), typeof(geom)}(geom, isclosed) end @@ -86,7 +89,11 @@ function Base.eltype(::LazyClosedRingTuplePointIterator{hasZ, hasM}) where {hasZ end function Base.iterate(iter::LazyClosedRingTuplePointIterator) - return (GI.getpoint(iter.ring, 1), 1) + if GI.isempty(iter.ring) + return nothing + else + return (GI.getpoint(iter.ring, 1), 1) + end end function Base.iterate(iter::LazyClosedRingTuplePointIterator, state) From e93db2fd8db35ebd5c6825e8766edd6dfb2acc07 Mon Sep 17 00:00:00 2001 From: Anshul Singhvi Date: Sun, 4 Aug 2024 22:16:54 -0400 Subject: [PATCH 08/11] fix syntax error, thanks copilot --- test/lazy_wrappers.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/lazy_wrappers.jl b/test/lazy_wrappers.jl index 26c38a2d0..1f0afe023 100644 --- a/test/lazy_wrappers.jl +++ b/test/lazy_wrappers.jl @@ -8,7 +8,7 @@ using Test using GeometryOps -using GeoInterface as GI +import GeoInterface as GI, GeometryOps as GO @testset "LazyClosedRing" begin # Helper function to create a simple LineString From a753bdfca08a379611bb845291b449efd9dfdfb9 Mon Sep 17 00:00:00 2001 From: Anshul Singhvi Date: Mon, 5 Aug 2024 06:32:06 -0400 Subject: [PATCH 09/11] Add a basic test for the lazy point iterator --- test/lazy_wrappers.jl | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/test/lazy_wrappers.jl b/test/lazy_wrappers.jl index 1f0afe023..48e480961 100644 --- a/test/lazy_wrappers.jl +++ b/test/lazy_wrappers.jl @@ -76,4 +76,12 @@ import GeoInterface as GI, GeometryOps as GO @test GI.ismeasured(wrapped_measured) == true @test GI.npoint(wrapped_measured) == 5 end +end + +@testset "LazyClosedRingTuplePointIterator" begin + @testset "Type inference" begin + ls = create_linestring() + wrapped = GO.LazyClosedRing(ls) + @inferred Tuple{Float64, Float64} eltype(GO.LazyClosedRingTuplePointIterator(wrapped)) + end end \ No newline at end of file From 47dcd14b4f68cae3e22aee7df10d905ca0d69755 Mon Sep 17 00:00:00 2001 From: Anshul Singhvi Date: Mon, 5 Aug 2024 09:17:45 -0400 Subject: [PATCH 10/11] Fix tests --- test/lazy_wrappers.jl | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/test/lazy_wrappers.jl b/test/lazy_wrappers.jl index 48e480961..f292a1e6f 100644 --- a/test/lazy_wrappers.jl +++ b/test/lazy_wrappers.jl @@ -79,6 +79,13 @@ import GeoInterface as GI, GeometryOps as GO end @testset "LazyClosedRingTuplePointIterator" begin + # Helper function to create a simple LineString + create_linestring(closed=false) = closed ? GI.LineString([ + (0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0), (0.0, 0.0), + + ]) : GI.LineString([ + (0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0) + ]) @testset "Type inference" begin ls = create_linestring() wrapped = GO.LazyClosedRing(ls) From 26c57131a090dd0f2991f97bf1d54252eec0602c Mon Sep 17 00:00:00 2001 From: Anshul Singhvi Date: Mon, 5 Aug 2024 13:18:33 -0400 Subject: [PATCH 11/11] Update lazy_wrappers.jl --- test/lazy_wrappers.jl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/lazy_wrappers.jl b/test/lazy_wrappers.jl index f292a1e6f..7802a82de 100644 --- a/test/lazy_wrappers.jl +++ b/test/lazy_wrappers.jl @@ -9,6 +9,7 @@ using Test using GeometryOps import GeoInterface as GI, GeometryOps as GO +using ..TestHelpers @testset "LazyClosedRing" begin # Helper function to create a simple LineString @@ -91,4 +92,4 @@ end wrapped = GO.LazyClosedRing(ls) @inferred Tuple{Float64, Float64} eltype(GO.LazyClosedRingTuplePointIterator(wrapped)) end -end \ No newline at end of file +end