├── .gitattributes ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .readthedocs.yaml ├── CONTRIBUTING.rst ├── Cargo.toml ├── README.rst ├── crates └── h3arrow │ ├── CHANGES.md │ ├── Cargo.toml │ ├── LICENSE-MIT │ ├── README.md │ └── src │ ├── algorithm │ ├── bounding_rect.rs │ ├── centroid.rs │ ├── change_resolution.rs │ ├── compact.rs │ ├── concave_hull.rs │ ├── convex_hull.rs │ ├── coordinates.rs │ ├── grid.rs │ ├── localij.rs │ ├── mod.rs │ └── string.rs │ ├── array │ ├── cell.rs │ ├── directededge.rs │ ├── from_geo.rs │ ├── from_geoarrow.rs │ ├── list.rs │ ├── mod.rs │ ├── resolution.rs │ ├── to_geo.rs │ ├── to_geoarrow.rs │ ├── validity.rs │ └── vertex.rs │ ├── error.rs │ ├── export.rs │ ├── lib.rs │ └── spatial_index.rs └── h3ronpy ├── CHANGES.rst ├── Cargo.toml ├── LICENSE.txt ├── MANIFEST.in ├── data ├── europe-and-north-africa.tif ├── naturalearth_110m_admin_0_countries.fgb ├── population-841fa8bffffffff.parquet └── r.tiff ├── docs ├── Makefile ├── make.bat ├── requirements.txt └── source │ ├── api │ ├── core.rst │ ├── index.rst │ ├── pandas.rst │ └── polars.rst │ ├── changelog.rst │ ├── conf.py │ ├── contributing.rst │ ├── index.rst │ ├── installation.rst │ ├── license.rst │ └── usage │ ├── grid.rst │ ├── index.rst │ ├── raster.rst │ └── vector.rst ├── install-dev-dependencies.py ├── justfile ├── pyproject.toml ├── python └── h3ronpy │ ├── __init__.py │ ├── pandas │ ├── __init__.py │ ├── raster.py │ └── vector.py │ ├── polars.py │ ├── raster.py │ └── vector.py ├── src ├── array.rs ├── arrow_interop.rs ├── error.rs ├── lib.rs ├── op │ ├── compact.rs │ ├── localij.rs │ ├── measure.rs │ ├── mod.rs │ ├── neighbor.rs │ ├── resolution.rs │ ├── string.rs │ └── valid.rs ├── raster.rs ├── resolution.rs ├── transform.rs └── vector.rs └── tests ├── __init__.py ├── arrow ├── __init__.py ├── test_benches.py ├── test_compact.py ├── test_coordinates.py ├── test_localij.py ├── test_measure.py ├── test_neighbor.py ├── test_raster.py ├── test_resolution.py ├── test_utf8.py └── test_vector.py ├── pandas ├── __init__.py └── test_vector.py ├── polars ├── __init__.py ├── test_expr.py └── test_series.py └── test_transform.py /.gitattributes: -------------------------------------------------------------------------------- 1 | # hints for github linguist 2 | *.ipynb linguist-documentation -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | tags: 8 | - 'v*' 9 | # release: 10 | # types: [ created ] 11 | pull_request: 12 | branches: 13 | - main 14 | workflow_dispatch: 15 | 16 | permissions: 17 | contents: read 18 | 19 | env: 20 | PYTHON_VERSION: "3.9" 21 | RUST_BACKTRACE: "1" 22 | 23 | 24 | jobs: 25 | linux-x86_64: 26 | runs-on: ubuntu-latest 27 | needs: 28 | - rusttest 29 | - black 30 | steps: 31 | - uses: actions/checkout@v4 32 | - uses: actions/setup-python@v5 33 | with: 34 | python-version: ${{env.PYTHON_VERSION}} 35 | - name: Install dependencies 36 | run: python h3ronpy/install-dev-dependencies.py 37 | - name: Build wheels 38 | uses: PyO3/maturin-action@v1 39 | env: 40 | RUSTFLAGS: "-C target-feature=+fxsr,+sse,+sse2,+sse3,+ssse3,+sse4.1,+sse4.2,+popcnt,+avx,+fma" 41 | with: 42 | target: x86_64 43 | args: > 44 | --release 45 | --manifest-path h3ronpy/Cargo.toml 46 | --out dist 47 | manylinux: auto 48 | - name: Upload wheels 49 | uses: actions/upload-artifact@v4 50 | with: 51 | name: wheels-linux-x86_64 52 | path: dist 53 | - name: pytest 54 | shell: bash 55 | run: | 56 | set -e 57 | pip install --force-reinstall dist/*.whl 58 | python -m pytest -s h3ronpy/tests 59 | 60 | linux-aarch64: 61 | runs-on: ubuntu-latest 62 | needs: 63 | - rusttest 64 | - black 65 | - linux-x86_64 66 | steps: 67 | - uses: actions/checkout@v4 68 | - uses: actions/setup-python@v5 69 | with: 70 | python-version: ${{env.PYTHON_VERSION}} 71 | - name: Install dependencies 72 | run: python h3ronpy/install-dev-dependencies.py 73 | - name: Build wheels 74 | uses: PyO3/maturin-action@v1 75 | with: 76 | target: aarch64-unknown-linux-gnu 77 | args: > 78 | --release 79 | --manifest-path h3ronpy/Cargo.toml 80 | --out dist 81 | manylinux: auto 82 | - name: Upload wheels 83 | uses: actions/upload-artifact@v4 84 | with: 85 | name: wheels-linux-aarch64 86 | path: dist 87 | 88 | 89 | windows-x86_64: 90 | needs: 91 | - rusttest 92 | - black 93 | - linux-x86_64 94 | runs-on: windows-latest 95 | steps: 96 | - uses: actions/checkout@v4 97 | - uses: actions/setup-python@v5 98 | with: 99 | python-version: ${{ env.PYTHON_VERSION }} 100 | - name: Install dependencies 101 | run: python h3ronpy/install-dev-dependencies.py 102 | - name: Build wheels 103 | uses: PyO3/maturin-action@v1 104 | env: 105 | RUSTFLAGS: "-C target-feature=+fxsr,+sse,+sse2,+sse3,+sse4.1,+sse4.2" 106 | with: 107 | target: x86_64 108 | args: > 109 | --release 110 | --manifest-path h3ronpy/Cargo.toml 111 | --out dist 112 | -i python 113 | - name: Upload wheels 114 | uses: actions/upload-artifact@v4 115 | with: 116 | name: wheels-windows-x86_64 117 | path: dist 118 | - name: pytest 119 | shell: bash 120 | run: | 121 | set -e 122 | pip install --force-reinstall dist/*.whl 123 | python -m pytest h3ronpy/tests 124 | 125 | macos-x86_64: 126 | needs: 127 | - rusttest 128 | - black 129 | - linux-x86_64 130 | runs-on: macos-latest 131 | steps: 132 | - uses: actions/checkout@v4 133 | - uses: actions/setup-python@v5 134 | with: 135 | # https://github.com/pypa/cibuildwheel/issues/1410 136 | python-version: ${{ env.PYTHON_VERSION }} 137 | - name: Install dependencies 138 | shell: bash 139 | run: | 140 | python h3ronpy/install-dev-dependencies.py 141 | pip install --upgrade pip 142 | - name: Build wheels 143 | uses: PyO3/maturin-action@v1 144 | env: 145 | RUSTFLAGS: "-C target-feature=+sse3,+ssse3,+sse4.1,+sse4.2,+popcnt,+avx,+fma" 146 | # lower versions result in "illegal instruction" 147 | MACOSX_DEPLOYMENT_TARGET: "10.14" 148 | with: 149 | target: x86_64 150 | args: > 151 | --release 152 | --manifest-path h3ronpy/Cargo.toml 153 | --out dist 154 | -i python 155 | - name: Upload wheels 156 | uses: actions/upload-artifact@v4 157 | with: 158 | name: wheels-macos-x86_64 159 | path: dist 160 | #- name: pytest 161 | # shell: bash 162 | # run: | 163 | # set -e 164 | # pip install --force-reinstall --verbose dist/*.whl 165 | # python -m pytest h3ronpy/tests 166 | 167 | macos-aarch64: 168 | runs-on: macos-latest 169 | needs: 170 | - rusttest 171 | - black 172 | - linux-x86_64 173 | steps: 174 | - uses: actions/checkout@v4 175 | - name: Set up Rust targets 176 | run: rustup target add aarch64-apple-darwin 177 | - uses: actions/setup-python@v5 178 | with: 179 | python-version: ${{env.PYTHON_VERSION}} 180 | - name: Install dependencies 181 | run: | 182 | python h3ronpy/install-dev-dependencies.py 183 | - name: Build wheels 184 | uses: PyO3/maturin-action@v1 185 | with: 186 | target: aarch64-apple-darwin 187 | args: > 188 | --release 189 | --manifest-path h3ronpy/Cargo.toml 190 | --out dist 191 | - name: Upload wheels 192 | uses: actions/upload-artifact@v4 193 | with: 194 | name: wheels-macos-aarch64 195 | path: dist 196 | 197 | # sdist: 198 | # runs-on: ubuntu-latest 199 | # if: "startsWith(github.ref, 'refs/tags/v')" 200 | # needs: 201 | # - rusttest 202 | # - black 203 | # steps: 204 | # - uses: actions/checkout@v4 205 | # - name: Build sdist 206 | # uses: PyO3/maturin-action@v1 207 | # with: 208 | # command: sdist 209 | # args: > 210 | # --manifest-path h3ronpy/Cargo.toml 211 | # --out dist 212 | # - name: Upload sdist 213 | # uses: actions/upload-artifact@v4 214 | # with: 215 | # name: sdist 216 | # path: dist 217 | 218 | release: 219 | name: Release 220 | runs-on: ubuntu-latest 221 | if: "startsWith(github.ref, 'refs/tags/v')" 222 | needs: [ linux-x86_64, linux-aarch64, windows-x86_64, macos-x86_64, macos-aarch64 ] 223 | steps: 224 | - uses: actions/download-artifact@v4 225 | with: 226 | name: wheels-linux-x86_64 227 | - uses: actions/download-artifact@v4 228 | with: 229 | name: wheels-linux-aarch64 230 | - uses: actions/download-artifact@v4 231 | with: 232 | name: wheels-windows-x86_64 233 | - uses: actions/download-artifact@v4 234 | with: 235 | name: wheels-macos-aarch64 236 | - uses: actions/download-artifact@v4 237 | with: 238 | name: wheels-macos-x86_64 239 | #- uses: actions/download-artifact@v4 240 | # with: 241 | # name: sdist 242 | - name: Publish to PyPI 243 | uses: PyO3/maturin-action@v1 244 | env: 245 | MATURIN_PYPI_TOKEN: ${{ secrets.PYPI }} 246 | with: 247 | command: upload 248 | args: --skip-existing * 249 | 250 | rusttest: 251 | name: rust test 252 | runs-on: ubuntu-latest 253 | steps: 254 | - name: checkout repo 255 | uses: actions/checkout@v4 256 | - name: run rustfmt 257 | run: | 258 | cargo fmt -- --check 259 | 260 | - name: Lint with clippy -- no features 261 | run: cargo clippy --workspace --exclude h3ronpy 262 | 263 | - name: Test with cargo -- no features 264 | run: cargo test --workspace --exclude h3ronpy 265 | 266 | - name: Lint with clippy -- all features 267 | run: cargo clippy --workspace --exclude h3ronpy --all-features 268 | 269 | - name: Test with cargo -- all features 270 | run: cargo test --workspace --all-features --exclude h3ronpy 271 | 272 | black: 273 | name: black 274 | runs-on: ubuntu-latest 275 | steps: 276 | - name: checkout repo 277 | uses: actions/checkout@v4 278 | 279 | - uses: actions/setup-python@v5 280 | with: 281 | python-version: ${{env.PYTHON_VERSION}} 282 | - name: Install black 283 | run: | 284 | pip install black 285 | 286 | - name: run black 287 | working-directory: h3ronpy 288 | run: | 289 | black --check -l 120 python tests *.py docs/source/*.py 290 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | Cargo.lock 2 | target 3 | .idea 4 | *.gpkg 5 | *.gpkg-wal 6 | *.gpkg-shm 7 | perf.data 8 | perf.data.old 9 | flamegraph*.svg 10 | *.so 11 | .ipynb_checkpoints 12 | .benchmarks 13 | **/venv 14 | __pycache__ 15 | .pytest_cache 16 | .ruff_cache 17 | /h3ronpy/docs/build/ 18 | /h3ronpy/docs/source/generated/ 19 | /h3ronpy/docs/source/_build/ 20 | /h3ronpy/dist 21 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | sphinx: 4 | builder: html 5 | configuration: h3ronpy/docs/source/conf.py 6 | 7 | build: 8 | os: "ubuntu-22.04" 9 | tools: 10 | python: "3.10" 11 | # rust: "latest" 12 | 13 | commands: 14 | # install rust manually. https://github.com/readthedocs/readthedocs.org/issues/11488 15 | - asdf install rust 1.82.0 16 | - asdf global rust 1.82.0 17 | 18 | # readthedocs commands from the last non-commands controlled build (based on the commented out 19 | # "python" section of this file) 20 | - python -mvirtualenv $READTHEDOCS_VIRTUALENV_PATH 21 | - python -m pip install --upgrade --no-cache-dir pip setuptools 22 | - python -m pip install --upgrade --no-cache-dir sphinx readthedocs-sphinx-ext 23 | - python -m pip install --exists-action=w --no-cache-dir -r h3ronpy/docs/requirements.txt 24 | - python -m pip install --upgrade --upgrade-strategy only-if-needed --no-cache-dir ./h3ronpy 25 | - cat h3ronpy/docs/source/conf.py 26 | - python -m sphinx -T -b html -d _build/doctrees -D language=en h3ronpy/docs/source $READTHEDOCS_OUTPUT/html 27 | 28 | #python: 29 | # install: 30 | # - requirements: h3ronpy/docs/requirements.txt 31 | # - method: pip 32 | # path: ./h3ronpy 33 | -------------------------------------------------------------------------------- /CONTRIBUTING.rst: -------------------------------------------------------------------------------- 1 | Contributing 2 | ============ 3 | 4 | Please take a moment to review this document to make the contribution 5 | process easy and effective for everyone involved. 6 | 7 | License 8 | ------- 9 | 10 | You agree to license your contribution under the `MIT 11 | License `__. 12 | 13 | Submission guidelines 14 | --------------------- 15 | 16 | 1. Format your contribution with 17 | `rustfmt `__. 18 | 2. Resolve all warnings from 19 | `clippy `__. 20 | 3. Document your contribution per the `Rust documentation 21 | guidelines `__. 22 | 23 | Provide tests that prove the functionality you’re contributing is 24 | correct. 25 | 26 | Ensure your contribution is free of all warnings and errors during 27 | compilation and testing. maintain quality. 28 | 29 | Pull requests 30 | ------------- 31 | 32 | This project appreciates pull requests that fix bugs, make improvements 33 | or add new features. These are one of the main points of collaboration 34 | for the project. 35 | 36 | Pull requests should remain focused in scope and not contain unrelated 37 | commits. 38 | 39 | The project may use automated tools to verify submitted pull requests. 40 | Ensure you address any errors these tools may report. 41 | 42 | The project may choose to **not** accept your contribution. When this 43 | happens we’ll explain why and where possible make suggestions for how 44 | you might be able to achieve what you set out to with your pull request. 45 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | resolver = "2" 3 | 4 | members = ["h3ronpy", "crates/h3arrow"] 5 | 6 | [workspace.dependencies] 7 | geo = "0.29" 8 | geo-types = "0.7" 9 | h3o = { version = "0.7" } 10 | rayon = "^1" 11 | arrow = { version = "53" } 12 | 13 | [profile.release] 14 | lto = "thin" 15 | strip = true 16 | debug = false 17 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | h3ronpy 2 | ======= 3 | 4 | A data science toolkit for the `H3 geospatial grid `_. 5 | 6 | .. image:: https://img.shields.io/pypi/v/h3ronpy 7 | :alt: PyPI 8 | :target: https://pypi.python.org/pypi/h3ronpy/ 9 | 10 | .. image:: https://readthedocs.org/projects/h3ronpy/badge/?version=latest 11 | :alt: ReadTheDocs 12 | :target: https://h3ronpy.readthedocs.io/ 13 | 14 | .. image:: https://img.shields.io/conda/vn/conda-forge/h3ronpy.svg 15 | :alt: conda-forge 16 | :target: https://prefix.dev/channels/conda-forge/packages/h3ronpy 17 | 18 | 19 | This library is not a substitute for the official `python h3 library `_ - instead it provides more 20 | high-level functions on top of H3 and integrations into common dataframe libraries. 21 | 22 | Documentation is available on ``_. 23 | 24 | Features 25 | -------- 26 | 27 | * H3 algorithms provided using the performant `h3o `_ library. 28 | * Build on `Apache Arrow `_ and the lightweight `arro3 `_ for efficient data handling. The arrow memory model is compatible with dataframe libraries like `pandas `_ and `polars `_. 29 | * Extensions for the polars `Series`` and `Expr` APIs. 30 | * Some dedicated functions to work with `geopandas `_ `GeoSeries`. 31 | * Multi-threaded conversion of raster data to the H3 grid using `numpy arrays `_. 32 | * Multi-threaded conversion of vector data, including `geopandas` `GeoDataFrames` and any object which supports the python `__geo_interface__` protocol (`shapely`, `geojson`, ...). 33 | 34 | 35 | Limitations 36 | ----------- 37 | 38 | Not all functionalities of the H3 grid are wrapped by this library, the current feature-set was implemented 39 | when there was a need and the time for it. As a opensource library new features can be requested in the form of github issues 40 | or contributed using pull requests. 41 | 42 | License 43 | ------- 44 | 45 | MIT 46 | -------------------------------------------------------------------------------- /crates/h3arrow/CHANGES.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is loosely based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres 6 | to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## Unreleased (YYYY-MM-DD TBD) 9 | 10 | **This crate is not published to crates.io anymore.** 11 | 12 | * Update h3o to 0.7. 13 | * Added H3ArrayBuilder type. 14 | * Added LocalIj coordinate support. 15 | 16 | ## v0.4.0 (2024-03-01) 17 | 18 | * Update h3o to 0.6. 19 | * Upgrade geo to 0.28 20 | * Upgrade rstar to 0.12 21 | * Upgrade geozero to 0.12 22 | 23 | ## v0.3.0 (2024-02-06) 24 | 25 | * Extend documentation on ParseUtf8Array::parse_utf8array. 26 | * Add ChangeResolutionOp::change_resolution_list. 27 | * Update geozero to 0.11. 28 | * Update h3o to 0.5. 29 | * Migrate from arrow2 to the official apache arrow implementation and aligned naming. This comes along with many API 30 | changes. `geoarrow::ToWKBLines` has been removed. 31 | 32 | ## v0.2.0 (2023-08-31) 33 | 34 | * Upgrade h3o from v0.3 to v0.4. Due to the new polyfill modes this lead to API breakages in the `ToCellsOptions` 35 | struct. 36 | 37 | ## v0.1.0 (2023-07-24) 38 | 39 | * Initial release 40 | -------------------------------------------------------------------------------- /crates/h3arrow/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "h3arrow" 3 | version = "0.4.0" 4 | edition = "2021" 5 | authors = ["Nico Mandery "] 6 | description = "Integration of the H3 geospatial grid with the arrow memory model" 7 | license = "MIT" 8 | keywords = ["geo", "spatial", "h3", "arrow"] 9 | readme = "README.md" 10 | homepage = "https://github.com/nmandery/h3arrow" 11 | repository = "https://github.com/nmandery/h3arrow" 12 | 13 | [package.metadata.docs.rs] 14 | all-features = true 15 | 16 | [features] 17 | geoarrow = ["dep:geoarrow", "dep:geozero"] 18 | rayon = ["dep:rayon", "geoarrow/rayon"] 19 | spatial_index = ["dep:rstar"] 20 | 21 | [dependencies] 22 | ahash = "0.8" 23 | arrow = { workspace = true } 24 | geoarrow = { git = "https://github.com/geoarrow/geoarrow-rs", rev = "3ecf7dfc1816261b84f813eaf2a0174f2b5752d8", optional = true } 25 | geo-types = { workspace = true } 26 | geo = { workspace = true } 27 | geozero = { version = "^0.14", default-features = false, features = [ 28 | "with-geo", 29 | "with-wkb", 30 | ], optional = true } 31 | h3o = { workspace = true, features = ["geo"] } 32 | nom = "7" 33 | rayon = { workspace = true, optional = true } 34 | rstar = { version = "0.12", optional = true } 35 | thiserror = "1" 36 | -------------------------------------------------------------------------------- /crates/h3arrow/LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Permission is hereby granted, free of charge, to any 2 | person obtaining a copy of this software and associated 3 | documentation files (the "Software"), to deal in the 4 | Software without restriction, including without 5 | limitation the rights to use, copy, modify, merge, 6 | publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software 8 | is furnished to do so, subject to the following 9 | conditions: 10 | 11 | The above copyright notice and this permission notice 12 | shall be included in all copies or substantial portions 13 | of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 16 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 17 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 18 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 19 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 22 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | DEALINGS IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /crates/h3arrow/README.md: -------------------------------------------------------------------------------- 1 | # h3arrow 2 | 3 | Experimental integration of H3 (via [h3o](https://github.com/HydroniumLabs/h3o)) with the Arrow memory model. 4 | 5 | -------------------------------------------------------------------------------- /crates/h3arrow/src/algorithm/bounding_rect.rs: -------------------------------------------------------------------------------- 1 | use crate::array::to_geo::{ToLines, ToPoints, ToPolygons}; 2 | use crate::array::{CellIndexArray, DirectedEdgeIndexArray, VertexIndexArray}; 3 | use geo::bounding_rect::BoundingRect; 4 | use geo_types::Rect; 5 | 6 | impl BoundingRect for CellIndexArray { 7 | type Output = Option; 8 | 9 | fn bounding_rect(&self) -> Self::Output { 10 | collect_rect( 11 | self.to_polygons(true) 12 | .expect("polygon vec") 13 | .into_iter() 14 | .flatten() 15 | .filter_map(|p| p.bounding_rect()), 16 | ) 17 | } 18 | } 19 | 20 | impl BoundingRect for VertexIndexArray { 21 | type Output = Option; 22 | 23 | fn bounding_rect(&self) -> Self::Output { 24 | collect_rect( 25 | self.to_points(true) 26 | .expect("point vec") 27 | .into_iter() 28 | .flatten() 29 | .map(|point| point.bounding_rect()), 30 | ) 31 | } 32 | } 33 | 34 | impl BoundingRect for DirectedEdgeIndexArray { 35 | type Output = Option; 36 | 37 | fn bounding_rect(&self) -> Self::Output { 38 | collect_rect( 39 | self.to_lines(true) 40 | .expect("line vec") 41 | .into_iter() 42 | .flatten() 43 | .map(|line| line.bounding_rect()), 44 | ) 45 | } 46 | } 47 | 48 | fn collect_rect(iter: I) -> Option 49 | where 50 | I: Iterator, 51 | { 52 | iter.reduce(|a, b| { 53 | Rect::new( 54 | (a.min().x.min(b.min().x), a.min().y.min(b.min().y)), 55 | (a.max().x.max(b.max().x), a.max().y.max(b.max().y)), 56 | ) 57 | }) 58 | } 59 | 60 | // todo: H3ListArray 61 | -------------------------------------------------------------------------------- /crates/h3arrow/src/algorithm/centroid.rs: -------------------------------------------------------------------------------- 1 | use crate::array::to_geo::{ToLines, ToPoints}; 2 | use crate::array::{CellIndexArray, DirectedEdgeIndexArray, VertexIndexArray}; 3 | use geo::centroid::Centroid; 4 | use geo_types::{MultiPoint, Point}; 5 | 6 | macro_rules! impl_point_based_centroid { 7 | ($($array_type:ty),*) => { 8 | $( 9 | impl Centroid for $array_type { 10 | type Output = Option; 11 | 12 | fn centroid(&self) -> Self::Output { 13 | MultiPoint::new( 14 | self.to_points(true) 15 | .expect("point vec") 16 | .into_iter() 17 | .flatten() 18 | .collect(), 19 | ) 20 | .centroid() 21 | } 22 | } 23 | )* 24 | }; 25 | } 26 | 27 | impl_point_based_centroid!(CellIndexArray, VertexIndexArray); 28 | 29 | impl Centroid for DirectedEdgeIndexArray { 30 | type Output = Option; 31 | 32 | fn centroid(&self) -> Self::Output { 33 | MultiPoint::new( 34 | self.to_lines(true) 35 | .expect("line vec") 36 | .into_iter() 37 | .flatten() 38 | .map(|line| line.centroid()) 39 | .collect(), 40 | ) 41 | .centroid() 42 | } 43 | } 44 | 45 | // todo: H3ListArray 46 | -------------------------------------------------------------------------------- /crates/h3arrow/src/algorithm/change_resolution.rs: -------------------------------------------------------------------------------- 1 | use crate::array::{CellIndexArray, H3ListArray, H3ListArrayBuilder}; 2 | use crate::error::Error; 3 | use h3o::{CellIndex, Resolution}; 4 | use std::cmp::Ordering; 5 | use std::iter::repeat; 6 | 7 | pub struct ChangedResolutionPair { 8 | /// values before the resolution change 9 | pub before: T, 10 | 11 | /// values after the resolution change 12 | pub after: T, 13 | } 14 | 15 | pub trait ChangeResolutionOp 16 | where 17 | Self: Sized, 18 | { 19 | /// change the H3 resolutions of all contained values to `resolution`. 20 | /// 21 | /// In case of resolution increases all child indexes will be added, so the returned 22 | /// value may contain more indexes than `self`. 23 | /// 24 | /// Invalid/empty values are omitted. 25 | fn change_resolution(&self, resolution: Resolution) -> Result; 26 | 27 | /// Change the H3 resolutions of all contained values to `resolution`. 28 | /// 29 | /// The output list array has the same length as the input array, positions of the elements 30 | /// in input and output are corresponding to each other. 31 | /// 32 | /// Invalid/empty values are preserved as such. 33 | fn change_resolution_list( 34 | &self, 35 | resolution: Resolution, 36 | ) -> Result, Error>; 37 | 38 | /// change the H3 resolutions of all contained values to `resolution` and build a before-array 39 | /// with the input values for each after-value. 40 | /// 41 | /// The length of the returned `before` and `after` values are guaranteed to be the same, as 42 | /// before-elements are repeated according to the resulting number of after-elements. 43 | /// 44 | /// Invalid/empty values are omitted. 45 | fn change_resolution_paired( 46 | &self, 47 | resolution: Resolution, 48 | ) -> Result, Error>; 49 | } 50 | 51 | #[inline] 52 | fn extend_with_cell(out_vec: &mut Vec, cell: CellIndex, target_resolution: Resolution) { 53 | match cell.resolution().cmp(&target_resolution) { 54 | Ordering::Less => out_vec.extend(cell.children(target_resolution)), 55 | Ordering::Equal => out_vec.push(cell), 56 | Ordering::Greater => out_vec.extend(cell.parent(target_resolution).iter()), 57 | } 58 | } 59 | 60 | impl ChangeResolutionOp for CellIndexArray { 61 | fn change_resolution(&self, resolution: Resolution) -> Result { 62 | let mut out_vec: Vec = Vec::with_capacity(self.len()); 63 | 64 | self.iter() 65 | .flatten() 66 | .for_each(|cell| extend_with_cell(&mut out_vec, cell, resolution)); 67 | 68 | Ok(out_vec.into()) 69 | } 70 | 71 | fn change_resolution_list( 72 | &self, 73 | resolution: Resolution, 74 | ) -> Result, Error> { 75 | let mut builder = H3ListArrayBuilder::with_capacity(self.len(), self.len()); 76 | 77 | self.iter().for_each(|cell| match cell { 78 | Some(cell) => match cell.resolution().cmp(&resolution) { 79 | Ordering::Less => { 80 | builder.values().append_many(cell.children(resolution)); 81 | builder.append(true) 82 | } 83 | Ordering::Equal => { 84 | builder.values().append_value(cell); 85 | builder.append(true) 86 | } 87 | Ordering::Greater => match cell.parent(resolution) { 88 | Some(parent_cell) => { 89 | builder.values().append_value(parent_cell); 90 | builder.append(true) 91 | } 92 | None => builder.append(false), 93 | }, 94 | }, 95 | None => { 96 | builder.append(false); 97 | } 98 | }); 99 | builder.finish() 100 | } 101 | 102 | fn change_resolution_paired( 103 | &self, 104 | resolution: Resolution, 105 | ) -> Result, Error> { 106 | let mut before_vec: Vec = Vec::with_capacity(self.len()); 107 | let mut after_vec: Vec = Vec::with_capacity(self.len()); 108 | 109 | self.iter().flatten().for_each(|cell| { 110 | let len_before = after_vec.len(); 111 | extend_with_cell(&mut after_vec, cell, resolution); 112 | before_vec.extend(repeat(cell).take(after_vec.len() - len_before)); 113 | }); 114 | 115 | Ok(ChangedResolutionPair { 116 | before: before_vec.into(), 117 | after: after_vec.into(), 118 | }) 119 | } 120 | } 121 | 122 | #[cfg(test)] 123 | mod test { 124 | use crate::algorithm::ChangeResolutionOp; 125 | use crate::array::CellIndexArray; 126 | use ahash::HashSet; 127 | use arrow::array::Array; 128 | use h3o::{LatLng, Resolution}; 129 | 130 | #[test] 131 | fn change_resolution() { 132 | let arr: CellIndexArray = vec![ 133 | Some(LatLng::new(23.4, 12.4).unwrap().to_cell(Resolution::Five)), 134 | None, 135 | Some(LatLng::new(12.3, 0.5).unwrap().to_cell(Resolution::Nine)), 136 | ] 137 | .into(); 138 | 139 | let arr_res_six = arr.change_resolution(Resolution::Six).unwrap(); 140 | assert_eq!(arr_res_six.len(), 7 + 1); 141 | 142 | // no invalid values 143 | assert_eq!( 144 | arr_res_six 145 | .primitive_array() 146 | .nulls() 147 | .map(|nullbuf| nullbuf.null_count()) 148 | .unwrap_or(0), 149 | 0 150 | ) 151 | } 152 | 153 | #[test] 154 | fn change_resolution_list() { 155 | let arr: CellIndexArray = vec![ 156 | Some(LatLng::new(23.4, 12.4).unwrap().to_cell(Resolution::Five)), 157 | None, 158 | Some(LatLng::new(12.3, 0.5).unwrap().to_cell(Resolution::Nine)), 159 | ] 160 | .into(); 161 | 162 | let list_arr = arr.change_resolution_list(Resolution::Six).unwrap(); 163 | assert_eq!(list_arr.len(), 3); 164 | 165 | assert_eq!(list_arr.list_array.value(0).len(), 7); 166 | assert_eq!(list_arr.list_array.value(1).len(), 0); 167 | assert_eq!(list_arr.list_array.value(2).len(), 1); 168 | } 169 | 170 | #[test] 171 | fn change_resolution_paired() { 172 | let arr: CellIndexArray = vec![ 173 | Some(LatLng::new(23.4, 12.4).unwrap().to_cell(Resolution::Five)), 174 | None, 175 | Some(LatLng::new(12.3, 0.5).unwrap().to_cell(Resolution::Nine)), 176 | ] 177 | .into(); 178 | 179 | let arr_res_six = arr.change_resolution_paired(Resolution::Six).unwrap(); 180 | assert_eq!(arr_res_six.after.len(), 7 + 1); 181 | assert_eq!(arr_res_six.before.len(), arr_res_six.after.len()); 182 | assert_eq!( 183 | arr_res_six 184 | .before 185 | .iter() 186 | .flatten() 187 | .collect::>() 188 | .len(), 189 | 2 190 | ) 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /crates/h3arrow/src/algorithm/compact.rs: -------------------------------------------------------------------------------- 1 | use crate::array::CellIndexArray; 2 | use crate::error::Error; 3 | use ahash::HashSet; 4 | use h3o::{CellIndex, Resolution}; 5 | 6 | pub trait CompactOp 7 | where 8 | Self: Sized, 9 | { 10 | /// expects all indexes to be at the same resolution 11 | fn compact(&self) -> Result; 12 | 13 | fn compact_mixed_resolutions(&self) -> Result; 14 | 15 | fn uncompact(&self, resolution: Resolution) -> Self; 16 | } 17 | 18 | impl CompactOp for CellIndexArray { 19 | fn compact(&self) -> Result { 20 | Ok(CellIndex::compact(self.iter().flatten())?.collect()) 21 | } 22 | 23 | fn compact_mixed_resolutions(&self) -> Result { 24 | let mut cellset = CellSet::default(); 25 | for cell in self.iter().flatten() { 26 | cellset.insert(cell); 27 | } 28 | cellset.finalize(true)?; 29 | 30 | Ok(Self::from_iter(cellset.iter_compacted())) 31 | } 32 | 33 | fn uncompact(&self, resolution: Resolution) -> Self { 34 | CellIndex::uncompact(self.iter().flatten(), resolution).collect() 35 | } 36 | } 37 | 38 | struct CellSet { 39 | pub(crate) modified_resolutions: [bool; 16], 40 | 41 | /// cells by their resolution. The index of the array is the resolution for the referenced vec 42 | pub(crate) cells_by_resolution: [Vec; 16], 43 | } 44 | 45 | impl CellSet { 46 | #[allow(unused)] 47 | pub(crate) fn append(&mut self, other: &mut Self) { 48 | for ((r_idx, sink), source) in self 49 | .cells_by_resolution 50 | .iter_mut() 51 | .enumerate() 52 | .zip(other.cells_by_resolution.iter_mut()) 53 | { 54 | if source.is_empty() { 55 | continue; 56 | } 57 | self.modified_resolutions[r_idx] = true; 58 | sink.append(source); 59 | } 60 | } 61 | 62 | pub(crate) fn compact(&mut self) -> Result<(), Error> { 63 | self.dedup(false, false); 64 | 65 | if let Some((min_touched_res, _)) = self 66 | .modified_resolutions 67 | .iter() 68 | .enumerate() 69 | .rev() 70 | .find(|(_, modified)| **modified) 71 | { 72 | let mut res = Some(Resolution::try_from(min_touched_res as u8)?); 73 | 74 | while let Some(h3_res) = res { 75 | let r_idx: usize = h3_res.into(); 76 | let mut compacted_in = std::mem::take(&mut self.cells_by_resolution[r_idx]); 77 | compacted_in.sort_unstable(); 78 | compacted_in.dedup(); 79 | for cell in CellIndex::compact(compacted_in.into_iter())? { 80 | self.insert(cell); 81 | } 82 | res = h3_res.pred(); 83 | } 84 | 85 | // mark all resolutions as not-modified 86 | self.modified_resolutions 87 | .iter_mut() 88 | .for_each(|r| *r = false); 89 | } 90 | 91 | self.dedup(true, true); 92 | 93 | Ok(()) 94 | } 95 | 96 | pub fn iter_compacted(&self) -> Box + '_> { 97 | Box::new( 98 | self.cells_by_resolution 99 | .iter() 100 | .flat_map(|v| v.iter()) 101 | .copied(), 102 | ) 103 | } 104 | 105 | #[allow(unused)] 106 | pub fn iter_uncompacted(&self, r: Resolution) -> Box + '_> { 107 | let r_idx: usize = r.into(); 108 | Box::new((0..=r_idx).flat_map(move |r_idx| { 109 | self.cells_by_resolution[r_idx] 110 | .iter() 111 | .flat_map(move |cell| cell.children(r)) 112 | })) 113 | } 114 | 115 | #[allow(unused)] 116 | pub fn len(&self) -> usize { 117 | self.cells_by_resolution.iter().map(|v| v.len()).sum() 118 | } 119 | 120 | #[allow(unused)] 121 | pub fn is_empty(&self) -> bool { 122 | !self.cells_by_resolution.iter().any(|v| !v.is_empty()) 123 | } 124 | 125 | pub(crate) fn insert(&mut self, cell: CellIndex) { 126 | let idx: usize = cell.resolution().into(); 127 | self.cells_by_resolution[idx].push(cell); 128 | self.modified_resolutions[idx] = true; 129 | } 130 | 131 | pub(crate) fn dedup(&mut self, shrink: bool, parents: bool) { 132 | fn dedup_vec(v: &mut Vec, shrink: bool) { 133 | v.sort_unstable(); 134 | v.dedup(); 135 | if shrink { 136 | v.shrink_to_fit(); 137 | } 138 | } 139 | 140 | #[cfg(feature = "rayon")] 141 | { 142 | use rayon::prelude::{IntoParallelRefMutIterator, ParallelIterator}; 143 | self.cells_by_resolution.par_iter_mut().for_each(|v| { 144 | dedup_vec(v, shrink); 145 | }); 146 | } 147 | 148 | #[cfg(not(feature = "rayon"))] 149 | self.cells_by_resolution.iter_mut().for_each(|v| { 150 | dedup_vec(v, shrink); 151 | }); 152 | 153 | if parents 154 | && self 155 | .cells_by_resolution 156 | .iter() 157 | .filter(|v| !v.is_empty()) 158 | .count() 159 | > 1 160 | { 161 | // remove cells whose parents are already contained 162 | let mut seen = HashSet::default(); 163 | for v in self.cells_by_resolution.iter_mut() { 164 | if !seen.is_empty() { 165 | v.retain(|cell| { 166 | let mut is_contained = false; 167 | let mut r = Some(cell.resolution()); 168 | while let Some(resolution) = r { 169 | if let Some(cell) = cell.parent(resolution) { 170 | if seen.contains(&cell) { 171 | is_contained = true; 172 | break; 173 | } 174 | } 175 | r = resolution.pred(); 176 | } 177 | !is_contained 178 | }); 179 | } 180 | seen.extend(v.iter().copied()); 181 | } 182 | } 183 | } 184 | 185 | pub(crate) fn finalize(&mut self, compact: bool) -> Result<(), Error> { 186 | if compact { 187 | self.compact()?; 188 | } else { 189 | self.dedup(true, true); 190 | } 191 | Ok(()) 192 | } 193 | } 194 | 195 | #[allow(clippy::derivable_impls)] 196 | impl Default for CellSet { 197 | fn default() -> Self { 198 | Self { 199 | modified_resolutions: [false; 16], 200 | cells_by_resolution: Default::default(), 201 | } 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /crates/h3arrow/src/algorithm/concave_hull.rs: -------------------------------------------------------------------------------- 1 | use crate::array::to_geo::{ 2 | cellindexarray_to_multipolygon, directededgeindexarray_to_multipoint, 3 | vertexindexarray_to_multipoint, 4 | }; 5 | use crate::array::{CellIndexArray, DirectedEdgeIndexArray, VertexIndexArray}; 6 | use geo::concave_hull::ConcaveHull; 7 | use geo_types::Polygon; 8 | 9 | impl ConcaveHull for CellIndexArray { 10 | type Scalar = f64; 11 | 12 | fn concave_hull(&self, concavity: Self::Scalar) -> Polygon { 13 | cellindexarray_to_multipolygon(self).concave_hull(concavity) 14 | } 15 | } 16 | 17 | impl ConcaveHull for VertexIndexArray { 18 | type Scalar = f64; 19 | 20 | fn concave_hull(&self, concavity: Self::Scalar) -> Polygon { 21 | vertexindexarray_to_multipoint(self).concave_hull(concavity) 22 | } 23 | } 24 | 25 | impl ConcaveHull for DirectedEdgeIndexArray { 26 | type Scalar = f64; 27 | 28 | fn concave_hull(&self, concavity: Self::Scalar) -> Polygon { 29 | directededgeindexarray_to_multipoint(self).concave_hull(concavity) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /crates/h3arrow/src/algorithm/convex_hull.rs: -------------------------------------------------------------------------------- 1 | use crate::array::to_geo::{ 2 | cellindexarray_to_multipolygon, directededgeindexarray_to_multipoint, 3 | vertexindexarray_to_multipoint, 4 | }; 5 | use crate::array::{CellIndexArray, DirectedEdgeIndexArray, VertexIndexArray}; 6 | use geo::convex_hull::ConvexHull; 7 | use geo_types::Polygon; 8 | 9 | impl<'a> ConvexHull<'a, f64> for CellIndexArray { 10 | type Scalar = f64; 11 | 12 | fn convex_hull(&'a self) -> Polygon { 13 | cellindexarray_to_multipolygon(self).convex_hull() 14 | } 15 | } 16 | 17 | impl<'a> ConvexHull<'a, f64> for VertexIndexArray { 18 | type Scalar = f64; 19 | 20 | fn convex_hull(&'a self) -> Polygon { 21 | vertexindexarray_to_multipoint(self).convex_hull() 22 | } 23 | } 24 | 25 | impl<'a> ConvexHull<'a, f64> for DirectedEdgeIndexArray { 26 | type Scalar = f64; 27 | 28 | fn convex_hull(&'a self) -> Polygon { 29 | directededgeindexarray_to_multipoint(self).convex_hull() 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /crates/h3arrow/src/algorithm/coordinates.rs: -------------------------------------------------------------------------------- 1 | use crate::array::CellIndexArray; 2 | use crate::error::Error; 3 | use arrow::array::{Float64Array, Float64Builder}; 4 | use h3o::LatLng; 5 | 6 | pub struct CoordinateArrays { 7 | pub lat: Float64Array, 8 | pub lng: Float64Array, 9 | } 10 | 11 | pub trait ToCoordinatesOp { 12 | /// convert to point coordinates in degrees 13 | fn to_coordinates(&self) -> Result; 14 | 15 | /// convert to point coordinates in radians 16 | fn to_coordinates_radians(&self) -> Result; 17 | } 18 | 19 | impl ToCoordinatesOp for CellIndexArray { 20 | fn to_coordinates(&self) -> Result { 21 | Ok(to_coordinatearrays(self, |ll| ll.lat(), |ll| ll.lng())) 22 | } 23 | 24 | fn to_coordinates_radians(&self) -> Result { 25 | Ok(to_coordinatearrays( 26 | self, 27 | |ll| ll.lat_radians(), 28 | |ll| ll.lng_radians(), 29 | )) 30 | } 31 | } 32 | 33 | fn to_coordinatearrays( 34 | cellindexarray: &CellIndexArray, 35 | extract_lat: ExtractLat, 36 | extract_lng: ExtractLng, 37 | ) -> CoordinateArrays 38 | where 39 | ExtractLat: Fn(&LatLng) -> f64, 40 | ExtractLng: Fn(&LatLng) -> f64, 41 | { 42 | let mut lat_builder = Float64Builder::with_capacity(cellindexarray.len()); 43 | let mut lng_builder = Float64Builder::with_capacity(cellindexarray.len()); 44 | 45 | cellindexarray.iter().for_each(|cell| { 46 | if let Some(cell) = cell { 47 | let ll = LatLng::from(cell); 48 | lat_builder.append_value(extract_lat(&ll)); 49 | lng_builder.append_value(extract_lng(&ll)); 50 | } else { 51 | lat_builder.append_null(); 52 | lng_builder.append_null(); 53 | } 54 | }); 55 | 56 | CoordinateArrays { 57 | lat: lat_builder.finish(), 58 | lng: lng_builder.finish(), 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /crates/h3arrow/src/algorithm/grid.rs: -------------------------------------------------------------------------------- 1 | use crate::array::{CellIndexArray, H3Array, H3ListArray, H3ListArrayBuilder}; 2 | use crate::error::Error; 3 | use ahash::{HashMap, HashMapExt}; 4 | use arrow::array::{ 5 | Array, GenericListArray, GenericListBuilder, OffsetSizeTrait, PrimitiveArray, UInt32Array, 6 | UInt32Builder, 7 | }; 8 | use h3o::{max_grid_disk_size, CellIndex}; 9 | use std::cmp::{max, min}; 10 | use std::collections::hash_map::Entry; 11 | 12 | pub struct GridDiskDistances { 13 | pub cells: H3ListArray, 14 | pub distances: GenericListArray, 15 | } 16 | 17 | #[derive(Copy, Clone, Eq, PartialEq)] 18 | pub enum KAggregationMethod { 19 | Min, 20 | Max, 21 | } 22 | 23 | pub struct GridDiskAggregateK { 24 | pub cells: CellIndexArray, 25 | pub distances: UInt32Array, 26 | } 27 | 28 | pub trait GridOp 29 | where 30 | Self: Sized, 31 | { 32 | fn grid_disk(&self, k: u32) -> Result, Error>; 33 | fn grid_disk_distances( 34 | &self, 35 | k: u32, 36 | ) -> Result, Error>; 37 | fn grid_ring_distances( 38 | &self, 39 | k_min: u32, 40 | k_max: u32, 41 | ) -> Result, Error>; 42 | fn grid_disk_aggregate_k( 43 | &self, 44 | k: u32, 45 | k_agg_method: KAggregationMethod, 46 | ) -> Result; 47 | } 48 | 49 | impl GridOp for H3Array { 50 | fn grid_disk(&self, k: u32) -> Result, Error> { 51 | let mut builder = H3ListArrayBuilder::with_capacity( 52 | self.len(), 53 | self.len() * max_grid_disk_size(k) as usize, 54 | ); 55 | 56 | for cell in self.iter() { 57 | match cell { 58 | Some(cell) => { 59 | let disc: Vec<_> = cell.grid_disk(k); 60 | builder.values().append_many(disc); 61 | builder.append(true); 62 | } 63 | None => { 64 | builder.append(false); 65 | } 66 | } 67 | } 68 | builder.finish() 69 | } 70 | 71 | fn grid_disk_distances( 72 | &self, 73 | k: u32, 74 | ) -> Result, Error> { 75 | build_grid_disk(self, k, |_, _| true) 76 | } 77 | 78 | fn grid_ring_distances( 79 | &self, 80 | k_min: u32, 81 | k_max: u32, 82 | ) -> Result, Error> { 83 | build_grid_disk(self, k_max, |_, k| k >= k_min) 84 | } 85 | 86 | fn grid_disk_aggregate_k( 87 | &self, 88 | k: u32, 89 | k_agg_method: KAggregationMethod, 90 | ) -> Result { 91 | let mut cellmap: HashMap = HashMap::with_capacity(self.len()); 92 | for cell in self.iter().flatten() { 93 | for (grid_cell, grid_distance) in cell.grid_disk_distances::>(k).into_iter() { 94 | match cellmap.entry(grid_cell) { 95 | Entry::Occupied(mut e) => { 96 | e.insert(match k_agg_method { 97 | KAggregationMethod::Min => min(*e.get(), grid_distance), 98 | KAggregationMethod::Max => max(*e.get(), grid_distance), 99 | }); 100 | } 101 | Entry::Vacant(e) => { 102 | e.insert(grid_distance); 103 | } 104 | }; 105 | } 106 | } 107 | 108 | let mut cells = Vec::with_capacity(cellmap.len()); 109 | let mut distances = Vec::with_capacity(cellmap.len()); 110 | 111 | for (cell, distance) in cellmap.into_iter() { 112 | cells.push(cell); 113 | distances.push(distance); 114 | } 115 | 116 | Ok(GridDiskAggregateK { 117 | cells: CellIndexArray::from(cells), 118 | distances: PrimitiveArray::new(distances.into(), None), 119 | }) 120 | } 121 | } 122 | 123 | fn build_grid_disk( 124 | cellindexarray: &CellIndexArray, 125 | k: u32, 126 | filter: F, 127 | ) -> Result, Error> 128 | where 129 | F: Fn(CellIndex, u32) -> bool, 130 | { 131 | let mut grid_cells_builder = H3ListArrayBuilder::with_capacity( 132 | cellindexarray.len(), 133 | cellindexarray.len(), // TODO: multiply with k or k_max-k_min 134 | ); 135 | let mut grid_distancess_builder = GenericListBuilder::with_capacity( 136 | UInt32Builder::with_capacity( 137 | cellindexarray.len(), // TODO: multiply with k or k_max-k_min 138 | ), 139 | cellindexarray.len(), 140 | ); 141 | 142 | for cell in cellindexarray.iter() { 143 | let is_valid = match cell { 144 | Some(cell) => { 145 | for (grid_cell, grid_distance) in cell.grid_disk_distances::>(k).into_iter() 146 | { 147 | if filter(grid_cell, grid_distance) { 148 | grid_cells_builder.values().append_value(grid_cell); 149 | grid_distancess_builder.values().append_value(grid_distance); 150 | } 151 | } 152 | 153 | true 154 | } 155 | None => false, 156 | }; 157 | 158 | grid_cells_builder.append(is_valid); 159 | grid_distancess_builder.append(is_valid) 160 | } 161 | 162 | let grid_cells = grid_cells_builder.finish()?; 163 | let grid_distances = grid_distancess_builder.finish(); 164 | 165 | debug_assert_eq!(grid_cells.len(), grid_distances.len()); 166 | 167 | Ok(GridDiskDistances { 168 | cells: grid_cells, 169 | distances: grid_distances, 170 | }) 171 | } 172 | -------------------------------------------------------------------------------- /crates/h3arrow/src/algorithm/localij.rs: -------------------------------------------------------------------------------- 1 | use crate::array::{CellIndexArray, H3ArrayBuilder}; 2 | use crate::error::Error; 3 | use arrow::array::Int32Array; 4 | use h3o::{CellIndex, CoordIJ, LocalIJ}; 5 | use std::iter::repeat; 6 | 7 | pub struct LocalIJArrays { 8 | pub anchors: CellIndexArray, 9 | pub i: Int32Array, 10 | pub j: Int32Array, 11 | } 12 | 13 | impl LocalIJArrays { 14 | pub fn try_new(anchors: CellIndexArray, i: Int32Array, j: Int32Array) -> Result { 15 | let instance = Self { anchors, i, j }; 16 | instance.validate()?; 17 | Ok(instance) 18 | } 19 | 20 | pub fn validate(&self) -> Result<(), Error> { 21 | if self.j.len() != self.i.len() || self.j.len() != self.anchors.len() { 22 | return Err(Error::LengthMismatch); 23 | } 24 | Ok(()) 25 | } 26 | 27 | fn to_cells_internal(&self, mut adder: Adder) -> Result 28 | where 29 | Adder: FnMut(LocalIJ, &mut H3ArrayBuilder) -> Result<(), Error>, 30 | { 31 | self.validate()?; 32 | 33 | let mut builder = CellIndexArray::builder(self.i.len()); 34 | for ((i, j), anchor) in self.i.iter().zip(self.j.iter()).zip(self.anchors.iter()) { 35 | match (i, j, anchor) { 36 | (Some(i), Some(j), Some(anchor)) => { 37 | let localij = LocalIJ::new(anchor, CoordIJ::new(i, j)); 38 | adder(localij, &mut builder)?; 39 | } 40 | _ => builder.append_null(), 41 | } 42 | } 43 | Ok(builder.finish()) 44 | } 45 | 46 | pub fn to_cells(&self) -> Result { 47 | self.to_cells_internal(|localij, builder| match CellIndex::try_from(localij) { 48 | Ok(cell) => { 49 | builder.append_value(cell); 50 | Ok(()) 51 | } 52 | Err(e) => Err(e.into()), 53 | }) 54 | } 55 | 56 | pub fn to_cells_failing_to_invalid(&self) -> Result { 57 | self.to_cells_internal(|local_ij, builder| { 58 | match CellIndex::try_from(local_ij) { 59 | Ok(cell) => { 60 | builder.append_value(cell); 61 | } 62 | Err(_) => { 63 | builder.append_null(); 64 | } 65 | } 66 | Ok(()) 67 | }) 68 | } 69 | } 70 | 71 | impl TryFrom for CellIndexArray { 72 | type Error = Error; 73 | 74 | fn try_from(value: LocalIJArrays) -> Result { 75 | value.to_cells() 76 | } 77 | } 78 | 79 | pub trait ToLocalIJOp { 80 | fn to_local_ij( 81 | &self, 82 | anchor: CellIndex, 83 | set_failing_to_invalid: bool, 84 | ) -> Result; 85 | 86 | /// convert to point coordinates in radians 87 | fn to_local_ij_array( 88 | &self, 89 | anchors: CellIndexArray, 90 | set_failing_to_invalid: bool, 91 | ) -> Result; 92 | } 93 | 94 | impl ToLocalIJOp for CellIndexArray { 95 | fn to_local_ij( 96 | &self, 97 | anchor: CellIndex, 98 | set_failing_to_invalid: bool, 99 | ) -> Result { 100 | let anchors = CellIndexArray::from_iter(repeat(anchor).take(self.len())); 101 | self.to_local_ij_array(anchors, set_failing_to_invalid) 102 | } 103 | 104 | fn to_local_ij_array( 105 | &self, 106 | anchors: CellIndexArray, 107 | set_failing_to_invalid: bool, 108 | ) -> Result { 109 | if self.len() != anchors.len() { 110 | return Err(Error::LengthMismatch); 111 | } 112 | let mut i_array_builder = Int32Array::builder(self.len()); 113 | let mut j_array_builder = Int32Array::builder(self.len()); 114 | 115 | for (cell, anchor) in self.iter().zip(anchors.iter()) { 116 | match (cell, anchor) { 117 | (Some(cell), Some(anchor)) => match cell.to_local_ij(anchor) { 118 | Ok(localij) => { 119 | i_array_builder.append_value(localij.coord.i); 120 | j_array_builder.append_value(localij.coord.j); 121 | } 122 | Err(e) => { 123 | if set_failing_to_invalid { 124 | i_array_builder.append_null(); 125 | j_array_builder.append_null(); 126 | } else { 127 | return Err(e.into()); 128 | } 129 | } 130 | }, 131 | _ => { 132 | i_array_builder.append_null(); 133 | j_array_builder.append_null(); 134 | } 135 | } 136 | } 137 | 138 | Ok(LocalIJArrays { 139 | anchors, 140 | i: i_array_builder.finish(), 141 | j: j_array_builder.finish(), 142 | }) 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /crates/h3arrow/src/algorithm/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod bounding_rect; 2 | pub mod centroid; 3 | pub mod change_resolution; 4 | pub mod compact; 5 | pub mod concave_hull; 6 | pub mod convex_hull; 7 | pub mod coordinates; 8 | pub mod grid; 9 | pub mod localij; 10 | pub mod string; 11 | 12 | #[allow(unused_imports)] 13 | pub use bounding_rect::*; 14 | #[allow(unused_imports)] 15 | pub use centroid::*; 16 | #[allow(unused_imports)] 17 | pub use change_resolution::*; 18 | #[allow(unused_imports)] 19 | pub use compact::*; 20 | #[allow(unused_imports)] 21 | pub use concave_hull::*; 22 | #[allow(unused_imports)] 23 | pub use convex_hull::*; 24 | #[allow(unused_imports)] 25 | pub use coordinates::*; 26 | #[allow(unused_imports)] 27 | pub use grid::*; 28 | #[allow(unused_imports)] 29 | pub use string::*; 30 | -------------------------------------------------------------------------------- /crates/h3arrow/src/array/cell.rs: -------------------------------------------------------------------------------- 1 | use arrow::array::{Float64Array, UInt64Array}; 2 | use h3o::{CellIndex, Resolution}; 3 | 4 | use crate::array::{CellIndexArray, H3ListArray, H3ListArrayBuilder, ResolutionArray}; 5 | use crate::error::Error; 6 | 7 | impl CellIndexArray { 8 | pub fn resolution(&self) -> ResolutionArray { 9 | self.iter() 10 | .map(|cell| cell.map(|cell| cell.resolution())) 11 | .collect() 12 | } 13 | 14 | pub fn area_rads2(&self) -> Float64Array { 15 | self.iter() 16 | .map(|cell| cell.map(|cell| cell.area_rads2())) 17 | .collect() 18 | } 19 | 20 | pub fn area_km2(&self) -> Float64Array { 21 | self.iter() 22 | .map(|cell| cell.map(|cell| cell.area_km2())) 23 | .collect() 24 | } 25 | 26 | pub fn area_m2(&self) -> Float64Array { 27 | self.iter() 28 | .map(|cell| cell.map(|cell| cell.area_m2())) 29 | .collect() 30 | } 31 | 32 | pub fn parent(&self, resolution: Resolution) -> Self { 33 | self.iter() 34 | .map(|cell| cell.and_then(|cell| cell.parent(resolution))) 35 | .collect() 36 | } 37 | 38 | pub fn children(&self, resolution: Resolution) -> Result, Error> { 39 | let mut builder = H3ListArrayBuilder::with_capacity(self.len(), self.len()); 40 | 41 | for value in self.iter() { 42 | if let Some(cell) = value { 43 | builder.values().append_many(cell.children(resolution)); 44 | builder.append(true); 45 | } else { 46 | builder.append(false); 47 | } 48 | } 49 | builder.finish() 50 | } 51 | 52 | pub fn children_count(&self, resolution: Resolution) -> UInt64Array { 53 | self.iter() 54 | .map(|cell| cell.map(|cell| cell.children_count(resolution))) 55 | .collect() 56 | } 57 | } 58 | 59 | #[cfg(test)] 60 | mod test { 61 | use h3o::{LatLng, Resolution}; 62 | 63 | use crate::array::CellIndexArray; 64 | 65 | #[test] 66 | fn construct_invalid_fails() { 67 | let res: Result = vec![ 68 | u64::from(LatLng::new(23.4, 12.4).unwrap().to_cell(Resolution::Five)), 69 | 0, 70 | ] 71 | .try_into(); 72 | assert!(res.is_err()); 73 | } 74 | 75 | #[test] 76 | fn resolution() { 77 | let arr: CellIndexArray = vec![ 78 | LatLng::new(23.4, 12.4).unwrap().to_cell(Resolution::Five), 79 | LatLng::new(12.3, 0.5).unwrap().to_cell(Resolution::Nine), 80 | ] 81 | .into(); 82 | 83 | let r_arr = arr.resolution(); 84 | assert_eq!(r_arr.len(), arr.len()); 85 | let r_values: Vec<_> = r_arr.iter().collect(); 86 | assert_eq!( 87 | r_values, 88 | vec![Some(Resolution::Five), Some(Resolution::Nine)] 89 | ); 90 | } 91 | 92 | #[test] 93 | fn children() { 94 | let arr: CellIndexArray = vec![ 95 | LatLng::new(23.4, 12.4).unwrap().to_cell(Resolution::Five), 96 | LatLng::new(12.3, 0.5).unwrap().to_cell(Resolution::Nine), 97 | ] 98 | .into(); 99 | 100 | let children = arr.children(Resolution::Six).unwrap(); 101 | assert_eq!(children.len(), 2); 102 | let cellarray = children.iter_arrays().next().flatten().unwrap().unwrap(); 103 | assert_eq!(cellarray.len(), 7); 104 | 105 | assert_eq!(cellarray.len(), 7); 106 | for child in cellarray.iter().flatten() { 107 | assert_eq!(arr.iter().next().flatten(), child.parent(Resolution::Five)); 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /crates/h3arrow/src/array/directededge.rs: -------------------------------------------------------------------------------- 1 | use crate::array::{CellIndexArray, DirectedEdgeIndexArray}; 2 | use arrow::array::Float64Array; 3 | 4 | impl DirectedEdgeIndexArray { 5 | pub fn origin(&self) -> CellIndexArray { 6 | self.iter() 7 | .map(|edge| edge.map(|edge| edge.origin())) 8 | .collect() 9 | } 10 | 11 | pub fn destination(&self) -> CellIndexArray { 12 | self.iter() 13 | .map(|edge| edge.map(|edge| edge.destination())) 14 | .collect() 15 | } 16 | 17 | pub fn length_rads(&self) -> Float64Array { 18 | self.iter() 19 | .map(|edge| edge.map(|edge| edge.length_rads())) 20 | .collect() 21 | } 22 | 23 | pub fn length_km(&self) -> Float64Array { 24 | self.iter() 25 | .map(|edge| edge.map(|edge| edge.length_km())) 26 | .collect() 27 | } 28 | 29 | pub fn length_m(&self) -> Float64Array { 30 | self.iter() 31 | .map(|edge| edge.map(|edge| edge.length_m())) 32 | .collect() 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /crates/h3arrow/src/array/from_geoarrow.rs: -------------------------------------------------------------------------------- 1 | use super::from_geo::{ 2 | cell_vecs_to_h3listarray, IterToCellIndexArray, IterToCellListArray, ToCellIndexArray, 3 | ToCellListArray, ToCellsOptions, 4 | }; 5 | use crate::algorithm::CompactOp; 6 | use crate::array::from_geo::geometry_to_cells; 7 | use crate::array::{CellIndexArray, H3ListArray}; 8 | use crate::error::Error; 9 | use arrow::array::OffsetSizeTrait; 10 | use geo_types::Geometry; 11 | use geoarrow::array::WKBArray; 12 | use geoarrow::trait_::ArrayAccessor; 13 | use geoarrow::ArrayBase; 14 | use h3o::CellIndex; 15 | #[cfg(feature = "rayon")] 16 | use rayon::prelude::{IntoParallelIterator, ParallelIterator}; 17 | 18 | macro_rules! impl_to_cells { 19 | ($array_type:ty) => { 20 | impl ToCellListArray for $array_type { 21 | fn to_celllistarray( 22 | &self, 23 | options: &ToCellsOptions, 24 | ) -> Result, Error> { 25 | self.iter_geo() 26 | .map(|v| v.map(Geometry::from)) 27 | .to_celllistarray(options) 28 | } 29 | } 30 | 31 | impl ToCellIndexArray for $array_type { 32 | fn to_cellindexarray(&self, options: &ToCellsOptions) -> Result { 33 | self.iter_geo() 34 | .map(|v| v.map(Geometry::from)) 35 | .to_cellindexarray(options) 36 | } 37 | } 38 | }; 39 | } 40 | 41 | impl_to_cells!(geoarrow::array::LineStringArray); 42 | impl_to_cells!(geoarrow::array::MultiLineStringArray); 43 | impl_to_cells!(geoarrow::array::MultiPointArray); 44 | impl_to_cells!(geoarrow::array::MultiPolygonArray); 45 | impl_to_cells!(geoarrow::array::PointArray); 46 | impl_to_cells!(geoarrow::array::PolygonArray); 47 | 48 | impl ToCellListArray for WKBArray { 49 | fn to_celllistarray( 50 | &self, 51 | options: &ToCellsOptions, 52 | ) -> Result, Error> { 53 | #[cfg(not(feature = "rayon"))] 54 | let pos_iter = 0..self.len(); 55 | 56 | #[cfg(feature = "rayon")] 57 | let pos_iter = (0..self.len()).into_par_iter(); 58 | 59 | let cell_vecs = pos_iter 60 | .map(|pos| { 61 | self.get_as_geo(pos) 62 | .map(|geom| geometry_to_cells(&geom, options)) 63 | .transpose() 64 | }) 65 | .collect::, _>>()?; 66 | 67 | cell_vecs_to_h3listarray(cell_vecs) 68 | } 69 | } 70 | 71 | impl ToCellIndexArray for WKBArray { 72 | fn to_cellindexarray(&self, options: &ToCellsOptions) -> Result { 73 | let cellindexarray = self.to_celllistarray(options)?.into_flattened()?; 74 | 75 | if options.compact { 76 | cellindexarray.compact() 77 | } else { 78 | Ok(cellindexarray) // may contain duplicates 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /crates/h3arrow/src/array/list.rs: -------------------------------------------------------------------------------- 1 | use crate::array::{H3Array, H3IndexArrayValue}; 2 | use crate::error::Error; 3 | use arrow::array::{Array, GenericListBuilder, UInt64Array, UInt64Builder}; 4 | use arrow::array::{GenericListArray, OffsetSizeTrait}; 5 | use arrow::datatypes::DataType; 6 | use std::marker::PhantomData; 7 | 8 | pub struct H3ListArray { 9 | pub(crate) list_array: GenericListArray, 10 | pub(crate) h3index_phantom: PhantomData, 11 | } 12 | 13 | impl H3ListArray 14 | where 15 | IX: H3IndexArrayValue, 16 | H3Array: TryFrom, 17 | { 18 | pub fn listarray(&self) -> &GenericListArray { 19 | &self.list_array 20 | } 21 | 22 | pub fn len(&self) -> usize { 23 | self.list_array.len() 24 | } 25 | 26 | pub fn is_empty(&self) -> bool { 27 | self.list_array.is_empty() 28 | } 29 | 30 | pub fn iter_arrays(&self) -> impl Iterator, Error>>> + '_ { 31 | self.list_array.iter().map(|opt| { 32 | opt.map(|array| { 33 | array 34 | .as_any() 35 | .downcast_ref::() 36 | // TODO: this should already be validated. unwrap/expect? 37 | .ok_or(Error::NotAUint64Array) 38 | .and_then(|pa| pa.clone().try_into()) 39 | }) 40 | }) 41 | } 42 | 43 | pub fn into_flattened(self) -> Result, Error> { 44 | // TODO: check validity correctness 45 | self.list_array 46 | .values() 47 | .as_any() 48 | .downcast_ref::() 49 | // TODO: this should already be validated. unwrap/expect? 50 | .ok_or(Error::NotAUint64Array) 51 | .and_then(|pa| pa.clone().try_into()) 52 | } 53 | 54 | pub(crate) fn from_genericlistarray_unvalidated( 55 | value: GenericListArray, 56 | ) -> Result, Error> { 57 | if value.data_type() != &DataType::UInt64 { 58 | return Err(Error::NotAUint64Array); 59 | } 60 | 61 | Ok(Self { 62 | list_array: value, 63 | h3index_phantom: PhantomData::, 64 | }) 65 | } 66 | } 67 | 68 | impl From> for GenericListArray { 69 | fn from(value: H3ListArray) -> Self { 70 | value.list_array 71 | } 72 | } 73 | 74 | pub(crate) fn genericlistarray_to_h3listarray_unvalidated( 75 | value: GenericListArray, 76 | ) -> Result, Error> { 77 | let nested_datatype = match value.data_type() { 78 | DataType::List(field_ref) => field_ref.data_type().clone(), 79 | DataType::LargeList(field_ref) => field_ref.data_type().clone(), 80 | _ => return Err(Error::NotAUint64Array), 81 | }; 82 | if !nested_datatype.equals_datatype(&DataType::UInt64) { 83 | return Err(Error::NotAUint64Array); 84 | } 85 | 86 | Ok(H3ListArray { 87 | list_array: value, 88 | h3index_phantom: PhantomData::, 89 | }) 90 | } 91 | 92 | impl TryFrom> for H3ListArray 93 | where 94 | IX: H3IndexArrayValue, 95 | H3Array: TryFrom, 96 | { 97 | type Error = Error; 98 | 99 | fn try_from(value: GenericListArray) -> Result { 100 | let instance = Self::from_genericlistarray_unvalidated(value)?; 101 | 102 | // validate all values 103 | for a in instance.iter_arrays().flatten() { 104 | let _ = a?; 105 | } 106 | Ok(instance) 107 | } 108 | } 109 | 110 | pub struct H3ArrayBuilder<'a, IX> { 111 | array_builder: &'a mut UInt64Builder, 112 | h3index_phantom: PhantomData, 113 | } 114 | 115 | impl<'a, IX> H3ArrayBuilder<'a, IX> 116 | where 117 | IX: H3IndexArrayValue, 118 | { 119 | #[inline] 120 | pub fn append_value(&mut self, value: IX) { 121 | self.array_builder.append_value(value.into()) 122 | } 123 | 124 | pub fn append_many(&mut self, iter: I) 125 | where 126 | I: IntoIterator, 127 | { 128 | iter.into_iter().for_each(|value| self.append_value(value)) 129 | } 130 | } 131 | 132 | pub struct H3ListArrayBuilder { 133 | h3index_phantom: PhantomData, 134 | builder: GenericListBuilder, 135 | } 136 | 137 | impl H3ListArrayBuilder 138 | where 139 | IX: H3IndexArrayValue, 140 | { 141 | pub fn with_capacity(list_capacity: usize, values_capacity: usize) -> Self { 142 | let builder = GenericListBuilder::with_capacity( 143 | UInt64Builder::with_capacity(values_capacity), 144 | list_capacity, 145 | ); 146 | Self { 147 | h3index_phantom: Default::default(), 148 | builder, 149 | } 150 | } 151 | 152 | pub fn append(&mut self, is_valid: bool) { 153 | self.builder.append(is_valid) 154 | } 155 | 156 | pub fn values(&mut self) -> H3ArrayBuilder<'_, IX> { 157 | H3ArrayBuilder { 158 | array_builder: self.builder.values(), 159 | h3index_phantom: self.h3index_phantom, 160 | } 161 | } 162 | 163 | pub fn finish(mut self) -> Result, Error> { 164 | genericlistarray_to_h3listarray_unvalidated(self.builder.finish()) 165 | } 166 | } 167 | 168 | impl Default for H3ListArrayBuilder 169 | where 170 | IX: H3IndexArrayValue, 171 | { 172 | fn default() -> Self { 173 | Self::with_capacity(10, 10) 174 | } 175 | } 176 | 177 | #[cfg(test)] 178 | mod tests { 179 | use crate::array::H3ListArrayBuilder; 180 | use h3o::{CellIndex, LatLng, Resolution}; 181 | 182 | #[test] 183 | fn construct() { 184 | let cell = LatLng::new(23.4, 12.4).unwrap().to_cell(Resolution::Five); 185 | 186 | let mut builder = H3ListArrayBuilder::::default(); 187 | builder.values().append_many(cell.grid_disk::>(1)); 188 | builder.append(true); 189 | builder.append(false); 190 | builder.values().append_many(cell.grid_disk::>(2)); 191 | builder.append(true); 192 | 193 | let list = builder.finish().unwrap(); 194 | 195 | /* 196 | let list = H3ListArray::::try_from_iter( 197 | [Some(1), None, Some(2)] 198 | .into_iter() 199 | .map(|k| k.map(|k| cell.grid_disk::>(k))), 200 | ) 201 | .unwrap(); 202 | 203 | */ 204 | assert_eq!(list.len(), 3); 205 | let mut list_iter = list.iter_arrays(); 206 | assert_eq!(list_iter.next().unwrap().unwrap().unwrap().len(), 7); 207 | assert!(list_iter.next().unwrap().is_none()); 208 | assert_eq!(list_iter.next().unwrap().unwrap().unwrap().len(), 19); 209 | assert!(list_iter.next().is_none()); 210 | drop(list_iter); 211 | 212 | let cells = list.into_flattened().unwrap(); 213 | assert_eq!(cells.len(), 26); 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /crates/h3arrow/src/array/resolution.rs: -------------------------------------------------------------------------------- 1 | use std::mem::transmute; 2 | 3 | use arrow::array::{Float64Array, UInt64Array, UInt8Array}; 4 | use h3o::Resolution; 5 | 6 | use crate::error::Error; 7 | 8 | use super::{FromIteratorWithValidity, FromWithValidity}; 9 | 10 | pub struct ResolutionArray(UInt8Array); 11 | 12 | impl TryFrom for ResolutionArray { 13 | type Error = Error; 14 | 15 | fn try_from(value: UInt8Array) -> Result { 16 | // validate the contained h3 cells 17 | value 18 | .iter() 19 | .flatten() 20 | .try_for_each(|h3index| Resolution::try_from(h3index).map(|_| ()))?; 21 | Ok(Self(value)) 22 | } 23 | } 24 | 25 | impl ResolutionArray { 26 | /// Returns an iterator over the values and validity, Option. 27 | pub fn iter(&self) -> impl Iterator> + '_ { 28 | // as the array contents have been validated upon construction, we just transmute to the h3o type 29 | self.0 30 | .iter() 31 | .map(|v| v.map(|resolution_u8| unsafe { transmute::(resolution_u8) })) 32 | } 33 | 34 | pub fn len(&self) -> usize { 35 | self.0.len() 36 | } 37 | 38 | pub fn is_empty(&self) -> bool { 39 | self.0.is_empty() 40 | } 41 | 42 | pub fn slice(&self, offset: usize, length: usize) -> Self { 43 | Self(self.0.slice(offset, length)) 44 | } 45 | 46 | pub fn area_rads2(&self) -> Float64Array { 47 | Float64Array::from_iter(self.iter().map(|v| v.map(|r| r.area_rads2()))) 48 | } 49 | 50 | pub fn area_km2(&self) -> Float64Array { 51 | Float64Array::from_iter(self.iter().map(|v| v.map(|r| r.area_km2()))) 52 | } 53 | 54 | pub fn area_m2(&self) -> Float64Array { 55 | Float64Array::from_iter(self.iter().map(|v| v.map(|r| r.area_m2()))) 56 | } 57 | 58 | pub fn edge_length_rads(&self) -> Float64Array { 59 | Float64Array::from_iter(self.iter().map(|v| v.map(|r| r.edge_length_rads()))) 60 | } 61 | 62 | pub fn edge_length_km(&self) -> Float64Array { 63 | Float64Array::from_iter(self.iter().map(|v| v.map(|r| r.edge_length_km()))) 64 | } 65 | 66 | pub fn edge_length_m(&self) -> Float64Array { 67 | Float64Array::from_iter(self.iter().map(|v| v.map(|r| r.edge_length_m()))) 68 | } 69 | 70 | pub fn cell_count(&self) -> UInt64Array { 71 | UInt64Array::from_iter(self.iter().map(|v| v.map(|r| r.cell_count()))) 72 | } 73 | 74 | /// Return the next resolution, if any. 75 | pub fn succ(&self) -> Self { 76 | Self::from_iter(self.iter().map(|v| v.and_then(|r| r.succ()))) 77 | } 78 | 79 | /// Return the previous resolution, if any. 80 | pub fn pred(&self) -> Self { 81 | Self::from_iter(self.iter().map(|v| v.and_then(|r| r.pred()))) 82 | } 83 | 84 | pub fn into_inner(self) -> UInt8Array { 85 | self.0 86 | } 87 | } 88 | 89 | impl FromIterator for ResolutionArray { 90 | fn from_iter>(iter: T) -> Self { 91 | Self(UInt8Array::from_iter( 92 | iter.into_iter().map(|v| Some(u8::from(v))), 93 | )) 94 | } 95 | } 96 | 97 | impl FromIterator> for ResolutionArray { 98 | fn from_iter>>(iter: T) -> Self { 99 | Self(UInt8Array::from_iter( 100 | iter.into_iter().map(|v| v.map(u8::from)), 101 | )) 102 | } 103 | } 104 | 105 | impl From> for ResolutionArray { 106 | fn from(value: Vec) -> Self { 107 | Self::from_iter(value) 108 | } 109 | } 110 | 111 | impl From>> for ResolutionArray { 112 | fn from(value: Vec>) -> Self { 113 | Self::from_iter(value) 114 | } 115 | } 116 | 117 | impl From for UInt8Array { 118 | fn from(value: ResolutionArray) -> Self { 119 | value.0 120 | } 121 | } 122 | 123 | impl FromIteratorWithValidity for ResolutionArray { 124 | fn from_iter_with_validity>(iter: T) -> Self { 125 | Self(UInt8Array::from_iter( 126 | iter.into_iter() 127 | .map(|v| Resolution::try_from(v).ok().map(u8::from)), 128 | )) 129 | } 130 | } 131 | 132 | impl FromIteratorWithValidity> for ResolutionArray { 133 | fn from_iter_with_validity>>(iter: T) -> Self { 134 | Self(UInt8Array::from_iter(iter.into_iter().map(|v| { 135 | v.and_then(|v| Resolution::try_from(v).ok().map(u8::from)) 136 | }))) 137 | } 138 | } 139 | 140 | impl FromWithValidity> for ResolutionArray { 141 | fn from_with_validity(value: Vec) -> Self { 142 | Self::from_iter_with_validity(value) 143 | } 144 | } 145 | 146 | impl FromWithValidity>> for ResolutionArray { 147 | fn from_with_validity(value: Vec>) -> Self { 148 | Self::from_iter_with_validity(value) 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /crates/h3arrow/src/array/to_geo.rs: -------------------------------------------------------------------------------- 1 | use crate::array::{ 2 | CellIndexArray, DirectedEdgeIndexArray, H3ListArray, PrimitiveArrayH3IndexIter, 3 | VertexIndexArray, 4 | }; 5 | use crate::error::Error; 6 | use geo::{CoordsIter, ToRadians}; 7 | use geo_types::{Coord, Line, LineString, MultiPoint, MultiPolygon, Point, Polygon}; 8 | use h3o::{CellIndex, DirectedEdgeIndex, LatLng, VertexIndex}; 9 | use std::convert::Infallible; 10 | use std::iter::{repeat, Map, Repeat, Zip}; 11 | 12 | pub trait IterPolygons { 13 | type Error; 14 | 15 | type Iter<'a>: Iterator>> 16 | where 17 | Self: 'a; 18 | 19 | fn iter_polygons(&self, use_degrees: bool) -> Self::Iter<'_>; 20 | } 21 | 22 | impl IterPolygons for CellIndexArray { 23 | type Error = Infallible; 24 | type Iter<'a> = Map< 25 | Zip, Repeat>, 26 | fn((Option, bool)) -> Option>, 27 | >; 28 | 29 | fn iter_polygons(&self, use_degrees: bool) -> Self::Iter<'_> { 30 | self.iter() 31 | .zip(repeat(use_degrees)) 32 | .map(|(v, use_degrees)| { 33 | v.map(|cell| { 34 | let mut poly = Polygon::new(LineString::from(cell.boundary()), vec![]); 35 | if !use_degrees { 36 | poly.to_radians_in_place(); 37 | } 38 | Ok(poly) 39 | }) 40 | }) 41 | } 42 | } 43 | 44 | pub trait ToPolygons { 45 | type Error; 46 | fn to_polygons(&self, use_degrees: bool) -> Result>, Self::Error>; 47 | } 48 | 49 | impl ToPolygons for T 50 | where 51 | T: IterPolygons, 52 | { 53 | type Error = ::Error; 54 | 55 | fn to_polygons(&self, use_degrees: bool) -> Result>, Self::Error> { 56 | self.iter_polygons(use_degrees) 57 | .map(|p| p.transpose()) 58 | .collect() 59 | } 60 | } 61 | 62 | pub trait IterPoints { 63 | type Error; 64 | 65 | type Iter<'a>: Iterator>> 66 | where 67 | Self: 'a; 68 | 69 | fn iter_points(&self, use_degrees: bool) -> Self::Iter<'_>; 70 | } 71 | 72 | macro_rules! impl_iter_points { 73 | ($($array:ty, $index_type:ty),*) => { 74 | $( 75 | impl IterPoints for $array { 76 | type Error = Infallible; 77 | type Iter<'a> = Map< 78 | Zip, Repeat>, 79 | fn((Option<$index_type>, bool)) -> Option>, 80 | >; 81 | 82 | fn iter_points(&self, use_degrees: bool) -> Self::Iter<'_> { 83 | self.iter() 84 | .zip(repeat(use_degrees)) 85 | .map(|(v, use_degrees)| { 86 | v.map(|cell| { 87 | let ll = LatLng::from(cell); 88 | let pt: Point = if use_degrees { 89 | Coord { 90 | x: ll.lng(), 91 | y: ll.lat(), 92 | } 93 | .into() 94 | } else { 95 | Coord { 96 | x: ll.lng_radians(), 97 | y: ll.lat_radians(), 98 | } 99 | .into() 100 | }; 101 | Ok(pt) 102 | }) 103 | }) 104 | } 105 | } 106 | 107 | )* 108 | }; 109 | } 110 | 111 | impl_iter_points!(CellIndexArray, CellIndex, VertexIndexArray, VertexIndex); 112 | 113 | pub trait ToPoints { 114 | type Error; 115 | fn to_points(&self, use_degrees: bool) -> Result>, Self::Error>; 116 | } 117 | 118 | impl ToPoints for T 119 | where 120 | T: IterPoints, 121 | { 122 | type Error = ::Error; 123 | 124 | fn to_points(&self, use_degrees: bool) -> Result>, Self::Error> { 125 | self.iter_points(use_degrees) 126 | .map(|p| p.transpose()) 127 | .collect() 128 | } 129 | } 130 | 131 | pub trait IterLines { 132 | type Error; 133 | 134 | type Iter<'a>: Iterator>> 135 | where 136 | Self: 'a; 137 | 138 | fn iter_lines(&self, use_degrees: bool) -> Self::Iter<'_>; 139 | } 140 | 141 | impl IterLines for DirectedEdgeIndexArray { 142 | type Error = Infallible; 143 | type Iter<'a> = Map< 144 | Zip, Repeat>, 145 | fn((Option, bool)) -> Option>, 146 | >; 147 | 148 | fn iter_lines(&self, use_degrees: bool) -> Self::Iter<'_> { 149 | self.iter() 150 | .zip(repeat(use_degrees)) 151 | .map(|(v, use_degrees)| { 152 | v.map(|edge| { 153 | let mut line = Line::from(edge); 154 | if !use_degrees { 155 | line.to_radians_in_place(); 156 | } 157 | Ok(line) 158 | }) 159 | }) 160 | } 161 | } 162 | 163 | pub trait ToLines { 164 | type Error; 165 | fn to_lines(&self, use_degrees: bool) -> Result>, Self::Error>; 166 | } 167 | 168 | impl ToLines for DirectedEdgeIndexArray { 169 | type Error = Infallible; 170 | 171 | fn to_lines(&self, use_degrees: bool) -> Result>, Self::Error> { 172 | self.iter_lines(use_degrees) 173 | .map(|v| v.transpose()) 174 | .collect() 175 | } 176 | } 177 | 178 | pub trait ToLineStrings { 179 | type Error; 180 | fn to_linestrings(&self, use_degrees: bool) -> Result>, Self::Error>; 181 | } 182 | 183 | impl ToLineStrings for DirectedEdgeIndexArray { 184 | type Error = Infallible; 185 | fn to_linestrings(&self, use_degrees: bool) -> Result>, Self::Error> { 186 | self.iter_lines(use_degrees) 187 | .map(|v| v.transpose().map(|res| res.map(LineString::from))) 188 | .collect() 189 | } 190 | } 191 | 192 | pub trait ToMultiPolygons { 193 | type Error; 194 | type Output; 195 | fn to_multipolygons(&self, use_degrees: bool) -> Result; 196 | } 197 | 198 | impl ToMultiPolygons for H3ListArray { 199 | type Error = Error; 200 | type Output = Vec>; 201 | 202 | fn to_multipolygons(&self, use_degrees: bool) -> Result { 203 | self.iter_arrays() 204 | .map(|opt| { 205 | opt.map(|res| { 206 | res.and_then(|array| { 207 | array 208 | .to_multipolygons(use_degrees) 209 | .map_err(Self::Error::from) 210 | }) 211 | }) 212 | .transpose() 213 | }) 214 | .collect() 215 | } 216 | } 217 | 218 | impl ToMultiPolygons for CellIndexArray { 219 | type Error = Error; 220 | type Output = MultiPolygon; 221 | 222 | fn to_multipolygons(&self, use_degrees: bool) -> Result { 223 | let mut multi_polygons = h3o::geom::dissolve(self.iter().flatten())?; 224 | if !use_degrees { 225 | multi_polygons.to_radians_in_place(); 226 | } 227 | Ok(multi_polygons) 228 | } 229 | } 230 | 231 | /// used as base for the algorithms of the `geo` crate 232 | pub(crate) fn directededgeindexarray_to_multipoint(array: &DirectedEdgeIndexArray) -> MultiPoint { 233 | MultiPoint::new( 234 | array 235 | .to_lines(true) 236 | .expect("line vec") 237 | .into_iter() 238 | .flatten() 239 | .flat_map(|line| line.coords_iter().map(Point::from)) 240 | .collect(), 241 | ) 242 | } 243 | 244 | /// used as base for the algorithms of the `geo` crate 245 | pub(crate) fn vertexindexarray_to_multipoint(array: &VertexIndexArray) -> MultiPoint { 246 | MultiPoint::new( 247 | array 248 | .to_points(true) 249 | .expect("point vec") 250 | .into_iter() 251 | .flatten() 252 | .collect(), 253 | ) 254 | } 255 | 256 | pub(crate) fn cellindexarray_to_multipolygon(array: &CellIndexArray) -> MultiPolygon { 257 | MultiPolygon::new( 258 | array 259 | .to_polygons(true) 260 | .expect("polygon vec") 261 | .into_iter() 262 | .flatten() 263 | .collect(), 264 | ) 265 | } 266 | -------------------------------------------------------------------------------- /crates/h3arrow/src/array/to_geoarrow.rs: -------------------------------------------------------------------------------- 1 | use crate::array::to_geo::{ 2 | IterLines, IterPoints, IterPolygons, ToLineStrings, ToPoints, ToPolygons, 3 | }; 4 | use crate::array::{H3Array, H3IndexArrayValue}; 5 | use arrow::array::{Array, OffsetSizeTrait}; 6 | use geo::point; 7 | use geo_types::LineString; 8 | use geoarrow::array::{ 9 | LineStringArray, LineStringBuilder, PointArray, PointBuilder, PolygonArray, PolygonBuilder, 10 | WKBArray, WKBBuilder, WKBCapacity, 11 | }; 12 | use geoarrow::datatypes::Dimension; 13 | 14 | pub trait ToGeoArrowPolygons { 15 | type Error; 16 | fn to_geoarrow_polygons( 17 | &self, 18 | use_degrees: bool, 19 | ) -> Result; 20 | } 21 | 22 | impl ToGeoArrowPolygons for T 23 | where 24 | T: ToPolygons, 25 | { 26 | type Error = T::Error; 27 | 28 | fn to_geoarrow_polygons( 29 | &self, 30 | use_degrees: bool, 31 | ) -> Result { 32 | Ok(PolygonBuilder::from_nullable_polygons( 33 | &self.to_polygons(use_degrees)?, 34 | Dimension::XY, 35 | Default::default(), 36 | Default::default(), 37 | ) 38 | .into()) 39 | } 40 | } 41 | 42 | pub trait ToGeoArrowPoints { 43 | type Error; 44 | fn to_geoarrow_points(&self, use_degrees: bool) -> Result; 45 | } 46 | 47 | impl ToGeoArrowPoints for T 48 | where 49 | T: ToPoints, 50 | { 51 | type Error = T::Error; 52 | fn to_geoarrow_points(&self, use_degrees: bool) -> Result { 53 | Ok(PointBuilder::from_nullable_points( 54 | self.to_points(use_degrees)?.iter().map(|x| x.as_ref()), 55 | Dimension::XY, 56 | Default::default(), 57 | Default::default(), 58 | ) 59 | .into()) 60 | } 61 | } 62 | 63 | pub trait ToGeoArrowLineStrings { 64 | type Error; 65 | fn to_geoarrow_lines( 66 | &self, 67 | use_degrees: bool, 68 | ) -> Result; 69 | } 70 | 71 | impl ToGeoArrowLineStrings for T 72 | where 73 | T: ToLineStrings, 74 | { 75 | type Error = T::Error; 76 | fn to_geoarrow_lines( 77 | &self, 78 | use_degrees: bool, 79 | ) -> Result { 80 | Ok(LineStringBuilder::from_nullable_line_strings( 81 | &self.to_linestrings(use_degrees)?, 82 | Dimension::XY, 83 | Default::default(), 84 | Default::default(), 85 | ) 86 | .into()) 87 | } 88 | } 89 | 90 | pub trait ToWKBPolygons { 91 | type Error; 92 | fn to_wkb_polygons( 93 | &self, 94 | use_degrees: bool, 95 | ) -> Result, Self::Error>; 96 | } 97 | 98 | impl ToWKBPolygons for H3Array 99 | where 100 | Self: IterPolygons, 101 | T: H3IndexArrayValue, 102 | { 103 | type Error = ::Error; 104 | 105 | fn to_wkb_polygons( 106 | &self, 107 | use_degrees: bool, 108 | ) -> Result, Self::Error> { 109 | // just use the first value to estimate the required buffer size. This may be off a bit and require 110 | // a re-allocation in case the first element is a pentagon 111 | let geometry_wkb_size = if let Some(first_value) = self 112 | .iter_polygons(use_degrees) 113 | .flat_map(|v| v.transpose().ok().flatten()) 114 | .next() 115 | { 116 | let mut cap = WKBCapacity::new_empty(); 117 | cap.add_polygon(Some(&first_value)); 118 | cap.buffer_capacity() 119 | } else { 120 | 0 121 | }; 122 | 123 | // number of non-null geometries 124 | let num_non_null = self.primitive_array.len().saturating_sub( 125 | self.primitive_array 126 | .nulls() 127 | .map(|nb| nb.null_count()) 128 | .unwrap_or(0), 129 | ); 130 | let mut builder = WKBBuilder::with_capacity(WKBCapacity::new( 131 | num_non_null * geometry_wkb_size, 132 | self.len(), 133 | )); 134 | for poly in self.iter_polygons(use_degrees) { 135 | let poly = poly.transpose()?; 136 | builder.push_polygon(poly.as_ref()) 137 | } 138 | Ok(builder.finish()) 139 | } 140 | } 141 | 142 | pub trait ToWKBLineStrings { 143 | type Error; 144 | fn to_wkb_linestrings( 145 | &self, 146 | use_degrees: bool, 147 | ) -> Result, Self::Error>; 148 | } 149 | 150 | impl ToWKBLineStrings for H3Array 151 | where 152 | Self: IterLines, 153 | T: H3IndexArrayValue, 154 | { 155 | type Error = ::Error; 156 | 157 | fn to_wkb_linestrings( 158 | &self, 159 | use_degrees: bool, 160 | ) -> Result, Self::Error> { 161 | // just use the first value to estimate the required buffer size. All geometries have the same number of coordinates 162 | let geometry_wkb_size = if let Some(first_value) = self 163 | .iter_lines(use_degrees) 164 | .flat_map(|v| v.transpose().ok().flatten()) 165 | .next() 166 | { 167 | let mut cap = WKBCapacity::new_empty(); 168 | cap.add_line_string(Some(&LineString::from(first_value))); 169 | cap.buffer_capacity() 170 | } else { 171 | 0 172 | }; 173 | 174 | // number of non-null geometries 175 | let num_non_null = self.primitive_array.len().saturating_sub( 176 | self.primitive_array 177 | .nulls() 178 | .map(|nb| nb.null_count()) 179 | .unwrap_or(0), 180 | ); 181 | 182 | let mut builder = WKBBuilder::with_capacity(WKBCapacity::new( 183 | num_non_null * geometry_wkb_size, 184 | self.len(), 185 | )); 186 | for line in self.iter_lines(use_degrees) { 187 | let linestring = line.transpose()?.map(LineString::from); 188 | builder.push_line_string(linestring.as_ref()) 189 | } 190 | Ok(builder.finish()) 191 | } 192 | } 193 | 194 | pub trait ToWKBPoints { 195 | type Error; 196 | fn to_wkb_points( 197 | &self, 198 | use_degrees: bool, 199 | ) -> Result, Self::Error>; 200 | } 201 | 202 | impl ToWKBPoints for H3Array 203 | where 204 | Self: IterPoints, 205 | T: H3IndexArrayValue, 206 | { 207 | type Error = ::Error; 208 | 209 | fn to_wkb_points( 210 | &self, 211 | use_degrees: bool, 212 | ) -> Result, Self::Error> { 213 | // just use the first value to estimate the required buffer size 214 | let geometry_wkb_size = if self 215 | .iter_points(use_degrees) 216 | .flat_map(|v| v.transpose().ok().flatten()) 217 | .next() 218 | .is_some() 219 | { 220 | let mut cap = WKBCapacity::new_empty(); 221 | let point = point! {x:0.0f64, y:0.0f64}; 222 | cap.add_point(Some(&point)); 223 | cap.buffer_capacity() 224 | } else { 225 | 0 226 | }; 227 | 228 | // number of non-null geometries 229 | let num_non_null = self.primitive_array.len().saturating_sub( 230 | self.primitive_array 231 | .nulls() 232 | .map(|nb| nb.null_count()) 233 | .unwrap_or(0), 234 | ); 235 | let mut builder = WKBBuilder::with_capacity(WKBCapacity::new( 236 | geometry_wkb_size * num_non_null, 237 | self.len(), 238 | )); 239 | for point in self.iter_points(use_degrees) { 240 | let point = point.transpose()?; 241 | builder.push_point(point.as_ref()) 242 | } 243 | Ok(builder.finish()) 244 | } 245 | } 246 | -------------------------------------------------------------------------------- /crates/h3arrow/src/array/validity.rs: -------------------------------------------------------------------------------- 1 | use arrow::array::UInt64Array; 2 | 3 | /// Conversion corresponding to `From` with the difference that the validity mask 4 | /// is set accordingly to the validity to the contained values. 5 | pub trait FromWithValidity { 6 | #[allow(dead_code)] 7 | fn from_with_validity(value: T) -> Self; 8 | } 9 | 10 | /// Conversion corresponding to `FromIterator` with the difference that the validity mask 11 | /// is set accordingly to the validity to the contained values. 12 | pub trait FromIteratorWithValidity { 13 | fn from_iter_with_validity>(iter: T) -> Self; 14 | } 15 | 16 | impl FromWithValidity> for T 17 | where 18 | T: FromIteratorWithValidity, 19 | { 20 | fn from_with_validity(value: Vec) -> Self { 21 | Self::from_iter_with_validity(value) 22 | } 23 | } 24 | 25 | impl FromWithValidity>> for T 26 | where 27 | T: FromIteratorWithValidity>, 28 | { 29 | fn from_with_validity(value: Vec>) -> Self { 30 | Self::from_iter_with_validity(value) 31 | } 32 | } 33 | 34 | impl FromWithValidity for T 35 | where 36 | T: FromIteratorWithValidity>, 37 | { 38 | fn from_with_validity(value: UInt64Array) -> Self { 39 | Self::from_iter_with_validity(value.iter()) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /crates/h3arrow/src/array/vertex.rs: -------------------------------------------------------------------------------- 1 | use crate::array::{CellIndexArray, VertexIndexArray}; 2 | 3 | impl VertexIndexArray { 4 | pub fn owner(&self) -> CellIndexArray { 5 | self.iter().map(|vx| vx.map(|vx| vx.owner())).collect() 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /crates/h3arrow/src/error.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, thiserror::Error)] 2 | pub enum Error { 3 | #[error(transparent)] 4 | InvalidCellIndex(#[from] h3o::error::InvalidCellIndex), 5 | 6 | #[error(transparent)] 7 | InvalidVertexIndex(#[from] h3o::error::InvalidVertexIndex), 8 | 9 | #[error(transparent)] 10 | InvalidDirectedEdgeIndex(#[from] h3o::error::InvalidDirectedEdgeIndex), 11 | 12 | #[error(transparent)] 13 | InvalidResolution(#[from] h3o::error::InvalidResolution), 14 | 15 | #[error(transparent)] 16 | InvalidLatLng(#[from] h3o::error::InvalidLatLng), 17 | 18 | #[error(transparent)] 19 | InvalidGeometry(#[from] h3o::error::InvalidGeometry), 20 | 21 | #[error(transparent)] 22 | CompactionError(#[from] h3o::error::CompactionError), 23 | 24 | #[error(transparent)] 25 | PlotterError(#[from] h3o::error::PlotterError), 26 | 27 | #[error(transparent)] 28 | DissolutionError(#[from] h3o::error::DissolutionError), 29 | 30 | #[error(transparent)] 31 | LocalIJError(#[from] h3o::error::LocalIjError), 32 | 33 | #[error(transparent)] 34 | Arrow2(#[from] arrow::error::ArrowError), 35 | 36 | #[error("not a UintArray")] 37 | NotAUint64Array, 38 | 39 | #[error("non-parsable CellIndex")] 40 | NonParsableCellIndex, 41 | 42 | #[error("non-parsable VertexIndex")] 43 | NonParsableVertexIndex, 44 | 45 | #[error("non-parsable DirectedEdgeIndex")] 46 | NonParsableDirectedEdgeIndex, 47 | 48 | #[error("Invalid WKB encountered")] 49 | InvalidWKB, 50 | 51 | #[error("array length mismatch")] 52 | LengthMismatch, 53 | 54 | #[error(transparent)] 55 | IO(#[from] std::io::Error), 56 | } 57 | -------------------------------------------------------------------------------- /crates/h3arrow/src/export.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "geoarrow")] 2 | pub use geoarrow; 3 | 4 | pub use arrow; 5 | pub use h3o; 6 | -------------------------------------------------------------------------------- /crates/h3arrow/src/lib.rs: -------------------------------------------------------------------------------- 1 | // reexport h3o 2 | pub use h3o; 3 | 4 | pub mod algorithm; 5 | pub mod array; 6 | pub mod error; 7 | pub mod export; 8 | 9 | #[cfg(feature = "spatial_index")] 10 | pub mod spatial_index; 11 | -------------------------------------------------------------------------------- /crates/h3arrow/src/spatial_index.rs: -------------------------------------------------------------------------------- 1 | use arrow::array::{Array, BooleanArray, BooleanBufferBuilder}; 2 | use geo::{BoundingRect, Intersects}; 3 | use geo_types::{Coord, Line, LineString, MultiPolygon, Point, Polygon, Rect}; 4 | use h3o::{CellIndex, DirectedEdgeIndex, LatLng, VertexIndex}; 5 | use rstar::primitives::{GeomWithData, Rectangle}; 6 | use rstar::{RTree, AABB}; 7 | 8 | use crate::array::{H3Array, H3IndexArrayValue}; 9 | 10 | pub trait RectIndexable { 11 | fn spatial_index_rect(&self) -> Option; 12 | fn intersects_with_polygon(&self, poly: &Polygon) -> bool; 13 | } 14 | 15 | impl RectIndexable for CellIndex { 16 | fn spatial_index_rect(&self) -> Option { 17 | LineString::from(self.boundary()).bounding_rect() 18 | } 19 | 20 | fn intersects_with_polygon(&self, poly: &Polygon) -> bool { 21 | // do a cheaper centroid containment check first before comparing the polygons 22 | let centroid: Coord = LatLng::from(*self).into(); 23 | if poly.intersects(¢roid) { 24 | let self_poly = Polygon::new(LineString::from(self.boundary()), vec![]); 25 | poly.intersects(&self_poly) 26 | } else { 27 | false 28 | } 29 | } 30 | } 31 | 32 | impl RectIndexable for DirectedEdgeIndex { 33 | fn spatial_index_rect(&self) -> Option { 34 | Some(Line::from(*self).bounding_rect()) 35 | } 36 | 37 | fn intersects_with_polygon(&self, poly: &Polygon) -> bool { 38 | poly.intersects(&Line::from(*self)) 39 | } 40 | } 41 | 42 | impl RectIndexable for VertexIndex { 43 | fn spatial_index_rect(&self) -> Option { 44 | Some(Point::from(*self).bounding_rect()) 45 | } 46 | 47 | fn intersects_with_polygon(&self, poly: &Polygon) -> bool { 48 | poly.intersects(&Point::from(*self)) 49 | } 50 | } 51 | 52 | type RTreeCoord = [f64; 2]; 53 | type RTreeBBox = Rectangle; 54 | type LocatedArrayPosition = GeomWithData; 55 | 56 | #[inline] 57 | fn to_coord(coord: Coord) -> RTreeCoord { 58 | [coord.x, coord.y] 59 | } 60 | 61 | #[inline] 62 | fn to_bbox(rect: &Rect) -> RTreeBBox { 63 | RTreeBBox::from_corners(to_coord(rect.min()), to_coord(rect.max())) 64 | } 65 | 66 | pub struct SpatialIndex { 67 | array: H3Array, 68 | rtree: RTree, 69 | } 70 | 71 | impl From> for SpatialIndex 72 | where 73 | IX: H3IndexArrayValue + RectIndexable, 74 | { 75 | fn from(array: H3Array) -> Self { 76 | let entries: Vec<_> = array 77 | .iter() 78 | .enumerate() 79 | .filter_map(|(pos, maybe_index)| match maybe_index { 80 | Some(index) => index 81 | .spatial_index_rect() 82 | .map(|rect| LocatedArrayPosition::new(to_bbox(&rect), pos)), 83 | _ => None, 84 | }) 85 | .collect(); 86 | 87 | let rtree = RTree::bulk_load(entries); 88 | Self { array, rtree } 89 | } 90 | } 91 | 92 | impl H3Array 93 | where 94 | IX: H3IndexArrayValue + RectIndexable, 95 | { 96 | pub fn spatial_index(&self) -> SpatialIndex { 97 | SpatialIndex::from(self.clone()) 98 | } 99 | } 100 | 101 | impl SpatialIndex 102 | where 103 | IX: H3IndexArrayValue + RectIndexable, 104 | { 105 | fn intersect_impl(&self, rect: &Rect, builder: &mut BooleanBufferBuilder, detailed_check: F) 106 | where 107 | F: Fn(IX) -> bool, 108 | { 109 | debug_assert_eq!(builder.len(), self.array.len()); 110 | 111 | let envelope = AABB::from_corners(to_coord(rect.min()), to_coord(rect.max())); 112 | let locator = self.rtree.locate_in_envelope_intersecting(&envelope); 113 | for located_array_position in locator { 114 | if let Some(value) = self.array.get(located_array_position.data) { 115 | if !builder.get_bit(located_array_position.data) { 116 | builder.set_bit(located_array_position.data, detailed_check(value)) 117 | } 118 | } 119 | } 120 | } 121 | 122 | fn finish_bufferbuilder(&self, mut builder: BooleanBufferBuilder) -> BooleanArray { 123 | BooleanArray::new( 124 | builder.finish(), 125 | self.array.primitive_array().nulls().cloned(), 126 | ) 127 | } 128 | 129 | pub fn intersect_envelopes(&self, rect: &Rect) -> BooleanArray { 130 | let mut builder = negative_mask(self.array.len()); 131 | self.intersect_impl(rect, &mut builder, |_| true); 132 | self.finish_bufferbuilder(builder) 133 | } 134 | 135 | pub fn intersect_polygon(&self, poly: &Polygon) -> BooleanArray { 136 | let mut builder = negative_mask(self.array.len()); 137 | if let Some(poly_rect) = poly.bounding_rect() { 138 | self.intersect_impl(&poly_rect, &mut builder, |ix| { 139 | ix.intersects_with_polygon(poly) 140 | }); 141 | } 142 | self.finish_bufferbuilder(builder) 143 | } 144 | 145 | pub fn intersect_multipolygon(&self, mpoly: &MultiPolygon) -> BooleanArray { 146 | let mut builder = negative_mask(self.array.len()); 147 | for poly in mpoly.iter() { 148 | if let Some(poly_rect) = poly.bounding_rect() { 149 | self.intersect_impl(&poly_rect, &mut builder, |ix| { 150 | ix.intersects_with_polygon(poly) 151 | }) 152 | } 153 | } 154 | self.finish_bufferbuilder(builder) 155 | } 156 | 157 | /// The envelope of the indexed elements is with `distance` of the given [Coord] `coord`. 158 | pub fn envelopes_within_distance(&self, coord: Coord, distance: f64) -> BooleanArray { 159 | let mut builder = negative_mask(self.array.len()); 160 | let locator = self.rtree.locate_within_distance(to_coord(coord), distance); 161 | for located_array_position in locator { 162 | builder.set_bit(located_array_position.data, true); 163 | } 164 | 165 | self.finish_bufferbuilder(builder) 166 | } 167 | } 168 | 169 | pub(crate) fn negative_mask(size: usize) -> BooleanBufferBuilder { 170 | let mut builder = BooleanBufferBuilder::new(size); 171 | builder.append_n(size, false); 172 | builder 173 | } 174 | 175 | #[cfg(test)] 176 | mod tests { 177 | use arrow::array::Array; 178 | use geo_types::{coord, polygon}; 179 | use h3o::{LatLng, Resolution}; 180 | 181 | use crate::array::CellIndexArray; 182 | 183 | #[test] 184 | fn cell_create_empty_index() { 185 | let arr: CellIndexArray = Vec::::new().try_into().unwrap(); 186 | let _ = arr.spatial_index(); 187 | } 188 | 189 | fn some_cell_array() -> CellIndexArray { 190 | vec![ 191 | Some(LatLng::new(45.5, 45.5).unwrap().to_cell(Resolution::Seven)), 192 | Some( 193 | LatLng::new(-60.5, -60.5) 194 | .unwrap() 195 | .to_cell(Resolution::Seven), 196 | ), 197 | Some( 198 | LatLng::new(120.5, -70.5) 199 | .unwrap() 200 | .to_cell(Resolution::Seven), 201 | ), 202 | None, 203 | ] 204 | .into() 205 | } 206 | 207 | #[test] 208 | fn cell_envelopes_within_distance() { 209 | let idx = some_cell_array().spatial_index(); 210 | let mask = idx.envelopes_within_distance((-60.0, -60.0).into(), 2.0); 211 | 212 | assert_eq!(mask.len(), 4); 213 | 214 | assert!(mask.is_valid(0)); 215 | assert!(!mask.value(0)); 216 | 217 | assert!(mask.is_valid(1)); 218 | assert!(mask.value(1)); 219 | 220 | assert!(mask.is_valid(2)); 221 | assert!(!mask.value(2)); 222 | 223 | assert!(!mask.is_valid(3)); 224 | } 225 | 226 | #[test] 227 | fn cell_geometries_intersect_polygon() { 228 | let idx = some_cell_array().spatial_index(); 229 | let mask = idx.intersect_polygon(&polygon!(exterior: [ 230 | coord! {x: 40.0, y: 40.0}, 231 | coord! {x: 40.0, y: 50.0}, 232 | coord! {x: 49.0, y: 50.0}, 233 | coord! {x: 49.0, y: 40.0}, 234 | coord! {x: 40.0, y: 40.0}, 235 | ], interiors: [])); 236 | 237 | assert_eq!(mask.len(), 4); 238 | 239 | assert!(mask.is_valid(0)); 240 | assert!(mask.value(0)); 241 | 242 | assert!(mask.is_valid(1)); 243 | assert!(!mask.value(1)); 244 | 245 | assert!(mask.is_valid(2)); 246 | assert!(!mask.value(2)); 247 | 248 | assert!(!mask.is_valid(3)); 249 | } 250 | } 251 | -------------------------------------------------------------------------------- /h3ronpy/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "h3ronpy" 3 | version = "0.22.0" 4 | authors = ["Nico Mandery "] 5 | description = "Data science toolkit for the H3 geospatial grid" 6 | edition = "2021" 7 | license = "MIT" 8 | keywords = ["geo", "spatial", "h3", "arrow", "python"] 9 | homepage = "https://github.com/nmandery/h3ronpy" 10 | repository = "https://github.com/nmandery/h3ronpy" 11 | 12 | [lib] 13 | name = "h3ronpy" 14 | crate-type = ["cdylib"] 15 | 16 | [dependencies] 17 | arrow = { workspace = true } 18 | env_logger = "^0.11" 19 | geo-types = { workspace = true } 20 | geo = { workspace = true } 21 | h3arrow = { path = "../crates/h3arrow", features = ["geoarrow", "rayon"] } 22 | hashbrown = "0.14" 23 | itertools = "0.13" 24 | ndarray = { version = "0.16", features = ["rayon"] } 25 | numpy = "0.22" 26 | ordered-float = ">=2.0.1" 27 | py_geo_interface = { git = "https://github.com/nmandery/py_geo_interface", rev = "36723cdbabc2a7aad1746a8c06db17b4e39ce3b9", features = [ 28 | "f64", 29 | "wkb", 30 | ] } 31 | pyo3 = { version = "^0.22", features = [ 32 | "extension-module", 33 | "abi3", 34 | "abi3-py39", 35 | ] } 36 | pyo3-arrow = { version = "0.5.1", default-features = false } 37 | rasterh3 = { version = "0.10", features = ["rayon"] } 38 | rayon = { workspace = true } 39 | -------------------------------------------------------------------------------- /h3ronpy/LICENSE.txt: -------------------------------------------------------------------------------- 1 | Permission is hereby granted, free of charge, to any 2 | person obtaining a copy of this software and associated 3 | documentation files (the "Software"), to deal in the 4 | Software without restriction, including without 5 | limitation the rights to use, copy, modify, merge, 6 | publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software 8 | is furnished to do so, subject to the following 9 | conditions: 10 | 11 | The above copyright notice and this permission notice 12 | shall be included in all copies or substantial portions 13 | of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 16 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 17 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 18 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 19 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 22 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | DEALINGS IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /h3ronpy/MANIFEST.in: -------------------------------------------------------------------------------- 1 | exclude venv install-dev-dependencies.py README.ipynb requirements.documentation.txt data 2 | -------------------------------------------------------------------------------- /h3ronpy/data/europe-and-north-africa.tif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nmandery/h3ronpy/f4ab01637b105d07c1af63ce22467e4402d2dd03/h3ronpy/data/europe-and-north-africa.tif -------------------------------------------------------------------------------- /h3ronpy/data/naturalearth_110m_admin_0_countries.fgb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nmandery/h3ronpy/f4ab01637b105d07c1af63ce22467e4402d2dd03/h3ronpy/data/naturalearth_110m_admin_0_countries.fgb -------------------------------------------------------------------------------- /h3ronpy/data/population-841fa8bffffffff.parquet: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nmandery/h3ronpy/f4ab01637b105d07c1af63ce22467e4402d2dd03/h3ronpy/data/population-841fa8bffffffff.parquet -------------------------------------------------------------------------------- /h3ronpy/data/r.tiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nmandery/h3ronpy/f4ab01637b105d07c1af63ce22467e4402d2dd03/h3ronpy/data/r.tiff -------------------------------------------------------------------------------- /h3ronpy/docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = source 9 | BUILDDIR = build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /h3ronpy/docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=source 11 | set BUILDDIR=build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /h3ronpy/docs/requirements.txt: -------------------------------------------------------------------------------- 1 | sphinx 2 | sphinx_rtd_theme>=1.2 3 | sphinx-autodoc-typehints 4 | numpy 5 | h3<4 6 | matplotlib 7 | rasterio 8 | jupyter 9 | jupyter-sphinx 10 | scipy 11 | pandas 12 | geopandas 13 | polars 14 | pyarrow -------------------------------------------------------------------------------- /h3ronpy/docs/source/api/core.rst: -------------------------------------------------------------------------------- 1 | Core 2 | ==== 3 | 4 | .. automodule:: h3ronpy 5 | :members: 6 | :undoc-members: 7 | 8 | -------------------------------------------------------------------------------- /h3ronpy/docs/source/api/index.rst: -------------------------------------------------------------------------------- 1 | API documentation 2 | ================= 3 | 4 | .. toctree:: 5 | 6 | core 7 | pandas 8 | polars 9 | -------------------------------------------------------------------------------- /h3ronpy/docs/source/api/pandas.rst: -------------------------------------------------------------------------------- 1 | Pandas/GeoPandas 2 | ================ 3 | 4 | Functions 5 | --------- 6 | 7 | .. automodule:: h3ronpy.pandas 8 | :members: 9 | :undoc-members: 10 | 11 | 12 | Raster module 13 | ------------- 14 | 15 | .. automodule:: h3ronpy.pandas.raster 16 | :members: 17 | :undoc-members: 18 | 19 | 20 | Vector module 21 | ------------- 22 | 23 | .. automodule:: h3ronpy.pandas.vector 24 | :members: 25 | :undoc-members: 26 | -------------------------------------------------------------------------------- /h3ronpy/docs/source/api/polars.rst: -------------------------------------------------------------------------------- 1 | Polars 2 | ====== 3 | 4 | 5 | Functions 6 | --------- 7 | 8 | .. automodule:: h3ronpy.polars 9 | :members: 10 | :undoc-members: 11 | :exclude-members: H3SeriesShortcuts, H3Expr 12 | 13 | 14 | Polars API extensions 15 | --------------------- 16 | 17 | Polars itself provides `multiple ways to extend its API `_ - h3ronpy makes 18 | use of this to provide custom extensions for ``Series`` and ``Expr`` types in the ``h3`` namespace. 19 | 20 | To make these extensions available, the ``h3ronpy.polars`` module needs to be imported. 21 | 22 | Expressions 23 | ^^^^^^^^^^^ 24 | 25 | Example: 26 | 27 | .. jupyter-execute:: 28 | 29 | import polars as pl 30 | # to register extension functions in the polars API 31 | import h3ronpy.polars 32 | 33 | df = pl.DataFrame({ 34 | "cell": ["8852dc41cbfffff", "8852dc41bbfffff"], 35 | "value": ["a", "b"] 36 | }) 37 | 38 | (df.lazy() 39 | .select([ 40 | pl.col("cell") 41 | .h3.cells_parse() 42 | .h3.grid_disk(2) 43 | .alias("disk"), 44 | pl.col("value") 45 | ]) 46 | 47 | .group_by("value") 48 | .agg([ 49 | pl.col("disk") 50 | .explode() 51 | .h3.cells_area_km2() 52 | .sum() 53 | ]) 54 | .collect() 55 | ) 56 | 57 | All methods of the ``H3Expr`` class are available in the ``h3`` object of a polars ``Expr``: 58 | 59 | .. autoclass:: h3ronpy.polars.H3Expr 60 | :members: 61 | :undoc-members: 62 | 63 | Series 64 | ^^^^^^ 65 | 66 | Example: 67 | 68 | .. jupyter-execute:: 69 | 70 | import polars as pl 71 | # to register extension functions in the polars API 72 | import h3ronpy.polars 73 | 74 | cell_strings = (pl.Series("cells", ["8852dc41cbfffff"]) 75 | .h3.cells_parse() 76 | .h3.grid_disk(1) 77 | .explode() 78 | .sort() 79 | .h3.cells_to_string()) 80 | 81 | cell_strings 82 | 83 | 84 | All methods of the ``H3SeriesShortcuts`` class are available in the ``h3`` object of a polars ``Series``: 85 | 86 | .. autoclass:: h3ronpy.polars.H3SeriesShortcuts 87 | :members: 88 | :undoc-members: 89 | -------------------------------------------------------------------------------- /h3ronpy/docs/source/changelog.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../../CHANGES.rst 2 | -------------------------------------------------------------------------------- /h3ronpy/docs/source/conf.py: -------------------------------------------------------------------------------- 1 | import os 2 | from pathlib import Path 3 | 4 | from h3ronpy import __version__ as hp_version 5 | 6 | # Configuration file for the Sphinx documentation builder. 7 | 8 | # -- Project information 9 | 10 | project = "h3ronpy" 11 | copyright = "2023, the h3ronpy authors" 12 | author = "Nico Mandery" 13 | 14 | 15 | release = hp_version 16 | version = hp_version 17 | 18 | # -- General configuration 19 | 20 | extensions = [ 21 | "sphinx.ext.duration", 22 | "sphinx.ext.doctest", 23 | "sphinx.ext.autodoc", 24 | "sphinx.ext.autosummary", 25 | "sphinx.ext.intersphinx", 26 | "sphinx_rtd_theme", 27 | "jupyter_sphinx", 28 | ] 29 | 30 | intersphinx_mapping = { 31 | "python": ("https://docs.python.org/3/", None), 32 | "sphinx": ("https://www.sphinx-doc.org/en/master/", None), 33 | } 34 | intersphinx_disabled_domains = ["std"] 35 | 36 | templates_path = ["_templates"] 37 | 38 | # -- Options for HTML output 39 | 40 | html_theme = "sphinx_rtd_theme" 41 | 42 | # -- Options for EPUB output 43 | epub_show_urls = "footnote" 44 | 45 | autodoc_typehints = "both" 46 | 47 | os.environ["PROJECT_ROOT"] = str(Path(__file__).parent.parent.parent) 48 | -------------------------------------------------------------------------------- /h3ronpy/docs/source/contributing.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../../../CONTRIBUTING.rst 2 | -------------------------------------------------------------------------------- /h3ronpy/docs/source/index.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../../README.rst 2 | 3 | .. note:: 4 | 5 | This project is under active development. 6 | 7 | 8 | Contents 9 | -------- 10 | 11 | .. toctree:: 12 | :maxdepth: 3 13 | 14 | installation 15 | usage/index 16 | api/index 17 | changelog 18 | license 19 | contributing 20 | -------------------------------------------------------------------------------- /h3ronpy/docs/source/installation.rst: -------------------------------------------------------------------------------- 1 | Installation 2 | ============ 3 | 4 | .. note:: 5 | 6 | To avoid pulling in unused dependencies, `h3ronpy` does not declare a dependency to `pandas`, 7 | `geopandas` and `polars`. These packages need to be installed separately. 8 | 9 | 10 | From PyPi 11 | --------- 12 | 13 | .. code-block:: shell 14 | 15 | pip install h3ronpy 16 | 17 | 18 | From conda-forge 19 | ---------------- 20 | 21 | See `h3ronpy-feedstock `_. 22 | 23 | 24 | From source 25 | ----------- 26 | 27 | To build from source a recent version of the `rust language `_ is required. The easiest 28 | way to install is by using `rustup `_. 29 | 30 | An recent version of `pip` is required - version 23.1.2 works. `pip` can be upgraded by running 31 | 32 | .. code-block:: shell 33 | 34 | pip install --upgrade pip 35 | 36 | 37 | After that, everything is set up to build and install `h3ronpy`: 38 | 39 | .. code-block:: shell 40 | 41 | git clone https://github.com/nmandery/h3ronpy.git 42 | cd h3ronpy 43 | pip install . 44 | 45 | This will build the rust code using `maturin `_. For more information on this see its website. 46 | 47 | When encountering a circular import error after this installation procedure, just change the directory out of the 48 | h3ronpy source directory. 49 | -------------------------------------------------------------------------------- /h3ronpy/docs/source/license.rst: -------------------------------------------------------------------------------- 1 | License 2 | ======= 3 | 4 | MIT 5 | 6 | .. include:: ../../LICENSE.txt -------------------------------------------------------------------------------- /h3ronpy/docs/source/usage/grid.rst: -------------------------------------------------------------------------------- 1 | Grid traversal 2 | ============== 3 | 4 | Create a few test cells to the examples on this page: 5 | 6 | .. jupyter-execute:: 7 | 8 | import numpy as np 9 | import h3.api.numpy_int as h3 10 | import pandas as pd 11 | import pyarrow as pa 12 | 13 | from h3ronpy.pandas.vector import cells_dataframe_to_geodataframe 14 | from h3ronpy import DEFAULT_CELL_COLUMN_NAME 15 | 16 | cells = np.array( 17 | [ 18 | h3.geo_to_h3(5.2, -5.2, 7), 19 | h3.geo_to_h3(5.3, -5.1, 7), 20 | ], 21 | dtype=np.uint64, 22 | ) 23 | 24 | 25 | Grid-disks with :py:func:`h3ronpy.grid_disk` 26 | --------------------------------------------------- 27 | 28 | .. jupyter-execute:: 29 | 30 | from h3ronpy import grid_disk 31 | 32 | cells_dataframe_to_geodataframe( 33 | pd.DataFrame({ 34 | DEFAULT_CELL_COLUMN_NAME: pa.array(grid_disk(cells, 9, flatten=True)).to_pandas()} 35 | ) 36 | ).plot() 37 | 38 | 39 | Grid-disk aggregates with :py:func:`h3ronpy.grid_disk_aggregate_k` 40 | ------------------------------------------------------------------------- 41 | 42 | This builds ontop of :py:func:`h3ronpy.grid_disk_distances` while directly 43 | performing simple aggregations to avoid returning potentially very large dataframes. 44 | 45 | .. jupyter-execute:: 46 | 47 | from h3ronpy import grid_disk_aggregate_k 48 | 49 | cells_dataframe_to_geodataframe( 50 | pa.table(grid_disk_aggregate_k(cells, 9, "min")).to_pandas() 51 | ).plot(column="k", legend=True, legend_kwds={"label": "k", "orientation": "horizontal"},) 52 | 53 | 54 | .. jupyter-execute:: 55 | 56 | cells_dataframe_to_geodataframe( 57 | pa.table(grid_disk_aggregate_k(cells, 9, "max")).to_pandas() 58 | ).plot(column="k", legend=True, legend_kwds={"label": "k", "orientation": "horizontal"},) 59 | -------------------------------------------------------------------------------- /h3ronpy/docs/source/usage/index.rst: -------------------------------------------------------------------------------- 1 | .. _usage: 2 | 3 | Usage examples 4 | ============== 5 | 6 | We are mainly using `pandas` and `geopandas` here to be able to plot the geometries, but most non-geometry features are available 7 | in the other APIs as well. 8 | 9 | .. note:: 10 | 11 | These examples are supposed to show some of the provided functionalities. They certainly do not cover all aspects. 12 | 13 | .. toctree:: 14 | :maxdepth: 2 15 | 16 | raster 17 | vector 18 | grid 19 | -------------------------------------------------------------------------------- /h3ronpy/docs/source/usage/raster.rst: -------------------------------------------------------------------------------- 1 | Converting raster data to H3 2 | ============================ 3 | 4 | .. jupyter-execute:: 5 | 6 | from matplotlib import pyplot 7 | import rasterio 8 | from rasterio.plot import show 9 | import numpy as np 10 | import h3.api.numpy_int as h3 11 | from scipy import ndimage 12 | import geopandas as gpd 13 | from pathlib import Path 14 | import os 15 | 16 | # increase the plot size 17 | pyplot.rcParams['figure.dpi'] = 120 18 | 19 | project_root = Path(os.environ["PROJECT_ROOT"]) 20 | 21 | 22 | Prepare a dataset using rasterio first 23 | -------------------------------------- 24 | 25 | .. jupyter-execute:: 26 | 27 | import rasterio 28 | from rasterio.plot import show 29 | 30 | src = rasterio.open(project_root / "data/europe-and-north-africa.tif") 31 | print(src.colorinterp) 32 | 33 | green = src.read(2) 34 | blue = src.read(3) 35 | print(green.shape) 36 | 37 | show(src) 38 | 39 | Do some image processing - like this messy extraction of a vegetation mask here: 40 | 41 | .. jupyter-execute:: 42 | 43 | vegetation_mask = (green < 250) & (blue < 50) 44 | ocean_mask = (green >= 6) & (green <= 14) & (blue >= 47) & (blue <= 54) 45 | vegetation_nodata_value = 0 46 | 47 | vegetation = np.full(green.shape, 10, dtype="int8") 48 | vegetation[ocean_mask] = vegetation_nodata_value 49 | vegetation[vegetation_mask] = 20 50 | 51 | # smooth a bit to remove single pixels 52 | vegetation = ndimage.gaussian_filter(vegetation, sigma=.7) 53 | vegetation[vegetation <= 5] = vegetation_nodata_value 54 | vegetation[(vegetation > 0) & (vegetation < 15)] = 1 55 | vegetation[vegetation >= 15] = 2 56 | vegetation[ocean_mask] = vegetation_nodata_value 57 | 58 | vegetation_plot_args = dict(cmap='Greens', vmin=0, vmax=2) 59 | 60 | pyplot.imshow(vegetation, **vegetation_plot_args) 61 | 62 | .. jupyter-execute:: 63 | 64 | vegetation 65 | 66 | Convert the raster numpy array to H3 67 | ------------------------------------ 68 | 69 | Find the closest H3 resolution to use. See also the docstrings of the used functions and of the `h3ronpy.raster` module. 70 | 71 | .. jupyter-execute:: 72 | 73 | from h3ronpy.raster import nearest_h3_resolution 74 | 75 | h3_res = nearest_h3_resolution(vegetation.shape, src.transform, search_mode="smaller_than_pixel") 76 | print(f"Using H3 resolution {h3_res}") 77 | 78 | Now we convert the raster directly into a geopandas `GeoDataFrame`: 79 | 80 | .. jupyter-execute:: 81 | 82 | from h3ronpy.pandas.raster import raster_to_dataframe 83 | 84 | vegetation_h3_df = raster_to_dataframe( 85 | vegetation, 86 | src.transform, 87 | h3_res, 88 | nodata_value=vegetation_nodata_value, 89 | compact=True, 90 | geo=True 91 | ) 92 | 93 | vegetation_h3_df.plot(column="value", linewidth=0.2, edgecolor="black", **vegetation_plot_args) 94 | pyplot.show() 95 | 96 | 97 | Converting H3 cells to raster 98 | ============================= 99 | 100 | .. jupyter-execute:: 101 | 102 | import pandas as pd 103 | import pyarrow as pa 104 | from h3ronpy.pandas.raster import rasterize_cells 105 | from rasterio.plot import show 106 | 107 | df = pd.read_parquet(project_root / "data/population-841fa8bffffffff.parquet") 108 | size = 1000 109 | nodata_value = -1 110 | array, transform = rasterize_cells( 111 | pa.array(df["h3index"]), 112 | pa.array(df["pop_general"].astype("int32")), 113 | size, 114 | nodata_value=nodata_value 115 | ) 116 | 117 | show(array, cmap="viridis", transform=transform, contour=False) 118 | 119 | -------------------------------------------------------------------------------- /h3ronpy/docs/source/usage/vector.rst: -------------------------------------------------------------------------------- 1 | Converting vector data 2 | ====================== 3 | 4 | .. jupyter-execute:: 5 | 6 | from matplotlib import pyplot 7 | from pathlib import Path 8 | import os 9 | 10 | # increase the plot size 11 | pyplot.rcParams['figure.dpi'] = 120 12 | 13 | project_root = Path(os.environ["PROJECT_ROOT"]) 14 | 15 | 16 | .. jupyter-execute:: 17 | 18 | import geopandas as gpd 19 | 20 | world = gpd.read_file(project_root / "data/naturalearth_110m_admin_0_countries.fgb") 21 | africa = world[world["CONTINENT"] == "Africa"] 22 | africa.plot(column="NAME_EN") 23 | 24 | 25 | Converting a complete `GeoDataFrame` to cells 26 | --------------------------------------------- 27 | 28 | Includes building a new `GeoDataFrame` with the cell geometries using :py:func:`h3ronpy.pandas.vector.geodataframe_to_cells`: 29 | 30 | .. jupyter-execute:: 31 | 32 | from h3ronpy.pandas.vector import geodataframe_to_cells, cells_dataframe_to_geodataframe 33 | from h3ronpy import ContainmentMode 34 | 35 | df = geodataframe_to_cells(africa, 3) 36 | gdf = cells_dataframe_to_geodataframe(df) 37 | gdf.plot(column="NAME_EN") 38 | 39 | 40 | Polygon fill modes 41 | ------------------ 42 | 43 | Polygon to H3 conversion is based on centroid containment. 44 | Depending on the shape of the geometry the resulting cells may look like below: 45 | 46 | 47 | .. jupyter-execute:: 48 | 49 | namibia = africa[africa["NAME_EN"] == "Namibia"] 50 | 51 | def fill_namibia(**kw): 52 | cell_ax = cells_dataframe_to_geodataframe(geodataframe_to_cells(namibia, 3, **kw)).plot() 53 | return namibia.plot(ax=cell_ax, facecolor=(0,0,0,0), edgecolor='black') 54 | 55 | fill_namibia() 56 | 57 | The `containment_mode` argument allow the control how polygons are filled. See :py:class:`h3ronpy.ContainmentMode` for details. 58 | 59 | .. jupyter-execute:: 60 | 61 | fill_namibia(containment_mode=ContainmentMode.ContainsCentroid) 62 | 63 | .. jupyter-execute:: 64 | 65 | fill_namibia(containment_mode=ContainmentMode.ContainsBoundary) 66 | 67 | .. jupyter-execute:: 68 | 69 | fill_namibia(containment_mode=ContainmentMode.IntersectsBoundary) 70 | 71 | .. jupyter-execute:: 72 | 73 | fill_namibia(containment_mode=ContainmentMode.Covers) 74 | 75 | Merging cells into larger polygons 76 | ---------------------------------- 77 | 78 | .. jupyter-execute:: 79 | 80 | from h3ronpy.pandas.vector import cells_to_polygons, geoseries_to_cells 81 | 82 | gpd.GeoDataFrame({ 83 | "geometry": cells_to_polygons(geoseries_to_cells(namibia.geometry, 3, flatten=True), link_cells=True) 84 | }).plot() 85 | 86 | 87 | Single geometries 88 | ----------------- 89 | 90 | It is also possible to convert single `shapely` geometries or any other type providing the python `__geo_interface__`: 91 | 92 | .. jupyter-execute:: 93 | 94 | from h3ronpy.vector import geometry_to_cells 95 | 96 | namibia_geom = namibia["geometry"].iloc[0] 97 | print(namibia_geom) 98 | geometry_to_cells(namibia_geom, 3) 99 | 100 | 101 | -------------------------------------------------------------------------------- /h3ronpy/install-dev-dependencies.py: -------------------------------------------------------------------------------- 1 | # install the dependencies needed for development and ci by 2 | # collecting them from all relevant files 3 | 4 | import os 5 | import subprocess 6 | import sys 7 | from pathlib import Path 8 | 9 | 10 | def install(packages, upgrade=False): 11 | pkg_manager = os.environ.get("PKG_MANAGER") or "pip" 12 | cmd = [pkg_manager, "install"] 13 | if upgrade and pkg_manager == "pip": 14 | cmd.append("--upgrade") 15 | if packages: 16 | subprocess.run(cmd + packages, stdout=sys.stdout, stderr=sys.stderr) 17 | 18 | 19 | if __name__ == "__main__": 20 | # do not update pip - may fail on windows 21 | install(["toml", "black", "ruff"], upgrade=True) # always upgrade pip 22 | 23 | packages = [] 24 | 25 | if sys.platform == "linux": 26 | packages.append("patchelf") 27 | 28 | def harvest_deps(section, keys): 29 | for k in keys: 30 | packages.extend(section.get(k, [])) 31 | 32 | import toml # import only after it has been installed 33 | 34 | pyproject_toml = toml.load(Path(__file__).parent / "pyproject.toml") 35 | harvest_deps(pyproject_toml.get("build-system", {}), ("requires", "requires-dist")) 36 | project = pyproject_toml.get("project", {}) 37 | harvest_deps(project, ("dependencies",)) 38 | 39 | for deps in project.get("optional-dependencies", {}).values(): 40 | packages.extend(deps) 41 | 42 | pytest = pyproject_toml.get("tool", {}).get("pytest") 43 | if pytest is not None: 44 | pytest_package = "pytest" 45 | pytest_minversion = pytest.get("ini_options", {}).get("minversion") 46 | if pytest_minversion: 47 | packages.append(f"{pytest_package}>={pytest_minversion}") 48 | else: 49 | packages.append(f"{pytest_package}") 50 | 51 | install(packages) 52 | -------------------------------------------------------------------------------- /h3ronpy/justfile: -------------------------------------------------------------------------------- 1 | # to be executed via https://github.com/casey/just 2 | 3 | black: 4 | black -l 120 python tests *.py docs/source/*.py 5 | 6 | ruff: 7 | ruff check python tests *.py docs/source/*.py 8 | 9 | test: 10 | rm -f dist/*.whl 11 | maturin build --out dist 12 | pip install --force-reinstall dist/*.whl 13 | RUST_BACKTRACE=1 pytest -s -------------------------------------------------------------------------------- /h3ronpy/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["maturin>=1.7"] 3 | build-backend = "maturin" 4 | 5 | [tool.pytest.ini_options] 6 | minversion = "6.0" 7 | addopts = "--doctest-modules -v -s" 8 | testpaths = ["tests"] 9 | 10 | [tool.ruff] 11 | # Never enforce `E501` (line length violations). 12 | ignore = ["E501"] 13 | select = [ 14 | # Pyflakes 15 | "F", 16 | # Pycodestyle 17 | # "E", 18 | "W", 19 | # isort 20 | "I", 21 | ] 22 | 23 | [project] 24 | name = "h3ronpy" 25 | readme = "../README.rst" 26 | 27 | dependencies = [ 28 | "numpy>=1.24", 29 | "arro3-core>=0.4" 30 | ] 31 | classifiers = [ 32 | "Programming Language :: Python :: 3", 33 | "Topic :: Scientific/Engineering :: GIS", 34 | "License :: OSI Approved :: MIT License", 35 | ] 36 | requires-python = ">=3.9" 37 | 38 | 39 | [project.optional-dependencies] 40 | polars = ["polars>=1"] 41 | pandas = [ 42 | "geopandas>=1", 43 | "pyarrow>=15", 44 | ] 45 | test = [ 46 | "rasterio>=1.4", 47 | "Shapely>=1.7", 48 | "pytest>=6", 49 | "h3<4", 50 | "pytest-benchmark", 51 | "pyarrow>=15", 52 | ] 53 | 54 | [tool.maturin] 55 | python-source = "python" 56 | module-name = "h3ronpy.h3ronpyrs" 57 | -------------------------------------------------------------------------------- /h3ronpy/python/h3ronpy/pandas/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | API to use `h3ronpy` with the `pandas dataframe library `_ including `geopandas `_. 3 | 4 | .. warning:: 5 | 6 | To avoid pulling in unused dependencies, `h3ronpy` does not declare a dependency to `pandas` and `geopandas`. These 7 | packages need to be installed separately. 8 | 9 | """ 10 | -------------------------------------------------------------------------------- /h3ronpy/python/h3ronpy/pandas/raster.py: -------------------------------------------------------------------------------- 1 | import typing 2 | 3 | import geopandas as gpd 4 | import numpy as np 5 | import pandas as pd 6 | 7 | from .. import raster 8 | from .vector import cells_dataframe_to_geodataframe 9 | 10 | __doc__ = raster.__doc__ 11 | 12 | nearest_h3_resolution = raster.nearest_h3_resolution 13 | rasterize_cells = raster.rasterize_cells 14 | 15 | 16 | def raster_to_dataframe( 17 | in_raster: np.ndarray, 18 | transform, 19 | h3_resolution: int, 20 | nodata_value=None, 21 | axis_order: str = "yx", 22 | compact: bool = True, 23 | geo: bool = False, 24 | ) -> typing.Union[gpd.GeoDataFrame, pd.DataFrame]: 25 | """ 26 | Convert a raster/array to a pandas `DataFrame` containing H3 indexes 27 | 28 | This function is parallelized and uses the available CPUs by distributing tiles to a thread pool. 29 | 30 | The input geometry must be in WGS84. 31 | 32 | :param in_raster: input 2-d array 33 | :param transform: the affine transformation 34 | :param nodata_value: the nodata value. For these cells of the array there will be no h3 indexes generated 35 | :param axis_order: axis order of the 2d array. Either "xy" or "yx" 36 | :param h3_resolution: target h3 resolution 37 | :param compact: Return compacted h3 indexes (see H3 docs). This results in mixed H3 resolutions, but also can 38 | reduce the amount of required memory. 39 | :param geo: Return a geopandas `GeoDataFrame` with geometries. increases the memory usage. 40 | :return: pandas `DataFrame` or `GeoDataFrame` 41 | """ 42 | 43 | df = raster.raster_to_dataframe( 44 | in_raster, 45 | transform, 46 | h3_resolution, 47 | nodata_value=nodata_value, 48 | axis_order=axis_order, 49 | compact=compact, 50 | ).to_pandas() 51 | 52 | if geo: 53 | return cells_dataframe_to_geodataframe(df) 54 | else: 55 | return df 56 | 57 | 58 | def raster_to_geodataframe(*a, **kw) -> gpd.GeoDataFrame: 59 | """ 60 | convert to a geodataframe 61 | 62 | Uses the same parameters as array_to_dataframe 63 | """ 64 | kw["geo"] = True 65 | return raster_to_dataframe(*a, **kw) 66 | -------------------------------------------------------------------------------- /h3ronpy/python/h3ronpy/pandas/vector.py: -------------------------------------------------------------------------------- 1 | from functools import wraps 2 | from typing import Optional 3 | 4 | import geopandas as gpd 5 | import pandas as pd 6 | import pyarrow as pa 7 | import pyarrow.compute as pc 8 | 9 | import h3ronpy.vector as _hv 10 | from h3ronpy import DEFAULT_CELL_COLUMN_NAME, H3_CRS, ContainmentMode 11 | 12 | 13 | def _geoseries_from_wkb(func, doc: Optional[str] = None, name: Optional[str] = None): 14 | @wraps(func) 15 | def wrapper(*args, **kw): 16 | return gpd.GeoSeries.from_wkb(func(*args, **kw), crs=H3_CRS) 17 | 18 | # create a copy to avoid modifying the dict of the wrapped function 19 | wrapper.__annotations__ = dict(**wrapper.__annotations__) 20 | wrapper.__annotations__["return"] = gpd.GeoSeries 21 | if doc is not None: 22 | wrapper.__doc__ = doc 23 | if name is not None: 24 | wrapper.__name__ = name 25 | 26 | return wrapper 27 | 28 | 29 | cells_to_polygons = _geoseries_from_wkb( 30 | _hv.cells_to_wkb_polygons, 31 | doc="Create a geoseries containing the polygon geometries of a cell array", 32 | name="cells_to_polygons", 33 | ) 34 | cells_to_points = _geoseries_from_wkb( 35 | _hv.cells_to_wkb_points, 36 | doc="Create a geoseries containing the centroid point geometries of a cell array", 37 | name="cells_to_points", 38 | ) 39 | vertexes_to_points = _geoseries_from_wkb( 40 | _hv.vertexes_to_wkb_points, 41 | doc="Create a geoseries containing the point geometries of a vertex array", 42 | name="vertexes_to_points", 43 | ) 44 | directededges_to_linestrings = _geoseries_from_wkb( 45 | _hv.directededges_to_wkb_linestrings, 46 | doc="Create a geoseries containing the linestrings geometries of a directededge array", 47 | name="directededges_to_linestrings", 48 | ) 49 | 50 | 51 | @wraps(_hv.wkb_to_cells) 52 | def geoseries_to_cells(geoseries: gpd.GeoSeries, *args, **kw): 53 | return pa.array(_hv.wkb_to_cells(geoseries.to_wkb(), *args, **kw)).to_pandas() 54 | 55 | 56 | geoseries_to_cells.__name__ = "geoseries_to_cells" 57 | 58 | 59 | def cells_dataframe_to_geodataframe( 60 | df: pd.DataFrame, cell_column_name: str = DEFAULT_CELL_COLUMN_NAME 61 | ) -> gpd.GeoDataFrame: 62 | """ 63 | Convert a dataframe with a column containing cells to a geodataframe 64 | 65 | :param df: input dataframe 66 | :param cell_column_name: name of the column containing the h3 indexes 67 | :return: GeoDataFrame 68 | """ 69 | # wkb_polygons = uv.cells_to_wkb_polygons(df[cell_column_name]) 70 | # geometry = shapely.from_wkb(wkb_polygons) 71 | return gpd.GeoDataFrame(df, geometry=cells_to_polygons(df[cell_column_name]), crs=H3_CRS) 72 | 73 | 74 | def geodataframe_to_cells( 75 | gdf: gpd.GeoDataFrame, 76 | resolution: int, 77 | containment_mode: ContainmentMode = ContainmentMode.ContainsCentroid, 78 | compact: bool = False, 79 | cell_column_name: str = DEFAULT_CELL_COLUMN_NAME, 80 | ) -> pd.DataFrame: 81 | """ 82 | Convert a `GeoDataFrame` to H3 cells while exploding all other columns according to the number of cells derived 83 | from the rows geometry. 84 | 85 | The conversion of GeoDataFrames is parallelized using the available CPUs. 86 | 87 | The duplication of all non-cell columns leads to increased memory requirements. Depending on the use-case 88 | some of the more low-level conversion functions should be preferred. 89 | 90 | :param gdf: 91 | :param resolution: H3 resolution 92 | :param containment_mode: Containment mode used to decide if a cell is contained in a polygon or not. 93 | See the ContainmentMode class. 94 | :param compact: Compact the returned cells by replacing cells with their parent cells when all children 95 | of that cell are part of the set. 96 | :param cell_column_name: 97 | :return: 98 | """ 99 | cells = _hv.wkb_to_cells( 100 | gdf.geometry.to_wkb(), 101 | resolution, 102 | containment_mode=containment_mode, 103 | compact=compact, 104 | flatten=False, 105 | ) 106 | table = pa.Table.from_pandas(pd.DataFrame(gdf.drop(columns=gdf.geometry.name))).append_column( 107 | cell_column_name, cells 108 | ) 109 | return _explode_table_include_null(table, cell_column_name).to_pandas().reset_index(drop=True) 110 | 111 | 112 | # from https://issues.apache.org/jira/browse/ARROW-12099 113 | def _explode_table_include_null(table: pa.Table, column: str) -> pa.Table: 114 | other_columns = list(table.schema.names) 115 | other_columns.remove(column) 116 | indices = pc.list_parent_indices(pc.fill_null(table[column], [None])) 117 | result = table.select(other_columns) 118 | try: 119 | # may result in a large memory allocation 120 | result = result.take(indices) 121 | except pa.ArrowIndexError: 122 | # See https://github.com/nmandery/h3ronpy/issues/40 123 | # Using RuntimeWarning as ResourceWarning is often not displayed to the user. 124 | import warnings 125 | 126 | warnings.warn( 127 | "This ArrowIndexError may be a sign of the process running out of memory.", 128 | RuntimeWarning, 129 | ) 130 | raise 131 | result = result.append_column( 132 | pa.field(column, table.schema.field(column).type.value_type), 133 | pc.list_flatten(pc.fill_null(table[column], [None])), 134 | ) 135 | return result 136 | 137 | 138 | __all__ = [ 139 | cells_dataframe_to_geodataframe.__name__, 140 | geodataframe_to_cells.__name__, 141 | cells_to_polygons.__name__, 142 | cells_to_points.__name__, 143 | vertexes_to_points.__name__, 144 | directededges_to_linestrings.__name__, 145 | ] 146 | -------------------------------------------------------------------------------- /h3ronpy/python/h3ronpy/polars.py: -------------------------------------------------------------------------------- 1 | """ 2 | API to use `h3ronpy` with the `polars dataframe library `_. 3 | 4 | .. warning:: 5 | 6 | To avoid pulling in unused dependencies, `h3ronpy` does not declare a dependency to `polars`. This 7 | package needs to be installed separately. 8 | 9 | """ 10 | 11 | from __future__ import annotations 12 | 13 | import typing 14 | from functools import wraps 15 | 16 | import polars as pl 17 | from arro3.core import ChunkedArray 18 | from arro3.core.types import ArrowArrayExportable 19 | 20 | import h3ronpy 21 | 22 | 23 | # Wrapper for calling arrow-based operations on polars Series. 24 | def _wrap(func: typing.Callable[..., ArrowArrayExportable]): 25 | @wraps(func, updated=()) 26 | def wrapper(*args, **kw): 27 | # This _should_ always be a contiguous single-chunk Series already, because 28 | # we're inside map_batches. So combine_chunks should be free. 29 | ca = ChunkedArray.from_arrow(args[0]) 30 | array = ca.combine_chunks() 31 | new_args = list(args) 32 | new_args[0] = array 33 | result = func(*new_args, **kw) 34 | return pl.Series(result) 35 | 36 | return wrapper 37 | 38 | 39 | @pl.api.register_expr_namespace("h3") 40 | class H3Expr: 41 | """ 42 | Registers H3 functionality with polars Expr expressions. 43 | 44 | The methods of this class mirror the functionality provided by the functions of this 45 | module. Please refer to the module functions for more documentation. 46 | """ 47 | 48 | def __init__(self, expr: pl.Expr): 49 | self._expr = expr 50 | 51 | def __expr_map_series(self, func: typing.Callable[..., ArrowArrayExportable]) -> pl.Expr: 52 | wrapped_func = _wrap(func) 53 | 54 | if hasattr(self._expr, "map"): 55 | # polars < 1.0 56 | return self._expr.map(wrapped_func) 57 | 58 | return self._expr.map_batches(wrapped_func) 59 | 60 | def cells_resolution(self) -> pl.Expr: 61 | return self.__expr_map_series(h3ronpy.cells_resolution).alias("resolution") 62 | 63 | def change_resolution(self, resolution: int) -> pl.Expr: 64 | return self.__expr_map_series(lambda s: h3ronpy.change_resolution(s, resolution)) 65 | 66 | def change_resolution_list(self, resolution: int) -> pl.Expr: 67 | return self.__expr_map_series(lambda s: h3ronpy.change_resolution_list(s, resolution)) 68 | 69 | def cells_parse(self, set_failing_to_invalid: bool = False) -> pl.Expr: 70 | return self.__expr_map_series( 71 | lambda s: h3ronpy.cells_parse(s, set_failing_to_invalid=set_failing_to_invalid) 72 | ).alias("cell") 73 | 74 | def vertexes_parse(self, set_failing_to_invalid: bool = False) -> pl.Expr: 75 | return self.__expr_map_series( 76 | lambda s: h3ronpy.vertexes_parse(s, set_failing_to_invalid=set_failing_to_invalid) 77 | ).alias("vertex") 78 | 79 | def directededges_parse(self, set_failing_to_invalid: bool = False) -> pl.Expr: 80 | return self.__expr_map_series( 81 | lambda s: h3ronpy.directededges_parse(s, set_failing_to_invalid=set_failing_to_invalid) 82 | ).alias("directededge") 83 | 84 | def grid_disk(self, k: int, flatten: bool = False) -> pl.Expr: 85 | return self.__expr_map_series(lambda s: h3ronpy.grid_disk(s, k, flatten=flatten)) 86 | 87 | def compact(self, mixed_resolutions: bool = False) -> pl.Expr: 88 | return self.__expr_map_series(lambda s: h3ronpy.compact(s, mixed_resolutions=mixed_resolutions)) 89 | 90 | def uncompact(self, target_resolution: int) -> pl.Expr: 91 | return self.__expr_map_series(lambda s: h3ronpy.uncompact(s, target_resolution)) 92 | 93 | def cells_area_m2(self) -> pl.Expr: 94 | return self.__expr_map_series(h3ronpy.cells_area_m2).alias("area_m2") 95 | 96 | def cells_area_km2(self) -> pl.Expr: 97 | return self.__expr_map_series(h3ronpy.cells_area_km2).alias("area_km2") 98 | 99 | def cells_area_rads2(self) -> pl.Expr: 100 | return self.__expr_map_series(h3ronpy.cells_area_rads2).alias("area_rads2") 101 | 102 | def cells_valid(self) -> pl.Expr: 103 | return self.__expr_map_series(h3ronpy.cells_valid).alias("cells_valid") 104 | 105 | def vertexes_valid(self) -> pl.Expr: 106 | return self.__expr_map_series(h3ronpy.vertexes_valid).alias("vertexes_valid") 107 | 108 | def directededges_valid(self) -> pl.Expr: 109 | return self.__expr_map_series(h3ronpy.directededges_valid).alias("directededges_valid") 110 | 111 | def cells_to_string(self) -> pl.Expr: 112 | return self.__expr_map_series(h3ronpy.cells_to_string) 113 | 114 | def vertexes_to_string(self) -> pl.Expr: 115 | return self.__expr_map_series(h3ronpy.vertexes_to_string) 116 | 117 | def directededges_to_string(self) -> pl.Expr: 118 | return self.__expr_map_series(h3ronpy.directededges_to_string) 119 | 120 | 121 | @pl.api.register_series_namespace("h3") 122 | class H3SeriesShortcuts: 123 | """ 124 | Registers H3 functionality with polars Series. 125 | 126 | The methods of this class mirror the functionality provided by the functions of this 127 | module. Please refer to the module functions for more documentation. 128 | """ 129 | 130 | def __init__(self, s: pl.Series): 131 | self._s = s 132 | 133 | def cells_resolution(self) -> pl.Series: 134 | return _wrap(h3ronpy.cells_resolution)(self._s) 135 | 136 | def change_resolution(self, resolution: int) -> pl.Series: 137 | return _wrap(h3ronpy.change_resolution)(self._s, resolution) 138 | 139 | def change_resolution_list(self, resolution: int) -> pl.Series: 140 | return _wrap(h3ronpy.change_resolution_list)(self._s, resolution) 141 | 142 | def cells_parse(self, set_failing_to_invalid: bool = False) -> pl.Series: 143 | return _wrap(h3ronpy.cells_parse)(self._s, set_failing_to_invalid=set_failing_to_invalid) 144 | 145 | def vertexes_parse(self, set_failing_to_invalid: bool = False) -> pl.Series: 146 | return _wrap(h3ronpy.vertexes_parse)(self._s, set_failing_to_invalid=set_failing_to_invalid) 147 | 148 | def directededges_parse(self, set_failing_to_invalid: bool = False) -> pl.Series: 149 | return _wrap(h3ronpy.directededges_parse)(self._s, set_failing_to_invalid=set_failing_to_invalid) 150 | 151 | def grid_disk(self, k: int, flatten: bool = False) -> pl.Series: 152 | return _wrap(h3ronpy.grid_disk)(self._s, k, flatten=flatten) 153 | 154 | def compact(self, mixed_resolutions: bool = False) -> pl.Series: 155 | return _wrap(h3ronpy.compact)(self._s, mixed_resolutions=mixed_resolutions) 156 | 157 | def uncompact(self, target_resolution: int) -> pl.Series: 158 | return _wrap(h3ronpy.uncompact)(self._s, target_resolution) 159 | 160 | def cells_area_m2(self) -> pl.Series: 161 | return _wrap(h3ronpy.cells_area_m2)(self._s) 162 | 163 | def cells_area_km2(self) -> pl.Series: 164 | return _wrap(h3ronpy.cells_area_km2)(self._s) 165 | 166 | def cells_area_rads2(self) -> pl.Series: 167 | return _wrap(h3ronpy.cells_area_rads2)(self._s) 168 | 169 | def cells_valid(self) -> pl.Series: 170 | return _wrap(h3ronpy.cells_valid)(self._s) 171 | 172 | def vertexes_valid(self) -> pl.Series: 173 | return _wrap(h3ronpy.vertexes_valid)(self._s) 174 | 175 | def directededges_valid(self) -> pl.Series: 176 | return _wrap(h3ronpy.directededges_valid)(self._s) 177 | 178 | def cells_to_string(self) -> pl.Series: 179 | return _wrap(h3ronpy.cells_to_string)(self._s) 180 | 181 | def vertexes_to_string(self) -> pl.Series: 182 | return _wrap(h3ronpy.vertexes_to_string)(self._s) 183 | 184 | def directededges_to_string(self) -> pl.Series: 185 | return _wrap(h3ronpy.directededges_to_string)(self._s) 186 | 187 | 188 | __all__ = [ 189 | H3Expr.__name__, 190 | H3SeriesShortcuts.__name__, 191 | ] 192 | -------------------------------------------------------------------------------- /h3ronpy/python/h3ronpy/raster.py: -------------------------------------------------------------------------------- 1 | """ 2 | Conversion of 2D `numpy` arrays to H3 cells. 3 | 4 | The geo-context is passed to this library using a coordinate transformation matrix - this can be either 5 | a `GDAL-like array `_ of six float values, or a 6 | `Affine `_-object as used by `rasterio`. 7 | 8 | .. note:: 9 | 10 | As H3 itself used WGS84 (EPSG:4326) Lat/Lon coordinates, the coordinate transformation matrix used in this module 11 | must be based on WGS84 as well. Raster data using other coordinate systems need to be reprojected accordingly. 12 | 13 | 14 | While H3 cells are hexagons and pentagons, this raster conversion process only takes the raster value under the centroid 15 | of the cell into account. When the data shall be aggregated, use any of these methods: 16 | 17 | 1. Make use the `nearest_h3_resolution` function to convert to the H3 resolution nearest to the pixel size of the raster. 18 | After that the cell resolution can be changed using the `change_resolution` function and dataframe libraries can be used to 19 | perform the desired aggregations. This can be a rather memory-intensive process. 20 | 21 | 2. Scale the raster down using an interpolation algorithm. After that use method 1. This can save a lot of memory, but may 22 | not be applicable to all datasets - for example dataset with absolute values per pixel like population counts. 23 | 24 | Resolution search modes of `nearest_h3_resolution`: 25 | 26 | * "min_diff": chose the H3 resolution where the difference in the area of a pixel and the h3index is as small as possible. 27 | * "smaller_than_pixel": chose the H3 resolution where the area of the h3index is smaller than the area of a pixel. 28 | 29 | """ 30 | 31 | import typing 32 | 33 | import numpy as np 34 | import pyarrow as pa 35 | 36 | from h3ronpy import DEFAULT_CELL_COLUMN_NAME, _to_arrow_array, _to_uint64_array 37 | from h3ronpy.h3ronpyrs import raster 38 | from h3ronpy.vector import cells_bounds, cells_to_wkb_polygons 39 | 40 | try: 41 | # affine library is used by rasterio 42 | import affine 43 | 44 | __HAS_AFFINE_LIB = True 45 | except ImportError: 46 | __HAS_AFFINE_LIB = False 47 | 48 | Transform = raster.Transform 49 | 50 | 51 | def _get_transform(t): 52 | if isinstance(t, Transform): 53 | return t 54 | if __HAS_AFFINE_LIB: 55 | if isinstance(t, affine.Affine): 56 | return Transform.from_rasterio([t.a, t.b, t.c, t.d, t.e, t.f]) 57 | if type(t) in (list, tuple) and len(t) == 6: 58 | # probably native gdal 59 | return Transform.from_gdal(t) 60 | raise ValueError("unsupported object for transform") 61 | 62 | 63 | def nearest_h3_resolution(shape, transform, axis_order="yx", search_mode="min_diff") -> int: 64 | """ 65 | Find the H3 resolution closest to the size of a pixel in an array 66 | of the given shape with the given transform 67 | 68 | :param shape: dimensions of the 2d array 69 | :param transform: the affine transformation 70 | :param axis_order: axis order of the 2d array. Either "xy" or "yx" 71 | :param search_mode: resolution search mode (see documentation of this module) 72 | :return: 73 | """ 74 | return raster.nearest_h3_resolution(shape, _get_transform(transform), axis_order, search_mode) 75 | 76 | 77 | def raster_to_dataframe( 78 | in_raster: np.ndarray, 79 | transform, 80 | h3_resolution: int, 81 | nodata_value=None, 82 | axis_order: str = "yx", 83 | compact: bool = True, 84 | ) -> pa.Table: 85 | """ 86 | Convert a raster/array to a pandas `DataFrame` containing H3 cell indexes 87 | 88 | This function is parallelized and uses the available CPUs by distributing tiles to a thread pool. 89 | 90 | The input geometry must be in WGS84. 91 | 92 | :param in_raster: Input 2D array 93 | :param transform: The affine transformation 94 | :param nodata_value: The nodata value. For these cells of the array there will be no h3 indexes generated 95 | :param axis_order: Axis order of the 2d array. Either "xy" or "yx" 96 | :param h3_resolution: Target h3 resolution 97 | :param compact: Return compacted h3 indexes (see H3 docs). This results in mixed H3 resolutions, but also can 98 | reduce the amount of required memory. 99 | :return: Tuple of arrow arrays 100 | """ 101 | 102 | dtype = in_raster.dtype 103 | func = None 104 | if dtype == np.uint8: 105 | func = raster.raster_to_h3_u8 106 | elif dtype == np.int8: 107 | func = raster.raster_to_h3_i8 108 | elif dtype == np.uint16: 109 | func = raster.raster_to_h3_u16 110 | elif dtype == np.int16: 111 | func = raster.raster_to_h3_i16 112 | elif dtype == np.uint32: 113 | func = raster.raster_to_h3_u32 114 | elif dtype == np.int32: 115 | func = raster.raster_to_h3_i32 116 | elif dtype == np.uint64: 117 | func = raster.raster_to_h3_u64 118 | elif dtype == np.int64: 119 | func = raster.raster_to_h3_i64 120 | elif dtype == np.float32: 121 | func = raster.raster_to_h3_f32 122 | elif dtype == np.float64: 123 | func = raster.raster_to_h3_f64 124 | else: 125 | raise NotImplementedError(f"no raster_to_h3 implementation for dtype {dtype.name}") 126 | 127 | return pa.Table.from_arrays( 128 | arrays=func( 129 | in_raster, 130 | _get_transform(transform), 131 | h3_resolution, 132 | axis_order, 133 | compact, 134 | nodata_value, 135 | ), 136 | names=["value", DEFAULT_CELL_COLUMN_NAME], 137 | ) 138 | 139 | 140 | def rasterize_cells( 141 | cells, values, size: typing.Union[int, typing.Tuple[int, int]], nodata_value=0 142 | ) -> typing.Tuple[np.ndarray, typing.Tuple[float, float, float, float, float, float]]: 143 | """ 144 | Generate a raster numpy array from arrays of cells and values. 145 | 146 | This function requires the ``rasterio`` and `shapely` libraries to be installed. 147 | 148 | :param cells: array with H3 cells 149 | :param values: array with the values which shall be written into the raster 150 | :param size: The desired output size of the raster. Maybe a tuple of ints (width, height) or a single int. In case 151 | of the latter, the other dimension is interpolated from the bounds of the input data. 152 | :param nodata_value: The nodata value for the output array 153 | :return: 2D numpy array typed accordingly to the passed in values array, and the geotransform (WGS84 coordinate 154 | system, ordering used by the affine library and rasterio) 155 | """ 156 | import shapely 157 | from rasterio.features import rasterize 158 | from rasterio.transform import from_bounds 159 | 160 | cells = _to_uint64_array(cells) 161 | values = _to_arrow_array(values, None) 162 | 163 | if len(cells) != len(values): 164 | raise ValueError("length of cells and values array needs to be equal") 165 | bounds = cells_bounds(cells) 166 | 167 | if bounds is None: 168 | # no contents, nothing to rasterize 169 | return None, None 170 | 171 | if type(size) == int: 172 | (minx, miny, maxx, maxy) = bounds 173 | size = (size, int(float(size) / (maxx - minx) * (maxy - miny))) 174 | 175 | transform = from_bounds(*bounds, *size) 176 | 177 | # reduce the number of features to loop over by grouping by value 178 | # https://arrow.apache.org/docs/python/generated/pyarrow.TableGroupBy.html 179 | grouped = ( 180 | pa.Table.from_arrays([cells, values], names=["cells", "values"]) 181 | .group_by("values") 182 | .aggregate( 183 | [ 184 | ("cells", "hash_distinct"), 185 | ] 186 | ) 187 | ) 188 | 189 | # drop any unused references to free some memory 190 | del cells 191 | del values 192 | 193 | values_array = grouped["values"].to_numpy() 194 | rasterized = np.full(size, nodata_value, dtype=values_array.dtype) 195 | 196 | for cells, value in zip(grouped["cells_distinct"], grouped["values"]): 197 | value = value.as_py() 198 | 199 | # linking cells should speed up rendering in case of large homogenous areas 200 | polygons = pa.array(cells_to_wkb_polygons(pa.array(cells), link_cells=True)) 201 | polygons = [shapely.from_wkb(polygon.as_py()) for polygon in polygons.filter(polygons.is_valid())] 202 | 203 | # draw 204 | rasterize( 205 | polygons, 206 | out_shape=size, 207 | out=rasterized, 208 | default_value=value, 209 | transform=transform, 210 | all_touched=False, 211 | ) 212 | 213 | return rasterized, transform 214 | -------------------------------------------------------------------------------- /h3ronpy/python/h3ronpy/vector.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, Tuple 2 | 3 | from arro3.core import Array, DataType, RecordBatch 4 | 5 | from h3ronpy import ContainmentMode 6 | 7 | from . import _to_arrow_array, _to_uint64_array 8 | from .h3ronpyrs import vector 9 | 10 | 11 | def cells_to_coordinates(arr, radians: bool = False) -> RecordBatch: 12 | """ 13 | convert to point coordinates in degrees 14 | """ 15 | return vector.cells_to_coordinates(_to_uint64_array(arr), radians=radians) 16 | 17 | 18 | def coordinates_to_cells(latarray, lngarray, resarray, radians: bool = False) -> Array: 19 | """ 20 | Convert coordinates arrays to cells. 21 | 22 | :param latarray: array of lat values 23 | :param lngarray: array of lng values 24 | :param resarray: Either an array of resolutions or a single resolution as an integer to apply to all coordinates. 25 | :param radians: Set to True to pass `lat` and `lng` in radians 26 | :return: cell array 27 | """ 28 | if type(resarray) in (int, float): 29 | res = int(resarray) 30 | else: 31 | res = _to_arrow_array(resarray, DataType.uint8()) 32 | return vector.coordinates_to_cells( 33 | _to_arrow_array(latarray, DataType.float64()), 34 | _to_arrow_array(lngarray, DataType.float64()), 35 | res, 36 | radians=radians, 37 | ) 38 | 39 | 40 | def cells_bounds(arr) -> Optional[Tuple]: 41 | """ 42 | Bounds of the complete array as a tuple `(minx, miny, maxx, maxy)`. 43 | """ 44 | return vector.cells_bounds(_to_uint64_array(arr)) 45 | 46 | 47 | def cells_bounds_arrays(arr) -> RecordBatch: 48 | """ 49 | Build a table/dataframe with the columns `minx`, `miny`, `maxx` and `maxy` containing the bounds of the individual 50 | cells from the input array. 51 | """ 52 | return vector.cells_bounds_arrays(_to_uint64_array(arr)) 53 | 54 | 55 | def cells_to_wkb_polygons(arr, radians: bool = False, link_cells: bool = False) -> Array: 56 | """ 57 | Convert cells to polygons. 58 | 59 | The returned geometries in the output array will match the order of the input array - unless ``link_cells`` 60 | is set to True. 61 | 62 | :param: arr: The cell array 63 | :param radians: Generate geometries using radians instead of degrees 64 | :param link_cells: Combine neighboring cells into a single polygon geometry. All cell indexes must have the same resolution. 65 | """ 66 | return vector.cells_to_wkb_polygons(_to_uint64_array(arr), radians=radians, link_cells=link_cells) 67 | 68 | 69 | def cells_to_wkb_points(arr, radians: bool = False) -> Array: 70 | """ 71 | Convert cells to points using their centroids. 72 | 73 | The returned geometries in the output array will match the order of the input array. 74 | 75 | :param: arr: The cell array 76 | :param radians: Generate geometries using radians instead of degrees 77 | """ 78 | return vector.cells_to_wkb_points(_to_uint64_array(arr), radians=radians) 79 | 80 | 81 | def vertexes_to_wkb_points(arr, radians: bool = False) -> Array: 82 | """ 83 | Convert vertexes to points. 84 | 85 | The returned geometries in the output array will match the order of the input array. 86 | 87 | :param: arr: The vertex array 88 | :param radians: Generate geometries using radians instead of degrees 89 | """ 90 | return vector.vertexes_to_wkb_points(_to_uint64_array(arr), radians=radians) 91 | 92 | 93 | def directededges_to_wkb_linestrings(arr, radians: bool = False) -> Array: 94 | """ 95 | Convert directed edges to linestrings. 96 | 97 | The returned geometries in the output array will match the order of the input array. 98 | 99 | :param: arr: The directed edge array 100 | :param radians: Generate geometries using radians instead of degrees 101 | """ 102 | return vector.directededges_to_wkb_linestrings(_to_uint64_array(arr), radians=radians) 103 | 104 | 105 | def wkb_to_cells( 106 | arr, 107 | resolution: int, 108 | containment_mode: ContainmentMode = ContainmentMode.ContainsCentroid, 109 | compact: bool = False, 110 | flatten: bool = False, 111 | ) -> Array: 112 | """ 113 | Convert a Series/Array/List of WKB values to H3 cells. 114 | 115 | Unless ``flatten`` is set to True a list array will be returned, with the cells generated from a geometry being 116 | located at the same position as the geometry in the input array. 117 | 118 | :param arr: The input array. 119 | :param resolution: H3 resolution 120 | :param containment_mode: Containment mode used to decide if a cell is contained in a polygon or not. 121 | See the ContainmentMode class. 122 | :param compact: Compact the returned cells by replacing cells with their parent cells when all children 123 | of that cell are part of the set. 124 | :param flatten: Return a non-nested cell array instead of a list array. 125 | """ 126 | arr = _to_arrow_array(arr, DataType.binary()) 127 | return vector.wkb_to_cells( 128 | arr, 129 | resolution, 130 | containment_mode=containment_mode, 131 | compact=compact, 132 | flatten=flatten, 133 | ) 134 | 135 | 136 | def geometry_to_cells( 137 | geom, 138 | resolution: int, 139 | containment_mode: ContainmentMode = ContainmentMode.ContainsCentroid, 140 | compact: bool = False, 141 | ) -> Array: 142 | """ 143 | Convert a single object which supports the python `__geo_interface__` protocol to H3 cells 144 | 145 | :param geom: geometry 146 | :param resolution: H3 resolution 147 | :param containment_mode: Containment mode used to decide if a cell is contained in a polygon or not. 148 | See the ContainmentMode class. 149 | :param compact: Compact the returned cells by replacing cells with their parent cells when all children 150 | of that cell are part of the set. 151 | """ 152 | return vector.geometry_to_cells(geom, resolution, containment_mode=containment_mode, compact=compact) 153 | 154 | 155 | __all__ = [ 156 | cells_to_coordinates.__name__, 157 | coordinates_to_cells.__name__, 158 | cells_bounds.__name__, 159 | cells_bounds_arrays.__name__, 160 | cells_to_wkb_polygons.__name__, 161 | cells_to_wkb_points.__name__, 162 | vertexes_to_wkb_points.__name__, 163 | directededges_to_wkb_linestrings.__name__, 164 | wkb_to_cells.__name__, 165 | geometry_to_cells.__name__, 166 | ] 167 | -------------------------------------------------------------------------------- /h3ronpy/src/array.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use arrow::datatypes::{DataType, Field}; 4 | use h3arrow::array::{CellIndexArray, DirectedEdgeIndexArray, VertexIndexArray}; 5 | use pyo3::prelude::*; 6 | use pyo3::types::{PyCapsule, PyTuple}; 7 | use pyo3_arrow::ffi::to_array_pycapsules; 8 | 9 | use crate::arrow_interop::{ 10 | pyarray_to_cellindexarray, pyarray_to_directededgeindexarray, pyarray_to_vertexindexarray, 11 | }; 12 | use crate::resolution::PyResolution; 13 | 14 | #[pyclass(name = "CellArray")] 15 | pub struct PyCellArray(CellIndexArray); 16 | 17 | impl PyCellArray { 18 | pub fn into_inner(self) -> CellIndexArray { 19 | self.0 20 | } 21 | } 22 | 23 | #[pymethods] 24 | impl PyCellArray { 25 | #[pyo3(signature = (requested_schema = None))] 26 | fn __arrow_c_array__<'py>( 27 | &'py self, 28 | py: Python<'py>, 29 | requested_schema: Option>, 30 | ) -> PyResult> { 31 | let array = self.0.primitive_array(); 32 | let field = Arc::new(Field::new("", DataType::UInt64, true)); 33 | Ok(to_array_pycapsules(py, field, array, requested_schema)?) 34 | } 35 | 36 | fn __len__(&self) -> usize { 37 | self.0.len() 38 | } 39 | 40 | fn parent(&self, resolution: PyResolution) -> Self { 41 | Self(self.0.parent(resolution.into())) 42 | } 43 | 44 | fn slice(&self, offset: usize, length: usize) -> Self { 45 | Self(self.0.slice(offset, length)) 46 | } 47 | } 48 | 49 | impl AsRef for PyCellArray { 50 | fn as_ref(&self) -> &CellIndexArray { 51 | &self.0 52 | } 53 | } 54 | 55 | impl<'py> FromPyObject<'py> for PyCellArray { 56 | fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult { 57 | Ok(Self(pyarray_to_cellindexarray(ob)?)) 58 | } 59 | } 60 | 61 | #[pyclass(name = "DirectedEdgeArray")] 62 | pub struct PyDirectedEdgeArray(DirectedEdgeIndexArray); 63 | 64 | #[pymethods] 65 | impl PyDirectedEdgeArray { 66 | #[pyo3(signature = (requested_schema = None))] 67 | fn __arrow_c_array__<'py>( 68 | &'py self, 69 | py: Python<'py>, 70 | requested_schema: Option>, 71 | ) -> PyResult> { 72 | let array = self.0.primitive_array(); 73 | let field = Arc::new(Field::new("", DataType::UInt64, true)); 74 | Ok(to_array_pycapsules(py, field, array, requested_schema)?) 75 | } 76 | 77 | fn __len__(&self) -> usize { 78 | self.0.len() 79 | } 80 | 81 | pub fn origin(&self) -> PyCellArray { 82 | PyCellArray(self.0.origin()) 83 | } 84 | 85 | pub fn destination(&self) -> PyCellArray { 86 | PyCellArray(self.0.destination()) 87 | } 88 | 89 | fn slice(&self, offset: usize, length: usize) -> Self { 90 | Self(self.0.slice(offset, length)) 91 | } 92 | } 93 | 94 | impl AsRef for PyDirectedEdgeArray { 95 | fn as_ref(&self) -> &DirectedEdgeIndexArray { 96 | &self.0 97 | } 98 | } 99 | 100 | impl<'py> FromPyObject<'py> for PyDirectedEdgeArray { 101 | fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult { 102 | Ok(Self(pyarray_to_directededgeindexarray(ob)?)) 103 | } 104 | } 105 | 106 | #[pyclass(name = "VertexArray")] 107 | pub struct PyVertexArray(VertexIndexArray); 108 | 109 | #[pymethods] 110 | impl PyVertexArray { 111 | #[pyo3(signature = (requested_schema = None))] 112 | fn __arrow_c_array__<'py>( 113 | &'py self, 114 | py: Python<'py>, 115 | requested_schema: Option>, 116 | ) -> PyResult> { 117 | let array = self.0.primitive_array(); 118 | let field = Arc::new(Field::new("", DataType::UInt64, true)); 119 | Ok(to_array_pycapsules(py, field, array, requested_schema)?) 120 | } 121 | 122 | fn __len__(&self) -> usize { 123 | self.0.len() 124 | } 125 | 126 | pub fn owner(&self) -> PyCellArray { 127 | PyCellArray(self.0.owner()) 128 | } 129 | 130 | fn slice(&self, offset: usize, length: usize) -> Self { 131 | Self(self.0.slice(offset, length)) 132 | } 133 | } 134 | 135 | impl AsRef for PyVertexArray { 136 | fn as_ref(&self) -> &VertexIndexArray { 137 | &self.0 138 | } 139 | } 140 | 141 | impl<'py> FromPyObject<'py> for PyVertexArray { 142 | fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult { 143 | Ok(Self(pyarray_to_vertexindexarray(ob)?)) 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /h3ronpy/src/arrow_interop.rs: -------------------------------------------------------------------------------- 1 | use arrow::array::{Array, UInt64Array}; 2 | use pyo3_arrow::PyArray; 3 | use std::any::{type_name, Any}; 4 | use std::sync::Arc; 5 | 6 | use h3arrow::array::{ 7 | CellIndexArray, DirectedEdgeIndexArray, H3Array, H3IndexArrayValue, VertexIndexArray, 8 | }; 9 | use pyo3::exceptions::PyValueError; 10 | use pyo3::prelude::*; 11 | use pyo3::Python; 12 | 13 | use crate::error::{IntoPyErr, IntoPyResult}; 14 | 15 | #[inline] 16 | pub fn h3array_to_pyarray(h3array: H3Array, py: Python) -> PyResult 17 | where 18 | IX: H3IndexArrayValue, 19 | { 20 | let pa: UInt64Array = h3array.into(); 21 | PyArray::from_array_ref(Arc::new(pa)).to_arro3(py) 22 | } 23 | 24 | pub(crate) fn pyarray_to_native(obj: &Bound) -> PyResult { 25 | let array = obj.extract::()?; 26 | let (array, _field) = array.into_inner(); 27 | 28 | let array = array 29 | .as_any() 30 | .downcast_ref::() 31 | .ok_or_else(|| { 32 | PyValueError::new_err(format!( 33 | "Expected {}, found arrow array of type {:?}", 34 | type_name::(), 35 | array.data_type(), 36 | )) 37 | })? 38 | .clone(); 39 | 40 | // downcast to the concrete type 41 | Ok(array) 42 | } 43 | 44 | pub(crate) fn pyarray_to_cellindexarray(obj: &Bound) -> PyResult { 45 | pyarray_to_h3array::(obj) 46 | } 47 | 48 | pub(crate) fn pyarray_to_vertexindexarray(obj: &Bound) -> PyResult { 49 | pyarray_to_h3array::(obj) 50 | } 51 | 52 | pub(crate) fn pyarray_to_directededgeindexarray( 53 | obj: &Bound, 54 | ) -> PyResult { 55 | pyarray_to_h3array::(obj) 56 | } 57 | 58 | pub(crate) fn pyarray_to_uint64array(obj: &Bound) -> PyResult { 59 | pyarray_to_native::(obj) 60 | } 61 | 62 | #[inline] 63 | fn pyarray_to_h3array(obj: &Bound) -> PyResult 64 | where 65 | T: TryFrom, 66 | >::Error: IntoPyErr, 67 | { 68 | T::try_from(pyarray_to_uint64array(obj)?).into_pyresult() 69 | } 70 | -------------------------------------------------------------------------------- /h3ronpy/src/error.rs: -------------------------------------------------------------------------------- 1 | use h3arrow::error::Error as A3Error; 2 | use pyo3::exceptions::{PyIOError, PyRuntimeError, PyValueError}; 3 | use pyo3::{PyErr, PyResult}; 4 | use rasterh3::Error; 5 | 6 | pub trait IntoPyResult { 7 | fn into_pyresult(self) -> PyResult; 8 | } 9 | 10 | pub trait IntoPyErr { 11 | fn into_pyerr(self) -> PyErr; 12 | } 13 | 14 | impl IntoPyErr for h3arrow::export::arrow::error::ArrowError { 15 | fn into_pyerr(self) -> PyErr { 16 | PyRuntimeError::new_err(self.to_string()) 17 | } 18 | } 19 | 20 | impl IntoPyErr for A3Error { 21 | fn into_pyerr(self) -> PyErr { 22 | match self { 23 | A3Error::InvalidCellIndex(e) => e.into_pyerr(), 24 | A3Error::InvalidVertexIndex(e) => e.into_pyerr(), 25 | A3Error::InvalidDirectedEdgeIndex(e) => e.into_pyerr(), 26 | A3Error::InvalidResolution(e) => e.into_pyerr(), 27 | A3Error::InvalidLatLng(e) => e.into_pyerr(), 28 | A3Error::InvalidGeometry(e) => e.into_pyerr(), 29 | A3Error::CompactionError(e) => e.into_pyerr(), 30 | A3Error::DissolutionError(e) => e.into_pyerr(), 31 | A3Error::PlotterError(e) => e.into_pyerr(), 32 | A3Error::LocalIJError(e) => e.into_pyerr(), 33 | A3Error::Arrow2(e) => e.into_pyerr(), 34 | A3Error::NotAUint64Array 35 | | A3Error::NonParsableCellIndex 36 | | A3Error::NonParsableDirectedEdgeIndex 37 | | A3Error::NonParsableVertexIndex 38 | | A3Error::LengthMismatch 39 | | A3Error::InvalidWKB => PyValueError::new_err(self.to_string()), 40 | A3Error::IO(e) => e.into_pyerr(), 41 | } 42 | } 43 | } 44 | 45 | macro_rules! impl_h3o_value_err { 46 | ($($err_type:ty,)*) => { 47 | $( 48 | impl IntoPyErr for $err_type { 49 | fn into_pyerr(self) -> PyErr { 50 | PyValueError::new_err( 51 | self.to_string() 52 | ) 53 | } 54 | } 55 | )* 56 | } 57 | } 58 | 59 | impl_h3o_value_err!( 60 | h3arrow::export::h3o::error::CompactionError, 61 | h3arrow::export::h3o::error::InvalidCellIndex, 62 | h3arrow::export::h3o::error::InvalidDirectedEdgeIndex, 63 | h3arrow::export::h3o::error::InvalidGeometry, 64 | h3arrow::export::h3o::error::InvalidLatLng, 65 | h3arrow::export::h3o::error::InvalidResolution, 66 | h3arrow::export::h3o::error::InvalidVertexIndex, 67 | h3arrow::export::h3o::error::DissolutionError, 68 | h3arrow::export::h3o::error::PlotterError, 69 | h3arrow::export::h3o::error::LocalIjError, 70 | ); 71 | 72 | impl IntoPyErr for rasterh3::Error { 73 | fn into_pyerr(self) -> PyErr { 74 | match self { 75 | Error::TransformNotInvertible | Error::EmptyArray => { 76 | PyValueError::new_err(self.to_string()) 77 | } 78 | Error::InvalidLatLng(e) => e.into_pyerr(), 79 | Error::InvalidGeometry(e) => e.into_pyerr(), 80 | Error::InvalidResolution(e) => e.into_pyerr(), 81 | Error::CompactionError(e) => e.into_pyerr(), 82 | } 83 | } 84 | } 85 | 86 | impl IntoPyErr for std::io::Error { 87 | fn into_pyerr(self) -> PyErr { 88 | PyIOError::new_err(self.to_string()) 89 | } 90 | } 91 | 92 | impl IntoPyResult for Result 93 | where 94 | E: IntoPyErr, 95 | { 96 | fn into_pyresult(self) -> PyResult { 97 | self.map_err(IntoPyErr::into_pyerr) 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /h3ronpy/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![warn( 2 | clippy::all, 3 | clippy::correctness, 4 | clippy::suspicious, 5 | clippy::style, 6 | clippy::complexity, 7 | clippy::perf, 8 | nonstandard_style 9 | )] 10 | 11 | use pyo3::{prelude::*, wrap_pyfunction, Python}; 12 | 13 | use crate::op::init_op_submodule; 14 | use crate::raster::init_raster_submodule; 15 | use crate::vector::{init_vector_submodule, PyContainmentMode}; 16 | 17 | mod array; 18 | mod arrow_interop; 19 | mod error; 20 | mod op; 21 | mod raster; 22 | mod resolution; 23 | mod transform; 24 | mod vector; 25 | 26 | pub(crate) const DEFAULT_CELL_COLUMN_NAME: &str = "cell"; 27 | 28 | /// version of the module 29 | #[pyfunction] 30 | fn version() -> String { 31 | env!("CARGO_PKG_VERSION").to_string() 32 | } 33 | 34 | /// indicates if this extension has been compiled in release-mode 35 | #[pyfunction] 36 | fn is_release_build() -> bool { 37 | #[cfg(debug_assertions)] 38 | return false; 39 | 40 | #[cfg(not(debug_assertions))] 41 | return true; 42 | } 43 | 44 | #[pymodule] 45 | fn h3ronpyrs(py: Python<'_>, m: &Bound) -> PyResult<()> { 46 | env_logger::init(); // run with the environment variable RUST_LOG set to "debug" for log output 47 | 48 | m.add_class::()?; 49 | m.add_function(wrap_pyfunction!(version, m)?)?; 50 | m.add_function(wrap_pyfunction!(is_release_build, m)?)?; 51 | 52 | let raster_submod = PyModule::new_bound(py, "raster")?; 53 | init_raster_submodule(&raster_submod)?; 54 | m.add_submodule(&raster_submod)?; 55 | 56 | let op_submod = PyModule::new_bound(py, "op")?; 57 | init_op_submodule(&op_submod)?; 58 | m.add_submodule(&op_submod)?; 59 | 60 | let vector_submod = PyModule::new_bound(py, "vector")?; 61 | init_vector_submodule(&vector_submod)?; 62 | m.add_submodule(&vector_submod)?; 63 | 64 | m.add("DEFAULT_CELL_COLUMN_NAME", DEFAULT_CELL_COLUMN_NAME)?; 65 | 66 | Ok(()) 67 | } 68 | -------------------------------------------------------------------------------- /h3ronpy/src/op/compact.rs: -------------------------------------------------------------------------------- 1 | use h3arrow::algorithm::CompactOp; 2 | use h3arrow::export::h3o::Resolution; 3 | use pyo3::prelude::*; 4 | 5 | use crate::array::PyCellArray; 6 | use crate::arrow_interop::*; 7 | use crate::error::IntoPyResult; 8 | 9 | #[pyfunction] 10 | #[pyo3(signature = (cellarray, mixed_resolutions = false))] 11 | pub(crate) fn compact( 12 | py: Python<'_>, 13 | cellarray: PyCellArray, 14 | mixed_resolutions: bool, 15 | ) -> PyResult { 16 | let cellindexarray = cellarray.into_inner(); 17 | let compacted = py 18 | .allow_threads(|| { 19 | if mixed_resolutions { 20 | cellindexarray.compact_mixed_resolutions() 21 | } else { 22 | cellindexarray.compact() 23 | } 24 | }) 25 | .into_pyresult()?; 26 | 27 | h3array_to_pyarray(compacted, py) 28 | } 29 | 30 | #[pyfunction] 31 | #[pyo3(signature = (cellarray, target_resolution))] 32 | pub(crate) fn uncompact( 33 | py: Python<'_>, 34 | cellarray: PyCellArray, 35 | target_resolution: u8, 36 | ) -> PyResult { 37 | let target_resolution = Resolution::try_from(target_resolution).into_pyresult()?; 38 | let cellarray = cellarray.into_inner(); 39 | let out = py.allow_threads(|| cellarray.uncompact(target_resolution)); 40 | h3array_to_pyarray(out, py) 41 | } 42 | -------------------------------------------------------------------------------- /h3ronpy/src/op/localij.rs: -------------------------------------------------------------------------------- 1 | use crate::array::PyCellArray; 2 | use crate::arrow_interop::{h3array_to_pyarray, pyarray_to_cellindexarray, pyarray_to_native}; 3 | use crate::error::IntoPyResult; 4 | use arrow::array::{Array, ArrayRef, Int32Array, RecordBatch}; 5 | use arrow::datatypes::{Field, Schema}; 6 | use h3arrow::algorithm::localij::{LocalIJArrays, ToLocalIJOp}; 7 | use h3arrow::array::CellIndexArray; 8 | use h3arrow::h3o::CellIndex; 9 | use pyo3::exceptions::PyValueError; 10 | use pyo3::prelude::PyAnyMethods; 11 | use pyo3::{pyfunction, Bound, PyAny, PyObject, PyResult, Python}; 12 | use pyo3_arrow::error::PyArrowResult; 13 | use pyo3_arrow::PyRecordBatch; 14 | use std::iter::repeat; 15 | use std::sync::Arc; 16 | 17 | #[pyfunction] 18 | #[pyo3(signature = (cellarray, anchor, set_failing_to_invalid = false))] 19 | pub(crate) fn cells_to_localij( 20 | py: Python, 21 | cellarray: PyCellArray, 22 | anchor: &Bound, 23 | set_failing_to_invalid: bool, 24 | ) -> PyArrowResult { 25 | let cellindexarray = cellarray.into_inner(); 26 | let anchorarray = get_anchor_array(anchor, cellindexarray.len())?; 27 | 28 | let localij_arrays = cellindexarray 29 | .to_local_ij_array(anchorarray, set_failing_to_invalid) 30 | .into_pyresult()?; 31 | 32 | let i = localij_arrays.i.clone(); 33 | let j = localij_arrays.j.clone(); 34 | let anchor = localij_arrays.anchors.primitive_array().clone(); 35 | 36 | let schema = Schema::new(vec![ 37 | Field::new("i", i.data_type().clone(), true), 38 | Field::new("j", j.data_type().clone(), true), 39 | Field::new("anchor", anchor.data_type().clone(), true), 40 | ]); 41 | let columns: Vec = vec![ 42 | Arc::new(localij_arrays.i), 43 | Arc::new(localij_arrays.j), 44 | Arc::new(anchor), 45 | ]; 46 | let batch = RecordBatch::try_new(Arc::new(schema), columns)?; 47 | Ok(PyRecordBatch::new(batch).to_arro3(py)?) 48 | } 49 | 50 | #[pyfunction] 51 | #[pyo3(signature = (anchor, i_array, j_array, set_failing_to_invalid = false))] 52 | pub(crate) fn localij_to_cells( 53 | py: Python<'_>, 54 | anchor: &Bound, 55 | i_array: &Bound, 56 | j_array: &Bound, 57 | set_failing_to_invalid: bool, 58 | ) -> PyResult { 59 | let i_array = pyarray_to_native::(i_array)?; 60 | let j_array = pyarray_to_native::(j_array)?; 61 | let anchorarray = get_anchor_array(anchor, i_array.len())?; 62 | 63 | let cellarray = py.allow_threads(|| { 64 | let localij_arrays = 65 | LocalIJArrays::try_new(anchorarray, i_array, j_array).into_pyresult()?; 66 | 67 | if set_failing_to_invalid { 68 | localij_arrays.to_cells_failing_to_invalid().into_pyresult() 69 | } else { 70 | localij_arrays.to_cells().into_pyresult() 71 | } 72 | })?; 73 | 74 | h3array_to_pyarray(cellarray, py) 75 | } 76 | 77 | fn get_anchor_array(anchor: &Bound, len: usize) -> PyResult { 78 | if let Ok(anchor) = anchor.extract::() { 79 | let anchor_cell = CellIndex::try_from(anchor).into_pyresult()?; 80 | Ok(CellIndexArray::from_iter(repeat(anchor_cell).take(len))) 81 | } else if let Ok(anchorarray) = pyarray_to_cellindexarray(anchor) { 82 | Ok(anchorarray) 83 | } else { 84 | return Err(PyValueError::new_err(format!( 85 | "Expected a single cell or an array of cells, found type {:?}", 86 | anchor.get_type(), 87 | ))); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /h3ronpy/src/op/measure.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use crate::array::PyCellArray; 4 | use pyo3::prelude::*; 5 | use pyo3_arrow::PyArray; 6 | 7 | #[pyfunction] 8 | #[pyo3(signature = (cellarray))] 9 | pub(crate) fn cells_area_m2(py: Python, cellarray: PyCellArray) -> PyResult { 10 | let out = cellarray.as_ref().area_m2(); 11 | PyArray::from_array_ref(Arc::new(out)).to_arro3(py) 12 | } 13 | 14 | #[pyfunction] 15 | #[pyo3(signature = (cellarray))] 16 | pub(crate) fn cells_area_km2(py: Python, cellarray: PyCellArray) -> PyResult { 17 | let out = cellarray.as_ref().area_km2(); 18 | PyArray::from_array_ref(Arc::new(out)).to_arro3(py) 19 | } 20 | 21 | #[pyfunction] 22 | #[pyo3(signature = (cellarray))] 23 | pub(crate) fn cells_area_rads2(py: Python, cellarray: PyCellArray) -> PyResult { 24 | let out = cellarray.as_ref().area_rads2(); 25 | PyArray::from_array_ref(Arc::new(out)).to_arro3(py) 26 | } 27 | -------------------------------------------------------------------------------- /h3ronpy/src/op/mod.rs: -------------------------------------------------------------------------------- 1 | use pyo3::prelude::*; 2 | 3 | mod compact; 4 | mod localij; 5 | mod measure; 6 | mod neighbor; 7 | mod resolution; 8 | mod string; 9 | mod valid; 10 | 11 | pub fn init_op_submodule(m: &Bound) -> PyResult<()> { 12 | m.add_function(wrap_pyfunction!(resolution::change_resolution, m)?)?; 13 | m.add_function(wrap_pyfunction!(resolution::change_resolution_list, m)?)?; 14 | m.add_function(wrap_pyfunction!(resolution::change_resolution_paired, m)?)?; 15 | m.add_function(wrap_pyfunction!(resolution::cells_resolution, m)?)?; 16 | m.add_function(wrap_pyfunction!(neighbor::grid_disk, m)?)?; 17 | m.add_function(wrap_pyfunction!(neighbor::grid_disk_distances, m)?)?; 18 | m.add_function(wrap_pyfunction!(neighbor::grid_ring_distances, m)?)?; 19 | m.add_function(wrap_pyfunction!(neighbor::grid_disk_aggregate_k, m)?)?; 20 | m.add_function(wrap_pyfunction!(string::cells_parse, m)?)?; 21 | m.add_function(wrap_pyfunction!(string::vertexes_parse, m)?)?; 22 | m.add_function(wrap_pyfunction!(string::directededges_parse, m)?)?; 23 | m.add_function(wrap_pyfunction!(string::cells_to_string, m)?)?; 24 | m.add_function(wrap_pyfunction!(string::vertexes_to_string, m)?)?; 25 | m.add_function(wrap_pyfunction!(string::directededges_to_string, m)?)?; 26 | m.add_function(wrap_pyfunction!(compact::compact, m)?)?; 27 | m.add_function(wrap_pyfunction!(compact::uncompact, m)?)?; 28 | m.add_function(wrap_pyfunction!(valid::cells_valid, m)?)?; 29 | m.add_function(wrap_pyfunction!(valid::vertexes_valid, m)?)?; 30 | m.add_function(wrap_pyfunction!(valid::directededges_valid, m)?)?; 31 | m.add_function(wrap_pyfunction!(measure::cells_area_m2, m)?)?; 32 | m.add_function(wrap_pyfunction!(measure::cells_area_km2, m)?)?; 33 | m.add_function(wrap_pyfunction!(measure::cells_area_rads2, m)?)?; 34 | m.add_function(wrap_pyfunction!(localij::cells_to_localij, m)?)?; 35 | m.add_function(wrap_pyfunction!(localij::localij_to_cells, m)?)?; 36 | 37 | Ok(()) 38 | } 39 | -------------------------------------------------------------------------------- /h3ronpy/src/op/neighbor.rs: -------------------------------------------------------------------------------- 1 | use arrow::array::{ 2 | Array, ArrayRef, GenericListArray, LargeListArray, PrimitiveArray, RecordBatch, UInt32Array, 3 | }; 4 | use arrow::datatypes::{Field, Schema}; 5 | use h3arrow::algorithm::{GridDiskDistances, GridOp, KAggregationMethod}; 6 | use pyo3::exceptions::{PyRuntimeError, PyValueError}; 7 | use pyo3::{PyObject, PyResult}; 8 | use pyo3_arrow::error::PyArrowResult; 9 | use pyo3_arrow::{PyArray, PyRecordBatch}; 10 | use std::str::FromStr; 11 | use std::sync::Arc; 12 | 13 | use crate::array::PyCellArray; 14 | use crate::arrow_interop::*; 15 | use crate::error::IntoPyResult; 16 | use crate::DEFAULT_CELL_COLUMN_NAME; 17 | use pyo3::prelude::*; 18 | 19 | #[pyfunction] 20 | #[pyo3(signature = (cellarray, k, flatten = false))] 21 | pub(crate) fn grid_disk( 22 | py: Python, 23 | cellarray: PyCellArray, 24 | k: u32, 25 | flatten: bool, 26 | ) -> PyResult { 27 | let cellindexarray = cellarray.into_inner(); 28 | let listarray = cellindexarray.grid_disk(k).into_pyresult()?; 29 | if flatten { 30 | let cellindexarray = listarray.into_flattened().into_pyresult()?; 31 | h3array_to_pyarray(cellindexarray, py) 32 | } else { 33 | PyArray::from_array_ref(Arc::new(LargeListArray::from(listarray))).to_arro3(py) 34 | } 35 | } 36 | 37 | #[pyfunction] 38 | #[pyo3(signature = (cellarray, k, flatten = false))] 39 | pub(crate) fn grid_disk_distances( 40 | py: Python, 41 | cellarray: PyCellArray, 42 | k: u32, 43 | flatten: bool, 44 | ) -> PyArrowResult { 45 | let griddiskdistances = cellarray 46 | .into_inner() 47 | .grid_disk_distances(k) 48 | .into_pyresult()?; 49 | 50 | return_griddiskdistances_table(py, griddiskdistances, flatten) 51 | } 52 | 53 | #[pyfunction] 54 | #[pyo3(signature = (cellarray, k_min, k_max, flatten = false))] 55 | pub(crate) fn grid_ring_distances( 56 | py: Python, 57 | cellarray: PyCellArray, 58 | k_min: u32, 59 | k_max: u32, 60 | flatten: bool, 61 | ) -> PyArrowResult { 62 | if k_min >= k_max { 63 | return Err(PyValueError::new_err("k_min must be less than k_max").into()); 64 | } 65 | let griddiskdistances = cellarray 66 | .into_inner() 67 | .grid_ring_distances(k_min, k_max) 68 | .into_pyresult()?; 69 | 70 | return_griddiskdistances_table(py, griddiskdistances, flatten) 71 | } 72 | 73 | fn return_griddiskdistances_table( 74 | py: Python, 75 | griddiskdistances: GridDiskDistances, 76 | flatten: bool, 77 | ) -> PyArrowResult { 78 | let (cells, distances): (ArrayRef, ArrayRef) = if flatten { 79 | ( 80 | Arc::new(PrimitiveArray::from( 81 | griddiskdistances.cells.into_flattened().into_pyresult()?, 82 | )), 83 | Arc::new( 84 | griddiskdistances 85 | .distances 86 | .values() 87 | .as_any() 88 | .downcast_ref::() 89 | .ok_or_else(|| PyRuntimeError::new_err("expected primitivearray")) 90 | .cloned()?, 91 | ), 92 | ) 93 | } else { 94 | ( 95 | Arc::new(GenericListArray::::from(griddiskdistances.cells)), 96 | Arc::new(griddiskdistances.distances), 97 | ) 98 | }; 99 | 100 | let schema = Schema::new(vec![ 101 | Field::new(DEFAULT_CELL_COLUMN_NAME, cells.data_type().clone(), true), 102 | Field::new("k", distances.data_type().clone(), true), 103 | ]); 104 | let columns = vec![cells, distances]; 105 | let batch = RecordBatch::try_new(Arc::new(schema), columns)?; 106 | Ok(PyRecordBatch::new(batch).to_arro3(py)?) 107 | } 108 | 109 | struct KAggregationMethodWrapper(KAggregationMethod); 110 | 111 | impl FromStr for KAggregationMethodWrapper { 112 | type Err = PyErr; 113 | 114 | fn from_str(s: &str) -> Result { 115 | match s.to_lowercase().as_str() { 116 | "min" => Ok(Self(KAggregationMethod::Min)), 117 | "max" => Ok(Self(KAggregationMethod::Max)), 118 | _ => Err(PyValueError::new_err("unknown way to aggregate k")), 119 | } 120 | } 121 | } 122 | 123 | #[pyfunction] 124 | #[pyo3(signature = (cellarray, k, aggregation_method))] 125 | pub(crate) fn grid_disk_aggregate_k( 126 | py: Python, 127 | cellarray: PyCellArray, 128 | k: u32, 129 | aggregation_method: &str, 130 | ) -> PyArrowResult { 131 | let aggregation_method = KAggregationMethodWrapper::from_str(aggregation_method)?; 132 | 133 | let griddiskaggk = cellarray 134 | .as_ref() 135 | .grid_disk_aggregate_k(k, aggregation_method.0) 136 | .into_pyresult()?; 137 | 138 | let schema = Schema::new(vec![ 139 | Field::new( 140 | DEFAULT_CELL_COLUMN_NAME, 141 | griddiskaggk.cells.primitive_array().data_type().clone(), 142 | true, 143 | ), 144 | Field::new("k", griddiskaggk.distances.data_type().clone(), true), 145 | ]); 146 | let columns: Vec = vec![ 147 | Arc::new(griddiskaggk.cells.primitive_array().clone()), 148 | Arc::new(griddiskaggk.distances), 149 | ]; 150 | let batch = RecordBatch::try_new(Arc::new(schema), columns)?; 151 | Ok(PyRecordBatch::new(batch).to_arro3(py)?) 152 | } 153 | -------------------------------------------------------------------------------- /h3ronpy/src/op/resolution.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use arrow::array::{Array, ArrayRef, LargeListArray, RecordBatch}; 4 | use arrow::datatypes::{Field, Schema}; 5 | use h3arrow::algorithm::ChangeResolutionOp; 6 | use h3arrow::export::h3o::Resolution; 7 | use pyo3::prelude::*; 8 | use pyo3_arrow::error::PyArrowResult; 9 | use pyo3_arrow::{PyArray, PyRecordBatch}; 10 | 11 | use crate::array::PyCellArray; 12 | use crate::arrow_interop::*; 13 | use crate::error::IntoPyResult; 14 | use crate::DEFAULT_CELL_COLUMN_NAME; 15 | 16 | #[pyfunction] 17 | pub(crate) fn change_resolution( 18 | py: Python<'_>, 19 | cellarray: PyCellArray, 20 | h3_resolution: u8, 21 | ) -> PyResult { 22 | let cellindexarray = cellarray.into_inner(); 23 | let h3_resolution = Resolution::try_from(h3_resolution).into_pyresult()?; 24 | let out = py.allow_threads(|| { 25 | cellindexarray 26 | .change_resolution(h3_resolution) 27 | .into_pyresult() 28 | })?; 29 | 30 | h3array_to_pyarray(out, py) 31 | } 32 | 33 | #[pyfunction] 34 | pub(crate) fn change_resolution_list( 35 | py: Python, 36 | cellarray: PyCellArray, 37 | h3_resolution: u8, 38 | ) -> PyResult { 39 | let cellindexarray = cellarray.into_inner(); 40 | let h3_resolution = Resolution::try_from(h3_resolution).into_pyresult()?; 41 | let listarray = cellindexarray 42 | .change_resolution_list(h3_resolution) 43 | .into_pyresult()?; 44 | 45 | PyArray::from_array_ref(Arc::new(LargeListArray::from(listarray))).to_arro3(py) 46 | } 47 | 48 | #[pyfunction] 49 | pub(crate) fn change_resolution_paired( 50 | py: Python, 51 | cellarray: PyCellArray, 52 | h3_resolution: u8, 53 | ) -> PyArrowResult { 54 | let cellindexarray = cellarray.into_inner(); 55 | let h3_resolution = Resolution::try_from(h3_resolution).into_pyresult()?; 56 | let pair = cellindexarray 57 | .change_resolution_paired(h3_resolution) 58 | .into_pyresult()?; 59 | 60 | let before = pair.before; 61 | let after = pair.after; 62 | 63 | let schema = Schema::new(vec![ 64 | Field::new( 65 | format!("{}_before", DEFAULT_CELL_COLUMN_NAME), 66 | before.primitive_array().data_type().clone(), 67 | true, 68 | ), 69 | Field::new( 70 | format!("{}_after", DEFAULT_CELL_COLUMN_NAME), 71 | after.primitive_array().data_type().clone(), 72 | true, 73 | ), 74 | ]); 75 | let columns: Vec = vec![ 76 | Arc::new(before.primitive_array().clone()), 77 | Arc::new(after.primitive_array().clone()), 78 | ]; 79 | let batch = RecordBatch::try_new(Arc::new(schema), columns)?; 80 | Ok(PyRecordBatch::new(batch).to_arro3(py)?) 81 | } 82 | 83 | #[pyfunction] 84 | pub(crate) fn cells_resolution(py: Python, cellarray: PyCellArray) -> PyResult { 85 | let resarray = cellarray.as_ref().resolution(); 86 | PyArray::from_array_ref(Arc::new(resarray.into_inner())).to_arro3(py) 87 | } 88 | -------------------------------------------------------------------------------- /h3ronpy/src/op/string.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use arrow::array::{Array, LargeStringArray, StringArray}; 4 | use h3arrow::algorithm::{ParseGenericStringArray, ToGenericStringArray}; 5 | use h3arrow::array::{CellIndexArray, DirectedEdgeIndexArray, VertexIndexArray}; 6 | use pyo3::exceptions::PyValueError; 7 | use pyo3::prelude::*; 8 | use pyo3_arrow::PyArray; 9 | 10 | use crate::array::PyCellArray; 11 | use crate::arrow_interop::*; 12 | use crate::error::IntoPyResult; 13 | 14 | #[pyfunction] 15 | #[pyo3(signature = (stringarray, set_failing_to_invalid = false))] 16 | pub(crate) fn cells_parse( 17 | py: Python<'_>, 18 | stringarray: PyArray, 19 | set_failing_to_invalid: bool, 20 | ) -> PyResult { 21 | let (boxed_array, _field) = stringarray.into_inner(); 22 | let cells = py.allow_threads(|| { 23 | if let Some(stringarray) = boxed_array.as_any().downcast_ref::() { 24 | CellIndexArray::parse_genericstringarray(stringarray, set_failing_to_invalid) 25 | .into_pyresult() 26 | } else if let Some(stringarray) = boxed_array.as_any().downcast_ref::() { 27 | CellIndexArray::parse_genericstringarray(stringarray, set_failing_to_invalid) 28 | .into_pyresult() 29 | } else { 30 | Err(PyValueError::new_err( 31 | "unsupported array type to parse cells from", 32 | )) 33 | } 34 | })?; 35 | 36 | h3array_to_pyarray(cells, py) 37 | } 38 | 39 | #[pyfunction] 40 | #[pyo3(signature = (stringarray, set_failing_to_invalid = false))] 41 | pub(crate) fn vertexes_parse( 42 | py: Python<'_>, 43 | stringarray: PyArray, 44 | set_failing_to_invalid: bool, 45 | ) -> PyResult { 46 | let (boxed_array, _field) = stringarray.into_inner(); 47 | let vertexes = py.allow_threads(|| { 48 | if let Some(utf8array) = boxed_array.as_any().downcast_ref::() { 49 | VertexIndexArray::parse_genericstringarray(utf8array, set_failing_to_invalid) 50 | .into_pyresult() 51 | } else if let Some(utf8array) = boxed_array.as_any().downcast_ref::() { 52 | VertexIndexArray::parse_genericstringarray(utf8array, set_failing_to_invalid) 53 | .into_pyresult() 54 | } else { 55 | Err(PyValueError::new_err( 56 | "unsupported array type to parse vertexes from", 57 | )) 58 | } 59 | })?; 60 | 61 | h3array_to_pyarray(vertexes, py) 62 | } 63 | 64 | #[pyfunction] 65 | #[pyo3(signature = (stringarray, set_failing_to_invalid = false))] 66 | pub(crate) fn directededges_parse( 67 | py: Python<'_>, 68 | stringarray: PyArray, 69 | set_failing_to_invalid: bool, 70 | ) -> PyResult { 71 | let (boxed_array, _field) = stringarray.into_inner(); 72 | let edges = py.allow_threads(|| { 73 | if let Some(stringarray) = boxed_array.as_any().downcast_ref::() { 74 | DirectedEdgeIndexArray::parse_genericstringarray(stringarray, set_failing_to_invalid) 75 | .into_pyresult() 76 | } else if let Some(stringarray) = boxed_array.as_any().downcast_ref::() { 77 | DirectedEdgeIndexArray::parse_genericstringarray(stringarray, set_failing_to_invalid) 78 | .into_pyresult() 79 | } else { 80 | Err(PyValueError::new_err( 81 | "unsupported array type to parse directededges from", 82 | )) 83 | } 84 | })?; 85 | 86 | h3array_to_pyarray(edges, py) 87 | } 88 | 89 | #[pyfunction] 90 | #[pyo3(signature = (cellarray))] 91 | pub(crate) fn cells_to_string(py: Python, cellarray: PyCellArray) -> PyResult { 92 | let stringarray: LargeStringArray = 93 | cellarray.as_ref().to_genericstringarray().into_pyresult()?; 94 | PyArray::from_array_ref(Arc::new(stringarray)).to_arro3(py) 95 | } 96 | 97 | #[pyfunction] 98 | #[pyo3(signature = (vertexarray))] 99 | pub(crate) fn vertexes_to_string(py: Python, vertexarray: &Bound) -> PyResult { 100 | let stringarray: LargeStringArray = pyarray_to_vertexindexarray(vertexarray)? 101 | .to_genericstringarray() 102 | .into_pyresult()?; 103 | PyArray::from_array_ref(Arc::new(stringarray)).to_arro3(py) 104 | } 105 | 106 | #[pyfunction] 107 | #[pyo3(signature = (directededgearray))] 108 | pub(crate) fn directededges_to_string( 109 | py: Python, 110 | directededgearray: &Bound, 111 | ) -> PyResult { 112 | let stringarray: LargeStringArray = pyarray_to_directededgeindexarray(directededgearray)? 113 | .to_genericstringarray() 114 | .into_pyresult()?; 115 | PyArray::from_array_ref(Arc::new(stringarray)).to_arro3(py) 116 | } 117 | -------------------------------------------------------------------------------- /h3ronpy/src/op/valid.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use arrow::array::{Array, BooleanArray}; 4 | use arrow::buffer::NullBuffer; 5 | use h3arrow::array::{FromIteratorWithValidity, H3Array, H3IndexArrayValue}; 6 | use h3arrow::h3o; 7 | use h3o::{CellIndex, DirectedEdgeIndex, VertexIndex}; 8 | use pyo3::prelude::*; 9 | use pyo3_arrow::PyArray; 10 | 11 | use crate::arrow_interop::*; 12 | 13 | fn h3index_valid(py: Python, arr: &Bound, booleanarray: bool) -> PyResult 14 | where 15 | IX: H3IndexArrayValue + Send, 16 | { 17 | let u64array = pyarray_to_uint64array(arr)?; 18 | let validated = py.allow_threads(|| H3Array::::from_iter_with_validity(u64array.iter())); 19 | 20 | if booleanarray { 21 | let nullbuffer = validated 22 | .primitive_array() 23 | .nulls() 24 | .cloned() 25 | .unwrap_or_else(|| NullBuffer::new_valid(validated.len())); 26 | let bools = BooleanArray::from(nullbuffer.into_inner()); 27 | PyArray::from_array_ref(Arc::new(bools)).to_arro3(py) 28 | } else { 29 | h3array_to_pyarray(validated, py) 30 | } 31 | } 32 | 33 | macro_rules! impl_h3index_valid { 34 | ($name:ident, $arr_type:ty) => { 35 | #[pyfunction] 36 | #[pyo3(signature = (array, booleanarray = false))] 37 | pub(crate) fn $name( 38 | py: Python, 39 | array: &Bound, 40 | booleanarray: bool, 41 | ) -> PyResult { 42 | h3index_valid::<$arr_type>(py, array, booleanarray) 43 | } 44 | }; 45 | } 46 | 47 | impl_h3index_valid!(cells_valid, CellIndex); 48 | impl_h3index_valid!(vertexes_valid, VertexIndex); 49 | impl_h3index_valid!(directededges_valid, DirectedEdgeIndex); 50 | -------------------------------------------------------------------------------- /h3ronpy/src/resolution.rs: -------------------------------------------------------------------------------- 1 | use h3arrow::export::h3o::Resolution; 2 | use pyo3::exceptions::PyValueError; 3 | use pyo3::prelude::*; 4 | 5 | pub struct PyResolution(Resolution); 6 | 7 | impl<'py> FromPyObject<'py> for PyResolution { 8 | fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult { 9 | let int = ob.extract::()?; 10 | let res = 11 | Resolution::try_from(int).map_err(|err| PyValueError::new_err(err.to_string()))?; 12 | Ok(Self(res)) 13 | } 14 | } 15 | 16 | impl From for Resolution { 17 | fn from(value: PyResolution) -> Self { 18 | value.0 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /h3ronpy/src/transform.rs: -------------------------------------------------------------------------------- 1 | use geo::AffineTransform; 2 | use pyo3::basic::CompareOp; 3 | use pyo3::exceptions::PyNotImplementedError; 4 | use pyo3::prelude::*; 5 | use rasterh3::transform::{from_gdal, from_rasterio}; 6 | 7 | /// affine geotransform 8 | #[pyclass] 9 | #[derive(Clone)] 10 | pub struct Transform { 11 | pub(crate) inner: AffineTransform, 12 | } 13 | 14 | #[pymethods] 15 | impl Transform { 16 | #[allow(clippy::many_single_char_names)] // using the same parameter names as the affine library 17 | #[new] 18 | pub fn new(a: f64, b: f64, c: f64, d: f64, e: f64, f: f64) -> Self { 19 | Self { 20 | inner: AffineTransform::new(a, b, c, d, e, f), 21 | } 22 | } 23 | 24 | /// construct a Transform from a six-values array as used by GDAL 25 | #[staticmethod] 26 | pub fn from_gdal(gdal_transform: [f64; 6]) -> Self { 27 | Transform { 28 | inner: from_gdal(&gdal_transform), 29 | } 30 | } 31 | 32 | /// construct a Transform from a six-values array as used by rasterio 33 | #[staticmethod] 34 | pub fn from_rasterio(rio_transform: [f64; 6]) -> Self { 35 | Transform { 36 | inner: from_rasterio(&rio_transform), 37 | } 38 | } 39 | 40 | fn __richcmp__(&self, other: Transform, op: CompareOp) -> PyResult { 41 | match op { 42 | CompareOp::Eq => Ok(self.inner == other.inner), 43 | CompareOp::Ne => Ok(self.inner != other.inner), 44 | _ => Err(PyNotImplementedError::new_err(format!( 45 | "{:?} is not implemented", 46 | op 47 | ))), 48 | } 49 | } 50 | 51 | fn __str__(&self) -> PyResult { 52 | Ok(format!("{:?}", self.inner)) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /h3ronpy/tests/__init__.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | import geopandas as gpd 4 | 5 | TESTDATA_PATH = Path(__file__).parent.parent / "data" 6 | 7 | 8 | def load_africa() -> gpd.GeoDataFrame: 9 | world = gpd.read_file(TESTDATA_PATH / "naturalearth_110m_admin_0_countries.fgb") 10 | return world[world["CONTINENT"] == "Africa"] 11 | -------------------------------------------------------------------------------- /h3ronpy/tests/arrow/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nmandery/h3ronpy/f4ab01637b105d07c1af63ce22467e4402d2dd03/h3ronpy/tests/arrow/__init__.py -------------------------------------------------------------------------------- /h3ronpy/tests/arrow/test_benches.py: -------------------------------------------------------------------------------- 1 | import h3.api.numpy_int as h3 2 | import numpy as np 3 | import polars as pl 4 | from h3ronpy import cells_to_string 5 | 6 | 7 | def some_cells() -> np.ndarray: 8 | return np.full(1000, h3.geo_to_h3(45.5, 10.2, 5), dtype="uint64") 9 | 10 | 11 | def benchmark_h3_to_string_python_list(cells): 12 | return [h3.h3_to_string(cell) for cell in cells] 13 | 14 | 15 | def test_cells_to_string(benchmark): 16 | benchmark(cells_to_string, pl.Series(some_cells())) 17 | 18 | 19 | def test_h3_to_string_python_list(benchmark): 20 | benchmark(benchmark_h3_to_string_python_list, list(some_cells())) 21 | 22 | 23 | h3_to_string_numpy_vectorized = np.vectorize( 24 | h3.h3_to_string, 25 | otypes=[ 26 | str, 27 | ], 28 | ) 29 | 30 | 31 | def test_h3_to_string_numpy_vectorized(benchmark): 32 | benchmark(h3_to_string_numpy_vectorized, some_cells()) 33 | -------------------------------------------------------------------------------- /h3ronpy/tests/arrow/test_compact.py: -------------------------------------------------------------------------------- 1 | import h3.api.numpy_int as h3 2 | import numpy as np 3 | import pytest 4 | from h3ronpy import change_resolution, compact, uncompact 5 | 6 | 7 | def compact_to_one(expected_cell, input_cells, **kw): 8 | compacted = compact(input_cells, **kw) 9 | assert len(compacted) == 1 10 | assert compacted[0] == expected_cell 11 | 12 | 13 | def test_compact(): 14 | cell = h3.geo_to_h3(10.3, 45.1, 8) 15 | h3indexes = change_resolution( 16 | np.array( 17 | [ 18 | cell, 19 | ], 20 | dtype=np.uint64, 21 | ), 22 | 10, 23 | ) 24 | compact_to_one(cell, h3indexes) 25 | 26 | 27 | def test_compact_mixed_fail(): 28 | cell = h3.geo_to_h3(10.3, 45.1, 8) 29 | with pytest.raises(ValueError, match="heterogen"): 30 | compact_to_one(cell, [cell, h3.h3_to_parent(cell, 4)]) 31 | 32 | 33 | def test_compact_mixed(): 34 | cell = h3.geo_to_h3(10.3, 45.1, 8) 35 | compact_to_one(cell, [cell, h3.geo_to_h3(10.3, 45.1, 9)], mixed_resolutions=True) 36 | 37 | 38 | def test_uncompact(): 39 | cells = uncompact( 40 | [ 41 | h3.geo_to_h3(10.3, 45.1, 8), 42 | ], 43 | 9, 44 | ) 45 | assert len(cells) == 7 46 | -------------------------------------------------------------------------------- /h3ronpy/tests/arrow/test_coordinates.py: -------------------------------------------------------------------------------- 1 | import h3.api.numpy_int as h3 2 | import numpy as np 3 | from arro3.core import RecordBatch 4 | from h3ronpy.vector import ( 5 | cells_bounds, 6 | cells_bounds_arrays, 7 | cells_to_coordinates, 8 | coordinates_to_cells, 9 | ) 10 | 11 | 12 | def test_cells_to_coordinates(): 13 | h3indexes = np.array( 14 | [ 15 | h3.geo_to_h3(10.3, 45.1, 8), 16 | ], 17 | dtype=np.uint64, 18 | ) 19 | coords = cells_to_coordinates(h3indexes) 20 | assert coords.num_rows == 1 21 | assert 10.0 < coords["lat"][0].as_py() < 11.0 22 | assert 45.0 < coords["lng"][0].as_py() < 46.0 23 | 24 | 25 | def test_coordinates_to_cells(): 26 | lat = np.array([10.3, 23.1], dtype=np.float64) 27 | lng = np.array([45.1, 2.3], dtype=np.float64) 28 | r = 7 29 | cells = coordinates_to_cells(lat, lng, r) 30 | assert len(cells) == 2 31 | assert cells[0] == h3.geo_to_h3(lat[0], lng[0], r) 32 | assert cells[1] == h3.geo_to_h3(lat[1], lng[1], r) 33 | 34 | 35 | def test_coordinates_to_cells_resolution_array(): 36 | lat = np.array([10.3, 23.1], dtype=np.float64) 37 | lng = np.array([45.1, 2.3], dtype=np.float64) 38 | r = np.array([9, 12], dtype=np.uint8) 39 | cells = coordinates_to_cells(lat, lng, r) 40 | assert len(cells) == 2 41 | assert cells[0] == h3.geo_to_h3(lat[0], lng[0], r[0]) 42 | assert cells[1] == h3.geo_to_h3(lat[1], lng[1], r[1]) 43 | 44 | 45 | def test_cells_bounds(): 46 | h3indexes = np.array( 47 | [ 48 | h3.geo_to_h3(10.3, 45.1, 8), 49 | ], 50 | dtype=np.uint64, 51 | ) 52 | bounds = cells_bounds(h3indexes) 53 | assert bounds is not None 54 | assert type(bounds) == tuple 55 | assert len(bounds) == 4 56 | assert bounds[0] < bounds[2] 57 | assert bounds[1] < bounds[3] 58 | 59 | 60 | def test_cells_bounds_arrays(): 61 | h3indexes = np.array( 62 | [ 63 | h3.geo_to_h3(10.3, 45.1, 8), 64 | h3.geo_to_h3(10.3, 45.1, 5), 65 | ], 66 | dtype=np.uint64, 67 | ) 68 | bounds_df = cells_bounds_arrays(h3indexes) 69 | assert bounds_df is not None 70 | assert isinstance(bounds_df, RecordBatch) 71 | assert bounds_df.num_rows == 2 72 | assert "minx" in bounds_df.schema.names 73 | assert "maxx" in bounds_df.schema.names 74 | assert "miny" in bounds_df.schema.names 75 | assert "maxy" in bounds_df.schema.names 76 | assert bounds_df["minx"][0].as_py() < 45.1 77 | assert bounds_df["maxx"][0].as_py() > 45.1 78 | assert bounds_df["miny"][0].as_py() < 10.3 79 | assert bounds_df["maxy"][0].as_py() > 10.3 80 | -------------------------------------------------------------------------------- /h3ronpy/tests/arrow/test_localij.py: -------------------------------------------------------------------------------- 1 | import polars as pl 2 | from h3ronpy import cells_parse, cells_to_localij, localij_to_cells 3 | from polars.testing import assert_series_equal 4 | 5 | anchors = cells_parse( 6 | [ 7 | "85283473fffffff", 8 | ] 9 | ) 10 | cells = cells_parse( 11 | [ 12 | "8528342bfffffff", 13 | ] 14 | ) 15 | 16 | 17 | def test_cells_to_localij_array(): 18 | df = cells_to_localij(cells, anchors) 19 | assert df.num_rows == 1 20 | 21 | left = pl.Series(df["anchor"]) 22 | right = pl.Series(anchors) 23 | assert_series_equal(left, right, check_names=False) 24 | assert df["i"][0] == 25 25 | assert df["j"][0] == 13 26 | 27 | 28 | def test_cells_to_localij_single_anchor(): 29 | df = cells_to_localij(cells, anchors[0]) 30 | assert df.num_rows == 1 31 | 32 | left = pl.Series(df["anchor"]) 33 | right = pl.Series(anchors) 34 | assert_series_equal(left, right, check_names=False) 35 | assert df["i"][0] == 25 36 | assert df["j"][0] == 13 37 | 38 | 39 | def test_localij_to_cells(): 40 | cells2 = localij_to_cells( 41 | anchors, 42 | pl.Series( 43 | [ 44 | 25, 45 | ], 46 | dtype=pl.Int32(), 47 | ), 48 | pl.Series( 49 | [ 50 | 13, 51 | ], 52 | dtype=pl.Int32(), 53 | ), 54 | ) 55 | 56 | left = pl.Series(cells) 57 | right = pl.Series(cells2) 58 | assert_series_equal(left, right, check_names=False) 59 | -------------------------------------------------------------------------------- /h3ronpy/tests/arrow/test_measure.py: -------------------------------------------------------------------------------- 1 | import h3.api.numpy_int as h3 2 | import numpy as np 3 | from arro3.core import Array 4 | from h3ronpy import cells_area_km2 5 | 6 | 7 | def test_cells_area_km2(): 8 | cells = np.array( 9 | [ 10 | h3.geo_to_h3(10.3, 45.1, 8), 11 | h3.geo_to_h3(10.3, 45.1, 5), 12 | h3.geo_to_h3(10.3, 45.1, 3), 13 | ], 14 | dtype=np.uint64, 15 | ) 16 | areas = cells_area_km2(cells) 17 | assert isinstance(areas, Array) 18 | assert len(areas) == 3 19 | assert int(areas[0].as_py() * 100) == 62 20 | assert int(areas[1].as_py()) == 213 21 | assert int(areas[2].as_py()) == 10456 22 | -------------------------------------------------------------------------------- /h3ronpy/tests/arrow/test_neighbor.py: -------------------------------------------------------------------------------- 1 | import h3.api.numpy_int as h3 2 | import numpy as np 3 | import polars as pl 4 | import pyarrow as pa 5 | from arro3.core import RecordBatch 6 | from h3ronpy import ( 7 | grid_disk, 8 | grid_disk_aggregate_k, 9 | grid_disk_distances, 10 | grid_ring_distances, 11 | ) 12 | 13 | 14 | def test_grid_disk(): 15 | h3indexes = np.array( 16 | [ 17 | h3.geo_to_h3(10.3, 45.1, 8), 18 | h3.geo_to_h3(5.3, -5.1, 8), 19 | ], 20 | dtype=np.uint64, 21 | ) 22 | disks = grid_disk(h3indexes, 2) 23 | assert len(disks) == 2 24 | # Arro3 has some bugs to fix around data type equality for nested types 25 | assert pa.field(disks.type).type == pa.large_list(pa.uint64()) 26 | 27 | disks_flat = grid_disk(h3indexes, 2, flatten=True) 28 | assert len(disks_flat) > 20 29 | assert disks_flat.type == pa.uint64() 30 | 31 | 32 | def test_grid_disk_distances(): 33 | h3indexes = np.array( 34 | [ 35 | h3.geo_to_h3(10.3, 45.1, 8), 36 | h3.geo_to_h3(5.3, -5.1, 8), 37 | ], 38 | dtype=np.uint64, 39 | ) 40 | disks = grid_disk_distances(h3indexes, 2) 41 | assert type(disks) == RecordBatch 42 | assert disks.num_rows == len(h3indexes) 43 | 44 | # Arro3 has some bugs to fix around data type equality for nested types 45 | assert pa.field(disks["cell"].type).type == pa.large_list(pa.uint64()) 46 | assert pa.field(disks["k"].type).type == pa.large_list(pa.uint32()) 47 | 48 | centers = pl.DataFrame(grid_disk_distances(h3indexes, 2, flatten=True)).filter( 49 | pl.col("cell").is_in(pl.Series(h3indexes)) 50 | ) 51 | assert len(centers) == len(h3indexes) 52 | assert len(centers["k"].unique()) == 1 53 | assert centers["k"].unique()[0] == 0 54 | 55 | # TODO: check values 56 | 57 | 58 | def test_grid_ring_distances(): 59 | h3indexes = np.array( 60 | [ 61 | h3.geo_to_h3(10.3, 45.1, 8), 62 | h3.geo_to_h3(5.3, -5.1, 8), 63 | ], 64 | dtype=np.uint64, 65 | ) 66 | disks = grid_ring_distances(h3indexes, 1, 2) 67 | assert type(disks) == RecordBatch 68 | assert disks.num_rows == len(h3indexes) 69 | 70 | # Arro3 has some bugs to fix around data type equality for nested types 71 | assert pa.field(disks["cell"].type).type == pa.large_list(pa.uint64()) 72 | assert pa.field(disks["k"].type).type == pa.large_list(pa.uint32()) 73 | 74 | centers = pl.DataFrame(grid_ring_distances(h3indexes, 1, 2, flatten=True)).filter( 75 | pl.col("cell").is_in(pl.Series(h3indexes)) 76 | ) 77 | assert len(centers) == 0 78 | 79 | # TODO: check values 80 | 81 | 82 | def test_grid_disk_aggregate_k(): 83 | h3indexes = np.array( 84 | [ 85 | h3.geo_to_h3(10.3, 45.1, 8), 86 | h3.geo_to_h3(5.3, -5.1, 8), 87 | ], 88 | dtype=np.uint64, 89 | ) 90 | disks = grid_disk_aggregate_k(h3indexes, 2, "max") 91 | assert type(disks) == RecordBatch 92 | assert disks["cell"].type == pa.uint64() 93 | assert disks["k"].type == pa.uint32() 94 | 95 | # TODO: check values 96 | -------------------------------------------------------------------------------- /h3ronpy/tests/arrow/test_raster.py: -------------------------------------------------------------------------------- 1 | try: 2 | import rasterio 3 | 4 | HAS_RASTERIO = True 5 | except ImportError: 6 | # rasterio is an optional dependency 7 | HAS_RASTERIO = False 8 | 9 | import numpy as np 10 | import polars as pl 11 | import pyarrow as pa 12 | import pytest 13 | from h3ronpy import DEFAULT_CELL_COLUMN_NAME, H3_CRS 14 | from h3ronpy.raster import raster_to_dataframe, rasterize_cells 15 | 16 | from tests import TESTDATA_PATH 17 | 18 | 19 | @pytest.mark.skipif(not HAS_RASTERIO, reason="requires rasterio") 20 | def test_r_tiff(): 21 | dataset = rasterio.open(TESTDATA_PATH / "r.tiff") 22 | band = dataset.read(1) 23 | df = raster_to_dataframe(band, dataset.transform, 8, nodata_value=0, compact=True) 24 | assert len(df) > 100 25 | assert df[DEFAULT_CELL_COLUMN_NAME].type == pa.uint64() 26 | assert df["value"].type == pa.uint8() 27 | 28 | 29 | @pytest.mark.skipif(not HAS_RASTERIO, reason="requires rasterio") 30 | def test_r_tiff_float32(): 31 | dataset = rasterio.open(TESTDATA_PATH / "r.tiff") 32 | band = dataset.read(1).astype(np.float32) 33 | df = raster_to_dataframe(band, dataset.transform, 8, nodata_value=np.nan, compact=True) 34 | assert len(df) > 100 35 | assert df[DEFAULT_CELL_COLUMN_NAME].type == pa.uint64() 36 | assert df["value"].type == pa.float32() 37 | 38 | 39 | def write_gtiff(filename, array, transform, nodata_value): 40 | with rasterio.open( 41 | filename, 42 | mode="w", 43 | driver="GTiff", 44 | compress="lzw", 45 | height=array.shape[0], 46 | width=array.shape[1], 47 | count=1, 48 | dtype=array.dtype, 49 | crs=H3_CRS, 50 | transform=transform, 51 | nodata_value=nodata_value, 52 | ) as ds: 53 | ds.write(array, 1) 54 | 55 | 56 | @pytest.mark.skipif(not HAS_RASTERIO, reason="requires rasterio") 57 | def test_rasterize_cells(): 58 | df = pl.read_parquet(TESTDATA_PATH / "population-841fa8bffffffff.parquet") 59 | size = (1000, 1000) 60 | nodata_value = -1 61 | array, transform = rasterize_cells(df["h3index"], df["pop_general"].cast(pl.Int32), size, nodata_value=nodata_value) 62 | 63 | assert array.shape == size 64 | assert np.int32 == array.dtype.type 65 | assert np.any(array > 0) 66 | 67 | # for inspection during debugging 68 | if False: 69 | write_gtiff("/tmp/rasterized.tif", array, transform, nodata_value) 70 | 71 | 72 | @pytest.mark.skipif(not HAS_RASTERIO, reason="requires rasterio") 73 | def test_rasterize_cells_auto_aspect(): 74 | df = pl.read_parquet(TESTDATA_PATH / "population-841fa8bffffffff.parquet") 75 | size = 1000 76 | nodata_value = -1 77 | array, transform = rasterize_cells(df["h3index"], df["pop_general"].cast(pl.Int32), size, nodata_value=nodata_value) 78 | 79 | assert array.shape[0] == size 80 | # print(array.shape) 81 | assert np.int32 == array.dtype.type 82 | assert np.any(array > 0) 83 | 84 | # for inspection during debugging 85 | if False: 86 | write_gtiff("/tmp/rasterized_auto_aspect.tif", array, transform, nodata_value) 87 | -------------------------------------------------------------------------------- /h3ronpy/tests/arrow/test_resolution.py: -------------------------------------------------------------------------------- 1 | import math 2 | 3 | import h3.api.numpy_int as h3 4 | import numpy as np 5 | from h3ronpy import cells_resolution, change_resolution, change_resolution_paired 6 | 7 | 8 | def test_change_resolution_up(): 9 | h3indexes = np.array([h3.geo_to_h3(10.2, 45.5, 5), h3.geo_to_h3(10.3, 45.1, 8)], dtype=np.uint64) 10 | out_res = 9 11 | changed = change_resolution(h3indexes, out_res) 12 | assert len(changed) == (int(math.pow(7, 4)) + 7) 13 | for i in range(len(changed)): 14 | assert h3.h3_get_resolution(changed[i].as_py()) == out_res 15 | 16 | 17 | def test_change_resolution_paired_up(): 18 | h3indexes = np.array( 19 | [ 20 | h3.geo_to_h3(10.3, 45.1, 8), 21 | ], 22 | dtype=np.uint64, 23 | ) 24 | out_res = 9 25 | changed_df = change_resolution_paired(h3indexes, out_res) 26 | assert changed_df.num_rows == 7 27 | for i in range(changed_df.num_rows): 28 | assert h3.h3_get_resolution(changed_df["cell_before"][i].as_py()) == 8 29 | assert h3.h3_get_resolution(changed_df["cell_after"][i].as_py()) == out_res 30 | 31 | 32 | def test_change_resolution_down(): 33 | h3indexes = np.array([h3.geo_to_h3(10.2, 45.5, 5), h3.geo_to_h3(10.3, 45.1, 8)], dtype=np.uint64) 34 | out_res = 4 35 | changed = change_resolution(h3indexes, out_res) 36 | assert len(changed) == 2 37 | assert h3.h3_get_resolution(changed[0].as_py()) == out_res 38 | assert h3.h3_get_resolution(changed[1].as_py()) == out_res 39 | 40 | 41 | def test_cells_resolution(): 42 | h3indexes = np.array([h3.geo_to_h3(10.2, 45.5, 5), h3.geo_to_h3(10.3, 45.1, 8)], dtype=np.uint64) 43 | res = cells_resolution(h3indexes) 44 | assert len(res) == 2 45 | assert res[0] == 5 46 | assert res[1] == 8 47 | -------------------------------------------------------------------------------- /h3ronpy/tests/arrow/test_utf8.py: -------------------------------------------------------------------------------- 1 | import h3.api.numpy_int as h3 2 | import numpy as np 3 | import pyarrow as pa 4 | import pytest 5 | from arro3.core import Array 6 | from h3ronpy import cells_parse, cells_to_string, cells_valid 7 | 8 | 9 | def test_cells_parse(): 10 | strings = np.array([h3.h3_to_string(h3.geo_to_h3(45.5, 10.2, 5)), "10.2, 45.5, 5"]) 11 | cells = cells_parse(strings) 12 | assert len(cells) == 2 13 | assert cells[0] == cells[1] 14 | 15 | 16 | def test_cells_parse_largeutf8(): 17 | # polars uses LargeUtf8 datatype for strings 18 | cells = cells_parse(pa.array(["801ffffffffffff"], type=pa.large_utf8())) 19 | assert len(cells) == 1 20 | 21 | 22 | def test_parse_cell_fail(): 23 | strings = np.array( 24 | [ 25 | "invalid", 26 | ] 27 | ) 28 | with pytest.raises(ValueError, match="non-parsable CellIndex"): 29 | cells_parse(strings) 30 | 31 | 32 | def test_parse_cell_set_invalid(): 33 | strings = np.array( 34 | [ 35 | "invalid", 36 | ] 37 | ) 38 | cells = cells_parse(strings, set_failing_to_invalid=True) 39 | assert len(cells) == 1 40 | assert not cells[0].is_valid 41 | 42 | 43 | def test_cells_valid(): 44 | input = np.array( 45 | [45, h3.geo_to_h3(45.5, 10.2, 5)], 46 | dtype=np.uint64, 47 | ) 48 | cells = cells_valid(input, booleanarray=False) 49 | assert len(cells) == 2 50 | assert cells.type == pa.uint64() 51 | assert not cells[0].is_valid 52 | assert cells[1].is_valid 53 | 54 | bools = cells_valid(input, booleanarray=True) 55 | assert len(bools) == 2 56 | assert bools.type == pa.bool_() 57 | assert bools[0].as_py() is False 58 | assert bools[1].as_py() is True 59 | 60 | assert pa.array(cells).is_valid() == pa.array(bools) 61 | 62 | 63 | def test_cells_to_string(): 64 | cells = np.array( 65 | [ 66 | h3.geo_to_h3(45.5, 10.2, 5), 67 | ] 68 | ) 69 | strings = cells_to_string(cells) 70 | assert len(strings) == len(cells) 71 | assert isinstance(strings, Array) 72 | assert strings.type == pa.large_utf8() 73 | assert strings[0] == "851f9923fffffff" 74 | -------------------------------------------------------------------------------- /h3ronpy/tests/arrow/test_vector.py: -------------------------------------------------------------------------------- 1 | import h3.api.numpy_int as h3 2 | import shapely 3 | from arro3.core import Array, DataType, Scalar 4 | from h3ronpy.vector import ContainmentMode, cells_to_wkb_points, geometry_to_cells 5 | from shapely import wkb 6 | from shapely.geometry import Point 7 | 8 | 9 | def test_geometry_to_cells(): 10 | geom = shapely.Polygon(((0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (1.0, 0.0), (0.0, 0.0))) 11 | cells = geometry_to_cells(geom, 5, containment_mode=ContainmentMode.IntersectsBoundary) 12 | assert isinstance(cells, Array) 13 | assert cells.type == DataType.uint64() 14 | assert len(cells) > 10 15 | 16 | 17 | def test_geometry_to_cells_central_park(): 18 | # Manhattan Central Park 19 | point = Point(-73.9575, 40.7938) 20 | 21 | arr = geometry_to_cells(point, 8).to_numpy() 22 | assert len(arr) == 1 23 | assert arr[0] == h3.geo_to_h3(point.y, point.x, 8) 24 | 25 | 26 | def test_coordinate_values_are_not_equal_issue_58(): 27 | # Step 1: Create a point (latitude and longitude) 28 | lat, lon = 37.7749, -122.4194 # Example coordinates (San Francisco) 29 | point = Point(lon, lat) # shapely expects (longitude, latitude) 30 | 31 | # Step 2: Convert the point to an H3 cell (resolution 9 for example) 32 | resolution = 9 33 | h3_cells = geometry_to_cells(point, resolution) 34 | 35 | # Step 3: Convert the H3 cell back to WKB points 36 | wkb_points = cells_to_wkb_points(h3_cells) 37 | 38 | assert len(wkb_points) == 1 39 | 40 | # Step 4: Decode the WKB point to a Shapely geometry 41 | for wkb_point in iter(wkb_points): 42 | assert isinstance(wkb_point, Scalar) # Ensure it's an arro3 Scalar 43 | shapely_point = wkb.loads(wkb_point.as_py()) 44 | assert int(lat) == int(shapely_point.y) 45 | assert int(lon) == int(shapely_point.x) 46 | -------------------------------------------------------------------------------- /h3ronpy/tests/pandas/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nmandery/h3ronpy/f4ab01637b105d07c1af63ce22467e4402d2dd03/h3ronpy/tests/pandas/__init__.py -------------------------------------------------------------------------------- /h3ronpy/tests/pandas/test_vector.py: -------------------------------------------------------------------------------- 1 | import geopandas as gpd 2 | import pandas as pd 3 | import pytest 4 | from h3ronpy import DEFAULT_CELL_COLUMN_NAME, ContainmentMode 5 | from h3ronpy.pandas.vector import cells_dataframe_to_geodataframe, geodataframe_to_cells 6 | from shapely.geometry import GeometryCollection, Point, Polygon 7 | 8 | from .. import load_africa 9 | 10 | 11 | def test_cells_dataframe_to_geodataframe(): 12 | df = pd.DataFrame( 13 | { 14 | DEFAULT_CELL_COLUMN_NAME: [ 15 | 0x8009FFFFFFFFFFF, 16 | ], 17 | "id": [ 18 | 5, 19 | ], 20 | } 21 | ) 22 | gdf = cells_dataframe_to_geodataframe(df) 23 | assert isinstance(gdf, gpd.GeoDataFrame) 24 | assert len(gdf) == len(df) 25 | assert (gdf[DEFAULT_CELL_COLUMN_NAME] == df[DEFAULT_CELL_COLUMN_NAME]).all() 26 | assert (gdf["id"] == df["id"]).all() 27 | assert gdf.geometry.geom_type[0] == "Polygon" 28 | 29 | 30 | def test_cells_dataframe_to_geodataframe_empty(): 31 | # https://github.com/nmandery/h3ron/issues/17 32 | df = pd.DataFrame({DEFAULT_CELL_COLUMN_NAME: []}) 33 | gdf = cells_dataframe_to_geodataframe(df) # should not raise an ValueError. 34 | assert gdf.empty 35 | 36 | 37 | def test_cells_geodataframe_to_cells(): 38 | africa = load_africa() 39 | df = geodataframe_to_cells(africa, 4) 40 | assert len(df) > len(africa) 41 | assert df.dtypes[DEFAULT_CELL_COLUMN_NAME] == "uint64" 42 | 43 | 44 | @pytest.mark.skip( 45 | reason="GeometryCollections are unsupported until https://github.com/geoarrow/geoarrow-rs/blob/3a2aaa883126274037cabaf46b1f5f6459938297/src/io/wkb/reader/geometry_collection.rs#L23 is fixed" 46 | ) 47 | def test_empty_geometrycollection_omitted(): 48 | gdf = gpd.GeoDataFrame( 49 | { 50 | "geometry": [ 51 | GeometryCollection(), 52 | ] 53 | }, 54 | crs="epsg:4326", 55 | ) 56 | df = geodataframe_to_cells(gdf, 4) 57 | assert len(df) == 0 58 | 59 | 60 | @pytest.mark.skip( 61 | reason="Empty points are unsupported until https://github.com/geoarrow/geoarrow-rs/issues/852 is fixed" 62 | ) 63 | def test_fail_on_empty_point(): 64 | gdf = gpd.GeoDataFrame( 65 | { 66 | "geometry": [ 67 | Point(), 68 | ] 69 | }, 70 | crs="epsg:4326", 71 | ) 72 | # Note: in geoarrow-rs this currently panics, and so raises a 73 | # pyo3_runtime.PanicException. geoarrow-rs should be updated to not panic here. 74 | with pytest.raises(Exception): 75 | geodataframe_to_cells(gdf, 4) 76 | 77 | 78 | def test_geometry_results_in_no_cells(): 79 | gdf = gpd.GeoDataFrame( 80 | { 81 | "geometry": [ 82 | Polygon( 83 | [ 84 | (1.100000, 4.50000), 85 | (1.100001, 4.50000), 86 | (1.100001, 4.50001), 87 | (1.100000, 4.50001), 88 | (1.100000, 4.50000), 89 | ] 90 | ), 91 | ], 92 | "col1": [1], 93 | }, 94 | crs="epsg:4326", 95 | ) 96 | df = geodataframe_to_cells(gdf, 4, containment_mode=ContainmentMode.ContainsCentroid) 97 | assert len(df) == 0 98 | 99 | 100 | def test_non_standard_geometry_column_name(): 101 | africa = load_africa() 102 | assert africa.geometry.name == "geometry" 103 | africa.rename_geometry("something_else", inplace=True) 104 | assert africa.geometry.name == "something_else" 105 | df = geodataframe_to_cells(africa, 4) 106 | assert len(df) > len(africa) 107 | assert df.dtypes[DEFAULT_CELL_COLUMN_NAME] == "uint64" 108 | -------------------------------------------------------------------------------- /h3ronpy/tests/polars/__init__.py: -------------------------------------------------------------------------------- 1 | import h3.api.numpy_int as h3 2 | import numpy as np 3 | import polars as pl 4 | 5 | 6 | def some_cell_series() -> pl.Series: 7 | return pl.Series( 8 | np.array( 9 | [ 10 | h3.geo_to_h3(10.3, 45.1, 8), 11 | ], 12 | dtype=np.uint64, 13 | ) 14 | ) 15 | -------------------------------------------------------------------------------- /h3ronpy/tests/polars/test_expr.py: -------------------------------------------------------------------------------- 1 | import h3.api.numpy_int as h3 2 | 3 | # register expressions with polars 4 | import h3ronpy.polars as _ # noqa: F401 5 | import numpy as np 6 | import polars as pl 7 | 8 | 9 | def some_cell_series() -> pl.Series: 10 | return pl.Series( 11 | np.array( 12 | [ 13 | h3.geo_to_h3(10.3, 45.1, 8), 14 | ], 15 | dtype=np.uint64, 16 | ) 17 | ) 18 | 19 | 20 | def test_expr_cells_resolution(): 21 | df = pl.DataFrame({"cells": some_cell_series()}) 22 | df.lazy().with_columns( 23 | [ 24 | pl.col("cells").h3.cells_resolution().alias("resolution"), 25 | ] 26 | ).collect() 27 | 28 | pl.col("cells") 29 | 30 | df = ( 31 | pl.DataFrame({"cells": some_cell_series()}) 32 | .lazy() 33 | .with_columns( 34 | [ 35 | pl.col("cells").h3.cells_resolution().alias("resolution"), 36 | ] 37 | ) 38 | .collect() 39 | ) 40 | assert df["resolution"].dtype == pl.UInt8 41 | assert df["resolution"][0] == 8 42 | 43 | 44 | def test_expr_grid_disk(): 45 | df = ( 46 | pl.DataFrame({"cells": some_cell_series()}) 47 | .lazy() 48 | .with_columns( 49 | [ 50 | pl.col("cells").h3.grid_disk(1).alias("disk"), 51 | ] 52 | ) 53 | .collect() 54 | ) 55 | assert df["disk"].dtype == pl.List 56 | assert df["disk"].dtype.inner == pl.UInt64 57 | assert len(df["disk"][0]) == 7 58 | 59 | 60 | def test_series(): 61 | s = some_cell_series() 62 | assert s.h3.cells_resolution()[0] == 8 63 | 64 | assert s.h3.change_resolution(5)[0] == 600436446234411007 65 | -------------------------------------------------------------------------------- /h3ronpy/tests/polars/test_series.py: -------------------------------------------------------------------------------- 1 | # register expressions with polars 2 | import h3ronpy.polars as _ # noqa: F401 3 | import polars as pl 4 | 5 | from . import some_cell_series 6 | 7 | 8 | def test_series_cells_resolution(): 9 | resolution = some_cell_series().h3.cells_resolution() 10 | assert resolution.dtype == pl.UInt8 11 | assert resolution[0] == 8 12 | -------------------------------------------------------------------------------- /h3ronpy/tests/test_transform.py: -------------------------------------------------------------------------------- 1 | from h3ronpy.raster import Transform 2 | 3 | 4 | def test_transform_cmp(): 5 | assert Transform(1, 1, 0, 1, 0, 1) == Transform(1, 1, 0, 1, 0, 1) 6 | assert Transform(1, 1, 0, 0, 0, 1) != Transform(1, 1, 0, 1, 0, 1) 7 | --------------------------------------------------------------------------------