Skip to content

NOTE: This functionality is experimental and may change at any time.

Topology predicate framework

Port of JTS TopologyPredicate, BasicPredicate, IMPredicate (operation/relateng), plus the intersects and disjoint BasicPredicate kinds from RelatePredicate.java (the other named predicates are IMPredicate kinds and live in relate_predicates.jl).

Julia has no field inheritance, so JTS's class triangle becomes two kind-parameterized mutable structs (BasicPredicate{K}, IMPredicate{K}). Per-kind behavior (is_determined, value_im, requirement flags, init overrides) dispatches on the kind singleton; requirement flags are pure functions of the type, so evaluation specializes per predicate.

TopologyPredicate API (TopologyPredicate.java)

julia
"""
    TopologyPredicate

The abstract supertype for strategy types implementing spatial predicates
based on the DE-9IM topology model (port of JTS `TopologyPredicate`).
Concrete predicates implement `predicate_name(p)`,
`update_dim!(p, locA, locB, dim)`, `finish!(p)`, `is_known(p)`, and
`predicate_value(p)`, and may override the requirement flags and the
`init_dims!`/`init_bounds!` hooks. Evaluate one against a pair of
geometries with `relate_predicate`.
"""
abstract type TopologyPredicate end

Whether the predicate requires self-noding for geometries with crossing edges. JTS default: true.

julia
require_self_noding(::Type{<:TopologyPredicate}) = true

Whether the predicate requires interaction between the input geometries (i.e. some entry of IM[I/B, I/B] >= 0). JTS default: true.

julia
require_interaction(::Type{<:TopologyPredicate}) = true

Whether the predicate requires the source to cover the target. JTS default: false.

julia
require_covers(::Type{<:TopologyPredicate}, is_source_a::Bool) = false

Whether the predicate requires checking if the source input intersects the exterior of the target input. JTS default: true.

julia
require_exterior_check(::Type{<:TopologyPredicate}, is_source_a::Bool) = true

Instance-level forwarding of the requirement flags. Most predicates' flags are pure functions of the type (so evaluation can specialize on them), but runtime-data-dependent predicates (e.g. IMPatternMatcher, whose require_interaction is computed from its pattern matrix) can override these instance methods.

julia
require_self_noding(p::TopologyPredicate) = require_self_noding(typeof(p))
require_interaction(p::TopologyPredicate) = require_interaction(typeof(p))
require_covers(p::TopologyPredicate, is_source_a::Bool) = require_covers(typeof(p), is_source_a)
require_exterior_check(p::TopologyPredicate, is_source_a::Bool) = require_exterior_check(typeof(p), is_source_a)

Initializes the predicate for a specific geometric case from the input dimensions. Default: dimensions provide no information.

julia
init_dims!(p::TopologyPredicate, dimA::Integer, dimB::Integer) = nothing

Initializes the predicate from the input bounds (JTS init(Envelope, Envelope)). Default: bounds provide no information.

julia
init_bounds!(p::TopologyPredicate, extA, extB) = nothing

Tri-state value and BasicPredicate (BasicPredicate.java)

The base for relate predicates with a boolean value, with tri-state logic to detect when the final value has been determined.

julia
const TRI_UNKNOWN = Int8(-1)
const TRI_FALSE   = Int8(0)
const TRI_TRUE    = Int8(1)

Tests if two geometries intersect based on an interaction at given locations.

julia
is_intersection(locA::Integer, locB::Integer) =
    locA != LOC_EXTERIOR && locB != LOC_EXTERIOR

JTS Envelope.intersects/covers return false when either envelope is null. A null (empty-geometry) extent is represented as nothing here (get_extent(rg) of an empty RelateGeometry), so the nothing methods mirror the Java null-envelope behavior exactly.

julia
ext_intersects(extA, extB) = Extents.intersects(extA, extB)
ext_intersects(::Nothing, extB) = false
ext_intersects(extA, ::Nothing) = false
ext_intersects(::Nothing, ::Nothing) = false
ext_covers(extA, extB) = Extents.covers(extA, extB)
ext_covers(::Nothing, extB) = false
ext_covers(extA, ::Nothing) = false
ext_covers(::Nothing, ::Nothing) = false

mutable struct BasicPredicate{K} <: TopologyPredicate
    const kind::K
    value::Int8
end
BasicPredicate(kind) = BasicPredicate(kind, TRI_UNKNOWN)

is_known(p::TopologyPredicate) = p.value != TRI_UNKNOWN
predicate_value(p::TopologyPredicate) = p.value == TRI_TRUE

