use las::{Header, crs::GeoTiffCrs};
use log::{Level, log};
use thiserror::Error;
type Result<T> = std::result::Result<T, Error>;
pub const EPSG_RANGE: std::ops::Range<u16> = 1024..(i16::MAX as u16);
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct EpsgCRS {
horizontal: u16,
vertical: Option<u16>,
}
impl EpsgCRS {
pub fn new(horizontal_code: u16, vertical_code: Option<u16>) -> Result<Self> {
let code = EpsgCRS {
horizontal: horizontal_code,
vertical: vertical_code,
};
if code.in_epsg_range() {
Ok(code)
} else {
Err(Error::BadEPSGCrs)
}
}
pub fn new_unchecked(horizontal_code: u16, vertical_code: Option<u16>) -> Self {
EpsgCRS {
horizontal: horizontal_code,
vertical: vertical_code,
}
}
pub fn in_epsg_range(&self) -> bool {
if let Some(vc) = &self.vertical
&& !EPSG_RANGE.contains(vc)
{
return false;
}
EPSG_RANGE.contains(&self.horizontal)
}
pub fn get_horizontal(&self) -> u16 {
self.horizontal
}
pub fn get_vertical(&self) -> Option<u16> {
self.vertical
}
pub fn set_horizontal(&mut self, horizontal_code: u16) -> Result<()> {
if EPSG_RANGE.contains(&horizontal_code) {
self.horizontal = horizontal_code;
Ok(())
} else {
Err(Error::SetBadCode(horizontal_code))
}
}
pub fn set_vertical(&mut self, vertical_code: u16) -> Result<()> {
if EPSG_RANGE.contains(&vertical_code) {
self.vertical = Some(vertical_code);
Ok(())
} else {
Err(Error::SetBadCode(vertical_code))
}
}
pub fn set_horizontal_unchecked(&mut self, horizontal_code: u16) {
self.horizontal = horizontal_code;
}
pub fn set_vertical_unchecked(&mut self, vertical_code: u16) {
self.vertical = Some(vertical_code)
}
}
#[derive(Error, Debug)]
pub enum Error {
#[error(transparent)]
LasError(#[from] las::Error),
#[error("Parsing of User Defined CRS not implemented")]
UserDefinedCrs,
#[error("Unable to parse the found WKT-CRS (E)VLR")]
UnreadableWktCrs,
#[error("Unknown GeoTiff model type in GeoTiff EVLR")]
UnreadableGeoTiffCrs,
#[error("The parsed code for the horizontal component is outside of the EPSG-range")]
BadHorizontalCodeParsed(EpsgCRS),
#[error("The provided code for setting is outside of EPSG_RANGE")]
SetBadCode(u16),
#[error("A component of the EPSG code is outside of EPSG_RANGE")]
BadEPSGCrs,
}
pub trait ParseEpsgCRS {
fn get_epsg_crs(&self) -> Result<Option<EpsgCRS>>;
}
impl ParseEpsgCRS for Header {
fn get_epsg_crs(&self) -> Result<Option<EpsgCRS>> {
if let Some(wkt) = self.get_wkt_crs_bytes() {
if !self.has_wkt_crs() {
log!(
Level::Warn,
"WKT CRS (E)VLR found, but header says it does not exist"
);
}
Ok(Some(get_epsg_from_wkt_crs_bytes(wkt)?))
} else if let Some(geotiff) = self.get_geotiff_crs()? {
if self.has_wkt_crs() {
log!(
Level::Warn,
"Only Geotiff CRS (E)VLR(s) found, but header says WKT exists"
);
}
Ok(Some(get_epsg_from_geotiff_crs(&geotiff)?))
} else {
if self.has_wkt_crs() {
log!(
Level::Warn,
"No CRS (E)VLR(s) found, but header says WKT exists"
);
}
Ok(None)
}
}
}
pub fn get_epsg_from_wkt_crs_bytes(bytes: &[u8]) -> Result<EpsgCRS> {
let wkt = String::from_utf8_lossy(bytes);
enum WktPieces<'a> {
One(&'a [u8]),
Two(&'a [u8], &'a [u8]),
}
impl WktPieces<'_> {
fn parse_codes(&self) -> (u16, u16) {
match self {
WktPieces::One(hor) => (Self::get_code(hor), 0),
WktPieces::Two(hor, ver) => (Self::get_code(hor), Self::get_code(ver)),
}
}
fn get_code(bytes: &[u8]) -> u16 {
let mut epsg_code = 0;
let mut code_has_started = false;
let mut power = 1;
for byte in bytes.trim_ascii_end().iter().rev().take(10) {
if byte.is_ascii_digit() {
code_has_started = true;
epsg_code += power * (byte - 48) as u16;
power *= 10;
} else if code_has_started {
break;
}
}
epsg_code
}
}
let pieces = if let Some((horizontal, vertical)) = wkt.split_once("VERTCRS") {
WktPieces::Two(horizontal.as_bytes(), vertical.as_bytes())
} else if let Some((horizontal, vertical)) = wkt.split_once("VERTICALCRS") {
WktPieces::Two(horizontal.as_bytes(), vertical.as_bytes())
} else if let Some((horizontal, vertical)) = wkt.split_once("VERT_CS") {
WktPieces::Two(horizontal.as_bytes(), vertical.as_bytes())
} else {
WktPieces::One(wkt.as_bytes())
};
let codes = pieces.parse_codes();
let mut code = EpsgCRS {
horizontal: codes.0,
vertical: Some(codes.1),
};
if !EPSG_RANGE.contains(&code.horizontal) {
return Err(Error::BadHorizontalCodeParsed(code));
}
if let Some(v_code) = code.vertical
&& !EPSG_RANGE.contains(&v_code)
{
code.vertical = None;
}
Ok(code)
}
pub fn get_epsg_from_geotiff_crs(geotiff_crs_data: &GeoTiffCrs) -> Result<EpsgCRS> {
let horizontal = match geotiff_crs_data.get_gt_model_type_geo_key_value() {
Some(1) => geotiff_crs_data.get_projected_crs_geo_key_value(),
Some(2) | Some(3) => geotiff_crs_data.get_geodetic_crs_geo_key_value(),
Some(32767) => return Err(Error::UserDefinedCrs),
_ => return Err(Error::UnreadableGeoTiffCrs),
};
let vertical = geotiff_crs_data.get_vertical_crs_geo_key_value();
if horizontal.is_none() {
return Err(Error::UnreadableGeoTiffCrs);
}
let mut code = EpsgCRS {
horizontal: horizontal.unwrap(),
vertical,
};
if !EPSG_RANGE.contains(&code.horizontal) {
return Err(Error::BadHorizontalCodeParsed(code));
}
if let Some(v_code) = code.vertical
&& !EPSG_RANGE.contains(&v_code)
{
code.vertical = None;
}
Ok(code)
}
#[cfg(test)]
mod tests {
use crate::ParseEpsgCRS;
use las::Reader;
#[test]
fn test_get_epsg_crs_wkt_vlr_autzen() {
let reader = Reader::from_path("testdata/autzen.copc.laz").expect("Cannot open reader");
let crs = reader
.header()
.get_epsg_crs()
.expect("Could not get epsg code")
.expect("The found EPSG was None");
assert!(crs.horizontal == 2992);
assert!(crs.vertical == Some(6360))
}
#[test]
fn test_get_epsg_crs_geotiff_vlr_norway() {
let reader = Reader::from_path("testdata/32-1-472-150-76.laz").expect("Cannot open reader");
let crs = reader.header().get_epsg_crs().unwrap().unwrap();
assert!(crs.horizontal == 25832);
assert!(crs.vertical == Some(5941));
}
#[test]
fn test_get_epsg_crs_wkt_vlr_autzen_las() {
let reader = Reader::from_path("testdata/autzen.las").expect("Cannot open reader");
let crs = reader
.header()
.get_epsg_crs()
.expect("Could not get epsg code")
.expect("The found EPSG was None");
assert!(crs.horizontal == 2994);
assert!(crs.vertical.is_none())
}
}