Skip to content

Line-curve interaction

julia
#= Code is based off of DE-9IM Standards (https://en.wikipedia.org/wiki/DE-9IM)
and attempts a standardized solution for most of the functions.
=#

"""
    Enum PointOrientation

Enum for the orientation of a point with respect to a curve. A point can be
`point_in` the curve, `point_on` the curve, or `point_out` of the curve.
"""
@enum PointOrientation point_in=1 point_on=2 point_out=3

Determines if a point meets the given checks with respect to a curve.

If in_allow is true, the point can be on the curve interior. If on_allow is true, the point can be on the curve boundary. If out_allow is true, the point can be disjoint from the curve.

If the point is in an "allowed" location, return true. Else, return false.

If closed_curve is true, curve is treated as a closed curve where the first and last point are connected by a segment.

julia
function _point_curve_process(
    point, curve;
    in_allow, on_allow, out_allow,
    closed_curve = false,
)

Determine if curve is closed

julia
    n = GI.npoint(curve)
    first_last_equal = equals(GI.getpoint(curve, 1), GI.getpoint(curve, n))
    closed_curve |= first_last_equal
    n -= first_last_equal ? 1 : 0

Loop through all curve segments

julia
    p_start = GI.getpoint(curve, closed_curve ? n : 1)
    @inbounds for i in (closed_curve ? 1 : 2):n
        p_end = GI.getpoint(curve, i)
        seg_val = _point_segment_orientation(point, p_start, p_end)
        seg_val == point_in && return in_allow
        if seg_val == point_on
            if !closed_curve  # if point is on curve endpoints, it is "on"
                i == 2 && equals(point, p_start) && return on_allow
                i == n && equals(point, p_end) && return on_allow
            end
            return in_allow
        end
        p_start = p_end
    end
    return out_allow
end

Determines if a point meets the given checks with respect to a polygon.

If in_allow is true, the point can be within the polygon interior If on_allow is true, the point can be on the polygon boundary. If out_allow is true, the point can be disjoint from the polygon.

If the point is in an "allowed" location, return true. Else, return false.

julia
function _point_polygon_process(
    point, polygon;
    in_allow, on_allow, out_allow, exact,
)

Check interaction of geom with polygon's exterior boundary

julia
    ext_val = _point_filled_curve_orientation(point, GI.getexterior(polygon); exact)

If a point is outside, it isn't interacting with any holes

julia
    ext_val == point_out && return out_allow

if a point is on an external boundary, it isn't interacting with any holes

julia
    ext_val == point_on && return on_allow

If geom is within the polygon, need to check interactions with holes

julia
    for hole in GI.gethole(polygon)
        hole_val = _point_filled_curve_orientation(point, hole; exact)

If a point in in a hole, it is outside of the polygon

julia
        hole_val == point_in && return out_allow

If a point in on a hole edge, it is on the edge of the polygon

julia
        hole_val == point_on && return on_allow
    end

Point is within external boundary and on in/on any holes

julia
    return in_allow
end

Determines if a line meets the given checks with respect to a curve.

If over_allow is true, segments of the line and curve can be co-linear. If cross_allow is true, segments of the line and curve can cross. If on_allow is true, endpoints of either the line or curve can intersect a segment of the other geometry. If cross_allow is true, segments of the line and curve can be disjoint.

If in_require is true, the interiors of the line and curve must meet in at least one point. If on_require is true, the boundary of one of the two geometries can meet the interior or boundary of the other geometry in at least one point. If out_require is true, there must be at least one point of the given line that is exterior of the curve.

If the point is in an "allowed" location and meets all requirements, return true. Else, return false.

If closed_line is true, line is treated as a closed line where the first and last point are connected by a segment. Same with closed_curve.

julia
@inline function _line_curve_process(line, curve;
    over_allow, cross_allow, kw...
)
    skip, returnval = _maybe_skip_disjoint_extents(line, curve;
        in_allow=(over_allow | cross_allow), kw...
    )
    skip && return returnval

    return _inner_line_curve_process(line, curve; over_allow, cross_allow, kw...)
end

