use super::copc::{NodeSink, VoxelKey};
use super::ept::EptDataType;
use super::las::{LasHeader, Layout};
use super::values::{ValueType, Values};
use super::write::LasStreamWriter;
use crate::crs::SpatialRef;
use crate::{Error, Result};
use serde_json::{Value, json};
use std::collections::BTreeMap;
use std::io::{Cursor, Write};
use std::path::{Path, PathBuf};
pub(crate) fn dataset_dir(path: &str) -> PathBuf {
let p = path.strip_prefix("ept://").unwrap_or(path);
let p = Path::new(p);
if p.file_name().is_some_and(|n| n.eq_ignore_ascii_case("ept.json")) {
p.parent().map(Path::to_path_buf).unwrap_or_default()
} else {
p.to_path_buf()
}
}
pub(crate) struct EptTarget {
dir: PathBuf,
work: PathBuf,
data_type: EptDataType,
crs: SpatialRef,
}
struct SchemaDim {
name: String,
source: String,
stored: ValueType,
scale: Option<(f64, f64)>,
factor: f64,
derive: Option<fn(f64) -> f64>,
}
fn legacy_class(c: f64) -> f64 {
if c == 12.0 { 1.0 } else { c }
}
fn legacy_overlap(c: f64) -> f64 {
if c == 12.0 { 1.0 } else { 0.0 }
}
impl Drop for EptTarget {
fn drop(&mut self) {
if self.work.exists() {
let _ = std::fs::remove_dir_all(&self.work);
}
}
}
fn unique_dir(dir: &Path, prefix: &str) -> Result<PathBuf> {
for k in 0u32.. {
let d = dir.join(format!("{prefix}-{k}"));
match std::fs::create_dir(&d) {
Ok(()) => return Ok(d),
Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => continue,
Err(e) => return Err(Error::Io(std::io::Error::new(e.kind(), format!("'{}': {e}", d.display())))),
}
}
Err(Error::Io(std::io::Error::other(format!("'{}': no free directory name", dir.display()))))
}
impl EptTarget {
pub fn new(dir: PathBuf, data_type: EptDataType, crs: SpatialRef) -> Result<Self> {
let io = |d: &Path, e: std::io::Error| Error::Io(std::io::Error::new(e.kind(), format!("'{}': {e}", d.display())));
std::fs::create_dir_all(&dir).map_err(|e| io(&dir, e))?;
let work = unique_dir(&dir, ".terrio-ept-new")?;
for sub in ["ept-data", "ept-hierarchy"] {
let d = work.join(sub);
std::fs::create_dir_all(&d).map_err(|e| io(&d, e))?;
}
Ok(Self { dir, work, data_type, crs })
}
fn commit(&self) -> Result<()> {
let io = |d: &Path, e: std::io::Error| Error::Io(std::io::Error::new(e.kind(), format!("'{}': {e}", d.display())));
let old = unique_dir(&self.dir, ".terrio-ept-old")?;
let names = ["ept.json", "ept-data", "ept-hierarchy"];
let mut aside = Vec::new();
let mut moved_in = Vec::new();
let swapped = (|| -> Result<()> {
for name in names {
let current = self.dir.join(name);
if current.exists() {
std::fs::rename(¤t, old.join(name)).map_err(|e| io(¤t, e))?;
aside.push(name);
}
}
for name in names.iter().rev() {
let target = self.dir.join(name);
std::fs::rename(self.work.join(name), &target).map_err(|e| io(&target, e))?;
moved_in.push(*name);
}
Ok(())
})();
if let Err(e) = swapped {
let mut restored = true;
for name in &moved_in {
restored &= std::fs::rename(self.dir.join(name), self.work.join(name)).is_ok();
}
for name in &aside {
restored &= std::fs::rename(old.join(name), self.dir.join(name)).is_ok();
}
if !restored {
return Err(Error::Io(std::io::Error::other(format!(
"{e}; the previous dataset could not be restored entirely: what remains of it is in '{}'",
old.display()
))));
}
let _ = std::fs::remove_dir_all(&old);
return Err(e);
}
std::fs::remove_dir_all(&old).map_err(|e| io(&old, e))?;
std::fs::remove_dir(&self.work).map_err(|e| io(&self.work, e))
}
pub fn write(
self,
header: LasHeader,
layout: &Layout,
bounds: [f64; 6],
conforming: [f64; 6],
span: u64,
build: impl FnOnce(&mut dyn NodeSink) -> Result<()>,
) -> Result<()> {
self.build_all(header, layout, bounds, conforming, span, build).and_then(|()| self.commit())
}
fn build_all(
&self,
header: LasHeader,
layout: &Layout,
bounds: [f64; 6],
conforming: [f64; 6],
span: u64,
build: impl FnOnce(&mut dyn NodeSink) -> Result<()>,
) -> Result<()> {
let schema = schema(&header, layout)?;
let mut sink = EptSink { target: self, header: &header, layout, schema: &schema, counts: BTreeMap::new() };
build(&mut sink)?;
let counts = std::mem::take(&mut sink.counts);
let points: u64 = counts.values().sum();
let page: serde_json::Map<String, Value> = counts.iter().map(|(k, n)| (key_name(*k), json!(n))).collect();
write_json(&self.work.join("ept-hierarchy/0-0-0-0.json"), &Value::Object(page))?;
let schema_json: Vec<Value> = schema
.iter()
.map(|d| {
let (kind, size) = type_name(d.stored);
let mut o = json!({ "name": d.name, "type": kind, "size": size });
if let Some((s, off)) = d.scale {
o["scale"] = json!(s);
o["offset"] = json!(off);
}
o
})
.collect();
let mut srs = serde_json::Map::new();
if let Some(code) = self.crs.epsg {
srs.insert("authority".into(), json!("EPSG"));
srs.insert("horizontal".into(), json!(code.to_string()));
}
if let Some(wkt) = self.crs.to_wkt() {
srs.insert("wkt".into(), json!(wkt));
}
let ept = json!({
"bounds": bounds,
"boundsConforming": conforming,
"dataType": self.data_type.name(),
"hierarchyType": "json",
"points": points,
"schema": schema_json,
"span": span,
"srs": Value::Object(srs),
"version": "1.1.0",
});
write_json(&self.work.join("ept.json"), &ept)
}
}
fn write_json(path: &Path, v: &Value) -> Result<()> {
let text = serde_json::to_string_pretty(v).map_err(|e| Error::InvalidData(format!("JSON: {e}")))?;
std::fs::write(path, text).map_err(|e| Error::Io(std::io::Error::new(e.kind(), format!("'{}': {e}", path.display()))))
}
fn key_name(k: VoxelKey) -> String {
format!("{}-{}-{}-{}", k.level, k.x, k.y, k.z)
}
fn type_name(t: ValueType) -> (&'static str, usize) {
let kind = match t {
ValueType::I8 | ValueType::I16 | ValueType::I32 | ValueType::I64 => "signed",
ValueType::U8 | ValueType::U16 | ValueType::U32 | ValueType::U64 => "unsigned",
ValueType::F32 | ValueType::F64 => "float",
};
(kind, t.size())
}
fn schema(header: &LasHeader, layout: &Layout) -> Result<Vec<SchemaDim>> {
let empty = las_bytes(header, layout, &[], false)?;
let reader = super::read::open_points_bytes(empty, "schema.las")?;
let legacy = header.point_format < 6;
let mut out = Vec::new();
for (name, t) in reader.dimensions() {
let axis = ["x", "y", "z"].iter().position(|a| *a == name);
let dim = match (name.as_str(), axis) {
(_, Some(k)) => SchemaDim {
name: super::names::to_pdal(&name),
source: name.clone(),
stored: ValueType::I32,
scale: Some((header.scale[k], header.offset[k])),
factor: 1.0,
derive: None,
},
("scan_angle", _) => SchemaDim {
name: "ScanAngleRank".into(),
source: name.clone(),
stored: ValueType::F32,
scale: None,
factor: 0.006,
derive: None,
},
("scan_angle_rank", _) => SchemaDim {
name: "ScanAngleRank".into(),
source: name.clone(),
stored: ValueType::F32,
scale: None,
factor: 1.0,
derive: None,
},
(n, _)
if n.starts_with("wavepacket")
|| n.starts_with("waveform")
|| n.starts_with("return_point")
|| n == "x_t"
|| n == "y_t"
|| n == "z_t" =>
{
continue;
}
("classification", _) if legacy => SchemaDim {
name: "Classification".into(),
source: name.clone(),
stored: t,
scale: None,
factor: 1.0,
derive: Some(legacy_class),
},
_ => SchemaDim { name: super::names::to_pdal(&name), source: name.clone(), stored: t, scale: None, factor: 1.0, derive: None },
};
out.push(dim);
if legacy && name == "withheld" {
out.push(SchemaDim {
name: "Overlap".into(),
source: "classification".into(),
stored: ValueType::U8,
scale: None,
factor: 1.0,
derive: Some(legacy_overlap),
});
}
}
Ok(out)
}
fn las_bytes(header: &LasHeader, layout: &Layout, records: &[u8], compressed: bool) -> Result<Vec<u8>> {
let mut h = header.clone();
h.compressed = compressed;
h.vlrs.retain(|v| !v.is_laszip());
h.evlrs.retain(|v| !v.is_waveform_data());
h.start_of_waveform_data = 0;
h.waveform_data_size = 0;
let mut c = Cursor::new(Vec::new());
let mut w = LasStreamWriter::new(&mut c, h, layout.clone())?;
w.write_records(records)?;
w.finish(None)?;
Ok(c.into_inner())
}
struct EptSink<'a> {
target: &'a EptTarget,
header: &'a LasHeader,
layout: &'a Layout,
schema: &'a [SchemaDim],
counts: BTreeMap<VoxelKey, u64>,
}
impl NodeSink for EptSink<'_> {
fn write_node(&mut self, key: VoxelKey, records: &[u8], rl: usize) -> Result<()> {
let n = records.len() / rl;
if n == 0 {
return Ok(());
}
let t = self.target;
if self.counts.contains_key(&key) {
return Err(Error::InvalidData(format!("EPT: node {} written twice", key_name(key))));
}
let path = t.work.join(format!("ept-data/{}.{}", key_name(key), t.data_type.extension()));
let bytes = match t.data_type {
EptDataType::Laszip => las_bytes(self.header, self.layout, records, true)?,
kind => {
let cloud = super::read::open_points_bytes(las_bytes(self.header, self.layout, records, false)?, "node.las")?
.read(&Default::default())?;
let cols: Vec<Values> = self
.schema
.iter()
.map(|d| {
let src = cloud.dim(&d.source).ok_or_else(|| Error::InvalidData(format!("EPT: no '{}' to write", d.source)))?;
Ok(match d.scale {
Some((s, o)) => Values::I32(src.to_f64().iter().map(|v| ((v - o) / s).round() as i32).collect()),
None => match d.derive {
Some(f) => Values::F64(src.to_f64().into_iter().map(f).collect()).cast(d.stored),
None if d.factor != 1.0 => Values::F64(src.to_f64().iter().map(|v| v * d.factor).collect()).cast(d.stored),
None => src.values.cast(d.stored),
},
})
})
.collect::<Result<_>>()?;
let len: usize = self.schema.iter().map(|d| d.stored.size()).sum();
let mut out = vec![0u8; n * len];
for (i, rec) in out.chunks_exact_mut(len).enumerate() {
let mut at = 0;
for (c, d) in cols.iter().zip(self.schema) {
let size = d.stored.size();
c.write_le(i, &mut rec[at..at + size]);
at += size;
}
}
if kind == EptDataType::Zstandard { zstd::bulk::compress(&out, 3)? } else { out }
}
};
let mut f = std::io::BufWriter::new(
std::fs::File::create(&path).map_err(|e| Error::Io(std::io::Error::new(e.kind(), format!("'{}': {e}", path.display()))))?,
);
f.write_all(&bytes)?;
f.flush()?;
*self.counts.entry(key).or_default() += n as u64;
Ok(())
}
}