las 0.8.0

Read and write point clouds stored in the ASPRS las file format.
Documentation
use std::fmt;

use crate::point::Error;
use crate::Result;

const TIME_FORMATS: &[u8] = &[1, 3, 4, 5, 6, 7, 8, 9, 10];
const COLOR_FORMATS: &[u8] = &[2, 3, 5, 7, 8, 10];
const WAVEFORM_FORMATS: &[u8] = &[4, 5, 9, 10];
const NIR_FORMATS: &[u8] = &[8, 10];
const IS_COMPRESSED_MASK: u8 = 0x80;

fn is_point_format_compressed(point_format_id: u8) -> bool {
    point_format_id & IS_COMPRESSED_MASK == IS_COMPRESSED_MASK
}

fn point_format_id_compressed_to_uncompressd(point_format_id: u8) -> u8 {
    point_format_id & 0x3f
}

fn point_format_id_uncompressed_to_compressed(point_format_id: u8) -> u8 {
    point_format_id | 0x80
}

/// Point formats are defined by the las spec.
///
/// As of las 1.4, there are eleven point formats (0-10). A new `Format` can be created from its
/// code and converted back into it:
///
/// ```
/// use las::point::Format;
///
/// let format_1 = Format::new(1).unwrap();
/// assert!(format_1.has_gps_time);
/// assert_eq!(1, format_1.to_u8().unwrap());
///
/// assert!(Format::new(11).is_err());
/// ```
///
/// Point formats can have extra bytes, which are user-defined attributes. Extra bytes were
/// introduced in las 1.4.
///
/// ```
/// use las::point::Format;
/// let mut format = Format::new(0).unwrap();
/// format.extra_bytes = 1;
/// assert_eq!(21, format.len());
/// ```
///
/// Certain combinations of attributes in a point format are illegal, e.g. gps time is required for
/// all formats >= 6:
///
/// ```
/// use las::point::Format;
/// let mut format = Format::new(6).unwrap();
/// format.has_gps_time = false;
/// assert!(format.to_u8().is_err());
/// ```
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct Format {
    /// Does this point format include gps time?
    pub has_gps_time: bool,
    /// Does this point format include red, green, and blue colors?
    pub has_color: bool,
    /// Does this point format use two bytes for its flags and scaled scan angles?
    pub is_extended: bool,
    /// Does this point format have waveforms?
    pub has_waveform: bool,
    /// Does this point format have near infrared data?
    pub has_nir: bool,
    /// The number of extra bytes on each point.
    pub extra_bytes: u16,
    /// Is this point format compressed?
    pub is_compressed: bool,
}

#[allow(clippy::len_without_is_empty)]
impl Format {
    /// Creates a new point format from a u8.
    ///
    /// # Examples
    ///
    /// ```
    /// use las::point::Format;
    /// let format = Format::new(0).unwrap();
    /// assert!(!format.has_gps_time);
    /// assert!(!format.has_color);
    ///
    /// let format = Format::new(3).unwrap();
    /// assert!(format.has_gps_time);
    /// assert!(format.has_color);
    ///
    /// assert!(Format::new(11).is_err());
    /// ```
    pub fn new(n: u8) -> Result<Format> {
        let is_compressed = is_point_format_compressed(n);
        if n > 10 && !is_compressed {
            Err(Error::FormatNumber(n).into())
        } else {
            let n = point_format_id_compressed_to_uncompressd(n);
            Ok(Format {
                has_gps_time: TIME_FORMATS.contains(&n),
                has_color: COLOR_FORMATS.contains(&n),
                has_waveform: WAVEFORM_FORMATS.contains(&n),
                has_nir: NIR_FORMATS.contains(&n),
                is_extended: n >= 6,
                extra_bytes: 0,
                is_compressed,
            })
        }
    }

    /// Converts this point format into an extended format.
    ///
    /// "Extended" formats can contain more information per point, and must have gps time.
    ///
    /// # Examples
    ///
    /// ```
    /// use las::point::Format;
    /// let mut format = Format::default();
    /// assert!(!format.has_gps_time);
    /// assert!(!format.is_extended);
    /// format.extend();
    /// assert!(format.has_gps_time);
    /// assert!(format.is_extended);
    pub fn extend(&mut self) {
        self.has_gps_time = true;
        self.is_extended = true;
    }

    /// Returns this point format's length.
    ///
    /// # Examples
    ///
    /// ```
    /// use las::point::Format;
    /// let mut format = Format::new(0).unwrap();
    /// assert_eq!(20, format.len());
    /// format.has_gps_time = true;
    /// assert_eq!(28, format.len());
    /// ```
    pub fn len(&self) -> u16 {
        let mut len = if self.is_extended { 22 } else { 20 } + self.extra_bytes;
        if self.has_gps_time {
            len += 8;
        }
        if self.has_color {
            len += 6;
        }
        if self.has_nir {
            len += 2;
        }
        if self.has_waveform {
            len += 29;
        }
        len
    }