function _inner_line_curve_process(
    line, curve;
    over_allow, cross_allow, on_allow, out_allow,
    in_require, on_require, out_require,
    closed_line = false, closed_curve = false,
    exact,
)

Set up requirements

julia
    in_req_met = !in_require
    on_req_met = !on_require
    out_req_met = !out_require

Determine curve endpoints

julia
    nl = GI.npoint(line)
    nc = GI.npoint(curve)
    first_last_equal_line = equals(GI.getpoint(line, 1), GI.getpoint(line, nl))
    first_last_equal_curve = equals(GI.getpoint(curve, 1), GI.getpoint(curve, nc))
    nl -= first_last_equal_line ? 1 : 0
    nc -= first_last_equal_curve ? 1 : 0
    closed_line |= first_last_equal_line
    closed_curve |= first_last_equal_curve

Loop over each line segment

julia
    l_start = _tuple_point(GI.getpoint(line, closed_line ? nl : 1))
    i = closed_line ? 1 : 2
    while i  nl
        l_end = _tuple_point(GI.getpoint(line, i))
        c_start = _tuple_point(GI.getpoint(curve, closed_curve ? nc : 1))

Loop over each curve segment

julia
        for j in (closed_curve ? 1 : 2):nc
            c_end = _tuple_point(GI.getpoint(curve, j))

Check if line and curve segments meet

julia
            seg_val, intr1, _ = _intersection_point(Float64, (l_start, l_end), (c_start, c_end); exact)

If segments are co-linear

julia
            if seg_val == line_over
                !over_allow && return false

at least one point in, meets requirements

julia
                in_req_met = true
                point_val = _point_segment_orientation(l_start, c_start, c_end)

If entire segment isn't covered, consider remaining section

julia
                if point_val != point_out
                    i, l_start, break_off = _find_new_seg(i, l_start, l_end, c_start, c_end)
                    break_off && break
                end
            else
                if seg_val == line_cross
                    !cross_allow && return false
                    in_req_met = true
                elseif seg_val == line_hinge  # could cross or overlap

Determine location of intersection point on each segment

julia
                    (_, (α, β)) = intr1
                    if ( # Don't consider edges of curves as they can't cross
                        (!closed_line && ((α == 0 && i == 2) ||== 1 && i == nl))) ||
                        (!closed_curve && ((β == 0 && j == 2) ||== 1 && j == nc)))
                    )
                        !on_allow && return false
                        on_req_met = true
                    else
                        in_req_met = true

If needed, determine if hinge actually crosses

julia
                        if (!cross_allow || !over_allow) && α != 0 && β != 0

Find next pieces of hinge to see if line and curve cross

julia
                            l, c = _find_hinge_next_segments(
                                α, β, l_start, l_end, c_start, c_end,
                                i, line, j, curve,
                            )
                            next_val, _, _ = _intersection_point(Float64, l, c; exact)
                            if next_val == line_hinge
                                !cross_allow && return false
                            else
                                !over_allow && return false
                            end
                        end
                    end
                end

no overlap for a give segment, some of segment must be out of curve

julia
                if j == nc
                    !out_allow && return false
                    out_req_met = true
                end
            end
            c_start = c_end  # consider next segment of curve
            if j == nc  # move on to next line segment
                i += 1
                l_start = l_end
            end
        end
    end
    return in_req_met && on_req_met && out_req_met
end

#= If entire segment (le to ls) isn't covered by segment (cs to ce), find remaining section
part of section outside of cs to ce. If completely covered, increase segment index i. =#
function _find_new_seg(i, ls, le, cs, ce)
    break_off = true
    if _point_segment_orientation(le, cs, ce) != point_out
        ls = le
        i += 1
    elseif !equals(ls, cs) && _point_segment_orientation(cs, ls, le) != point_out
        ls = cs
    elseif !equals(ls, ce) && _point_segment_orientation(ce, ls, le) != point_out
        ls = ce
    else
        break_off = false
    end
    return i, ls, break_off
end

