diff --git a/src/GeometryOps.jl b/src/GeometryOps.jl index ea19f3b31..9e19dd553 100644 --- a/src/GeometryOps.jl +++ b/src/GeometryOps.jl @@ -31,6 +31,7 @@ include("methods/overlaps.jl") include("methods/within.jl") include("methods/polygonize.jl") include("methods/barycentric.jl") +include("methods/equals.jl") include("transformations/flip.jl") include("transformations/simplify.jl") diff --git a/src/methods/bools.jl b/src/methods/bools.jl index fd6cffa6f..30b8716e1 100644 --- a/src/methods/bools.jl +++ b/src/methods/bools.jl @@ -11,7 +11,8 @@ export line_on_line, line_in_polygon, polygon_in_polygon """ isclockwise(line::Union{LineString, Vector{Position}})::Bool -Take a ring and return true or false whether or not the ring is clockwise or counter-clockwise. +Take a ring and return true or false whether or not the ring is clockwise or +counter-clockwise. ## Example @@ -26,6 +27,7 @@ true ``` """ isclockwise(geom)::Bool = isclockwise(GI.trait(geom), geom) + function isclockwise(::AbstractCurveTrait, line)::Bool sum = 0.0 prev = GI.getpoint(line, 1) @@ -88,30 +90,6 @@ function isconcave(poly)::Bool return false end -equals(geo1, geo2) = _equals(trait(geo1), geo1, trait(geo2), geo2) - -_equals(::T, geo1, ::T, geo2) where T = error("Cant compare $T yet") -function _equals(::T, p1, ::T, p2) where {T<:PointTrait} - GI.ncoord(p1) == GI.ncoord(p2) || return false - GI.x(p1) == GI.x(p2) || return false - GI.y(p1) == GI.y(p2) || return false - if GI.is3d(p1) - GI.z(p1) == GI.z(p2) || return false - end - return true -end -function _equals(::T, l1, ::T, l2) where {T<:AbstractCurveTrait} - # Check line lengths match - GI.npoint(l1) == GI.npoint(l2) || return false - - # Then check all points are the same - for (p1, p2) in zip(GI.getpoint(l1), GI.getpoint(l2)) - equals(p1, p2) || return false - end - return true -end -_equals(t1, geo1, t2, geo2) = false - # """ # isparallel(line1::LineString, line2::LineString)::Bool @@ -193,6 +171,26 @@ function point_on_line(point, line; ignore_end_vertices::Bool=false)::Bool return false end +function point_on_seg(point, start, stop) + # Parse out points + x, y = GI.x(point), GI.y(point) + x1, y1 = GI.x(start), GI.y(start) + x2, y2 = GI.x(stop), GI.y(stop) + Δxl = x2 - x1 + Δyl = y2 - y1 + # Determine if point is on segment + cross = (x - x1) * Δyl - (y - y1) * Δxl + if cross == 0 # point is on line extending to infinity + # is line between endpoints + if abs(Δxl) >= abs(Δyl) # is line between endpoints + return Δxl > 0 ? x1 <= x <= x2 : x2 <= x <= x1 + else + return Δyl > 0 ? y1 <= y <= y2 : y2 <= y <= y1 + end + end + return false +end + function point_on_segment(point, (start, stop); exclude_boundary::Symbol=:none)::Bool x, y = GI.x(point), GI.y(point) x1, y1 = GI.x(start), GI.y(start) diff --git a/src/methods/centroid.jl b/src/methods/centroid.jl index 03dbc6798..6a15808d7 100644 --- a/src/methods/centroid.jl +++ b/src/methods/centroid.jl @@ -216,7 +216,7 @@ function centroid_and_area(::GI.MultiPolygonTrait, geom) xcentroid *= area ycentroid *= area # Loop over any polygons within the multipolygon - for i in 2:GI.ngeom(geom) #poly in GI.getpolygon(geom) + for i in 2:GI.ngeom(geom) # Polygon centroid and area (xpoly, ypoly), poly_area = centroid_and_area(GI.getpolygon(geom, i)) # Accumulate the area component into `area` diff --git a/src/methods/crosses.jl b/src/methods/crosses.jl index 3aa62d62e..f8a580db0 100644 --- a/src/methods/crosses.jl +++ b/src/methods/crosses.jl @@ -55,7 +55,7 @@ end function line_crosses_line(line1, line2) np2 = GI.npoint(line2) - if intersects(line1, line2; meets=MEETS_CLOSED) + if intersects(line1, line2) for i in 1:GI.npoint(line1) - 1 for j in 1:GI.npoint(line2) - 1 exclude_boundary = (j === 1 || j === np2 - 2) ? :none : :both diff --git a/src/methods/equals.jl b/src/methods/equals.jl new file mode 100644 index 000000000..568256845 --- /dev/null +++ b/src/methods/equals.jl @@ -0,0 +1,192 @@ +# # Equals + +export equals + +#= +## What is equals? + +The equals function checks if two geometries are equal. They are equal if they +share the same set of points and edges. + +To provide an example, consider these two lines: +```@example cshape +using GeometryOps +using GeometryOps.GeometryBasics +using Makie +using CairoMakie + +l1 = GI.LineString([(0.0, 0.0), (0.0, 10.0)]) +l2 = GI.LineString([(0.0, -10.0), (0.0, 3.0)]) +f, a, p = lines(GI.getpoint(l1), color = :blue) +scatter!(GI.getpoint(l1), color = :blue) +lines!(GI.getpoint(l2), color = :orange) +scatter!(GI.getpoint(l2), color = :orange) +``` +We can see that the two lines do not share a commen set of points and edges in +the plot, so they are not equal: +```@example cshape +equals(l1, l2) # returns false +``` + +## Implementation + +This is the GeoInterface-compatible implementation. + +First, we implement a wrapper method that dispatches to the correct +implementation based on the geometry trait. This is also used in the +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. +=# + +""" + equals(geom1, geom2)::Bool + +Compare two Geometries return true if they are the same geometry. + +## Examples +```jldoctest +import GeometryOps as GO, GeoInterface as GI +poly1 = GI.Polygon([[(0,0), (0,5), (5,5), (5,0), (0,0)]]) +poly2 = GI.Polygon([[(0,0), (0,5), (5,5), (5,0), (0,0)]]) + +GO.equals(poly1, poly2) +# output +true +``` +""" +equals(geom_a, geom_b) = equals( + GI.trait(geom_a), geom_a, + GI.trait(geom_b), geom_b, +) + +""" + equals(::T, geom_a, ::T, geom_b)::Bool + +Two geometries of the same type, which don't have a equals function to dispatch +off of should throw an error. +""" +equals(::T, geom_a, ::T, geom_b) where T = error("Cant compare $T yet") + +""" + equals(trait_a, geom_a, trait_b, geom_b) + +Two geometries which are not of the same type cannot be equal so they always +return false. +""" +equals(trait_a, geom_a, trait_b, geom_b) = false + +""" + equals(::GI.PointTrait, p1, ::GI.PointTrait, p2)::Bool + +Two points are the same if they have the same x and y (and z if 3D) coordinates. +""" +function equals(::GI.PointTrait, p1, ::GI.PointTrait, p2) + GI.ncoord(p1) == GI.ncoord(p2) || return false + GI.x(p1) == GI.x(p2) || return false + GI.y(p1) == GI.y(p2) || return false + if GI.is3d(p1) + GI.z(p1) == GI.z(p2) || return false + end + return true +end + +""" + equals(::GI.MultiPointTrait, mp1, ::GI.MultiPointTrait, mp2)::Bool + +Two multipoints are equal if they share the same set of points. +""" +function equals(::GI.MultiPointTrait, mp1, ::GI.MultiPointTrait, mp2) + GI.npoint(mp1) == GI.npoint(mp2) || return false + for p1 in GI.getpoint(mp1) + has_match = false # if point has a matching point in other multipoint + for p2 in GI.getpoint(mp2) + if equals(p1, p2) + has_match = true + break + end + end + has_match || return false # if no matching point, can't be equal + end + return true # all points had a match +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. +""" +function equals(::T, l1, ::T, l2) where {T<:GI.AbstractCurveTrait} + # Check line lengths match + n1 = GI.npoint(l1) + n2 = GI.npoint(l2) + # TODO: do we need to account for repeated last point?? + n1 == n2 || return false + + # Find first matching point if it exists + p1 = GI.getpoint(l1, 1) + offset = findfirst(p2 -> equals(p1, p2), GI.getpoint(l2)) + isnothing(offset) && return false + offset -= 1 + + # 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 + end + return true +end + +""" + 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 + # 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) + has_match = true + break + end + end + has_match || return false + end + return true +end + +""" + equals(::GI.PolygonTrait, geom_a, ::GI.PolygonTrait, geom_b)::Bool + +Two multipolygons are equal if they share the same set of polygons. +""" +function equals(::GI.MultiPolygonTrait, geom_a, ::GI.MultiPolygonTrait, geom_b) + # Check if same number of polygons + GI.npolygon(geom_a) == GI.npolygon(geom_b) || return false + # Check if each polygon has a matching polygon + for poly_a in GI.getpolygon(geom_a) + has_match = false + for poly_b in GI.getpolygon(geom_b) + if equals(poly_a, poly_b) + has_match = true + break + end + end + has_match || return false + end + return true +end \ No newline at end of file diff --git a/src/methods/intersects.jl b/src/methods/intersects.jl index 78f5784f1..2efaf1b78 100644 --- a/src/methods/intersects.jl +++ b/src/methods/intersects.jl @@ -52,16 +52,10 @@ intersect and _intersection_point which determines the intersection point between two line segments. =# -const MEETS_CLOSED = 0 -const MEETS_OPEN = 1 - """ - intersects(geom1, geom2; kw...)::Bool + intersects(geom1, geom2)::Bool Check if two geometries intersect, returning true if so and false otherwise. -Takes in a Int keyword meets, which can either be MEETS_OPEN (1), meaning that -only intersections through open edges where edge endpoints are not included are -recorded, versus MEETS_CLOSED (0) where edge endpoints are included. ## Example @@ -76,73 +70,78 @@ GO.intersects(line1, line2) true ``` """ -intersects(geom1, geom2; kw...) = intersects( +intersects(geom1, geom2) = intersects( GI.trait(geom1), geom1, GI.trait(geom2), - geom2; - kw... + geom2 ) """ - intersects(::GI.LineTrait, a, ::GI.LineTrait, b; meets = MEETS_OPEN)::Bool + intersects(::GI.LineTrait, a, ::GI.LineTrait, b)::Bool -Returns true if two line segments intersect and false otherwise. Line segment -endpoints are excluded in check if `meets = MEETS_OPEN` (1) and included if -`meets = MEETS_CLOSED` (0). +Returns true if two line segments intersect and false otherwise. """ -function intersects(::GI.LineTrait, a, ::GI.LineTrait, b; meets = MEETS_OPEN) +function intersects(::GI.LineTrait, a, ::GI.LineTrait, b) a1 = _tuple_point(GI.getpoint(a, 1)) a2 = _tuple_point(GI.getpoint(a, 2)) b1 = _tuple_point(GI.getpoint(b, 1)) b2 = _tuple_point(GI.getpoint(b, 2)) meet_type = ExactPredicates.meet(a1, a2, b1, b2) - return meet_type == MEETS_OPEN || meet_type == meets + return meet_type == 0 || meet_type == 1 end """ - intersects(::GI.AbstractTrait, a, ::GI.AbstractTrait, b; kw...)::Bool + intersects(::GI.AbstractTrait, a, ::GI.AbstractTrait, b)::Bool Returns true if two geometries intersect with one another and false -otherwise. For all geometries but lines, conver the geometry to a list of edges +otherwise. For all geometries but lines, convert the geometry to a list of edges and cross compare the edges for intersections. """ function intersects( - trait_a::GI.AbstractTrait, a, - trait_b::GI.AbstractTrait, b; - kw..., -) - edges_a, edges_b = map(sort! ∘ to_edges, (a, b)) - return _line_intersects(edges_a, edges_b; kw...) || - within(trait_a, a, trait_b, b) || within(trait_b, b, trait_a, a) + trait_a::GI.AbstractTrait, a_geom, + trait_b::GI.AbstractTrait, b_geom, +) edges_a, edges_b = map(sort! ∘ to_edges, (a_geom, b_geom)) + return _line_intersects(edges_a, edges_b) || + within(trait_a, a_geom, trait_b, b_geom) || + within(trait_b, b_geom, trait_a, a_geom) end """ _line_intersects( edges_a::Vector{Edge}, - edges_b::Vector{Edge}; - meets = MEETS_OPEN, + edges_b::Vector{Edge} )::Bool Returns true if there is at least one intersection between edges within the -two lists. Line segment endpoints are excluded in check if `meets = MEETS_OPEN` -(1) and included if `meets = MEETS_CLOSED` (0). +two lists of edges. """ function _line_intersects( edges_a::Vector{Edge}, - edges_b::Vector{Edge}; - meets = MEETS_OPEN, + edges_b::Vector{Edge} ) # Extents.intersects(to_extent(edges_a), to_extent(edges_b)) || return false for edge_a in edges_a for edge_b in edges_b - meet_type = ExactPredicates.meet(edge_a..., edge_b...) - (meet_type == MEETS_OPEN || meet_type == meets) && return true + _line_intersects(edge_a, edge_b) && return true end end return false end +""" + _line_intersects( + edge_a::Edge, + edge_b::Edge, + )::Bool + +Returns true if there is at least one intersection between two edges. +""" +function _line_intersects(edge_a::Edge, edge_b::Edge) + meet_type = ExactPredicates.meet(edge_a..., edge_b...) + return meet_type == 0 || meet_type == 1 +end + """ intersection(geom_a, geom_b)::Union{Tuple{::Real, ::Real}, ::Nothing} diff --git a/src/methods/overlaps.jl b/src/methods/overlaps.jl index 6d84f393b..f99b75c9d 100644 --- a/src/methods/overlaps.jl +++ b/src/methods/overlaps.jl @@ -1,17 +1,61 @@ -# # Overlap checks +# # Overlaps export overlaps -# This code checks whether geometries overlap with each other. +#= +## What is overlaps? -# It does not compute the overlap or intersection geometry. +The overlaps function checks if two geometries overlap. Two geometries can only +overlap if they have the same dimension, and if they overlap, but one is not +contained, within, or equal to the other. + +Note that this means it is impossible for a single point to overlap with a +single point and a line only overlaps with another line if only a section of +each line is colinear. + +To provide an example, consider these two lines: +```@example cshape +using GeometryOps +using GeometryOps.GeometryBasics +using Makie +using CairoMakie + +l1 = GI.LineString([(0.0, 0.0), (0.0, 10.0)]) +l2 = GI.LineString([(0.0, -10.0), (0.0, 3.0)]) +f, a, p = lines(GI.getpoint(l1), color = :blue) +scatter!(GI.getpoint(l1), color = :blue) +lines!(GI.getpoint(l2), color = :orange) +scatter!(GI.getpoint(l2), color = :orange) +``` +We can see that the two lines overlap in the plot: +```@example cshape +overlap(l1, l2) +``` + +## Implementation + +This is the GeoInterface-compatible implementation. + +First, we implement a wrapper method that dispatches to the correct +implementation based on the geometry trait. This is also used in the +implementation, since it's a lot less work! + +Note that that since only elements of the same dimension can overlap, any two +geometries with traits that are of different dimensions autmoatically can +return false. + +For geometries with the same trait dimension, we must make sure that they share +a point, an edge, or area for points, lines, and polygons/multipolygons +respectivly, without being contained. +=# """ overlaps(geom1, geom2)::Bool -Compare two Geometries of the same dimension and return true if their intersection set results in a geometry -different from both but of the same dimension. It applies to Polygon/Polygon, LineString/LineString, -Multipoint/Multipoint, MultiLineString/MultiLineString and MultiPolygon/MultiPolygon. +Compare two Geometries of the same dimension and return true if their +intersection set results in a geometry different from both but of the same +dimension. This means one geometry cannot be within or contain the other and +they cannot be equal ## Examples ```jldoctest @@ -24,28 +68,166 @@ GO.overlaps(poly1, poly2) true ``` """ -overlaps(g1, g2)::Bool = overlaps(trait(g1), g1, trait(g2), g2)::Bool -overlaps(t1::FeatureTrait, g1, t2, g2)::Bool = overlaps(GI.geometry(g1), g2) -overlaps(t1, g1, t2::FeatureTrait, g2)::Bool = overlaps(g1, geometry(g2)) -overlaps(t1::FeatureTrait, g1, t2::FeatureTrait, g2)::Bool = overlaps(geometry(g1), geometry(g2)) -overlaps(::PolygonTrait, mp, ::MultiPolygonTrait, p)::Bool = overlaps(p, mp) -function overlaps(::MultiPointTrait, g1, ::MultiPointTrait, g2)::Bool - for p1 in GI.getpoint(g1) - for p2 in GI.getpoint(g2) - equals(p1, p2) && return true +overlaps(geom1, geom2)::Bool = overlaps( + GI.trait(geom1), + geom1, + GI.trait(geom2), + geom2, +) + +""" + overlaps(::GI.AbstractTrait, geom1, ::GI.AbstractTrait, geom2)::Bool + +For any non-specified pair, all have non-matching dimensions, return false. +""" +overlaps(::GI.AbstractTrait, geom1, ::GI.AbstractTrait, geom2) = false + +""" + overlaps( + ::GI.MultiPointTrait, points1, + ::GI.MultiPointTrait, points2, + )::Bool + +If the multipoints overlap, meaning some, but not all, of the points within the +multipoints are shared, return true. +""" +function overlaps( + ::GI.MultiPointTrait, points1, + ::GI.MultiPointTrait, points2, +) + one_diff = false # assume that all the points are the same + one_same = false # assume that all points are different + for p1 in GI.getpoint(points1) + match_point = false + for p2 in GI.getpoint(points2) + if equals(p1, p2) # Point is shared + one_same = true + match_point = true + break + end + end + one_diff |= !match_point # Point isn't shared + one_same && one_diff && return true + end + return false +end + +""" + overlaps(::GI.LineTrait, line1, ::GI.LineTrait, line)::Bool + +If the lines overlap, meaning that they are colinear but each have one endpoint +outside of the other line, return true. Else false. +""" +overlaps(::GI.LineTrait, line1, ::GI.LineTrait, line) = + _overlaps((a1, a2), (b1, b2)) + +""" + overlaps( + ::Union{GI.LineStringTrait, GI.LinearRing}, line1, + ::Union{GI.LineStringTrait, GI.LinearRing}, line2, + )::Bool + +If the curves overlap, meaning that at least one edge of each curve overlaps, +return true. Else false. +""" +function overlaps( + ::Union{GI.LineStringTrait, GI.LinearRing}, line1, + ::Union{GI.LineStringTrait, GI.LinearRing}, line2, +) + edges_a, edges_b = map(sort! ∘ to_edges, (line1, line2)) + for edge_a in edges_a + for edge_b in edges_b + _overlaps(edge_a, edge_b) && return true end end + return false end -function overlaps(::PolygonTrait, g1, ::PolygonTrait, g2)::Bool - return intersects(g1, g2) + +""" + overlaps( + trait_a::GI.PolygonTrait, poly_a, + trait_b::GI.PolygonTrait, poly_b, + )::Bool + +If the two polygons intersect with one another, but are not equal, return true. +Else false. +""" +function overlaps( + trait_a::GI.PolygonTrait, poly_a, + trait_b::GI.PolygonTrait, poly_b, +) + edges_a, edges_b = map(sort! ∘ to_edges, (poly_a, poly_b)) + return _line_intersects(edges_a, edges_b) && + !equals(trait_a, poly_a, trait_b, poly_b) end -function overlaps(t1::MultiPolygonTrait, mp, t2::PolygonTrait, p1)::Bool - for p2 in GI.getgeom(mp) - overlaps(p1, p2) && return true + +""" + overlaps( + ::GI.PolygonTrait, poly1, + ::GI.MultiPolygonTrait, polys2, + )::Bool + +Return true if polygon overlaps with at least one of the polygons within the +multipolygon. Else false. +""" +function overlaps( + ::GI.PolygonTrait, poly1, + ::GI.MultiPolygonTrait, polys2, +) + for poly2 in GI.getgeom(polys2) + overlaps(poly1, poly2) && return true end + return false end -function overlaps(::MultiPolygonTrait, g1, ::MultiPolygonTrait, g2)::Bool - for p1 in GI.getgeom(g1) - overlaps(PolygonTrait(), mp, PolygonTrait(), p1) && return true + +""" + overlaps( + ::GI.MultiPolygonTrait, polys1, + ::GI.PolygonTrait, poly2, + )::Bool + +Return true if polygon overlaps with at least one of the polygons within the +multipolygon. Else false. +""" +overlaps(trait1::GI.MultiPolygonTrait, polys1, trait2::GI.PolygonTrait, poly2) = + overlaps(trait2, poly2, trait1, polys1) + +""" + overlaps( + ::GI.MultiPolygonTrait, polys1, + ::GI.MultiPolygonTrait, polys2, + )::Bool + +Return true if at least one pair of polygons from multipolygons overlap. Else +false. +""" +function overlaps( + ::GI.MultiPolygonTrait, polys1, + ::GI.MultiPolygonTrait, polys2, +) + for poly1 in GI.getgeom(polys1) + overlaps(poly1, polys2) && return true end + return false +end + +""" + _overlaps( + (a1, a2)::Edge, + (b1, b2)::Edge + )::Bool + +If the edges overlap, meaning that they are colinear but each have one endpoint +outside of the other edge, return true. Else false. +""" +function _overlaps( + (a1, a2)::Edge, + (b1, b2)::Edge +) + # meets in more than one point + on_top = ExactPredicates.meet(a1, a2, b1, b2) == 0 + # one end point is outside of other segment + a_fully_within = point_on_seg(a1, b1, b2) && point_on_seg(a2, b1, b2) + b_fully_within = point_on_seg(b1, a1, a2) && point_on_seg(b2, a1, a2) + return on_top && (!a_fully_within && !b_fully_within) end diff --git a/test/methods/bools.jl b/test/methods/bools.jl index 791f0598e..cb1ff945c 100644 --- a/test/methods/bools.jl +++ b/test/methods/bools.jl @@ -114,38 +114,4 @@ import GeometryOps as GO @test GO.crosses(GI.MultiPoint([(1, 2), (12, 12)]), GI.LineString([(1, 1), (1, 2), (1, 3), (1, 4)])) == true @test GO.crosses(GI.MultiPoint([(1, 0), (12, 12)]), GI.LineString([(1, 1), (1, 2), (1, 3), (1, 4)])) == false @test GO.crosses(GI.LineString([(-2, 2), (-4, 2)]), poly7) == false - - pl1 = GI.Polygon([[(0, 0), (0, 5), (5, 5), (5, 0), (0, 0)]]) - pl2 = GI.Polygon([[(1, 1), (1, 6), (6, 6), (6, 1), (1, 1)]]) - - @test GO.overlaps(pl1, pl2) == true - @test_throws MethodError GO.overlaps(pl1, (1, 1)) - @test_throws MethodError GO.overlaps((1, 1), pl2) - - pl3 = pl4 = GI.Polygon([[ - (-53.57208251953125, 28.287451910503744), - (-53.33038330078125, 28.29228897739706), - (-53.34136962890625, 28.430052892335723), - (-53.57208251953125, 28.287451910503744), - ]]) - @test GO.overlaps(pl3, pl4) == true # this was false before... why? - - mp1 = GI.MultiPoint([ - (-36.05712890625, 26.480407161007275), - (-35.7220458984375, 27.137368359795584), - (-35.13427734375, 26.83387451505858), - (-35.4638671875, 27.254629577800063), - (-35.5462646484375, 26.86328062676624), - (-35.3924560546875, 26.504988828743404) - ]) - mp2 = GI.MultiPoint([ - (-35.4638671875, 27.254629577800063), - (-35.5462646484375, 26.86328062676624), - (-35.3924560546875, 26.504988828743404), - (-35.2001953125, 26.12091815959972), - (-34.9969482421875, 26.455820238459893) - ]) - - @test GO.overlaps(mp1, mp2) == true - @test GO.overlaps(mp1, mp2) == GO.overlaps(mp2, mp1) end diff --git a/test/methods/equals.jl b/test/methods/equals.jl new file mode 100644 index 000000000..a0b60d6cd --- /dev/null +++ b/test/methods/equals.jl @@ -0,0 +1,104 @@ +@testset "Points/MultiPoints" begin + p1 = LG.Point([0.0, 0.0]) + p2 = LG.Point([0.0, 1.0]) + # Same points + @test GO.equals(p1, p1) + @test GO.equals(p2, p2) + # Different points + @test !GO.equals(p1, p2) + + mp1 = LG.MultiPoint([[0.0, 1.0], [2.0, 2.0]]) + mp2 = LG.MultiPoint([[0.0, 1.0], [2.0, 2.0], [3.0, 3.0]]) + # Same points + @test LG.equals(mp1, mp1) + @test LG.equals(mp2, mp2) + # Different points + @test !LG.equals(mp1, mp2) + @test !LG.equals(mp1, p1) +end + +@testset "Lines/Rings" begin + l1 = LG.LineString([[0.0, 0.0], [0.0, 10.0]]) + l2 = LG.LineString([[0.0, -10.0], [0.0, 20.0]]) + # Equal lines + @test LG.equals(l1, l1) + @test LG.equals(l2, l2) + # Different lines + @test !LG.equals(l1, l2) && !LG.equals(l2, l1) + + 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]]) + 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) + @test GO.equals(r2, r2) + # Different rings + @test !GO.equals(r1, r2) && !GO.equals(r2, r1) + # Equal linear ring and line string + @test !GO.equals(r2, l3) # TODO: should these be equal? +end + +@testset "Polygons/MultiPolygons" begin + 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( + [ + [[10.0, 0.0], [10.0, 20.0], [30.0, 20.0], [30.0, 0.0], [10.0, 0.0]], + [[15.0, 1.0], [15.0, 11.0], [25.0, 11.0], [25.0, 1.0], [15.0, 1.0]] + ] + ) + p4 = LG.Polygon( + [ + [[10.0, 0.0], [10.0, 20.0], [30.0, 20.0], [30.0, 0.0], [10.0, 0.0]], + [[16.0, 1.0], [16.0, 11.0], [25.0, 11.0], [25.0, 1.0], [16.0, 1.0]] + ] + ) + p5 = LG.Polygon( + [ + [[10.0, 0.0], [10.0, 20.0], [30.0, 20.0], [30.0, 0.0], [10.0, 0.0]], + [[15.0, 1.0], [15.0, 11.0], [25.0, 11.0], [25.0, 1.0], [15.0, 1.0]], + [[11.0, 1.0], [11.0, 2.0], [12.0, 2.0], [12.0, 1.0], [11.0, 1.0]] + ] + ) + # Equal polygon + @test GO.equals(p1, p1) + @test GO.equals(p2, p2) + # Different polygons + @test !GO.equals(p1, p2) + # Equal polygons with holes + @test GO.equals(p3, p3) + # Same exterior, different hole + @test !GO.equals(p3, p4) + # Same exterior and first hole, has an extra hole + @test !GO.equals(p3, p5) + + p3 = GI.Polygon( + [[ + [-53.57208251953125, 28.287451910503744], + [-53.33038330078125, 28.29228897739706], + [-53.34136962890625, 28.430052892335723], + [-53.57208251953125, 28.287451910503744], + ]] + ) + # Complex polygon + @test GO.equals(p3, p3) + + m1 = LG.MultiPolygon([ + [[[0.0, 0.0], [0.0, 5.0], [5.0, 5.0], [5.0, 0.0], [0.0, 0.0]]], + [ + [[10.0, 0.0], [10.0, 20.0], [30.0, 20.0], [30.0, 0.0], [10.0, 0.0]], + [[15.0, 1.0], [15.0, 11.0], [25.0, 11.0], [25.0, 1.0], [15.0, 1.0]] + ] + ]) + m2 = LG.MultiPolygon([ + [ + [[10.0, 0.0], [10.0, 20.0], [30.0, 20.0], [30.0, 0.0], [10.0, 0.0]], + [[15.0, 1.0], [15.0, 11.0], [25.0, 11.0], [25.0, 1.0], [15.0, 1.0]] + ], + [[[0.0, 0.0], [0.0, 5.0], [5.0, 5.0], [5.0, 0.0], [0.0, 0.0]]] + ]) + # Equal multipolygon + @test GO.equals(m1, m1) + # Equal multipolygon with different order + @test GO.equals(m1, m2) +end \ No newline at end of file diff --git a/test/methods/intersects.jl b/test/methods/intersects.jl index f3d35c68f..4251d45a8 100644 --- a/test/methods/intersects.jl +++ b/test/methods/intersects.jl @@ -4,29 +4,25 @@ # Test for parallel lines l1 = GI.Line([(0.0, 0.0), (2.5, 0.0)]) l2 = GI.Line([(0.0, 1.0), (2.5, 1.0)]) - @test !GO.intersects(l1, l2; meets = 0) - @test !GO.intersects(l1, l2; meets = 1) + @test !GO.intersects(l1, l2) @test isnothing(GO.intersection(l1, l2)) # Test for non-parallel lines that don't intersect l1 = GI.Line([(0.0, 0.0), (2.5, 0.0)]) l2 = GI.Line([(2.0, -3.0), (3.0, 0.0)]) - @test !GO.intersects(l1, l2; meets = 0) - @test !GO.intersects(l1, l2; meets = 1) + @test !GO.intersects(l1, l2) @test isnothing(GO.intersection(l1, l2)) # Test for lines only touching at endpoint l1 = GI.Line([(0.0, 0.0), (2.5, 0.0)]) l2 = GI.Line([(2.0, -3.0), (2.5, 0.0)]) - @test GO.intersects(l1, l2; meets = 0) - @test !GO.intersects(l1, l2; meets = 1) + @test GO.intersects(l1, l2) @test all(GO.intersection(l1, l2) .≈ (2.5, 0.0)) # Test for lines that intersect in the middle l1 = GI.Line([(0.0, 0.0), (5.0, 5.0)]) l2 = GI.Line([(0.0, 5.0), (5.0, 0.0)]) - @test GO.intersects(l1, l2; meets = 0) - @test GO.intersects(l1, l2; meets = 1) + @test GO.intersects(l1, l2) @test all(GO.intersection(l1, l2) .≈ (2.5, 2.5)) # Line string test intersects ---------------------------------------------- @@ -34,8 +30,7 @@ # Single element line strings crossing over each other l1 = LG.LineString([[5.5, 7.2], [11.2, 12.7]]) l2 = LG.LineString([[4.3, 13.3], [9.6, 8.1]]) - @test GO.intersects(l1, l2; meets = 0) - @test GO.intersects(l1, l2; meets = 1) + @test GO.intersects(l1, l2) go_inter = GO.intersection(l1, l2) lg_inter = LG.intersection(l1, l2) @test go_inter[1][1] .≈ GI.x(lg_inter) @@ -44,9 +39,7 @@ # Multi-element line strings crossing over on vertex l1 = LG.LineString([[0.0, 0.0], [2.5, 0.0], [5.0, 0.0]]) l2 = LG.LineString([[2.0, -3.0], [3.0, 0.0], [4.0, 3.0]]) - @test GO.intersects(l1, l2; meets = 0) - # TODO: Do we want this to be false? It is vertex of segment, not of whole line string - @test !GO.intersects(l1, l2; meets = 1) + @test GO.intersects(l1, l2) go_inter = GO.intersection(l1, l2) @test length(go_inter) == 1 lg_inter = LG.intersection(l1, l2) @@ -56,8 +49,7 @@ # Multi-element line strings crossing over with multiple intersections l1 = LG.LineString([[0.0, -1.0], [1.0, 1.0], [2.0, -1.0], [3.0, 1.0]]) l2 = LG.LineString([[0.0, 0.0], [1.0, 0.0], [3.0, 0.0]]) - @test GO.intersects(l1, l2; meets = 0) - @test GO.intersects(l1, l2; meets = 1) + @test GO.intersects(l1, l2) go_inter = GO.intersection(l1, l2) @test length(go_inter) == 3 lg_inter = LG.intersection(l1, l2) @@ -69,22 +61,19 @@ # Line strings far apart so extents don't overlap l1 = LG.LineString([[100.0, 0.0], [101.0, 0.0], [103.0, 0.0]]) l2 = LG.LineString([[0.0, 0.0], [1.0, 0.0], [3.0, 0.0]]) - @test !GO.intersects(l1, l2; meets = 0) - @test !GO.intersects(l1, l2; meets = 1) + @test !GO.intersects(l1, l2) @test isnothing(GO.intersection(l1, l2)) # Line strings close together that don't overlap l1 = LG.LineString([[3.0, 0.25], [5.0, 0.25], [7.0, 0.25]]) l2 = LG.LineString([[0.0, 0.0], [5.0, 10.0], [10.0, 0.0]]) - @test !GO.intersects(l1, l2; meets = 0) - @test !GO.intersects(l1, l2; meets = 1) + @test !GO.intersects(l1, l2) @test isempty(GO.intersection(l1, l2)) # Closed linear ring with open line string r1 = LG.LinearRing([[0.0, 0.0], [5.0, 5.0], [10.0, 0.0], [5.0, -5.0], [0.0, 0.0]]) l2 = LG.LineString([[0.0, -2.0], [12.0, 10.0],]) - @test GO.intersects(r1, l2; meets = 0) - @test GO.intersects(r1, l2; meets = 1) + @test GO.intersects(r1, l2) go_inter = GO.intersection(r1, l2) @test length(go_inter) == 2 lg_inter = LG.intersection(r1, l2) @@ -96,8 +85,7 @@ # Closed linear ring with closed linear ring 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.LineString([[3.0, 0.0], [8.0, 5.0], [13.0, 0.0], [8.0, -5.0], [3.0, 0.0]]) - @test GO.intersects(r1, r2; meets = 0) - @test GO.intersects(r1, r2; meets = 1) + @test GO.intersects(r1, r2) go_inter = GO.intersection(r1, r2) @test length(go_inter) == 2 lg_inter = LG.intersection(r1, r2) @@ -111,22 +99,19 @@ end # Two polygons that intersect p1 = LG.Polygon([[[0.0, 0.0], [5.0, 5.0], [10.0, 0.0], [5.0, -5.0], [0.0, 0.0]]]) p2 = LG.Polygon([[[3.0, 0.0], [8.0, 5.0], [13.0, 0.0], [8.0, -5.0], [3.0, 0.0]]]) - @test GO.intersects(p1, p2; meets = 0) - @test GO.intersects(p1, p2; meets = 1) + @test GO.intersects(p1, p2) @test all(GO.intersection_points(p1, p2) .== [(6.5, 3.5), (6.5, -3.5)]) # Two polygons that don't intersect p1 = LG.Polygon([[[0.0, 0.0], [5.0, 5.0], [10.0, 0.0], [5.0, -5.0], [0.0, 0.0]]]) p2 = LG.Polygon([[[13.0, 0.0], [18.0, 5.0], [23.0, 0.0], [18.0, -5.0], [13.0, 0.0]]]) - @test !GO.intersects(p1, p2; meets = 0) - @test !GO.intersects(p1, p2; meets = 1) + @test !GO.intersects(p1, p2) @test isnothing(GO.intersection_points(p1, p2)) # Polygon that intersects with linestring p1 = LG.Polygon([[[0.0, 0.0], [5.0, 5.0], [10.0, 0.0], [5.0, -5.0], [0.0, 0.0]]]) l2 = LG.LineString([[0.0, 0.0], [10.0, 0.0]]) - @test GO.intersects(p1, l2; meets = 0) - @test GO.intersects(p1, l2; meets = 1) + @test GO.intersects(p1, l2) GO.intersection_points(p1, l2) @test all(GO.intersection_points(p1, l2) .== [(0.0, 0.0), (10.0, 0.0)]) @@ -136,8 +121,7 @@ end [[2.0, -1.0], [2.0, 1.0], [3.0, 1.0], [3.0, -1.0], [2.0, -1.0]] ]) l2 = LG.LineString([[0.0, 0.0], [10.0, 0.0]]) - @test GO.intersects(p1, l2; meets = 0) - @test GO.intersects(p1, l2; meets = 1) + @test GO.intersects(p1, l2) @test all(GO.intersection_points(p1, l2) .== [(0.0, 0.0), (2.0, 0.0), (3.0, 0.0), (10.0, 0.0)]) # Polygon with a hole, line only within the hole @@ -146,8 +130,7 @@ end [[2.0, -1.0], [2.0, 1.0], [3.0, 1.0], [3.0, -1.0], [2.0, -1.0]] ]) l2 = LG.LineString([[2.25, 0.0], [2.75, 0.0]]) - @test !GO.intersects(p1, l2; meets = 0) - @test !GO.intersects(p1, l2; meets = 1) + @test !GO.intersects(p1, l2) @test isempty(GO.intersection_points(p1, l2)) end diff --git a/test/methods/overlaps.jl b/test/methods/overlaps.jl new file mode 100644 index 000000000..5123fd3f7 --- /dev/null +++ b/test/methods/overlaps.jl @@ -0,0 +1,105 @@ +@testset "Points/MultiPoints" begin + p1 = LG.Point([0.0, 0.0]) + p2 = LG.Point([0.0, 1.0]) + # Two points can't overlap + @test GO.overlaps(p1, p1) == LG.overlaps(p1, p2) + + mp1 = LG.MultiPoint([[0.0, 1.0], [4.0, 4.0]]) + mp2 = LG.MultiPoint([[0.0, 1.0], [2.0, 2.0]]) + mp3 = LG.MultiPoint([[0.0, 1.0], [2.0, 2.0], [3.0, 3.0]]) + # No shared points, doesn't overlap + @test GO.overlaps(p1, mp1) == LG.overlaps(p1, mp1) + # One shared point, does overlap + @test GO.overlaps(p2, mp1) == LG.overlaps(p2, mp1) + # All shared points, doesn't overlap + @test GO.overlaps(mp1, mp1) == LG.overlaps(mp1, mp1) + # Not all shared points, overlaps + @test GO.overlaps(mp1, mp2) == LG.overlaps(mp1, mp2) + # One set of points entirely inside other set, doesn't overlap + @test GO.overlaps(mp2, mp3) == LG.overlaps(mp2, mp3) + # Not all points shared, overlaps + @test GO.overlaps(mp1, mp3) == LG.overlaps(mp1, mp3) + + mp1 = LG.MultiPoint([ + [-36.05712890625, 26.480407161007275], + [-35.7220458984375, 27.137368359795584], + [-35.13427734375, 26.83387451505858], + [-35.4638671875, 27.254629577800063], + [-35.5462646484375, 26.86328062676624], + [-35.3924560546875, 26.504988828743404], + ]) + mp2 = GI.MultiPoint([ + [-35.4638671875, 27.254629577800063], + [-35.5462646484375, 26.86328062676624], + [-35.3924560546875, 26.504988828743404], + [-35.2001953125, 26.12091815959972], + [-34.9969482421875, 26.455820238459893], + ]) + # Some shared points, overlaps + @test GO.overlaps(mp1, mp2) == LG.overlaps(mp1, mp2) + @test GO.overlaps(mp1, mp2) == GO.overlaps(mp2, mp1) +end + +@testset "Lines/Rings" begin + l1 = LG.LineString([[0.0, 0.0], [0.0, 10.0]]) + l2 = LG.LineString([[0.0, -10.0], [0.0, 20.0]]) + l3 = LG.LineString([[0.0, -10.0], [0.0, 3.0]]) + l4 = LG.LineString([[5.0, -5.0], [5.0, 5.0]]) + # Line can't overlap with itself + @test GO.overlaps(l1, l1) == LG.overlaps(l1, l1) + # Line completely within other line doesn't overlap + @test GO.overlaps(l1, l2) == GO.overlaps(l2, l1) == LG.overlaps(l1, l2) + # Overlapping lines + @test GO.overlaps(l1, l3) == GO.overlaps(l3, l1) == LG.overlaps(l1, l3) + # Lines that don't touch + @test GO.overlaps(l1, l4) == LG.overlaps(l1, l4) + # Linear rings that intersect but don't overlap + r1 = LG.LinearRing([[0.0, 0.0], [0.0, 5.0], [5.0, 5.0], [5.0, 0.0], [0.0, 0.0]]) + r2 = LG.LinearRing([[1.0, 1.0], [1.0, 6.0], [6.0, 6.0], [6.0, 1.0], [1.0, 1.0]]) + @test LG.overlaps(r1, r2) == LG.overlaps(r1, r2) +end + +@testset "Polygons/MultiPolygons" begin + p1 = LG.Polygon([[[0.0, 0.0], [0.0, 5.0], [5.0, 5.0], [5.0, 0.0], [0.0, 0.0]]]) + p2 = LG.Polygon([ + [[10.0, 0.0], [10.0, 20.0], [30.0, 20.0], [30.0, 0.0], [10.0, 0.0]], + [[15.0, 1.0], [15.0, 11.0], [25.0, 11.0], [25.0, 1.0], [15.0, 1.0]] + ]) + # Test basic polygons that don't overlap + @test GO.overlaps(p1, p2) == LG.overlaps(p1, p2) + + p3 = LG.Polygon([[[1.0, 1.0], [1.0, 6.0], [6.0, 6.0], [6.0, 1.0], [1.0, 1.0]]]) + # Test basic polygons that overlap + @test GO.overlaps(p1, p3) == LG.overlaps(p1, p3) + + p4 = LG.Polygon([[[20.0, 5.0], [20.0, 10.0], [18.0, 10.0], [18.0, 5.0], [20.0, 5.0]]]) + # Test one polygon within the other + @test GO.overlaps(p2, p4) == GO.overlaps(p4, p2) == LG.overlaps(p2, p4) + + # @test_throws MethodError GO.overlaps(pl1, (1, 1)) # I think these should be false + # @test_throws MethodError GO.overlaps((1, 1), pl2) + + p5 = LG.Polygon( + [[ + [-53.57208251953125, 28.287451910503744], + [-53.33038330078125, 28.29228897739706], + [-53.34136352890625, 28.430052892335723], + [-53.57208251953125, 28.287451910503744], + ]] + ) + # Test equal polygons + @test GO.overlaps(p5, p5) == LG.overlaps(p5, p5) + + # Test multipolygons + m1 = LG.MultiPolygon([ + [[[0.0, 0.0], [0.0, 5.0], [5.0, 5.0], [5.0, 0.0], [0.0, 0.0]]], + [ + [[10.0, 0.0], [10.0, 20.0], [30.0, 20.0], [30.0, 0.0], [10.0, 0.0]], + [[15.0, 1.0], [15.0, 11.0], [25.0, 11.0], [25.0, 1.0], [15.0, 1.0]] + ] + ]) + # Test polygon that overlaps with multipolygon + @test GO.overlaps(m1, p3) == LG.overlaps(m1, p3) + # Test polygon in hole of multipolygon, doesn't overlap + @test GO.overlaps(m1, p4) == LG.overlaps(m1, p4) +end diff --git a/test/runtests.jl b/test/runtests.jl index 7c96de785..ee2065017 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -18,8 +18,10 @@ const GO = GeometryOps @testset "Barycentric coordinate operations" begin include("methods/barycentric.jl") end @testset "Bools" begin include("methods/bools.jl") end @testset "Centroid" begin include("methods/centroid.jl") end + @testset "Equals" begin include("methods/equals.jl") end @testset "Intersect" begin include("methods/intersects.jl") end @testset "Signed Area" begin include("methods/signed_area.jl") end + @testset "Overlaps" begin include("methods/overlaps.jl") end # Transformations @testset "Reproject" begin include("transformations/reproject.jl") end @testset "Flip" begin include("transformations/flip.jl") end