pub mod copc;
pub(crate) mod crs;
pub mod grid;
pub mod info;
pub mod las;
mod read;
mod values;
pub mod vector;
mod write;
pub use copc::{CopcInfo, CopcNode, VoxelKey};
pub use las::{ExtraBytes, LasHeader, Vlr, standard_value_type};
pub use read::{PointCloudReader, open_points, open_points_bytes};
pub use values::{Dimension, ValueType, Values};
pub use write::{PointFormat, PointWriteOptions, from_dimensions, write_points, write_points_bytes};
use crate::Result;
use crate::crs::SpatialRef;
use serde::Serialize;
#[derive(Debug, Clone, PartialEq)]
pub struct PointCloud {
pub header: LasHeader,
pub crs: SpatialRef,
pub dimensions: Vec<Dimension>,
}
impl PointCloud {
pub fn new(dimensions: Vec<Dimension>, crs: SpatialRef) -> Self {
Self { header: LasHeader::default(), crs, dimensions }
}
pub fn len(&self) -> usize {
self.dimensions.first().map_or(0, |d| d.values.len())
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
pub fn dim(&self, name: &str) -> Option<&Dimension> {
self.dimensions.iter().find(|d| d.name == name)
}
pub fn dim_mut(&mut self, name: &str) -> Option<&mut Dimension> {
self.dimensions.iter_mut().find(|d| d.name == name)
}
pub fn dimension_names(&self) -> Vec<&str> {
self.dimensions.iter().map(|d| d.name.as_str()).collect()
}
pub fn coords(&self, name: &str) -> Option<&[f64]> {
match &self.dim(name)?.values {
Values::F64(v) => Some(v),
_ => None,
}
}
pub fn bounds(&self) -> Option<[f64; 6]> {
let (x0, x1) = self.dim("x")?.values.min_max()?;
let (y0, y1) = self.dim("y")?.values.min_max()?;
let (z0, z1) = self.dim("z")?.values.min_max()?;
Some([x0, y0, z0, x1, y1, z1])
}
pub fn select(&self, keep: &[bool]) -> PointCloud {
PointCloud {
header: self.header.clone(),
crs: self.crs.clone(),
dimensions: self.dimensions.iter().map(|d| Dimension { values: d.values.select(keep), ..d.clone() }).collect(),
}
}
pub fn filter(&self, filter: &PointFilter) -> PointCloud {
if filter.is_empty() {
return self.clone();
}
let get = |name: &str| self.dim(name);
let (x, y, z) = (get("x"), get("y"), get("z"));
let (rn, nr, class) = (get("return_number"), get("number_of_returns"), get("classification"));
let keep: Vec<bool> = (0..self.len())
.map(|i| {
let v = |d: Option<&Dimension>| d.map_or(0.0, |d| d.values.get_f64(i));
filter.keeps(&las::RecordCore {
x: v(x),
y: v(y),
z: v(z),
return_number: v(rn) as u8,
number_of_returns: v(nr) as u8,
classification: v(class) as u8,
})
})
.collect();
self.select(&keep)
}
pub fn reproject(&mut self, dst: &SpatialRef) -> Result<()> {
if self.crs.is_unknown() {
return Err(crate::Error::Crs("the point cloud has no CRS to reproject from".into()));
}
let t = crate::crs::Transformer::new(&self.crs, dst)?;
let n = self.len();
let xs = self.coords("x").map(<[f64]>::to_vec).unwrap_or_default();
let ys = self.coords("y").map(<[f64]>::to_vec).unwrap_or_default();
let mut nx = Vec::with_capacity(n);
let mut ny = Vec::with_capacity(n);
for i in 0..n {
let (a, b) = t.transform(xs[i], ys[i])?;
nx.push(a);
ny.push(b);
}
if let Some(d) = self.dim_mut("x") {
d.values = Values::F64(nx);
}
if let Some(d) = self.dim_mut("y") {
d.values = Values::F64(ny);
}
self.crs = dst.clone();
let geographic = dst.is_geographic();
let s = if geographic { 1e-7 } else { self.header.scale[0].max(self.header.scale[1]).clamp(1e-4, 0.01) };
self.header.scale[0] = s;
self.header.scale[1] = s;
if let Some(b) = self.bounds() {
self.header.offset[0] = round_offset(b[0], s);
self.header.offset[1] = round_offset(b[1], s);
}
Ok(())
}
pub fn append(&mut self, other: &PointCloud) -> Result<()> {
if self.dimensions.is_empty() {
self.dimensions = other.dimensions.clone();
return Ok(());
}
for d in &mut self.dimensions {
let o = other
.dim(&d.name)
.ok_or_else(|| crate::Error::InvalidArgument(format!("the point clouds differ: no '{}' in the other", d.name)))?;
d.values.extend_from(&o.values);
}
Ok(())
}
pub fn classification_counts(&self) -> Vec<(u8, u64)> {
let mut counts = [0u64; 256];
if let Some(Values::U8(v)) = self.dim("classification").map(|d| &d.values) {
for c in v {
counts[*c as usize] += 1;
}
}
counts.iter().enumerate().filter(|(_, n)| **n > 0).map(|(c, n)| (c as u8, *n)).collect()
}
pub fn return_counts(&self) -> Vec<(u8, u64)> {
let mut counts = [0u64; 256];
if let Some(Values::U8(v)) = self.dim("return_number").map(|d| &d.values) {
for c in v {
counts[*c as usize] += 1;
}
}
counts.iter().enumerate().filter(|(_, n)| **n > 0).map(|(c, n)| (c as u8, *n)).collect()
}
}
pub(crate) fn round_offset(min: f64, scale: f64) -> f64 {
if !min.is_finite() {
return 0.0;
}
let unit = if scale >= 1e-3 { 1000.0 } else { 1.0 };
(min / unit).floor() * unit
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum ReturnKind {
First,
Last,
Intermediate,
Only,
}
impl ReturnKind {
pub fn parse(s: &str) -> Result<Self> {
match s.trim().to_ascii_lowercase().as_str() {
"first" => Ok(ReturnKind::First),
"last" => Ok(ReturnKind::Last),
"intermediate" => Ok(ReturnKind::Intermediate),
"only" | "single" => Ok(ReturnKind::Only),
other => Err(crate::Error::InvalidArgument(format!("unknown return kind '{other}' (first, last, intermediate, only)"))),
}
}
fn matches(self, rn: u8, nr: u8) -> bool {
match self {
ReturnKind::First => rn == 1 && nr > 1,
ReturnKind::Last => rn == nr && nr > 1,
ReturnKind::Intermediate => rn > 1 && rn < nr,
ReturnKind::Only => nr == 1,
}
}
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct PointFilter {
pub bounds: Option<[f64; 4]>,
pub z_range: Option<(f64, f64)>,
pub classes: Vec<u8>,
pub returns: Vec<ReturnKind>,
}
impl PointFilter {
pub fn is_empty(&self) -> bool {
self.bounds.is_none() && self.z_range.is_none() && self.classes.is_empty() && self.returns.is_empty()
}
pub(crate) fn keeps(&self, p: &las::RecordCore) -> bool {
if let Some([x0, y0, x1, y1]) = self.bounds
&& !(p.x >= x0 && p.x <= x1 && p.y >= y0 && p.y <= y1)
{
return false;
}
if let Some((z0, z1)) = self.z_range
&& !(p.z >= z0 && p.z <= z1)
{
return false;
}
if !self.classes.is_empty() && !self.classes.contains(&p.classification) {
return false;
}
if !self.returns.is_empty() && !self.returns.iter().any(|r| r.matches(p.return_number, p.number_of_returns)) {
return false;
}
true
}
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct PointReadOptions {
pub filter: PointFilter,
pub max_level: Option<u32>,
pub resolution: Option<f64>,
pub max_points: Option<u64>,
}
pub fn read_points(path: &str, options: &PointReadOptions) -> Result<PointCloud> {
open_points(path)?.read(options)
}
pub fn is_point_cloud_path(path: &str) -> bool {
let name = crate::vfs::Location::parse(path).file_name().to_ascii_lowercase();
let name = name.split(['?', '#']).next().unwrap_or_default();
name.ends_with(".las") || name.ends_with(".laz")
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct ConvertOptions {
pub read: PointReadOptions,
pub assign_srs: Option<SpatialRef>,
pub target_srs: Option<SpatialRef>,
pub write: PointWriteOptions,
pub layer: vector::ToLayerOptions,
}
pub fn convert(src: &str, dst: &str, options: &ConvertOptions) -> Result<()> {
let mut cloud = read_points(src, &options.read)?;
if let Some(crs) = &options.assign_srs {
cloud.crs = crs.clone();
}
if let Some(dst_srs) = &options.target_srs {
cloud.reproject(dst_srs)?;
}
if options.write.format.is_some() || PointFormat::for_path(dst).is_some() {
return write_points(&cloud, dst, &options.write);
}
if crate::formats::VectorDriver::for_extension(dst).is_some() {
let layer = vector::to_layer(&cloud, &options.layer)?;
return crate::write_vector(&[layer], dst, &Default::default());
}
Err(crate::Error::InvalidArgument(format!("cannot tell the output format of '{dst}': use .las, .laz, .copc.laz or a vector format")))
}