#= Find next set of segments needed to determine if given hinge segments cross or not.=#
function _find_hinge_next_segments(α, β, ls, le, cs, ce, i, line, j, curve)
    next_seg = if β == 1
        if α == 1  # hinge at endpoints, so next segment of both is needed
            ((le, _tuple_point(GI.getpoint(line, i + 1))), (ce, _tuple_point(GI.getpoint(curve, j + 1))))
        else  # hinge at curve endpoint and line interior point, curve next segment needed
            ((ls, le), (ce, _tuple_point(GI.getpoint(curve, j + 1))))
        end
    else  # hinge at curve interior point and line endpoint, line next segment needed
        ((le, _tuple_point(GI.getpoint(line, i + 1))), (cs, ce))
    end
    return next_seg
end

Determines if a line meets the given checks with respect to a polygon.

If in_allow is true, segments of the line can be in the polygon interior. If on_allow is true, segments of the line can be on the polygon's boundary. If out_allow is true, segments of the line can be outside of the polygon.

If in_require is true, the interiors of the line and polygon must meet in at least one point. If on_require is true, the line must have at least one point on the polygon'same boundary. If out_require is true, the line must have at least one point outside of the polygon.

If the point is in an "allowed" location and meets all requirements, return true. Else, return false.

If closed_line is true, line is treated as a closed line where the first and last point are connected by a segment.

julia
@inline function _line_polygon_process(line, polygon; kw...)
    skip, returnval = _maybe_skip_disjoint_extents(line, polygon; kw...)
    skip && return returnval
    return _inner_line_polygon_process(line, polygon; kw...)
end

function _inner_line_polygon_process(
    line, polygon;
    in_allow, on_allow, out_allow,
    in_require, on_require, out_require,
    exact, closed_line = false,
)
    in_req_met = !in_require
    on_req_met = !on_require
    out_req_met = !out_require

Check interaction of line with polygon's exterior boundary

julia
    in_curve, on_curve, out_curve = _line_filled_curve_interactions(
        line, GI.getexterior(polygon);
        exact, closed_line = closed_line,
    )
    if on_curve
        !on_allow && return false
        on_req_met = true
    end
    if out_curve
        !out_allow && return false
        out_req_met = true
    end

If no points within the polygon, the line is disjoint and we are done

julia
    !in_curve && return in_req_met && on_req_met && out_req_met

Loop over polygon holes

julia
    for hole in GI.gethole(polygon)
        in_hole, on_hole, out_hole =_line_filled_curve_interactions(
            line, hole;
            exact, closed_line = closed_line,
        )
        if in_hole  # line in hole is equivalent to being out of polygon
            !out_allow && return false
            out_req_met = true
        end
        if on_hole  # hole boundary is polygon boundary
            !on_allow && return false
            on_req_met = true
        end
        if !out_hole  # entire line is in/on hole, can't be in/on other holes
            in_curve = false
            break
        end
    end
    if in_curve  # entirely of curve isn't within a hole
        !in_allow && return false
        in_req_met = true
    end
    return in_req_met && on_req_met && out_req_met
end

Determines if a polygon meets the given checks with respect to a polygon.

If in_allow is true, the polygon's interiors must intersect. If on_allow is true, the one of the polygon's boundaries must either interact with the other polygon's boundary or interior. If out_allow is true, the first polygon must have interior regions outside of the second polygon.

If in_require is true, the polygon interiors must meet in at least one point. If on_require is true, one of the polygon's must have at least one boundary point in or on the other polygon. If out_require is true, the first polygon must have at least one interior point outside of the second polygon.

If the point is in an "allowed" location and meets all requirements, return true. Else, return false.

julia
@inline function _polygon_polygon_process(poly1, poly2; kw...)
    skip, returnval = _maybe_skip_disjoint_extents(poly1, poly2; kw...)
    skip && return returnval
    return _inner_polygon_polygon_process(poly1, poly2; kw...)
end

function _inner_polygon_polygon_process(
    poly1, poly2;
    in_allow, on_allow, out_allow,
    in_require, on_require, out_require,
    exact,
)
    in_req_met = !in_require
    on_req_met = !on_require
    out_req_met = !out_require

Check if exterior of poly1 is within poly2

julia
    ext1 = GI.getexterior(poly1)
    ext2 = GI.getexterior(poly2)

Check if exterior of poly1 is in polygon 2

