diff --git a/Project.toml b/Project.toml index 318c474d8..f6787b264 100644 --- a/Project.toml +++ b/Project.toml @@ -26,7 +26,4 @@ Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" [targets] -test = [ - "ArchGDAL", "Distributions", "GeoFormatTypes", "GeoJSON", "LibGEOS", - "Random", "Test", -] +test = ["ArchGDAL", "Distributions", "GeoFormatTypes", "GeoJSON", "LibGEOS", "Random", "Test"] 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 aac4f8075..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) @@ -265,63 +263,66 @@ function point_in_polygon( end # Then check the point is inside the exterior ring - point_in_polygon(point, GI.getexterior(poly); ignore_boundary, check_extent=false) || return false + point_in_polygon( + point,GI.getexterior(poly); + ignore_boundary, check_extent=false, + ) || return false # Finally make sure the point is not in any of the holes, # flipping the boundary condition for ring in GI.gethole(poly) - point_in_polygon(point, ring; ignore_boundary=!ignore_boundary) && return false + point_in_polygon( + point, ring; + ignore_boundary=!ignore_boundary, + ) && return false end return true end + function point_in_polygon( ::PointTrait, pt, ::Union{LineStringTrait,LinearRingTrait}, ring; ignore_boundary::Bool=false, check_extent::Bool=false, )::Bool + x, y = GI.x(pt), GI.y(pt) # Cheaply check that the point is inside the ring extent if check_extent point_in_extent(point, GI.extent(ring)) || return false end - # Then check the point is inside the ring inside = false n = GI.npoint(ring) p_start = GI.getpoint(ring, 1) p_end = GI.getpoint(ring, n) - - # Handle closed on non-closed rings - l = if GI.x(p_start) == GI.x(p_end) && GI.y(p_start) == GI.y(p_end) - l = n - 1 - else - n + # Handle closed vs opne rings + if GI.x(p_start) == GI.x(p_end) && GI.y(p_start) == GI.y(p_end) + n -= 1 end - # Loop over all points in the ring - for i in 1:l - 1 - j = i + 1 - + for i in 1:(n - 1) + # First point on edge p_i = GI.getpoint(ring, i) - p_j = GI.getpoint(ring, j) - xi = GI.x(p_i) - yi = GI.y(p_i) - xj = GI.x(p_j) - yj = GI.y(p_j) - - on_boundary = (GI.y(pt) * (xi - xj) + yi * (xj - GI.x(pt)) + yj * (GI.x(pt) - xi) == 0) && - ((xi - GI.x(pt)) * (xj - GI.x(pt)) <= 0) && ((yi - GI.y(pt)) * (yj - GI.y(pt)) <= 0) - + xi, yi = GI.x(p_i), GI.y(p_i) + # Second point on edge (j = i + 1) + p_j = GI.getpoint(ring, i + 1) + xj, yj = GI.x(p_j), GI.y(p_j) + # Check if point is on the ring boundary + on_boundary = ( # vertex to point has same slope as edge + yi * (xj - x) + yj * (x - xi) == y * (xj - xi) && + (xi - x) * (xj - x) <= 0 && # x is between xi and xj + (yi - y) * (yj - y) <= 0 # y is between yi and yj + ) on_boundary && return !ignore_boundary - - intersects = ((yi > GI.y(pt)) !== (yj > GI.y(pt))) && - (GI.x(pt) < (xj - xi) * (GI.y(pt) - yi) / (yj - yi) + xi) - + # Check if ray from point passes through edge + intersects = ( + (yi > y) !== (yj > y) && + (x < (xj - xi) * (y - yi) / (yj - yi) + xi) + ) if intersects inside = !inside end end - return inside end @@ -341,6 +342,7 @@ function line_on_line(t1::GI.AbstractCurveTrait, line1, t2::AbstractCurveTrait, end line_in_polygon(line, poly) = line_in_polygon(trait(line), line, trait(poly), poly) + function line_in_polygon( ::AbstractCurveTrait, line, ::Union{AbstractPolygonTrait,LinearRingTrait}, poly @@ -365,19 +367,19 @@ function line_in_polygon( end function polygon_in_polygon(poly1, poly2) - # edges1, edges2 = to_edges(poly1), to_edges(poly2) - # extent1, extent2 = to_extent(edges1), to_extent(edges2) - # Check the extents intersect - Extents.intersects(GI.extent(poly1), GI.extent(poly2)) || return false - - # Check all points in poly1 are in poly2 - for point in GI.getpoint(poly1) - point_in_polygon(point, poly2) || return false - end + # edges1, edges2 = to_edges(poly1), to_edges(poly2) + # extent1, extent2 = to_extent(edges1), to_extent(edges2) + # Check the extents intersect + Extents.intersects(GI.extent(poly1), GI.extent(poly2)) || return false + + # Check all points in poly1 are in poly2 + for point in GI.getpoint(poly1) + point_in_polygon(point, poly2) || return false + end - # Check the line of poly1 does not intersect the line of poly2 - line_intersects(poly1, poly2) && return false + # Check the line of poly1 does not intersect the line of poly2 + #intersects(poly1, poly2) && return false - # poly1 must be in poly2 - return true + # poly1 must be in poly2 + return true end 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 7c215c857..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 line_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 @@ -71,7 +71,7 @@ end function line_crosses_poly(line, poly) for l in flatten(AbstractCurveTrait, poly) - line_intersects(line, l) && return true + intersects(line, l) && return true end return false end diff --git a/src/methods/disjoint.jl b/src/methods/disjoint.jl index b51e5ab66..02b7d4e46 100644 --- a/src/methods/disjoint.jl +++ b/src/methods/disjoint.jl @@ -38,5 +38,5 @@ function polygon_disjoint(poly1, poly2) for point in GI.getpoint(poly2) point_in_polygon(point, poly1) && return false end - return !line_intersects(poly1, poly2) + return !intersects(poly1, poly2) end diff --git a/src/methods/equals.jl b/src/methods/equals.jl new file mode 100644 index 000000000..f4b710b81 --- /dev/null +++ b/src/methods/equals.jl @@ -0,0 +1,334 @@ +# # 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 define the same shape. + +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. 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. +=# + +""" + 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.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) + +""" + 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_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_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 + 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(c2, i)) + jstart = i + break + end + end + # 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_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_curves( + ihole, jhole, + true, true, # linear rings are closed by definition + ) + has_match = true + break + end + end + has_match || return false + end + 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) + +""" + 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 e0476bd81..2efaf1b78 100644 --- a/src/methods/intersects.jl +++ b/src/methods/intersects.jl @@ -1,21 +1,61 @@ # # Intersection checks -export intersects, intersection +export intersects, intersection, intersection_points -# This code checks whether geometries intersect with each other. +#= +## What is `intersects` vs `intersection` vs `intersection_points`? -# !!! note -# This does not compute intersections, only checks if they exist. +The `intersects` methods check whether two geometries intersect with each other. +The `intersection` methods return the geometry intersection between the two +input geometries. The `intersection_points` method returns a list of +intersection points between two geometries. -const MEETS_OPEN = 1 -const MEETS_CLOSED = 0 +The `intersects` methods will always return a Boolean. However, note that the +`intersection` methods will not all return the same type. For example, the +intersection of two lines will be a point in most cases, unless the lines are +parallel. On the other hand, the intersection of two polygons will be another +polygon in most cases. Finally, the `intersection_points` method returns a list +of tuple points. -""" - line_intersects(line_a, line_b) +To provide an example, consider these two lines: +```@example intersects_intersection +using GeometryOps +using GeometryOps.GeometryBasics +using Makie +using CairoMakie +point1, point2 = Point(124.584961,-12.768946), Point(126.738281,-17.224758) +point3, point4 = Point(123.354492,-15.961329), Point(127.22168,-14.008696) +line1 = Line(point1, point2) +line2 = Line(point3, point4) +f, a, p = lines([point1, point2]) +lines!([point3, point4]) +``` +We can see that they intersect, so we expect intersects to return true, and we +can visualize the intersection point in red. +```@example intersects_intersection +int_bool = GO.intersects(line1, line2) +println(int_bool) +int_point = GO.intersection(line1, line2) +scatter!(int_point, color = :red) +f +``` -Check if `line_a` intersects with `line_b`. +## Implementation -These can be `LineTrait`, `LineStringTrait` or `LinearRingTrait` +This is the GeoInterface-compatible implementation. + +First, we implement a wrapper method for intersects, intersection, and +intersection_points that dispatches to the correct implementation based on the +geometry trait. The two underlying helper functions that are widely used in all +geometry dispatches are _line_intersects, which determines if two line segments +intersect and _intersection_point which determines the intersection point +between two line segments. +=# + +""" + intersects(geom1, geom2)::Bool + +Check if two geometries intersect, returning true if so and false otherwise. ## Example @@ -24,41 +64,90 @@ import GeoInterface as GI, GeometryOps as GO line1 = GI.Line([(124.584961,-12.768946), (126.738281,-17.224758)]) line2 = GI.Line([(123.354492,-15.961329), (127.22168,-14.008696)]) -GO.line_intersects(line1, line2) +GO.intersects(line1, line2) # output true ``` """ -line_intersects(a, b; kw...) = line_intersects(trait(a), a, trait(b), b; kw...) -# Skip to_edges for LineTrait -function line_intersects(::GI.LineTrait, a, ::GI.LineTrait, b; meets=MEETS_OPEN) +intersects(geom1, geom2) = intersects( + GI.trait(geom1), + geom1, + GI.trait(geom2), + geom2 +) + +""" + intersects(::GI.LineTrait, a, ::GI.LineTrait, b)::Bool + +Returns true if two line segments intersect and false otherwise. +""" +function intersects(::GI.LineTrait, a, ::GI.LineTrait, b) a1 = _tuple_point(GI.getpoint(a, 1)) - b1 = _tuple_point(GI.getpoint(b, 1)) a2 = _tuple_point(GI.getpoint(a, 2)) + b1 = _tuple_point(GI.getpoint(b, 1)) b2 = _tuple_point(GI.getpoint(b, 2)) - return ExactPredicates.meet(a1, a2, b1, b2) == meets + meet_type = ExactPredicates.meet(a1, a2, b1, b2) + return meet_type == 0 || meet_type == 1 end -function line_intersects(::GI.AbstractTrait, a, ::GI.AbstractTrait, b; kw...) - edges_a, edges_b = map(sort! ∘ to_edges, (a, b)) - return line_intersects(edges_a, edges_b; kw...) + +""" + 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, convert the geometry to a list of edges +and cross compare the edges for intersections. +""" +function intersects( + 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 -function line_intersects(edges_a::Vector{Edge}, edges_b::Vector{Edge}; meets=MEETS_OPEN) + +""" + _line_intersects( + edges_a::Vector{Edge}, + edges_b::Vector{Edge} + )::Bool + +Returns true if there is at least one intersection between edges within the +two lists of edges. +""" +function _line_intersects( + edges_a::Vector{Edge}, + 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 - ExactPredicates.meet(edge_a..., edge_b...) == meets && return true + _line_intersects(edge_a, edge_b) && return true end end return false end """ - line_intersection(line_a, line_b) + _line_intersects( + edge_a::Edge, + edge_b::Edge, + )::Bool -Find a point that intersects LineStrings with two coordinates each. +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} -Returns `nothing` if no point is found. +Return an intersection point between two geometries. Return nothing if none are +found. Else, the return type depends on the input. It will be a union between: +a point, a line, a linear ring, a polygon, or a multipolygon ## Example @@ -67,59 +156,193 @@ import GeoInterface as GI, GeometryOps as GO line1 = GI.Line([(124.584961,-12.768946), (126.738281,-17.224758)]) line2 = GI.Line([(123.354492,-15.961329), (127.22168,-14.008696)]) -GO.line_intersection(line1, line2) +GO.intersection(line1, line2) # output (125.58375366067547, -14.83572303404496) ``` """ -line_intersection(line_a, line_b) = line_intersection(trait(line_a), line_a, trait(line_b), line_b) -function line_intersection(::GI.AbstractTrait, a, ::GI.AbstractTrait, b) - Extents.intersects(GI.extent(a), GI.extent(b)) || return nothing - result = Tuple{Float64,Float64}[] - edges_a, edges_b = map(sort! ∘ to_edges, (a, b)) - for edge_a in edges_a - for edge_b in edges_b - x = _line_intersection(edge_a, edge_b) - isnothing(x) || push!(result, x) - end - end - return result -end -function line_intersection(::GI.LineTrait, line_a, ::GI.LineTrait, line_b) +intersection(geom_a, geom_b) = + intersection(GI.trait(geom_a), geom_a, GI.trait(geom_b), geom_b) + +""" + intersection( + ::GI.LineTrait, line_a, + ::GI.LineTrait, line_b, + )::Union{ + ::Tuple{::Real, ::Real}, + ::Nothing + } + +Calculates the intersection between two line segments. Return nothing if +there isn't one. +""" +function intersection(::GI.LineTrait, line_a, ::GI.LineTrait, line_b) + # Get start and end points for both lines a1 = GI.getpoint(line_a, 1) - b1 = GI.getpoint(line_b, 1) a2 = GI.getpoint(line_a, 2) + b1 = GI.getpoint(line_b, 1) b2 = GI.getpoint(line_b, 2) + # Determine the intersection point + point, fracs = _intersection_point((a1, a2), (b1, b2)) + # Determine if intersection point is on line segments + if !isnothing(point) && 0 <= fracs[1] <= 1 && 0 <= fracs[2] <= 1 + return point + end + return nothing +end + +intersection( + trait_a::Union{GI.LineStringTrait, GI.LinearRingTrait}, + geom_a, + trait_b::Union{GI.LineStringTrait, GI.LinearRingTrait}, + geom_b, +) = intersection_points(trait_a, geom_a, trait_b, geom_b) + +""" + intersection( + ::GI.PolygonTrait, poly_a, + ::GI.PolygonTrait, poly_b, + )::Union{ + ::Vector{Vector{Tuple{::Real, ::Real}}}, # is this a good return type? + ::Nothing + } + +Calculates the intersection between two line segments. Return nothing if +there isn't one. +""" +function intersection(::GI.PolygonTrait, poly_a, ::GI.PolygonTrait, poly_b) + @assert false "Polygon intersection isn't implemented yet." + return nothing +end - return _line_intersection((a1, a2), (b1, b2)) +""" + intersection( + ::GI.AbstractTrait, geom_a, + ::GI.AbstractTrait, geom_b, + )::Union{ + ::Vector{Vector{Tuple{::Real, ::Real}}}, # is this a good return type? + ::Nothing + } + +Calculates the intersection between two line segments. Return nothing if +there isn't one. +""" +function intersection( + trait_a::GI.AbstractTrait, geom_a, + trait_b::GI.AbstractTrait, geom_b, +) + @assert( + false, + "Intersection between $trait_a and $trait_b isn't implemented yet.", + ) + return nothing end -function _line_intersection((p11, p12)::Tuple, (p21, p22)::Tuple) - # Get points from lines - x1, y1 = GI.x(p11), GI.y(p11) - x2, y2 = GI.x(p12), GI.y(p12) - x3, y3 = GI.x(p21), GI.y(p21) - x4, y4 = GI.x(p22), GI.y(p22) - - d = ((y4 - y3) * (x2 - x1)) - ((x4 - x3) * (y2 - y1)) - a = ((x4 - x3) * (y1 - y3)) - ((y4 - y3) * (x1 - x3)) - b = ((x2 - x1) * (y1 - y3)) - ((y2 - y1) * (x1 - x3)) - - if d == 0 - if a == 0 && b == 0 - return nothing + +""" + intersection_points( + geom_a, + geom_b, + )::Union{ + ::Vector{::Tuple{::Real, ::Real}}, + ::Nothing, + } + +Return a list of intersection points between two geometries. If no intersection +point was possible given geometry extents, return nothing. If none are found, +return an empty list. +""" +intersection_points(geom_a, geom_b) = + intersection_points(GI.trait(geom_a), geom_a, GI.trait(geom_b), geom_b) + +""" + intersection_points( + ::GI.AbstractTrait, geom_a, + ::GI.AbstractTrait, geom_b, + )::Union{ + ::Vector{::Tuple{::Real, ::Real}}, + ::Nothing, + } + +Calculates the list of intersection points between two geometries, inlcuding +line segments, line strings, linear rings, polygons, and multipolygons. If no +intersection points were possible given geometry extents, return nothing. If +none are found, return an empty list. +""" +function intersection_points(::GI.AbstractTrait, a, ::GI.AbstractTrait, b) + # Check if the geometries extents even overlap + Extents.intersects(GI.extent(a), GI.extent(b)) || return nothing + # Create a list of edges from the two input geometries + edges_a, edges_b = map(sort! ∘ to_edges, (a, b)) + npoints_a, npoints_b = length(edges_a), length(edges_b) + a_closed = npoints_a > 1 && edges_a[1][1] == edges_a[end][1] + b_closed = npoints_b > 1 && edges_b[1][1] == edges_b[end][1] + if npoints_a > 0 && npoints_b > 0 + # Initialize an empty list of points + T = typeof(edges_a[1][1][1]) # x-coordinate of first point in first edge + result = Tuple{T,T}[] + # Loop over pairs of edges and add any intersection points to results + for i in eachindex(edges_a) + for j in eachindex(edges_b) + point, fracs = _intersection_point(edges_a[i], edges_b[j]) + if !isnothing(point) + #= + Determine if point is on edge (all edge endpoints excluded + except for the last edge for an open geometry) + =# + α, β = fracs + on_a_edge = (!a_closed && i == npoints_a && 0 <= α <= 1) || + (0 <= α < 1) + on_b_edge = (!b_closed && j == npoints_b && 0 <= β <= 1) || + (0 <= β < 1) + if on_a_edge && on_b_edge + push!(result, point) + end + end + end end - return nothing + return result end + return nothing +end + +""" + _intersection_point( + (a1, a2)::Tuple, + (b1, b2)::Tuple, + ) - ã = a / d - b̃ = b / d +Calculates the intersection point between two lines if it exists, and as if the +line extended to infinity, and the fractional component of each line from the +initial end point to the intersection point. +Inputs: + (a1, a2)::Tuple{Tuple{::Real, ::Real}, Tuple{::Real, ::Real}} first line + (b1, b2)::Tuple{Tuple{::Real, ::Real}, Tuple{::Real, ::Real}} second line +Outputs: + (x, y)::Tuple{::Real, ::Real} intersection point + (t, u)::Tuple{::Real, ::Real} fractional length of lines to intersection + Both are ::Nothing if point doesn't exist! - if ã >= 0 && ã <= 1 && b̃ >= 0 && b̃ <= 1 - x = x1 + (ã * (x2 - x1)) - y = y1 + (ã * (y2 - y1)) - return (x, y) +Calculation derivation can be found here: + https://stackoverflow.com/questions/563198/ +""" +function _intersection_point((a1, a2)::Tuple, (b1, b2)::Tuple) + # First line runs from p to p + r + px, py = GI.x(a1), GI.y(a1) + rx, ry = GI.x(a2) - px, GI.y(a2) - py + # Second line runs from q to q + s + qx, qy = GI.x(b1), GI.y(b1) + sx, sy = GI.x(b2) - qx, GI.y(b2) - qy + # Intersection will be where p + tr = q + us where 0 < t, u < 1 and + r_cross_s = rx * sy - ry * sx + if r_cross_s != 0 + Δqp_x = qx - px + Δqp_y = qy - py + t = (Δqp_x * sy - Δqp_y * sx) / r_cross_s + u = (Δqp_x * ry - Δqp_y * rx) / r_cross_s + x = px + t * rx + y = py + t * ry + return (x, y), (t, u) end - - return nothing + return nothing, nothing end diff --git a/src/methods/overlaps.jl b/src/methods/overlaps.jl index b846e43de..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 line_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, thp2) && 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/src/methods/within.jl b/src/methods/within.jl index c930ce62f..16366f944 100644 --- a/src/methods/within.jl +++ b/src/methods/within.jl @@ -23,11 +23,21 @@ GO.within(point, line) true ``` """ +# Syntactic sugar within(g1, g2)::Bool = within(trait(g1), g1, trait(g2), g2)::Bool within(::GI.FeatureTrait, g1, ::Any, g2)::Bool = within(GI.geometry(g1), g2) -within(::Any, g1, t2::GI.FeatureTrait, g2)::Bool = within(g1, geometry(g2)) +within(::Any, g1, t2::GI.FeatureTrait, g2)::Bool = within(g1, GI.geometry(g2)) +# Points in geometries within(::GI.PointTrait, g1, ::GI.LineStringTrait, g2)::Bool = point_on_line(g1, g2; ignore_end_vertices=true) +within(::GI.PointTrait, g1, ::GI.LinearRingTrait, g2)::Bool = point_on_line(g1, g2; ignore_end_vertices=true) within(::GI.PointTrait, g1, ::GI.PolygonTrait, g2)::Bool = point_in_polygon(g1, g2; ignore_boundary=true) +# Lines in geometries +within(::GI.LineStringTrait, g1, ::GI.LineStringTrait, g2)::Bool = line_on_line(g1, g2) +within(::GI.LineStringTrait, g1, ::GI.LinearRingTrait, g2)::Bool = line_on_line(g1, g2) within(::GI.LineStringTrait, g1, ::GI.PolygonTrait, g2)::Bool = line_in_polygon(g1, g2) -within(::GI.LineStringTrait, g1, ::GI.LineStringTrait, g2)::Bool = line_on_line(g1, g2) +# Polygons within geometries within(::GI.PolygonTrait, g1, ::GI.PolygonTrait, g2)::Bool = polygon_in_polygon(g1, g2) + +# Everything not specified +# TODO: Add multipolygons +within(::GI.AbstractTrait, g1, ::GI.AbstractCurveTrait, g2)::Bool = false \ No newline at end of file diff --git a/src/transformations/extent.jl b/src/transformations/extent.jl index 2e230672d..a5e180d76 100644 --- a/src/transformations/extent.jl +++ b/src/transformations/extent.jl @@ -10,7 +10,7 @@ embed_extent(x) = apply(extent_applicator, AbstractTrait, x) extent_applicator(x) = extent_applicator(trait(x), x) extent_applicator(::Nothing, xs::AbstractArray) = embed_extent.(xs) -function extent_applicator(::Union{AbstractCurveTrait,MultiPointTrait}, point) = point +extent_applicator(::Union{AbstractCurveTrait,MultiPointTrait}, point) = point function extent_applicator(trait::AbstractGeometryTrait, geom) children_with_extents = map(GI.getgeom(geom)) do g diff --git a/src/utils.jl b/src/utils.jl index dc6c078eb..b95b78172 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -54,7 +54,7 @@ end to_edges() Convert any geometry or collection of geometries into a flat -vector of `Tuple{Tuple{Float64,Float64},{Float64,Float64}}` edges. +vector of `Tuple{Tuple{Float64,Float64},Tuple{Float64,Float64}}` edges. """ function to_edges(x) edges = Vector{Edge}(undef, _nedge(x)) diff --git a/test/methods/bools.jl b/test/methods/bools.jl index b7650cb87..cb1ff945c 100644 --- a/test/methods/bools.jl +++ b/test/methods/bools.jl @@ -83,7 +83,7 @@ import GeometryOps as GO line8 = GI.LineString([(124.584961, -12.768946), (126.738281, -17.224758)]) line9 = GI.LineString([(123.354492, -15.961329), (127.22168, -14.008696)]) - @test all(GO.line_intersection(line8, line9)[1] .≈ (125.583754, -14.835723)) + @test all(GO.intersection(line8, line9)[1] .≈ (125.583754, -14.835723)) line10 = GI.LineString([ (142.03125, -11.695273), @@ -105,7 +105,7 @@ import GeometryOps as GO (132.890625, -7.754537), ]) - points = GO.line_intersection(line10, line11) + points = GO.intersection(line10, line11) @test all(points[1] .≈ (119.832884, -19.58857)) @test all(points[2] .≈ (132.808697, -11.6309378)) @@ -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) == false - - 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..ea1eb73fb --- /dev/null +++ b/test/methods/equals.jl @@ -0,0 +1,131 @@ +@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) == LG.equals(p1, p1) + @test GO.equals(p2, p2) == LG.equals(p2, p2) + # Different points + @test GO.equals(p1, p2) == LG.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]]) + mp3 = LG.MultiPoint([p2]) + # Same points + @test GO.equals(mp1, mp1) == LG.equals(mp1, mp1) + @test GO.equals(mp2, mp2) == LG.equals(mp2, mp2) + # Different points + @test GO.equals(mp1, mp2) == LG.equals(mp1, mp2) + @test GO.equals(mp1, p1) == LG.equals(mp1, p1) + # Point and multipoint + @test GO.equals(p2, mp3) == LG.equals(p2, mp3) +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 GO.equals(l1, l1) == LG.equals(l1, l1) + @test GO.equals(l2, l2) == LG.equals(l2, l2) + # Different lines + @test GO.equals(l1, l2) == GO.equals(l2, l1) == LG.equals(l1, l2) + + 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 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( + [ + [[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]] + ] + ) + 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 + @test GO.equals(p3, p3) == LG.equals(p3, p3) + # Same exterior, different hole + @test GO.equals(p3, p4) == LG.equals(p3, p4) + # Same exterior and first hole, has an extra hole + @test GO.equals(p3, p5) == LG.equals(p3, p5) + + p9 = LG.Polygon( + [[ + [-53.57208251953125, 28.287451910503744], + [-53.33038330078125, 28.29228897739706], + [-53.34136962890625, 28.430052892335723], + [-53.57208251953125, 28.287451910503744], + ]] + ) + # Complex polygon + @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]]], + [ + [[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) == LG.equals(m1, m1) + # Equal multipolygon with different order + @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) +end \ No newline at end of file diff --git a/test/methods/intersects.jl b/test/methods/intersects.jl new file mode 100644 index 000000000..4251d45a8 --- /dev/null +++ b/test/methods/intersects.jl @@ -0,0 +1,145 @@ +@testset "Lines/Rings" begin + # Line test intersects ----------------------------------------------------- + + # 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) + @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) + @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) + @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) + @test all(GO.intersection(l1, l2) .≈ (2.5, 2.5)) + + # Line string test intersects ---------------------------------------------- + + # 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) + go_inter = GO.intersection(l1, l2) + lg_inter = LG.intersection(l1, l2) + @test go_inter[1][1] .≈ GI.x(lg_inter) + @test go_inter[1][2] .≈ GI.y(lg_inter) + + # 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) + go_inter = GO.intersection(l1, l2) + @test length(go_inter) == 1 + lg_inter = LG.intersection(l1, l2) + @test go_inter[1][1] .≈ GI.x(lg_inter) + @test go_inter[1][2] .≈ GI.y(lg_inter) + + # 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) + go_inter = GO.intersection(l1, l2) + @test length(go_inter) == 3 + lg_inter = LG.intersection(l1, l2) + @test issetequal( + Set(go_inter), + Set(GO._tuple_point.(GI.getpoint(lg_inter))) + ) + + # 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) + @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) + @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) + go_inter = GO.intersection(r1, l2) + @test length(go_inter) == 2 + lg_inter = LG.intersection(r1, l2) + @test issetequal( + Set(go_inter), + Set(GO._tuple_point.(GI.getpoint(lg_inter))) + ) + + # 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) + go_inter = GO.intersection(r1, r2) + @test length(go_inter) == 2 + lg_inter = LG.intersection(r1, r2) + @test issetequal( + Set(go_inter), + Set(GO._tuple_point.(GI.getpoint(lg_inter))) + ) +end + +@testset "Polygons" begin + # 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) + @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) + @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) + GO.intersection_points(p1, l2) + @test all(GO.intersection_points(p1, l2) .== [(0.0, 0.0), (10.0, 0.0)]) + + # Polygon with a hole, line through polygon and hole + p1 = LG.Polygon([ + [[0.0, 0.0], [5.0, 5.0], [10.0, 0.0], [5.0, -5.0], [0.0, 0.0]], + [[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) + @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 + p1 = LG.Polygon([ + [[0.0, 0.0], [5.0, 5.0], [10.0, 0.0], [5.0, -5.0], [0.0, 0.0]], + [[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) + @test isempty(GO.intersection_points(p1, l2)) +end + +@testset "MultiPolygons" begin + # TODO: Add these tests + # Multi-polygon and polygon that intersect + + # Multi-polygon and polygon that don't intersect + + # Multi-polygon that intersects with linestring + +end \ No newline at end of file diff --git a/test/methods/overlaps.jl b/test/methods/overlaps.jl new file mode 100644 index 000000000..0c2dab96d --- /dev/null +++ b/test/methods/overlaps.jl @@ -0,0 +1,104 @@ +@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) + @test !GO.overlaps(p1, (1, 1)) + @test !GO.overlaps((1, 1), 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) + + 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 c4fc39f3e..ee2065017 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -18,7 +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