n3gb_rs/lib.rs
1//! # n3gb-rs
2//!
3//! Rust implementation of hex-based spatial indexing for British National Grid.
4//!
5//! Inspired by [GDS NUAR n3gb](https://github.com/national-underground-asset-register/n3gb) and [h3o](https://github.com/HydroniumLabs/h3o).
6//!
7//! ## Core Structs
8//!
9//! ### HexCell
10//!
11//! A single hexagonal cell with a unique ID, center point, and zoom level.
12//!
13//! **Create one:**
14//!
15//! ```no_run
16//! use n3gb_rs::HexCell;
17//! use geo_types::Point;
18//!
19//! // From BNG coordinates
20//! let point = Point::new(383640.0, 398260.0);
21//! let cell = HexCell::from_bng(&point, 12).unwrap();
22//!
23//! // From an existing ID
24//! let cell = HexCell::from_hex_id("AAF3kQBBMZQM").unwrap();
25//! ```
26//!
27//! **What you can do:**
28//!
29//! ```ignore
30//! cell.id // Unique Base64 identifier
31//! cell.easting() // Center X coordinate
32//! cell.northing() // Center Y coordinate
33//! cell.to_polygon() // Get the hex as a geo_types Polygon
34//! ```
35//!
36//! ### HexGrid
37//!
38//! A collection of HexCells covering a bounding box.
39//!
40//! **Create one:**
41//!
42//! ```
43//! use n3gb_rs::HexGrid;
44//!
45//! # fn main() -> Result<(), n3gb_rs::N3gbError> {
46//! // Using builder
47//! let grid = HexGrid::builder()
48//! .zoom_level(12)
49//! .bng_extent(&(300000.0, 300000.0), &(350000.0, 350000.0))
50//! .build()?;
51//!
52//! // Direct construction
53//! let grid = HexGrid::from_bng_extent(&(300000.0, 300000.0), &(350000.0, 350000.0), 12)?;
54//! # Ok(())
55//! # }
56//! ```
57//!
58//! **What you can do:**
59//!
60//! ```no_run
61//! use n3gb_rs::HexGrid;
62//! use geo_types::point;
63//!
64//! # let grid = HexGrid::from_bng_extent(&(300000.0, 300000.0), &(350000.0, 350000.0), 12).unwrap();
65//! // Look up a cell by point
66//! let pt = point! { x: 325000.0, y: 325000.0 };
67//! if let Some(cell) = grid.get_cell_at(&pt) {
68//! println!("{}", cell.id);
69//! }
70//!
71//! // Iterate over all cells
72//! for cell in grid.cells() {
73//! println!("{}", cell.id);
74//! }
75//!
76//! // Export to GeoParquet
77//! grid.to_geoparquet("output.parquet").unwrap();
78//!
79//! // Export to Arrow RecordBatch
80//! let batch = grid.to_record_batch().unwrap();
81//! ```
82//!
83//! ## API Reference
84//!
85//! For people used to similar hexagonal indexing systems (like H3), here is the mapping to n3gb-rs.
86//!
87//! ### Indexing functions
88//!
89//! | Concept | n3gb-rs |
90//! | :----------------------- | :--------------------------------------- |
91//! | Point to cell (BNG) | `HexCell::from_bng` |
92//! | Point to cell (WGS84) | `HexCell::from_wgs84` |
93//! | Geometry to cells | `HexCell::from_geometry` |
94//! | Cell ID to cell | `HexCell::from_hex_id` |
95//! | Generate cell ID | `generate_hex_identifier` |
96//! | Decode cell ID | `decode_hex_identifier` |
97//! | Point to row/col | `point_to_row_col` |
98//! | Row/col to center | `row_col_to_center` |
99//!
100//! ### Cell inspection functions
101//!
102//! | Concept | n3gb-rs |
103//! | :----------------------- | :--------------------------------------- |
104//! | Get zoom level | `HexCell::zoom_level` |
105//! | Get cell ID | `HexCell::id` |
106//! | Get center point | `HexCell::center` |
107//! | Get easting | `HexCell::easting` |
108//! | Get northing | `HexCell::northing` |
109//! | Get row index | `HexCell::row` |
110//! | Get column index | `HexCell::col` |
111//! | Cell to polygon | `HexCell::to_polygon` |
112//!
113//! ### Grid functions
114//!
115//! | Concept | n3gb-rs |
116//! | :------------------------ | :-------------------------------------- |
117//! | Grid from extent (BNG) | `HexGrid::from_bng_extent` |
118//! | Grid from extent (WGS84) | `HexGrid::from_wgs84_extent` |
119//! | Grid from rect | `HexGrid::from_rect` |
120//! | Grid from polygon (BNG) | `HexGrid::from_bng_polygon` |
121//! | Grid from polygon (WGS84) | `HexGrid::from_wgs84_polygon` |
122//! | Grid from multipolygon | `HexGrid::from_bng_multipolygon` |
123//! | Grid builder | `HexGridBuilder` |
124//! | Get cells | `HexGrid::cells` |
125//! | Get cell count | `HexGrid::len` |
126//! | Find cell at point | `HexGrid::get_cell_at` |
127//! | Filter cells | `HexGrid::filter` |
128//! | Grid to polygons | `HexGrid::to_polygons` |
129//!
130//! ### Line coverage functions
131//!
132//! | Concept | n3gb-rs |
133//! | :----------------------- | :--------------------------------------- |
134//! | Line to cells (BNG) | `HexCell::from_line_string_bng` |
135//! | Line to cells (WGS84) | `HexCell::from_line_string_wgs84` |
136//!
137//! ### Coordinate transformation functions
138//!
139//! | Concept | n3gb-rs |
140//! | :------------------------ | :-------------------------------------- |
141//! | WGS84 to BNG point | `wgs84_to_bng` |
142//! | WGS84 to BNG polygon | `wgs84_polygon_to_bng` |
143//! | WGS84 to BNG multipolygon | `wgs84_multipolygon_to_bng` |
144//! | WGS84 to BNG line | `wgs84_line_to_bng` |
145//!
146//! ### Hexagon dimension functions
147//!
148//! | Concept | n3gb-rs |
149//! | :------------------------- | :------------------------------------- |
150//! | Dims from side length | `HexagonDims::from_side` |
151//! | Dims from circumradius | `HexagonDims::from_circumradius` |
152//! | Dims from apothem | `HexagonDims::from_apothem` |
153//! | Dims from flat-to-flat | `HexagonDims::from_across_flats` |
154//! | Dims from corner-to-corner | `HexagonDims::from_across_corners` |
155//! | Dims from area | `HexagonDims::from_area` |
156//! | Bounding box | `bounding_box` |
157//!
158//! ### Geometry functions
159//!
160//! | Concept | n3gb-rs |
161//! | :----------------------- | :--------------------------------------- |
162//! | Create hex cell polygon | `create_hexagon` (used in to_polygon) |
163//! | Parse WKT/GeoJSON | `parse_geometry` |
164//!
165//! ### Arrow/Parquet I/O functions
166//!
167//! | Concept | n3gb-rs |
168//! | :----------------------- | :--------------------------------------- |
169//! | Cell to Arrow points | `HexCell::to_arrow_points` |
170//! | Cell to Arrow polygons | `HexCell::to_arrow_polygons` |
171//! | Cell to RecordBatch | `HexCell::to_record_batch` |
172//! | Cell to GeoParquet | `HexCell::to_geoparquet` |
173//! | Grid to Arrow points | `HexGrid::to_arrow_points` |
174//! | Grid to Arrow polygons | `HexGrid::to_arrow_polygons` |
175//! | Grid to RecordBatch | `HexGrid::to_record_batch` |
176//! | Grid to GeoParquet | `HexGrid::to_geoparquet` |
177//! | Write GeoParquet | `write_geoparquet` |
178//!
179//! ### CSV I/O functions
180//!
181//! | Concept | n3gb-rs |
182//! | :----------------------- | :--------------------------------------- |
183//! | CSV to hex-indexed CSV | `csv_to_hex_csv` |
184//! | CSV config (geometry) | `CsvHexConfig::new` |
185//! | CSV config (coords) | `CsvHexConfig::from_coords` |
186//!
187//! ### Constants
188//!
189//! | Concept | n3gb-rs |
190//! | :----------------------- | :--------------------------------------- |
191//! | Max zoom level | `MAX_ZOOM_LEVEL` |
192//! | Cell radii by zoom | `CELL_RADIUS` |
193//! | Cell widths by zoom | `CELL_WIDTHS` |
194//! | Grid extents (BNG) | `GRID_EXTENTS` |
195//! | Identifier version | `IDENTIFIER_VERSION` |
196
197mod cell;
198mod coord;
199mod dimensions;
200mod error;
201mod geom;
202mod grid;
203mod index;
204mod io;
205
206pub use cell::HexCell;
207pub use coord::{ConversionMethod, Coordinate, Crs};
208pub use dimensions::{
209 HexagonDims, bounding_box, from_across_corners, from_across_flats, from_apothem, from_area,
210 from_circumradius, from_side,
211};
212pub use error::N3gbError;
213pub use grid::{HexGrid, HexGridBuilder};
214pub use index::{
215 CELL_RADIUS, CELL_WIDTHS, GRID_EXTENTS, IDENTIFIER_VERSION, MAX_ZOOM_LEVEL,
216 decode_hex_identifier, generate_hex_identifier, point_to_row_col, row_col_to_center,
217};
218pub use io::{
219 CoordinateSource, CsvHexConfig, GeometryFormat, HexCellsToArrow, HexCellsToGeoParquet,
220 csv_to_hex_csv, write_geoparquet,
221};
222
223pub use geom::{create_hexagon, parse_geometry};
224
225pub use geo_types;
226pub use geoarrow_array;
227pub use geoarrow_schema;
228pub use geoparquet;
229
230#[cfg(test)]
231mod tests {
232 use super::*;
233 use geo_types::{Rect, coord, point};
234
235 #[test]
236 fn test_end_to_end_workflow() -> Result<(), N3gbError> {
237 let grid = HexGrid::builder()
238 .zoom_level(10)
239 .bng_extent(&(457000.0, 339500.0), &(458000.0, 340500.0))
240 .build()?;
241
242 assert!(!grid.is_empty());
243 assert_eq!(grid.zoom_level(), 10);
244
245 let pt = point! { x: 457500.0, y: 340000.0 };
246 let cell = grid.get_cell_at(&pt);
247 assert!(cell.is_some());
248
249 if let Some(cell) = cell {
250 let (version, _easting, _northing, zoom) = decode_hex_identifier(&cell.id)?;
251 assert_eq!(version, IDENTIFIER_VERSION);
252 assert_eq!(zoom, 10);
253
254 let polygon = cell.to_polygon();
255 assert_eq!(polygon.exterior().coords().count(), 7);
256 }
257 Ok(())
258 }
259
260 #[test]
261 fn test_using_geo_types_macros() -> Result<(), N3gbError> {
262 let pt = point! { x: 457996.0, y: 339874.0 };
263 let (row, col) = point_to_row_col(&pt, 10)?;
264 assert!(row > 0);
265 assert!(col > 0);
266
267 let rect = Rect::new(
268 coord! { x: 457000.0, y: 339500.0 },
269 coord! { x: 458000.0, y: 340500.0 },
270 );
271 let grid = HexGrid::from_rect(&rect, 10)?;
272 assert!(!grid.is_empty());
273 Ok(())
274 }
275
276 #[test]
277 fn test_dimensions_workflow() -> Result<(), N3gbError> {
278 let dims = from_side(10.0)?;
279
280 assert!((dims.a - 10.0).abs() < 0.001);
281 assert!((dims.perimeter - 60.0).abs() < 0.001);
282
283 let dims2 = from_area(dims.area)?;
284 assert!((dims2.a - 10.0).abs() < 0.001);
285 Ok(())
286 }
287
288 #[test]
289 fn test_grid_iteration() -> Result<(), N3gbError> {
290 let grid = HexGrid::from_bng_extent(&(457000.0, 339500.0), &(458000.0, 340500.0), 10)?;
291
292 let mut count = 0;
293 for cell in grid.iter() {
294 assert_eq!(cell.zoom_level, 10);
295 count += 1;
296 }
297
298 assert_eq!(count, grid.len());
299 Ok(())
300 }
301
302 #[test]
303 fn test_grid_filtering() -> Result<(), N3gbError> {
304 let grid = HexGrid::from_bng_extent(&(457000.0, 339500.0), &(458000.0, 340500.0), 10)?;
305
306 let high_easting = grid.filter(|cell| cell.easting() > 457500.0);
307 assert!(!high_easting.is_empty());
308 assert!(high_easting.len() < grid.len());
309 Ok(())
310 }
311
312 #[test]
313 fn test_hexcell_from_bng() -> Result<(), N3gbError> {
314 let cell = HexCell::from_bng(&(383640.0, 398260.0), 12)?;
315
316 assert_eq!(cell.zoom_level, 12);
317 assert!(!cell.id.is_empty());
318 assert!(cell.row > 0);
319 assert!(cell.col > 0);
320
321 let polygon = cell.to_polygon();
322 assert_eq!(polygon.exterior().coords().count(), 7);
323 Ok(())
324 }
325
326 #[test]
327 fn test_hexcell_from_wgs84() -> Result<(), N3gbError> {
328 let cell = HexCell::from_wgs84(&(-2.248, 53.481), 12, ConversionMethod::default())?;
329
330 assert_eq!(cell.zoom_level, 12);
331 assert!(!cell.id.is_empty());
332 assert!(cell.easting() > 380000.0 && cell.easting() < 390000.0);
333 assert!(cell.northing() > 390000.0 && cell.northing() < 400000.0);
334 Ok(())
335 }
336
337 #[test]
338 fn test_hexcell_consistency_with_hexgrid() -> Result<(), N3gbError> {
339 let cell_direct = HexCell::from_bng(&(457500.0, 340000.0), 10)?;
340
341 let grid = HexGrid::from_bng_extent(&(457000.0, 339500.0), &(458000.0, 340500.0), 10)?;
342 let pt = point! { x: 457500.0, y: 340000.0 };
343 let cell_from_grid = grid.get_cell_at(&pt);
344
345 assert!(cell_from_grid.is_some());
346 let cell_from_grid = cell_from_grid.unwrap();
347
348 assert_eq!(cell_direct.id, cell_from_grid.id);
349 assert_eq!(cell_direct.row, cell_from_grid.row);
350 assert_eq!(cell_direct.col, cell_from_grid.col);
351 Ok(())
352 }
353}