diff --git a/.github/workflows/rust-ci.yml b/.github/workflows/rust-ci.yml new file mode 100644 index 0000000..5899e9f --- /dev/null +++ b/.github/workflows/rust-ci.yml @@ -0,0 +1,67 @@ +name: Rust CI + +on: + push: + branches: [ "main", "master" ] + pull_request: + branches: [ "main", "master" ] + +env: + CARGO_TERM_COLOR: always + PKG_CONFIG_PATH: /usr/lib/pkgconfig + +jobs: + build: + strategy: + matrix: + os: [ ubuntu-latest, macos-latest, windows-latest ] + arch: [x64, arm64] + + runs-on: ${{ matrix.os }} + + steps: + - uses: actions/checkout@v4 + - name: Build + run: cargo build --verbose + - name: Run tests + run: cargo test --verbose -- --test-threads=2 + + clippy: + name: Lints + runs-on: ubuntu-latest + steps: + - name: Checkout the repository + uses: actions/checkout@v4 + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@stable + with: + components: clippy + - name: Check the lints + run: cargo clippy --tests --verbose -- -D warnings + + + rustfmt: + name: Formatting + runs-on: ubuntu-latest + steps: + - name: Checkout the repository + uses: actions/checkout@v4 + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt + - name: Check the formatting + run: cargo fmt -- --check --verbose + + lychee: + name: Links + runs-on: ubuntu-latest + steps: + - name: Checkout the repository + uses: actions/checkout@v4 + - name: Check the links + uses: lycheeverse/lychee-action@v2 + with: + args: -v *.md + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml deleted file mode 100644 index b66d667..0000000 --- a/.github/workflows/rust.yml +++ /dev/null @@ -1,27 +0,0 @@ -name: Rust CI - -on: - push: - branches: [ "main", "master" ] - pull_request: - branches: [ "main", "master" ] - -env: - CARGO_TERM_COLOR: always - PKG_CONFIG_PATH: /usr/lib/pkgconfig - -jobs: - build: - strategy: - matrix: - os: [ ubuntu-latest, macos-latest, windows-latest ] - arch: [x64, arm64] - - runs-on: ${{ matrix.os }} - - steps: - - uses: actions/checkout@v4 - - name: Build - run: cargo build --verbose - - name: Run tests - run: cargo test --verbose -- --test-threads=2 \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 009cbf4..1a99b4d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -220,6 +220,27 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28a80e3145d8ad11ba0995949bbcf48b9df2be62772b3d351ef017dff6ecb853" +[[package]] +name = "geo-types" +version = "0.7.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62ddb1950450d67efee2bbc5e429c68d052a822de3aad010d28b351fbb705224" +dependencies = [ + "approx", + "num-traits", + "serde", +] + +[[package]] +name = "geohash" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fb94b1a65401d6cbf22958a9040aa364812c26674f841bee538b12c135db1e6" +dependencies = [ + "geo-types", + "libm", +] + [[package]] name = "getrandom" version = "0.2.16" @@ -333,6 +354,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", + "libm", ] [[package]] @@ -386,10 +408,11 @@ dependencies = [ [[package]] name = "qbin" -version = "0.1.0" +version = "0.1.1" dependencies = [ "approx", "criterion", + "geohash", "h3o", ] diff --git a/Cargo.toml b/Cargo.toml index c7068e9..4676fdf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "qbin" -version = "0.1.0" +version = "0.1.1" authors = ["Anatoly Tsyplenkov "] edition = "2024" license = "MIT" @@ -17,6 +17,7 @@ categories = ["data-structures", "science::geo"] [dev-dependencies] approx = "0.5.1" criterion = "0.5.1" +geohash = "0.13.1" h3o = "0.8.0" [[bench]] diff --git a/README.md b/README.md index 02d297e..1e5ac43 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,25 @@ # Quadbin -[![License](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/atsyplenkov/qbin/blob/main/LICENSE) -[![crates.io](https://img.shields.io/crates/v/qbin.svg?logo=rust)](https://crates.io/crates/qbin) -[![Docs.rs](https://img.shields.io/docsrs/qbin?label=docs.rs&logo=rust)](https://docs.rs/qbin) -[![Build & Test](https://github.com/atsyplenkov/qbin/actions/workflows/rust.yml/badge.svg)](https://github.com/atsyplenkov/qbin/actions/workflows/rust.yml) -[![codecov](https://codecov.io/gh/atsyplenkov/qbin/graph/badge.svg?token=4SZ4RI3ILS)](https://codecov.io/gh/atsyplenkov/qbin) +

