h3o 0.3.0

A Rust implementation of the H3 geospatial indexing system.
Documentation
use crate::{
    error::InvalidGeometry, geom::ToCells, CellIndex, LatLng, Resolution,
};
use geo::Coord;
use std::boxed::Box;

/// A line segment made up of exactly two [`geo::Coord`]s.
///
/// Note that the `ToCells` implementation suffers from the same limitation
/// that [`grid_path_cells`](CellIndex::grid_path_cells), which means that on
/// error `max_cells_count` returns 0 and `to_cells` an empty iterator.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Line(geo::Line<f64>);

impl Line {
    /// Initialize a new line from a line whose coordinates are in radians.
    ///
    /// # Errors
    ///
    /// [`InvalidGeometry`] if the line is invalid (e.g. contains non-finite
    /// coordinates).
    ///
    /// # Example
    ///
    /// ```
    /// use h3o::geom::Line;
    ///
    /// let line = geo::Line::new(
    ///     geo::coord! { x: 0.05470401801197459, y: 0.8005260881667454 },
    ///     geo::coord! { x: 0.0420053741471695, y: 0.8218402563603641 },
    /// );
    /// let line = Line::from_radians(line)?;
    /// # Ok::<(), h3o::error::InvalidGeometry>(())
    /// ```
    pub fn from_radians(line: geo::Line<f64>) -> Result<Self, InvalidGeometry> {
        Self::check_coords(&line).map(|_| Self(line))
    }

    /// Initialize a new line from a line whose coordinates are in degrees.
    ///
    /// # Errors
    ///
    /// [`InvalidGeometry`] if the line is invalid (e.g. contains non-finite
    /// coordinates).
    ///
    /// # Example
    ///
    /// ```
    /// use h3o::geom::Line;
    ///
    /// let line = geo::Line::new(
    ///     geo::coord! { x: 3.13430935449378,  y: 45.866766242072146 },
    ///     geo::coord! { x: 2.406730655500752, y: 47.08797812339847 },
    /// );
    /// let line = Line::from_degrees(line)?;
    /// # Ok::<(), h3o::error::InvalidGeometry>(())
    /// ```
    pub fn from_degrees(line: geo::Line<f64>) -> Result<Self, InvalidGeometry> {
        let line = geo::Line::new(
            Coord {
                x: line.start.x.to_radians(),
                y: line.start.y.to_radians(),
            },
            Coord {
                x: line.end.x.to_radians(),
                y: line.end.y.to_radians(),
            },
        );
        Self::from_radians(line)
    }

    // Check that the line's coordinates are finite.
    fn check_coords(line: &geo::Line<f64>) -> Result<(), InvalidGeometry> {
        if !super::coord_is_valid(line.start)
            || !super::coord_is_valid(line.end)
        {
            return Err(InvalidGeometry::new("start and end must be valid"));
        }
        Ok(())
    }
}

impl From<Line> for geo::Line<f64> {
    fn from(value: Line) -> Self {
        value.0
    }
}

impl ToCells for Line {
    fn max_cells_count(&self, resolution: Resolution) -> usize {
        cells_count(self.0, resolution)
    }

    fn to_cells(
        &self,
        resolution: Resolution,
    ) -> Box<dyn Iterator<Item = CellIndex> + '_> {
        Box::new(to_cells(self.0, resolution))
    }
}

// ----------------------------------------------------------------------------

pub fn cells_count(line: geo::Line<f64>, resolution: Resolution) -> usize {
    let (start, end) = start_end_cells(&line, resolution);
    // Ideally, this should return an error but see comments in `to_cells`
    // below.
    usize::try_from(start.grid_path_cells_size(end).unwrap_or_default())
        .expect("positive cells count")
}

pub fn to_cells(
    line: geo::Line<f64>,
    resolution: Resolution,
) -> impl Iterator<Item = CellIndex> {
    let (start, end) = start_end_cells(&line, resolution);

    // TODO: We should avoid `collect` here and returns errors on the fly.
    // BUT this would be make the API less convenient since one would need
    // to handle `Error` for `Geometry` while only `Line` and `LineString`
    // can fail.
    // And for no good reason, since sooner or later we should get an
    // implementation of `grid_disk_path` that correctly handles pentagons and
    // stuff.
    //
    // Until then, let's use the suboptimal collect internally to preserve a
    // nice external API.
    let cells = start.grid_path_cells(end).map_or_else(
        |_| Vec::new(),
        |iter| iter.collect::<Result<Vec<_>, _>>().unwrap_or_default(),
    );

    cells.into_iter()
}

// Returns the cell indexes at the start and end of the line for the given
// resolution.
fn start_end_cells(
    line: &geo::Line<f64>,
    resolution: Resolution,
) -> (CellIndex, CellIndex) {
    // TODO: precompute those at creation time?
    // Expect valid coordinates, checked by `check_coords` before.
    let start = LatLng::from_radians(line.start.y, line.start.x)
        .expect("valid start")
        .to_cell(resolution);
    let end = LatLng::from_radians(line.end.y, line.end.x)
        .expect("valid end")
        .to_cell(resolution);

    (start, end)
}