use crate::cell::HexCell;
use crate::coord::{
ConversionMethod, Coordinate, convert_multipolygon_to_bng, convert_polygon_to_bng,
convert_to_bng,
};
use crate::error::N3gbError;
use crate::index::{GRID_EXTENTS, generate_hex_identifier, point_to_row_col, row_col_to_center};
use crate::io::arrow::HexCellsToArrow;
use crate::io::parquet::HexCellsToGeoParquet;
use arrow_array::RecordBatch;
use geo::{BoundingRect, Intersects};
use geo_types::{MultiPolygon, Point, Polygon, Rect};
use geoarrow_array::array::{PointArray, PolygonArray};
use rayon::prelude::*;
use std::collections::HashMap;
use std::path::Path;
#[derive(Debug, Clone)]
pub struct HexGrid {
cells: Vec<HexCell>,
index: HashMap<(i64, i64), usize>,
zoom_level: u8,
}
impl HexGrid {
fn new(cells: Vec<HexCell>, zoom_level: u8) -> Self {
let index = cells
.iter()
.enumerate()
.map(|(i, cell)| ((cell.row, cell.col), i))
.collect();
Self {
cells,
index,
zoom_level,
}
}
pub fn builder() -> HexGridBuilder {
HexGridBuilder::new()
}
fn from_extent(
min_x: f64,
min_y: f64,
max_x: f64,
max_y: f64,
zoom_level: u8,
) -> Result<Self, N3gbError> {
let cells = generate_cells_for_extent(min_x, min_y, max_x, max_y, zoom_level)?;
Ok(Self::new(cells, zoom_level))
}
pub fn from_rect(rect: &Rect<f64>, zoom_level: u8) -> Result<Self, N3gbError> {
Self::from_extent(
rect.min().x,
rect.min().y,
rect.max().x,
rect.max().y,
zoom_level,
)
}
pub fn from_bng_extent(
min: &impl Coordinate,
max: &impl Coordinate,
zoom_level: u8,
) -> Result<Self, N3gbError> {
Self::from_extent(min.x(), min.y(), max.x(), max.y(), zoom_level)
}
pub fn from_wgs84_extent(
min: &impl Coordinate,
max: &impl Coordinate,
zoom_level: u8,
method: ConversionMethod,
) -> Result<Self, N3gbError> {
let min_bng = convert_to_bng(min, method)?;
let max_bng = convert_to_bng(max, method)?;
Self::from_extent(
min_bng.x(),
min_bng.y(),
max_bng.x(),
max_bng.y(),
zoom_level,
)
}
pub fn from_bng_polygon(polygon: &Polygon<f64>, zoom_level: u8) -> Result<Self, N3gbError> {
let bbox = match polygon.bounding_rect() {
Some(rect) => rect,
None => return Ok(Self::new(Vec::new(), zoom_level)),
};
Ok(Self::from_rect(&bbox, zoom_level)?
.retain(|cell| polygon.intersects(&cell.to_polygon())))
}
pub fn from_wgs84_polygon(
polygon: &Polygon<f64>,
zoom_level: u8,
method: ConversionMethod,
) -> Result<Self, N3gbError> {
let bng_polygon = convert_polygon_to_bng(polygon, method)?;
Self::from_bng_polygon(&bng_polygon, zoom_level)
}
pub fn from_bng_multipolygon(
multipolygon: &MultiPolygon<f64>,
zoom_level: u8,
) -> Result<Self, N3gbError> {
let bbox = match multipolygon.bounding_rect() {
Some(rect) => rect,
None => return Ok(Self::new(Vec::new(), zoom_level)),
};
Ok(Self::from_rect(&bbox, zoom_level)?
.retain(|cell| multipolygon.intersects(&cell.to_polygon())))
}
pub fn from_wgs84_multipolygon(
multipolygon: &MultiPolygon<f64>,
zoom_level: u8,
method: ConversionMethod,
) -> Result<Self, N3gbError> {
let bng_multipolygon = convert_multipolygon_to_bng(multipolygon, method)?;
Self::from_bng_multipolygon(&bng_multipolygon, zoom_level)
}
fn retain<F>(self, predicate: F) -> Self
where
F: Fn(&HexCell) -> bool + Sync,
{
let cells: Vec<HexCell> = self
.cells
.into_par_iter()
.filter(|cell| predicate(cell))
.collect();
Self::new(cells, self.zoom_level)
}
pub fn zoom_level(&self) -> u8 {
self.zoom_level
}
pub fn len(&self) -> usize {
self.cells.len()
}
pub fn is_empty(&self) -> bool {
self.cells.is_empty()
}
pub fn cells(&self) -> &[HexCell] {
&self.cells
}
pub fn iter(&self) -> impl Iterator<Item = &HexCell> {
self.cells.iter()
}
pub fn get_cell_at(&self, point: &Point<f64>) -> Option<&HexCell> {
let (row, col) = point_to_row_col(point, self.zoom_level).ok()?;
self.index.get(&(row, col)).map(|&i| &self.cells[i])
}
pub fn to_polygons(&self) -> Vec<Polygon<f64>> {
self.cells
.par_iter()
.map(|cell| cell.to_polygon())
.collect()
}
pub fn filter<F>(&self, predicate: F) -> Vec<&HexCell>
where
F: Fn(&HexCell) -> bool,
{
self.cells.iter().filter(|cell| predicate(cell)).collect()
}
pub fn to_arrow_points(&self) -> PointArray {
self.cells.to_arrow_points()
}
pub fn to_arrow_polygons(&self) -> PolygonArray {
self.cells.to_arrow_polygons()
}
pub fn to_record_batch(&self) -> Result<RecordBatch, N3gbError> {
self.cells.to_record_batch()
}
pub fn to_geoparquet(&self, path: impl AsRef<Path>) -> Result<(), N3gbError> {
self.cells.to_geoparquet(path)
}
}
impl<'a> IntoIterator for &'a HexGrid {
type Item = &'a HexCell;
type IntoIter = std::slice::Iter<'a, HexCell>;
fn into_iter(self) -> Self::IntoIter {
self.cells.iter()
}
}
impl IntoIterator for HexGrid {
type Item = HexCell;
type IntoIter = std::vec::IntoIter<HexCell>;
fn into_iter(self) -> Self::IntoIter {
self.cells.into_iter()
}
}
#[derive(Debug, Default, Clone)]
pub struct HexGridBuilder {
zoom_level: Option<u8>,
min_x: Option<f64>,
min_y: Option<f64>,
max_x: Option<f64>,
max_y: Option<f64>,
polygon: Option<Polygon<f64>>,
multipolygon: Option<MultiPolygon<f64>>,
conversion_method: ConversionMethod,
}
impl HexGridBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn zoom_level(mut self, zoom_level: u8) -> Self {
self.zoom_level = Some(zoom_level);
self
}
pub fn conversion_method(mut self, method: ConversionMethod) -> Self {
self.conversion_method = method;
self
}
pub fn rect(mut self, rect: &Rect<f64>) -> Self {
self.min_x = Some(rect.min().x);
self.min_y = Some(rect.min().y);
self.max_x = Some(rect.max().x);
self.max_y = Some(rect.max().y);
self
}
pub fn bng_extent(mut self, min: &impl Coordinate, max: &impl Coordinate) -> Self {
self.min_x = Some(min.x());
self.min_y = Some(min.y());
self.max_x = Some(max.x());
self.max_y = Some(max.y());
self
}
pub fn wgs84_extent(
mut self,
min: &impl Coordinate,
max: &impl Coordinate,
) -> Result<Self, N3gbError> {
let min_bng = convert_to_bng(min, self.conversion_method)?;
let max_bng = convert_to_bng(max, self.conversion_method)?;
self.min_x = Some(min_bng.x());
self.min_y = Some(min_bng.y());
self.max_x = Some(max_bng.x());
self.max_y = Some(max_bng.y());
Ok(self)
}
pub fn bng_polygon(mut self, polygon: Polygon<f64>) -> Self {
self.polygon = Some(polygon);
self
}
pub fn wgs84_polygon(mut self, polygon: Polygon<f64>) -> Result<Self, N3gbError> {
let bng_polygon = convert_polygon_to_bng(&polygon, self.conversion_method)?;
self.polygon = Some(bng_polygon);
Ok(self)
}
pub fn bng_multipolygon(mut self, multipolygon: MultiPolygon<f64>) -> Self {
self.multipolygon = Some(multipolygon);
self
}
pub fn wgs84_multipolygon(
mut self,
multipolygon: MultiPolygon<f64>,
) -> Result<Self, N3gbError> {
let bng_multipolygon = convert_multipolygon_to_bng(&multipolygon, self.conversion_method)?;
self.multipolygon = Some(bng_multipolygon);
Ok(self)
}
pub fn build(self) -> Result<HexGrid, N3gbError> {
let zoom_level = self.zoom_level.expect("zoom_level must be set");
match (self.multipolygon, self.polygon) {
(Some(mp), _) => HexGrid::from_bng_multipolygon(&mp, zoom_level),
(_, Some(p)) => HexGrid::from_bng_polygon(&p, zoom_level),
(None, None) => {
let min_x = self
.min_x
.expect("extent, polygon, or multipolygon must be set");
let min_y = self
.min_y
.expect("extent, polygon, or multipolygon must be set");
let max_x = self
.max_x
.expect("extent, polygon, or multipolygon must be set");
let max_y = self
.max_y
.expect("extent, polygon, or multipolygon must be set");
HexGrid::from_extent(min_x, min_y, max_x, max_y, zoom_level)
}
}
}
}
fn generate_cells_for_extent(
min_x: f64,
min_y: f64,
max_x: f64,
max_y: f64,
zoom_level: u8,
) -> Result<Vec<HexCell>, N3gbError> {
let (ll_row, ll_col) = point_to_row_col(&(min_x, min_y), zoom_level)?;
let (lr_row, lr_col) = point_to_row_col(&(max_x, min_y), zoom_level)?;
let (ur_row, ur_col) = point_to_row_col(&(max_x, max_y), zoom_level)?;
let (ul_row, ul_col) = point_to_row_col(&(min_x, max_y), zoom_level)?;
let min_row = ll_row.min(lr_row).min(ur_row).min(ul_row);
let max_row = ll_row.max(lr_row).max(ur_row).max(ul_row);
let min_col = ll_col.min(lr_col).min(ur_col).min(ul_col);
let max_col = ll_col.max(lr_col).max(ur_col).max(ul_col);
let row_cols: Vec<(i64, i64)> = (min_row..=max_row)
.flat_map(|row| (min_col..=max_col).map(move |col| (row, col)))
.collect();
let cells: Vec<HexCell> = row_cols
.into_par_iter()
.filter_map(|(row, col)| {
let center = row_col_to_center(row, col, zoom_level).ok()?;
if center.x() < GRID_EXTENTS[0] || center.y() < GRID_EXTENTS[1] {
return None;
}
let id = generate_hex_identifier(center.x(), center.y(), zoom_level);
Some(HexCell::new(id, center, zoom_level, row, col))
})
.collect();
Ok(cells)
}
#[cfg(test)]
mod tests {
use super::*;
use geo_types::{coord, point};
#[test]
fn test_hex_grid_from_bng_extent() -> Result<(), N3gbError> {
let grid = HexGrid::from_bng_extent(&(457000.0, 339500.0), &(458000.0, 340500.0), 10)?;
assert!(!grid.is_empty());
assert_eq!(grid.zoom_level(), 10);
for cell in grid.iter() {
assert_eq!(cell.zoom_level, 10);
}
Ok(())
}
#[test]
fn test_hex_grid_from_rect() -> Result<(), N3gbError> {
let rect = Rect::new(
coord! { x: 457000.0, y: 339500.0 },
coord! { x: 458000.0, y: 340500.0 },
);
let grid = HexGrid::from_rect(&rect, 10)?;
assert!(!grid.is_empty());
Ok(())
}
#[test]
fn test_hex_grid_builder() -> Result<(), N3gbError> {
let grid = HexGrid::builder()
.zoom_level(10)
.bng_extent(&(457000.0, 339500.0), &(458000.0, 340500.0))
.build()?;
assert!(!grid.is_empty());
assert_eq!(grid.zoom_level(), 10);
Ok(())
}
#[test]
fn test_hex_grid_builder_with_rect() -> Result<(), N3gbError> {
let rect = Rect::new(
coord! { x: 457000.0, y: 339500.0 },
coord! { x: 458000.0, y: 340500.0 },
);
let grid = HexGrid::builder().zoom_level(10).rect(&rect).build()?;
assert!(!grid.is_empty());
Ok(())
}
#[test]
fn test_get_cell_at() -> Result<(), N3gbError> {
let grid = HexGrid::from_bng_extent(&(457000.0, 339500.0), &(458000.0, 340500.0), 10)?;
let pt = point! { x: 457500.0, y: 340000.0 };
let cell = grid.get_cell_at(&pt);
assert!(cell.is_some());
Ok(())
}
#[test]
fn test_filter_cells() -> Result<(), N3gbError> {
let grid = HexGrid::from_bng_extent(&(457000.0, 339500.0), &(458000.0, 340500.0), 10)?;
let filtered = grid.filter(|cell| cell.easting() > 457500.0);
assert!(!filtered.is_empty());
Ok(())
}
#[test]
fn test_to_polygons() -> Result<(), N3gbError> {
let grid = HexGrid::from_bng_extent(&(457000.0, 339500.0), &(458000.0, 340500.0), 10)?;
let polygons = grid.to_polygons();
assert_eq!(polygons.len(), grid.len());
Ok(())
}
#[test]
fn test_invalid_zoom_level() {
let result = HexGrid::from_bng_extent(&(457000.0, 339500.0), &(458000.0, 340500.0), 20);
assert!(matches!(result, Err(N3gbError::InvalidZoomLevel(20))));
}
}