Skip to main content

las_crs/
lib.rs

1//! Small library for getting the CRS in the form of a EPSG code from lidar files.
2//! Either call [ParseEpsgCRS::get_epsg_crs] on a [las::Header] or use one of
3//! [get_epsg_from_geotiff_crs] or [get_epsg_from_wkt_crs_bytes] on the output
4//! of [las::Header::get_geotiff_crs] or [las::Header::get_wkt_crs_bytes] respectively
5//!
6//! The library should be able to parse CRS's stored in WKT-CRS v1 and v2 and GeoTiff U16 (E)VLR(s) stored in both las and laz files (with the laz feature flag activated).
7//!
8//! The CRS is returend in a `Result<EpsgCRS, crate::Error>`
9//! CRS has the fields horizontal, which is a u16 EPSG code, and vertical, which is an optional u16 EPSG code.
10//! If a parsed vertical code is outside [EPSG_RANGE] it is ignored and set to `None`.
11//! If a parsed horizontal code is outside [EPSG_RANGE] an `Err(Error::BadHorizontalCodeParsed(EpsgCRS))` is returned
12//!
13//! The validity of the extracted code is only checked against being in [EPSG_RANGE].
14//! Use the [crs-definitions](https://docs.rs/crs-definitions/latest/crs_definitions/) crate for checking validity of EPSG codes.
15//!
16//! Be aware that certain software adds invalid CRS VLRs when writing CRS-less lidar files (f.ex when QGIS convert .la[s,z] files without a CRS-VLR to .copc.laz files).
17//! This is because the las 1.4 spec (which .copc.laz demands), requires a WKT-CRS (E)VLR to be present.
18//! These VLRs often contain the invalid EPSG code 0 and trying to extract that code will return a BadHorizontalCodeParsed Error.
19//!
20//! Parsing EPSG codes from user-defined CRS's and CRS's stored in GeoTiff String or Double data is not supported.
21//! But the relevant [las::crs::GeoTiffData] is returned with the `Error::UnimplementedForGeoTiffStringAndDoubleData(las::crs::GeoTiffData)`
22//! If you have a Lidar file with CRS defined in this way please make an issue on Github so I can create tests for it
23//! I have yet to see a Lidar file with CRS defined in that way
24
25use las::{Header, crs::GeoTiffCrs};
26use log::{Level, log};
27use thiserror::Error;
28
29type Result<T> = std::result::Result<T, Error>;
30
31pub const EPSG_RANGE: std::ops::Range<u16> = 1024..(i16::MAX as u16);
32
33/// Horizontal and optional vertical CRS given by EPSG code(s)
34#[derive(Debug, Clone, Copy, PartialEq)]
35pub struct EpsgCRS {
36    /// EPSG code for the horizontal CRS
37    horizontal: u16,
38
39    /// Optional EPSG code for the vertical CRS
40    vertical: Option<u16>,
41}
42
43impl EpsgCRS {
44    /// Construct a new EpsgCrs both components are checked against EPSG_RANGE
45    pub fn new(horizontal_code: u16, vertical_code: Option<u16>) -> Result<Self> {
46        let code = EpsgCRS {
47            horizontal: horizontal_code,
48            vertical: vertical_code,
49        };
50        if code.in_epsg_range() {
51            Ok(code)
52        } else {
53            Err(Error::BadEPSGCrs)
54        }
55    }
56
57    /// Construct a new EpsgCrs neither component is checked against EPSG_RANGE
58    pub fn new_unchecked(horizontal_code: u16, vertical_code: Option<u16>) -> Self {
59        EpsgCRS {
60            horizontal: horizontal_code,
61            vertical: vertical_code,
62        }
63    }
64
65    /// Checked both components against EPSG_RANGE
66    pub fn in_epsg_range(&self) -> bool {
67        if let Some(vc) = &self.vertical
68            && !EPSG_RANGE.contains(vc)
69        {
70            return false;
71        }
72        EPSG_RANGE.contains(&self.horizontal)
73    }
74
75    /// get the horizontal code
76    pub fn get_horizontal(&self) -> u16 {
77        self.horizontal
78    }
79
80    /// get the optional vertical code
81    pub fn get_vertical(&self) -> Option<u16> {
82        self.vertical
83    }
84
85    /// set the horizontal code, the new code is checked against EPSG_RANGE before setting
86    pub fn set_horizontal(&mut self, horizontal_code: u16) -> Result<()> {
87        if EPSG_RANGE.contains(&horizontal_code) {
88            self.horizontal = horizontal_code;
89            Ok(())
90        } else {
91            Err(Error::SetBadCode(horizontal_code))
92        }
93    }
94
95    /// set the vertical code, the new code is checked against EPSG_RANGE before setting
96    pub fn set_vertical(&mut self, vertical_code: u16) -> Result<()> {
97        if EPSG_RANGE.contains(&vertical_code) {
98            self.vertical = Some(vertical_code);
99            Ok(())
100        } else {
101            Err(Error::SetBadCode(vertical_code))
102        }
103    }
104
105    /// set the horizontal code without checking against EPSG_RANGE
106    pub fn set_horizontal_unchecked(&mut self, horizontal_code: u16) {
107        self.horizontal = horizontal_code;
108    }
109
110    /// set the vertical code without checking against EPSG_RANGE
111    pub fn set_vertical_unchecked(&mut self, vertical_code: u16) {
112        self.vertical = Some(vertical_code)
113    }
114}
115
116/// Error enum
117#[derive(Error, Debug)]
118pub enum Error {
119    /// Error propagated from the Las lib
120    #[error(transparent)]
121    LasError(#[from] las::Error),
122    /// User defined CRS cannot be parsed
123    #[error("Parsing of User Defined CRS not implemented")]
124    UserDefinedCrs,
125    /// Could not parse the CRS from the WKT-data
126    #[error("Unable to parse the found WKT-CRS (E)VLR")]
127    UnreadableWktCrs,
128    /// Could not parse the CRS from the GeoTiff-data
129    #[error("Unknown GeoTiff model type in GeoTiff EVLR")]
130    UnreadableGeoTiffCrs,
131    /// The parsed horizontal code is outside of [EPSG_RANGE]
132    #[error("The parsed code for the horizontal component is outside of the EPSG-range")]
133    BadHorizontalCodeParsed(EpsgCRS),
134    /// The provided code for setting is outside of EPSG_RANGE
135    #[error("The provided code for setting is outside of EPSG_RANGE")]
136    SetBadCode(u16),
137    /// The EPSG CRS is outside of EPSG_RANGE
138    #[error("A component of the EPSG code is outside of EPSG_RANGE")]
139    BadEPSGCrs,
140}
141
142pub trait ParseEpsgCRS {
143    fn get_epsg_crs(&self) -> Result<Option<EpsgCRS>>;
144}
145
146impl ParseEpsgCRS for Header {
147    /// Parse the EPSG coordinate reference system (CRSes) code(s) from the header.
148    ///
149    /// Las stores CRS-info in (E)VLRs either as Well Known Text (WKT) or in GeoTIff-format
150    /// **Most**, but not all, CRS' used for Aerial Lidar has an associated EPSG code.
151    /// Use this function to try and parse the EPSG code(s) from the header.
152    ///
153    /// WKT takes precedence over GeoTiff in this function, but they should not co-exist.
154    ///
155    /// Just because this function fails does not mean that no CRS-data is available.
156    /// Use functions [Self::get_wkt_crs_bytes] or [Self::get_geotiff_crs] to get all data stored in the CRS-(E)VLRs.
157    ///
158    /// Parsing code(s) from WKT-CRS v1 or v2 and GeoTiff U16-data is supported.
159    ///
160    /// The validity of the extracted code is not checked, beyond checking that it is in [EPSG_RANGE].
161    /// Use the [crs-definitions](https://docs.rs/crs-definitions/latest/crs_definitions/) crate for checking the validity of a horizontal EPSG code.
162    ///
163    /// # Example
164    ///
165    /// ```
166    /// use las::Reader;
167    /// use las_crs::ParseEpsgCRS;
168    ///
169    /// let reader = Reader::from_path("testdata/autzen.las").expect("Cannot open reader");
170    /// let epsg = reader.header().get_epsg_crs().expect("Cannot parse EPSG code(s) from the CRS-(E)VLR(s)").expect("The Lidar file had no CRS");
171    /// ```
172    fn get_epsg_crs(&self) -> Result<Option<EpsgCRS>> {
173        if let Some(wkt) = self.get_wkt_crs_bytes() {
174            if !self.has_wkt_crs() {
175                log!(
176                    Level::Warn,
177                    "WKT CRS (E)VLR found, but header says it does not exist"
178                );
179            }
180            Ok(Some(get_epsg_from_wkt_crs_bytes(wkt)?))
181        } else if let Some(geotiff) = self.get_geotiff_crs()? {
182            if self.has_wkt_crs() {
183                log!(
184                    Level::Warn,
185                    "Only Geotiff CRS (E)VLR(s) found, but header says WKT exists"
186                );
187            }
188            Ok(Some(get_epsg_from_geotiff_crs(&geotiff)?))
189        } else {
190            if self.has_wkt_crs() {
191                log!(
192                    Level::Warn,
193                    "No CRS (E)VLR(s) found, but header says WKT exists"
194                );
195            }
196            Ok(None)
197        }
198    }
199}
200
201/// Tries to parse EPSG code(s) from WKT-CRS bytes.
202///
203/// By parsing the EPSG codes at the end of the vertical and horizontal CRS sub-strings
204/// This is not true WKT parser and might provide a bad code if
205/// the WKT-CRS bytes does not look as expected
206pub fn get_epsg_from_wkt_crs_bytes(bytes: &[u8]) -> Result<EpsgCRS> {
207    let wkt = String::from_utf8_lossy(bytes);
208
209    enum WktPieces<'a> {
210        One(&'a [u8]),
211        Two(&'a [u8], &'a [u8]),
212    }
213
214    impl WktPieces<'_> {
215        fn parse_codes(&self) -> (u16, u16) {
216            match self {
217                WktPieces::One(hor) => (Self::get_code(hor), 0),
218                WktPieces::Two(hor, ver) => (Self::get_code(hor), Self::get_code(ver)),
219            }
220        }
221
222        fn get_code(bytes: &[u8]) -> u16 {
223            // the EPSG code is located at the end of the substrings
224            // and so we iterate through the substrings backwards collecting
225            // digits and adding them to our EPSG code
226            let mut epsg_code = 0;
227            let mut code_has_started = false;
228            let mut power = 1;
229            // the 10 last bytes should be enough (with a small margin)
230            // as the code is 4 or 5 digits starting at the 2nd or 3rd byte from the back
231            for byte in bytes.trim_ascii_end().iter().rev().take(10) {
232                // if the byte is an ASCII encoded digit
233                if byte.is_ascii_digit() {
234                    // mark that the EPSG code has started
235                    // so that we can break when we no
236                    // longer find digits
237                    code_has_started = true;
238
239                    // translate from ASCII to digits
240                    // and multiply by powers of 10
241                    // sum it to build the EPSG
242                    // code digit by digit
243                    epsg_code += power * (byte - 48) as u16;
244                    power *= 10;
245                } else if code_has_started {
246                    // we no longer see digits
247                    // so the code must be over
248                    break;
249                }
250            }
251            epsg_code
252        }
253    }
254
255    // VERT_CS for WKT v1 and VERTCRS or VERTICALCRS for v2
256    let pieces = if let Some((horizontal, vertical)) = wkt.split_once("VERTCRS") {
257        WktPieces::Two(horizontal.as_bytes(), vertical.as_bytes())
258    } else if let Some((horizontal, vertical)) = wkt.split_once("VERTICALCRS") {
259        WktPieces::Two(horizontal.as_bytes(), vertical.as_bytes())
260    } else if let Some((horizontal, vertical)) = wkt.split_once("VERT_CS") {
261        WktPieces::Two(horizontal.as_bytes(), vertical.as_bytes())
262    } else {
263        WktPieces::One(wkt.as_bytes())
264    };
265
266    let codes = pieces.parse_codes();
267    let mut code = EpsgCRS {
268        horizontal: codes.0,
269        vertical: Some(codes.1),
270    };
271
272    if !EPSG_RANGE.contains(&code.horizontal) {
273        return Err(Error::BadHorizontalCodeParsed(code));
274    }
275    if let Some(v_code) = code.vertical
276        && !EPSG_RANGE.contains(&v_code)
277    {
278        code.vertical = None;
279    }
280    Ok(code)
281}
282
283/// Get the EPSG code(s) from GeoTiff-CRS-data
284/// Only handles geotiff u16 data
285pub fn get_epsg_from_geotiff_crs(geotiff_crs_data: &GeoTiffCrs) -> Result<EpsgCRS> {
286    let horizontal = match geotiff_crs_data.get_gt_model_type_geo_key_value() {
287        Some(1) => geotiff_crs_data.get_projected_crs_geo_key_value(),
288        Some(2) | Some(3) => geotiff_crs_data.get_geodetic_crs_geo_key_value(),
289        Some(32767) => return Err(Error::UserDefinedCrs),
290        _ => return Err(Error::UnreadableGeoTiffCrs),
291    };
292    let vertical = geotiff_crs_data.get_vertical_crs_geo_key_value();
293
294    if horizontal.is_none() {
295        return Err(Error::UnreadableGeoTiffCrs);
296    }
297
298    let mut code = EpsgCRS {
299        horizontal: horizontal.unwrap(),
300        vertical,
301    };
302
303    if !EPSG_RANGE.contains(&code.horizontal) {
304        return Err(Error::BadHorizontalCodeParsed(code));
305    }
306    if let Some(v_code) = code.vertical
307        && !EPSG_RANGE.contains(&v_code)
308    {
309        code.vertical = None;
310    }
311    Ok(code)
312}
313
314#[cfg(test)]
315mod tests {
316    use crate::ParseEpsgCRS;
317    use las::Reader;
318
319    #[test]
320    fn test_get_epsg_crs_wkt_vlr_autzen() {
321        let reader = Reader::from_path("testdata/autzen.copc.laz").expect("Cannot open reader");
322        let crs = reader
323            .header()
324            .get_epsg_crs()
325            .expect("Could not get epsg code")
326            .expect("The found EPSG was None");
327
328        assert!(crs.horizontal == 2992);
329        assert!(crs.vertical == Some(6360))
330    }
331
332    #[test]
333    fn test_get_epsg_crs_geotiff_vlr_norway() {
334        let reader = Reader::from_path("testdata/32-1-472-150-76.laz").expect("Cannot open reader");
335        let crs = reader.header().get_epsg_crs().unwrap().unwrap();
336        assert!(crs.horizontal == 25832);
337        assert!(crs.vertical == Some(5941));
338    }
339
340    #[test]
341    fn test_get_epsg_crs_wkt_vlr_autzen_las() {
342        let reader = Reader::from_path("testdata/autzen.las").expect("Cannot open reader");
343        let crs = reader
344            .header()
345            .get_epsg_crs()
346            .expect("Could not get epsg code")
347            .expect("The found EPSG was None");
348
349        assert!(crs.horizontal == 2994);
350        assert!(crs.vertical.is_none())
351    }
352}