Skip to content

Commit

Permalink
Add comments and tests
Browse files Browse the repository at this point in the history
  • Loading branch information
skygering committed Oct 25, 2023
1 parent ab211f3 commit 41eb3a1
Show file tree
Hide file tree
Showing 3 changed files with 175 additions and 39 deletions.
181 changes: 149 additions & 32 deletions src/methods/equals.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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.
=#

"""
Expand Down Expand Up @@ -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)

Expand All @@ -125,58 +141,147 @@ 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
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
Expand All @@ -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)

Expand Down
6 changes: 3 additions & 3 deletions src/try.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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)
27 changes: 23 additions & 4 deletions test/methods/equals.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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
Expand All @@ -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],
Expand All @@ -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]]],
Expand All @@ -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)
Expand Down

0 comments on commit 41eb3a1

Please sign in to comment.