pub(crate) mod copc_types;
pub(crate) mod node_store;
pub(crate) mod octree;
pub(crate) mod validate;
pub(crate) mod writer;
pub(crate) mod chunking;
pub use chunking::{
ChunkPlan, HeaderBoundsMismatch, PlannedChunk, compute_chunk_target, select_grid_size,
};
#[cfg(feature = "tools")]
pub mod tools;
use std::marker::PhantomData;
use std::path::{Path, PathBuf};
use tracing::info;
use copc_types::VoxelKey;
use octree::OctreeBuilder;
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("CRS mismatch: {file_a:?} has a different WKT CRS than {file_b:?}")]
CrsMismatch {
file_a: PathBuf,
file_b: PathBuf,
},
#[error(
"Point format mismatch: {file_a:?} has format {format_a} but {file_b:?} has format {format_b}"
)]
PointFormatMismatch {
file_a: PathBuf,
format_a: u8,
file_b: PathBuf,
format_b: u8,
},
#[error("Temporal index requested but input point format {format} does not include GPS time")]
NoGpsTime {
format: u8,
},
#[error("No LAZ/LAS files found in {path:?}")]
NoInputFiles {
path: PathBuf,
},
#[error(transparent)]
Internal(#[from] anyhow::Error),
}
pub type Result<T> = std::result::Result<T, Error>;
pub struct Scanned(());
pub struct Validated(());
pub struct Distributed(());
pub struct Built(());
#[derive(Debug, Clone)]
pub enum ProgressEvent {
StageStart {
name: &'static str,
total: u64,
},
StageProgress {
done: u64,
},
StageDone,
}
pub trait ProgressObserver: Send + Sync {
fn on_progress(&self, event: ProgressEvent);
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum TempCompression {
#[default]
None,
Lz4,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum NodeStorage {
#[default]
Files,
Packed,
}
pub struct PipelineConfig {
pub memory_budget: u64,
pub temp_dir: Option<PathBuf>,
pub temporal_index: bool,
pub temporal_stride: u32,
pub progress: Option<std::sync::Arc<dyn ProgressObserver>>,
pub chunk_target_override: Option<u64>,
pub temp_compression: TempCompression,
pub node_storage: NodeStorage,
}
impl PipelineConfig {
pub(crate) fn report(&self, event: ProgressEvent) {
if let Some(ref observer) = self.progress {
observer.on_progress(event);
}
}
}
pub struct Pipeline<S> {
inner: PipelineInner,
_stage: PhantomData<S>,
}
struct PipelineInner {
input_files: Vec<PathBuf>,
config: PipelineConfig,
scan_results: Vec<octree::ScanResult>,
canonical_wkt: Option<Vec<u8>>,
validated: Option<validate::ValidatedInputs>,
builder: Option<OctreeBuilder>,
node_keys: Option<Vec<(VoxelKey, usize)>>,
}
impl Pipeline<Scanned> {
pub fn scan(input_files: &[PathBuf], config: PipelineConfig) -> Result<Self> {
config.report(ProgressEvent::StageStart {
name: "Scanning",
total: input_files.len() as u64,
});
let scan_output = OctreeBuilder::scan(input_files, &config)?;
config.report(ProgressEvent::StageDone);
Ok(Pipeline {
inner: PipelineInner {
input_files: input_files.to_vec(),
config,
scan_results: scan_output.results,
canonical_wkt: scan_output.canonical_wkt,
validated: None,
builder: None,
node_keys: None,
},
_stage: PhantomData,
})
}
pub fn validate(mut self) -> Result<Pipeline<Validated>> {
info!("=== Validating inputs ===");
let validated = validate::validate(
&self.inner.input_files,
&self.inner.scan_results,
self.inner.canonical_wkt.take(),
self.inner.config.temporal_index,
)?;
self.inner.validated = Some(validated);
Ok(Pipeline {
inner: self.inner,
_stage: PhantomData,
})
}
}
impl Pipeline<Validated> {
pub fn analyze_chunking(&self, chunk_target_override: Option<u64>) -> Result<ChunkPlan> {
let validated = self.inner.validated.as_ref().expect("validated");
Ok(chunking::analyze_chunking(
&self.inner.input_files,
&self.inner.scan_results,
validated,
&self.inner.config,
chunk_target_override,
)?)
}
pub fn distribute(mut self) -> Result<Pipeline<Distributed>> {
let validated = self.inner.validated.as_ref().unwrap();
let mut builder =
OctreeBuilder::from_scan(&self.inner.scan_results, validated, &self.inner.config)?;
builder.distribute(&self.inner.input_files, &self.inner.config)?;
self.inner.builder = Some(builder);
Ok(Pipeline {
inner: self.inner,
_stage: PhantomData,
})
}
}
impl Pipeline<Distributed> {
pub fn header_bounds_mismatch(&self) -> Option<&HeaderBoundsMismatch> {
self.inner
.builder
.as_ref()
.and_then(|b| b.chunked_plan.as_ref())
.and_then(|p| p.header_mismatch.as_ref())
}
pub fn build(mut self) -> Result<Pipeline<Built>> {
let builder = self.inner.builder.as_ref().unwrap();
let node_keys = builder.build_node_map(&self.inner.config)?;
self.inner.node_keys = Some(node_keys);
Ok(Pipeline {
inner: self.inner,
_stage: PhantomData,
})
}
}
impl Pipeline<Built> {
pub fn write(self, output_path: impl AsRef<Path>) -> Result<()> {
let output_path = output_path.as_ref();
let node_count = self
.inner
.node_keys
.as_ref()
.unwrap()
.iter()
.filter(|(_, c)| *c > 0)
.count() as u64;
self.inner.config.report(ProgressEvent::StageStart {
name: "Writing",
total: node_count,
});
let builder = self.inner.builder.as_ref().unwrap();
let node_keys = self.inner.node_keys.as_ref().unwrap();
writer::write_copc(output_path, builder, node_keys, &self.inner.config)?;
self.inner.config.report(ProgressEvent::StageDone);
Ok(())
}
}
pub fn collect_input_files(raw: PathBuf) -> Result<Vec<PathBuf>> {
if raw.is_dir() {
let mut files: Vec<PathBuf> = std::fs::read_dir(&raw)
.map_err(|e| anyhow::anyhow!("Cannot read directory {:?}: {}", raw, e))?
.filter_map(|e| e.ok())
.map(|e| e.path())
.filter(|p| {
p.is_file()
&& matches!(
p.extension().and_then(|s| s.to_str()),
Some("laz") | Some("las") | Some("LAZ") | Some("LAS")
)
})
.collect();
files.sort();
if files.is_empty() {
return Err(Error::NoInputFiles { path: raw });
}
Ok(files)
} else {
Ok(vec![raw])
}
}