Skip to main content

spatialrust_io/las/
schema.rs

1use las::{point::Format, Header};
2use spatialrust_core::{DType, FieldSemantic, PointField, PointSchema, StandardSchemas};
3
4use crate::error::{las_format, IoError};
5
6/// Maps a LAS-derived field name to a SpatialRust semantic.
7#[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
27/// Builds a SpatialRust schema for the fields exported from a LAS header.
28pub 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
54/// Selects a COPC-compatible LAS point format for writing the given cloud schema.
55///
56/// COPC accepts PDRF 1/3 (upgraded to 6/7) or native 6–8. SpatialRust maps clouds to PDRF 1 or 3.
57pub 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
77/// Selects a LAS point format for writing the given cloud schema.
78pub 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}