pub mod copc;
pub(crate) mod crs;
mod e57;
pub mod ept;
#[cfg(not(target_arch = "wasm32"))]
mod ept_write;
pub mod filters;
pub mod grid;
pub mod info;
mod kdtree;
pub mod las;
pub mod names;
mod ply;
mod read;
mod values;
pub mod vector;
pub mod waveform;
mod write;
pub use copc::{CopcInfo, CopcNode, VoxelKey};
pub use ept::{EptDataType, EptDimension, EptInfo};
pub use las::{ExtraBytes, LasHeader, Vlr, standard_value_type};
pub use read::{DEFAULT_CHUNK_POINTS, PointChunks, PointCloudReader, open_points, open_points_bytes, open_points_sparse};
pub use values::{Dimension, ValueType, Values};
pub use waveform::{WavePacketDescriptor, Waveform};
pub use write::{PlyEncoding, PointFormat, PointWriteOptions, PointWriter, 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| d.with_values(d.values.select(keep))).collect(),
}
}
pub fn slice(&self, start: usize, end: usize) -> PointCloud {
PointCloud {
header: self.header.clone(),
crs: self.crs.clone(),
dimensions: self.dimensions.iter().map(|d| d.with_values(d.values.slice(start, end))).collect(),
}
}
pub fn take(&self, idx: &[usize]) -> PointCloud {
PointCloud {
header: self.header.clone(),
crs: self.crs.clone(),
dimensions: self.dimensions.iter().map(|d| d.with_values(d.values.take(idx))).collect(),
}
}
pub fn filter(&self, filter: &PointFilter) -> PointCloud {
if filter.is_empty() {
return self.clone();
}
self.select(&self.filter_mask(filter))
}
pub fn filter_mask(&self, filter: &PointFilter) -> Vec<bool> {
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"));
(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()
}
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)?;
self.transform_coords(&t)?;
self.crs = dst.clone();
let bounds = self.bounds().map(|b| [b[0], b[1]]);
reproject_header(&mut self.header, dst, bounds);
Ok(())
}
pub(crate) fn transform_coords(&mut self, t: &crate::crs::Transformer) -> Result<()> {
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.min(xs.len()).min(ys.len()) {
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);
}
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 reproject_header(header: &mut LasHeader, dst: &SpatialRef, min: Option<[f64; 2]>) {
let s = if dst.is_geographic() { 1e-7 } else { header.scale[0].max(header.scale[1]).clamp(1e-4, 0.01) };
header.scale[0] = s;
header.scale[1] = s;
if let Some(m) = min {
header.offset[0] = round_offset(m[0], s);
header.offset[1] = round_offset(m[1], s);
}
}
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 {
if path.starts_with("ept://") {
return true;
}
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") || name.ends_with(".ply") || name.ends_with(".e57") || name == "ept.json"
}
#[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 chunk_points: usize,
}
pub fn convert(src: &str, dst: &str, options: &ConvertOptions) -> Result<()> {
let format = options.write.format.or_else(|| PointFormat::for_path(dst));
match format {
Some(PointFormat::E57) if cfg!(target_arch = "wasm32") => {}
Some(_) => return convert_streaming(src, dst, options),
None => {
if crate::formats::VectorDriver::for_extension(dst).is_none() {
return Err(crate::Error::InvalidArgument(format!(
"cannot tell the output format of '{dst}': use .las, .laz, .copc.laz, .ply, .e57, ept.json or a vector format"
)));
}
}
}
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 format.is_some() {
return write_points(&cloud, dst, &options.write);
}
let layer = vector::to_layer(&cloud, &options.layer)?;
crate::write_vector(&[layer], dst, &Default::default())
}
#[cfg(unix)]
fn same_file(a: &std::path::Path, b: &std::path::Path) -> bool {
use std::os::unix::fs::MetadataExt;
match (std::fs::metadata(a), std::fs::metadata(b)) {
(Ok(x), Ok(y)) => x.dev() == y.dev() && x.ino() == y.ino(),
_ => false,
}
}
#[cfg(not(unix))]
fn same_file(_: &std::path::Path, _: &std::path::Path) -> bool {
false
}
fn convert_streaming(src: &str, dst: &str, options: &ConvertOptions) -> Result<()> {
let mut reader = open_points(src)?;
let mut template = reader.template()?;
if let Some(crs) = &options.assign_srs {
template.crs = crs.clone();
}
let transformer = match &options.target_srs {
Some(dst_srs) => {
if template.crs.is_unknown() {
return Err(crate::Error::Crs("the point cloud has no CRS to reproject from".into()));
}
let t = crate::crs::Transformer::new(&template.crs, dst_srs)?;
let h = &template.header;
let min = t.transform_bounds((h.min[0], h.min[1], h.max[0], h.max[1])).ok().map(|b| [b.0, b.1]);
reproject_header(&mut template.header, dst_srs, min);
template.crs = dst_srs.clone();
Some(t)
}
None => None,
};
let mut writer = PointWriter::create(dst, &template, &options.write)?;
reader.start_chunks(&options.read, options.chunk_points)?;
while let Some(mut chunk) = reader.next_chunk()? {
if let Some(t) = &transformer {
chunk.transform_coords(t)?;
}
writer.write(&chunk)?;
}
writer.finish_with_waveforms(&mut reader)?;
if let Some(wdp) = reader.waveform_file()
&& PointFormat::for_path(dst).is_some_and(|f| matches!(f, PointFormat::Las | PointFormat::Laz))
{
let target = std::path::Path::new(dst).with_extension("wdp");
let same = match (std::fs::canonicalize(wdp), std::fs::canonicalize(&target)) {
(Ok(a), Ok(b)) => a == b || same_file(&a, &b),
_ => false,
};
if !same {
std::fs::copy(wdp, target)?;
}
}
Ok(())
}