Skip to main content

spatialrust_io/copc/
query.rs

1//! COPC spatial query types and helpers.
2
3use copc_streaming::Aabb;
4
5use crate::error::{copc_format, IoError};
6
7/// Axis-aligned bounds for COPC spatial queries.
8#[derive(Clone, Copy, Debug, PartialEq)]
9pub struct CopcBounds {
10    /// Minimum corner `[x, y, z]`.
11    pub min: [f64; 3],
12    /// Maximum corner `[x, y, z]`.
13    pub max: [f64; 3],
14}
15
16impl CopcBounds {
17    /// Creates bounds from minimum and maximum corners.
18    #[must_use]
19    pub const fn new(min: [f64; 3], max: [f64; 3]) -> Self {
20        Self { min, max }
21    }
22
23    /// Creates bounds from separate axis ranges.
24    #[must_use]
25    pub fn from_ranges(x: (f64, f64), y: (f64, f64), z: (f64, f64)) -> Self {
26        Self { min: [x.0, y.0, z.0], max: [x.1, y.1, z.1] }
27    }
28
29    /// Validates that min <= max on every axis.
30    pub fn validate(&self) -> Result<(), IoError> {
31        for axis in 0..3 {
32            if self.min[axis] > self.max[axis] {
33                return Err(copc_format(format!(
34                    "invalid COPC bounds on axis {axis}: min {} > max {}",
35                    self.min[axis], self.max[axis]
36                )));
37            }
38        }
39        Ok(())
40    }
41
42    pub(crate) fn to_aabb(self) -> Aabb {
43        Aabb { min: self.min, max: self.max }
44    }
45}
46
47/// Spatial query parameters for partial COPC reads.
48#[derive(Clone, Copy, Debug, PartialEq)]
49pub struct CopcQuery {
50    /// Region of interest.
51    pub bounds: CopcBounds,
52    /// Optional target point spacing in file units (typically meters).
53    pub max_resolution: Option<f64>,
54    /// Optional explicit maximum octree level. Overrides `max_resolution` when set.
55    pub max_level: Option<i32>,
56}
57
58impl CopcQuery {
59    /// Creates a bounds-only query at full available detail inside the region.
60    #[must_use]
61    pub fn bounds(bounds: CopcBounds) -> Self {
62        Self { bounds, max_resolution: None, max_level: None }
63    }
64
65    /// Creates a bounds query limited by target point spacing.
66    #[must_use]
67    pub fn with_resolution(bounds: CopcBounds, max_resolution: f64) -> Self {
68        Self { bounds, max_resolution: Some(max_resolution), max_level: None }
69    }
70
71    /// Creates a bounds query limited by explicit octree level.
72    #[must_use]
73    pub fn with_level(bounds: CopcBounds, max_level: i32) -> Self {
74        Self { bounds, max_resolution: None, max_level: Some(max_level) }
75    }
76
77    /// Validates query parameters.
78    pub fn validate(&self) -> Result<(), IoError> {
79        self.bounds.validate()?;
80        if let Some(resolution) = self.max_resolution {
81            if !resolution.is_finite() || resolution <= 0.0 {
82                return Err(copc_format(
83                    "max_resolution must be a positive finite value".to_owned(),
84                ));
85            }
86        }
87        if let Some(level) = self.max_level {
88            if level < 0 {
89                return Err(copc_format("max_level must be non-negative".to_owned()));
90            }
91        }
92        Ok(())
93    }
94
95    pub(crate) fn max_level_for_spacing(&self, base_spacing: f64) -> Option<i32> {
96        if self.max_level.is_some() {
97            return self.max_level;
98        }
99        self.max_resolution.map(|resolution| copc_level_for_resolution(base_spacing, resolution))
100    }
101}
102
103/// Metadata exposed from a COPC header without loading point data.
104#[derive(Clone, Debug, PartialEq)]
105pub struct CopcFileInfo {
106    /// Root octree bounds.
107    pub root_bounds: CopcBounds,
108    /// Base point spacing at octree level 0.
109    pub spacing: f64,
110    /// Declared number of points in the file header.
111    pub point_count: u64,
112}
113
114/// Computes the shallowest octree level whose spacing is at most `resolution`.
115#[must_use]
116pub fn copc_level_for_resolution(base_spacing: f64, resolution: f64) -> i32 {
117    if resolution <= 0.0 || base_spacing <= 0.0 {
118        return 0;
119    }
120    (base_spacing / resolution).log2().ceil().max(0.0) as i32
121}
122
123#[cfg(test)]
124mod tests {
125    use super::{copc_level_for_resolution, CopcBounds, CopcQuery};
126
127    #[test]
128    fn validates_bounds() {
129        let bounds = CopcBounds::from_ranges((0.0, 1.0), (0.0, 1.0), (0.0, 1.0));
130        assert!(bounds.validate().is_ok());
131        let invalid = CopcBounds::from_ranges((1.0, 0.0), (0.0, 1.0), (0.0, 1.0));
132        assert!(invalid.validate().is_err());
133    }
134
135    #[test]
136    fn level_for_resolution_matches_copc_formula() {
137        assert_eq!(copc_level_for_resolution(10.0, 0.5), 5);
138        assert_eq!(copc_level_for_resolution(10.0, 10.0), 0);
139    }
140
141    #[test]
142    fn explicit_level_overrides_resolution() {
143        let query = CopcQuery {
144            bounds: CopcBounds::new([0.0; 3], [1.0; 3]),
145            max_resolution: Some(0.5),
146            max_level: Some(2),
147        };
148        assert_eq!(query.max_level_for_spacing(10.0), Some(2));
149    }
150}