Updates the predicate value to the given state if it is currently unknown. (JTS setValue: doesn't change an already-known value.)

julia
function set_value!(p::TopologyPredicate, val::Bool)
    is_known(p) && return nothing
    p.value = val ? TRI_TRUE : TRI_FALSE
    return nothing
end
set_value_if!(p::TopologyPredicate, val::Bool, cond::Bool) =
    cond ? set_value!(p, val) : nothing
require!(p::TopologyPredicate, cond::Bool) =
    cond ? nothing : set_value!(p, false)
require_covers!(p::TopologyPredicate, extA, extB) =
    require!(p, ext_covers(extA, extB))

intersects and disjoint kinds (RelatePredicate.java)

These are the only named predicates which are plain BasicPredicates in JTS (everything else tracks an intersection matrix).

julia
struct IntersectsPred end
struct DisjointPred end

intersects (RelatePredicate.java intersects())

julia
pred_intersects() = BasicPredicate(IntersectsPred())
predicate_name(::BasicPredicate{IntersectsPred}) = "intersects"

self-noding is not required to check for a simple interaction

julia
require_self_noding(::Type{BasicPredicate{IntersectsPred}}) = false

intersects only requires testing interaction

julia
require_exterior_check(::Type{BasicPredicate{IntersectsPred}}, is_source_a::Bool) = false
init_bounds!(p::BasicPredicate{IntersectsPred}, extA, extB) =
    require!(p, ext_intersects(extA, extB))
update_dim!(p::BasicPredicate{IntersectsPred}, locA, locB, dim) =
    set_value_if!(p, true, is_intersection(locA, locB))

if no intersecting locations were found

julia
finish!(p::BasicPredicate{IntersectsPred}) = set_value!(p, false)

disjoint (RelatePredicate.java disjoint())

julia
pred_disjoint() = BasicPredicate(DisjointPred())
predicate_name(::BasicPredicate{DisjointPred}) = "disjoint"

self-noding is not required to check for a simple interaction

julia
require_self_noding(::Type{BasicPredicate{DisjointPred}}) = false
require_interaction(::Type{BasicPredicate{DisjointPred}}) = false

disjoint only requires testing interaction

julia
require_exterior_check(::Type{BasicPredicate{DisjointPred}}, is_source_a::Bool) = false
init_bounds!(p::BasicPredicate{DisjointPred}, extA, extB) =
    set_value_if!(p, true, !ext_intersects(extA, extB))
update_dim!(p::BasicPredicate{DisjointPred}, locA, locB, dim) =
    set_value_if!(p, false, is_intersection(locA, locB))

if no intersecting locations were found

julia
finish!(p::BasicPredicate{DisjointPred}) = set_value!(p, true)

IMPredicate core (IMPredicate.java)

The base for predicates which are determined using entries in an intersection matrix. Each kind must implement is_determined(p) and value_im(p); the kinds themselves are ported in relate_predicates.jl.

allow Points coveredBy zero-length Lines

julia
is_dims_compatible_with_covers(dim0::Integer, dim1::Integer) =
    (dim0 == DIM_P && dim1 == DIM_L) ? true : dim0 >= dim1

const DIM_UNKNOWN = DIM_DONTCARE   # JTS IMPredicate.DIM_UNKNOWN = Dimension.DONTCARE

mutable struct IMPredicate{K} <: TopologyPredicate
    const kind::K
    dimA::Int8
    dimB::Int8
    im::DE9IM
    value::Int8
end

JTS IntersectionMatrix() initializes all entries to Dimension.FALSE, then the IMPredicate constructor presets E/E, which is always dim = 2.

julia
IMPredicate(kind) = IMPredicate(kind, DIM_UNKNOWN, DIM_UNKNOWN,
    with_entry(DE9IM(), LOC_EXTERIOR, LOC_EXTERIOR, DIM_A), TRI_UNKNOWN)

function init_dims!(p::IMPredicate, dimA::Integer, dimB::Integer)
    p.dimA = dimA
    p.dimB = dimB
    init_dims_kind!(p)   # per-kind hook (JTS subclasses override `init` and call super)
    return nothing
end
init_dims_kind!(p::IMPredicate) = nothing

function update_dim!(p::IMPredicate, locA, locB, dim)

only record an increased dimension value

julia
    if is_dim_changed(p, locA, locB, dim)
        p.im = with_entry(p.im, locA, locB, dim)

set value if predicate value can be known

julia
        if is_determined(p)
            set_value!(p, value_im(p))
        end
    end
    return nothing
end

is_dim_changed(p::IMPredicate, locA, locB, dim) = dim > p.im[locA, locB]

Tests whether predicate evaluation can be short-circuited due to the current state of the matrix providing enough information to determine the predicate value. Implemented per kind.

julia
function is_determined end

Gets the value of the predicate according to the current intersection matrix state. Implemented per kind.

julia
function value_im end

Tests whether the exterior of the specified input geometry is intersected by any part of the other input.

julia
intersects_exterior_of(p::IMPredicate, is_a::Bool) = is_a ?
    (is_intersects_entry(p, LOC_EXTERIOR, LOC_INTERIOR) || is_intersects_entry(p, LOC_EXTERIOR, LOC_BOUNDARY)) :
    (is_intersects_entry(p, LOC_INTERIOR, LOC_EXTERIOR) || is_intersects_entry(p, LOC_BOUNDARY, LOC_EXTERIOR))

is_intersects_entry(p::IMPredicate, locA, locB) = p.im[locA, locB] >= DIM_P

JTS's isKnownEntry is not ported: entries here are initialized to DIM_FALSE and only ever increase, so they never hold DIM_UNKNOWN and the check could never return false.

julia
is_dimension_entry(p::IMPredicate, locA, locB, dim) = p.im[locA, locB] == dim
get_dimension(p::IMPredicate, locA, locB) = p.im[locA, locB]

Sets the final value based on the state of the IM.

julia
finish!(p::IMPredicate) = set_value!(p, value_im(p))

Base.show(io::IO, p::IMPredicate) =
    print(io, predicate_name(p), ": ", string(p.im))

This page was generated using Literate.jl.