oxigdal-copc 0.1.6

Pure Rust COPC (Cloud Optimized Point Cloud) reader for OxiGDAL - LAS/LAZ format with spatial index
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
//! LAS/LAZ point record types and 3D bounding box.
//!
//! Implements [`Point3D`] following the ASPRS LAS 1.4 specification (R15, November 2019)
//! and [`BoundingBox3D`] for spatial queries.

/// Waveform packet data per ASPRS LAS 1.4 R15 Tables 17-18.
/// Present in point data record formats 9 and 10.
#[derive(Debug, Clone, PartialEq)]
pub struct WaveformPacket {
    /// Index into the Waveform Packet Descriptor lookup table (1-255).
    pub descriptor_index: u8,
    /// Byte offset in the waveform data packets section (or external file).
    pub byte_offset: u64,
    /// Size in bytes of the waveform data packet.
    pub packet_size: u32,
    /// Return point waveform location: parametric t value at which the associated
    /// return pulse was detected, measured in picoseconds.
    pub return_point_loc: f32,
    /// X(t) parametric displacement in the direction of the laser pulse
    /// (metres per picosecond).
    pub x_t: f32,
    /// Y(t) parametric displacement in the direction of the laser pulse
    /// (metres per picosecond).
    pub y_t: f32,
    /// Z(t) parametric displacement in the direction of the laser pulse
    /// (metres per picosecond).
    pub z_t: f32,
}

/// A single LAS/LAZ point record.
///
/// Covers the core fields present in all LAS point data format IDs (0–10).
/// Optional fields (`gps_time`, `red`, `green`, `blue`) are `None` for format
/// IDs that do not carry them.
#[derive(Debug, Clone, PartialEq)]
pub struct Point3D {
    /// X coordinate (scaled and offset per LAS header).
    pub x: f64,
    /// Y coordinate (scaled and offset per LAS header).
    pub y: f64,
    /// Z coordinate (scaled and offset per LAS header).
    pub z: f64,
    /// Laser pulse return intensity (0–65535).
    pub intensity: u16,
    /// Return number within the pulse (1-based, ≤ `number_of_returns`).
    pub return_number: u8,
    /// Total number of returns for this pulse.
    pub number_of_returns: u8,
    /// ASPRS classification code (see [`Point3D::classification_name`]).
    pub classification: u8,
    /// Scan angle rank in degrees (−90 to +90, rounded to integer for formats 0-5).
    pub scan_angle_rank: i8,
    /// User-defined data byte.
    pub user_data: u8,
    /// Point source ID (flight line ID for airborne surveys).
    pub point_source_id: u16,
    /// GPS time of the point (present in formats 1, 3, 5, 6–10).
    pub gps_time: Option<f64>,
    /// Red channel colour value (present in formats 2, 3, 5, 7, 8).
    pub red: Option<u16>,
    /// Green channel colour value (present in formats 2, 3, 5, 7, 8).
    pub green: Option<u16>,
    /// Blue channel colour value (present in formats 2, 3, 5, 7, 8).
    pub blue: Option<u16>,
    /// Full-waveform packet data (present in formats 9 and 10 only).
    pub waveform: Option<WaveformPacket>,
}

impl Point3D {
    /// Create a new point at `(x, y, z)` with all other fields zeroed / `None`.
    #[inline]
    pub fn new(x: f64, y: f64, z: f64) -> Self {
        Self {
            x,
            y,
            z,
            intensity: 0,
            return_number: 1,
            number_of_returns: 1,
            classification: 0,
            scan_angle_rank: 0,
            user_data: 0,
            point_source_id: 0,
            gps_time: None,
            red: None,
            green: None,
            blue: None,
            waveform: None,
        }
    }

    /// Builder: set the intensity value.
    #[inline]
    pub fn with_intensity(mut self, intensity: u16) -> Self {
        self.intensity = intensity;
        self
    }

    /// Builder: set the ASPRS classification code.
    #[inline]
    pub fn with_classification(mut self, class: u8) -> Self {
        self.classification = class;
        self
    }

    /// Builder: set red / green / blue colour values.
    #[inline]
    pub fn with_color(mut self, r: u16, g: u16, b: u16) -> Self {
        self.red = Some(r);
        self.green = Some(g);
        self.blue = Some(b);
        self
    }

    /// Builder: set the GPS timestamp.
    #[inline]
    pub fn with_gps_time(mut self, t: f64) -> Self {
        self.gps_time = Some(t);
        self
    }

