Skip to main content

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}