julia
    e1_in_p2, e1_on_p2, e1_out_p2 = _line_polygon_interactions(
        ext1, poly2;
        exact, closed_line = true,
    )
    if e1_on_p2
        !on_allow && return false
        on_req_met = true
    end
    if e1_out_p2
        !out_allow && return false
        out_req_met = true
    end

    if !e1_in_p2

if exterior ring isn't in poly2, check if it surrounds poly2

julia
        _, _, e2_out_e1 = _line_filled_curve_interactions(
            ext2, ext1;
            exact, closed_line = true,
        )  # if they really are disjoint, we are done
        e2_out_e1 && return in_req_met && on_req_met && out_req_met
    end

If interiors interact, check if poly2 interacts with any of poly1's holes

julia
    for h1 in GI.gethole(poly1)
        h1_in_p2, h1_on_p2, h1_out_p2 = _line_polygon_interactions(
            h1, poly2;
            exact, closed_line = true,
        )
        if h1_on_p2
            !on_allow && return false
            on_req_met = true
        end
        if h1_out_p2
            !out_allow && return false
            out_req_met = true
        end
        if !h1_in_p2

If hole isn't in poly2, see if poly2 is in hole

julia
            _, _, e2_out_h1 = _line_filled_curve_interactions(
                ext2, h1;
                exact, closed_line = true,
            )

hole encompasses all of poly2

julia
            !e2_out_h1 && return in_req_met && on_req_met && out_req_met
            break
        end
    end
    #=
    poly2 isn't outside of poly1 and isn't in a hole, poly1 interior must
    interact with poly2 interior
    =#
    !in_allow && return false
    in_req_met = true

If any of poly2 holes are within poly1, part of poly1 is exterior to poly2

julia
    for h2 in GI.gethole(poly2)
        h2_in_p1, h2_on_p1, _ = _line_polygon_interactions(
            h2, poly1;
            exact, closed_line = true,
        )
        if h2_on_p1
            !on_allow && return false
            on_req_met = true
        end
        if h2_in_p1
            !out_allow && return false
            out_req_met = true
        end
    end
    return in_req_met && on_req_met && out_req_met
end

Determines if a point is in, on, or out of a segment. If the point is on the segment it is on one of the segments endpoints. If it is in, it is on any other point of the segment. If the point is not on any part of the segment, it is out of the segment.

Point should be an object of point trait and curve should be an object with a linestring or linearring trait.

Can provide values of in, on, and out keywords, which determines return values for each scenario.

julia
function _point_segment_orientation(
    point, start, stop;
    in::T = point_in, on::T = point_on, out::T = point_out,
) where {T}

Parse out points

julia
    x, y = GI.x(point), GI.y(point)
    x1, y1 = GI.x(start), GI.y(start)
    x2, y2 = GI.x(stop), GI.y(stop)
    Δx_seg = x2 - x1
    Δy_seg = y2 - y1
    Δx_pt = x - x1
    Δy_pt = y - y1
    if (Δx_pt == 0 && Δy_pt == 0) || (Δx_pt == Δx_seg && Δy_pt == Δy_seg)

If point is equal to the segment start or end points

julia
        return on
    else
        #=
        Determine if the point is on the segment -> see if vector from segment
        start to point is parallel to segment and if point is between the
        segment endpoints
        =#
        on_line = _isparallel(Δx_seg, Δy_seg, Δx_pt, Δy_pt)
        !on_line && return out
        between_endpoints =
            (x2 > x1 ? x1 <= x <= x2 : x2 <= x <= x1) &&
            (y2 > y1 ? y1 <= y <= y2 : y2 <= y <= y1)
        !between_endpoints && return out
    end
    return in
end

Determine if point is in, on, or out of a closed curve, which includes the space enclosed by the closed curve.

In means the point is within the closed curve (excluding edges and vertices). On means the point is on an edge or a vertex of the closed curve. Out means the point is outside of the closed curve.

Point should be an object of point trait and curve should be an object with a linestring or linearring trait, that is assumed to be closed, regardless of repeated last point.

Can provide values of in, on, and out keywords, which determines return values for each scenario.

