use super::{PointCloudReader, PointReadOptions};
use crate::Result;
use crate::crs::{SpatialRef, Transformer};
use crate::info::CrsSummary;
use serde::Serialize;
use std::fmt::Write as _;
#[derive(Debug, Clone, Serialize)]
pub struct PointCloudInfo {
pub driver: String,
pub version: String,
pub point_format: u8,
pub point_record_length: u16,
pub point_count: u64,
pub points_by_return: Vec<u64>,
pub bounds: [f64; 6],
pub wgs84_bounds: Option<[f64; 4]>,
pub scale: [f64; 3],
pub offset: [f64; 3],
pub crs: CrsSummary,
pub vertical_epsg: Option<u32>,
pub vertical_crs: Option<String>,
pub system_identifier: String,
pub generating_software: String,
pub creation: [u16; 2],
pub file_source_id: u16,
pub global_encoding: u16,
pub guid: String,
pub dimensions: Vec<DimensionInfo>,
pub vlrs: Vec<VlrInfo>,
pub evlrs: Vec<VlrInfo>,
#[serde(skip_serializing_if = "Option::is_none")]
pub copc: Option<CopcSummary>,
#[serde(skip_serializing_if = "Option::is_none")]
pub statistics: Option<PointStatistics>,
}
#[derive(Debug, Clone, Serialize)]
pub struct DimensionInfo {
pub name: String,
#[serde(rename = "type")]
pub value_type: String,
pub extra: bool,
}
#[derive(Debug, Clone, Serialize)]
pub struct VlrInfo {
pub user_id: String,
pub record_id: u16,
pub description: String,
pub length: usize,
}
#[derive(Debug, Clone, Serialize)]
pub struct CopcSummary {
pub center: [f64; 3],
pub halfsize: f64,
pub spacing: f64,
pub gpstime: [f64; 2],
pub node_count: usize,
pub depth: usize,
pub points_per_level: Vec<u64>,
}
#[derive(Debug, Clone, Serialize)]
pub struct PointStatistics {
pub classification_counts: Vec<(u8, u64)>,
pub return_counts: Vec<(u8, u64)>,
pub dimensions: Vec<DimensionStatistics>,
}
#[derive(Debug, Clone, Serialize)]
pub struct DimensionStatistics {
pub name: String,
pub min: f64,
pub max: f64,
pub mean: f64,
}
pub fn point_cloud_info(r: &mut PointCloudReader, statistics: bool) -> Result<PointCloudInfo> {
let h = r.header().clone();
let crs = r.crs().clone();
let (vertical_epsg, vertical_crs) = r.vertical_crs();
let bounds = [h.min[0], h.min[1], h.min[2], h.max[0], h.max[1], h.max[2]];
let copc = match r.copc_info().copied() {
Some(info) => {
let nodes = r.copc_nodes()?;
let depth = nodes.iter().map(|n| n.key.level as usize + 1).max().unwrap_or(0);
let mut per_level = vec![0u64; depth];
for n in &nodes {
per_level[n.key.level as usize] += n.point_count.max(0) as u64;
}
Some(CopcSummary {
center: info.center,
halfsize: info.halfsize,
spacing: info.spacing,
gpstime: [info.gpstime_minimum, info.gpstime_maximum],
node_count: nodes.len(),
depth,
points_per_level: per_level,
})
}
None => None,
};
let statistics = if statistics {
let cloud = r.read(&PointReadOptions::default())?;
let dimensions = cloud
.dimensions
.iter()
.filter_map(|d| {
let v = d.to_f64();
let valid: Vec<f64> = v.into_iter().filter(|x| !x.is_nan()).collect();
if valid.is_empty() {
return None;
}
let min = valid.iter().copied().fold(f64::INFINITY, f64::min);
let max = valid.iter().copied().fold(f64::NEG_INFINITY, f64::max);
let mean = valid.iter().sum::<f64>() / valid.len() as f64;
Some(DimensionStatistics { name: d.name.clone(), min, max, mean })
})
.collect();
Some(PointStatistics { classification_counts: cloud.classification_counts(), return_counts: cloud.return_counts(), dimensions })
} else {
None
};
let vlr_info = |v: &super::Vlr| VlrInfo {
user_id: v.user_id.clone(),
record_id: v.record_id,
description: v.description.clone(),
length: v.data.len(),
};
let standard = super::las::STANDARD_DIMENSIONS;
Ok(PointCloudInfo {
driver: r.format_name().to_string(),
version: h.version_string(),
point_format: h.point_format,
point_record_length: h.point_record_length,
point_count: h.point_count,
points_by_return: h.points_by_return.clone(),
bounds,
wgs84_bounds: wgs84_bounds(&crs, bounds),
scale: h.scale,
offset: h.offset,
crs: CrsSummary::new(&crs),
vertical_epsg,
vertical_crs,
system_identifier: h.system_identifier.clone(),
generating_software: h.generating_software.clone(),
creation: [h.creation_day, h.creation_year],
file_source_id: h.file_source_id,
global_encoding: h.global_encoding,
guid: h.guid.clone(),
dimensions: r
.dimensions()
.into_iter()
.map(|(name, t)| DimensionInfo { extra: !standard.contains(&name.as_str()), name, value_type: t.name().to_string() })
.collect(),
vlrs: h.vlrs.iter().map(vlr_info).collect(),
evlrs: h.evlrs.iter().map(vlr_info).collect(),
copc,
statistics,
})
}
fn wgs84_bounds(crs: &SpatialRef, b: [f64; 6]) -> Option<[f64; 4]> {
if crs.is_unknown() || !(b[0] <= b[3] && b[1] <= b[4]) {
return None;
}
let wgs84 = SpatialRef::from_epsg(4326).ok()?;
let t = Transformer::new(crs, &wgs84).ok()?;
let r = t.transform_bounds((b[0], b[1], b[3], b[4])).ok()?;
Some([r.0, r.1, r.2, r.3])
}
impl PointCloudInfo {
pub fn to_text(&self) -> String {
let mut s = String::new();
let _ = writeln!(
s,
"Driver: {} (LAS {}, point format {}, {} bytes per point)",
self.driver, self.version, self.point_format, self.point_record_length
);
let _ = writeln!(s, "Points: {}", self.point_count);
let returns: Vec<String> =
self.points_by_return.iter().enumerate().filter(|(_, n)| **n > 0).map(|(i, n)| format!("{}: {n}", i + 1)).collect();
if !returns.is_empty() {
let _ = writeln!(s, "Points by return: {}", returns.join(", "));
}
let crs = match (self.crs.epsg, &self.crs.name) {
(Some(c), Some(n)) => format!("EPSG:{c} ({n})"),
(Some(c), None) => format!("EPSG:{c}"),
(None, Some(n)) => n.clone(),
(None, None) => "unknown".into(),
};
let _ = writeln!(s, "CRS: {crs}");
if let Some(v) = &self.vertical_crs {
let _ = writeln!(s, "Vertical CRS: {v}{}", self.vertical_epsg.map(|c| format!(" (EPSG:{c})")).unwrap_or_default());
}
let b = self.bounds;
let _ = writeln!(s, "Bounds: ({}, {}, {}) - ({}, {}, {})", b[0], b[1], b[2], b[3], b[4], b[5]);
if let Some(w) = self.wgs84_bounds {
let _ = writeln!(s, "WGS 84 bounds: ({}, {}) - ({}, {})", w[0], w[1], w[2], w[3]);
}
let _ = writeln!(s, "Scale: ({}, {}, {})", self.scale[0], self.scale[1], self.scale[2]);
let _ = writeln!(s, "Offset: ({}, {}, {})", self.offset[0], self.offset[1], self.offset[2]);
let _ = writeln!(s, "System identifier: {}", self.system_identifier);
let _ = writeln!(s, "Generating software: {}", self.generating_software);
let _ = writeln!(s, "Created: day {} of {}", self.creation[0], self.creation[1]);
let dims: Vec<String> = self.dimensions.iter().map(|d| format!("{} ({})", d.name, d.value_type)).collect();
let _ = writeln!(s, "Dimensions: {}", dims.join(", "));
let extra: Vec<&str> = self.dimensions.iter().filter(|d| d.extra).map(|d| d.name.as_str()).collect();
if !extra.is_empty() {
let _ = writeln!(s, "Extra dimensions: {}", extra.join(", "));
}
for (label, list) in [("VLR", &self.vlrs), ("EVLR", &self.evlrs)] {
for v in list {
let _ = writeln!(s, "{label}: {} {} \"{}\" ({} bytes)", v.user_id, v.record_id, v.description, v.length);
}
}
if let Some(c) = &self.copc {
let _ = writeln!(
s,
"COPC: {} nodes, depth {}, spacing {}, center ({}, {}, {}), halfsize {}",
c.node_count, c.depth, c.spacing, c.center[0], c.center[1], c.center[2], c.halfsize
);
let levels: Vec<String> = c.points_per_level.iter().enumerate().map(|(l, n)| format!("{l}: {n}")).collect();
let _ = writeln!(s, "COPC points per level: {}", levels.join(", "));
}
if let Some(st) = &self.statistics {
let classes: Vec<String> = st.classification_counts.iter().map(|(c, n)| format!("{c} ({}): {n}", class_name(*c))).collect();
let _ = writeln!(s, "Classifications: {}", classes.join(", "));
let returns: Vec<String> = st.return_counts.iter().map(|(r, n)| format!("{r}: {n}")).collect();
let _ = writeln!(s, "Return numbers: {}", returns.join(", "));
for d in &st.dimensions {
let _ = writeln!(s, " {}: min={} max={} mean={:.6}", d.name, d.min, d.max, d.mean);
}
}
s
}
}
pub fn class_name(c: u8) -> &'static str {
match c {
0 => "never classified",
1 => "unassigned",
2 => "ground",
3 => "low vegetation",
4 => "medium vegetation",
5 => "high vegetation",
6 => "building",
7 => "low point",
8 => "model key point",
9 => "water",
10 => "rail",
11 => "road surface",
12 => "overlap",
13 => "wire guard",
14 => "wire conductor",
15 => "transmission tower",
16 => "wire connector",
17 => "bridge deck",
18 => "high noise",
19 => "overhead structure",
20 => "ignored ground",
21 => "snow",
22 => "temporal exclusion",
_ => "user defined",
}
}