Skip to content

Angles

julia
export angles

What is angles?

Angles are the angles formed by a given geometries line segments, if it has line segments.

To provide an example, consider this rectangle:

julia
import GeometryOps as GO
import GeoInterface as GI
using Makie, CairoMakie

rect = GI.Polygon([[(0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (1.0, 0.0), (0.0, 0.0)]])
f, a, p = poly(collect(GI.getpoint(rect)); axis = (; aspect = DataAspect()))

This is clearly a rectangle, with angles of 90 degrees.

julia
GO.angles(rect)  # [90, 90, 90, 90]
4-element Vector{Float64}:
 90.0
 90.0
 90.0
 90.0

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!

julia
const _ANGLE_TARGETS = TraitTarget{Union{GI.PolygonTrait,GI.AbstractCurveTrait,GI.MultiPointTrait,GI.PointTrait}}()

"""
    angles(geom, ::Type{T} = Float64)

Returns the angles of a geometry or collection of geometries.
This is computed differently for different geometries:

    - The angles of a point is an empty vector.
    - The angles of a single line segment is an empty vector.
    - The angles of a linestring or linearring is a vector of angles formed by the curve.
    - The angles of a polygon is a vector of vectors of angles formed by each ring.
    - The angles of a multi-geometry collection is a vector of the angles of each of the
        sub-geometries as defined above.

Result will be a Vector, or nested set of vectors, of type T where an optional argument with
a default value of Float64.
"""
function angles(geom, ::Type{T} = Float64; threaded =false) where T <: AbstractFloat
    applyreduce(vcat, _ANGLE_TARGETS, geom; threaded, init = Vector{T}()) do g
        _angles(T, GI.trait(g), g)
    end
end

Points and single line segments have no angles

julia
_angles(::Type{T}, ::Union{GI.PointTrait, GI.MultiPointTrait, GI.LineTrait}, geom) where T = T[]

#= The angles of a linestring are the angles formed by the line. If the first and last point
are not explicitly repeated, the geom is not considered closed. The angles should all be on
one side of the line, but a particular side is not guaranteed by this function. =#
function _angles(::Type{T}, ::GI.LineStringTrait, geom) where T
    npoints = GI.npoint(geom)
    first_last_equal = equals(GI.getpoint(geom, 1), GI.getpoint(geom, npoints))
    angle_list = Vector{T}(undef, npoints - (first_last_equal ? 1 : 2))
    _find_angles!(
        T, angle_list, geom;
        offset = first_last_equal, close_geom = false,
    )
    return angle_list
end

#= The angles of a linearring are the angles within the closed line and include the angles
formed by connecting the first and last points of the curve. =#
function _angles(::Type{T}, ::GI.LinearRingTrait, geom; interior = true) where T
    npoints = GI.npoint(geom)
    first_last_equal = equals(GI.getpoint(geom, 1), GI.getpoint(geom, npoints))
    angle_list = Vector{T}(undef, npoints - (first_last_equal ? 1 : 0))
    _find_angles!(
        T, angle_list, geom;
        offset = true, close_geom = !first_last_equal, interior = interior,
    )
    return angle_list
end

#= The angles of a polygon is a vector of polygon angles. Note that if there are holes
within the polygon, the angles will be listed after the exterior ring angles in order of the
holes. All angles, including the hole angles, are interior angles of the polygon.=#
function _angles(::Type{T}, ::GI.PolygonTrait, geom) where T
    angles = _angles(T, GI.LinearRingTrait(), GI.getexterior(geom); interior = true)
    for h in GI.gethole(geom)
        append!(angles, _angles(T, GI.LinearRingTrait(), h; interior = false))
    end
    return angles
end

Find angles of a curve and insert the values into the angle_list. If offset is true, then save space for the angle at the first vertex, as the curve is closed, at the front of angle_list. If close_geom is true, then despite the first and last point not being explicitly repeated, the curve is closed and the angle of the last point should be added to angle_list. If interior is true, then all angles will be on the same side of the line

julia
function _find_angles!(
    ::Type{T}, angle_list, geom;
    offset, close_geom, interior = true,
) where T
    local p1, prev_p1_diff, p2_p1_diff
    local start_point, start_diff
    local extreem_idx, extreem_x, extreem_y
    i_offset = offset ? 1 : 0

Loop through the curve and find each of the angels

julia
    for (i, p2) in enumerate(GI.getpoint(geom))
        xp2, yp2 = GI.x(p2), GI.y(p2)
        #= Find point with smallest x values (and smallest y in case of a tie) as this point
        is know to be convex. =#
        if i == 1 || (xp2 < extreem_x || (xp2 == extreem_x && yp2 < extreem_y))
            extreem_idx = i
            extreem_x, extreem_y = xp2, yp2
        end
        if i > 1
            p2_p1_diff = (xp2 - GI.x(p1), yp2 - GI.y(p1))
            if i == 2
                start_point = p1
                start_diff = p2_p1_diff
            else
                angle_list[i - 2 + i_offset] = _diffs_calc_angle(T, prev_p1_diff, p2_p1_diff)
            end
            prev_p1_diff = -1 .* p2_p1_diff
        end
        p1 = p2
    end

If the last point of geometry should be the same as the first, calculate closing angle

julia
    if close_geom
        p2_p1_diff = (GI.x(start_point) - GI.x(p1), GI.y(start_point) - GI.y(p1))
        angle_list[end] = _diffs_calc_angle(T, prev_p1_diff, p2_p1_diff)
        prev_p1_diff = -1 .* p2_p1_diff
    end

If needed, calculate first angle corresponding to the first point

julia
    if offset
        angle_list[1] = _diffs_calc_angle(T, prev_p1_diff, start_diff)
    end
    #= Make sure that all of the angles are on the same side of the line and inside of the
    closed ring if the input geometry is closed. =#
    inside_sgn = sign(angle_list[extreem_idx]) * (interior ? 1 : -1)
    for i in eachindex(angle_list)
        idx_sgn = sign(angle_list[i])
        if idx_sgn == -1
            angle_list[i] = abs(angle_list[i])
        end
        if idx_sgn != inside_sgn
            angle_list[i] = 360 - angle_list[i]
        end
    end
    return
end

Calculate the angle between two vectors defined by the previous and current Δx and Δys. Angle will have a sign corresponding to the sign of the cross product between the two vectors. All angles of one sign in a given geometry are convex, while those of the other sign are concave. However, the sign corresponding to each of these can vary based on geometry and thus you must compare to an angle that is know to be convex or concave.

julia
function _diffs_calc_angle(::Type{T}, (Δx_prev, Δy_prev), (Δx_curr, Δy_curr)) where T
    cross_prod = Δx_prev * Δy_curr - Δy_prev * Δx_curr
    dot_prod = Δx_prev * Δx_curr + Δy_prev * Δy_curr
    prev_mag = max(sqrt(Δx_prev^2 + Δy_prev^2), eps(T))
    curr_mag = max(sqrt(Δx_curr^2 + Δy_curr^2), eps(T))
    val = clamp(dot_prod / (prev_mag * curr_mag), -one(T), one(T))
    angle = real(acos(val) * 180 / π)
    return angle * (cross_prod < 0 ? -1 : 1)
end

This page was generated using Literate.jl.