Note that this uses the Algorithm by Hao and Sun (2018): https://doi.org/10.3390/sym10100477 Paper separates orientation of point and edge into 26 cases. For each case, it is either a case where the point is on the edge (returns on), where a ray from the point (x, y) to infinity along the line y = y cut through the edge (k += 1), or the ray does not pass through the edge (do nothing and continue). If the ray passes through an odd number of edges, it is within the curve, else outside of of the curve if it didn't return 'on'. See paper for more information on cases denoted in comments.

julia
function _point_filled_curve_orientation(
    point, curve;
    in::T = point_in, on::T = point_on, out::T = point_out, exact,
) where {T}
    x, y = GI.x(point), GI.y(point)
    n = GI.npoint(curve)
    n -= equals(GI.getpoint(curve, 1), GI.getpoint(curve, n)) ? 1 : 0
    k = 0  # counter for ray crossings
    p_start = GI.getpoint(curve, n)
    for (i, p_end) in enumerate(GI.getpoint(curve))
        i > n && break
        v1 = GI.y(p_start) - y
        v2 = GI.y(p_end) - y
        if !((v1 < 0 && v2 < 0) || (v1 > 0 && v2 > 0)) # if not cases 11 or 26
            u1, u2 = GI.x(p_start) - x, GI.x(p_end) - x
            f = Predicates.cross((u1, u2), (v1, v2); exact)
            if v2 > 0 && v1  0                # Case 3, 9, 16, 21, 13, or 24
                f == 0 && return on         # Case 16 or 21
                f > 0 && (k += 1)              # Case 3 or 9
            elseif v1 > 0 && v2  0            # Case 4, 10, 19, 20, 12, or 25
                f == 0 && return on         # Case 19 or 20
                f < 0 && (k += 1)              # Case 4 or 10
            elseif v2 == 0 && v1 < 0           # Case 7, 14, or 17
                f == 0 && return on         # Case 17
            elseif v1 == 0 && v2 < 0           # Case 8, 15, or 18
                f == 0 && return on         # Case 18
            elseif v1 == 0 && v2 == 0          # Case 1, 2, 5, 6, 22, or 23
                u2  0 && u1  0 && return on  # Case 1
                u1  0 && u2  0 && return on  # Case 2
            end
        end
        p_start = p_end
    end
    return iseven(k) ? out : in
end

Determines the types of interactions of a line with a filled-in curve. By filled-in curve, I am referring to the exterior ring of a poylgon, for example.

Returns a tuple of booleans: (in_curve, on_curve, out_curve).

If in_curve is true, some of the lines interior points interact with the curve's interior points. If on_curve is true, endpoints of either the line intersect with the curve or the line interacts with the polygon boundary. If out_curve is true, at least one segments of the line is outside the curve.

If closed_line is true, line is treated as a closed line where the first and last point are connected by a segment.

julia
function _line_filled_curve_interactions(
    line, curve;
    exact, closed_line = false,
)
    in_curve = false
    on_curve = false
    out_curve = false

Determine number of points in curve and line

julia
    nl = GI.npoint(line)
    nc = GI.npoint(curve)
    first_last_equal_line = equals(GI.getpoint(line, 1), GI.getpoint(line, nl))
    first_last_equal_curve = equals(GI.getpoint(curve, 1), GI.getpoint(curve, nc))
    nl -= first_last_equal_line ? 1 : 0
    nc -= first_last_equal_curve ? 1 : 0
    closed_line |= first_last_equal_line

See if first point is in an acceptable orientation

julia
    l_start = _tuple_point(GI.getpoint(line, closed_line ? nl : 1))
    point_val = _point_filled_curve_orientation(l_start, curve; exact)
    if point_val == point_in
        in_curve = true
    elseif point_val == point_on
        on_curve = true
    else  # point_val == point_out
        out_curve = true
    end

Check for any intersections between line and curve

julia
    for i in (closed_line ? 1 : 2):nl
        l_end = _tuple_point(GI.getpoint(line, i))
        c_start = _tuple_point(GI.getpoint(curve, nc))

If already interacted with all regions of curve, can stop

julia
        in_curve && on_curve && out_curve && break