    /// 3-D Euclidean distance to another point.
    #[inline]
    pub fn distance_to(&self, other: &Point3D) -> f64 {
        let dx = self.x - other.x;
        let dy = self.y - other.y;
        let dz = self.z - other.z;
        (dx * dx + dy * dy + dz * dz).sqrt()
    }

    /// 2-D horizontal distance (ignores Z) to another point.
    #[inline]
    pub fn distance_2d(&self, other: &Point3D) -> f64 {
        let dx = self.x - other.x;
        let dy = self.y - other.y;
        (dx * dx + dy * dy).sqrt()
    }

    /// Human-readable name for the ASPRS LAS 1.4 classification code.
    ///
    /// Returns the standard name for codes 0–18 and `"Reserved/Unknown"` for
    /// everything else.
    pub fn classification_name(&self) -> &'static str {
        match self.classification {
            0 => "Created, Never Classified",
            1 => "Unclassified",
            2 => "Ground",
            3 => "Low Vegetation",
            4 => "Medium Vegetation",
            5 => "High Vegetation",
            6 => "Building",
            7 => "Low Point (Noise)",
            8 => "Reserved",
            9 => "Water",
            10 => "Rail",
            11 => "Road Surface",
            12 => "Reserved",
            13 => "Wire - Guard (Shield)",
            14 => "Wire - Conductor (Phase)",
            15 => "Transmission Tower",
            16 => "Wire-Structure Connector (Insulator)",
            17 => "Bridge Deck",
            18 => "High Noise",
            _ => "Reserved/Unknown",
        }
    }
}

// ---------------------------------------------------------------------------
// BoundingBox3D
// ---------------------------------------------------------------------------

/// An axis-aligned 3-D bounding box.
///
/// Invariant: `min_x ≤ max_x`, `min_y ≤ max_y`, `min_z ≤ max_z`.
/// This invariant is enforced by [`BoundingBox3D::new`].
#[derive(Debug, Clone, PartialEq)]
pub struct BoundingBox3D {
    /// Minimum X coordinate.
    pub min_x: f64,
    /// Minimum Y coordinate.
    pub min_y: f64,
    /// Minimum Z coordinate.
    pub min_z: f64,
    /// Maximum X coordinate.
    pub max_x: f64,
    /// Maximum Y coordinate.
    pub max_y: f64,
    /// Maximum Z coordinate.
    pub max_z: f64,
}

impl BoundingBox3D {
    /// Construct a new bounding box.
    ///
    /// Returns `None` if any `min > max` invariant is violated.
    pub fn new(
        min_x: f64,
        min_y: f64,
        min_z: f64,
        max_x: f64,
        max_y: f64,
        max_z: f64,
    ) -> Option<Self> {
        if min_x > max_x || min_y > max_y || min_z > max_z {
            return None;
        }
        Some(Self {
            min_x,
            min_y,
            min_z,
            max_x,
            max_y,
            max_z,
        })
    }

    /// Build the tightest bounding box that contains every point in `points`.
    ///
    /// Returns `None` when `points` is empty.
    pub fn from_points(points: &[Point3D]) -> Option<Self> {
        let first = points.first()?;
        let mut min_x = first.x;
        let mut min_y = first.y;
        let mut min_z = first.z;
        let mut max_x = first.x;
        let mut max_y = first.y;
        let mut max_z = first.z;

        for p in points.iter().skip(1) {
            if p.x < min_x {
                min_x = p.x;
            }
            if p.y < min_y {
                min_y = p.y;
            }
            if p.z < min_z {
                min_z = p.z;
            }
            if p.x > max_x {
                max_x = p.x;
            }
            if p.y > max_y {
                max_y = p.y;
            }
            if p.z > max_z {
                max_z = p.z;
            }
        }

        Some(Self {
            min_x,
            min_y,
            min_z,
            max_x,
            max_y,
            max_z,
        })
    }

    /// Return `true` when `p` is strictly inside or on the boundary.
    #[inline]
    pub fn contains(&self, p: &Point3D) -> bool {
        p.x >= self.min_x
            && p.x <= self.max_x
            && p.y >= self.min_y
            && p.y <= self.max_y
            && p.z >= self.min_z
            && p.z <= self.max_z
    }

