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}