Check next segment of line against curve

julia
        for j in 1:nc
            c_end = _tuple_point(GI.getpoint(curve, j))

Check if two line and curve segments meet

julia
            seg_val, _, _ = _intersection_point(Float64, (l_start, l_end), (c_start, c_end); exact)
            if seg_val != line_out

If line and curve meet, then at least one point is on boundary

julia
                on_curve = true
                if seg_val == line_cross

When crossing boundary, line is both in and out of curve

julia
                    in_curve = true
                    out_curve = true
                else
                    if seg_val == line_over
                        sp = _point_segment_orientation(l_start, c_start, c_end)
                        lp = _point_segment_orientation(l_end, c_start, c_end)
                        if sp != point_in || lp != point_in
                            #=
                            Line crosses over segment endpoint, creating a hinge
                            with another segment.
                            =#
                            seg_val = line_hinge
                        end
                    end
                    if seg_val == line_hinge
                        #=
                        Can't determine all types of interactions (in, out) with
                        hinge as it could pass through multiple other segments
                        so calculate if segment endpoints and intersections are
                        in/out of filled curve
                        =#
                        ipoints = intersection_points(GI.Line(StaticArrays.SVector(l_start, l_end)), curve)
                        npoints = length(ipoints)  # since hinge, at least one
                        dist_from_lstart = let l_start = l_start
                            x -> _euclid_distance(Float64, x, l_start)
                        end
                        sort!(ipoints, by = dist_from_lstart)
                        p_start = _tuple_point(l_start)
                        for i in 1:(npoints + 1)
                            p_end = i  npoints ? _tuple_point(ipoints[i]) : l_end
                            mid_val = _point_filled_curve_orientation((p_start .+ p_end) ./ 2, curve; exact)
                            if mid_val == point_in
                                in_curve = true
                            elseif mid_val == point_out
                                out_curve = true
                            end
                        end

already checked segment against whole filled curve

julia
                        l_start = l_end
                        break
                    end
                end
            end
            c_start = c_end
        end
        l_start = l_end
    end
    return in_curve, on_curve, out_curve
end

Determines the types of interactions of a line with a polygon.

Returns a tuple of booleans: (in_poly, on_poly, out_poly).

If in_poly is true, some of the lines interior points interact with the polygon interior points. If in_poly is true, endpoints of either the line intersect with the polygon or the line interacts with the polygon boundary, including hole boundaries. If out_curve is true, at least one segments of the line is outside the polygon, including inside of holes.

If closed_line is true, line is treated as a closed line where the first and last point are connected by a segment.

julia
function _line_polygon_interactions(
    line, polygon;
    exact, closed_line = false,
)

    in_poly, on_poly, out_poly = _line_filled_curve_interactions(
        line, GI.getexterior(polygon);
        exact, closed_line = closed_line,
    )
    !in_poly && return (in_poly, on_poly, out_poly)

Loop over polygon holes

julia
    for hole in GI.gethole(polygon)
        in_hole, on_hole, out_hole =_line_filled_curve_interactions(
            line, hole;
            exact, closed_line = closed_line,
        )
        if in_hole
            out_poly = true
        end
        if on_hole
            on_poly = true
        end
        if !out_hole  # entire line is in/on hole, can't be in/on other holes
            in_poly = false
            return (in_poly, on_poly, out_poly)
        end
    end
    return in_poly, on_poly, out_poly
end

Disjoint extent optimisation: skip work based on geom extent intersection returns Tuple{Bool, Bool} for (skip, returnval)

julia
@inline function _maybe_skip_disjoint_extents(a, b;
    in_allow, on_allow, out_allow,
    in_require, on_require, out_require,
    kw...
)
    ext_disjoint = Extents.disjoint(GI.extent(a), GI.extent(b))
    skip, returnval = if !ext_disjoint

can't tell anything about this case

julia
        false, false
    elseif out_allow # && ext_disjoint
        if in_require || on_require
            true, false
        else
            true, true
        end
    else  # !out_allow && ext_disjoint

points not allowed in exterior, but geoms are disjoint

julia
        true, false
    end
    return skip, returnval
end

This page was generated using Literate.jl.