    /// Return `true` when the XY footprints of `self` and `other` overlap (or
    /// touch).
    #[inline]
    pub fn intersects_2d(&self, other: &BoundingBox3D) -> bool {
        self.min_x <= other.max_x
            && self.max_x >= other.min_x
            && self.min_y <= other.max_y
            && self.max_y >= other.min_y
    }

    /// Return `true` when `self` and `other` share any volume (or face).
    #[inline]
    pub fn intersects_3d(&self, other: &BoundingBox3D) -> bool {
        self.intersects_2d(other) && self.min_z <= other.max_z && self.max_z >= other.min_z
    }

    /// Centre point of the box as `(cx, cy, cz)`.
    #[inline]
    pub fn center(&self) -> (f64, f64, f64) {
        (
            (self.min_x + self.max_x) * 0.5,
            (self.min_y + self.max_y) * 0.5,
            (self.min_z + self.max_z) * 0.5,
        )
    }

    /// Length of the space diagonal (3-D).
    #[inline]
    pub fn diagonal(&self) -> f64 {
        let dx = self.max_x - self.min_x;
        let dy = self.max_y - self.min_y;
        let dz = self.max_z - self.min_z;
        (dx * dx + dy * dy + dz * dz).sqrt()
    }

    /// Return a box expanded symmetrically in every direction by `delta`.
    ///
    /// If `delta` is negative the box may collapse; the result will still
    /// satisfy the `min ≤ max` invariant because `f64` arithmetic naturally
    /// produces equal values when expansion < 0 produces min > max (the
    /// caller should validate the result if that matters).
    #[inline]
    pub fn expand_by(&self, delta: f64) -> Self {
        Self {
            min_x: self.min_x - delta,
            min_y: self.min_y - delta,
            min_z: self.min_z - delta,
            max_x: self.max_x + delta,
            max_y: self.max_y + delta,
            max_z: self.max_z + delta,
        }
    }

    /// Volume of the box.
    #[inline]
    pub fn volume(&self) -> f64 {
        (self.max_x - self.min_x) * (self.max_y - self.min_y) * (self.max_z - self.min_z)
    }

    /// Subdivide the box into eight equal octant children.
    ///
    /// Children are ordered by `(x_high, y_high, z_high)` bits:
    /// index 0 = `(lo, lo, lo)`, index 7 = `(hi, hi, hi)`.
    pub fn split_octants(&self) -> [BoundingBox3D; 8] {
        let (cx, cy, cz) = self.center();
        [
            // 0: x-lo, y-lo, z-lo
            BoundingBox3D {
                min_x: self.min_x,
                min_y: self.min_y,
                min_z: self.min_z,
                max_x: cx,
                max_y: cy,
                max_z: cz,
            },
            // 1: x-lo, y-lo, z-hi
            BoundingBox3D {
                min_x: self.min_x,
                min_y: self.min_y,
                min_z: cz,
                max_x: cx,
                max_y: cy,
                max_z: self.max_z,
            },
            // 2: x-lo, y-hi, z-lo
            BoundingBox3D {
                min_x: self.min_x,
                min_y: cy,
                min_z: self.min_z,
                max_x: cx,
                max_y: self.max_y,
                max_z: cz,
            },
            // 3: x-lo, y-hi, z-hi
            BoundingBox3D {
                min_x: self.min_x,
                min_y: cy,
                min_z: cz,
                max_x: cx,
                max_y: self.max_y,
                max_z: self.max_z,
            },
            // 4: x-hi, y-lo, z-lo
            BoundingBox3D {
                min_x: cx,
                min_y: self.min_y,
                min_z: self.min_z,
                max_x: self.max_x,
                max_y: cy,
                max_z: cz,
            },
            // 5: x-hi, y-lo, z-hi
            BoundingBox3D {
                min_x: cx,
                min_y: self.min_y,
                min_z: cz,
                max_x: self.max_x,
                max_y: cy,
                max_z: self.max_z,
            },
            // 6: x-hi, y-hi, z-lo
            BoundingBox3D {
                min_x: cx,
                min_y: cy,
                min_z: self.min_z,
                max_x: self.max_x,
                max_y: self.max_y,
                max_z: cz,
            },
            // 7: x-hi, y-hi, z-hi
            BoundingBox3D {
                min_x: cx,
                min_y: cy,
                min_z: cz,
                max_x: self.max_x,
                max_y: self.max_y,
                max_z: self.max_z,
            },
        ]
    }
}