From 41eb3a13e47b82c658d0fe7e1f52ef064c4b5209 Mon Sep 17 00:00:00 2001 From: Skylar Gering Date: Wed, 25 Oct 2023 13:54:06 -0700 Subject: [PATCH] Add comments and tests --- src/methods/equals.jl | 181 +++++++++++++++++++++++++++++++++-------- src/try.jl | 6 +- test/methods/equals.jl | 27 +++++- 3 files changed, 175 insertions(+), 39 deletions(-) diff --git a/src/methods/equals.jl b/src/methods/equals.jl index 7c0147f36..f4b710b81 100644 --- a/src/methods/equals.jl +++ b/src/methods/equals.jl @@ -39,9 +39,13 @@ implementation, since it's a lot less work! Note that while we need the same set of points and edges, they don't need to be provided in the same order for polygons. For for example, we need the same set points for two multipoints to be equal, but they don't have to be saved in the -same order. This requires checking every point against every other point in the -two geometries we are comparing. Additionally, geometries and multi-geometries -can be equal if the multi-geometry only includes that single geometry. +same order. The winding order also doesn't have to be the same to represent the +same geometry. This requires checking every point against every other point in +the two geometries we are comparing. Also, some geometries must be "closed" like +polygons and linear rings. These will be assumed to be closed, even if they +don't have a repeated last point explicity written in the coordinates. +Additionally, geometries and multi-geometries can be equal if the multi-geometry +only includes that single geometry. =# """ @@ -96,11 +100,23 @@ function equals(::GI.PointTrait, p1, ::GI.PointTrait, p2) return true end +""" + equals(::GI.PointTrait, p1, ::GI.MultiPointTrait, mp2)::Bool + +A point and a multipoint are equal if the multipoint is composed of a single +point that is equivalent to the given point. +""" function equals(::GI.PointTrait, p1, ::GI.MultiPointTrait, mp2) GI.npoint(mp2) == 1 || return false return equals(p1, GI.getpoint(mp2, 1)) end +""" + equals(::GI.MultiPointTrait, mp1, ::GI.PointTrait, p2)::Bool + +A point and a multipoint are equal if the multipoint is composed of a single +point that is equivalent to the given point. +""" equals(trait1::GI.MultiPointTrait, mp1, trait2::GI.PointTrait, p2) = equals(trait2, p2, trait1, mp1) @@ -125,43 +141,126 @@ function equals(::GI.MultiPointTrait, mp1, ::GI.MultiPointTrait, mp2) end """ - equals(::T, l1, ::T, l2) where {T<:GI.AbstractCurveTrait} ::Bool - -Two curves are equal if they share the same set of points going around the -curve. + _equals_curves(c1, c2, closed_type1, closed_type2)::Bool + +Two curves are equal if they share the same set of point, representing the same +geometry. Both curves must must be composed of the same set of points, however, +they do not have to wind in the same direction, or start on the same point to be +equivalent. +Inputs: + c1 first geometry + c2 second geometry + closed_type1::Bool true if c1 is closed by definition (polygon, linear ring) + closed_type2::Bool true if c2 is closed by definition (polygon, linear ring) """ -function equals( - ::Union{GI.LineTrait, GI.LineStringTrait, GI.LinearRingTrait}, l1, - ::Union{GI.LineTrait, GI.LineStringTrait, GI.LinearRingTrait}, l2, -) - # Check line lengths match - n1 = GI.npoint(l1) - n2 = GI.npoint(l2) - # TODO: do we need to account for repeated last point?? +function _equals_curves(c1, c2, closed_type1, closed_type2) + # Check if both curves are closed or not + n1 = GI.npoint(c1) + n2 = GI.npoint(c2) + c1_repeat_point = GI.getpoint(c1, 1) == GI.getpoint(c1, n1) + n2 = GI.npoint(c2) + c2_repeat_point = GI.getpoint(c2, 1) == GI.getpoint(c2, n2) + closed1 = closed_type1 || c1_repeat_point + closed2 = closed_type2 || c2_repeat_point + closed1 == closed2 || return false + # How many points in each curve + n1 -= c1_repeat_point ? 1 : 0 + n2 -= c2_repeat_point ? 1 : 0 n1 == n2 || return false - - # Find first matching point if it exists - p1 = GI.getpoint(l1, 1) - offset = nothing + n1 == 0 && return true + # Find offset between curves + jstart = nothing + p1 = GI.getpoint(c1, 1) for i in 1:n2 - if equals(p1, GI.getpoint(l2, i)) - offset = i - 1 + if equals(p1, GI.getpoint(c2, i)) + jstart = i break end end - isnothing(offset) && return false - - # Then check all points are the same wrapping around line - for i in 1:n1 - pi = GI.getpoint(l1, i) - j = i + offset - j = j <= n1 ? j : (j - n1) - pj = GI.getpoint(l2, j) - equals(pi, pj) || return false + # no point matches the first point + isnothing(jstart) && return false + # found match for only point + n1 == 1 && return true + # if isn't closed and first or last point don't match, not same curve + !closed_type1 && (jstart != 1 && jstart != n1) && return false + # Check if curves are going in same direction + i = 2 + j = jstart + 1 + j -= j > n2 ? n2 : 0 + same_direction = equals(GI.getpoint(c1, i), GI.getpoint(c2, j)) + # if only 2 points, we have already compared both + n1 == 2 && return same_direction + # Check all remaining points are the same wrapping around line + jstep = same_direction ? 1 : -1 + for i in 2:n1 + ip = GI.getpoint(c1, i) + j = jstart + (i - 1) * jstep + j += (0 < j <= n2) ? 0 : (n2 * -jstep) + jp = GI.getpoint(c2, j) + equals(ip, jp) || return false end return true end +""" + equals( + ::Union{GI.LineTrait, GI.LineStringTrait}, l1, + ::Union{GI.LineTrait, GI.LineStringTrait}, l2, + )::Bool + +Two lines/linestrings are equal if they share the same set of points going +along the curve. Note that lines/linestrings aren't closed by defintion. +""" +equals( + ::Union{GI.LineTrait, GI.LineStringTrait}, l1, + ::Union{GI.LineTrait, GI.LineStringTrait}, l2, +) = _equals_curves(l1, l2, false, false) + +""" + equals( + ::Union{GI.LineTrait, GI.LineStringTrait}, l1, + ::GI.LinearRingTrait, l2, + )::Bool + +A line/linestring and a linear ring are equal if they share the same set of +points going along the curve. Note that lines aren't closed by defintion, but +rings are, so the line must have a repeated last point to be equal +""" +equals( + ::Union{GI.LineTrait, GI.LineStringTrait}, l1, + ::GI.LinearRingTrait, l2, +) = _equals_curves(l1, l2, false, true) + +""" + equals( + ::GI.LinearRingTrait, l1, + ::Union{GI.LineTrait, GI.LineStringTrait}, l2, + )::Bool + +A linear ring and a line/linestring are equal if they share the same set of +points going along the curve. Note that lines aren't closed by defintion, but +rings are, so the line must have a repeated last point to be equal +""" +equals( + ::GI.LinearRingTrait, l1, + ::Union{GI.LineTrait, GI.LineStringTrait}, l2, +) = _equals_curves(l1, l2, true, false) + +""" + equals( + ::GI.LinearRingTrait, l1, + ::GI.LinearRingTrait, l2, + )::Bool + +Two linear rings are equal if they share the same set of points going along the +curve. Note that rings are closed by definition, so they can have, but don't +need, a repeated last point to be equal. +""" +equals( + ::GI.LinearRingTrait, l1, + ::GI.LinearRingTrait, l2, +) = _equals_curves(l1, l2, true, true) + """ equals(::GI.PolygonTrait, geom_a, ::GI.PolygonTrait, geom_b)::Bool @@ -169,14 +268,20 @@ Two polygons are equal if they share the same exterior edge and holes. """ function equals(::GI.PolygonTrait, geom_a, ::GI.PolygonTrait, geom_b) # Check if exterior is equal - equals(GI.getexterior(geom_a), GI.getexterior(geom_b)) || return false + _equals_curves( + GI.getexterior(geom_a), GI.getexterior(geom_b), + true, true, # linear rings are closed by definition + ) || return false # Check if number of holes are equal GI.nhole(geom_a) == GI.nhole(geom_b) || return false # Check if holes are equal for ihole in GI.gethole(geom_a) has_match = false for jhole in GI.gethole(geom_b) - if equals(ihole, jhole) + if _equals_curves( + ihole, jhole, + true, true, # linear rings are closed by definition + ) has_match = true break end @@ -186,11 +291,23 @@ function equals(::GI.PolygonTrait, geom_a, ::GI.PolygonTrait, geom_b) return true end +""" + equals(::GI.PolygonTrait, geom_a, ::GI.MultiPolygonTrait, geom_b)::Bool + +A polygon and a multipolygon are equal if the multipolygon is composed of a +single polygon that is equivalent to the given polygon. +""" function equals(::GI.PolygonTrait, geom_a, ::MultiPolygonTrait, geom_b) GI.npolygon(geom_b) == 1 || return false return equals(geom_a, GI.getpolygon(geom_b, 1)) end +""" + equals(::GI.MultiPolygonTrait, geom_a, ::GI.PolygonTrait, geom_b)::Bool + +A polygon and a multipolygon are equal if the multipolygon is composed of a +single polygon that is equivalent to the given polygon. +""" equals(trait_a::GI.MultiPolygonTrait, geom_a, trait_b::PolygonTrait, geom_b) = equals(trait_b, geom_b, trait_a, geom_a) diff --git a/src/try.jl b/src/try.jl index 4aecdcdeb..19b12cfe7 100644 --- a/src/try.jl +++ b/src/try.jl @@ -2,6 +2,6 @@ import GeometryOps as GO import GeoInterface as GI import LibGEOS as LG -p2 = LG.Point([0.0, 1.0]) -mp3 = LG.MultiPoint([p2]) -GO.equals(p2, mp3) +r1 = LG.LinearRing([[0.0, 0.0], [5.0, 5.0], [10.0, 0.0], [5.0, -5.0], [0.0, 0.0]]) +r2 = LG.LinearRing([[3.0, 0.0], [8.0, 5.0], [13.0, 0.0], [8.0, -5.0], [3.0, 0.0]]) +GO.equals(r1, r1) diff --git a/test/methods/equals.jl b/test/methods/equals.jl index d31adfd87..ea1eb73fb 100644 --- a/test/methods/equals.jl +++ b/test/methods/equals.jl @@ -31,19 +31,25 @@ end r1 = LG.LinearRing([[0.0, 0.0], [5.0, 5.0], [10.0, 0.0], [5.0, -5.0], [0.0, 0.0]]) r2 = LG.LinearRing([[3.0, 0.0], [8.0, 5.0], [13.0, 0.0], [8.0, -5.0], [3.0, 0.0]]) + r3 = GI.LinearRing([[3.0, 0.0], [8.0, 5.0], [13.0, 0.0], [8.0, -5.0]]) l3 = LG.LineString([[3.0, 0.0], [8.0, 5.0], [13.0, 0.0], [8.0, -5.0], [3.0, 0.0]]) # Equal rings @test GO.equals(r1, r1) == LG.equals(r1, r1) @test GO.equals(r2, r2) == LG.equals(r2, r2) + # Test equal rings without closing point + @test GO.equals(r2, r3) + @test GO.equals(r3, l3) # Different rings @test GO.equals(r1, r2) == GO.equals(r2, r1) == LG.equals(r1, r2) # Equal linear ring and line string @test GO.equals(r2, l3) == LG.equals(r2, l3) - # Equal linear ring and line + # Equal line string and line @test GO.equals(l1, GI.Line([(0.0, 0.0), (0.0, 10.0)])) end @testset "Polygons/MultiPolygons" begin + pt1 = LG.Point([0.0, 0.0]) + r1 = GI.LinearRing([(0, 0), (0, 5), (5, 5), (5, 0), (0, 0)]) p1 = GI.Polygon([[(0, 0), (0, 5), (5, 5), (5, 0), (0, 0)]]) p2 = GI.Polygon([[(1, 1), (1, 6), (6, 6), (6, 1), (1, 1)]]) p3 = LG.Polygon( @@ -65,9 +71,22 @@ end [[11.0, 1.0], [11.0, 2.0], [12.0, 2.0], [12.0, 1.0], [11.0, 1.0]] ] ) + p6 = GI.Polygon([[(6, 6), (6, 1), (1, 1), (1, 6), (6, 6)]]) + p7 = GI.Polygon([[(6, 6), (1, 6), (1, 1), (6, 1), (6, 6)]]) + p8 = GI.Polygon([[(6, 6), (1, 6), (1, 1), (6, 1)]]) + # Point and polygon aren't equal + GO.equals(pt1, p1) == LG.equals(pt1, p1) + # Linear ring and polygon aren't equal + @test GO.equals(r1, p1) == LG.equals(r1, p1) # Equal polygon @test GO.equals(p1, p1) == LG.equals(p1, p1) @test GO.equals(p2, p2) == LG.equals(p2, p2) + # Equal but offset polygons + @test GO.equals(p2, p6) == LG.equals(p2, p6) + # Equal but opposite winding orders + @test GO.equals(p2, p7) == LG.equals(p2, p7) + # Equal but without closing point (implied) + @test GO.equals(p7, p8) # Different polygons @test GO.equals(p1, p2) == LG.equals(p1, p2) # Equal polygons with holes @@ -77,7 +96,7 @@ end # Same exterior and first hole, has an extra hole @test GO.equals(p3, p5) == LG.equals(p3, p5) - p6 = LG.Polygon( + p9 = LG.Polygon( [[ [-53.57208251953125, 28.287451910503744], [-53.33038330078125, 28.29228897739706], @@ -86,7 +105,7 @@ end ]] ) # Complex polygon - @test GO.equals(p6, p6) == LG.equals(p6, p6) + @test GO.equals(p9, p9) == LG.equals(p9, p9) m1 = LG.MultiPolygon([ [[[0.0, 0.0], [0.0, 5.0], [5.0, 5.0], [5.0, 0.0], [0.0, 0.0]]], @@ -105,7 +124,7 @@ end # Equal multipolygon @test GO.equals(m1, m1) == LG.equals(m1, m1) # Equal multipolygon with different order - @test GO.equals(m1, m2) == LG.equals(m2, m2) + @test GO.equals(m2, m2) == LG.equals(m2, m2) # Equal polygon to multipolygon m3 = LG.MultiPolygon([p3]) @test GO.equals(p1, m3) == LG.equals(p1, m3)