n3gb-rs 0.2.2

A Rust implementation of a hierarchical hex-based spatial indexing system based on the OSGB National Grid.
Documentation
use crate::coord::Coordinate;
use crate::error::N3gbError;
use crate::index::constants::{
    CELL_RADIUS as RADIUS, CELL_WIDTHS as WIDTHS, GRID_EXTENTS as EXTENTS, MAX_ZOOM_LEVEL,
};
use geo_types::Point;

/// Converts a BNG coordinate to hex grid row/column indices.
///
/// Returns `(row, col)` for the cell containing the given point at the specified zoom level.
///
/// # Arguments
///
/// * `coord` - The BNG coordinate to locate.
/// * `z` - The grid zoom level (must not exceed `MAX_ZOOM_LEVEL`).
///
/// # Returns
///
/// A `(row, col)` tuple identifying the cell that contains the given point.
///
/// # Errors
///
/// Returns [`N3gbError::InvalidZoomLevel`] if `z` exceeds `MAX_ZOOM_LEVEL`.
pub fn point_to_row_col<C: Coordinate>(coord: &C, z: u8) -> Result<(i64, i64), N3gbError> {
    if z > MAX_ZOOM_LEVEL {
        return Err(N3gbError::InvalidZoomLevel(z));
    }

    let hex_width = WIDTHS[z as usize];
    let r = RADIUS[z as usize];
    let dx = hex_width;
    let dy = 1.5 * r;

    let qx = (coord.x() - EXTENTS[0]) / dx;
    let ry = (coord.y() - EXTENTS[1]) / dy;

    let row = ry.round() as i64;
    let col = (qx - row.rem_euclid(2) as f64).round() as i64;

    Ok((row, col))
}

/// Converts hex grid row/column indices to a BNG center point.
///
/// Returns the center point of the cell at the given row, column, and zoom level.
///
/// # Arguments
///
/// * `row` - The row index of the cell.
/// * `col` - The column index of the cell.
/// * `z` - The grid zoom level (must not exceed `MAX_ZOOM_LEVEL`).
///
/// # Returns
///
/// The BNG center [`Point<f64>`] of the cell at the given row, column, and zoom level.
///
/// # Errors
///
/// Returns [`N3gbError::InvalidZoomLevel`] if `z` exceeds `MAX_ZOOM_LEVEL`.
pub fn row_col_to_center(row: i64, col: i64, z: u8) -> Result<Point<f64>, N3gbError> {
    if z > MAX_ZOOM_LEVEL {
        return Err(N3gbError::InvalidZoomLevel(z));
    }

    let hex_width = WIDTHS[z as usize];
    let r = RADIUS[z as usize];
    let dx = hex_width;
    let dy = 1.5 * r;

    let x = EXTENTS[0] + col as f64 * dx + ((row % 2) as f64 * (dx / 2.0));
    let y = EXTENTS[1] + row as f64 * dy;

    Ok(Point::new(x, y))
}

/// Converts odd-r offset (row, col) to cube coordinates (q, r, s).
///
/// # Arguments
///
/// * `row` - The row index in odd-r offset coordinates.
/// * `col` - The column index in odd-r offset coordinates.
///
/// # Returns
///
/// A `(q, r, s)` tuple of cube coordinates equivalent to the given offset coordinates.
pub(crate) fn offset_to_cube(row: i64, col: i64) -> (i64, i64, i64) {
    let q = col - row / 2;
    let r = row;
    let s = -q - r;
    (q, r, s)
}

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

    #[test]
    fn test_point_to_row_col_and_back() -> Result<(), N3gbError> {
        let easting = 457996.0;
        let northing = 339874.0;
        let zoom = 10;

        let (row, col) = point_to_row_col(&(easting, northing), zoom)?;
        let point = row_col_to_center(row, col, zoom)?;

        assert!((point.x() - 457925.0).abs() < 100.0);
        assert!((point.y() - 339888.99).abs() < 100.0);
        Ok(())
    }

    #[test]
    fn test_point_to_row_col_with_point() -> Result<(), N3gbError> {
        let pt = point! { x: 457996.0, y: 339874.0 };
        let zoom = 10;

        let (row, col) = point_to_row_col(&pt, zoom)?;
        let center = row_col_to_center(row, col, zoom)?;

        assert!((center.x() - 457925.0).abs() < 100.0);
        assert!((center.y() - 339888.99).abs() < 100.0);
        Ok(())
    }

    #[test]
    fn test_invalid_zoom_level() {
        let result = point_to_row_col(&(457996.0, 339874.0), 20);
        assert!(matches!(result, Err(N3gbError::InvalidZoomLevel(20))));
    }

    #[test]
    fn test_row_col_to_center_invalid_zoom() {
        let result = row_col_to_center(100, 100, 16);
        assert!(result.is_err());
    }
}