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