///|
/// Summary statistics useful to import pipelines and CLI reports.
pub(all) struct ShapeStats {
record_count : Int
point_count : Int
part_count : Int
null_count : Int
bounds : Bounds?
total_length : Double
total_area : Double
} derive(Debug)
///|
pub fn summarize(shapes : Array[Shape]) -> ShapeStats raise ShapeError {
let mut points = 0
let mut parts = 0
let mut nulls = 0
let mut length = 0.0
let mut area = 0.0
let mut bounds : Bounds? = None
for shape in shapes {
shape.validate()
points += shape.points.length()
parts += shape.parts.length()
if shape.kind == Null {
nulls += 1
}
length += shape.planar_length()
area += shape.planar_area()
bounds = merge_bounds(bounds, shape.bounds())
}
{
record_count: shapes.length(),
point_count: points,
part_count: parts,
null_count: nulls,
bounds,
total_length: length,
total_area: area,
}
}
///|
/// Returns physical record indices whose bounding boxes overlap the query.
pub fn intersecting_indices(
shapes : Array[Shape],
query : Bounds,
) -> Array[Int] raise ShapeError {
query.validate()
let result : Array[Int] = []
for i in 0.. Array[Shape] raise ShapeError {
let result : Array[Shape] = []
for i in indices {
if i < 0 || i >= shapes.length() {
raise InvalidData(i, "shape index outside array")
}
result.push(shapes[i])
}
result
}
///|
/// Validate that a collection has one file-compatible shape family.
pub fn validate_shape_family(
kind : ShapeType,
shapes : Array[Shape],
) -> Unit raise ShapeError {
let mut seen_z = false
let mut seen_m = false
for shape in shapes {
if shape.kind != Null && shape.kind != kind {
raise InvalidData(0, "mixed shape families")
}
shape.validate()
seen_z = seen_z || shape.kind.has_z()
seen_m = seen_m || shape.kind.has_m()
}
if seen_z != kind.has_z() || seen_m != kind.has_m() {
raise InvalidData(0, "shape family dimensions disagree")
}
}
///|
pub fn bounds_of(shapes : Array[Shape]) -> Bounds? raise ShapeError {
let mut result : Bounds? = None
for shape in shapes {
shape.validate()
result = merge_bounds(result, shape.bounds())
}
result
}
///|
pub fn count_vertices(shapes : Array[Shape]) -> Int raise ShapeError {
let mut n = 0
for shape in shapes {
shape.validate()
n += shape.points.length()
}
n
}
///|
pub fn count_parts(shapes : Array[Shape]) -> Int raise ShapeError {
let mut n = 0
for shape in shapes {
shape.validate()
n += shape.parts.length()
}
n
}
///|
/// Return the first record that contains a point under inclusive XY semantics.
pub fn locate_point(
shapes : Array[Shape],
point : Coordinate,
) -> Int? raise ShapeError {
for i in 0.. Map[Int, Int] raise ShapeError {
let result : Map[Int, Int] = Map([])
for shape in shapes {
shape.validate()
let code = shape.kind.code()
result[code] = result.get(code).unwrap_or(0) + 1
}
result
}
///|
/// Check that all bounds are finite and mutually compatible with their shapes.
pub fn validate_bounds(
shapes : Array[Shape],
declared : Bounds,
) -> Unit raise ShapeError {
declared.validate()
for shape in shapes {
shape.validate()
match shape.bounds() {
Some(b) =>
if b.xmin < declared.xmin ||
b.ymin < declared.ymin ||
b.xmax > declared.xmax ||
b.ymax > declared.ymax {
raise InvalidData(0, "shape lies outside declared bounds")
}
None => ()
}
}
}
///|
/// Calculate the fraction of points that carry a Z ordinate.
pub fn z_coverage(shapes : Array[Shape]) -> Double raise ShapeError {
let mut total = 0
let mut with_z = 0
for shape in shapes {
shape.validate()
for p in shape.points {
total += 1
if p.z is Some(_) {
with_z += 1
}
}
}
if total == 0 {
0.0
} else {
with_z.to_double() / total.to_double()
}
}
///|
/// Calculate the fraction of points that carry an M ordinate.
pub fn m_coverage(shapes : Array[Shape]) -> Double raise ShapeError {
let mut total = 0
let mut with_m = 0
for shape in shapes {
shape.validate()
for p in shape.points {
total += 1
if p.m is Some(_) {
with_m += 1
}
}
}
if total == 0 {
0.0
} else {
with_m.to_double() / total.to_double()
}
}
///|
test "summarize and spatial selection preserve physical order" {
let shapes = [
{ kind: Point, points: [coordinate(1.0, 2.0)], parts: [] },
{ kind: Point, points: [coordinate(9.0, 9.0)], parts: [] },
]
let stats = summarize(shapes)
assert_eq(stats.record_count, 2)
assert_eq(stats.point_count, 2)
assert_eq(
intersecting_indices(shapes, { xmin: 0.0, ymin: 0.0, xmax: 2.0, ymax: 3.0 }),
[0],
)
assert_eq(stable_select(shapes, [1, 0, 1]).length(), 3)
}