///|
pub(all) struct RegionDecision {
id : Int32
location : Location
component : Int32?
boundary : BoundaryWitness?
} derive(Debug, Eq)
///|
pub(all) struct RegionQueryReport {
revision : Int32
candidates : Int32
nodes_visited : Int32
entries_tested : Int32
hits : Array[Int32]
decisions : Array[RegionDecision]
} derive(Debug)
///|
struct RegionIndex {
tree : RTree
regions : Map[Int32, MultiPolygon]
mut revision : Int32
}
///|
pub fn RegionIndex::new(capacity~ : Int32 = 8) -> RegionIndex raise GeoError {
{ tree: RTree::new(capacity~), regions: {}, revision: 0, }
}
///|
pub fn RegionIndex::insert(
self : RegionIndex,
id : Int32,
region : MultiPolygon,
) -> Unit raise GeoError {
guard region.bounds() is Some(bounds) else {
raise InvalidPolygon("indexed region must not be empty")
}
self.tree.insert(id, bounds)
self.regions[id] = region
self.revision = self.revision + 1
}
///|
pub fn RegionIndex::insert_polygon(
self : RegionIndex,
id : Int32,
polygon : Polygon,
) -> Unit raise GeoError {
self.insert(id, MultiPolygon::from_polygon(polygon))
}
///|
pub fn RegionIndex::remove(self : RegionIndex, id : Int32) -> Bool {
if !self.tree.remove(id) {
return false
}
self.regions.remove(id)
self.revision = self.revision + 1
true
}
///|
/// Validates the replacement before changing the index. Missing IDs return false.
pub fn RegionIndex::replace(
self : RegionIndex,
id : Int32,
region : MultiPolygon,
) -> Bool raise GeoError {
if !self.regions.contains(id) {
return false
}
guard region.bounds() is Some(bounds) else {
raise InvalidPolygon("indexed region must not be empty")
}
ignore(self.tree.remove(id))
self.tree.insert(id, bounds)
self.regions[id] = region
self.revision = self.revision + 1
true
}
///|
pub fn RegionIndex::length(self : RegionIndex) -> Int32 {
self.regions.length()
}
///|
pub fn RegionIndex::stats(self : RegionIndex) -> TreeStats {
self.tree.stats()
}
///|
pub fn RegionIndex::validate(self : RegionIndex) -> Array[String] {
let errors = self.tree.validate()
if self.regions.length() != self.tree.length() {
errors.push("region directory mismatch")
}
for id, region in self.regions {
if self.tree.directory.get(id) != region.bounds() {
errors.push("region envelope mismatch")
}
}
errors
}
///|
/// Results are sorted by ID. By default hits include boundary contacts; every
/// candidate receives an explicit decision, including envelope false positives.
pub fn RegionIndex::query_point(
self : RegionIndex,
p : Point,
include_boundary~ : Bool = true,
rule~ : FillRule = EvenOdd,
) -> RegionQueryReport {
let envelope = { min_x: p.x, min_y: p.y, max_x: p.x, max_y: p.y, }
let range = self.tree.query(envelope)
let hits : Array[Int32] = []
let decisions : Array[RegionDecision] = []
for entry in range.entries {
if self.regions.get(entry.id) is Some(region) {
let mut location = Outside
let mut component = None
let mut boundary = None
for i in 0..