spatialrust_io/las/
schema.rs1use las::{point::Format, Header};
2use spatialrust_core::{DType, FieldSemantic, PointField, PointSchema, StandardSchemas};
3
4use crate::error::{las_format, IoError};
5
6#[must_use]
8pub fn infer_las_field_semantic(name: &str) -> FieldSemantic {
9 match name {
10 "x" => FieldSemantic::PositionX,
11 "y" => FieldSemantic::PositionY,
12 "z" => FieldSemantic::PositionZ,
13 "intensity" => FieldSemantic::Intensity,
14 "classification" => FieldSemantic::Label,
15 "return_number" => FieldSemantic::Unknown,
16 "number_of_returns" => FieldSemantic::Unknown,
17 "scan_angle" => FieldSemantic::Unknown,
18 "point_source_id" => FieldSemantic::Unknown,
19 "gps_time" => FieldSemantic::TimeOffset,
20 "red" => FieldSemantic::ColorR,
21 "green" => FieldSemantic::ColorG,
22 "blue" => FieldSemantic::ColorB,
23 _ => FieldSemantic::Unknown,
24 }
25}
26
27pub fn schema_for_las_header(header: &Header) -> PointSchema {
29 let format = header.point_format();
30 let mut schema = StandardSchemas::point_xyzi().with_field(PointField::scalar(
31 "classification",
32 FieldSemantic::Label,
33 DType::U8,
34 ));
35
36 if format.has_gps_time {
37 schema = schema.with_field(PointField::scalar(
38 "gps_time",
39 FieldSemantic::TimeOffset,
40 DType::F64,
41 ));
42 }
43
44 if format.has_color {
45 schema = schema
46 .with_field(PointField::scalar("red", FieldSemantic::ColorR, DType::U16))
47 .with_field(PointField::scalar("green", FieldSemantic::ColorG, DType::U16))
48 .with_field(PointField::scalar("blue", FieldSemantic::ColorB, DType::U16));
49 }
50
51 schema
52}
53
54pub fn schema_from_point_cloud_for_copc(
58 schema: &PointSchema,
59) -> Result<(Format, PointSchema), IoError> {
60 let has_color = schema.fields().iter().any(|field| {
61 matches!(
62 field.semantic,
63 FieldSemantic::ColorR | FieldSemantic::ColorG | FieldSemantic::ColorB
64 )
65 });
66
67 let format = if has_color {
68 Format::new(3).map_err(|error| las_format(error.to_string()))?
69 } else {
70 Format::new(1).map_err(|error| las_format(error.to_string()))?
71 };
72
73 let export_schema = export_schema_for_format(schema, &format);
74 Ok((format, export_schema))
75}
76
77pub fn schema_from_point_cloud(schema: &PointSchema) -> Result<(Format, PointSchema), IoError> {
79 let has_color = schema.fields().iter().any(|field| {
80 matches!(
81 field.semantic,
82 FieldSemantic::ColorR | FieldSemantic::ColorG | FieldSemantic::ColorB
83 )
84 });
85 let has_gps_time =
86 schema.fields().iter().any(|field| field.semantic == FieldSemantic::TimeOffset);
87
88 let format = if has_color {
89 Format::new(2).map_err(|error| las_format(error.to_string()))?
90 } else if has_gps_time {
91 Format::new(1).map_err(|error| las_format(error.to_string()))?
92 } else {
93 Format::new(0).map_err(|error| las_format(error.to_string()))?
94 };
95
96 let export_schema = export_schema_for_format(schema, &format);
97 Ok((format, export_schema))
98}
99
100fn export_schema_for_format(source: &PointSchema, format: &Format) -> PointSchema {
101 let mut schema = StandardSchemas::point_xyz().with_field(PointField::scalar(
102 "classification",
103 FieldSemantic::Label,
104 DType::U8,
105 ));
106
107 if source.find_semantic(FieldSemantic::Intensity).is_some() {
108 schema = schema.with_field(PointField::scalar(
109 "intensity",
110 FieldSemantic::Intensity,
111 DType::F32,
112 ));
113 }
114
115 if format.has_gps_time && source.find_semantic(FieldSemantic::TimeOffset).is_some() {
116 schema = schema.with_field(PointField::scalar(
117 "gps_time",
118 FieldSemantic::TimeOffset,
119 DType::F64,
120 ));
121 }
122
123 if format.has_color {
124 for (name, semantic) in [
125 ("red", FieldSemantic::ColorR),
126 ("green", FieldSemantic::ColorG),
127 ("blue", FieldSemantic::ColorB),
128 ] {
129 if source.find_semantic(semantic).is_some() {
130 schema = schema.with_field(PointField::scalar(name, semantic, DType::U16));
131 }
132 }
133 }
134
135 schema
136}
137
138#[cfg(test)]
139mod tests {
140 use super::{schema_for_las_header, schema_from_point_cloud};
141 use las::{Builder, Header};
142 use spatialrust_core::{FieldSemantic, StandardSchemas};
143
144 #[test]
145 fn builds_default_las_schema() {
146 let header: Header = Builder::default().into_header().unwrap();
147 let schema = schema_for_las_header(&header);
148 assert!(schema.find_semantic(FieldSemantic::PositionX).is_some());
149 assert!(schema.find_semantic(FieldSemantic::Label).is_some());
150 }
151
152 #[test]
153 fn selects_format_zero_for_xyz_cloud() {
154 let (format, _) =
155 schema_from_point_cloud(&StandardSchemas::point_xyz()).expect("format selection");
156 assert_eq!(format.to_u8().unwrap(), 0);
157 }
158
159 #[test]
160 fn selects_format_two_for_rgb_cloud() {
161 let (format, export_schema) =
162 schema_from_point_cloud(&StandardSchemas::point_xyzrgb()).expect("format selection");
163 assert_eq!(format.to_u8().unwrap(), 2);
164 assert_eq!(export_schema.find_semantic(FieldSemantic::ColorR).unwrap().name, "red");
165 }
166}