+ + + + + + +
+ + + + +
+

+ +

+ Documentation | + Website +

+ A Rust implementation of Quadbin, a hierarchical geospatial index tiling approach developed by [CARTO](https://github.com/CartoDB). Like the [Microsoft's Bing Maps Tile System](https://docs.microsoft.com/en-us/bingmaps/articles/bing-maps-tile-system) (aka Quadkey), Quadbin uniformly subdivides a map in Mercator projection into four squares at different resolution levels, from 0 to 26 (less than 1 mยฒ at the equator). However, unlike Quadkey, Quadbin stores the grid cell index in a 64-bit integer. diff --git a/benches/encode_point.rs b/benches/encode_point.rs new file mode 100644 index 0000000..75f04b1 --- /dev/null +++ b/benches/encode_point.rs @@ -0,0 +1,28 @@ +use criterion::{Criterion, black_box}; +use h3o::{LatLng, Resolution}; +use qbin::Cell; + +const LAT: f64 = -41.28303675124842; +const LNG: f64 = 174.77727344223067; + +pub fn bench(c: &mut Criterion) { + let mut group = c.benchmark_group("encodePoint"); + + group.bench_function("geohash", |b| { + let coord = geohash::Coord { x: LNG, y: LAT }; + let index = geohash::encode(coord, 12).expect("Invalid coordinate"); + b.iter(|| black_box(&index)) + }); + group.bench_function("qbin", |b| { + let index = Cell::from_point(LAT, LNG, 12); + b.iter(|| black_box(&index)) + }); + + group.bench_function("h3o", |b| { + let latlng = LatLng::new(LAT, LNG).expect("Invalid coordinate"); + let index = latlng.to_cell(Resolution::Twelve); + b.iter(|| black_box(&index)) + }); + + group.finish(); +} diff --git a/benches/main.rs b/benches/main.rs index 78f9096..896e2e7 100644 --- a/benches/main.rs +++ b/benches/main.rs @@ -1,8 +1,14 @@ use criterion::{criterion_group, criterion_main}; +mod encode_point; mod get_cell_area; mod get_resolution; -criterion_group!(benches, get_resolution::bench, get_cell_area::bench); +criterion_group!( + benches, + get_resolution::bench, + get_cell_area::bench, + encode_point::bench +); criterion_main!(benches); diff --git a/cliff.toml b/cliff.toml new file mode 100644 index 0000000..74e75df --- /dev/null +++ b/cliff.toml @@ -0,0 +1,84 @@ +# git-cliff ~ default configuration file +# https://git-cliff.org/docs/configuration +# +# Lines starting with "#" are comments. +# Configuration options are organized into tables and keys. +# See documentation for more information on available options. + +[changelog] +# template for the changelog header +header = """ +# Changelog\n +All notable changes to this project will be documented in this file.\n +""" +# template for the changelog body +# https://keats.github.io/tera/docs/#introduction +body = """ +{% if version %}\ + ## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }} +{% else %}\ + ## [unreleased] +{% endif %}\ +{% for group, commits in commits | group_by(attribute="group") %} + ### {{ group | striptags | trim | upper_first }} + {% for commit in commits %} + - {% if commit.scope %}*({{ commit.scope }})* {% endif %}\ + {% if commit.breaking %}[**breaking**] {% endif %}\ + {{ commit.message | upper_first }}\ + {% endfor %} +{% endfor %}\n +""" +# template for the changelog footer +footer = """ + +""" +# remove the leading and trailing s +trim = true +# postprocessors +postprocessors = [ + # { pattern = '', replace = "https://github.com/orhun/git-cliff" }, # replace repository URL +] +# render body even when there are no releases to process +# render_always = true +# output file path +# output = "test.md" + +[git] +# parse the commits based on https://www.conventionalcommits.org +conventional_commits = true +# filter out the commits that are not conventional +filter_unconventional = true +# process each line of a commit as an individual commit +split_commits = false +# regex for preprocessing the commit messages +commit_preprocessors = [ + # Replace issue numbers + #{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](/issues/${2}))"}, + # Check spelling of the commit with https://github.com/crate-ci/typos + # If the spelling is incorrect, it will be automatically fixed. + #{ pattern = '.*', replace_command = 'typos --write-changes -' }, +] +# regex for parsing and grouping commits +commit_parsers = [ + { message = "^feat", group = "๐Ÿš€ Features" }, + { message = "^fix", group = "๐Ÿ› Bug Fixes" }, + { message = "^doc", group = "๐Ÿ“š Documentation", skip = true}, + { message = "^perf", group = "โšก Performance" }, + { message = "^refactor", group = "๐Ÿšœ Refactor", skip = true}, + { message = "^style", group = "๐ŸŽจ Styling" }, + { message = "^test", group = "๐Ÿงช Testing", skip = true}, + { message = "^chore\\(release\\): prepare for", skip = true }, + { message = "^chore\\(deps.*\\)", skip = true }, + { message = "^chore\\(pr\\)", skip = true }, + { message = "^chore\\(pull\\)", skip = true }, + { message = "^chore|^ci", group = "โš™๏ธ Miscellaneous Tasks" , skip = true}, + { body = ".*security", group = "๐Ÿ›ก๏ธ Security" }, + { message = "^revert", group = "โ—€๏ธ Revert" }, + { message = ".*", group = "๐Ÿ’ผ Other" }, +] +# filter out the commits that are not matched by commit parsers +filter_commits = true +# sort the tags topologically +topo_order = false +# sort the commits inside sections by oldest/newest order +sort_commits = "oldest" diff --git a/src/cells.rs b/src/cells.rs index 9735492..f9c8e8c 100644 --- a/src/cells.rs +++ b/src/cells.rs @@ -1,6 +1,228 @@ +use crate::Direction; use crate::constants::*; -use crate::types::*; +use crate::tiles::*; use crate::utils::*; +use core::{fmt, num::NonZeroU64}; + +/// Represents a cell in the Quadbin grid system at a +/// particular resolution. +/// +/// The index is encoded on 64-bit with the following bit layout: +/// +/// ```text +/// โ”โ”โ”ณโ”โ”โ”โ”ณโ”โ”โ”โ”โ”ณโ”โ”โ”โ”โ”โ”โ”โ”ณโ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”ˆโ”ˆโ”ˆโ”ˆโ”ˆโ”ˆโ”ˆโ”ˆโ”โ”โ”โ”โ”โ”โ”โ”โ”“ +/// โ”ƒUโ”ƒ H โ”ƒ M โ”ƒ R โ”ƒ XY in Morton order โ”ƒ +/// โ”—โ”โ”ปโ”โ”โ”โ”ปโ”โ”โ”โ”โ”ปโ”โ”โ”โ”โ”โ”โ”โ”ปโ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”ˆโ”ˆโ”ˆโ”ˆโ”ˆโ”ˆโ”ˆโ”ˆโ”โ”โ”โ”โ”โ”โ”โ”โ”› +/// 63 62 59 56 52 0 +/// ``` +/// +/// Where: +/// - `U`: Unused reserved bit (bit 63), always set to `0`; +/// - `H`: Header bit (bit 62), always set to `1`; +/// - `M`: Index mode, fixed to `1`, encoded over 4 bits (bits 59โ€“62); +/// - `R`: Cell resolution, ranging from `0` to `26`, encoded in bits 52โ€“56; +/// - Remaining bits (0โ€“51) encode the cellโ€™s XY position in Morton order (Z-order curve). +/// +#[derive(Debug, PartialEq, Eq, Copy, Clone)] +pub struct Cell(NonZeroU64); + +impl Cell { + /// Returns the inner u64 value of the cell. + pub fn get(&self) -> u64 { + self.0.get() + } + + /// Create new Quadbin cell from index. + /// + /// # Example + /// ``` + /// let qb_cell = qbin::Cell::new(5234261499580514303); + /// ``` + pub fn new(value: u64) -> Cell { + assert!( + is_valid_cell(value), + "Provided Quadbin Cell index is invalid" + ); + Cell(NonZeroU64::new(value).expect("non-zero cell index")) + } + + /// Quadbin cell index validation. + /// + /// # Example + /// ``` + /// let qb_cell = qbin::Cell::new(5234261499580514303); + /// assert_eq!(qb_cell.is_valid(), true) + /// ``` + pub fn is_valid(&self) -> bool { + is_valid_cell(self.get()) + } + + /// Returns the resolution of the cell index. + /// + /// # Example + /// ``` + /// let qb_cell = qbin::Cell::new(5234261499580514303); + /// let res = qb_cell.resolution(); + /// assert_eq!(res, 10) + /// ``` + pub fn resolution(&self) -> u8 { + ((self.0.get() >> 52) & 0x1F) as u8 + } + + /// Compute the parent cell for a specific resolution. + /// + /// # Example + /// ``` + /// let qb_cell = qbin::Cell::new(5209574053332910079); + /// let parent = qb_cell.parent(2_u8); + /// assert_eq!(parent, qbin::Cell::new(5200813144682790911)) + /// ``` + pub fn parent(&self, parent_res: u8) -> Cell { + cell_to_parent(self, parent_res) + } + + // TODO: + // Add child and/or children + + /// Find the Cell's neighbor in a specific [Direction]. + /// + /// In the original JavaScript implementation, this operation is called + /// sibling. However, following the H3 naming convention, we decided + /// to name sibling's as neighbors. + /// + /// See [Direction] for allowed arguments. + /// + /// # Example + /// ``` + /// use qbin::{Cell, Direction}; + /// + /// let sibling = Cell::new(5209574053332910079).neighbor(Direction::Right); + /// assert_eq!(sibling, Some(Cell::new(5209626829891043327))); + /// ``` + pub fn neighbor(&self, direction: Direction) -> Option { + let tile = self.to_tile().neighbor(direction); + tile.map(Tile::to_cell) + } + + /// Find the Cell's sibling in a specific [Direction]. + /// + /// See [Cell::neighbor]. + pub fn sibling(&self, direction: Direction) -> Option { + let tile = self.to_tile().neighbor(direction); + tile.map(Tile::to_cell) + } + + /// List all Cell's neighbors. + pub fn neighbors(&self) -> [Option; 4] { + let mut neighbors = [None; 4]; + + for (i, neighbor) in neighbors.iter_mut().enumerate() { + *neighbor = self.neighbor(Direction::new_unchecked(i as u8)); + } + + neighbors + } + + // TODO: + // Add `direction_to_neighbor` -- return Direction to neighbor + + /// Computes the area of this Quadbin cell, in mยฒ. + /// + /// See also [Cell::area_km2]. + /// + /// # Example + /// ``` + /// use approx::assert_relative_eq; + /// + /// let area = qbin::Cell::new(5234261499580514303_u64).area_m2(); + /// assert_relative_eq!(area, 888546364.7859862, epsilon = 1e-6) + /// + /// ``` + pub fn area_m2(&self) -> f64 { + self.to_tile().area() + } + + /// Computes the area of this Quadbin cell, in kmยฒ. + /// + /// See also [Cell::area_m2]. + /// + /// # Example + /// ``` + /// use approx::assert_relative_eq; + /// + /// let area = qbin::Cell::new(5234261499580514303_u64).area_km2(); + /// assert_relative_eq!(area, 888.5463647859862, epsilon = 1e-6) + /// + /// ``` + pub fn area_km2(&self) -> f64 { + self.area_m2() / 1_000_000_f64 + } + + /// Convert a Quadbin cell into geographic point. + /// + /// Returns a tuple with latitude and longitude in degrees. + /// + /// # Example + /// ``` + /// use qbin::Cell; + /// + /// let coords = Cell::new(5209574053332910079_u64).to_point(); + /// assert_eq!(coords, [-11.178401873711776, 33.75]); + /// ``` + /// + pub fn to_point(&self) -> [f64; 2] { + cell_to_point(self) + } + + /// Convert a Quadbin cell into a bounding box. + /// + /// Returns an array with [xmin, ymin, xmax, ymax] + /// in degrees. + /// + /// # Example + /// ``` + /// let bbox = qbin::Cell::new(5209574053332910079).to_bbox(); + /// assert_eq!( bbox, [22.5, -21.943045533438166, 45.0, 0.0]) + /// ``` + pub fn to_bbox(&self) -> [f64; 4] { + let tile = self.to_tile(); + + let xmin = tile.to_longitude(0.0); + let xmax = tile.to_longitude(1.0); + let ymin = tile.to_latitude(1.0); + let ymax = tile.to_latitude(0.0); + + [xmin, ymin, xmax, ymax] + } + + /// Convert a geographic point into a Quadbin cell. + /// + /// # Example + /// + /// ``` + /// let cell = qbin::Cell::from_point(-41.28303675124842, 174.77727344223067, 26); + /// assert_eq!(cell.get(), 5309133744805926483_u64) + /// ``` + pub fn from_point(lat: f64, lng: f64, res: u8) -> Cell { + point_to_cell(lat, lng, res) + } + + /// Convert a Quadbin cell into a tile. + pub(crate) fn to_tile(self) -> Tile { + cell_to_tile(&self) + } +} + +impl fmt::Display for Cell { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.get()) + } +} + +// TODO: +// Detect direction from neighbor https://github.com/HydroniumLabs/h3o/blob/ad2bebf52eab218d66b0bf213b14a2802bf616f7/src/base_cell.rs#L135C1-L150C6 + +// Internal functions ------------------------------------------------ /// Quadbin cell validation pub(crate) fn is_valid_cell(cell64: u64) -> bool { @@ -18,8 +240,6 @@ pub(crate) fn is_valid_cell(cell64: u64) -> bool { (cell64 & header == header) && mode == 1 && resolution <= 26 && (cell64 & unused == unused) } -// Internal functions ------------------------------------------------ - /// Convert a tile into a Quadbin cell. pub(crate) fn tile_to_cell(tile: Tile) -> Cell { let mut x = tile.x as u64; diff --git a/src/direction.rs b/src/directions.rs similarity index 97% rename from src/direction.rs rename to src/directions.rs index d0362ff..023f12c 100644 --- a/src/direction.rs +++ b/src/directions.rs @@ -1,4 +1,4 @@ -use crate::error; +use crate::errors; use core::fmt; /// Maximum value for a direction. @@ -62,7 +62,7 @@ impl Direction { } impl TryFrom for Direction { - type Error = error::InvalidDirection; + type Error = errors::InvalidDirection; fn try_from(value: u8) -> Result { match value { diff --git a/src/error.rs b/src/errors.rs similarity index 100% rename from src/error.rs rename to src/errors.rs diff --git a/src/lib.rs b/src/lib.rs index 0fe5dd4..0af4a97 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,15 +1,20 @@ #![doc = include_str!("../README.md")] +// Quadbin cell itself pub mod cells; -mod constants; -pub mod error; -pub mod utils; +pub use crate::cells::Cell; + +// Direction struct +mod directions; +pub use crate::directions::Direction; -mod types; -pub use crate::types::Cell; +// Errors +pub mod errors; -mod direction; -pub use crate::direction::Direction; +// Internal stuff +mod constants; +mod tiles; +mod utils; #[cfg(test)] mod test; diff --git a/src/test/cells.rs b/src/test/cells.rs index cab56db..9b09b14 100644 --- a/src/test/cells.rs +++ b/src/test/cells.rs @@ -1,5 +1,6 @@ -use crate::direction::Direction; -use crate::types::*; +use crate::cells::*; +use crate::directions::Direction; +use crate::tiles::*; use approx::assert_relative_eq; // Constants to save some typing @@ -98,8 +99,8 @@ fn test_cell_to_bbox() { for i in cases.iter() { let bbox = Cell::new(*i).to_bbox(); - assert_eq!(bbox[0] < bbox[2], true); - assert_eq!(bbox[1] < bbox[3], true); + assert!(bbox[0] < bbox[2]); + assert!(bbox[1] < bbox[3]); } } @@ -146,7 +147,7 @@ fn test_cell_to_parent_invalid_resolution() { #[test] fn test_cell_area() { let area = Cell::new(5209574053332910079_u64).area_m2(); - assert_relative_eq!(area, 6023040823252.6641, epsilon = 1e-2); + assert_relative_eq!(area, 6023040823252.664, epsilon = 1e-2); } // Find cell's neighbors diff --git a/src/test/direction.rs b/src/test/directions.rs similarity index 88% rename from src/test/direction.rs rename to src/test/directions.rs index 5dd6a31..a044a50 100644 --- a/src/test/direction.rs +++ b/src/test/directions.rs @@ -1,12 +1,12 @@ -use crate::direction::*; -use crate::error::*; +use crate::directions::*; +use crate::errors::*; #[test] fn test_valid_direction() { let dirs = [0, 1, 2, 3]; for val in dirs.iter() { - assert_eq!(u8::from(Direction::new_unchecked(*val)), *val as u8); + assert_eq!(u8::from(Direction::new_unchecked(*val)), { *val }); assert_eq!(u64::from(Direction::new_unchecked(*val)), *val as u64); assert_eq!(usize::from(Direction::new_unchecked(*val)), *val as usize); } diff --git a/src/test/hashing.rs b/src/test/hashing.rs index 45f7315..7db0050 100644 --- a/src/test/hashing.rs +++ b/src/test/hashing.rs @@ -1,4 +1,4 @@ -use crate::types::Tile; +use crate::tiles::Tile; use crate::utils::point_cover; #[test] diff --git a/src/test/mod.rs b/src/test/mod.rs index 002bf6c..191cc0d 100644 --- a/src/test/mod.rs +++ b/src/test/mod.rs @@ -1,4 +1,4 @@ mod cells; -mod direction; +mod directions; mod hashing; -mod utils; +mod tiles; diff --git a/src/test/utils.rs b/src/test/tiles.rs similarity index 98% rename from src/test/utils.rs rename to src/test/tiles.rs index 98519e9..ab8360a 100644 --- a/src/test/utils.rs +++ b/src/test/tiles.rs @@ -1,5 +1,5 @@ -use crate::direction::Direction; -use crate::types::Tile; +use crate::directions::Direction; +use crate::tiles::Tile; use crate::utils::{point_to_tile_fraction, tile_scalefactor}; use approx::assert_relative_eq; diff --git a/src/tiles.rs b/src/tiles.rs new file mode 100644 index 0000000..2974109 --- /dev/null +++ b/src/tiles.rs @@ -0,0 +1,68 @@ +use crate::Direction; +use crate::cells::*; +use crate::utils::*; + +/// A single tile coordinates +/// +/// _Internal struct_ +#[derive(Debug, PartialEq, Eq, Copy, Clone)] +pub(crate) struct Tile { + pub x: u32, + pub y: u32, + pub z: u8, +} + +impl Tile { + /// Create a new tile. + pub fn new(x: u32, y: u32, z: u8) -> Tile { + Tile { x, y, z } + } + + /// Convert to Quadbin cell. + pub fn to_cell(self) -> Cell { + tile_to_cell(self) + } + + /// Compute the tile for a longitude and latitude in a specific resolution. + pub fn from_point(lat: f64, lng: f64, res: u8) -> Self { + point_to_tile(lat, lng, res) + } + + /// Approximate tile area in square meters. + pub fn area(&self) -> f64 { + tile_area(self) + } + + /// Return tile's latitude. + /// + /// See also [Tile::to_longitude]. + /// + pub fn to_latitude(self, offset: f64) -> f64 { + tile_to_latitude(&self, offset) + } + + /// Return tile's longitude. + /// + /// See also [Tile::to_latitude]. + /// + pub fn to_longitude(self, offset: f64) -> f64 { + tile_to_longitude(&self, offset) + } + + /// Get tile's siblings. + pub fn neighbor(&self, direction: Direction) -> Option { + tile_neighbor(self, direction) + } + + /// Compute a hash from the tile. + #[allow(dead_code)] + pub fn to_hash(self) -> u64 { + to_tile_hash(&self) + } + + /// Compute a tile from the hash. + #[allow(dead_code)] + pub fn from_hash(tile_hash: u64) -> Tile { + from_tile_hash(tile_hash) + } +} diff --git a/src/types.rs b/src/types.rs deleted file mode 100644 index 2236a9d..0000000 --- a/src/types.rs +++ /dev/null @@ -1,289 +0,0 @@ -use crate::Direction; -use crate::cells::*; -use crate::utils::*; -use core::{fmt, num::NonZeroU64}; - -/// Represents a cell in the Quadbin grid system at a -/// particular resolution. -/// -/// The index is encoded on 64-bit with the following bit layout: -/// -/// ```text -/// โ”โ”โ”ณโ”โ”โ”โ”ณโ”โ”โ”โ”โ”ณโ”โ”โ”โ”โ”โ”โ”โ”ณโ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”ˆโ”ˆโ”ˆโ”ˆโ”ˆโ”ˆโ”ˆโ”ˆโ”โ”โ”โ”โ”โ”โ”โ”โ”“ -/// โ”ƒUโ”ƒ H โ”ƒ M โ”ƒ R โ”ƒ XY in Morton order โ”ƒ -/// โ”—โ”โ”ปโ”โ”โ”โ”ปโ”โ”โ”โ”โ”ปโ”โ”โ”โ”โ”โ”โ”โ”ปโ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”ˆโ”ˆโ”ˆโ”ˆโ”ˆโ”ˆโ”ˆโ”ˆโ”โ”โ”โ”โ”โ”โ”โ”โ”› -/// 63 62 59 56 52 0 -/// ``` -/// -/// Where: -/// - `U`: Unused reserved bit (bit 63), always set to `0`; -/// - `H`: Header bit (bit 62), always set to `1`; -/// - `M`: Index mode, fixed to `1`, encoded over 4 bits (bits 59โ€“62); -/// - `R`: Cell resolution, ranging from `0` to `26`, encoded in bits 52โ€“56; -/// - Remaining bits (0โ€“51) encode the cellโ€™s XY position in Morton order (Z-order curve). -/// -#[derive(Debug, PartialEq, Eq, Copy, Clone)] -pub struct Cell(NonZeroU64); - -impl Cell { - /// Returns the inner u64 value of the cell. - pub fn get(&self) -> u64 { - self.0.get() - } - - /// Create new Quadbin cell from index. - /// - /// # Example - /// ``` - /// let qb_cell = qbin::Cell::new(5234261499580514303); - /// ``` - pub fn new(value: u64) -> Cell { - assert!( - is_valid_cell(value), - "Provided Quadbin Cell index is invalid" - ); - Cell(NonZeroU64::new(value).expect("non-zero cell index")) - } - - /// Quadbin cell index validation. - /// - /// # Example - /// ``` - /// let qb_cell = qbin::Cell::new(5234261499580514303); - /// assert_eq!(qb_cell.is_valid(), true) - /// ``` - pub fn is_valid(&self) -> bool { - is_valid_cell(self.get()) - } - - /// Returns the resolution of the cell index. - /// - /// # Example - /// ``` - /// let qb_cell = qbin::Cell::new(5234261499580514303); - /// let res = qb_cell.resolution(); - /// assert_eq!(res, 10) - /// ``` - pub fn resolution(&self) -> u8 { - ((self.0.get() >> 52) & 0x1F) as u8 - } - - /// Compute the parent cell for a specific resolution. - /// - /// # Example - /// ``` - /// let qb_cell = qbin::Cell::new(5209574053332910079); - /// let parent = qb_cell.parent(2_u8); - /// assert_eq!(parent, qbin::Cell::new(5200813144682790911)) - /// ``` - pub fn parent(&self, parent_res: u8) -> Cell { - cell_to_parent(self, parent_res) - } - - // TODO: - // Add child and/or children - - /// Find the Cell's neighbor in a specific [Direction]. - /// - /// In the original JavaScript implementation, this operation is called - /// sibling. However, following the H3 naming convention, we decided - /// to name sibling's as neighbors. - /// - /// See [Direction] for allowed arguments. - /// - /// # Example - /// ``` - /// use qbin::{Cell, Direction}; - /// - /// let sibling = Cell::new(5209574053332910079).neighbor(Direction::Right); - /// assert_eq!(sibling, Some(Cell::new(5209626829891043327))); - /// ``` - pub fn neighbor(&self, direction: Direction) -> Option { - let tile = self.to_tile().neighbor(direction); - tile.map(Tile::to_cell) - } - - /// Find the Cell's sibling in a specific [Direction]. - /// - /// See [Cell::neighbor]. - pub fn sibling(&self, direction: Direction) -> Option { - let tile = self.to_tile().neighbor(direction); - tile.map(Tile::to_cell) - } - - /// List all Cell's neighbors. - pub fn neighbors(&self) -> [Option; 4] { - let mut neighbors = [None; 4]; - - for (i, neighbor) in neighbors.iter_mut().enumerate() { - *neighbor = self.neighbor(Direction::new_unchecked(i as u8)); - } - - neighbors - } - - // TODO: - // Add `direction_to_neighbor` -- return Direction to neighbor - - /// Computes the area of this Quadbin cell, in mยฒ. - /// - /// See also [Cell::area_km2]. - /// - /// # Example - /// ``` - /// use approx::assert_relative_eq; - /// - /// let area = qbin::Cell::new(5234261499580514303_u64).area_m2(); - /// assert_relative_eq!(area, 888546364.7859862, epsilon = 1e-6) - /// - /// ``` - pub fn area_m2(&self) -> f64 { - self.to_tile().area() - } - - /// Computes the area of this Quadbin cell, in kmยฒ. - /// - /// See also [Cell::area_m2]. - /// - /// # Example - /// ``` - /// use approx::assert_relative_eq; - /// - /// let area = qbin::Cell::new(5234261499580514303_u64).area_km2(); - /// assert_relative_eq!(area, 888.5463647859862, epsilon = 1e-6) - /// - /// ``` - pub fn area_km2(&self) -> f64 { - self.area_m2() / 1_000_000_f64 - } - - /// Convert a Quadbin cell into geographic point. - /// - /// Returns a tuple with latitude and longitude in degrees. - /// - /// # Example - /// ``` - /// use qbin::Cell; - /// - /// let coords = Cell::new(5209574053332910079_u64).to_point(); - /// assert_eq!(coords, [-11.178401873711776, 33.75]); - /// ``` - /// - pub fn to_point(&self) -> [f64; 2] { - cell_to_point(self) - } - - /// Convert a Quadbin cell into a bounding box. - /// - /// Returns an array with [xmin, ymin, xmax, ymax] - /// in degrees. - /// - /// # Example - /// ``` - /// let bbox = qbin::Cell::new(5209574053332910079).to_bbox(); - /// assert_eq!( bbox, [22.5, -21.943045533438166, 45.0, 0.0]) - /// ``` - pub fn to_bbox(&self) -> [f64; 4] { - let tile = self.to_tile(); - - let xmin = tile.to_longitude(0.0); - let xmax = tile.to_longitude(1.0); - let ymin = tile.to_latitude(1.0); - let ymax = tile.to_latitude(0.0); - - [xmin, ymin, xmax, ymax] - } - - /// Convert a geographic point into a Quadbin cell. - /// - /// # Example - /// - /// ``` - /// let cell = qbin::Cell::from_point(-41.28303675124842, 174.77727344223067, 26); - /// assert_eq!(cell.get(), 5309133744805926483_u64) - /// ``` - pub fn from_point(lat: f64, lng: f64, res: u8) -> Cell { - point_to_cell(lat, lng, res) - } - - /// Convert a Quadbin cell into a tile. - pub(crate) fn to_tile(self) -> Tile { - cell_to_tile(&self) - } -} - -impl fmt::Display for Cell { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.get()) - } -} - -// TODO: -// Detect direction from neighbor https://github.com/HydroniumLabs/h3o/blob/ad2bebf52eab218d66b0bf213b14a2802bf616f7/src/base_cell.rs#L135C1-L150C6 - -// -------------------------------------------------------- - -/// A single tile coordinates -/// -/// _Internal struct_ -#[derive(Debug, PartialEq, Eq, Copy, Clone)] -pub(crate) struct Tile { - pub x: u32, - pub y: u32, - pub z: u8, -} - -impl Tile { - /// Create a new tile. - pub fn new(x: u32, y: u32, z: u8) -> Tile { - Tile { x, y, z } - } - - /// Convert to Quadbin cell. - pub fn to_cell(self) -> Cell { - tile_to_cell(self) - } - - /// Compute the tile for a longitude and latitude in a specific resolution. - pub fn from_point(lat: f64, lng: f64, res: u8) -> Self { - point_to_tile(lat, lng, res) - } - - /// Approximate tile area in square meters. - pub fn area(&self) -> f64 { - tile_area(self) - } - - /// Return tile's latitude. - /// - /// See also [Tile::to_longitude]. - /// - pub fn to_latitude(self, offset: f64) -> f64 { - tile_to_latitude(&self, offset) - } - - /// Return tile's longitude. - /// - /// See also [Tile::to_latitude]. - /// - pub fn to_longitude(self, offset: f64) -> f64 { - tile_to_longitude(&self, offset) - } - - /// Get tile's siblings. - pub fn neighbor(&self, direction: Direction) -> Option { - tile_neighbor(self, direction) - } - - /// Compute a hash from the tile. - #[allow(dead_code)] - pub fn to_hash(self) -> u64 { - to_tile_hash(&self) - } - - /// Compute a tile from the hash. - #[allow(dead_code)] - pub fn from_hash(tile_hash: u64) -> Tile { - from_tile_hash(tile_hash) - } -} diff --git a/src/utils.rs b/src/utils.rs index 33a2650..31d4cc1 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,6 +1,6 @@ use crate::constants::*; -use crate::direction::Direction; -use crate::types::Tile; +use crate::directions::Direction; +use crate::tiles::Tile; use std::f64::consts::PI; /// Clip a value between a minimum and maximum value