spatialrust_io/copc/
query.rs1use copc_streaming::Aabb;
4
5use crate::error::{copc_format, IoError};
6
7#[derive(Clone, Copy, Debug, PartialEq)]
9pub struct CopcBounds {
10 pub min: [f64; 3],
12 pub max: [f64; 3],
14}
15
16impl CopcBounds {
17 #[must_use]
19 pub const fn new(min: [f64; 3], max: [f64; 3]) -> Self {
20 Self { min, max }
21 }
22
23 #[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 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#[derive(Clone, Copy, Debug, PartialEq)]
49pub struct CopcQuery {
50 pub bounds: CopcBounds,
52 pub max_resolution: Option<f64>,
54 pub max_level: Option<i32>,
56}
57
58impl CopcQuery {
59 #[must_use]
61 pub fn bounds(bounds: CopcBounds) -> Self {
62 Self { bounds, max_resolution: None, max_level: None }
63 }
64
65 #[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 #[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 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#[derive(Clone, Debug, PartialEq)]
105pub struct CopcFileInfo {
106 pub root_bounds: CopcBounds,
108 pub spacing: f64,
110 pub point_count: u64,
112}
113
114#[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}