use geo_types::{Geometry, LineString, MultiPolygon, Polygon, coord};
use crate::error::{Error, Result};
use crate::geom::{self, Geom};
const MAX_CELLS: i64 = 100_000;
fn cell_in(lo: f64, hi: f64, min: f64, max: f64) -> bool {
lo <= max && hi > min
}
fn cell_range(min: f64, max: f64, size: f64) -> (i64, i64) {
((min / size).floor() as i64, (max / size).floor() as i64)
}
fn bounds_of(bounds: &[u8], func: &'static str) -> Result<Option<(Geom, [f64; 4])>> {
use geo::BoundingRect;
let g = geom::decode_auto(bounds)?;
let Some(r) = g.geometry.bounding_rect() else {
return Ok(None);
};
if ![r.min().x, r.min().y, r.max().x, r.max().y]
.iter()
.all(|v| v.is_finite())
{
return Err(Error::Unsupported {
func,
reason: "the bounds have a non-finite coordinate".into(),
});
}
Ok(Some((g, [r.min().x, r.min().y, r.max().x, r.max().y])))
}
fn out_2d(cells: Vec<Polygon<f64>>, srid: i32, func: &'static str) -> Result<Vec<u8>> {
geom::encode_canonical_gpb(
&Geom {
geometry: Geometry::MultiPolygon(MultiPolygon::new(cells)),
srid,
has_zm: false,
},
func,
)
}
fn too_many(func: &'static str, wanted: i64) -> Error {
Error::Unsupported {
func,
reason: format!(
"that is {wanted} cells, over kenro's {MAX_CELLS} limit; PostGIS streams grid rows \
but kenro returns one MULTIPOLYGON, so enlarge the size or shrink the bounds"
),
}
}
pub fn st_square_grid(size: f64, bounds: &[u8]) -> Result<Vec<u8>> {
const FUNC: &str = "ST_SquareGrid";
let Some((g, [minx, miny, maxx, maxy])) = bounds_of(bounds, FUNC)? else {
return out_2d(vec![], geom::decode_auto(bounds)?.srid, FUNC);
};
if !size.is_finite() || size <= 0.0 {
return out_2d(vec![], g.srid, FUNC);
}
let (i0, i1) = cell_range(minx, maxx, size);
let (j0, j1) = cell_range(miny, maxy, size);
let count = (i1 - i0 + 1).saturating_mul(j1 - j0 + 1);
if count > MAX_CELLS {
return Err(too_many(FUNC, count));
}
let mut cells = Vec::with_capacity(count.max(0) as usize);
for i in i0..=i1 {
for j in j0..=j1 {
let (x0, y0) = (i as f64 * size, j as f64 * size);
let (x1, y1) = (x0 + size, y0 + size);
cells.push(Polygon::new(
LineString::new(vec![
coord! { x: x0, y: y0 },
coord! { x: x0, y: y1 },
coord! { x: x1, y: y1 },
coord! { x: x1, y: y0 },
coord! { x: x0, y: y0 },
]),
vec![],
));
}
}
out_2d(cells, g.srid, FUNC)
}
pub fn st_hexagon_grid(size: f64, bounds: &[u8]) -> Result<Vec<u8>> {
const FUNC: &str = "ST_HexagonGrid";
let Some((g, [minx, miny, maxx, maxy])) = bounds_of(bounds, FUNC)? else {
return out_2d(vec![], geom::decode_auto(bounds)?.srid, FUNC);
};
if !size.is_finite() || size <= 0.0 {
return out_2d(vec![], g.srid, FUNC);
}
let half_h = size * 3.0_f64.sqrt() / 2.0; let row = half_h * 2.0;
let i0 = ((minx - size) / (1.5 * size)).floor() as i64 - 1;
let i1 = ((maxx + size) / (1.5 * size)).ceil() as i64 + 1;
let mut cells = Vec::new();
for i in i0..=i1 {
let cx = 1.5 * size * i as f64;
if !cell_in(cx - size, cx + size, minx, maxx) {
continue;
}
let stagger = if i.rem_euclid(2) == 1 { half_h } else { 0.0 };
let j0 = ((miny - stagger - half_h) / row).floor() as i64 - 1;
let j1 = ((maxy - stagger + half_h) / row).ceil() as i64 + 1;
for j in j0..=j1 {
let cy = row * j as f64 + stagger;
if !cell_in(cy - half_h, cy + half_h, miny, maxy) {
continue;
}
if cells.len() as i64 >= MAX_CELLS {
return Err(too_many(FUNC, MAX_CELLS + 1));
}
cells.push(Polygon::new(
LineString::new(vec![
coord! { x: cx - size, y: cy },
coord! { x: cx - size / 2.0, y: cy - half_h },
coord! { x: cx + size / 2.0, y: cy - half_h },
coord! { x: cx + size, y: cy },
coord! { x: cx + size / 2.0, y: cy + half_h },
coord! { x: cx - size / 2.0, y: cy + half_h },
coord! { x: cx - size, y: cy },
]),
vec![],
));
}
}
out_2d(cells, g.srid, FUNC)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::functions::accessors::st_num_geometries;
use crate::functions::io::{st_as_text, st_geom_from_text, st_set_srid, st_srid};
fn env(minx: f64, miny: f64, maxx: f64, maxy: f64) -> Vec<u8> {
st_geom_from_text(
&format!(
"POLYGON(({minx} {miny},{maxx} {miny},{maxx} {maxy},{minx} {maxy},{minx} {miny}))"
),
None,
)
.unwrap()
}
#[test]
fn the_square_grid_is_anchored_at_the_origin_like_postgis() {
let grid = st_square_grid(1.0, &env(0.0, 0.0, 3.0, 2.0)).unwrap();
assert_eq!(st_num_geometries(&grid).unwrap(), 12);
let small = st_as_text(&st_square_grid(1.0, &env(0.0, 0.0, 2.0, 1.0)).unwrap()).unwrap();
assert!(
small.starts_with("MULTIPOLYGON(((0 0,0 1,1 1,1 0,0 0)),((0 1,0 2,1 2,1 1,0 1))"),
"{small}"
);
assert_eq!(
st_num_geometries(&st_square_grid(1.0, &env(0.0, 0.0, 2.0, 1.0)).unwrap()).unwrap(),
6
);
let offset = st_as_text(&st_square_grid(1.0, &env(0.5, 0.5, 1.6, 1.4)).unwrap()).unwrap();
assert_eq!(
offset,
"MULTIPOLYGON(((0 0,0 1,1 1,1 0,0 0)),((0 1,0 2,1 2,1 1,0 1)),\
((1 0,1 1,2 1,2 0,1 0)),((1 1,1 2,2 2,2 1,1 1)))"
);
let neg = st_as_text(&st_square_grid(1.0, &env(-1.5, -1.5, -0.4, -0.4)).unwrap()).unwrap();
assert!(
neg.starts_with("MULTIPOLYGON(((-2 -2,-2 -1,-1 -1,-1 -2,-2 -2))"),
"{neg}"
);
assert_eq!(
st_num_geometries(&st_square_grid(1.0, &env(-1.5, -1.5, -0.4, -0.4)).unwrap()).unwrap(),
4
);
}
#[test]
fn the_hexagon_layout_is_postgis_s() {
let one = st_as_text(&st_hexagon_grid(1.0, &env(0.0, 0.0, 0.1, 0.1)).unwrap()).unwrap();
assert_eq!(
one,
"MULTIPOLYGON(((-1 0,-0.5 -0.8660254037844386,0.5 -0.8660254037844386,1 0,\
0.5 0.8660254037844386,-0.5 0.8660254037844386,-1 0)))"
);
assert_eq!(
st_num_geometries(&st_hexagon_grid(1.0, &env(0.0, 0.0, 3.0, 3.0)).unwrap()).unwrap(),
8
);
assert_eq!(
st_num_geometries(&st_hexagon_grid(1.0, &env(0.0, 0.0, 1.0, 1.0)).unwrap()).unwrap(),
3
);
assert_eq!(
st_num_geometries(&st_hexagon_grid(1.0, &env(0.0, 0.0, 6.0, 2.0)).unwrap()).unwrap(),
10
);
}
#[test]
fn the_edges_behave_as_postgis_does() {
assert_eq!(
st_as_text(&st_square_grid(0.0, &env(0.0, 0.0, 2.0, 2.0)).unwrap()).unwrap(),
"MULTIPOLYGON EMPTY"
);
assert_eq!(
st_as_text(&st_hexagon_grid(-1.0, &env(0.0, 0.0, 2.0, 2.0)).unwrap()).unwrap(),
"MULTIPOLYGON EMPTY"
);
let tri = st_geom_from_text("POLYGON((0 0,3 0,0 3,0 0))", None).unwrap();
assert_eq!(
st_num_geometries(&st_square_grid(1.0, &tri).unwrap()).unwrap(),
16
);
let pt = st_geom_from_text("POINT(0.5 0.5)", None).unwrap();
assert_eq!(
st_num_geometries(&st_square_grid(1.0, &pt).unwrap()).unwrap(),
1
);
let labelled = st_set_srid(&env(0.0, 0.0, 2.0, 2.0), 4326).unwrap();
assert_eq!(
st_srid(&st_square_grid(1.0, &labelled).unwrap()).unwrap(),
4326
);
let err = st_square_grid(1.0, &env(0.0, 0.0, 1000.0, 1000.0))
.unwrap_err()
.to_string();
assert!(err.contains("cells"), "{err}");
}
}