    /// Converts this point format to a u8.
    ///
    /// Can return an error if there is an invalid combination of attributes.
    ///
    /// # Examples
    ///
    /// ```
    /// use las::point::Format;
    /// let mut format = Format::default();
    /// assert_eq!(0, format.to_u8().unwrap());
    /// format.is_extended = true;
    /// assert!(format.to_u8().is_err());
    /// format.has_gps_time = true;
    /// assert_eq!(6, format.to_u8().unwrap());
    /// ```
    pub fn to_u8(&self) -> Result<u8> {
        if !cfg!(feature = "laz") && self.is_compressed {
            Err(Error::Format(*self).into())
        } else if self.is_extended {
            if self.has_gps_time {
                if self.has_color {
                    if self.has_nir {
                        if self.has_waveform {
                            Ok(10)
                        } else {
                            Ok(8)
                        }
                    } else if self.has_waveform {
                        Err(Error::Format(*self).into())
                    } else {
                        Ok(7)
                    }
                } else if self.has_nir {
                    Err(Error::Format(*self).into())
                } else if self.has_waveform {
                    Ok(9)
                } else {
                    Ok(6)
                }
            } else {
                Err(Error::Format(*self).into())
            }
        } else if self.has_nir {
            Err(Error::Format(*self).into())
        } else if self.has_waveform {
            if self.has_gps_time {
                if self.has_color {
                    Ok(5)
                } else {
                    Ok(4)
                }
            } else {
                Err(Error::Format(*self).into())
            }
        } else {
            let mut n = if self.has_gps_time { 1 } else { 0 };
            if self.has_color {
                n += 2;
            }
            Ok(n)
        }
    }

    /// When the data is compressed (LAZ) the point format id written in the
    /// header is slightly different to let readers know the data is compressed
    pub(crate) fn to_writable_u8(self) -> Result<u8> {
        self.to_u8().map(|id| {
            if self.is_compressed {
                point_format_id_uncompressed_to_compressed(id)
            } else {
                id
            }
        })
    }
}

impl fmt::Display for Format {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        if let Ok(n) = self.to_u8() {
            write!(f, "point format {}", n)
        } else {
            write!(f, "point format that does not map onto a code: {:?}", self)
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    macro_rules! format {
        ($name:ident, $n:expr, $expected:expr, $len:expr) => {
            mod $name {
                use crate::point::Format;

                #[test]
                fn new() {
                    assert_eq!($expected, Format::new($n).unwrap());
                }

                #[test]
                fn len() {
                    assert_eq!($len, Format::new($n).unwrap().len());
                }

                #[test]
                fn to_u8() {
                    assert_eq!($n, Format::new($n).unwrap().to_u8().unwrap());
                }
            }
        };
    }

    format!(format_0, 0, Format::default(), 20);
    format!(
        format_1,
        1,
        Format {
            has_gps_time: true,
            ..Default::default()
        },
        28
    );
    format!(
        format_2,
        2,
        Format {
            has_color: true,
            ..Default::default()
        },
        26
    );
    format!(
        format_3,
        3,
        Format {
            has_gps_time: true,
            has_color: true,
            ..Default::default()
        },
        34
    );
    format!(
        format_4,
        4,
        Format {
            has_gps_time: true,
            has_waveform: true,
            ..Default::default()
        },
        57
    );
    format!(
        format_5,
        5,
        Format {
            has_gps_time: true,
            has_color: true,
            has_waveform: true,
            ..Default::default()
        },
        63
    );
    format!(
        format_6,
        6,
        Format {
            has_gps_time: true,
            is_extended: true,
            ..Default::default()
        },
        30
    );
    format!(
        format_7,
        7,
        Format {
            has_gps_time: true,
            has_color: true,
            is_extended: true,
            ..Default::default()
        },
        36
    );
    format!(
        format_8,
        8,
        Format {
            has_gps_time: true,
            has_color: true,
            has_nir: true,
            is_extended: true,
            ..Default::default()
        },
        38
    );
    format!(
        format_9,
        9,
        Format {
            has_gps_time: true,
            has_waveform: true,
            is_extended: true,
            ..Default::default()
        },
        59
    );
    format!(
        format_10,
        10,
        Format {
            has_gps_time: true,
            has_color: true,
            has_nir: true,
            has_waveform: true,
            is_extended: true,
            ..Default::default()
        },
        67
    );

    #[test]
    fn waveform_without_gps_time() {
        let format = Format {
            has_waveform: true,
            ..Default::default()
        };
        assert!(format.to_u8().is_err());
    }

    #[test]
    fn extended_without_gps_time() {
        let format = Format {
            is_extended: true,
            ..Default::default()
        };
        assert!(format.to_u8().is_err());
    }

    #[test]
    fn nir_without_extended() {
        let format = Format {
            has_nir: true,
            ..Default::default()
        };
        assert!(format.to_u8().is_err());
    }

    #[test]
    fn nir_without_color() {
        let format = Format {
            is_extended: true,
            has_nir: true,
            ..Default::default()
        };
        assert!(format.to_u8().is_err());
    }

    #[test]
    fn extra_bytes() {
        let format = Format {
            extra_bytes: 1,
            ..Default::default()
        };
        assert_eq!(21, format.len());
    }

    #[test]
    fn is_compressed() {
        let format = Format {
            is_compressed: true,
            ..Default::default()
        };
        if cfg!(feature = "laz") {
            assert!(format.to_u8().is_ok());
        } else {
            assert!(format.to_u8().is_err());
        }
    }
}