pax_global_header00006660000000000000000000000064152064316710014517gustar00rootroot0000000000000052 comment=2883945e3434d30061b3cb18f7d0256f16242767 uber-h3-py-2883945/000077500000000000000000000000001520643167100136225ustar00rootroot00000000000000uber-h3-py-2883945/.github/000077500000000000000000000000001520643167100151625ustar00rootroot00000000000000uber-h3-py-2883945/.github/workflows/000077500000000000000000000000001520643167100172175ustar00rootroot00000000000000uber-h3-py-2883945/.github/workflows/build_docs.yml000066400000000000000000000014651520643167100220570ustar00rootroot00000000000000name: build_docs on: push: branches: [master] pull_request: branches: ['*'] permissions: contents: write jobs: build-docs: runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v6.0.2 with: submodules: recursive - uses: astral-sh/setup-uv@v7 with: python-version: '3.11' - uses: extractions/setup-just@v2 - name: Build the book run: just docs - name: Upload artifacts uses: actions/upload-artifact@v6.0.0 with: name: html path: docs/_build/html - name: Upload to GitHub Pages uses: peaceiris/actions-gh-pages@v4 if: github.ref == 'refs/heads/master' with: github_token: ${{ secrets.GITHUB_TOKEN }} publish_dir: docs/_build/html uber-h3-py-2883945/.github/workflows/lint_and_coverage.yml000066400000000000000000000011321520643167100234020ustar00rootroot00000000000000name: lint_and_coverage on: push: branches: [master] pull_request: branches: ['*'] jobs: linting: name: Lint and Coverage runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v6.0.2 with: submodules: recursive - uses: astral-sh/setup-uv@v7 with: python-version: '3.11' - uses: extractions/setup-just@v2 - name: Lint run: just lint - name: Install run: uv sync - name: Coverage - Library run: just ci-test - name: Coverage - Cython run: just ci-test-cython uber-h3-py-2883945/.github/workflows/tests.yml000066400000000000000000000020451520643167100211050ustar00rootroot00000000000000name: tests on: push: branches: [master] pull_request: branches: ['*'] jobs: tests: name: Py${{ matrix.python-version }} ${{ matrix.os }} runs-on: ${{ matrix.os }} strategy: matrix: os: [macos-latest, windows-latest, ubuntu-22.04] python-version: ['3.10', '3.11', '3.12', '3.13', '3.14'] steps: - uses: actions/checkout@v6.0.2 with: submodules: recursive - uses: astral-sh/setup-uv@v7 with: python-version: '${{ matrix.python-version }}' - uses: extractions/setup-just@v2 ## Start Windows stuff - uses: ilammy/msvc-dev-cmd@v1.13.0 if: startsWith(matrix.os, 'windows') - name: Set Windows Compiler if: startsWith(matrix.os, 'windows') run: | echo "CC=cl.exe" >> $GITHUB_ENV echo "CXX=cl.exe" >> $GITHUB_ENV ## End Windows stuff - name: Install run: uv sync - name: Tests run: just ci-test - name: Cython Tests run: just ci-test-cython uber-h3-py-2883945/.github/workflows/wheels.yml000066400000000000000000000137151520643167100212400ustar00rootroot00000000000000name: wheels on: push: branches: [master] pull_request: branches: ['*'] types: # Opened, synchronize, and reopened are the default types # We add ready_for_review to additionally trigger when converting from draft to non-draft # https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request - opened - synchronize - reopened - ready_for_review release: types: - published jobs: make_sdist: if: ${{ github.event_name != 'pull_request' || !github.event.pull_request.draft }} runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v6.0.2 with: submodules: recursive - uses: astral-sh/setup-uv@v7 with: python-version: '3.11' - name: Make sdist run: uv build --sdist # Test the sdist, NOT the local checkout. # Use a fresh venv + uv pip install (not uv sync, which would build from source). - name: Install from sdist run: | uv venv cp dist/h3-*.tar.gz h3.tar.gz uv pip install h3.tar.gz[numpy] pytest pytest-cov - name: Test sdist # run pytest directly from the venv to avoid `uv run` syncing from the local checkout run: .venv/bin/pytest - name: Upload artifacts to GitHub uses: actions/upload-artifact@v6.0.0 with: name: temp_result_sdist path: ./dist make_wheels: name: 'cibw: ${{ matrix.name }}' if: ${{ github.event_name != 'pull_request' || !github.event.pull_request.draft }} runs-on: ${{ matrix.os }} strategy: matrix: include: - os: macos-15 build: 'cp*-macosx_arm64' name: macOS15 - os: windows-latest build: 'cp3*-win_amd64' name: Windows 64-bit - os: windows-11-arm build: 'cp311-win_arm64' name: Windows ARM64 3.11 - os: windows-11-arm build: 'cp312-win_arm64' name: Windows ARM64 3.12 - os: windows-11-arm build: 'cp313-win_arm64' name: Windows ARM64 3.13 - os: windows-11-arm build: 'cp314-win_arm64' name: Windows ARM64 3.14 - os: ubuntu-22.04 build: 'cp*-manylinux_x86_64' name: Linux Intel glibc 64-bit - os: ubuntu-22.04 build: 'cp310-musllinux_x86_64' name: Linux Intel musl 64-bit 3.10 - os: ubuntu-22.04 build: 'cp311-musllinux_x86_64' name: Linux Intel musl 64-bit 3.11 - os: ubuntu-22.04 build: 'cp312-musllinux_x86_64' name: Linux Intel musl 64-bit 3.12 - os: ubuntu-22.04 build: 'cp313-musllinux_x86_64' name: Linux Intel musl 64-bit 3.13 - os: ubuntu-22.04 build: 'cp314-musllinux_x86_64' name: Linux Intel musl 64-bit 3.14 - os: ubuntu-22.04 build: 'cp310-manylinux_aarch64' name: Linux Aarch64 3.10 - os: ubuntu-22.04 build: 'cp311-manylinux_aarch64' name: Linux Aarch64 3.11 - os: ubuntu-22.04 build: 'cp312-manylinux_aarch64' name: Linux Aarch64 3.12 - os: ubuntu-22.04 build: 'cp313-manylinux_aarch64' name: Linux Aarch64 3.13 - os: ubuntu-22.04 build: 'cp314-manylinux_aarch64' name: Linux Aarch64 3.14 steps: - uses: actions/checkout@v6.0.2 with: submodules: recursive - uses: astral-sh/setup-uv@v7 - name: Set MSVC for Windows-x64 uses: microsoft/setup-msbuild@v2 if: matrix.os == 'windows-latest' with: msbuild-architecture: x64 - name: Set MSVC for Windows-ARM64 uses: microsoft/setup-msbuild@v2 if: matrix.os == 'windows-11-arm' with: msbuild-architecture: arm64 - name: Set Windows variables if: runner.os == 'Windows' shell: bash run: | echo "CC=cl.exe" >> $GITHUB_ENV echo "CXX=cl.exe" >> $GITHUB_ENV - name: Set up QEMU if: runner.os == 'Linux' uses: docker/setup-qemu-action@v3.7.0 with: platforms: aarch64 - uses: pypa/cibuildwheel@v3.3.1 env: CIBW_BUILD_FRONTEND: 'build[uv]' CIBW_TEST_REQUIRES: pytest pytest-cov numpy CIBW_TEST_COMMAND: pytest {project}/tests CIBW_ARCHS_LINUX: auto aarch64 CIBW_BUILD: ${{ matrix.build }} - name: Check with Twine run: uvx twine check wheelhouse/* - name: Upload artifacts to GitHub uses: actions/upload-artifact@v6.0.0 with: name: temp_result_${{ matrix.os }}-${{ strategy.job-index }} path: wheelhouse/*.whl merge: needs: [make_sdist, make_wheels] runs-on: ubuntu-latest steps: - name: Merge Artifacts uses: actions/upload-artifact/merge@v6 with: name: wheels_and_sdist pattern: temp_result_* delete-merged: true retention-days: 7 compression-level: 9 check_merged: needs: [merge] runs-on: ubuntu-latest steps: - uses: astral-sh/setup-uv@v7 with: enable-cache: false ignore-empty-workdir: true - name: Download merged artifacts uses: actions/download-artifact@v7.0.0 with: name: wheels_and_sdist path: dist - name: Check all wheels and sdist with Twine run: uvx twine check dist/* to_pypi: needs: [check_merged] if: github.event_name == 'release' && github.event.action == 'published' runs-on: ubuntu-latest steps: - uses: actions/download-artifact@v7.0.0 with: name: wheels_and_sdist path: dist - uses: pypa/gh-action-pypi-publish@v1.13.0 with: password: ${{ secrets.pypi_password }} uber-h3-py-2883945/.gitignore000066400000000000000000000010171520643167100156110ustar00rootroot00000000000000# Byte-compiled / optimized *.py[cod] __pycache__/ # C extensions *.so # Generated C code from Cython test tests/**/*.c # Cython HTML annotations *.html # Build artifacts build/ dist/ *.egg-info # Coverage .coverage coverage.xml coverage.lcov htmlcov/ # Test .pytest_cache/ # Tools .ruff_cache/ .cache/ # Docs _build/ docs/_build/ # Environment .venv/ uv.lock # OS .DS_Store # Editor backup files *~ \#*\# .\#* .*.sw* # IDE .idea/ # IPython / Jupyter .ipynb_checkpoints/ # Legacy build dirs _skbuild/ h3c/ h3/out/ uber-h3-py-2883945/.gitmodules000066400000000000000000000001141520643167100157730ustar00rootroot00000000000000[submodule "src/h3lib"] path = src/h3lib url = https://github.com/uber/h3 uber-h3-py-2883945/CHANGELOG.md000066400000000000000000000140661520643167100154420ustar00rootroot00000000000000# CHANGELOG ## Versioning The H3 core library adheres to [Semantic Versioning](http://semver.org/). H3-Py has a `major.minor.patch` version scheme. The major and minor version numbers of H3-Py are the major and minor version of the bound core library, respectively. The patch version is incremented independently of the core library. We use [this changelog structure](http://keepachangelog.com/). Because H3-Py is versioned in lockstep with the H3 core library, please avoid adding features or APIs which do not map onto the [H3 core API](https://uber.github.io/h3/#/documentation/api-reference/). ## Unreleased ## [4.5.0] - 2026-05-29 - Final v4.5 release includes all v4.5.x changes below ## [4.5.0a3] - 2026-05-22 - Update `h3lib` to v4.5.0 release (#491) ## [4.5.0a2] - 2026-02-27 - `cells_to_h3shape`/`cells_to_geo` now handle all valid cell sets, including sets that would produce global polygons crossing the antimeridian, poles, or are larger than a hemisphere - Duplicate cells now raise `H3DuplicateInputError` - Mixed-resolution cells now raise `H3ResMismatchError` - Internal: switch from linked-list to array-based C API for cells-to-polygon conversion ## [4.5.0a1] - 2026-02-25 - Update `h3lib` with draft v4.5 changes - Drop Python 3.8 and 3.9 (EOL); minimum is now Python 3.10 - Document Python 3.14 support - Migrate build tooling from Makefile/pip to justfile/uv, including CI ## [4.4.2] - 2026-01-29 - Check for error from `cellsToLinkedMultiPolygon` (#477) ## [4.4.1] - 2025-12-15 - Fix PyPI publishing issue (#474) ## [4.4.0] - 2025-12-12 (never published on PyPI) - Update `h3lib` to v4.4.1. (#470) - Add new error codes: - `H3IndexInvalidError` - `H3BaseCellDomainError` - `H3DigitDomainError` - `H3DeletedDigitError` - Add new functions: - `is_valid_index` - `get_index_digit` - `construct_cell` - `deconstruct_cell` ## [4.3.1] - 2025-08-10 - Build wheels for Windows on ARM64 (#463) ## [4.3.0] - 2025-06-17 - Update `h3lib` to v4.3.0. (#461) - Uses `gridRing` (uber/h3#1016) instead of Cython fallback logic. ## [4.2.2] - 2025-03-10 - Update h3lib to v4.2.1. (#450) ## [4.2.1] - 2025-01-31 - Correct pyproject.toml links for PyPI (#441) - Update coverage badge (#443) ## [4.2.0] - 2025-01-26 - Update h3lib to v4.2.0. (#432) - Add `h3shape_to_cells_experimental` (#436) - Add `polygon_to_cells_experimental` alias ## [4.1.2] - 2024-10-26 - Build Python 3.13 wheels (#425) ## [4.1.1] - 2024-10-13 - Make sure docs and PyPI page are up-to-date. ## [4.1.0] - 2024-10-13 - Final v4.1 release inculdes all v4.x changes below. ## [4.1.0b3] - 2024-10-05 - Allow for `str` subtypes, like `numpy.str_` (#408) ## [4.1.0b2] - 2024-09-27 - Add `cell_to_child_pos`, `child_pos_to_cell`, `cell_to_children_size` (#405) ## [4.1.0b1] - 2024-09-26 - Bump h3lib to v4.1.0 (#402) - Add `polygon_to_cells` alias (#399) ## [4.0.0b7] - 2024-09-04 - Use `pyproject.toml` and `scikit-build-core` (#378) ## [4.0.0b6] - 2024-09-03 - Added bindings for `cellToVertex`, `cellToVertexes`, `vertexToLatLng`, and `isValidVertex` (#388) ## [4.0.0b5] - 2024-05-19 - Rename `H3Poly` and `H3MultiPoly` to `LatLngPoly` and `LatLngMultiPoly` (#364) - Add ability to convert from `__geo_interface__` objects with Z-coordinate (#371) ## [4.0.0b4] - 2024-04-14 No changes, just testing: #360 ## [4.0.0b3] - 2024-03-11 - Change supported Python versions to 3.7, 3.8, 3.9, 3.10, 3.11, 3.12 (#324, #325, #347, #348) - New `h3.Polygon()`/GeoJSON interface (#301) - Use functions instead of methods for the interface functions (#334) - Use `list` instead of `set` for unordered Python outputs (#339) ## [4.0.0b2] - 2022-11-23 - Build Python 3.11 wheels (#297) ## [4.0.0b1] - 2022-08-23 Beta release; feedback welcome! - Move to v4.0 of the core C library (#250) + Function name changes: https://github.com/uber/h3/blob/master/dev-docs/RFCs/v4.0.0/names_for_concepts_types_functions.md + New error system + New interfaces for `polygon_to_cells` and `cells_to_polygons` involving a new `h3.Polygon` class - Expose the Cython API (#234) + Note: The Cython API is not yet stable, and should only be used for experimentation ## [3.7.4] - 2022-04-14 - Website and API documentation; along with docstring cleanup. - Add support for linters and IDE tooling (#213) - Remove Py 3.5 wheel for Windows (#214) - Py 3.10 wheels and on more architectures (#220, #221, #223, #225) - Greatly reduce sdist size (#227, #229) ## [3.7.3] - 2021-06-12 - Wheels for Linux Aarch64 (#189) ## [3.7.2] - 2021-03-01 - Add vectorized h3_to_parent and h3_get_resolution (#166) ## [3.7.1] - 2020-12-18 - fix for #169: `h3_distance` error reporting (#175) - build Python 3.9 wheel for Mac (#175) - bump h3lib version to v3.7.1 (#175) ## [3.7.0] - 2020-10-02 - Add functions (#171) + `cell_area` + `exact_edge_length` + `point_dist` ## [3.6.4] - 2020-07-20 - Add `requirements.in` for `pip-compile` usage (#157) - Update `h3-c` to v3.6.4 (#157) - Add functions: + `experimental_local_ij_to_h3` (#155) + `experimental_h3_to_local_ij` (#155) + `h3.unstable.vect.cell_haversine` (#147) + `h3.unstable.vect.geo_to_h3` (#147) + Prototype v4 names under `h3.unstable.v4` (#146) ## [3.6.3] - 2020-06-04 - Add functions: + `get_res0_indexes` + `h3_to_center_child` + `h3_get_faces` ## [3.6.2] - 2020-06-02 - Improve error reporting on `str_to_int` (https://github.com/uber/h3-py/pull/127) - Build Linux wheels for Python 2.7 ## [3.6.1] - 2020-05-29 - Switch to Cython wrapper implementation - Pre-built wheels on PyPI for Linux/Mac/Windows - Provide multiple APIs: + `h3.api.basic_str` + `h3.api.basic_int` + `h3.api.numpy_int` + `h3.api.memview_int` ## [3.4.3] - 2019-04-18 - Removed null values in k_ring_distances. - Support on Windows Platform - Fixed some install problems ## [3.4.2] - 2019-03-13 - Added h3_line support. ## [3.1.0] - 2018-09-06 ### Added - Added h3_distance function. ### Changed - Updated the core library to v3.1.0. ## [3.0.0] - 2018-07-24 ### Added - First public release. uber-h3-py-2883945/CMakeLists.txt000066400000000000000000000023021520643167100163570ustar00rootroot00000000000000cmake_minimum_required(VERSION 3.15...3.26) project(${SKBUILD_PROJECT_NAME} LANGUAGES C) set(CMAKE_POSITION_INDEPENDENT_CODE ON) # Always make a release build set(CMAKE_BUILD_TYPE Release) find_package( Python COMPONENTS Interpreter Development.Module REQUIRED) # Avoid building tooling we won't need for release # See all options with `cmake -LA` in an `h3/build` directory, # or at https://h3geo.org/docs/next/core-library/compilation-options/ macro(turn_off option_name) set(${option_name} OFF CACHE BOOL "" FORCE) endmacro() turn_off(BUILD_ALLOC_TESTS) turn_off(BUILD_BENCHMARKS) turn_off(BUILD_FILTERS) turn_off(BUILD_FUZZERS) turn_off(BUILD_GENERATORS) turn_off(BUILD_TESTING) turn_off(ENABLE_COVERAGE) turn_off(ENABLE_DOCS) turn_off(ENABLE_FORMAT) turn_off(ENABLE_LIBFUZZER) turn_off(ENABLE_LINTING) # Build the core library as static set(BUILD_SHARED_LIBS OFF) add_subdirectory(src/h3lib) # Build the rest (other than the core library dependency) as shared set(BUILD_SHARED_LIBS ON) add_subdirectory(src/h3) # Include built h3api.h for Cython API install( FILES "${CMAKE_CURRENT_BINARY_DIR}/src/h3lib/src/h3lib/include/h3api.h" DESTINATION ${SKBUILD_PROJECT_NAME}/_cy) uber-h3-py-2883945/LICENSE000066400000000000000000000261361520643167100146370ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. uber-h3-py-2883945/dev_notes.md000066400000000000000000000034471520643167100161420ustar00rootroot00000000000000# Development notes for `h3-py` ## Install for development This repo uses [`just`](https://github.com/casey/just) for running common development commands. To get started: - `git clone --recurse-submodules git@github.com:uber/h3-py.git` - or fork the repository and `git clone --recurse-submodules https://github.com/{username}/h3-py.git` - `cd h3-py` - `just lint` - `just test` - `just test-cython` To build and view docs: - `just docs` - `just view` ## Docstrings - Follow something like the [`numpydoc` docstring guide](https://numpydoc.readthedocs.io/en/latest/format.html) or the [`pandas` docstring guide](https://python-sprints.github.io/pandas/guide/pandas_docstring.html) ## Updating the H3 submodule ```sh cd src/h3lib git checkout master git pull cd .. git add h3lib git commit ... ``` for a specific version tag: ```sh cd src/h3lib git checkout v3.7.1 # or whatever version tag you'd like cd .. git add h3lib git commit ... ``` ## Resetting the submodule For when moving between `h3-py` branches using different versions of the `h3lib` submodule. ```sh git submodule deinit -f . git submodule update --init ``` ## Releasing a new version - update `CHANGELOG.md` to reflect any changes since the last release - update the `h3-py` version in `pyproject.toml` - if updating the C `h3lib` version, update the version badge on the readme - create PR, get reviews, and merge with these changes - go to https://github.com/uber/h3-py/releases and "Draft a new release" - set the tag version and the release to the version. e.g., `v3.7.2` (alternatively: `git tag v3.7.2` && `git push origin --tags`) - add the updated `CHANGELOG.md` text to the release notes - publish release - GitHub actions should trigger on the release event and then go on to build and upload the wheels to PyPI uber-h3-py-2883945/docs/000077500000000000000000000000001520643167100145525ustar00rootroot00000000000000uber-h3-py-2883945/docs/_changelog.md000066400000000000000000000001111520643167100171530ustar00rootroot00000000000000```{include} ../CHANGELOG.md :relative-docs: docs/ :relative-images: ``` uber-h3-py-2883945/docs/_config.yml000066400000000000000000000007151520643167100167040ustar00rootroot00000000000000title: h3-py author: 'Uber Technologies' copyright: "2024" logo: logo.svg html: favicon: logo.svg home_page_in_navbar: false use_edit_page_button: true use_repository_button: true baseurl: https://uber.github.io/h3-py/ repository: url: https://github.com/uber/h3-py branch: master path_to_book: docs sphinx: extra_extensions: - 'sphinx.ext.autodoc' - 'sphinx.ext.autosummary' - 'sphinx.ext.napoleon' - 'sphinx.ext.viewcode' uber-h3-py-2883945/docs/_toc.yml000066400000000000000000000004761520643167100162300ustar00rootroot00000000000000# Learn more at https://jupyterbook.org/customize/toc.html format: jb-book root: intro parts: - caption: h3-py chapters: - file: api_quick - file: api_verbose - file: polygon_tutorial - file: api_comparison - caption: Sundries chapters: - file: _changelog - file: contributing - file: project_links uber-h3-py-2883945/docs/api_comparison.md000066400000000000000000000157631520643167100201130ustar00rootroot00000000000000# Comparison of APIs The `h3-py` library provides several APIs which share the same functions, but differ in their input and output types: - `h3.api.basic_str` (the *default* `import h3` is equivalent to `import h3.api.basic_str as h3`) - `h3.api.basic_int` - `h3.api.numpy_int` - `h3.api.memview_int` ## Why are there multiple APIs? Under the hood in the C library, H3 indices are represented as unsigned 64-bit integers (`uint64_t`), and collections of indexes are represented with C arrays. For human readability, we often represent integer indices (`578536630256664575`) in their hexadecimal format (`8075fffffffffff`). The *default* Python API (via `import h3` or `import h3.api.basic_str`), represents indices in their hexadecimal format as a Python `str`s and uses standard Python collection types like `set` and `list`. These conventions make it more natural to work with `h3-py` objects in Python, but come at the cost of converting back and forth between the C and Python representations. Alternative APIs like `h3.api.numpy_int` give up some of the Python conveniences to gain speed by dealing with the C memory more directly and avoiding the conversion costs. Multiple APIs give users the power to choose the one that best fits their needs, based on speed and convenience. ```{tip} Note that the APIs are all 100% compatible, and it is easy to convert between them with functions like `int_to_str` (links!) and `str_to_int`. For example, one common pattern is to use `h3.api.numpy_int` for any computationally-heavy work, and convert the output to `str` and `list`/`set` (akin to `h3.api.basic_str`) to inspect or report the results. ``` ## API Options ### `h3.api.basic_str` H3 indexes are represented as Python `str`s, using `list` and `set` for collections. ```python >>> import h3 >>> h = h3.latlng_to_cell(0, 0, 0) >>> h '8075fffffffffff' >>> h3.grid_ring(h, 1) {'8055fffffffffff', '8059fffffffffff', '807dfffffffffff', '8083fffffffffff', '8099fffffffffff'} ``` ````{note} `basic_str` is the default API provided when you `import h3`. That is, ```python import h3.api.basic_str as h3 ``` and ```python import h3 ``` are equivalent. ```` ### `h3.api.basic_int` H3 indexes are represented as Python `int`s, using `list` and `set` for collections. ```python >>> import h3.api.basic_int as h3 >>> h = h3.latlng_to_cell(0, 0, 0) >>> h 578536630256664575 >>> h3.grid_ring(h, 1) {577973680303243263, 578044049047420927, 578677367745019903, 578782920861286399, 579169948954263551} ``` ### `h3.api.numpy_int` H3 indexes are represented as `numpy.uint64`s, using `numpy.ndarray` for collections. The intention is for this API to be faster and more memory-efficient by not requiring `int` to `str` conversion and by using no-copy `numpy` arrays instead of Python `list`s and `set`s. ```python >>> import h3.api.numpy_int as h3 >>> h = h3.latlng_to_cell(0, 0, 0) >>> h 578536630256664575 >>> h3.grid_ring(h, 1) array([578782920861286399, 578044049047420927, 577973680303243263, 578677367745019903, 579169948954263551], dtype=uint64) ``` ```{note} `h3-py` has no runtime dependencies on other libraries, so the standard `pip install h3` will not install any additional libraries. However, `h3.api.numpy_int` does require `numpy`. To install `numpy` (if it isn't already) along with `h3`, you can run `pip install h3[numpy]`. ``` ### `h3.api.memview_int` H3 indexes are represented as `uint64`s, using Python [`memoryview` objects](https://docs.python.org/dev/library/stdtypes.html#memoryview) for collections. This API has the same benefits as `numpy_int`, except it uses (the less well-known but dependency-free) `memoryview`. In fact, `h3.api.numpy_int` is essentially just a light wrapper around `h3.api.memview_int`. ```python >>> import h3.api.memview_int as h3 >>> h = h3.latlng_to_cell(0, 0, 0) >>> h 578536630256664575 >>> mv = h3.grid_ring(h, 1) >>> mv >>> mv[0] 578782920861286399 >>> list(mv) [578782920861286399, 578044049047420927, 577973680303243263, 578677367745019903, 579169948954263551] ``` ````{warning} When using the `memview_int` API with the `numpy` library, note that the [`numpy.array`](https://numpy.org/doc/stable/reference/generated/numpy.array.html) conversion function **creates a copy** of the data. On the other hand, [`numpy.asarray`](https://numpy.org/doc/stable/reference/generated/numpy.asarray.html) **does not create a copy**, and the result points to the same memory location as the original `memoryview` object. For example, consider the setup: ```python >>> import h3.api.memview_int as h3 >>> import numpy as np >>> h = h3.latlng_to_cell(0, 0, 0) >>> mv = h3.grid_ring(h, 1) >>> list(mv) [578782920861286399, 578044049047420927, 577973680303243263, 578677367745019903, 579169948954263551] ``` Running `a = np.array(mv)` **creates a copy** of the memory view, so modifying `mv` leaves `a` unchanged: ```python >>> a = np.array(mv) >>> mv[0] = 0 >>> a array([578782920861286399, 578044049047420927, 577973680303243263, 578677367745019903, 579169948954263551], dtype=uint64) ``` Running `a = np.asarray(mv)` **does not create a copy**, so modifying `mv` also modifies `a`: ```python >>> mv = h3.grid_ring(h, 1) >>> a = np.asarray(mv) >>> mv[0] = 0 >>> a array([ 0, 578044049047420927, 577973680303243263, 578677367745019903, 579169948954263551], dtype=uint64) ``` ```` ## API speed comparison We time an example task to compare across APIs. We also compare the option of doing most of the computation in one of the faster APIs (using the `int` representation of H3 indexes), and then converting the results to the more familiar format of Python `str` objects. ### Example code ```python import h3 import h3.api.numpy_int def compute(h3_lib, N=100): h = h3_lib.latlng_to_cell(0, 0, 9) out = h3_lib.grid_disk(h, N) out = h3_lib.compact_cells(out) return out def compute_and_convert(h3_lib, N=100): out = compute(h3_lib, N) out = [h3.int_to_str(h) for h in out] return out ``` ### Timing results ```{attention} These are example timings on a single computer; you can run the benchmarks yourself with the [original notebook](https://github.com/uber/h3-py-notebooks/blob/master/notebooks/time_h3_apis.ipynb). ``` | API | `compute()` | `compute_and_convert()` | |----------------------|-------------|-------------------------| | `h3.api.basic_str` | `59.8 ms` | n/a | | `h3.api.basic_int` | `35.4 ms` | `35.6 ms` | | `h3.api.memview_int` | `7.12 ms` | `7.76 ms` | | `h3.api.numpy_int` | `7.06 ms` | `7.61 ms` | ```{note} For this example, we typically see about a 6--8x speedup between: - computing with the `h3.api.basic_str` interface - computing with the `h3.api.numpy_int` interface, and then converting the results back to `str` ``` uber-h3-py-2883945/docs/api_quick.md000066400000000000000000000074211520643167100170450ustar00rootroot00000000000000# API Quick Reference ```{tip} Function names changed significantly with `h3-py` `v4.0` to align with the C library: https://h3geo.org/docs/library/migration-3.x/functions ``` We list the functions that are shared between the [provided APIs](api_comparison). The APIs differ only in their input/output types (e.g., `int` vs. `str` or `list` vs `numpy.array`). These functions correspond to those explained in the [H3 C library documentation](https://h3geo.org/docs/api/indexing), and should be generally aligned with the [other language bindings](https://h3geo.org/docs/community/bindings). ## Identification ```{eval-rst} .. currentmodule:: h3 .. autosummary:: is_valid_cell is_pentagon is_res_class_III is_valid_directed_edge is_valid_vertex is_valid_index ``` ## Index representation ```{eval-rst} .. currentmodule:: h3 .. autosummary:: int_to_str str_to_int ``` ## Cell properties ```{eval-rst} .. currentmodule:: h3 .. autosummary:: get_res0_cells get_pentagons get_num_cells get_resolution get_base_cell_number get_index_digit construct_cell deconstruct_cell ``` ## Geographic coordinates Functions relating H3 objects to geographic (lat/lng) coordinates. ```{eval-rst} .. currentmodule:: h3 .. autosummary:: latlng_to_cell cell_to_latlng cell_area edge_length cell_to_boundary directed_edge_to_boundary great_circle_distance average_hexagon_area average_hexagon_edge_length ``` ## Hierarchical relationships ```{eval-rst} .. currentmodule:: h3 .. autosummary:: cell_to_parent cell_to_children cell_to_center_child cell_to_children_size cell_to_child_pos child_pos_to_cell compact_cells uncompact_cells ``` ## Cell grid relationships ```{eval-rst} .. currentmodule:: h3 .. autosummary:: grid_ring grid_disk grid_distance are_neighbor_cells grid_path_cells ``` ## Edges ```{eval-rst} .. currentmodule:: h3 .. autosummary:: cells_to_directed_edge directed_edge_to_cells get_directed_edge_origin get_directed_edge_destination origin_to_directed_edges ``` ## Vertexes ```{eval-rst} .. currentmodule:: h3 .. autosummary:: cell_to_vertex cell_to_vertexes vertex_to_latlng ``` ## Polygon interface The ``LatLngPoly`` and ``LatLngMultiPoly`` objects and their related functions allow users to represent (multi)polygons of lat/lng points and convert back and forth between H3 cells. The objects and functions also compatible with the popular [``__geo_interface__`` protocol](https://gist.github.com/sgillies/2217756), which is used by Python geospatial libraries like [GeoPandas](https://geopandas.org/en/stable/docs/reference/api/geopandas.GeoDataFrame.__geo_interface__.html) and many plotting libraries. See the [polygon tutorial](./polygon_tutorial.ipynb) for a walkthrough. ```{tip} As with other `h3-py` functions, the polygon interface expects coordinate pairs in **lat/lng** order. Note that this is reversed from [``__geo_interface__``](https://gist.github.com/sgillies/2217756) objects, which are given in **lng/lat** order. ``` ### Polygon objects ```{eval-rst} .. currentmodule:: h3 .. autosummary:: H3Shape LatLngPoly LatLngMultiPoly ``` ### Conversion functions ```{eval-rst} .. currentmodule:: h3 .. autosummary:: h3shape_to_geo geo_to_h3shape h3shape_to_cells cells_to_h3shape geo_to_cells cells_to_geo ``` #### Additional functions ```{eval-rst} .. currentmodule:: h3 .. autosummary:: polygon_to_cells polygon_to_cells_experimental h3shape_to_cells_experimental ``` ## Specialized functions ### `h3-py` library ```{eval-rst} .. currentmodule:: h3 .. autosummary:: versions ``` ### IJ-indexing ```{eval-rst} .. currentmodule:: h3 .. autosummary:: get_icosahedron_faces cell_to_local_ij local_ij_to_cell ``` uber-h3-py-2883945/docs/api_verbose.md000066400000000000000000000003531520643167100173730ustar00rootroot00000000000000# API Verbose Reference This page gives the full documentation for each function and object. Refer back to [](api_quick.md) for context and logical groupings. ```{eval-rst} .. automodule:: h3 :members: :imported-members: ``` uber-h3-py-2883945/docs/contributing.md000066400000000000000000000005731520643167100176100ustar00rootroot00000000000000# Contributing Contributions from the community are welcome! Please feel free to [file an issue](https://github.com/uber/h3-py/issues), open a [pull request](https://github.com/uber/h3-py/pulls), or start a [discussion in Github](https://github.com/uber/h3-py/discussions) or in the [H3 Slack](https://join.slack.com/t/h3-core/shared_invite/zt-g6u5r1hf-W_~uVJmfeiWtMQuBGc1NNg).uber-h3-py-2883945/docs/intro.md000066400000000000000000000001061520643167100162240ustar00rootroot00000000000000```{include} ../readme.md :relative-docs: docs/ :relative-images: ``` uber-h3-py-2883945/docs/logo.svg000066400000000000000000000071711520643167100162410ustar00rootroot00000000000000 uber-h3-py-2883945/docs/polygon_tutorial.ipynb000066400000000000000000000635741520643167100212460ustar00rootroot00000000000000{ "cells": [ { "cell_type": "markdown", "id": "03ee1f11-7a60-4761-aaf7-673ea04e7dca", "metadata": {}, "source": [ "# Polygon Tutorial\n", "\n", "`h3-py` can convert between sets of cells and GeoJSON-like polygon and multipolygon shapes.\n", "\n", "We use the abstract base class `H3Shape` and its concrete child classes `LatLngPoly` and `LatLngMultiPoly`\n", "to represent these shapes. \n", "Any references or function names that use \"H3Shape\" will apply to both `LatLngPoly` and `LatLngMultiPoly` objects.\n", "\n", "`h3-py` is also compatible with Python objects that implement `__geo_interface__` (https://gist.github.com/sgillies/2217756),\n", "making it easy to interface with other Python geospatial libraries.\n", "We'll refer to any such object as a \"geo\" or \"geo object\".\n", "\n", "`LatLngPoly` and `LatLngMultiPoly` both implement `__geo_interface__` (making them geo objects themselves),\n", "and can be created from other geo objects, like Shapely's `Polygon` or `MultiPolygon` objects that occur\n", "when using `geopandas`.\n", "\n", "Below, we'll explain the `h3-py` API for working with these shapes, and how the library interacts\n", "with other Python geospatial libraries.\n", "\n", "To start, we'll import relevant libraries, and define some plotting helper functions to visualize the shapes we're dealing with." ] }, { "cell_type": "code", "execution_count": null, "id": "0f5454f0-f65e-491c-b534-f1ef08320263", "metadata": {}, "outputs": [], "source": [ "import h3\n", "\n", "import geopandas\n", "import geodatasets\n", "import contextily as cx\n", "import matplotlib.pyplot as plt\n", "\n", "\n", "def plot_df(df, column=None, ax=None):\n", " \"Plot based on the `geometry` column of a GeoPandas dataframe\"\n", " df = df.copy()\n", " df = df.to_crs(epsg=3857) # web mercator\n", "\n", " if ax is None:\n", " _, ax = plt.subplots(figsize=(8,8))\n", " ax.get_xaxis().set_visible(False)\n", " ax.get_yaxis().set_visible(False)\n", "\n", " df.plot(\n", " ax=ax,\n", " alpha=0.5, edgecolor='k',\n", " column=column, categorical=True,\n", " legend=True, legend_kwds={'loc': 'upper left'},\n", " )\n", " cx.add_basemap(ax, crs=df.crs, source=cx.providers.CartoDB.Positron)\n", "\n", "\n", "def plot_shape(shape, ax=None):\n", " df = geopandas.GeoDataFrame({'geometry': [shape]}, crs='EPSG:4326')\n", " plot_df(df, ax=ax)\n", "\n", "\n", "def plot_cells(cells, ax=None):\n", " shape = h3.cells_to_h3shape(cells)\n", " plot_shape(shape, ax=ax)\n", "\n", "\n", "def plot_shape_and_cells(shape, res=9):\n", " fig, axs = plt.subplots(1,2, figsize=(10,5), sharex=True, sharey=True)\n", " plot_shape(shape, ax=axs[0])\n", " plot_cells(h3.h3shape_to_cells(shape, res), ax=axs[1])\n", " fig.tight_layout()" ] }, { "cell_type": "markdown", "id": "c5a41e2b-0c9e-45f5-8005-51af2ef8af32", "metadata": {}, "source": [ "# LatLngPoly\n", "\n", "We can create a simple `LatLngPoly` object by providing a list of the **latitude/longitude pairs** that describe its exterior.\n", "Optionally, holes can be added to the polygon by appending additional lat/lng lists do describe them." ] }, { "cell_type": "code", "execution_count": null, "id": "1e9e3e2a-a4cf-43df-a498-ee7d15a8cc4b", "metadata": {}, "outputs": [], "source": [ "outer = [\n", " (37.804, -122.412),\n", " (37.778, -122.507),\n", " (37.733, -122.501)\n", "]\n", "\n", "poly = h3.LatLngPoly(outer)\n", "print(poly)\n", "plot_shape(poly)" ] }, { "cell_type": "code", "execution_count": null, "id": "6872b341-afc9-4405-b27a-b5f2b8a19847", "metadata": {}, "outputs": [], "source": [ "hole1 = [\n", " (37.782, -122.449),\n", " (37.779, -122.465),\n", " (37.788, -122.454),\n", "]\n", "\n", "poly = h3.LatLngPoly(outer, hole1)\n", "print(poly)\n", "plot_shape(poly)" ] }, { "cell_type": "code", "execution_count": null, "id": "2d7ae0fb-fa96-46fe-8d06-7778575642dd", "metadata": {}, "outputs": [], "source": [ "hole2 = [\n", " (37.771, -122.484),\n", " (37.761, -122.481),\n", " (37.758, -122.494),\n", " (37.769, -122.496),\n", "]\n", "\n", "poly = h3.LatLngPoly(outer, hole1, hole2)\n", "print(poly)\n", "plot_shape(poly)" ] }, { "cell_type": "markdown", "id": "62317cfd-8a0e-4c8a-b899-131acdec4305", "metadata": {}, "source": [ "## String representation and attributes\n", "\n", "The `LatLngPoly` string representation given by its `__repr__` shows the number of vertices in the outer loop of the polygon, followed\n", "by the number of vertices in each hole.\n", "\n", "A representation like\n", "\n", "```\n", "\n", "```\n", "\n", "denotes a polygon whose outer boundary consists of 3 vertices and which has 2 holes,\n", "with 3 and 4 vertices respectively.\n", "\n", "We can access the coordinates describing the polygon through the attributes:\n", "\n", "- `LatLngPoly.outer` gives the list of lat/lng points making up the outer loop of the polygon.\n", "- `LatLngPoly.holes` gives each of the lists of lat/lng points making up the holes of the polygon." ] }, { "cell_type": "code", "execution_count": null, "id": "97c8aa44-12fb-4c47-9254-d931522575ed", "metadata": {}, "outputs": [], "source": [ "poly = h3.LatLngPoly(outer, hole1, hole2)\n", "poly" ] }, { "cell_type": "code", "execution_count": null, "id": "150ae04c-bae2-4b7e-88d0-f5fcf0835e7b", "metadata": {}, "outputs": [], "source": [ "poly.outer" ] }, { "cell_type": "code", "execution_count": null, "id": "84061e68-60f3-4df4-a279-a0630af7dcc2", "metadata": {}, "outputs": [], "source": [ "poly.holes" ] }, { "cell_type": "markdown", "id": "ae175854-6770-44aa-b796-8d9390f7c856", "metadata": {}, "source": [ "## `__geo_interface__`\n", "\n", "`LatLngPoly.__geo_interface__` gives a GeoJSON representation of the polygon as described in https://gist.github.com/sgillies/2217756\n", "\n", "**Note the differences in this representation**: Points are given in **lng/lat** order (but the `LatLngPoly` constructor expects **lat/lng** order), and the last vertex repeats the first." ] }, { "cell_type": "code", "execution_count": null, "id": "fb79c742-27aa-481c-971b-28c100109efe", "metadata": {}, "outputs": [], "source": [ "d = poly.__geo_interface__\n", "d" ] }, { "cell_type": "code", "execution_count": null, "id": "fc1b0ae8-bcce-4e5f-ad88-57e3797518f4", "metadata": {}, "outputs": [], "source": [ "print(poly.outer)\n", "print(poly.holes)" ] }, { "cell_type": "markdown", "id": "8f567fae-9f4e-472f-afcd-bd6f7bb32cba", "metadata": {}, "source": [ "We can create an `LatLngPoly` object from a GeoJSON-like dictionary or an object that implements `__geo_interface__` using `h3.geo_to_h3shape()`." ] }, { "cell_type": "code", "execution_count": null, "id": "42d7d580-2bba-4cd3-8d13-2434323e4081", "metadata": {}, "outputs": [], "source": [ "h3.geo_to_h3shape(d)" ] }, { "cell_type": "code", "execution_count": null, "id": "c70b6fa3-d85d-420e-a295-a75a32bf3dad", "metadata": {}, "outputs": [], "source": [ "class MockGeo:\n", " def __init__(self, d):\n", " self.d = d\n", "\n", " @property\n", " def __geo_interface__(self):\n", " return self.d\n", "\n", "\n", "geo = MockGeo(d)\n", "h3.geo_to_h3shape(geo)" ] }, { "cell_type": "markdown", "id": "0ab82119-9d9e-465d-94b5-61083e9d9df3", "metadata": {}, "source": [ "Also note that `LatLngPoly.__geo_interface__` is equivalent to calling `h3.h3shape_to_geo()` on an `LatLngPoly` object." ] }, { "cell_type": "code", "execution_count": null, "id": "fd9bd77d-ed1e-46bb-991a-f0cfc10f634b", "metadata": {}, "outputs": [], "source": [ "poly.__geo_interface__ == h3.h3shape_to_geo(poly)" ] }, { "cell_type": "markdown", "id": "55c31b56-c65d-4e06-bf60-edf96bb57e93", "metadata": {}, "source": [ "## Polygon to cells\n", "\n", "We can get all the H3 cells whose centroids fall within an `LatLngPoly` by using `h3.h3shape_to_cells()` and specifying the resolution." ] }, { "cell_type": "code", "execution_count": null, "id": "6c310200-30f2-4b24-a967-d705ffc05bd6", "metadata": {}, "outputs": [], "source": [ "h3.h3shape_to_cells(poly, res=7)" ] }, { "cell_type": "markdown", "id": "ec0d8ac7-ba97-46c4-8740-3a12a00e318d", "metadata": {}, "source": [ "We'll use a helper function to show a few different resolutions." ] }, { "cell_type": "code", "execution_count": null, "id": "0ac7cd51-1c7e-4509-94b4-92e59fd22c2a", "metadata": {}, "outputs": [], "source": [ "plot_shape_and_cells(poly, 7)" ] }, { "cell_type": "code", "execution_count": null, "id": "b0330d60-0adc-46e7-9689-7be1ba957dfc", "metadata": {}, "outputs": [], "source": [ "plot_shape_and_cells(poly, 9)" ] }, { "cell_type": "code", "execution_count": null, "id": "1fd40b0a-34fd-4ac7-9c3a-c367e5041cd8", "metadata": {}, "outputs": [], "source": [ "plot_shape_and_cells(poly, 10)" ] }, { "cell_type": "markdown", "id": "faa6a726-2adc-40cc-9e1e-5d4ee66a08ea", "metadata": {}, "source": [ "## H3 Polygons don't need to follow the right-hand rule\n", "\n", "`LatLngPoly` objects do not need to follow the \"right-hand rule\", unlike GeoJSON Polygons. \n", "The right-hand rule requires that vertices in outer loops are listed in counterclockwise\n", "order and holes are listed in clockwise order.\n", "`h3-py` accepts loops in any order and will usually interpret them as the user intended, for example,\n", "converting to sets of cells. However, `h3-py` won't re-order your loops to\n", "conform to the right-hand rule, so be careful if you're using `__geo_interface__` to plot them.\n", "\n", "Obeying the right-hand rule is only a concern when creating `LatLngPoly` objects from external input; `LatLngPoly` or `LatLngMultiPoly`\n", "objects created through `h3.cells_to_h3shape()` **will respect the right-hand rule**.\n", "\n", "For example, if we reverse the order of one of the holes in our example polygon above,\n", "the hole won't be rendered correctly, but the conversion to cells will remain unchanged." ] }, { "cell_type": "code", "execution_count": null, "id": "34ef8624-c852-419d-9e98-e9ec70ec6bc0", "metadata": {}, "outputs": [], "source": [ "# Respects right-hand rule\n", "poly = h3.LatLngPoly(outer, hole1, hole2)\n", "plot_shape_and_cells(poly, res=10)" ] }, { "cell_type": "code", "execution_count": null, "id": "e52ae550-e34d-406b-9c82-b283fd34a170", "metadata": {}, "outputs": [], "source": [ "# Does not respect right-hand-rule; second hole is reversed\n", "# Conversion to cells still works, tho!\n", "poly = h3.LatLngPoly(outer, hole1[::-1], hole2)\n", "plot_shape_and_cells(poly, res=10)" ] }, { "cell_type": "code", "execution_count": null, "id": "f1940071-eb32-44bc-b07c-2a2eef14b21e", "metadata": {}, "outputs": [], "source": [ "# Does not respect right-hand-rule; outer loop and second hole are both reversed\n", "# Conversion to cells still works, tho!\n", "poly = h3.LatLngPoly(outer[::-1], hole1[::-1], hole2)\n", "plot_shape_and_cells(poly, res=10)" ] }, { "cell_type": "markdown", "id": "b0b13d4a-a376-4a8f-b314-898d9cf94c59", "metadata": {}, "source": [ "# LatLngMultiPoly\n", "\n", "An `LatLngMultiPoly` can be created from `LatLngPoly` objects. The string representation of the `LatLngMultiPoly`\n", "gives the number of vertices in the outer loop of each `LatLngPoly`, along with the number of vertices\n", "in each hole (if there are any).\n", "\n", "For example `` represents an `LatLngMultiPoly` consisting of two `LatLngPoly` polygons:\n", "\n", "- the first polygon has 3 outer vertices and no holes\n", "- the second polygon has 4 outer vertices and 1 hole with 5 vertices" ] }, { "cell_type": "code", "execution_count": null, "id": "16b0b4b2-2cfb-461d-bd69-511c87e208a3", "metadata": {}, "outputs": [], "source": [ "poly1 = h3.LatLngPoly([(37.804, -122.412), (37.778, -122.507), (37.733, -122.501)])\n", "poly2 = h3.LatLngPoly(\n", " [(37.803, -122.408), (37.736, -122.491), (37.738, -122.380), (37.787, -122.39)],\n", " [(37.760, -122.441), (37.772, -122.427), (37.773, -122.404), (37.758, -122.401), (37.745, -122.428)]\n", ")\n", "mpoly = h3.LatLngMultiPoly(poly1, poly2)\n", "\n", "print(poly1)\n", "print(poly2)\n", "print(mpoly)" ] }, { "cell_type": "code", "execution_count": null, "id": "f2faf991-9d3b-4e0a-8fd4-8c31e19fd07b", "metadata": {}, "outputs": [], "source": [ "plot_shape(mpoly)" ] }, { "cell_type": "markdown", "id": "850912ae-85f1-470c-aeea-1140f549fc95", "metadata": {}, "source": [ "## MultiPolygon to cells\n", "\n", "`h3.h3shape_to_cells()` works on both `LatLngMultiPoly` and `LatLngPoly` objects (both are subclasses of `H3Shape`)." ] }, { "cell_type": "code", "execution_count": null, "id": "944564c0-8198-49ae-81de-cf980dc4b348", "metadata": {}, "outputs": [], "source": [ "cells = h3.h3shape_to_cells(mpoly, res=9)\n", "plot_cells(cells)" ] }, { "cell_type": "markdown", "id": "f3712d99-cbe2-4f42-80e3-0acf3fcb8e25", "metadata": {}, "source": [ "## LatLngMultiPoly affordances\n", "\n", "- Calling `len()` on an `LatLngMultiPoly` gives the number of polygons\n", "- You can iterate through a `LatLngMultiPoly`, with the elements being the underlying `LatLngPoly`s" ] }, { "cell_type": "code", "execution_count": null, "id": "217d6e5a-bb8b-4923-b311-0f84ebf48872", "metadata": {}, "outputs": [], "source": [ "len(mpoly)" ] }, { "cell_type": "code", "execution_count": null, "id": "03f6cd31-b18f-4fe7-b259-78741f12c95d", "metadata": {}, "outputs": [], "source": [ "for p in mpoly:\n", " print(p)" ] }, { "cell_type": "code", "execution_count": null, "id": "4eca4cec-2881-4ed1-a65c-3aea38ac9c63", "metadata": {}, "outputs": [], "source": [ "list(mpoly)" ] }, { "cell_type": "markdown", "id": "2fd0a70c-d0cf-487c-b492-6b459dc13a30", "metadata": {}, "source": [ "## `__geo_interface__`\n", "\n", "`LatLngMultiPoly` implements `__geo_interface__`, and `LatLngMultiPoly` objects can also be created through `h3.geo_to_h3shape()`." ] }, { "cell_type": "code", "execution_count": null, "id": "2731474c-b3f8-4438-8932-62af21f8d6cf", "metadata": {}, "outputs": [], "source": [ "d = mpoly.__geo_interface__\n", "d" ] }, { "cell_type": "code", "execution_count": null, "id": "b5cc947e-0672-492d-a4bf-f303afbb5cfa", "metadata": {}, "outputs": [], "source": [ "geo = MockGeo(d)\n", "h3.geo_to_h3shape(geo)" ] }, { "cell_type": "markdown", "id": "a2b71ee2-deed-4336-8b2c-2ab740555188", "metadata": {}, "source": [ "# Cells to LatLngPoly or LatLngMultiPoly\n", "\n", "If you have a set of H3 cells that you would like to visualize, you may want to convert them to `LatLngPoly` or `LatLngMultiPoly` objects using `h3.cells_to_h3shape()` and then use `__geo_interface__` to get their GeoJSON representation. Or you could\n", "use `h3.cells_to_geo()` to get the GeoJSON dictionary directly." ] }, { "cell_type": "code", "execution_count": null, "id": "a2305120-adf4-4cee-9e2a-58e793e55cfb", "metadata": {}, "outputs": [], "source": [ "h = h3.latlng_to_cell(37.804, -122.412, res=9)\n", "cells = h3.grid_ring(h, 2)\n", "cells" ] }, { "cell_type": "code", "execution_count": null, "id": "3e242238-af2c-45a3-acd0-8daf3773a2bd", "metadata": {}, "outputs": [], "source": [ "plot_cells(cells)" ] }, { "cell_type": "code", "execution_count": null, "id": "a9d124ff-4e49-4e34-a6be-4a49bc00bcd9", "metadata": {}, "outputs": [], "source": [ "h3.cells_to_h3shape(cells)" ] }, { "cell_type": "code", "execution_count": null, "id": "b3ca5d18-a78a-4bc6-80d9-d98099da924d", "metadata": {}, "outputs": [], "source": [ "str(h3.cells_to_geo(cells))[:200]" ] }, { "cell_type": "markdown", "id": "0ae6d480-4807-4ca1-ac20-4095a20ded31", "metadata": {}, "source": [ "# API Summary\n", "\n", "```\n", "\"Geo object\" <-> H3Shape <-> H3 cells\n", "```\n", "\n", "\"Geo objects\" are any Python object that implements `__geo_interface__`. This is a widely used\n", "standard in geospatial Python and is used by libraries like `geopandas` and Shapely.\n", "\n", "`H3Shape` is an abstract class implemented by `LatLngPoly` and `LatLngMultiPoly`, each of which\n", "implement `__geo_interface__` and can be created from external \"Geo objects\":\n", "\n", "- `geo_to_h3shape()`\n", "- `h3shape_to_geo()`\n", "\n", "`H3Shape` objects can be converted to and from sets of H3 cells:\n", "\n", "- `h3shape_to_cells()`\n", "- `cells_to_h3shape()`\n", "\n", "For convience, we provide functions that hide the intermediate `H3Shape` representation:\n", "\n", "- `geo_to_cells()`\n", "- `cells_to_geo()`\n", "\n", "## Equivalance notes\n", "\n", "Note that if an object `a` is an `H3Shape`\n", "then the following two expressions are equivalent:\n", "\n", "- `h3shape_to_geo(a)`\n", "- `a.__geo_interface__`\n", "\n", "Also note that if `a` is an `H3Shape`, then `a` \"passes through\" `geo_to_h3shape()`:\n", "\n", "```python\n", "geo_to_h3shape(a) == a\n", "```" ] }, { "cell_type": "markdown", "id": "5838f6c8-aacb-4813-be7c-756c87945476", "metadata": {}, "source": [ "# Interfacing with GeoPandas and other libraries\n", "\n", "The `__geo_interface__` compatibility allows `h3-py` to work with `geopandas` and other geospatial libraries easily.\n", "\n", "To demonstrate, we start with a GeoPandas `GeoDataFrame` describing the five New York City boroughs." ] }, { "cell_type": "code", "execution_count": null, "id": "a872e2a1-dd51-4262-bc98-800500121bc0", "metadata": {}, "outputs": [], "source": [ "df = geopandas.read_file(geodatasets.get_path('nybb'))\n", "df" ] }, { "cell_type": "code", "execution_count": null, "id": "bbca69dc-1722-4be4-a1bb-329d7e77ec83", "metadata": {}, "outputs": [], "source": [ "plot_df(df, column='BoroName')" ] }, { "cell_type": "markdown", "id": "a060b933-8f32-402b-ac61-7cd04dcf70df", "metadata": {}, "source": [ "## Use a compatible CRS before applying H3 functions\n", "\n", "The function `h3.geo_to_cells(geo, res)` takes some \"geo object\" that implements `__geo_interface__` (https://gist.github.com/sgillies/2217756) — like a Shapely Polygon or MultiPolygon — and converts it to the set of cells whose centroids are contained in `geo`.\n", "\n", "**Common Pitfall**: Be careful about what CRS you are using. `h3-py` expects coordinates as latitude-longitude pairs. The CRS of the current dataframe (EPSG:2263) gives coordinates in feet! You should first convert the data to something compatible. A common choice is **EPSG:4326/WGS84**. You'll get incorrect results if you apply `h3.geo_to_cells` without converting." ] }, { "cell_type": "code", "execution_count": null, "id": "83fced81-89f1-4795-84b8-c852c8c3f938", "metadata": {}, "outputs": [], "source": [ "# Original CRS\n", "df.crs" ] }, { "cell_type": "code", "execution_count": null, "id": "dfaf3b8a-2e53-4adf-87a5-ef9fe616df32", "metadata": {}, "outputs": [], "source": [ "df.geometry[0]" ] }, { "cell_type": "code", "execution_count": null, "id": "00e1026a-0a0c-448f-9223-8ce6d4d5a9a3", "metadata": {}, "outputs": [], "source": [ "# Converting to EPSG:4326/WGS84\n", "df = df.to_crs(epsg=4326)\n", "df.crs" ] }, { "cell_type": "code", "execution_count": null, "id": "81071108-3c13-4e10-8608-892ed3742054", "metadata": {}, "outputs": [], "source": [ "# Note that the `geometry` column now has coordinates in degrees\n", "df" ] }, { "cell_type": "markdown", "id": "01ca62b0-61f3-4204-bb0f-a14c9859cd4d", "metadata": {}, "source": [ "## Converting a geo to H3 cells\n", "\n", "First, we select one of the boroughs and get the Shapely `MultiPolygon` describing it." ] }, { "cell_type": "code", "execution_count": null, "id": "0be200e1-2536-4430-90ef-38dc72a75f9f", "metadata": {}, "outputs": [], "source": [ "geo = df.geometry[0]\n", "type(geo)" ] }, { "cell_type": "markdown", "id": "0a3c3c9c-b547-4a69-9030-fb88f486c4aa", "metadata": {}, "source": [ "Note that Shapely `MultiPolygon` objects implement `__geo_interface__`, so we can use `h3.geo_to_cells()` to get the associated set of H3 cells at various resolutions." ] }, { "cell_type": "code", "execution_count": null, "id": "c1a12743-b5e2-4cd1-a7fd-848e920fa928", "metadata": {}, "outputs": [], "source": [ "h3.geo_to_cells(geo, res=6)" ] }, { "cell_type": "code", "execution_count": null, "id": "27d24ca5-123f-4462-b37d-bcba6acc4f64", "metadata": {}, "outputs": [], "source": [ "plot_cells(h3.geo_to_cells(geo, res=9))" ] }, { "cell_type": "markdown", "id": "e00d088e-47b9-4bca-a8b3-d22dab747be3", "metadata": {}, "source": [ "## Converting all geos in a GeoDataFrame to cells\n", "\n", "We can apply `geo_to_cells()` to each of the Shapely geometries in the `df.geometry` column." ] }, { "cell_type": "code", "execution_count": null, "id": "f9901cc3-cd8d-4543-a8fb-98178010700e", "metadata": {}, "outputs": [], "source": [ "cell_column = df.geometry.apply(lambda x: h3.geo_to_cells(x, res=8))\n", "cell_column" ] }, { "cell_type": "markdown", "id": "502efd48-2c0b-4860-8796-7ea2e4e88687", "metadata": {}, "source": [ "## Converting cells to \"geo objects\"\n", "\n", "If we assign `df.geometry = cell_column` we'll get an error because the `geometry` column of a `geopandas.GeoDataFrame` must contain valid geometry objects.\n", "We can obtain compatible objects by converting the cells to `H3Shape` by applying `h3.cells_to_h3shape()`.\n", "\n", "(Note that, unfortunately, Pandas has some logic to identify iterable members of a series and then renders a tuple of the elements, rather than our preferred `LatLngMultiPoly.__repr__` representation.)" ] }, { "cell_type": "code", "execution_count": null, "id": "0c5cb00c-9796-4356-8e2a-8dc50c86103c", "metadata": {}, "outputs": [], "source": [ "shape_column = cell_column.apply(h3.cells_to_h3shape)\n", "shape_column" ] }, { "cell_type": "markdown", "id": "1965d53f-2bba-4840-a627-1bbf55405be3", "metadata": {}, "source": [ "Note that the column now consists of `LatLngPoly` and `LatLngMultiPoly` objects." ] }, { "cell_type": "code", "execution_count": null, "id": "9b3eedce-2570-4708-992e-e6d739eecc2f", "metadata": {}, "outputs": [], "source": [ "shape_column[0]" ] }, { "cell_type": "code", "execution_count": null, "id": "014bc63d-5cfa-4a65-95e6-390ddddd1703", "metadata": {}, "outputs": [], "source": [ "shape_column[1]" ] }, { "cell_type": "markdown", "id": "6ae6afb2-8267-492a-836e-cf371df91e89", "metadata": {}, "source": [ "Now, if we assign `df.geometry = shape_column`, our `H3Shape` objects will automatically be converted to Shapely Polygon and MultiPolygon objects via `__geo_interface__`." ] }, { "cell_type": "code", "execution_count": null, "id": "0c5cd4cf-226b-405f-9c36-7f1ccc09c342", "metadata": {}, "outputs": [], "source": [ "df.geometry = shape_column\n", "df.geometry" ] }, { "cell_type": "code", "execution_count": null, "id": "2608ce79-f2c5-4dbc-bf22-a28255343aea", "metadata": {}, "outputs": [], "source": [ "type(df.geometry[0])" ] }, { "cell_type": "markdown", "id": "d568b20e-88cb-4269-99ce-dc96a73d93d1", "metadata": {}, "source": [ "## Visualizing H3 cells\n", "\n", "We took some Shapely geometry objects, converted them to H3 cells, and converted those back to Shapely geometry objects in a `geopandas.GeoDataFrame`. \n", "\n", "We can visualize the results with our helper function." ] }, { "cell_type": "code", "execution_count": null, "id": "63ab3ec1-49c2-482d-8b84-154dd8196eda", "metadata": {}, "outputs": [], "source": [ "plot_df(df, column='BoroName')" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.11.8" } }, "nbformat": 4, "nbformat_minor": 5 } uber-h3-py-2883945/docs/project_links.md000066400000000000000000000011741520643167100177450ustar00rootroot00000000000000# Project Links ## H3 Links for the overall H3 project including the core C library and all language bindings (including Python!). - [H3 Project Homepage](https://h3geo.org/) - [H3 on GitHub](https://github.com/uber/h3) - [H3 Project GitHub Discussions](https://github.com/uber/h3/discussions) - [H3 on Stack Overflow](https://stackoverflow.com/questions/tagged/h3) - [H3 on Slack](https://join.slack.com/t/h3-core/shared_invite/zt-g6u5r1hf-W_~uVJmfeiWtMQuBGc1NNg) ## h3-py Links specific to the H3 Python bindings. - [Example Notebooks](https://github.com/uber/h3-py-notebooks) - [h3-py on GitHub](https://github.com/uber/h3-py) uber-h3-py-2883945/justfile000066400000000000000000000026051520643167100153750ustar00rootroot00000000000000# disable editable installs so uv sync does a full build of the C extension export UV_NO_EDITABLE := "1" _: just --list # force-rebuild the C extension to avoid stale builds reinstall: uv cache clean h3 uv sync --reinstall-package h3 # locally, always reinstall before testing test: reinstall ci-test test-cython: reinstall ci-test-cython fix: uvx ruff check --fix lab: uv sync --group docs uv run jupyter lab view: open docs/_build/html/index.html purge: just _rm .venv just _rm '*.pytest_cache' just _rm .DS_Store just _rm '*.egg-info' just _rm dist just _rm '*.ipynb_checkpoints' just _rm __pycache__ just _rm uv.lock just _rm .coverage just _rm build just _rm _build just _rm _skbuild just _rm .ruff_cache uv cache clean h3 _rm pattern: -@find . -name "{{pattern}}" -prune -exec rm -rf {} + # Recipes below are used by CI workflows (.github/workflows/). # Changes here may break CI — update the workflows accordingly. ci-test: uv run pytest tests/test_lib --cov=h3 --cov=tests/test_lib --cov-fail-under=100 ci-test-cython: uv run --with cython --with setuptools cythonize tests/test_cython/cython_example.pyx uv run pytest tests/test_cython --cov=tests/test_cython lint: uvx ruff check docs: uv sync --group docs uv run jupyter-book build docs/ --warningiserror --keep-going --all uber-h3-py-2883945/pyproject.toml000066400000000000000000000065021520643167100165410ustar00rootroot00000000000000[build-system] requires = ['scikit-build-core', 'cython'] build-backend = 'scikit_build_core.build' [project] name = 'h3' version = '4.5.0' description = "Uber's hierarchical hexagonal geospatial indexing system" readme = 'readme.md' license = {file = 'LICENSE'} authors = [ { name = 'Uber Technologies', email = 'ajfriend@gmail.com' }, ] maintainers = [ { name = 'AJ Friend', email = 'ajfriend@gmail.com' }, ] requires-python = '>=3.10' dependencies = [] classifiers = [ 'Development Status :: 5 - Production/Stable', 'Intended Audience :: Developers', 'Intended Audience :: Science/Research', 'License :: OSI Approved :: Apache Software License', 'Programming Language :: C', 'Programming Language :: Cython', 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', 'Programming Language :: Python :: 3.12', 'Programming Language :: Python :: 3.13', 'Programming Language :: Python :: 3.14', 'Operating System :: MacOS :: MacOS X', 'Operating System :: POSIX :: Linux', 'Operating System :: Microsoft :: Windows', 'Topic :: Scientific/Engineering :: GIS', ] [project.urls] Homepage = 'https://github.com/uber/h3-py' Documentation = 'https://uber.github.io/h3-py/' 'Bug Tracker' = 'https://github.com/uber/h3-py/issues' Discussions = 'https://github.com/uber/h3-py/discussions' Changelog = 'https://uber.github.io/h3-py/_changelog.html' [project.optional-dependencies] numpy = ['numpy'] [dependency-groups] dev = ['pytest', 'pytest-cov', 'numpy'] docs = [ 'numpy', 'jupyter-book<2', 'sphinx>=7.3.3', # https://github.com/sphinx-doc/sphinx/issues/12290 'jupyterlab', 'jupyterlab-geojson', 'geopandas', 'geodatasets', 'matplotlib', 'contextily', 'cartopy', 'geoviews', ] [tool.pytest.ini_options] addopts = "--cov-report=term-missing --durations=10" [tool.coverage.run] omit = [ '*/h3/api/basic_int/__init__.py', '*/h3/api/memview_int/__init__.py', '*/h3/api/numpy_int/__init__.py' ] [tool.uv] default-groups = ['dev'] [tool.scikit-build] sdist.exclude = [ 'src/h3lib', 'docs', 'dev_notes.md', 'justfile', '.github', '.gitignore', '.gitmodules', ] sdist.include = [ 'src/h3lib/LICENSE', 'src/h3lib/README.md', 'src/h3lib/VERSION', 'src/h3lib/CMakeLists.txt', 'src/h3lib/h3.pc.in', 'src/h3lib/cmake/*', 'src/h3lib/src/h3lib/*' ] [tool.ruff] src = [ 'src', 'tests', ] line-length = 88 [tool.ruff.lint] flake8-quotes.inline-quotes = 'single' # https://docs.astral.sh/ruff/rules/ preview = true # https://docs.astral.sh/ruff/preview/#selecting-single-preview-rules select = [ 'A', # 'B', # todo # 'COM', # todo 'E', 'F', 'Q', 'W', # 'D', # todo. this is nice! # 'PL', # todo. some interesting ideas # 'DOC', # todo: interesting ] ignore = [ 'E221', # Multiple spaces before operator 'E231', # Missing whitespace after ',' 'E241', # Multiple spaces after ',' 'E251', # Unexpected spaces around keyword / parameter equals 'E272', # Multiple spaces before keyword 'E731', # Do not assign a `lambda` expression, use a `def` # 'PLR2004', ] [tool.ruff.lint.per-file-ignores] '*.ipynb' = [ 'E501', # Line too long (104 > 88) ] 'test*.py' = [ # 'D', # maybe just the need for docstring ] uber-h3-py-2883945/readme.md000066400000000000000000000061271520643167100154070ustar00rootroot00000000000000H3 Logo # **h3-py**: Uber's H3 Hexagonal Hierarchical Geospatial Indexing System in Python [![PyPI version](https://badge.fury.io/py/h3.svg)](https://badge.fury.io/py/h3) [![PyPI downloads](https://img.shields.io/pypi/dm/h3.svg)](https://pypistats.org/packages/h3) [![conda](https://img.shields.io/conda/vn/conda-forge/h3-py.svg)](https://anaconda.org/conda-forge/h3-py) [![version](https://img.shields.io/badge/h3-v4.5.0-blue.svg)](https://github.com/uber/h3/releases/tag/v4.5.0) [![version](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://github.com/uber/h3-py/blob/master/LICENSE) [![Tests](https://github.com/uber/h3-py/workflows/tests/badge.svg)](https://github.com/uber/h3-py/actions) [![Coverage 100%](https://img.shields.io/badge/coverage-100%25-green.svg)](https://github.com/uber/h3-py/blob/master/.github/workflows/lint_and_coverage.yml#L31) Python bindings for the [H3 core library](https://h3geo.org/). - Documentation: [uber.github.io/h3-py](https://uber.github.io/h3-py) - GitHub repo: [github.com/uber/h3-py](https://github.com/uber/h3-py) ## Installation From [PyPI](https://pypi.org/project/h3/): ```console pip install h3 ``` From [conda](https://github.com/conda-forge/h3-py-feedstock): ```console conda config --add channels conda-forge conda install h3-py ``` ## Usage ```python >>> import h3 >>> lat, lng = 37.769377, -122.388903 >>> resolution = 9 >>> h3.latlng_to_cell(lat, lng, resolution) '89283082e73ffff' ``` ## APIs [api_comparison]: https://uber.github.io/h3-py/api_comparison [api_quick]: https://uber.github.io/h3-py/api_quick We provide [multiple APIs][api_comparison] in `h3-py`. - All APIs have the same set of functions; see the [API reference][api_quick]. - The APIs differ only in their input/output formats; see the [API comparison page][api_comparison]. ## Example gallery Browse [a collection of example notebooks](https://github.com/uber/h3-py-notebooks), and if you have examples or visualizations of your own, please feel free to contribute! [walkthrough]: https://nbviewer.jupyter.org/github/uber/h3-py-notebooks/blob/master/notebooks/usage.ipynb We also have an introductory [walkthrough of the API][walkthrough]. ## Versioning `h3-py` wraps the [H3 core library](https://github.com/uber/h3), which is written in C. The C and Python projects each employ [semantic versioning](https://semver.org/), where versions take the form `X.Y.Z`. The `h3-py` version string is guaranteed to match the C library string in both *major* and *minor* numbers (`X.Y`), but may differ on the *patch* (`Z`) number. This convention provides users with information on breaking changes and feature additions, while providing downstream bindings (like this one!) with the versioning freedom to fix bugs. Use `h3.versions()` to see the version numbers for both `h3-py` and the C library. For example, ```python >>> import h3 >>> h3.versions() {'c': '4.1.0', 'python': '4.1.1'} ``` uber-h3-py-2883945/src/000077500000000000000000000000001520643167100144115ustar00rootroot00000000000000uber-h3-py-2883945/src/h3/000077500000000000000000000000001520643167100147235ustar00rootroot00000000000000uber-h3-py-2883945/src/h3/CMakeLists.txt000066400000000000000000000000261520643167100174610ustar00rootroot00000000000000add_subdirectory(_cy) uber-h3-py-2883945/src/h3/__init__.py000066400000000000000000000012441520643167100170350ustar00rootroot00000000000000# flake8: noqa from .api.basic_str import * from ._version import __version__ from ._cy import ( UnknownH3ErrorCode, H3BaseException, H3GridNavigationError, H3MemoryError, H3ValueError, H3FailedError, H3DomainError, H3LatLngDomainError, H3ResDomainError, H3CellInvalidError, H3DirEdgeInvalidError, H3UndirEdgeInvalidError, H3VertexInvalidError, H3PentagonError, H3DuplicateInputError, H3NotNeighborsError, H3ResMismatchError, H3MemoryAllocError, H3MemoryBoundsError, H3OptionInvalidError, H3IndexInvalidError, H3BaseCellDomainError, H3DigitDomainError, H3DeletedDigitError, ) uber-h3-py-2883945/src/h3/_cy/000077500000000000000000000000001520643167100154755ustar00rootroot00000000000000uber-h3-py-2883945/src/h3/_cy/CMakeLists.txt000066400000000000000000000027701520643167100202430ustar00rootroot00000000000000list(APPEND CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}) include_directories(${CMAKE_CURRENT_SOURCE_DIR}) macro(add_cython_file filename) add_custom_command( OUTPUT "${filename}.c" COMMENT "Making ${CMAKE_CURRENT_BINARY_DIR}/${filename}.c from ${CMAKE_CURRENT_SOURCE_DIR}/${filename}.pyx" COMMAND Python::Interpreter -m cython "${CMAKE_CURRENT_SOURCE_DIR}/${filename}.pyx" --output-file "${filename}.c" -I ${CMAKE_CURRENT_SOURCE_DIR} DEPENDS "${filename}.pyx" VERBATIM) python_add_library(${filename} MODULE "${filename}.c" WITH_SOABI) set_property(TARGET ${filename} PROPERTY C_STANDARD 99) target_link_libraries(${filename} PRIVATE h3) install(TARGETS ${filename} LIBRARY DESTINATION ${SKBUILD_PROJECT_NAME}/_cy) endmacro() # GLOB pattern is recommended against # https://cmake.org/cmake/help/v3.14/command/file.html?highlight=file#filesystem add_cython_file(cells) add_cython_file(edges) add_cython_file(error_system) add_cython_file(latlng) add_cython_file(memory) add_cython_file(vertex) add_cython_file(to_multipoly) add_cython_file(util) # Include pyx and pxd files in distribution for use by Cython API install( FILES cells.pxd cells.pyx edges.pxd edges.pyx error_system.pyx h3lib.pxd latlng.pxd latlng.pyx memory.pxd memory.pyx util.pxd util.pyx vertex.pxd vertex.pyx DESTINATION ${SKBUILD_PROJECT_NAME}/_cy ) uber-h3-py-2883945/src/h3/_cy/__init__.py000066400000000000000000000052161520643167100176120ustar00rootroot00000000000000# flake8: noqa """ This module should serve as the interface between the C/Cython code and the Python code. That is, it is an internal API. This module should import all the Cython functions we intend to expose to be used in pure Python code, and each of the H3-py APIs should *only* reference functions and symbols listed here. These functions should handle input validation, guard against the possibility of segfaults, raise appropriate errors, and handle memory management. The API wrapping code around this should focus on the cosmetic function interface and input conversion (string to int, for instance). """ from .cells import ( is_valid_index, is_valid_cell, is_pentagon, get_base_cell_number, get_resolution, get_index_digit, construct_cell, cell_to_parent, grid_distance, grid_disk, grid_ring, cell_to_children_size, cell_to_children, cell_to_child_pos, child_pos_to_cell, compact_cells, uncompact_cells, get_num_cells, average_hexagon_area, cell_area, grid_path_cells, is_res_class_iii, get_pentagons, get_res0_cells, cell_to_center_child, get_icosahedron_faces, cell_to_local_ij, local_ij_to_cell, ) from .edges import ( are_neighbor_cells, cells_to_directed_edge, is_valid_directed_edge, get_directed_edge_origin, get_directed_edge_destination, directed_edge_to_cells, origin_to_directed_edges, average_hexagon_edge_length, edge_length, ) from .latlng import ( latlng_to_cell, cell_to_latlng, polygon_to_cells, polygons_to_cells, polygon_to_cells_experimental, polygons_to_cells_experimental, cell_to_boundary, directed_edge_to_boundary, great_circle_distance, ) from .vertex import ( cell_to_vertex, cell_to_vertexes, vertex_to_latlng, is_valid_vertex, ) from .to_multipoly import ( cells_to_multi_polygon ) from .util import ( c_version, str_to_int, int_to_str, ) from .memory import ( iter_to_mv, ) from .error_system import ( UnknownH3ErrorCode, H3BaseException, H3GridNavigationError, H3MemoryError, H3ValueError, H3FailedError, H3DomainError, H3LatLngDomainError, H3ResDomainError, H3CellInvalidError, H3DirEdgeInvalidError, H3UndirEdgeInvalidError, H3VertexInvalidError, H3PentagonError, H3DuplicateInputError, H3NotNeighborsError, H3ResMismatchError, H3MemoryAllocError, H3MemoryBoundsError, H3OptionInvalidError, H3IndexInvalidError, H3BaseCellDomainError, H3DigitDomainError, H3DeletedDigitError, get_H3_ERROR_END, error_code_to_exception, ) uber-h3-py-2883945/src/h3/_cy/cells.pxd000066400000000000000000000027241520643167100173210ustar00rootroot00000000000000from .h3lib cimport bool, int64_t, H3int cpdef bool is_valid_index(H3int h) cpdef bool is_valid_cell(H3int h) cpdef bool is_pentagon(H3int h) cpdef int get_base_cell_number(H3int h) except -1 cpdef int get_resolution(H3int h) except -1 cpdef int get_index_digit(H3int h, int res) except -1 cpdef H3int construct_cell(int baseCellNumber, const int[:] digits) except 0 cpdef int grid_distance(H3int h1, H3int h2) except -1 cpdef H3int[:] grid_disk(H3int h, int k) cpdef H3int[:] grid_ring(H3int h, int k) cpdef H3int cell_to_parent(H3int h, res=*) except 0 cpdef int64_t cell_to_children_size(H3int h, res=*) except -1 cpdef H3int[:] cell_to_children(H3int h, res=*) cpdef H3int cell_to_center_child(H3int h, res=*) except 0 cpdef int64_t cell_to_child_pos(H3int child, int parent_res) except -1 cpdef H3int child_pos_to_cell(H3int parent, int child_res, int64_t child_pos) except 0 cpdef H3int[:] compact_cells(const H3int[:] hu) cpdef H3int[:] uncompact_cells(const H3int[:] hc, int res) cpdef int64_t get_num_cells(int resolution) except -1 cpdef double average_hexagon_area(int resolution, unit=*) except -1 cpdef double cell_area(H3int h, unit=*) except -1 cpdef H3int[:] grid_path_cells(H3int start, H3int end) cpdef bool is_res_class_iii(H3int h) cpdef H3int[:] get_pentagons(int res) cpdef H3int[:] get_res0_cells() cpdef get_icosahedron_faces(H3int h) cpdef (int, int) cell_to_local_ij(H3int origin, H3int h) except * cpdef H3int local_ij_to_cell(H3int origin, int i, int j) except 0 uber-h3-py-2883945/src/h3/_cy/cells.pyx000066400000000000000000000240271520643167100173460ustar00rootroot00000000000000cimport h3lib from .h3lib cimport bool, int64_t, H3int, H3ErrorCodes from .util cimport ( check_cell, check_index, check_distance, ) from .error_system cimport ( H3Error, check_for_error, check_for_error_msg, ) from .memory cimport ( H3MemoryManager, int_mv, ) # todo: add notes about Cython exception handling cpdef bool is_valid_index(H3int h): """Validates an H3 index (cell, vertex, or directed edge). Returns ------- boolean """ return h3lib.isValidIndex(h) == 1 # bool is a python type, so we don't need the except clause cpdef bool is_valid_cell(H3int h): """Validates an H3 cell (hexagon or pentagon) Returns ------- boolean """ return h3lib.isValidCell(h) == 1 cpdef bool is_pentagon(H3int h): return h3lib.isPentagon(h) == 1 cpdef int get_base_cell_number(H3int h) except -1: check_cell(h) return h3lib.getBaseCellNumber(h) cpdef int get_resolution(H3int h) except -1: """Returns the resolution of an H3 Index 0--15 """ check_cell(h) return h3lib.getResolution(h) cpdef int get_index_digit(H3int h, int res) except -1: cdef: int digit check_index(h) check_for_error( h3lib.getIndexDigit(h, res, &digit) ) return digit cpdef H3int construct_cell(int base_cell_number, const int[:] digits) except 0: cdef: H3int out int res = len(digits) H3Error err if res > 0: err = h3lib.constructCell(res, base_cell_number, &digits[0], &out) else: err = h3lib.constructCell(res, base_cell_number, NULL, &out) check_for_error(err) return out cpdef int grid_distance(H3int h1, H3int h2) except -1: """ Compute the grid distance between two cells """ cdef: int64_t distance check_cell(h1) check_cell(h2) check_for_error( h3lib.gridDistance(h1, h2, &distance) ) return distance cpdef H3int[:] grid_disk(H3int h, int k): """ Return cells at grid distance `<= k` from `h`. """ cdef: int64_t n check_cell(h) check_distance(k) check_for_error( h3lib.maxGridDiskSize(k, &n) ) hmm = H3MemoryManager(n) check_for_error( h3lib.gridDisk(h, k, hmm.ptr) ) mv = hmm.to_mv() return mv cpdef H3int[:] grid_ring(H3int h, int k): """ Return cells at grid distance `== k` from `h`. Collection is "hollow" for k >= 1. """ check_cell(h) check_distance(k) n = 6*k if k > 0 else 1 hmm = H3MemoryManager(n) check_for_error( h3lib.gridRing(h, k, hmm.ptr) ) mv = hmm.to_mv() return mv cpdef H3int cell_to_parent(H3int h, res=None) except 0: cdef: H3int parent check_cell(h) if res is None: res = get_resolution(h) - 1 err = h3lib.cellToParent(h, res, &parent) if err: msg = 'Invalid parent resolution {} for cell {}.' msg = msg.format(res, hex(h)) check_for_error_msg(err, msg) return parent cpdef int64_t cell_to_children_size(H3int h, res=None) except -1: cdef: int64_t n check_cell(h) if res is None: res = get_resolution(h) + 1 err = h3lib.cellToChildrenSize(h, res, &n) if err: msg = 'Invalid child resolution {} for cell {}.' msg = msg.format(res, hex(h)) check_for_error_msg(err, msg) return n cpdef H3int[:] cell_to_children(H3int h, res=None): check_cell(h) if res is None: res = get_resolution(h) + 1 n = cell_to_children_size(h, res) hmm = H3MemoryManager(n) check_for_error( h3lib.cellToChildren(h, res, hmm.ptr) ) mv = hmm.to_mv() return mv cpdef H3int cell_to_center_child(H3int h, res=None) except 0: cdef: H3int child check_cell(h) if res is None: res = get_resolution(h) + 1 err = h3lib.cellToCenterChild(h, res, &child) if err: msg = 'Invalid child resolution {} for cell {}.' msg = msg.format(res, hex(h)) check_for_error_msg(err, msg) return child cpdef int64_t cell_to_child_pos(H3int child, int parent_res) except -1: cdef: int64_t child_pos check_cell(child) err = h3lib.cellToChildPos(child, parent_res, &child_pos) if err: msg = "Couldn't find child pos of cell {} at res {}." msg = msg.format(hex(child), parent_res) check_for_error_msg(err, msg) return child_pos cpdef H3int child_pos_to_cell(H3int parent, int child_res, int64_t child_pos) except 0: cdef: H3int child check_cell(parent) err = h3lib.childPosToCell(child_pos, parent, child_res, &child) if err: msg = "Couldn't find child with pos {} at res {} from parent {}." msg = msg.format(child_pos, child_res, hex(parent)) check_for_error_msg(err, msg) return child cpdef H3int[:] compact_cells(const H3int[:] hu): # todo: fix this with my own Cython object "wrapper" class? # everything has a .ptr interface? # todo: the Clib can handle 0-len arrays because it **avoids** # dereferencing the pointer, but Cython's syntax of # `&hu[0]` **requires** a dereference. For Cython, checking for array # length of zero and returning early seems like the easiest solution. # note: open to better ideas! if len(hu) == 0: return H3MemoryManager(0).to_mv() for h in hu: ## todo: should we have an array version? would that be faster? check_cell(h) cdef size_t n = len(hu) hmm = H3MemoryManager(n) check_for_error( h3lib.compactCells(&hu[0], hmm.ptr, n) ) mv = hmm.to_mv() return mv # todo: https://stackoverflow.com/questions/50684977/cython-exception-type-for-a-function-returning-a-typed-memoryview # apparently, memoryviews are python objects, so we don't need to do the except clause cpdef H3int[:] uncompact_cells(const H3int[:] hc, int res): # todo: the Clib can handle 0-len arrays because it **avoids** # dereferencing the pointer, but Cython's syntax of # `&hc[0]` **requires** a dereference. For Cython, checking for array # length of zero and returning early seems like the easiest solution. # note: open to better ideas! cdef: int64_t n if len(hc) == 0: return H3MemoryManager(0).to_mv() for h in hc: check_cell(h) check_for_error( h3lib.uncompactCellsSize(&hc[0], len(hc), res, &n) ) hmm = H3MemoryManager(n) check_for_error( h3lib.uncompactCells( &hc[0], # todo: symmetry here with the wrapper object might be nice. hc.ptr / hc.n len(hc), hmm.ptr, hmm.n, res ) ) mv = hmm.to_mv() return mv cpdef int64_t get_num_cells(int resolution) except -1: cdef: int64_t num_cells check_for_error( h3lib.getNumCells(resolution, &num_cells) ) return num_cells cpdef double average_hexagon_area(int resolution, unit='km^2') except -1: cdef: double area check_for_error( h3lib.getHexagonAreaAvgKm2(resolution, &area) ) # todo: multiple units convert = { 'km^2': 1.0, 'm^2': 1000*1000.0 } try: area *= convert[unit] except: raise ValueError('Unknown unit: {}'.format(unit)) return area cpdef double cell_area(H3int h, unit='km^2') except -1: cdef: double area if unit == 'rads^2': err = h3lib.cellAreaRads2(h, &area) elif unit == 'km^2': err = h3lib.cellAreaKm2(h, &area) elif unit == 'm^2': err = h3lib.cellAreaM2(h, &area) else: raise ValueError('Unknown unit: {}'.format(unit)) check_for_error(err) return area cdef _could_not_find_line(err, start, end): msg = "Couldn't find line between cells {} and {}" msg = msg.format(hex(start), hex(end)) check_for_error_msg(err, msg) cpdef H3int[:] grid_path_cells(H3int start, H3int end): cdef: int64_t n # todo: can we segfault here with invalid inputs? # Can we trust the c library to validate the start/end cells? # probably applies to all size/work pairs of functions... err = h3lib.gridPathCellsSize(start, end, &n) _could_not_find_line(err, start, end) hmm = H3MemoryManager(n) err = h3lib.gridPathCells(start, end, hmm.ptr) _could_not_find_line(err, start, end) # todo: probably here too? mv = hmm.to_mv() return mv cpdef bool is_res_class_iii(H3int h): return h3lib.isResClassIII(h) == 1 cpdef H3int[:] get_pentagons(int res): n = h3lib.pentagonCount() hmm = H3MemoryManager(n) check_for_error( h3lib.getPentagons(res, hmm.ptr) ) mv = hmm.to_mv() return mv cpdef H3int[:] get_res0_cells(): n = h3lib.res0CellCount() hmm = H3MemoryManager(n) check_for_error( h3lib.getRes0Cells(hmm.ptr) ) mv = hmm.to_mv() return mv # oh, this is returning a set?? # todo: convert to int[:]? cpdef get_icosahedron_faces(H3int h): cdef: int n int[:] faces ## todo: weird, this needs to be specified to avoid errors. cython bug? check_for_error( h3lib.maxFaceCount(h, &n) ) faces = int_mv(n) check_for_error( h3lib.getIcosahedronFaces(h, &faces[0]) ) # todo: wait? do faces start from 0 or 1? # we could do this check/processing in the int_mv object out = [f for f in faces if f >= 0] return out cpdef (int, int) cell_to_local_ij(H3int origin, H3int h) except *: cdef: h3lib.CoordIJ c err = h3lib.cellToLocalIj(origin, h, 0, &c) if err: msg = "Couldn't find local (i,j) between cells {} and {}." msg = msg.format(hex(origin), hex(h)) check_for_error_msg(err, msg) return c.i, c.j cpdef H3int local_ij_to_cell(H3int origin, int i, int j) except 0: cdef: h3lib.CoordIJ c H3int out c.i, c.j = i, j err = h3lib.localIjToCell(origin, &c, 0, &out) if err: msg = "Couldn't find cell at local ({},{}) from cell {}." msg = msg.format(i, j, hex(origin)) check_for_error_msg(err, msg) return out uber-h3-py-2883945/src/h3/_cy/edges.pxd000066400000000000000000000010611520643167100172770ustar00rootroot00000000000000from .h3lib cimport bool, H3int cpdef bool are_neighbor_cells(H3int h1, H3int h2) cpdef H3int cells_to_directed_edge(H3int origin, H3int destination) except * cpdef bool is_valid_directed_edge(H3int e) cpdef H3int get_directed_edge_origin(H3int e) except 1 cpdef H3int get_directed_edge_destination(H3int e) except 1 cpdef (H3int, H3int) directed_edge_to_cells(H3int e) except * cpdef H3int[:] origin_to_directed_edges(H3int origin) cpdef double average_hexagon_edge_length(int resolution, unit=*) except -1 cpdef double edge_length(H3int e, unit=*) except -1 uber-h3-py-2883945/src/h3/_cy/edges.pyx000066400000000000000000000046671520643167100173430ustar00rootroot00000000000000cimport h3lib from .h3lib cimport bool, H3int from .error_system cimport check_for_error from .memory cimport H3MemoryManager # todo: make bint cpdef bool are_neighbor_cells(H3int h1, H3int h2): cdef: int out err = h3lib.areNeighborCells(h1, h2, &out) # note: we are intentionally not raising an error here, and just # returning false. # todo: is this choice consistent across the Python and C libs? if err: return False return out == 1 cpdef H3int cells_to_directed_edge(H3int origin, H3int destination) except *: cdef: int neighbor_out H3int out check_for_error( h3lib.cellsToDirectedEdge(origin, destination, &out) ) return out cpdef bool is_valid_directed_edge(H3int e): return h3lib.isValidDirectedEdge(e) == 1 cpdef H3int get_directed_edge_origin(H3int e) except 1: cdef: H3int out check_for_error( h3lib.getDirectedEdgeOrigin(e, &out) ) return out cpdef H3int get_directed_edge_destination(H3int e) except 1: cdef: H3int out check_for_error( h3lib.getDirectedEdgeDestination(e, &out) ) return out cpdef (H3int, H3int) directed_edge_to_cells(H3int e) except *: # todo: use directed_edge_to_cells in h3lib return get_directed_edge_origin(e), get_directed_edge_destination(e) cpdef H3int[:] origin_to_directed_edges(H3int origin): """ Returns the 6 (or 5 for pentagons) directed edges for the given origin cell """ hmm = H3MemoryManager(6) check_for_error( h3lib.originToDirectedEdges(origin, hmm.ptr) ) mv = hmm.to_mv() return mv cpdef double average_hexagon_edge_length(int resolution, unit='km') except -1: cdef: double length check_for_error( h3lib.getHexagonEdgeLengthAvgKm(resolution, &length) ) # todo: multiple units convert = { 'km': 1.0, 'm': 1000.0 } try: length *= convert[unit] except: raise ValueError('Unknown unit: {}'.format(unit)) return length cpdef double edge_length(H3int e, unit='km') except -1: cdef: double length if unit == 'rads': err = h3lib.edgeLengthRads(e, &length) elif unit == 'km': err = h3lib.edgeLengthKm(e, &length) elif unit == 'm': err = h3lib.edgeLengthM(e, &length) else: raise ValueError('Unknown unit: {}'.format(unit)) check_for_error(err) return length uber-h3-py-2883945/src/h3/_cy/error_system.pxd000066400000000000000000000002721520643167100207500ustar00rootroot00000000000000from .h3lib cimport H3Error cpdef error_code_to_exception(H3Error err) cdef check_for_error(H3Error err) cdef check_for_error_msg(H3Error err, str msg) cpdef H3Error get_H3_ERROR_END() uber-h3-py-2883945/src/h3/_cy/error_system.pyx000066400000000000000000000172671520643167100210110ustar00rootroot00000000000000""" Exceptions from the h3-py library have three possible sources: - the Python code - the Cython code - the underlying H3 C library code The Python and Cython `h3-py` code will only raise standard Python built-in exceptions; **no custom** exception classes will be used. Conversely, many functions in the H3 C library return a `uint32_t` error code (aliased as type `H3Error`). When these errors happen (and `h3-py` can't recover from them internally), they are passed up to the Python/Cython code, where their `uint32_t` error values are converted to **custom** Python exception types. These custom exception classes all inherit from `H3BaseException`. There is a 1-1 correspondence between the concrete subclasses of `H3BaseException` and the H3 C library `H3ErrorCodes` values. The correspondence is intentional, so that the user can refer to the H3 C library documentation on these errors. The (`uint32_t` <-> Exception) correspondence should be clear from the names of each error/exception, but the explicit mapping is given by a dictionary in the code below. Note that some "abstract" subclasses of `H3BaseException` are also included to group the exceptions by type. (We say "abstract" because Python has no easy way to make true abstract exception classes.) These "abstract" exceptions will never be raised directly by `h3-py`, but they allow the user to catch general groups of errors. Note that `h3-py` will only ever directly raise the "concrete" exception classes. Summarizing, all exceptions originating from the C library inherit from `H3BaseException`, which has both "abstract" and "concrete" subclasses. **Abstract classes**: - H3BaseException - H3ValueError - H3MemoryError - H3GridNavigationError **Concrete classes**: - H3FailedError - H3DomainError - H3LatLngDomainError - H3ResDomainError - H3CellInvalidError - H3DirEdgeInvalidError - H3UndirEdgeInvalidError - H3VertexInvalidError - H3PentagonError - H3DuplicateInputError - H3NotNeighborsError - H3ResMismatchError - H3MemoryAllocError - H3MemoryBoundsError - H3OptionInvalidError - H3IndexInvalidError - H3BaseCellDomainError - H3DigitDomainError - H3DeletedDigitError # TODO: add tests verifying that concrete exception classes have the right error codes associated with them """ from contextlib import contextmanager from .h3lib cimport ( H3Error, # H3ErrorCodes enum values E_SUCCESS, E_FAILED, E_DOMAIN, E_LATLNG_DOMAIN, E_RES_DOMAIN, E_CELL_INVALID, E_DIR_EDGE_INVALID, E_UNDIR_EDGE_INVALID, E_VERTEX_INVALID, E_PENTAGON, E_DUPLICATE_INPUT, E_NOT_NEIGHBORS, E_RES_MISMATCH, E_MEMORY_ALLOC, E_MEMORY_BOUNDS, E_OPTION_INVALID, E_INDEX_INVALID, E_BASE_CELL_DOMAIN, E_DIGIT_DOMAIN, E_DELETED_DIGIT, H3_ERROR_END # sentinel value ) @contextmanager def _the_error(obj): """ Syntactic maple syrup for grouping exception definitions. The associated `with` statement ends up as a not-half-bad approximation to a valid sentence fragment. This provides sort of a "pretend scope", in that it allows for block indentation which helps to visually indicate the "scope" of the `... as e` statement. Just note that Python doesn't treat the `with` block as a "true" separate scope. Note that this doesn't actually do anything context-manager-y, outside of the variable assignment and block indentation. """ yield obj # # Base exception for C library error codes # class H3BaseException(Exception): """ Base H3 exception class. Concrete subclasses of this class correspond to specific error codes from the C library. Base/abstract subclasses will have `h3_error_code = None`, while concrete subclasses will have `h3_error_code` equal to their associated C library error code. """ h3_error_code = None # # A few "abstract" exceptions; organizational. # with _the_error(H3BaseException) as e: class H3ValueError(e, ValueError): ... class H3MemoryError(e, MemoryError): ... class H3GridNavigationError(e, RuntimeError): ... # # Concrete exceptions # class UnknownH3ErrorCode(H3BaseException): """ Indicates that the h3-py Python bindings have received an unrecognized error code from the C library. This should never happen. Please report if you get this error. Note that this exception is *outside* of the H3BaseException class hierarchy. """ pass with _the_error(H3BaseException) as e: class H3FailedError(e): ... with _the_error(H3GridNavigationError) as e: class H3PentagonError(e): ... with _the_error(H3MemoryError) as e: class H3MemoryAllocError(e): ... class H3MemoryBoundsError(e): ... with _the_error(H3ValueError) as e: class H3DomainError(e): ... class H3LatLngDomainError(e): ... class H3ResDomainError(e): ... class H3CellInvalidError(e): ... class H3DirEdgeInvalidError(e): ... class H3UndirEdgeInvalidError(e): ... class H3VertexInvalidError(e): ... class H3DuplicateInputError(e): ... class H3NotNeighborsError(e): ... class H3ResMismatchError(e): ... class H3OptionInvalidError(e): ... class H3IndexInvalidError(e): ... class H3BaseCellDomainError(e): ... class H3DigitDomainError(e): ... class H3DeletedDigitError(e): ... """ This defines a mapping between uint32_t error codes and concrete Python exception classes. Note that we intentionally omit E_SUCCESS, as it isn't an actual error. """ error_mapping = { E_FAILED: H3FailedError, E_DOMAIN: H3DomainError, E_LATLNG_DOMAIN: H3LatLngDomainError, E_RES_DOMAIN: H3ResDomainError, E_CELL_INVALID: H3CellInvalidError, E_DIR_EDGE_INVALID: H3DirEdgeInvalidError, E_UNDIR_EDGE_INVALID: H3UndirEdgeInvalidError, E_VERTEX_INVALID: H3VertexInvalidError, E_PENTAGON: H3PentagonError, E_DUPLICATE_INPUT: H3DuplicateInputError, E_NOT_NEIGHBORS: H3NotNeighborsError, E_RES_MISMATCH: H3ResMismatchError, E_MEMORY_ALLOC: H3MemoryAllocError, E_MEMORY_BOUNDS: H3MemoryBoundsError, E_OPTION_INVALID: H3OptionInvalidError, E_INDEX_INVALID: H3IndexInvalidError, E_BASE_CELL_DOMAIN: H3BaseCellDomainError, E_DIGIT_DOMAIN: H3DigitDomainError, E_DELETED_DIGIT: H3DeletedDigitError, } # Go back and modify the class definitions so that each concrete exception # stores its associated error code. for code, ex in error_mapping.items(): ex.h3_error_code = code # # Helper functions # # TODO: Move the helpers to util? # TODO: Unclear how/where to expose these functions. cdef/cpdef? cpdef error_code_to_exception(H3Error err): """ Return Python exception corresponding to integer error code given via the H3ErrorCodes enum in `h3api.h.in` in the C library. """ if err == E_SUCCESS: return None elif err in error_mapping: return error_mapping[err] else: return UnknownH3ErrorCode(err) cdef check_for_error(H3Error err): ex = error_code_to_exception(err) if ex: raise ex cpdef H3Error get_H3_ERROR_END(): """ Return integer H3_ERROR_END from the H3ErrorCodes enum in `h3api.h.in` in the C library, which is one greater than the last valid error code. """ return H3_ERROR_END # todo: There's no easy way to do `*args` in `cdef` functions, but I'm also # not sure this even needs to be a Cython `cdef` function at all, or that # any of the other helper functions need to be in Cython. # todo: Revisit after we've played with this a bit. # todo: also: maybe the extra messages aren't that much more helpful... cdef check_for_error_msg(H3Error err, str msg): ex = error_code_to_exception(err) if ex: raise ex(msg) uber-h3-py-2883945/src/h3/_cy/h3lib.pxd000066400000000000000000000155471520643167100172270ustar00rootroot00000000000000# cython: c_string_type=unicode, c_string_encoding=utf8 from cpython cimport bool from libc.stdint cimport uint32_t, uint64_t, int64_t ctypedef object H3str cdef extern from 'h3api.h': cdef int H3_VERSION_MAJOR cdef int H3_VERSION_MINOR cdef int H3_VERSION_PATCH ctypedef uint64_t H3int 'H3Index' ctypedef uint32_t H3Error ctypedef enum H3ErrorCodes: E_SUCCESS = 0 E_FAILED = 1 E_DOMAIN = 2 E_LATLNG_DOMAIN = 3 E_RES_DOMAIN = 4 E_CELL_INVALID = 5 E_DIR_EDGE_INVALID = 6 E_UNDIR_EDGE_INVALID = 7 E_VERTEX_INVALID = 8 E_PENTAGON = 9 E_DUPLICATE_INPUT = 10 E_NOT_NEIGHBORS = 11 E_RES_MISMATCH = 12 E_MEMORY_ALLOC = 13 E_MEMORY_BOUNDS = 14 E_OPTION_INVALID = 15 E_INDEX_INVALID = 16 E_BASE_CELL_DOMAIN = 17 E_DIGIT_DOMAIN = 18 E_DELETED_DIGIT = 19 H3_ERROR_END # sentinel value ctypedef struct LatLng: double lat # in radians double lng # in radians ctypedef struct CellBoundary: int num_verts 'numVerts' LatLng verts[10] # MAX_CELL_BNDRY_VERTS ctypedef struct CoordIJ: int i int j ctypedef struct GeoLoop: int numVerts LatLng *verts ctypedef struct GeoPolygon: GeoLoop geoloop int numHoles GeoLoop *holes int isValidCell(H3int h) nogil int isPentagon(H3int h) nogil int isResClassIII(H3int h) nogil int isValidDirectedEdge(H3int edge) nogil int isValidVertex(H3int v) nogil int isValidIndex(H3int h) nogil double degsToRads(double degrees) nogil double radsToDegs(double radians) nogil int getResolution(H3int h) nogil int getBaseCellNumber(H3int h) nogil H3Error getIndexDigit(H3int h, int res, int *out) nogil H3Error constructCell(int res, int baseCellNumber, const int *digits, H3int *out) nogil H3Error latLngToCell(const LatLng *g, int res, H3int *out) nogil H3Error cellToLatLng(H3int h, LatLng *) nogil H3Error gridDistance(H3int h1, H3int h2, int64_t *distance) nogil H3Error cellToVertex(H3int cell, int vertexNum, H3int *out) nogil H3Error cellToVertexes(H3int cell, H3int *vertexes) nogil H3Error vertexToLatLng(H3int vertex, LatLng *coord) nogil H3Error maxGridDiskSize(int k, int64_t *out) nogil # num/out/N? H3Error gridDisk(H3int h, int k, H3int *out) nogil H3Error cellToParent( H3int h, int parentRes, H3int *parent) nogil H3Error cellToCenterChild(H3int h, int childRes, H3int *child) nogil H3Error cellToChildPos(H3int child, int parentRes, int64_t *out) nogil H3Error childPosToCell(int64_t childPos, H3int parent, int childRes, H3int *child) nogil H3Error cellToChildrenSize(H3int h, int childRes, int64_t *num) nogil # num/out/N? H3Error cellToChildren( H3int h, int childRes, H3int *children) nogil H3Error compactCells( const H3int *cells_u, H3int *cells_c, const int num_u ) nogil H3Error uncompactCellsSize( const H3int *cells_c, const int64_t num_c, const int res, int64_t *num_u ) nogil H3Error uncompactCells( const H3int *cells_c, const int num_c, H3int *cells_u, const int num_u, const int res ) nogil H3Error getNumCells(int res, int64_t *out) nogil int pentagonCount() nogil int res0CellCount() nogil H3Error getPentagons(int res, H3int *out) nogil H3Error getRes0Cells(H3int *out) nogil H3Error gridPathCellsSize(H3int start, H3int end, int64_t *size) nogil H3Error gridPathCells(H3int start, H3int end, H3int *out) nogil H3Error getHexagonAreaAvgKm2(int res, double *out) nogil H3Error getHexagonAreaAvgM2(int res, double *out) nogil H3Error cellAreaRads2(H3int h, double *out) nogil H3Error cellAreaKm2(H3int h, double *out) nogil H3Error cellAreaM2(H3int h, double *out) nogil H3Error maxFaceCount(H3int h, int *out) nogil H3Error getIcosahedronFaces(H3int h3, int *out) nogil H3Error cellToLocalIj(H3int origin, H3int h3, uint32_t mode, CoordIJ *out) nogil H3Error localIjToCell(H3int origin, const CoordIJ *ij, uint32_t mode, H3int *out) nogil H3Error gridDiskDistances(H3int origin, int k, H3int *out, int *distances) nogil H3Error gridRing(H3int origin, int k, H3int *out) nogil H3Error gridRingUnsafe(H3int origin, int k, H3int *out) nogil H3Error areNeighborCells(H3int origin, H3int destination, int *out) nogil H3Error cellsToDirectedEdge(H3int origin, H3int destination, H3int *out) nogil H3Error getDirectedEdgeOrigin(H3int edge, H3int *out) nogil H3Error getDirectedEdgeDestination(H3int edge, H3int *out) nogil H3Error originToDirectedEdges(H3int origin, H3int *edges) nogil # todo: directedEdgeToCells H3Error getHexagonEdgeLengthAvgKm(int res, double *out) nogil H3Error getHexagonEdgeLengthAvgM(int res, double *out) nogil H3Error edgeLengthRads(H3int edge, double *out) nogil H3Error edgeLengthKm(H3int edge, double *out) nogil H3Error edgeLengthM(H3int edge, double *out) nogil H3Error cellToBoundary(H3int h3, CellBoundary *gp) nogil H3Error directedEdgeToBoundary(H3int edge, CellBoundary *gb) nogil double greatCircleDistanceRads(const LatLng *a, const LatLng *b) nogil double greatCircleDistanceKm(const LatLng *a, const LatLng *b) nogil double greatCircleDistanceM(const LatLng *a, const LatLng *b) nogil H3Error maxPolygonToCellsSize(const GeoPolygon *geoPolygon, int res, uint32_t flags, uint64_t *count) H3Error polygonToCells(const GeoPolygon *geoPolygon, int res, uint32_t flags, H3int *out) H3Error maxPolygonToCellsSizeExperimental(const GeoPolygon *geoPolygon, int res, uint32_t flags, uint64_t *count) H3Error polygonToCellsExperimental(const GeoPolygon *geoPolygon, int res, uint32_t flags, uint64_t sz, H3int *out) ctypedef struct GeoMultiPolygon: int numPolygons GeoPolygon *polygons # int hexRange(H3int origin, int k, H3int *out) # int hexRangeDistances(H3int origin, int k, H3int *out, int *distances) # int hexRanges(H3int *h3Set, int length, int k, H3int *out) # void h3SetToLinkedGeo(const H3int *h3Set, const int numCells, LinkedGeoPolygon *out) # void destroyLinkedPolygon(LinkedGeoPolygon *polygon) # H3int stringToH3(const char *str) # void h3ToString(H3int h, char *str, size_t sz) # void getH3intesFromUnidirectionalEdge(H3int edge, H3int *originDestination) # cellsToMultiPolygon and destroyGeoMultiPolygon are temporarily excluded # from the public API, so we use an internal header until they're moved # back into h3api.h cdef extern from 'cellsToMultiPoly.h': H3Error cellsToMultiPolygon(const H3int *cells, const int64_t numCells, GeoMultiPolygon *out) void destroyGeoMultiPolygon(GeoMultiPolygon *mpoly) uber-h3-py-2883945/src/h3/_cy/latlng.pxd000066400000000000000000000004121520643167100174700ustar00rootroot00000000000000from .h3lib cimport H3int cpdef H3int latlng_to_cell(double lat, double lng, int res) except 1 cpdef (double, double) cell_to_latlng(H3int h) except * cpdef double great_circle_distance( double lat1, double lng1, double lat2, double lng2, unit=*) except -1 uber-h3-py-2883945/src/h3/_cy/latlng.pyx000066400000000000000000000202621520643167100175220ustar00rootroot00000000000000from libc.stdint cimport uint64_t cimport h3lib from h3lib cimport bool, H3int from .util cimport ( check_cell, check_edge, check_res, deg2coord, coord2deg ) from .error_system cimport check_for_error from .memory cimport H3MemoryManager # TODO: We might be OK with taking the GIL for the functions in this module from libc.stdlib cimport ( # malloc as h3_malloc, # not used calloc as h3_calloc, realloc as h3_realloc, free as h3_free, ) cpdef H3int latlng_to_cell(double lat, double lng, int res) except 1: cdef: h3lib.LatLng c H3int out c = deg2coord(lat, lng) check_for_error( h3lib.latLngToCell(&c, res, &out) ) return out cpdef (double, double) cell_to_latlng(H3int h) except *: """Map an H3 cell into its centroid geo-coordinate (lat/lng)""" cdef: h3lib.LatLng c check_cell(h) # todo: think about: if you give this an invalid cell, should it still return a lat/lng? # idea: safe and unsafe APIs? check_for_error( h3lib.cellToLatLng(h, &c) ) return coord2deg(c) cdef h3lib.GeoLoop make_geoloop(latlngs) except *: """ The returned `GeoLoop` must be freed with a call to `free_geoloop`. Parameters ---------- latlngs : list or tuple GeoLoop: A sequence of >= 3 (lat, lng) pairs where the last element may or may not be same as the first (to form a closed loop). The order of the pairs may be either clockwise or counterclockwise. """ cdef: h3lib.GeoLoop gl gl.numVerts = len(latlngs) # todo: need for memory management # can automatically free? gl.verts = h3_calloc(gl.numVerts, sizeof(h3lib.LatLng)) for i, (lat, lng) in enumerate(latlngs): gl.verts[i] = deg2coord(lat, lng) return gl cdef free_geoloop(h3lib.GeoLoop* gl): h3_free(gl.verts) gl.verts = NULL cdef class GeoPolygon: cdef: h3lib.GeoPolygon gp def __cinit__(self, outer, holes=None): """ Parameters ---------- outer : list or tuple GeoLoop A GeoLoop is a sequence of >= 3 (lat, lng) pairs where the last element may or may not be same as the first (to form a closed loop). The order of the pairs may be either clockwise or counterclockwise. holes : list or tuple A sequence of GeoLoops """ if holes is None: holes = [] self.gp.geoloop = make_geoloop(outer) self.gp.numHoles = len(holes) self.gp.holes = NULL if len(holes) > 0: self.gp.holes = h3_calloc(len(holes), sizeof(h3lib.GeoLoop)) for i, hole in enumerate(holes): self.gp.holes[i] = make_geoloop(hole) def __dealloc__(self): free_geoloop(&self.gp.geoloop) for i in range(self.gp.numHoles): free_geoloop(&self.gp.holes[i]) h3_free(self.gp.holes) def polygon_to_cells(outer, int res, holes=None): """ Get the set of cells whose center is contained in a polygon. The polygon is defined similarity to the GeoJson standard, with an exterior `outer` ring of lat/lng points, and a list of `holes`, each of which are also rings of lat/lng points. Each ring may be in clockwise or counter-clockwise order (right-hand rule or not), and may or may not be a closed loop (where the last element is equal to the first). The GeoJSON spec requires the right-hand rule and a closed loop, but this function relaxes those constraints. Unlike the GeoJson standard, the elements of the lat/lng pairs of each ring are in lat/lng order, instead of lng/lat order. We'll handle translation to different formats in the Python code, rather than the Cython code. Parameters ---------- outer : list or tuple A ring given by a sequence of lat/lng pairs. res : int The resolution of the output hexagons holes : list or tuple A collection of rings, each given by a sequence of lat/lng pairs. These describe any the "holes" in the polygon. """ cdef: uint64_t n check_res(res) if not outer: return H3MemoryManager(0).to_mv() gp = GeoPolygon(outer, holes=holes) check_for_error( h3lib.maxPolygonToCellsSize(&gp.gp, res, 0, &n) ) hmm = H3MemoryManager(n) check_for_error( h3lib.polygonToCells(&gp.gp, res, 0, hmm.ptr) ) mv = hmm.to_mv() return mv def polygons_to_cells(polygons, int res): mvs = [ polygon_to_cells(outer=poly.outer, res=res, holes=poly.holes) for poly in polygons ] n = sum(map(len, mvs)) hmm = H3MemoryManager(n) # probably super inefficient, but it is working! # tood: move this to C k = 0 for mv in mvs: for v in mv: hmm.ptr[k] = v k += 1 return hmm.to_mv() def polygon_to_cells_experimental(outer, int res, int flag, holes=None): """ Get the set of cells whose center is contained in a polygon. The polygon is defined similarity to the GeoJson standard, with an exterior `outer` ring of lat/lng points, and a list of `holes`, each of which are also rings of lat/lng points. Each ring may be in clockwise or counter-clockwise order (right-hand rule or not), and may or may not be a closed loop (where the last element is equal to the first). The GeoJSON spec requires the right-hand rule and a closed loop, but this function relaxes those constraints. Unlike the GeoJson standard, the elements of the lat/lng pairs of each ring are in lat/lng order, instead of lng/lat order. We'll handle translation to different formats in the Python code, rather than the Cython code. Parameters ---------- outer : list or tuple A ring given by a sequence of lat/lng pairs. res : int The resolution of the output hexagons flag : int Polygon to cells flag, such as containment mode. holes : list or tuple A collection of rings, each given by a sequence of lat/lng pairs. These describe any the "holes" in the polygon. """ cdef: uint64_t n check_res(res) if not outer: return H3MemoryManager(0).to_mv() gp = GeoPolygon(outer, holes=holes) check_for_error( h3lib.maxPolygonToCellsSizeExperimental(&gp.gp, res, flag, &n) ) hmm = H3MemoryManager(n) check_for_error( h3lib.polygonToCellsExperimental(&gp.gp, res, flag, n, hmm.ptr) ) mv = hmm.to_mv() return mv def polygons_to_cells_experimental(polygons, int res, int flag): mvs = [ polygon_to_cells_experimental(outer=poly.outer, res=res, holes=poly.holes, flag=flag) for poly in polygons ] n = sum(map(len, mvs)) hmm = H3MemoryManager(n) # probably super inefficient, but it is working! # tood: move this to C k = 0 for mv in mvs: for v in mv: hmm.ptr[k] = v k += 1 return hmm.to_mv() def cell_to_boundary(H3int h): """Compose an array of geo-coordinates that outlines a hexagonal cell""" cdef: h3lib.CellBoundary gb check_cell(h) h3lib.cellToBoundary(h, &gb) verts = tuple( coord2deg(gb.verts[i]) for i in range(gb.num_verts) ) return verts def directed_edge_to_boundary(H3int edge): """ Returns the CellBoundary containing the coordinates of the edge """ cdef: h3lib.CellBoundary gb check_edge(edge) h3lib.directedEdgeToBoundary(edge, &gb) # todo: move this verts transform into the CellBoundary object verts = tuple( coord2deg(gb.verts[i]) for i in range(gb.num_verts) ) return verts cpdef double great_circle_distance( double lat1, double lng1, double lat2, double lng2, unit='km') except -1: a = deg2coord(lat1, lng1) b = deg2coord(lat2, lng2) if unit == 'rads': d = h3lib.greatCircleDistanceRads(&a, &b) elif unit == 'km': d = h3lib.greatCircleDistanceKm(&a, &b) elif unit == 'm': d = h3lib.greatCircleDistanceM(&a, &b) else: raise ValueError('Unknown unit: {}'.format(unit)) return d uber-h3-py-2883945/src/h3/_cy/memory.pxd000066400000000000000000000003541520643167100175240ustar00rootroot00000000000000from .h3lib cimport H3int cdef class H3MemoryManager: cdef: size_t n H3int* ptr cdef H3int[:] to_mv(self) cdef H3int[:] to_mv_keep_zeros(self) cdef int[:] int_mv(size_t n) cpdef H3int[:] iter_to_mv(cells) uber-h3-py-2883945/src/h3/_cy/memory.pyx000066400000000000000000000156771520643167100175670ustar00rootroot00000000000000from cython.view cimport array from .h3lib cimport H3int """ ### Memory allocation options We have a few options for the memory allocation functions. There's a trade-off between using the Python allocators which let Python track memory usage and offers some optimizations vs the system allocators, which do not need to acquire the GIL. """ """ System allocation functions. These do not acquire the GIL. """ from libc.stdlib cimport ( # malloc as h3_malloc, # not used calloc as h3_calloc, realloc as h3_realloc, free as h3_free, ) """ PyMem_Raw* functions should just be wrappers around system allocators also given in libc.stdlib. These functions do not acquire the GIL. Note that these do not have a calloc function until py 3.5 and Cython 3.0, so we would need to zero-out memory manually. https://python.readthedocs.io/en/stable/c-api/memory.html#raw-memory-interface """ # from cpython.mem cimport ( # PyMem_RawMalloc as h3_malloc, # # PyMem_RawCalloc as h3_calloc, # only in Python >=3.5 (and Cython >=3.0?) # PyMem_RawRealloc as h3_realloc, # PyMem_RawFree as h3_free, # ) """ These functions use the Python allocator (instead of the system allocator), which offers some optimizations for Python, and allows Python to track memory usage. However, these functions must acquire the GIL. Note that these do not have a calloc function until py 3.5 and Cython 3.0, so we would need to zero-out memory manually. https://cython.readthedocs.io/en/stable/src/tutorial/memory_allocation.html https://python.readthedocs.io/en/stable/c-api/memory.html#memory-interface """ # from cpython.mem cimport ( # PyMem_Malloc as h3_malloc, # # PyMem_Calloc as h3_calloc, # only in Python >=3.5 (and Cython >=3.0?) # PyMem_Realloc as h3_realloc, # PyMem_Free as h3_free, # ) cdef size_t move_nonzeros(H3int* a, size_t n): """ Move nonzero elements to front of array `a` of length `n`. Return the number of nonzero elements. Loop invariant: Everything *before* `i` or *after* `j` is "done". Move `i` and `j` inwards until they equal, and exit. You can move `i` forward until there's a zero in front of it. You can move `j` backward until there's a nonzero to the left of it. Anything to the right of `j` is "junk" that can be reallocated. | a | b | 0 | c | d | ... | ^ ^ i j | a | b | d | c | d | ... | ^ ^ i j """ cdef: size_t i = 0 size_t j = n while i < j: if a[j-1] == 0: j -= 1 continue if a[i] != 0: i += 1 continue # if we're here, we know: # a[i] == 0 # a[j-1] != 0 # i < j # so we can swap! (actually, move a[j-1] -> a[i]) a[i] = a[j-1] j -= 1 return i cdef H3int[:] empty_memory_view(): # todo: get rid of this? # there's gotta be a better way to do this... # create an empty cython.view.array? cdef: H3int a[1] return (a)[:0] cdef _remove_zeros(H3MemoryManager x): x.n = move_nonzeros(x.ptr, x.n) if x.n == 0: h3_free(x.ptr) x.ptr = NULL else: x.ptr = h3_realloc(x.ptr, x.n*sizeof(H3int)) if not x.ptr: raise MemoryError() cdef H3int[:] _copy_to_mv(const H3int* ptr, size_t n): cdef: array arr arr = ptr arr.callback_free_data = h3_free return arr cdef H3int[:] _create_mv(H3MemoryManager x): if x.n == 0: h3_free(x.ptr) x.ptr = NULL mv = empty_memory_view() else: mv = _copy_to_mv(x.ptr, x.n) # responsibility for the memory moves from this object to the array/memoryview x.ptr = NULL x.n = 0 return mv """ TODO: The not None declaration for the argument automatically rejects None values as input, which would otherwise be allowed. The reason why None is allowed by default is that it is conveniently used for return arguments: https://cython.readthedocs.io/en/latest/src/userguide/memoryviews.html#syntax TODO: potential optimization: https://cython.readthedocs.io/en/latest/src/userguide/memoryviews.html#performance-disabling-initialization-checks ## future improvements: - abolish any appearance of &thing[0]. (i.e., identical interfaces) - can i make the interface for all these memory views identical? """ cdef class H3MemoryManager: """ Cython object in charge of allocating and freeing memory for arrays of H3 indexes. Initially allocates memory and provides access through `self.ptr` and `self.n`. The `to_mv()` function removes responsibility for the allocated memory from this object to a memory view object. A memory view object automatically deallocates its memory during garbage collection. If the H3MemoryManager is garbage collected before running `to_mv()`, it will deallocate its memory itself. This pattern is useful for a few reasons: - provide convenient access to the raw memory pointer and length for passing to h3lib functions - remove zeroes from the array output (some h3lib functions may return results with zeros/H3NULL values) - cython and python array types have weird interfaces; memoryviews are much cleaner If we find a better way to do these then this class may no longer be necessary. TODO: consider a context manager pattern """ def __cinit__(self, size_t n): self.n = n self.ptr = h3_calloc(self.n, sizeof(H3int)) if not self.ptr: raise MemoryError() cdef H3int[:] to_mv_keep_zeros(self): # todo: this could be a private method return _create_mv(self) cdef H3int[:] to_mv(self): _remove_zeros(self) return _create_mv(self) def __dealloc__(self): # If the memory has been handed off to a memoryview, this pointer # should be NULL, and deallocing on NULL is fine. # If the pointer is *not* NULL, then this means the MemoryManager # has is still responsible for the memory (it hasn't given the memory away to another object). h3_free(self.ptr) """ todo: combine with the H3MemoryManager using fused types? https://cython.readthedocs.io/en/stable/src/userguide/fusedtypes.html """ cdef int[:] int_mv(size_t n): cdef: array arr if n == 0: raise MemoryError() else: ptr = h3_calloc(n, sizeof(int)) if ptr is NULL: raise MemoryError() arr = ptr arr.callback_free_data = h3_free return arr cpdef H3int[:] iter_to_mv(cells): """ cells needs to be an iterable that knows its size... or should we have it match the np.fromiter function, which infers if not available? """ cdef: H3int[:] mv n = len(cells) mv = H3MemoryManager(n).to_mv_keep_zeros() for i,h in enumerate(cells): mv[i] = h return mv uber-h3-py-2883945/src/h3/_cy/to_multipoly.pyx000066400000000000000000000017631520643167100210060ustar00rootroot00000000000000cimport h3lib from h3lib cimport H3int from .util cimport check_cell, coord2deg from .error_system cimport check_for_error cdef _loop_to_list(const h3lib.GeoLoop *loop): return [coord2deg(loop.verts[v]) for v in range(loop.numVerts)] cdef _poly_to_lists(const h3lib.GeoPolygon *poly): return ( [_loop_to_list(&poly.geoloop)] + [_loop_to_list(&poly.holes[h]) for h in range(poly.numHoles)] ) def _to_multi_polygon(const H3int[:] cells): cdef: h3lib.GeoMultiPolygon mpoly H3int cell for cell in cells: check_cell(cell) check_for_error( h3lib.cellsToMultiPolygon(&cells[0], len(cells), &mpoly) ) try: out = [ _poly_to_lists(&mpoly.polygons[p]) for p in range(mpoly.numPolygons) ] finally: h3lib.destroyGeoMultiPolygon(&mpoly) return out def cells_to_multi_polygon(const H3int[:] cells): if len(cells) == 0: return [] return _to_multi_polygon(cells) uber-h3-py-2883945/src/h3/_cy/util.pxd000066400000000000000000000005641520643167100171740ustar00rootroot00000000000000from .h3lib cimport H3int, H3str, LatLng cdef LatLng deg2coord(double lat, double lng) nogil cdef (double, double) coord2deg(LatLng c) nogil cpdef H3int str_to_int(H3str h) except? 0 cpdef H3str int_to_str(H3int x) cdef check_cell(H3int h) cdef check_edge(H3int e) cdef check_vertex(H3int v) cdef check_index(H3int h) cdef check_res(int res) cdef check_distance(int k) uber-h3-py-2883945/src/h3/_cy/util.pyx000066400000000000000000000050021520643167100172110ustar00rootroot00000000000000from .h3lib cimport ( H3int, H3str, isValidCell, isValidDirectedEdge, isValidVertex, isValidIndex, ) cimport h3lib from .error_system import ( H3ResDomainError, H3DomainError, H3DirEdgeInvalidError, H3CellInvalidError, H3VertexInvalidError, H3IndexInvalidError, ) cdef h3lib.LatLng deg2coord(double lat, double lng) nogil: cdef: h3lib.LatLng c c.lat = h3lib.degsToRads(lat) c.lng = h3lib.degsToRads(lng) return c cdef (double, double) coord2deg(h3lib.LatLng c) nogil: return ( h3lib.radsToDegs(c.lat), h3lib.radsToDegs(c.lng) ) cpdef basestring c_version(): v = ( h3lib.H3_VERSION_MAJOR, h3lib.H3_VERSION_MINOR, h3lib.H3_VERSION_PATCH, ) return '{}.{}.{}'.format(*v) cpdef H3int str_to_int(H3str h) except? 0: return int(h, 16) cpdef H3str int_to_str(H3int x): """ Convert H3 integer to hex string representation Need to be careful in Python 2 because `hex(x)` may return a string with a trailing `L` character (denoting a "large" integer). The formatting approach below avoids this. Also need to be careful about unicode/str differences. """ return '{:x}'.format(x) cdef check_cell(H3int h): """ Check if valid H3 "cell" (hexagon or pentagon). Does not check if a valid H3 edge, for example. Since this function is used by multiple interfaces (int or str), we want the error message to be informative to the user in either case. We use the builtin `hex` function instead of `int_to_str` to prepend `0x` to indicate that this **integer** representation is incorrect, but in a format that is easily compared to `str` inputs. """ if isValidCell(h) == 0: raise H3CellInvalidError('Integer is not a valid H3 cell: {}'.format(hex(h))) cdef check_edge(H3int e): if isValidDirectedEdge(e) == 0: raise H3DirEdgeInvalidError('Integer is not a valid H3 edge: {}'.format(hex(e))) cdef check_vertex(H3int v): if isValidVertex(v) == 0: raise H3VertexInvalidError('Integer is not a valid H3 vertex: {}'.format(hex(v))) cdef check_index(H3int h): if isValidIndex(h) == 0: raise H3IndexInvalidError('Integer is not a valid H3 index: {}'.format(hex(h))) cdef check_res(int res): if (res < 0) or (res > 15): raise H3ResDomainError(res) cdef check_distance(int k): if k < 0: raise H3DomainError( 'Grid distances must be nonnegative. Received: {}'.format(k) ) uber-h3-py-2883945/src/h3/_cy/vertex.pxd000066400000000000000000000003451520643167100175310ustar00rootroot00000000000000from .h3lib cimport bool, H3int cpdef H3int cell_to_vertex(H3int h, int vertex_num) except 1 cpdef H3int[:] cell_to_vertexes(H3int h) cpdef (double, double) vertex_to_latlng(H3int v) except * cpdef bool is_valid_vertex(H3int v) uber-h3-py-2883945/src/h3/_cy/vertex.pyx000066400000000000000000000016161520643167100175600ustar00rootroot00000000000000cimport h3lib from h3lib cimport bool, H3int from .util cimport ( check_cell, check_vertex, coord2deg ) from .error_system cimport check_for_error from .memory cimport H3MemoryManager cpdef H3int cell_to_vertex(H3int h, int vertex_num) except 1: cdef: H3int out check_cell(h) check_for_error( h3lib.cellToVertex(h, vertex_num, &out) ) return out cpdef H3int[:] cell_to_vertexes(H3int h): cdef: H3int out check_cell(h) hmm = H3MemoryManager(6) check_for_error( h3lib.cellToVertexes(h, hmm.ptr) ) mv = hmm.to_mv() return mv cpdef (double, double) vertex_to_latlng(H3int v) except *: cdef: h3lib.LatLng c check_vertex(v) check_for_error( h3lib.vertexToLatLng(v, &c) ) return coord2deg(c) cpdef bool is_valid_vertex(H3int v): return h3lib.isValidVertex(v) == 1 uber-h3-py-2883945/src/h3/_h3shape.py000066400000000000000000000205571520643167100170000ustar00rootroot00000000000000from abc import ABCMeta, abstractmethod class H3Shape(metaclass=ABCMeta): """ Abstract parent class of ``LatLngPoly`` and ``LatLngMultiPoly``. """ @property @abstractmethod def __geo_interface__(self): """ https://github.com/pytest-dev/pytest-cov/issues/428 """ class LatLngPoly(H3Shape): """ Container for loops of lat/lng points describing a polygon, possibly with holes. Attributes ---------- outer : list[tuple[float, float]] List of lat/lng points describing the outer loop of the polygon holes : list[list[tuple[float, float]]] List of loops of lat/lng points describing the holes of the polygon Examples -------- A polygon with a single outer ring consisting of 4 points, having no holes: >>> LatLngPoly( ... [(37.68, -122.54), (37.68, -122.34), (37.82, -122.34), (37.82, -122.54)], ... ) The same polygon, but with one hole consisting of 3 points: >>> LatLngPoly( ... [(37.68, -122.54), (37.68, -122.34), (37.82, -122.34), (37.82, -122.54)], ... [(37.76, -122.51), (37.76, -122.44), (37.81, -122.51)], ... ) The same as above, but with one additional hole, made up of 5 points: >>> LatLngPoly( ... [(37.68, -122.54), (37.68, -122.34), (37.82, -122.34), (37.82, -122.54)], ... [(37.76, -122.51), (37.76, -122.44), (37.81, -122.51)], ... [(37.71, -122.43), (37.71, -122.37), (37.73, -122.37), (37.75, -122.41), ... (37.73, -122.43)], ... ) """ def __init__(self, outer, *holes): loops = [outer] + list(holes) for loop in loops: if len(loop) in (1, 2): raise ValueError('Non-empty LatLngPoly loops need at least 3 points.') point_dimensions = set(map(len, loop)) # empty set is possible for empty polygons, so we check if a subset if not (point_dimensions <= {2}): raise ValueError('LatLngPoly only accepts 2D points: lat/lng.') self.outer = tuple(_open_ring(outer)) self.holes = tuple( _open_ring(hole) for hole in holes ) def __repr__(self): return ''.format(self.loopcode) def __len__(self): """ Should this be the number of points in the outer loop, the number of holes (or +1 for the outer loop)? """ raise NotImplementedError('No clear definition of length for LatLngPoly.') @property def loopcode(self): """ Short code for describing the length of the outer loop and each hole Example: ``[382/(18, 6, 6)]`` indicates an outer loop of 382 points, along with 3 holes with 18, 6, and 6 points, respectively. Example: ``[15]`` indicates an outer loop of 15 points and no holes. """ outer = len(self.outer) holes = tuple(map(len, self.holes)) outer = str(outer) if holes: out = outer + '/' + str(holes) else: out = outer return '[' + out + ']' @property def __geo_interface__(self): ll2 = _polygon_to_LL2(self) gj_dict = _LL2_to_geojson_dict(ll2) return gj_dict class LatLngMultiPoly(H3Shape): """ Container for multiple ``LatLngPoly`` polygons. Attributes ---------- polys : list[LatLngPoly] List of lat/lng points describing the outer loop of the polygon """ def __init__(self, *polys): self.polys = tuple(polys) for p in self.polys: if not isinstance(p, LatLngPoly): raise ValueError('LatLngMultiPoly requires each input to be an LatLngPoly object, instead got: ' + str(p)) # noqa def __repr__(self): out = [p.loopcode for p in self.polys] out = ', '.join(out) out = ''.format(out) return out def __iter__(self): return iter(self.polys) def __len__(self): """ Give the number of polygons in this multi-polygon. """ """ TODO: Pandas series or dataframe representation changes depending on if __len__ is defined. I'd prefer the one that states `LatLngMultiPoly`. It seems like Pandas is assuming an iterable is best-described by its elements when choosing the representation. when __len__ *IS NOT* defined: 0 1 , , ) 1 (, , , , , , <... """ return len(self.polys) def __getitem__(self, index): return self.polys[index] @property def __geo_interface__(self): ll3 = _mpoly_to_LL3(self) gj_dict = _LL3_to_geojson_dict(ll3) return gj_dict """ Helpers for cells_to_geojson and geojson_to_cells. Dealing with GeoJSON Polygons and MultiPolygons can be confusing because there are so many nested lists. To help keep track, we use the following symbols to denote different levels of nesting. LL0: lat/lng or lng/lat pair LL1: list of LL0s LL2: list of LL1s (i.e., a polygon with holes) LL3: list of LL2s (i.e., several polygons with holes) ## TODO - Allow user to specify "container" in `cells_to_geojson`. - That is, they may want a MultiPolygon even if the output fits in a Polygon - 'auto', Polygon, MultiPolygon, FeatureCollection, GeometryCollection, ... """ def _mpoly_to_LL3(mpoly): ll3 = tuple( _polygon_to_LL2(poly) for poly in mpoly ) return ll3 def _LL3_to_mpoly(ll3): polys = [ _LL2_to_polygon(ll2) for ll2 in ll3 ] mpoly = LatLngMultiPoly(*polys) return mpoly def _polygon_to_LL2(poly): ll2 = [poly.outer] + list(poly.holes) ll2 = tuple( _close_ring(_swap_latlng(ll1)) for ll1 in ll2 ) return ll2 def _remove_z(ll1): ll1 = [(a, b) for a, b, *z in ll1] return ll1 def _LL2_to_polygon(ll2): ll2 = [ _remove_z(ll1) for ll1 in ll2 ] ll2 = [ _swap_latlng(ll1) for ll1 in ll2 ] h3poly = LatLngPoly(*ll2) return h3poly def _LL2_to_geojson_dict(ll2): gj_dict = { 'type': 'Polygon', 'coordinates': ll2, } return gj_dict def _LL3_to_geojson_dict(ll3): gj_dict = { 'type': 'MultiPolygon', 'coordinates': ll3, } return gj_dict def _swap_latlng(ll1): ll1 = tuple( (b, a) for a, b in ll1 ) return ll1 def _close_ring(ll1): """ Idempotent """ if ll1 and (ll1[0] != ll1[-1]): ll1 = tuple(ll1) + (ll1[0],) return ll1 def _open_ring(ll1): """ Idempotent """ if ll1 and (ll1[0] == ll1[-1]): ll1 = ll1[:-1] return ll1 def geo_to_h3shape(geo): """ Translate from ``__geo_interface__`` to H3Shape. ``geo`` either implements ``__geo_interface__`` or is a dict matching the format Returns ------- H3Shape """ # geo can be dict, a __geo_interface__, a string, LatLngPoly or LatLngMultiPoly if isinstance(geo, H3Shape): return geo if hasattr(geo, '__geo_interface__'): # get dict geo = geo.__geo_interface__ assert isinstance(geo, dict) # todo: remove t = geo['type'] coord = geo['coordinates'] if t == 'Polygon': ll2 = coord shape = _LL2_to_polygon(ll2) elif t == 'MultiPolygon': ll3 = coord shape = _LL3_to_mpoly(ll3) else: raise ValueError('Unrecognized type: ' + str(t)) return shape def h3shape_to_geo(h3shape): """ Translate from an ``H3Shape`` to a ``__geo_interface__`` dict. ``h3shape`` should be either ``LatLngPoly`` or ``LatLngMultiPoly`` Returns ------- dict """ return h3shape.__geo_interface__ uber-h3-py-2883945/src/h3/_version.py000066400000000000000000000001301520643167100171130ustar00rootroot00000000000000from importlib import metadata __version__ = metadata.version(__package__ or __name__) uber-h3-py-2883945/src/h3/api/000077500000000000000000000000001520643167100154745ustar00rootroot00000000000000uber-h3-py-2883945/src/h3/api/__init__.py000066400000000000000000000001621520643167100176040ustar00rootroot00000000000000# flake8: noqa from . import basic_int from . import basic_str from . import memview_int from . import numpy_int uber-h3-py-2883945/src/h3/api/basic_int/000077500000000000000000000000001520643167100174275ustar00rootroot00000000000000uber-h3-py-2883945/src/h3/api/basic_int/__init__.py000066400000000000000000000702641520643167100215510ustar00rootroot00000000000000# This file is **symlinked** across the APIs to ensure they are exactly the same. from typing import Literal from array import array from ... import _cy from ..._h3shape import ( H3Shape, LatLngPoly, LatLngMultiPoly, geo_to_h3shape, h3shape_to_geo, ) from ._convert import ( _in_scalar, _out_scalar, _in_collection, _out_collection, ) def versions(): """ Version numbers for the Python (wrapper) and C (wrapped) libraries. Versions are output as strings of the form ``'X.Y.Z'``. C and Python should match on ``X`` (major) and ``Y`` (minor), but may differ on ``Z`` (patch). Returns ------- dict like ``{'c': 'X.Y.Z', 'python': 'A.B.C'}`` """ from ..._version import __version__ v = { 'c': _cy.c_version(), 'python': __version__, } return v def str_to_int(h): """ Converts a hexadecimal string to an H3 64-bit integer index. Parameters ---------- h : str Hexadecimal string like ``'89754e64993ffff'`` Returns ------- int Unsigned 64-bit integer """ return _cy.str_to_int(h) def int_to_str(x): """ Converts an H3 64-bit integer index to a hexadecimal string. Parameters ---------- x : int Unsigned 64-bit integer Returns ------- str Hexadecimal string like ``'89754e64993ffff'`` """ return _cy.int_to_str(x) def get_num_cells(res): """ Return the total number of *cells* (hexagons and pentagons) for the given resolution. Returns ------- int """ return _cy.get_num_cells(res) def average_hexagon_area(res, unit='km^2'): """ Return the average area of an H3 *hexagon* for the given resolution. This average *excludes* pentagons. Parameters ---------- res : int H3 resolution unit: str Unit for area result (``'km^2'``, ``'m^2'``, or ``'rads^2'``) Returns ------- float """ return _cy.average_hexagon_area(res, unit) def average_hexagon_edge_length(res, unit='km'): """ Return the average *hexagon* edge length for the given resolution. This average *excludes* pentagons. Parameters ---------- res : int H3 resolution unit: str Unit for length result (``'km'``, ``'m'``, or ``'rads'``) Returns ------- float """ return _cy.average_hexagon_edge_length(res, unit) def is_valid_index(h): """Validates *any* H3 index (cell, vertex, or directed edge). Returns ------- bool """ try: h = _in_scalar(h) return _cy.is_valid_index(h) except (ValueError, TypeError): return False def is_valid_cell(h): """ Validates an H3 cell (hexagon or pentagon). Returns ------- bool """ try: h = _in_scalar(h) return _cy.is_valid_cell(h) except (ValueError, TypeError): return False def is_valid_directed_edge(edge): """ Validates an H3 directed edge. Returns ------- bool """ try: e = _in_scalar(edge) return _cy.is_valid_directed_edge(e) except (ValueError, TypeError): return False def latlng_to_cell(lat, lng, res): """ Return the cell containing the (lat, lng) point for a given resolution. Returns ------- H3Cell """ return _out_scalar(_cy.latlng_to_cell(lat, lng, res)) def cell_to_latlng(h): """ Return the center point of an H3 cell as a lat/lng pair. Parameters ---------- h : H3Cell Returns ------- lat : float Latitude lng : float Longitude """ h = _in_scalar(h) return _cy.cell_to_latlng(h) def get_resolution(h): """ Return the resolution of an H3 cell. Parameters ---------- h : H3Cell Returns ------- int """ # todo: could also work for edges h = _in_scalar(h) return _cy.get_resolution(h) def cell_to_parent(h, res=None): """ Get the parent of a cell. Parameters ---------- h : H3Cell res : int or None, optional The resolution for the parent If ``None``, then ``res = resolution(h) - 1`` Returns ------- H3Cell """ h = _in_scalar(h) p = _cy.cell_to_parent(h, res) p = _out_scalar(p) return p def grid_distance(h1, h2): """ Compute the grid distance between two cells. The grid distance is defined as the length of the shortest path between the cells in the graph formed by connecting adjacent cells. This function will raise an exception if the cells are too far apart to compute the distance. Parameters ---------- h1 : H3Cell h2 : H3Cell Returns ------- int """ h1 = _in_scalar(h1) h2 = _in_scalar(h2) d = _cy.grid_distance(h1, h2) return d def cell_to_boundary(h): """ Return tuple of lat/lng pairs describing the cell boundary. Parameters ---------- h : H3Cell Returns ------- tuple of (lat, lng) tuples """ h = _in_scalar(h) return _cy.cell_to_boundary(h) def grid_disk(h, k=1): """ Return unordered collection of cells with grid distance ``<= k`` from ``h``. That is, the "filled-in" disk. Parameters ---------- h : H3Cell k : int Size of disk. Returns ------- unordered collection of H3Cell Notes ----- There is currently no guaranteed order of the output cells. """ h = _in_scalar(h) mv = _cy.grid_disk(h, k) return _out_collection(mv) def grid_ring(h, k=1): """ Return unordered collection of cells with grid distance ``== k`` from ``h``. That is, the "hollow" ring. Parameters ---------- h : H3Cell k : int Size of ring. Returns ------- unordered collection of H3Cell Notes ----- There is currently no guaranteed order of the output cells. """ h = _in_scalar(h) mv = _cy.grid_ring(h, k) return _out_collection(mv) def cell_to_children_size(h, res=None): """ Number of children at resolution ``res`` of given cell. Parameters ---------- h : H3Cell res : int or None, optional The resolution for the children. If ``None``, then ``res = resolution(h) + 1`` Returns ------- int Count of children """ h = _in_scalar(h) return _cy.cell_to_children_size(h, res) def cell_to_children(h, res=None): """ Children of a cell as an unordered collection. Parameters ---------- h : H3Cell res : int or None, optional The resolution for the children. If ``None``, then ``res = resolution(h) + 1`` Returns ------- unordered collection of H3Cell Notes ----- There is currently no guaranteed order of the output cells. """ h = _in_scalar(h) mv = _cy.cell_to_children(h, res) return _out_collection(mv) def cell_to_child_pos(child, res_parent): """ Child position index of given cell, with respect to its parent at ``res_parent``. The reverse operation can be done with ``child_pos_to_cell``. Parameters ---------- child : H3Cell res_parent : int Returns ------- int Integer index of the child with respect to parent cell. """ child = _in_scalar(child) return _cy.cell_to_child_pos(child, res_parent) def child_pos_to_cell(parent, res_child, child_pos): """ Get child H3 cell from a parent cell, child resolution, and child position index. The reverse operation can be done with ``cell_to_child_pos``. Parameters ---------- parent : H3Cell res_child : int Child cell resolution child_pos : int Integer position of child cell, releative to parent. Returns ------- H3Cell """ parent = _in_scalar(parent) child = _cy.child_pos_to_cell(parent, res_child, child_pos) child = _out_scalar(child) return child # todo: nogil for expensive C operation? def compact_cells(cells): """ Compact a collection of H3 cells by combining smaller cells into larger cells, if all child cells are present. Input cells must all share the same resolution. Parameters ---------- cells : iterable of H3 Cells Returns ------- unordered collection of H3Cell Notes ----- There is currently no guaranteed order of the output cells. """ hu = _in_collection(cells) hc = _cy.compact_cells(hu) return _out_collection(hc) def uncompact_cells(cells, res): """ Reverse the ``compact_cells`` operation. Return a collection of H3 cells, all of resolution ``res``. Parameters ---------- cells : iterable of H3Cell res : int Resolution of desired output cells. Returns ------- unordered collection of H3Cell Notes ----- There is currently no guaranteed order of the output cells. """ # TODO: add test to make sure an error is returned when input contains cell # smaller than output res. hc = _in_collection(cells) hu = _cy.uncompact_cells(hc, res) return _out_collection(hu) def polygon_to_cells(h3shape, res): """ Alias for ``h3shape_to_cells``. """ return h3shape_to_cells(h3shape, res) def h3shape_to_cells(h3shape, res): """ Return the collection of H3 cells at a given resolution whose center points are contained within an ``LatLngPoly`` or ``LatLngMultiPoly``. Parameters ---------- h3shape : ``H3Shape`` res : int Resolution of the output cells Returns ------- list of H3Cell Examples -------- >>> poly = LatLngPoly( ... [(37.68, -122.54), (37.68, -122.34), (37.82, -122.34), ... (37.82, -122.54)], ... ) >>> h3.h3shape_to_cells(poly, 6) ['862830807ffffff', '862830827ffffff', '86283082fffffff', '862830877ffffff', '862830947ffffff', '862830957ffffff', '86283095fffffff'] Notes ----- There is currently no guaranteed order of the output cells. """ # todo: not sure if i want this dispatch logic here. maybe in the objects? if isinstance(h3shape, LatLngPoly): poly = h3shape mv = _cy.polygon_to_cells(poly.outer, res=res, holes=poly.holes) elif isinstance(h3shape, LatLngMultiPoly): mpoly = h3shape mv = _cy.polygons_to_cells(mpoly.polys, res=res) elif isinstance(h3shape, H3Shape): raise ValueError('Unrecognized H3Shape: ' + str(h3shape)) else: raise ValueError('Unrecognized type: ' + str(type(h3shape))) return _out_collection(mv) def polygon_to_cells_experimental( h3shape: H3Shape, res: int, contain: Literal['center', 'full', 'overlap', 'bbox_overlap'] = 'center', ): """ Alias for ``h3shape_to_cells_experimental``. """ return h3shape_to_cells_experimental(h3shape, res, contain) def h3shape_to_cells_experimental( h3shape: H3Shape, res: int, contain: Literal['center', 'full', 'overlap', 'bbox_overlap'] = 'center', ): """ Experimental function similar to ``h3shape_to_cells``, but with support for multiple cell containment modes. Using ``contain='center'`` should give identical behavior as ``h3shape_to_cells``. Note that this function is **experimental** and has no API stability gaurantees across versions, so it may change in the future. Parameters ---------- h3shape : ``H3Shape`` res : int Resolution of the output cells contain : {'center', 'full', 'overlap', 'bbox_overlap'}, optional Specifies the containment condition. - 'center': Cell center is contained in shape - 'full': Cell is fully contained in shape - 'overlap': Cell is partially contained in shape - 'bbox_overlap': Cell bounding box is partially contained in shape Default is 'center'. Returns ------- list of H3Cell Examples -------- >>> poly = LatLngPoly( ... [(37.68, -122.54), (37.68, -122.34), (37.82, -122.34), ... (37.82, -122.54)], ... ) >>> h3.h3shape_to_cells_experimental(poly, 6, 'center') ['862830807ffffff', '862830827ffffff', '86283082fffffff', '862830877ffffff', '862830947ffffff', '862830957ffffff', '86283095fffffff'] Notes ----- There is currently no guaranteed order of the output cells. """ contain_modes = { 'center': 0, 'full': 1, 'overlap': 2, 'bbox_overlap': 3, } flag = contain_modes[contain] # todo: not sure if i want this dispatch logic here. maybe in the objects? if isinstance(h3shape, LatLngPoly): poly = h3shape mv = _cy.polygon_to_cells_experimental( poly.outer, res = res, holes = poly.holes, flag = flag, ) elif isinstance(h3shape, LatLngMultiPoly): mpoly = h3shape mv = _cy.polygons_to_cells_experimental(mpoly.polys, res=res, flag=flag) elif isinstance(h3shape, H3Shape): raise ValueError('Unrecognized H3Shape: ' + str(h3shape)) else: raise ValueError('Unrecognized type: ' + str(type(h3shape))) return _out_collection(mv) def cells_to_h3shape(cells, *, tight=True): """ Return an ``H3Shape`` describing the area covered by a collection of H3 cells. Will return ``LatLngPoly`` or ``LatLngMultiPoly``. Parameters ---------- cells : iterable of H3 cells tight : bool If True, return ``LatLngPoly`` if possible. If False, always return ``LatLngMultiPoly``. Returns ------- LatLngPoly | LatLngMultiPoly Examples -------- >>> cells = ['8428309ffffffff', '842830dffffffff'] >>> h3.cells_to_h3shape(cells, tight=True) >>> h3.cells_to_h3shape(cells, tight=False) """ cells = _in_collection(cells) mpoly = _cy.cells_to_multi_polygon(cells) polys = [LatLngPoly(*poly) for poly in mpoly] out = LatLngMultiPoly(*polys) if tight and len(out) == 1: out = out[0] return out def geo_to_cells(geo, res): """Convert from ``__geo_interface__`` to cells. Parameters ---------- geo : an object implementing ``__geo_interface__`` or a dictionary in that format. Both ``LatLngPoly`` and ``LatLngMultiPoly`` implement the interface. res : int Resolution of desired output cells. Notes ----- There is currently no guaranteed order of the output cells. """ h3shape = geo_to_h3shape(geo) return h3shape_to_cells(h3shape, res) def cells_to_geo(cells, tight=True): """ Convert from cells to a ``__geo_interface__`` dict. Parameters ---------- cells : iterable of H3 Cells tight : bool When ``True``, returns an ``LatLngPoly`` when possible. When ``False``, always returns an ``LatLngMultiPoly``. Returns ------- dict in `__geo_interface__` format """ h3shape = cells_to_h3shape(cells, tight=tight) return h3shape_to_geo(h3shape) def is_pentagon(h): """ Identify if an H3 cell is a pentagon. Parameters ---------- h : H3Index Returns ------- bool ``True`` if input is a valid H3 cell which is a pentagon. Notes ----- A pentagon should *also* pass ``is_valid_cell()``. Will return ``False`` for valid H3Edge. """ return _cy.is_pentagon(_in_scalar(h)) def get_base_cell_number(h): """ Return the base cell *number* (``0`` to ``121``) of the given cell. The base cell *number* and the H3Index are two different representations of the same cell: the parent cell of resolution ``0``. The base cell *number* is encoded within the corresponding H3Index. todo: could work with edges Parameters ---------- h : H3Cell Returns ------- int Examples -------- >>> h = construct_cell(57, 2, 1, 4) >>> h '83728cfffffffff' >> get_base_cell_number(h) 57 """ return _cy.get_base_cell_number(_in_scalar(h)) def get_index_digit(h, res): """ Get the index digit of a cell at the given resolution. Parameters ---------- h : H3Cell Cell whose index digit will be returned. res : int Resolution (``>= 1``) at which to read the digit. Returns ------- int The index digit at the requested resolution. Examples -------- >>> h = construct_cell(7, 2, 1, 4) >>> h '830e8cfffffffff' >>> get_index_digit(h, 1) 2 >>> get_index_digit(h, 2) 1 >>> get_index_digit(h, 3) 4 """ return _cy.get_index_digit(_in_scalar(h), res) def construct_cell(base_cell_number, *digits, res=None): """ Construct cell from base cell and digits. Parameters ---------- base_cell_number : int Base cell *number* (``0`` to ``121``). *digits : int Sequence of index digits (``0`` to ``6``). Length of digits will be the resulting resolution of the output cell. res : int, optional Resolution of the constructed cell. If provided, it must equal ``len(digits)``; otherwise it is inferred from the number of digits. Returns ------- H3Cell The constructed cell. Examples -------- >>> construct_cell(7, 2, 1, 4) # resolution 3 cell '830e8cfffffffff' >>> construct_cell(15, 0, 0, 5, 3) # resolution 4 cell '841e057ffffffff' >>> construct_cell(15, 0, 0, 5, 3, res=4) '841e057ffffffff' """ if (res is not None) and (len(digits) != res): raise ValueError('Resolution must match number of digits.') digits = array('i', digits) o = _cy.construct_cell(base_cell_number, digits) return _out_scalar(o) def deconstruct_cell(h): """ Deconstruct cell into base cell and digits. Parameters ---------- h : H3Cell Cell to deconstruct. Returns ------- list of int [base_cell_number, digit1, digit2, ..., digitN] Examples -------- >>> h = construct_cell(7, 2, 1, 4) # resolution 3 cell >>> h '830e8cfffffffff' >>> deconstruct_cell(h) (7, 2, 1, 4) >>> h = construct_cell(15, 0, 0, 5, 3) # resolution 4 cell >>> h '841e057ffffffff' >>> deconstruct_cell(h) (15, 0, 0, 5, 3) >>> construct_cell(*deconstruct_cell(h), 0) == cell_to_center_child(h) """ res = get_resolution(h) bc = get_base_cell_number(h) digits = [get_index_digit(h, r + 1) for r in range(res)] return [bc, *digits] def are_neighbor_cells(h1, h2): """ Returns ``True`` if ``h1`` and ``h2`` are neighboring cells. Parameters ---------- h1 : H3Cell h2 : H3Cell Returns ------- bool """ h1 = _in_scalar(h1) h2 = _in_scalar(h2) return _cy.are_neighbor_cells(h1, h2) def cells_to_directed_edge(origin, destination): """ Create an H3 Index denoting a unidirectional edge. The edge is constructed from neighboring cells ``origin`` and ``destination``. Parameters ---------- origin : H3Cell destination : H3Cell Raises ------ ValueError When cells are not adjacent. Returns ------- H3Edge """ o = _in_scalar(origin) d = _in_scalar(destination) e = _cy.cells_to_directed_edge(o, d) e = _out_scalar(e) return e def get_directed_edge_origin(e): """ Origin cell from an H3 directed edge. Parameters ---------- e : H3Edge Returns ------- H3Cell """ e = _in_scalar(e) o = _cy.get_directed_edge_origin(e) o = _out_scalar(o) return o def get_directed_edge_destination(e): """ Destination cell from an H3 directed edge. Parameters ---------- e : H3Edge Returns ------- H3Cell """ e = _in_scalar(e) d = _cy.get_directed_edge_destination(e) d = _out_scalar(d) return d def directed_edge_to_cells(e): """ Return (origin, destination) tuple from H3 directed edge Parameters ---------- e : H3Edge Returns ------- H3Cell Origin cell of edge H3Cell Destination cell of edge """ e = _in_scalar(e) o, d = _cy.directed_edge_to_cells(e) o, d = _out_scalar(o), _out_scalar(d) return o, d def origin_to_directed_edges(origin): """ Return all directed edges starting from ``origin`` cell. Parameters ---------- origin : H3Cell Returns ------- unordered collection of H3Edge """ mv = _cy.origin_to_directed_edges(_in_scalar(origin)) return _out_collection(mv) def directed_edge_to_boundary(edge): """ Returns points representing the edge (line of points describing the boundary between two cells). Parameters ---------- edge : H3Edge Returns ------- tuple of (lat, lng) tuples """ return _cy.directed_edge_to_boundary(_in_scalar(edge)) def grid_path_cells(start, end): """ Returns the ordered collection of cells denoting a minimum-length non-unique path between cells. Parameters ---------- start : H3Cell end : H3Cell Returns ------- ordered collection of H3Cell Starting with ``start``, and ending with ``end``. """ mv = _cy.grid_path_cells(_in_scalar(start), _in_scalar(end)) return _out_collection(mv) def is_res_class_III(h): """ Determine if cell has orientation "Class II" or "Class III". The orientation of pentagons/hexagons on the icosahedron can be one of two types: "Class II" or "Class III". All cells within a resolution have the same type, and the type alternates between resolutions. "Class II" cells have resolutions: 0,2,4,6,8,10,12,14 "Class III" cells have resolutions: 1,3,5,7,9,11,13,15 Parameters ---------- h : H3Cell Returns ------- bool ``True`` if ``h`` is "Class III". ``False`` if ``h`` is "Class II". References ---------- 1. https://uber.github.io/h3/#/documentation/core-library/coordinate-systems """ return _cy.is_res_class_iii(_in_scalar(h)) def get_pentagons(res): """ Return all pentagons at a given resolution. Parameters ---------- res : int Resolution of the pentagons Returns ------- unordered collection of H3Cell """ mv = _cy.get_pentagons(res) return _out_collection(mv) def get_res0_cells(): """ Return all cells at resolution 0. Parameters ---------- None Returns ------- unordered collection of H3Cell Notes ----- There is currently no guaranteed order of the output cells. """ mv = _cy.get_res0_cells() return _out_collection(mv) def cell_to_center_child(h, res=None): """ Get the center child of a cell at some finer resolution. Parameters ---------- h : H3Cell res : int or None, optional The resolution for the child cell If ``None``, then ``res = resolution(h) + 1`` Returns ------- H3Cell """ h = _in_scalar(h) p = _cy.cell_to_center_child(h, res) p = _out_scalar(p) return p def get_icosahedron_faces(h): """ Return icosahedron faces intersecting a given H3 cell. There are twenty possible faces, ranging from 0--19. Note: Every interface returns a Python ``set`` of ``int``. Parameters ---------- h : H3Cell Returns ------- Python ``set`` of ``int`` """ h = _in_scalar(h) faces = _cy.get_icosahedron_faces(h) return faces def cell_to_local_ij(origin, h): """ Return local (i,j) coordinates of cell ``h`` in relation to ``origin`` cell Parameters ---------- origin : H3Cell Origin/central cell for defining i,j coordinates. h: H3Cell Destination cell whose i,j coordinates we'd like, based off of the origin cell. Returns ------- Tuple (i, j) of integer local coordinates of cell ``h`` Notes ----- The ``origin`` cell does not define (0, 0) for the IJ coordinate space. (0, 0) refers to the center of the base cell containing origin at the resolution of `origin`. Subtracting the IJ coordinates of ``origin`` from every cell would get you the property of (0, 0) being the ``origin``. This is done so we don't need to keep recomputing the coordinates of ``origin`` if not needed. """ origin = _in_scalar(origin) h = _in_scalar(h) i, j = _cy.cell_to_local_ij(origin, h) return i, j def local_ij_to_cell(origin, i, j): """ Return cell at local (i,j) position relative to the ``origin`` cell. Parameters ---------- origin : H3Cell Origin/central cell for defining i,j coordinates. i, j: int Integer coordinates with respect to ``origin`` cell. Returns ------- H3Cell at local (i,j) position relative to the ``origin`` cell Notes ----- The ``origin`` cell does not define (0, 0) for the IJ coordinate space. (0, 0) refers to the center of the base cell containing origin at the resolution of ``origin``. Subtracting the IJ coordinates of ``origin`` from every cell would get you the property of (0, 0) being the ``origin``. This is done so we don't need to keep recomputing the coordinates of ``origin`` if not needed. """ origin = _in_scalar(origin) h = _cy.local_ij_to_cell(origin, i, j) h = _out_scalar(h) return h def cell_area(h, unit='km^2'): """ Compute the spherical surface area of a specific H3 cell. Parameters ---------- h : H3Cell unit: str Unit for area result (``'km^2'``, ``'m^2'``, or ``'rads^2'``) Returns ------- The area of the H3 cell in the given units Notes ----- This function breaks the cell into spherical triangles, and computes their spherical area. The function uses the spherical distance calculation given by ``great_circle_distance()``. """ h = _in_scalar(h) return _cy.cell_area(h, unit=unit) def edge_length(e, unit='km'): """ Compute the spherical length of a specific H3 edge. Parameters ---------- h : H3Cell unit: str Unit for length result (``'km'``, ``'m'``, or ``'rads'``) Returns ------- The length of the edge in the given units Notes ----- This function uses the spherical distance calculation given by ``great_circle_distance()``. """ e = _in_scalar(e) return _cy.edge_length(e, unit=unit) def great_circle_distance(latlng1, latlng2, unit='km'): """ Compute the spherical distance between two (lat, lng) points. AKA: great circle distance or "haversine" distance. todo: overload to allow two cell inputs? Parameters ---------- latlng1 : tuple (lat, lng) tuple in degrees latlng2 : tuple (lat, lng) tuple in degrees unit: str Unit for distance result (``'km'``, ``'m'``, or ``'rads'``) Returns ------- The spherical distance between the points in the given units """ lat1, lng1 = latlng1 lat2, lng2 = latlng2 return _cy.great_circle_distance( lat1, lng1, lat2, lng2, unit = unit ) def cell_to_vertex(h, vertex_num): """ Return a (specified) vertex of an H3 cell. Parameters ---------- h : H3Cell vertex_num : int Vertex number (0-5) Returns ------- The vertex """ h = _in_scalar(h) h = _cy.cell_to_vertex(h, vertex_num) return _out_scalar(h) def cell_to_vertexes(h): """ Return a list of vertexes of an H3 cell. The list will be of length 5 for pentagons and 6 for hexagons. Parameters ---------- h : H3Cell Returns ------- A list of vertexes """ h = _in_scalar(h) mv = _cy.cell_to_vertexes(h) return _out_collection(mv) def vertex_to_latlng(v): """ Return latitude and longitude of a vertex. Returns ------- lat : float Latitude lng : float Longitude """ v = _in_scalar(v) return _cy.vertex_to_latlng(v) def is_valid_vertex(v): """ Validates an H3 vertex. Returns ------- bool """ try: v = _in_scalar(v) return _cy.is_valid_vertex(v) except (ValueError, TypeError): return False uber-h3-py-2883945/src/h3/api/basic_int/_convert.py000066400000000000000000000002731520643167100216220ustar00rootroot00000000000000from ... import _cy def _in_scalar(x): return x _out_scalar = _in_scalar def _in_collection(cells): it = list(cells) return _cy.iter_to_mv(it) _out_collection = list uber-h3-py-2883945/src/h3/api/basic_str/000077500000000000000000000000001520643167100174455ustar00rootroot00000000000000uber-h3-py-2883945/src/h3/api/basic_str/__init__.py000077700000000000000000000000001520643167100260302../basic_int/__init__.pyustar00rootroot00000000000000uber-h3-py-2883945/src/h3/api/basic_str/_convert.py000066400000000000000000000004001520643167100216300ustar00rootroot00000000000000from ... import _cy _in_scalar = _cy.str_to_int _out_scalar = _cy.int_to_str def _in_collection(cells): it = [_cy.str_to_int(h) for h in cells] return _cy.iter_to_mv(it) def _out_collection(mv): return list(_cy.int_to_str(h) for h in mv) uber-h3-py-2883945/src/h3/api/memview_int/000077500000000000000000000000001520643167100200175ustar00rootroot00000000000000uber-h3-py-2883945/src/h3/api/memview_int/__init__.py000077700000000000000000000000001520643167100264022../basic_int/__init__.pyustar00rootroot00000000000000uber-h3-py-2883945/src/h3/api/memview_int/_convert.py000066400000000000000000000001511520643167100222050ustar00rootroot00000000000000def _id(x): return x _in_scalar = _id _out_scalar = _id _in_collection = _id _out_collection = _id uber-h3-py-2883945/src/h3/api/numpy_int/000077500000000000000000000000001520643167100175165ustar00rootroot00000000000000uber-h3-py-2883945/src/h3/api/numpy_int/__init__.py000077700000000000000000000000001520643167100261012../basic_int/__init__.pyustar00rootroot00000000000000uber-h3-py-2883945/src/h3/api/numpy_int/_convert.py000066400000000000000000000004401520643167100217050ustar00rootroot00000000000000def _in_scalar(x): return x _out_scalar = _in_scalar def _in_collection(x): import numpy as np # array is copied only if dtype does not match # `list`s should work, but not `set`s of integers return np.asarray(x, dtype='uint64') _out_collection = _in_collection uber-h3-py-2883945/src/h3lib/000077500000000000000000000000001520643167100154125ustar00rootroot00000000000000uber-h3-py-2883945/tests/000077500000000000000000000000001520643167100147645ustar00rootroot00000000000000uber-h3-py-2883945/tests/readme.md000066400000000000000000000011471520643167100165460ustar00rootroot00000000000000We separate the `h3-py` tests into two groups, organized by subfolder. We do this because we expect 100% coverage on the library tests, but are still working through getting full coverage on the Cython tests. ## `test_lib` The tests in this folder are for the main `h3-py` Python API for folks who are using the pure-Python library functionality. ## `test_cython` The tests in this folder are for the advanced Cython API, that allows for other *Cython* packages or scripts to use the Cython code provided by the `h3-py` internals. Note that the "Cython API" of `h3-py` is not currently externally supported. uber-h3-py-2883945/tests/test_cython/000077500000000000000000000000001520643167100173275ustar00rootroot00000000000000uber-h3-py-2883945/tests/test_cython/__init__.py000066400000000000000000000000001520643167100214260ustar00rootroot00000000000000uber-h3-py-2883945/tests/test_cython/cython_example.pyx000066400000000000000000000006271520643167100231150ustar00rootroot00000000000000#cython: language_level=3 from cython cimport boundscheck, wraparound from h3._cy.h3lib cimport H3int from h3._cy.latlng cimport latlng_to_cell @boundscheck(False) @wraparound(False) cpdef void latlng_to_cell_vect( const double[:] lat, const double[:] lng, int res, H3int[:] out ): cdef Py_ssize_t i for i in range(len(lat)): out[i] = latlng_to_cell(lat[i], lng[i], res) uber-h3-py-2883945/tests/test_cython/test_cython.py000066400000000000000000000013651520643167100222510ustar00rootroot00000000000000import numpy as np # Avoid checking for import-error here because cython_example may not have # been compiled yet. try: from .cython_example import latlng_to_cell_vect # pylint: disable=import-error except ImportError: latlng_to_cell_vect = None np.random.seed(0) def test_cython_api(): if latlng_to_cell_vect is None: print('Not running Cython test because cython example was not compiled') return N = 100000 lats, lngs = np.random.uniform(0, 90, N), np.random.uniform(0, 90, N) res = 9 lats = np.array(lats, dtype=np.float64) lngs = np.array(lngs, dtype=np.float64) out = np.zeros(len(lats), dtype='uint64') latlng_to_cell_vect(lats, lngs, res, out) assert out[0] == 617284541015654399 uber-h3-py-2883945/tests/test_lib/000077500000000000000000000000001520643167100165715ustar00rootroot00000000000000uber-h3-py-2883945/tests/test_lib/__init__.py000066400000000000000000000000001520643167100206700ustar00rootroot00000000000000uber-h3-py-2883945/tests/test_lib/polyfill/000077500000000000000000000000001520643167100204235ustar00rootroot00000000000000uber-h3-py-2883945/tests/test_lib/polyfill/__init__.py000066400000000000000000000000001520643167100225220ustar00rootroot00000000000000uber-h3-py-2883945/tests/test_lib/polyfill/test_h3.py000066400000000000000000000276241520643167100223610ustar00rootroot00000000000000import h3 import pytest from .. import util as u class MockGeoInterface: def __init__(self, dictionary): assert isinstance(dictionary, dict) self.dictionary = dictionary @property def __geo_interface__(self): return self.dictionary def latlng_open(): return [ [37.813, -122.408], [37.707, -122.512], [37.815, -122.479], ] def latlng_closed(): loop = latlng_open() return loop + [loop[0]] def swap_latlng(ll1): ll1 = tuple( (b, a) for a, b in ll1 ) return ll1 def lnglat_open(): return swap_latlng(latlng_open()) def lnglat_closed(): return swap_latlng(latlng_closed()) def get_mocked(loop): geo = MockGeoInterface({ 'type': 'Polygon', 'coordinates': [loop] }) return geo sf_7x7 = [ (37.813319, -122.408987), (37.786630, -122.380544), (37.719806, -122.354474), (37.707613, -122.512344), (37.783587, -122.524719), (37.815157, -122.479877), ] sf_hole1 = [ (37.786980, -122.447120), (37.766410, -122.459078), (37.771068, -122.413710), ] sf_hole2 = [ (37.747976, -122.490025), (37.731550, -122.503758), (37.725440, -122.452603), ] def test_geo_interface(): poly = h3.LatLngPoly(sf_hole1) mpoly = h3.LatLngMultiPoly(poly) assert poly.__geo_interface__['type'] == 'Polygon' assert mpoly.__geo_interface__['type'] == 'MultiPolygon' assert ( poly.__geo_interface__['coordinates'] == mpoly.__geo_interface__['coordinates'][0] ) def test_shape_repr(): poly = h3.LatLngPoly(sf_hole1) mpoly = h3.LatLngMultiPoly(poly) assert ( '' == str(mpoly) == repr(mpoly) ) def test_polyfill(): poly = h3.LatLngPoly(sf_7x7) out = h3.h3shape_to_cells(poly, res=9) assert len(out) == 1253 assert '89283080527ffff' in out assert '89283095edbffff' in out def test_polygon_to_cells(): poly = h3.LatLngPoly(sf_7x7) # Note that `polygon_to_cells` is an alias for `h3shape_to_cells` out = h3.polygon_to_cells(poly, res=9) assert len(out) == 1253 assert '89283080527ffff' in out assert '89283095edbffff' in out def test_polygon_to_cells_experimental(): poly = h3.LatLngPoly(sf_7x7) # Note that `polygon_to_cells_experimental` is an alias for # `h3shape_to_cells_experimental` out = h3.polygon_to_cells_experimental(poly, res=9, contain='center') assert len(out) == 1253 assert '89283080527ffff' in out assert '89283095edbffff' in out def test_h3shape_to_cells_experimental(): poly = h3.LatLngPoly(sf_7x7) out = h3.h3shape_to_cells_experimental(poly, res=9, contain='center') assert len(out) == 1253 assert '89283080527ffff' in out assert '89283095edbffff' in out def test_h3shape_to_cells_experimental_full(): poly = h3.LatLngPoly(sf_7x7) out = h3.h3shape_to_cells_experimental(poly, res=9, contain='full') assert len(out) == 1175 assert '89283082a1bffff' in out assert '89283080527ffff' not in out assert '89283095edbffff' in out def test_h3shape_to_cells_experimental_overlapping(): poly = h3.LatLngPoly(sf_7x7) out = h3.h3shape_to_cells_experimental(poly, res=9, contain='overlap') assert len(out) == 1334 assert '89283080527ffff' in out assert '89283095edbffff' in out def test_h3shape_to_cells_experimental_overlapping_bbox(): poly = h3.LatLngPoly(sf_7x7) out = h3.h3shape_to_cells_experimental(poly, res=9, contain='bbox_overlap') assert len(out) == 1416 assert '89283080527ffff' in out assert '89283095edbffff' in out def test_h3shape_to_cells_experimental_invalid_mode(): poly = h3.LatLngPoly(sf_7x7) with pytest.raises(KeyError): h3.h3shape_to_cells_experimental( poly, res = 9, contain = 'contain_overlapping_bbox_abc', ) def test_poly_to_cells_experimental_mpoly(): mpoly = h3.LatLngMultiPoly( h3.LatLngPoly(sf_hole1), h3.LatLngPoly(sf_hole2), ) assert ( set(h3.h3shape_to_cells_experimental(mpoly, res=9)) == set(h3.h3shape_to_cells_experimental(mpoly, res=9, contain='center')) ) assert ( set(h3.h3shape_to_cells_experimental(mpoly, res=9)) < set(h3.h3shape_to_cells_experimental( mpoly, res = 9, contain = 'overlap' )) ) assert 120 == len(h3.h3shape_to_cells_experimental( mpoly, res = 9, contain = 'overlap' )) def test_polyfill_with_hole(): poly = h3.LatLngPoly(sf_7x7, sf_hole1) out = h3.h3shape_to_cells(poly, res=9) assert len(out) == 1214 foo = lambda x: set(h3.h3shape_to_cells(h3.LatLngPoly(x), 9)) assert u.same_set( out, foo(sf_7x7) - foo(sf_hole1) ) def test_polyfill_with_two_holes(): poly = h3.LatLngPoly(sf_7x7, sf_hole1, sf_hole2) out = h3.h3shape_to_cells(poly, 9) assert len(out) == 1172 foo = lambda x: set(h3.h3shape_to_cells(h3.LatLngPoly(x), 9)) assert u.same_set( out, foo(sf_7x7) - (foo(sf_hole1) | foo(sf_hole2)) ) def test_polyfill_geo_json_compliant(): geo = get_mocked(lnglat_open()).__geo_interface__ out = h3.geo_to_cells(geo, 9) assert len(out) > 300 def test_polyfill_geo_interface_compliant(): geo = get_mocked(lnglat_open()) out = h3.geo_to_cells(geo, 9) assert len(out) > 300 def test_poly_opens_loop(): loop = lnglat_closed() poly = h3.LatLngPoly(loop) assert loop[0] == loop[-1] assert len(poly.outer) == len(loop) - 1 def test_geo_to_h3shape(): shapes = [ h3.geo_to_h3shape(get_mocked(lnglat_open())), h3.geo_to_h3shape(get_mocked(lnglat_closed())), h3.LatLngPoly(latlng_open()), h3.LatLngPoly(latlng_closed()), ] for s in shapes: assert len(h3.h3shape_to_cells(s, 8)) == 48 expected = { 'type': 'Polygon', 'coordinates': (( (-122.408, 37.813), (-122.512, 37.707), (-122.479, 37.815), (-122.408, 37.813), ),) } for shape in shapes: assert shape.__geo_interface__ == expected mpolys = map(h3.LatLngMultiPoly, shapes) multi_expected = { 'type': 'MultiPolygon', 'coordinates': ((( (-122.408, 37.813), (-122.512, 37.707), (-122.479, 37.815), (-122.408, 37.813), ),),) } for mp in mpolys: assert mp.__geo_interface__ == multi_expected def test_geo_to_h3shape_passthrough(): poly = h3.LatLngPoly(latlng_open()) mpoly = h3.LatLngMultiPoly(poly) for shape in [poly, mpoly]: assert h3.geo_to_h3shape(shape) is shape def test_polyfill_down_under(): sydney = [ (-33.8556, 151.1979), (-33.8520, 151.2075), (-33.8580, 151.2247), (-33.8582, 151.2255), (-33.8564, 151.2353), (-33.8594, 151.2348), (-33.8641, 151.2335), (-33.8716, 151.2332), (-33.8877, 151.2240), (-33.8874, 151.2194), (-33.8870, 151.2189), (-33.8863, 151.2181), (-33.8851, 151.2158), (-33.8852, 151.2157), (-33.8851, 151.2141), (-33.8847, 151.2116), (-33.8835, 151.2083), (-33.8828, 151.2080), (-33.8816, 151.2059), (-33.8828, 151.2044), (-33.8839, 151.2028), (-33.8839, 151.2023), (-33.8842, 151.2011), (-33.8843, 151.1986), (-33.8842, 151.1986), (-33.8773, 151.1948), (-33.8741, 151.1923), (-33.8697, 151.1851), (-33.8625, 151.1903), (-33.8613, 151.1987), (-33.8556, 151.1979), ] poly = h3.LatLngPoly(sydney) out = h3.h3shape_to_cells(poly, 9) assert len(out) == 92 assert '89be0e34207ffff' in out assert '89be0e35ddbffff' in out def test_polyfill_far_east(): geo = [ (41.925781, 142.864838), (42.299659, 142.864838), (42.299659, 143.415527), (41.925781, 143.415527), (41.925781, 142.864838), ] poly = h3.LatLngPoly(geo) out = h3.h3shape_to_cells(poly, 9) assert len(out) == 18507 assert '892e18d16c3ffff' in out assert '892e1ebb5a7ffff' in out def test_polyfill_southern_tip(): geo = [ (-55.416544, -67.642822), (-54.354956, -67.642822), (-54.354956, -64.742432), (-55.416544, -64.742432), (-55.416544, -67.642822), ] poly = h3.LatLngPoly(geo) out = h3.h3shape_to_cells(poly, 9) assert len(out) == 223247 assert '89df4000003ffff' in out assert '89df4636b27ffff' in out def test_polyfill_null_island(): geo = [ (-3, -3), (+3, -3), (+3, +3), (-3, +3), (-3, -3), ] poly = h3.LatLngPoly(geo) out = h3.h3shape_to_cells(poly, 4) assert len(out) == 345 assert '847421bffffffff' in out assert '84825ddffffffff' in out def test_cells_to_h3shape_empty(): mpoly = h3.cells_to_h3shape([]) assert list(mpoly) == [] def test_cells_to_h3shape_single(): h = '89283082837ffff' cells = {h} mpoly = h3.cells_to_h3shape(cells, tight=False) assert len(mpoly) == 1 poly = mpoly[0] vertices = h3.cell_to_boundary(h) expected_poly = h3.LatLngPoly(vertices) assert set(poly.outer) == set(expected_poly.outer) assert poly.holes == expected_poly.holes == () def test_cells_to_h3shape_contiguous(): a = '89283082837ffff' b = '89283082833ffff' assert h3.are_neighbor_cells(a, b) mpoly = h3.cells_to_h3shape([a, b], tight=False) assert len(mpoly) == 1 poly = mpoly[0] assert len(poly.outer) == 10 assert poly.holes == () verts_a = h3.cell_to_boundary(a) verts_b = h3.cell_to_boundary(b) assert set(poly.outer) == set(verts_a) | set(verts_b) def test_cells_to_h3shape_non_contiguous(): a = '89283082837ffff' b = '8928308280fffff' assert not h3.are_neighbor_cells(a, b) mpoly = h3.cells_to_h3shape([a, b]) assert len(mpoly) == 2 assert all(poly.holes == () for poly in mpoly) assert all(len(poly.outer) == 6 for poly in mpoly) verts_a = h3.cell_to_boundary(a) verts_b = h3.cell_to_boundary(b) verts_both = set.union(*[set(poly.outer) for poly in mpoly]) assert verts_both == set(verts_a) | set(verts_b) def test_cells_to_h3shape_hole(): # Six hexagons in a ring around a hole cells = [ '892830828c7ffff', '892830828d7ffff', '8928308289bffff', '89283082813ffff', '8928308288fffff', '89283082883ffff', ] mpoly = h3.cells_to_h3shape(cells, tight=False) assert len(mpoly) == 1 poly = mpoly[0] assert len(poly.holes) == 1 assert len(poly.holes[0]) == 6 assert len(poly.outer) == 6 * 3 def test_cells_to_h3shape_2grid_disk(): h = '8930062838bffff' cells = h3.grid_disk(h, 2) mpoly = h3.cells_to_h3shape(cells, tight=False) assert len(mpoly) == 1 poly = mpoly[0] assert len(poly.holes) == 0 assert len(poly.outer) == 6 * (2 * 2 + 1) def test_multipoly_checks(): with pytest.raises(ValueError): h3.LatLngMultiPoly('foo') with pytest.raises(ValueError): h3.LatLngMultiPoly(1) with pytest.raises(ValueError): h3.LatLngMultiPoly([[(1, 2), (3, 4)]]) def test_3d_geo(): loop = lnglat_open() loop2d = [(lng, lat) for lng, lat in loop] loop3d = [(lng, lat, 0.0) for lng, lat in loop] geo2d = get_mocked(loop2d) geo3d = get_mocked(loop3d) shape2 = h3.geo_to_h3shape(geo2d) shape3 = h3.geo_to_h3shape(geo3d) out2 = h3.h3shape_to_cells(shape2, 9) out3 = h3.h3shape_to_cells(shape3, 9) assert len(out2) == 344 assert set(out2) == set(out3) def test_against_3d_polygons(): # LatLngPoly still expects just a lat/lng. it won't handle 3d coords loop3d = [ (lat, lng, 0.0) for lat, lng in latlng_open() ] with pytest.raises(ValueError): h3.LatLngPoly(loop3d) uber-h3-py-2883945/tests/test_lib/polyfill/test_polyfill.py000066400000000000000000000076131520643167100236750ustar00rootroot00000000000000import h3 import pytest from h3 import H3ResDomainError from .. import util as u def get_us_box_coords(): # big center chunk of the US in lat/lng order outer = [ [42.68, -110.61], [32.17, -109.02], [31.57, -94.26], [42.94, -89.38], [42.68, -110.61] ] hole1 = [ [39.77, -105.07], [34.81, -104.72], [34.77, -98.39], [40.14, -96.72], [39.77, -105.07] ] hole2 = [ [41.37, -98.61], [40.04, -91.80], [42.32, -91.80], [41.37, -98.61] ] return outer, hole1, hole2 def test_h3shape_to_cells(): # approximate lat/lngs for State of Maine maine = [ (45.137, -67.137), (44.810, -66.965), (44.325, -68.033), (43.980, -69.060), (43.684, -70.116), (43.090, -70.646), (43.080, -70.751), (43.220, -70.798), (43.368, -70.982), (43.466, -70.944), (45.305, -71.085), (45.460, -70.660), (45.915, -70.305), (46.693, -70.000), (47.448, -69.237), (47.185, -68.905), (47.355, -68.234), (47.066, -67.790), (45.703, -67.791), (45.137, -67.137), ] # a very rough hexagonal approximation to the State of Maine expected = { '832b13fffffffff', '832b18fffffffff', '832b1afffffffff', '832b1efffffffff', '832ba9fffffffff', '832badfffffffff' } poly = h3.LatLngPoly(maine) out = h3.h3shape_to_cells(poly, 3) assert u.same_set(out, expected) def test_h3shape_to_cells2(): lnglat, _, _ = get_us_box_coords() poly = h3.LatLngPoly(lnglat) out = h3.h3shape_to_cells(poly, 5) assert len(out) == 7063 # todo: we can generate segfaults with malformed input data to polyfill # need to test for this and avoid segfault # def test_polyfill_segfault(): # pass def test_h3shape_to_cells_holes(): outer, hole1, hole2 = get_us_box_coords() assert 7063 == len( h3.h3shape_to_cells(h3.LatLngPoly(outer), 5) ) for res in 1, 2, 3, 4, 5: cells_all = h3.h3shape_to_cells(h3.LatLngPoly(outer), res) poly = h3.LatLngPoly(outer, hole1, hole2) cells_holes = set(h3.h3shape_to_cells(poly, res=res)) cells_1 = set(h3.h3shape_to_cells(h3.LatLngPoly(hole1), res)) cells_2 = set(h3.h3shape_to_cells(h3.LatLngPoly(hole2), res)) assert len(cells_all) == len(cells_holes) + len(cells_1) + len(cells_2) assert u.same_set( cells_all, set.union(cells_holes, cells_1, cells_2) ) def test_resolution(): poly = h3.LatLngPoly([]) assert h3.h3shape_to_cells(poly, 0) == [] assert h3.h3shape_to_cells(poly, 15) == [] with pytest.raises(H3ResDomainError): h3.h3shape_to_cells(poly, -1) with pytest.raises(H3ResDomainError): h3.h3shape_to_cells(poly, 16) def test_invalid_polygon(): """ We were previously seeing segfaults on data like this because we weren't raising errors inside some `cdef` functions. """ with pytest.raises(TypeError): h3.LatLngPoly([1, 2, 3]) with pytest.raises(ValueError): h3.LatLngPoly([[1, 2, 3]]) def test_bad_geo_input(): with pytest.raises(ValueError): h3.h3shape_to_cells('not a shape', 9) with pytest.raises(ValueError): h3.h3shape_to_cells_experimental('not a shape', res=9) with pytest.raises(ValueError): h3.geo_to_cells({'type': 'not a shape', 'coordinates': None}, 9) def test_cells_to_geo(): h = '89754e64993ffff' res = h3.get_resolution(h) geo = h3.cells_to_geo([h], tight=False) coord = geo['coordinates'] assert geo['type'] == 'MultiPolygon' # todo: TBD assert len(coord) == 1 coord = coord[0] assert len(coord[0]) == 7 assert coord[0][0] == coord[0][-1] assert h3.geo_to_cells(geo, res) == [h] uber-h3-py-2883945/tests/test_lib/polyfill/test_polyfill_ordering.py000066400000000000000000000040101520643167100255520ustar00rootroot00000000000000""" Test that `h3shape_to_cells` can take in polygon inputs where the LinearRings may or may not follow the right hand rule, and they may or may not be closed loops (where the last element is equal to the first). Test all permutations of these rules on polygons with 0, 1, and 2 holes. Ensure that for any polygon, each LinearRing may follow a different subset of rules. """ import h3 import itertools def reverse(loop): return list(reversed(loop)) def drop_last(loop): return loop[:-1] def toggle_map(func, poly): """ Return all permutations of `func` being applied or not to each element of `poly` returns iterable of length 2**len(poly) """ mapped = (list(func(loop)) for loop in poly) return itertools.product(*zip(poly, mapped)) def chain_toggle_map(func, seq): seq = (toggle_map(func, p) for p in seq) seq = itertools.chain(*seq) return seq def input_permutations(geo, res=5): g = [geo] g = chain_toggle_map(drop_last, g) g = chain_toggle_map(reverse, g) for p in g: poly = h3.LatLngPoly(*p) cells = h3.h3shape_to_cells(poly, res=res) yield cells def get_us_box_coords(): # big center chunk of the US in lat/lng order outer = [ [42.68, -110.61], [32.17, -109.02], [31.57, -94.26], [42.94, -89.38], [42.68, -110.61] ] hole1 = [ [39.77, -105.07], [34.81, -104.72], [34.77, -98.39], [40.14, -96.72], [39.77, -105.07] ] hole2 = [ [41.37, -98.61], [40.04, -91.80], [42.32, -91.80], [41.37, -98.61] ] return outer, hole1, hole2 def test_input_format(): geo = get_us_box_coords() assert len(geo) == 3 # two holes for cells in input_permutations(geo[:3]): assert len(cells) == 5437 # one hole for cells in input_permutations(geo[:2]): assert len(cells) == 5726 # zero holes for cells in input_permutations(geo[:1]): assert len(cells) == 7063 uber-h3-py-2883945/tests/test_lib/polyfill/test_polygon_class.py000066400000000000000000000021371520643167100247130ustar00rootroot00000000000000import h3 import pytest def test_repr(): a = '8928308280fffff' b = h3.grid_ring(a, 5).pop() cells1 = h3.grid_ring(b, 2) + [a] cells2 = cells1 + [b] mpoly1 = h3.cells_to_h3shape(cells1) mpoly2 = h3.cells_to_h3shape(cells2) # unfortunately output order is nondeterministic mpoly1 = sorted(map(str, mpoly1)) mpoly2 = sorted(map(str, mpoly2)) assert mpoly1 == [ '', '', ] assert mpoly2 == [ '', '', '', ] def test_LatLngPoly_len(): cells = {'8928308280fffff'} poly = h3.cells_to_h3shape(cells, tight=True) with pytest.raises(NotImplementedError): len(poly) def test_bad_subclass(): class H3Shoop(h3.H3Shape): def __geo_interface__(self): return 'foo' shoop = H3Shoop() shoop.__geo_interface__() with pytest.raises(ValueError): h3.h3shape_to_cells(shoop, res=9) with pytest.raises(ValueError): h3.h3shape_to_cells_experimental(shoop, res=9) uber-h3-py-2883945/tests/test_lib/polyfill/test_to_multipoly.py000066400000000000000000000337071520643167100246060ustar00rootroot00000000000000import pytest import h3 from h3._cy.error_system import H3DuplicateInputError, H3ResMismatchError, H3FailedError from .. import util as u def assert_cells_roundtrip(cells): """Convert cells to multi-polygon and back, checking we recover the original set.""" res = h3.get_resolution(cells[0]) mpoly = h3.cells_to_h3shape(cells, tight=False) recovered = h3.h3shape_to_cells(mpoly, res) assert u.same_set(recovered, cells) def test_cells_to_h3shape(): h = '8928308280fffff' cells = h3.grid_disk(h, 1) mpoly = h3.cells_to_h3shape(cells, tight=False) poly = mpoly[0] poly2 = h3.LatLngPoly(poly.outer, *poly.holes) out = h3.h3shape_to_cells(poly2, 9) assert u.same_set(out, cells) def test_cells_to_h3shape_tight(): h = '8928308280fffff' cells = h3.grid_disk(h, 1) poly = h3.cells_to_h3shape(cells, tight=True) poly2 = h3.LatLngPoly(poly.outer, *poly.holes) out = h3.h3shape_to_cells(poly2, 9) assert u.same_set(out, cells) def test_2_polys(): h = '8928308280fffff' cells = h3.grid_ring(h, 2) cells = cells + [h] # cells should be a center hex, and the 2-ring around it # (with the 1-ring being absent) mpoly = h3.cells_to_h3shape(cells) out = [ set(h3.h3shape_to_cells(poly, 9)) for poly in mpoly ] assert u.same_set( set.union(*out), cells ) assert set(map(len, out)) == {1, 12} def test_empty(): mpoly = h3.cells_to_h3shape([], tight=False) assert isinstance(mpoly, h3.LatLngMultiPoly) assert len(mpoly) == 0 mpoly = h3.cells_to_h3shape([], tight=True) assert isinstance(mpoly, h3.LatLngMultiPoly) assert len(mpoly) == 0 def test_all_res0_cells(): cells = h3.get_res0_cells() mpoly = h3.cells_to_h3shape(cells, tight=False) # We should get an "entire globe" multipolygon back # The current implementation returns 8 triangles assert len(mpoly) == 8 for poly in mpoly: assert len(poly.outer) == 3 assert len(poly.holes) == 0 # Roundtrip fails: h3shape_to_cells recovers only 69/122 cells. # TODO: assert_cells_roundtrip when h3shape_to_cells handles global polygons. cells2 = h3.h3shape_to_cells(mpoly, 0) assert len(cells2) == 69 def test_pentagons(): for res in range(16): cells = h3.get_pentagons(res) mpoly = h3.cells_to_h3shape(cells, tight=False) if res % 2 == 0: expected_verts = 5 else: # pentagons have distortion vertices at odd resolutions expected_verts = 10 assert len(mpoly) == 12 for poly in mpoly: assert len(poly.holes) == 0 assert len(poly.outer) == expected_verts assert_cells_roundtrip(cells) def test_duplicate_cells_error(): cell = '8928308280fffff' with pytest.raises(H3DuplicateInputError): h3.cells_to_h3shape([cell, cell]) def test_mixed_resolution_error(): with pytest.raises(H3ResMismatchError): h3.cells_to_h3shape(['8027fffffffffff', '81efbffffffffff']) def test_three_polygons(): cells = [ '8027fffffffffff', '802bfffffffffff', '804dfffffffffff', '8067fffffffffff', '806dfffffffffff', '8049fffffffffff', '805ffffffffffff', '8057fffffffffff', '807dfffffffffff', '80a5fffffffffff', '80a9fffffffffff', '808bfffffffffff', '801bfffffffffff', '8035fffffffffff', '803ffffffffffff', '8053fffffffffff', '8043fffffffffff', '8021fffffffffff', '8011fffffffffff', '801ffffffffffff', '8097fffffffffff', ] mpoly = h3.cells_to_h3shape(cells, tight=False) assert len(mpoly) == 3 hole_counts = [len(p.holes) for p in mpoly] # Polygons are ordered by size, and we know the hole counts of each assert hole_counts == [3, 1, 0] assert_cells_roundtrip(cells) def test_issue_1049(): cells = [ '827487fffffffff', '82748ffffffffff', '827497fffffffff', '82749ffffffffff', '8274affffffffff', '8274c7fffffffff', '8274cffffffffff', '8274d7fffffffff', '8274e7fffffffff', '8274effffffffff', '8274f7fffffffff', '82754ffffffffff', '827c07fffffffff', '827c27fffffffff', '827c2ffffffffff', '827c37fffffffff', '827d87fffffffff', '827d8ffffffffff', '827d97fffffffff', '827d9ffffffffff', '827da7fffffffff', '827daffffffffff', '82801ffffffffff', '8280a7fffffffff', '8280affffffffff', '8280b7fffffffff', '828197fffffffff', '82819ffffffffff', '8281a7fffffffff', '8281b7fffffffff', '828207fffffffff', '82820ffffffffff', '828227fffffffff', '82822ffffffffff', '8282e7fffffffff', '828307fffffffff', '82830ffffffffff', '82831ffffffffff', '82832ffffffffff', '828347fffffffff', '82834ffffffffff', '828357fffffffff', '82835ffffffffff', '828367fffffffff', '828377fffffffff', '82a447fffffffff', '82a457fffffffff', '82a45ffffffffff', '82a467fffffffff', '82a46ffffffffff', '82a477fffffffff', '82a4c7fffffffff', '82a4cffffffffff', '82a4d7fffffffff', '82a4e7fffffffff', '82a4effffffffff', '82a4f7fffffffff', '82a547fffffffff', '82a54ffffffffff', '82a557fffffffff', '82a55ffffffffff', '82a567fffffffff', '82a577fffffffff', '82a837fffffffff', '82a897fffffffff', '82a8a7fffffffff', '82a8b7fffffffff', '82a917fffffffff', '82a927fffffffff', '82a937fffffffff', '82a987fffffffff', '82a98ffffffffff', '82a997fffffffff', '82a99ffffffffff', '82a9a7fffffffff', '82a9affffffffff', '82ac47fffffffff', '82ac57fffffffff', '82ac5ffffffffff', '82ac67fffffffff', '82ac6ffffffffff', '82ac77fffffffff', '82ad47fffffffff', '82ad4ffffffffff', '82ad57fffffffff', '82ad5ffffffffff', '82ad67fffffffff', '82ad77fffffffff', '82c207fffffffff', '82c217fffffffff', '82c227fffffffff', '82c237fffffffff', '82c287fffffffff', '82c28ffffffffff', '82c29ffffffffff', '82c2a7fffffffff', '82c2affffffffff', '82c2b7fffffffff', '82c307fffffffff', '82c317fffffffff', '82c31ffffffffff', '82c337fffffffff', '82cfb7fffffffff', '82d0c7fffffffff', '82d0d7fffffffff', '82d0dffffffffff', '82d0e7fffffffff', '82d0f7fffffffff', '82d147fffffffff', '82d157fffffffff', '82d15ffffffffff', '82d167fffffffff', '82d177fffffffff', '82d187fffffffff', '82d18ffffffffff', '82d197fffffffff', '82d19ffffffffff', '82d1a7fffffffff', '82d1affffffffff', '82dc47fffffffff', '82dc57fffffffff', '82dc5ffffffffff', '82dc67fffffffff', '82dc6ffffffffff', '82dc77fffffffff', '82dcc7fffffffff', '82dccffffffffff', '82dcd7fffffffff', '82dce7fffffffff', '82dceffffffffff', '82dcf7fffffffff', '82dd1ffffffffff', '82dd47fffffffff', '82dd4ffffffffff', '82dd57fffffffff', '82dd5ffffffffff', '82dd6ffffffffff', '82dd87fffffffff', '82dd8ffffffffff', '82dd97fffffffff', '82dd9ffffffffff', '82ddaffffffffff', '82ddb7fffffffff', '82dec7fffffffff', '82decffffffffff', '82ded7fffffffff', '82dee7fffffffff', '82deeffffffffff', '82def7fffffffff', '82df0ffffffffff', '82df1ffffffffff', '82df47fffffffff', '82df4ffffffffff', '82df57fffffffff', '82df5ffffffffff', '82df77fffffffff', '82df8ffffffffff', '82df9ffffffffff', '82e6c7fffffffff', '82e6cffffffffff', '82e6d7fffffffff', '82e6dffffffffff', '82e6effffffffff', '82e6f7fffffffff', ] mpoly = h3.cells_to_h3shape(cells, tight=False) assert len(mpoly) == 12 for poly in mpoly: assert len(poly.holes) == 0 assert_cells_roundtrip(cells) def test_equator_cells(): """Continuous band of cells overlapping the equator. """ cells = [ '81807ffffffffff', '817efffffffffff', '81723ffffffffff', '817ebffffffffff', '817c3ffffffffff', '817e3ffffffffff', '817a3ffffffffff', '8166fffffffffff', '8172bffffffffff', '816afffffffffff', '81933ffffffffff', '8168fffffffffff', '8188fffffffffff', '81853ffffffffff', '817f7ffffffffff', '8180bffffffffff', '81783ffffffffff', '81743ffffffffff', '8170bffffffffff', '8173bffffffffff', '8179bffffffffff', '817cbffffffffff', '8188bffffffffff', '81857ffffffffff', '816f7ffffffffff', '8177bffffffffff', '81617ffffffffff', '816f3ffffffffff', '8174bffffffffff', '8180fffffffffff', '817a7ffffffffff', '81767ffffffffff', '81757ffffffffff', '81957ffffffffff', '81787ffffffffff', '81847ffffffffff', '81653ffffffffff', '817bbffffffffff', '816cfffffffffff', '816abffffffffff', '815f3ffffffffff', '817c7ffffffffff', '8168bffffffffff', '818cbffffffffff', '818cfffffffffff', '818afffffffffff', '8174fffffffffff', '8172fffffffffff', '8170fffffffffff', '816fbffffffffff', '81657ffffffffff', '816c7ffffffffff', '8186bffffffffff', '81763ffffffffff', '818a7ffffffffff', '8186fffffffffff', '81707ffffffffff', '8182bffffffffff', '818f3ffffffffff', '8182fffffffffff', ] mpoly = h3.cells_to_h3shape(cells, tight=False) assert len(mpoly) == 1 poly = mpoly[0] assert len(poly.holes) == 1 # Roundtrip fails: h3shape_to_cells recovers only 18/60 cells. # TODO: assert_cells_roundtrip when h3shape_to_cells handles global polygons. cells2 = h3.h3shape_to_cells(mpoly, 1) assert len(cells2) == 18 def test_prime_meridian(): """Continuous band of cells overlapping the prime meridian. """ cells = [ '81efbffffffffff', '81c07ffffffffff', '81d1bffffffffff', '81097ffffffffff', '8109bffffffffff', '81d0bffffffffff', '81987ffffffffff', '81017ffffffffff', '81e67ffffffffff', '81ddbffffffffff', '81ac7ffffffffff', '8158bffffffffff', '81397ffffffffff', '81593ffffffffff', '81c17ffffffffff', '81827ffffffffff', '81197ffffffffff', '81eebffffffffff', '81383ffffffffff', '81dcbffffffffff', '81757ffffffffff', '81093ffffffffff', '81073ffffffffff', '8159bffffffffff', '81f17ffffffffff', '81187ffffffffff', '81007ffffffffff', '81997ffffffffff', '81753ffffffffff', '81033ffffffffff', '81f2bffffffffff', '8138bffffffffff', ] mpoly = h3.cells_to_h3shape(cells, tight=False) assert len(mpoly) == 1 poly = mpoly[0] assert len(poly.holes) == 0 assert len(poly.outer) == 128 # Roundtrip fails: h3shape_to_cells raises on this global polygon. # TODO: assert_cells_roundtrip when h3shape_to_cells handles global polygons. with pytest.raises(H3FailedError): h3.h3shape_to_cells(mpoly, 1) def test_anti_meridian(): """Continuous band of cells overlapping the anti-meridian.""" cells = [ '817ebffffffffff', '8133bffffffffff', '81047ffffffffff', '81f3bffffffffff', '81dbbffffffffff', '8132bffffffffff', '810cbffffffffff', '81bb3ffffffffff', '81db3ffffffffff', '81bafffffffffff', '81177ffffffffff', '817fbffffffffff', '81ba3ffffffffff', '815abffffffffff', '815bbffffffffff', '81eafffffffffff', '81ed7ffffffffff', '81057ffffffffff', '819a7ffffffffff', '81eabffffffffff', '819b7ffffffffff', '81167ffffffffff', '81227ffffffffff', '8171bffffffffff', '81237ffffffffff', '810dbffffffffff', '81033ffffffffff', '81f2bffffffffff', '8147bffffffffff', '81f33ffffffffff', ] mpoly = h3.cells_to_h3shape(cells, tight=False) assert len(mpoly) == 1 poly = mpoly[0] assert len(poly.holes) == 0 assert len(poly.outer) == 117 # Roundtrip fails: h3shape_to_cells raises on this global polygon. # TODO: assert_cells_roundtrip when h3shape_to_cells handles global polygons. with pytest.raises(H3FailedError): h3.h3shape_to_cells(mpoly, 1) def test_issue_482_with_antimeridian(): """From https://github.com/uber/h3-py/issues/482 Test three versions: the full set, removal of antimeridian overlaps, and removal of one cell by the north pole also causing roundtrip issues. Roundtrip works after removal of all troublesome cells. """ cells0 = { '8001fffffffffff', '8005fffffffffff', '8009fffffffffff', '800bfffffffffff', '8011fffffffffff', '8015fffffffffff', '8017fffffffffff', '801ffffffffffff', '8021fffffffffff', '8025fffffffffff', '802dfffffffffff', '802ffffffffffff', '8031fffffffffff', '8033fffffffffff', '803dfffffffffff', '803ffffffffffff', '8041fffffffffff', '8043fffffffffff', '804bfffffffffff', '804ffffffffffff', '8053fffffffffff', '8059fffffffffff', '8061fffffffffff', '8063fffffffffff', '8065fffffffffff', '8069fffffffffff', '806bfffffffffff', '8073fffffffffff', '8077fffffffffff', } # remove cells that overlap the antimeridian cells1 = cells0 - {'8005fffffffffff', '8017fffffffffff', '8033fffffffffff'} # remove cell that is very close to north pole (maybe touches) cells2 = cells1 - {'8001fffffffffff'} # --- cells0: all 29 cells --- mpoly = h3.cells_to_h3shape(cells0, tight=False) assert len(mpoly) == 1 assert len(mpoly[0].holes) == 0 assert len(mpoly[0].outer) == 37 # Roundtrip fails: recovers ~23/29, and not even a subset of the original. # TODO: assert_cells_roundtrip when h3shape_to_cells handles global polygons. cells_out = h3.h3shape_to_cells(mpoly, 0) assert len(cells_out) < len(cells0) # exact count differs by OS assert not (set(cells_out) <= cells0) # --- cells1: remove antimeridian-crossing cells (26 remaining) --- mpoly = h3.cells_to_h3shape(cells1, tight=False) assert len(mpoly) == 1 assert len(mpoly[0].holes) == 0 assert len(mpoly[0].outer) == 37 # Roundtrip fails: recovers 18/26, and not even a subset of the original. # TODO: assert_cells_roundtrip when h3shape_to_cells handles global polygons. cells_out = h3.h3shape_to_cells(mpoly, 0) assert len(cells_out) == 18 assert not (set(cells_out) <= cells1) # --- cells2: also remove near-pole cell (25 remaining) --- mpoly = h3.cells_to_h3shape(cells2, tight=False) assert len(mpoly) == 1 assert len(mpoly[0].holes) == 0 assert len(mpoly[0].outer) == 37 # Roundtrip succeeds. assert_cells_roundtrip(list(cells2)) uber-h3-py-2883945/tests/test_lib/test_apis/000077500000000000000000000000001520643167100205645ustar00rootroot00000000000000uber-h3-py-2883945/tests/test_lib/test_apis/__init__.py000066400000000000000000000000001520643167100226630ustar00rootroot00000000000000uber-h3-py-2883945/tests/test_lib/test_apis/test_api_bindings_match.py000066400000000000000000000004151520643167100257770ustar00rootroot00000000000000import inspect def test_api_copy_match(): import h3 apis = [ h3.api.basic_int, h3.api.basic_str, h3.api.memview_int, h3.api.numpy_int, ] api_set = {inspect.getsource(api) for api in apis} assert len(api_set) == 1 uber-h3-py-2883945/tests/test_lib/test_apis/test_basic_int.py000066400000000000000000000013251520643167100241310ustar00rootroot00000000000000import h3.api.basic_int as h3 from .. import util as u def test_int_output(): lat = 37.7752702151959 lng = -122.418307270836 assert h3.latlng_to_cell(lat, lng, 9) == 617700169958293503 assert h3.latlng_to_cell(lat, lng, 9) == 0x8928308280fffff def test_grid_disk(): expected = [ 617700169957507071, 617700169957769215, 617700169958031359, 617700169958293503, 617700169961177087, 617700169964847103, 617700169965109247, ] out = h3.grid_disk(617700169958293503, 1) assert u.same_set(out, expected) def test_compact_cells(): h = 617700169958293503 cells = h3.cell_to_children(h) assert h3.compact_cells(cells) == [h] uber-h3-py-2883945/tests/test_lib/test_apis/test_basic_str.py000066400000000000000000000015411520643167100241470ustar00rootroot00000000000000import h3.api.basic_str as h3 from .. import util as u def test1(): lat, lng = 37.7752702151959, -122.418307270836 assert h3.latlng_to_cell(lat, lng, 9) == '8928308280fffff' def test5(): expected = [ '89283082873ffff', '89283082877ffff', '8928308283bffff', '89283082807ffff', '8928308280bffff', '8928308280fffff', '89283082803ffff' ] out = h3.grid_disk('8928308280fffff', 1) assert u.same_set(out, expected) def test_string_subtypes(): class my_str(str): pass expected = [ '89283082873ffff', '89283082877ffff', '8928308283bffff', '89283082807ffff', '8928308280bffff', '8928308280fffff', '89283082803ffff' ] out = h3.grid_disk(my_str('8928308280fffff'), 1) assert u.same_set(out, expected) uber-h3-py-2883945/tests/test_lib/test_apis/test_collection_inputs.py000066400000000000000000000066131520643167100257400ustar00rootroot00000000000000import numpy as np # only run this test suite if numpy is installed import pytest from h3.api import ( basic_int, numpy_int, memview_int, ) # todo: check when a copy is made, and when it isn't def test_set(): ints = { 619056821839331327, 619056821839593471, 619056821839855615, 619056821840117759, 619056821840379903, 619056821840642047, 619056821840904191, } h = 614553222213795839 assert basic_int.compact_cells(ints) == [h] with pytest.raises(TypeError): # numpy can't convert from a set numpy_int.compact_cells(ints) with pytest.raises(TypeError): # set isn't a memoryview memview_int.compact_cells(ints) def test_list(): ints = [ 619056821839331327, 619056821839593471, 619056821839855615, 619056821840117759, 619056821840379903, 619056821840642047, 619056821840904191, ] h = 614553222213795839 assert basic_int.compact_cells(ints) == [h] # numpy can convert from a list OK # (numpy knows to convert it to uint64) assert numpy_int.compact_cells(ints) == np.array([h], dtype='uint64') # little weird that numpy comparisons don't consider dtype assert numpy_int.compact_cells(ints) == np.array([h]) assert not numpy_int.compact_cells(ints).dtype == np.array([h]).dtype with pytest.raises(TypeError): # list isn't a memoryview memview_int.compact_cells(ints) def test_np_array(): ints = np.array([ 619056821839331327, 619056821839593471, 619056821839855615, 619056821840117759, 619056821840379903, 619056821840642047, 619056821840904191, ], dtype='uint64') h = 614553222213795839 assert basic_int.compact_cells(ints) == [h] assert numpy_int.compact_cells(ints) == np.array([h], dtype='uint64') assert numpy_int.compact_cells(ints).dtype == np.dtype('uint64') out = memview_int.compact_cells(ints) assert len(out) == 1 assert out[0] == h def test_list_to_array(): ints0 = [ 619056821839331327, 619056821839593471, 619056821839855615, 619056821840117759, 619056821840379903, 619056821840642047, 619056821840904191, ] ints = np.array(ints0) h = 614553222213795839 assert basic_int.compact_cells(ints) == [h] assert numpy_int.compact_cells(ints) == np.array([h], dtype='uint64') with pytest.raises(ValueError): # Without the explicit dtype given above, the array # assumes it has *signed* integers # The `memview_int` interface requires a dtype match # with uint64. memview_int.compact_cells(ints) def test_iterator(): def foo(): ints = iter([ 619056821839331327, 619056821839593471, 619056821839855615, 619056821840117759, 619056821840379903, 619056821840642047, 619056821840904191, ]) return ints h = 614553222213795839 ints = foo() assert basic_int.compact_cells(ints) == [h] ints = foo() with pytest.raises(TypeError): # numpy can't create an array from an iterator numpy_int.compact_cells(ints) ints = foo() with pytest.raises(TypeError): # requires a bytes-like input memview_int.compact_cells(ints) uber-h3-py-2883945/tests/test_lib/test_apis/test_ico_faces.py000066400000000000000000000014511520643167100241110ustar00rootroot00000000000000import h3 def test_icosahedron_faces(): """ get_icosahedron_faces is kind of a special case, because it always returns a list of integers no matter the API """ answers = { '804dfffffffffff': [2, 3, 7, 8, 12], '80c1fffffffffff': [13], '80bbfffffffffff': [15, 16], } interfaces = [ (h3.api.basic_str, lambda x: x), (h3.api.basic_int, h3.str_to_int), (h3.api.memview_int, h3.str_to_int), (h3.api.numpy_int, h3.str_to_int), ] for api, conv in interfaces: for h in answers: expected = answers[h] h = conv(h) # convert to int or str, depending on API out = api.get_icosahedron_faces(h) assert isinstance(out, list) assert set(out) == set(expected) uber-h3-py-2883945/tests/test_lib/test_apis/test_memview_int.py000066400000000000000000000011211520643167100245130ustar00rootroot00000000000000import h3.api.memview_int as h3 def test1(): lat, lng = 37.7752702151959, -122.418307270836 assert h3.latlng_to_cell(lat, lng, 9) == 617700169958293503 def test_line(): h1 = '8928308280fffff' h2 = '8928308287bffff' h1, h2 = h3.str_to_int(h1), h3.str_to_int(h2) out = h3.grid_path_cells(h1, h2) # todo: are we outputting `memoryviewslice`? should we just output a memoryview? assert 'memoryview' in str(type(out)) expected = [ 617700169958293503, 617700169964847103, 617700169965371391, ] assert list(out) == expected uber-h3-py-2883945/tests/test_lib/test_apis/test_numpy_int.py000066400000000000000000000022411520643167100242160ustar00rootroot00000000000000import h3.api.numpy_int as h3 import numpy as np # only run this test suite if numpy is installed from .. import util as u def test1(): lat, lng = ( 37.7752702151959, -122.418307270836, ) assert h3.latlng_to_cell(lat, lng, 9) == 617700169958293503 def test5(): expected = { 617700169957507071, 617700169957769215, 617700169958031359, 617700169958293503, 617700169961177087, 617700169964847103, 617700169965109247, } out = h3.grid_disk(617700169958293503, 1) assert isinstance(out, np.ndarray) assert u.same_set(out, expected) def test_compact_cells(): h = 617700169958293503 cells = h3.cell_to_children(h) assert isinstance(cells, np.ndarray) assert h3.compact_cells(cells) == [h] def test_numpy_str(): expected = [ 617700169957507071, 617700169957769215, 617700169958031359, 617700169958293503, 617700169961177087, 617700169964847103, 617700169965109247, ] cells = np.array([h3.int_to_str(h) for h in expected]) got = [h3.str_to_int(c) for c in cells] assert expected == got uber-h3-py-2883945/tests/test_lib/test_cells_and_edges.py000066400000000000000000000511221520643167100232760ustar00rootroot00000000000000import h3 import pytest from h3 import ( H3FailedError, H3ResDomainError, H3DomainError, H3ResMismatchError, H3CellInvalidError, H3NotNeighborsError, H3DigitDomainError, H3BaseCellDomainError, H3DeletedDigitError, H3IndexInvalidError, ) from . import util as u def test1(): lat, lng = 37.7752702151959, -122.418307270836 assert h3.latlng_to_cell(lat, lng, 9) == '8928308280fffff' def test2(): h = '8928308280fffff' expected = (37.77670234943567, -122.41845932318311) assert h3.cell_to_latlng(h) == pytest.approx(expected) def test3(): expected = ( (37.775197782893386, -122.41719971841658), (37.77688044840226, -122.41612835779264), (37.778385004930925, -122.4173879761762), (37.77820687262238, -122.41971895414807), (37.77652420699321, -122.42079024541877), (37.775019673792606, -122.4195306280734), ) out = h3.cell_to_boundary('8928308280fffff') assert u.approx2(out, expected) def test_grid_disk_distance(): with pytest.raises(H3DomainError): h3.grid_disk('8928308280fffff', -10) def test_grid_ring_distance(): with pytest.raises(H3DomainError): h3.grid_ring('8928308280fffff', -10) def test5(): expected = [ '89283082873ffff', '89283082877ffff', '8928308283bffff', '89283082807ffff', '8928308280bffff', '8928308280fffff', '89283082803ffff' ] out = h3.grid_disk('8928308280fffff', 1) assert u.same_set(out, expected) def test6(): expected = ['8928308280fffff'] out = h3.grid_ring('8928308280fffff', 0) assert u.same_set(out, expected) def test7(): expected = [ '89283082803ffff', '89283082807ffff', '8928308280bffff', '8928308283bffff', '89283082873ffff', '89283082877ffff' ] out = h3.grid_ring('8928308280fffff', 1) assert u.same_set(out, expected) def test8(): assert h3.is_valid_cell('89283082803ffff') assert not h3.is_valid_cell('abc') # looks like it might be valid, but it isn't! h_bad = '8a28308280fffff' assert not h3.is_valid_cell(h_bad) # other methods should validate and raise exception if bad input with pytest.raises(H3CellInvalidError): h3.get_resolution(h_bad) def test9(): assert h3.get_resolution('8928308280fffff') == 9 assert h3.get_resolution('8a28308280f7fff') == 10 def test_parent(): h = '8928308280fffff' assert h3.cell_to_parent(h, 7) == '872830828ffffff' assert h3.cell_to_parent(h, 8) == '8828308281fffff' assert h3.cell_to_parent(h, 9) == h with pytest.raises(H3ResMismatchError): h3.cell_to_parent(h, 10) def test_parent_err(): # Test 1 h = '8075fffffffffff' # latlng_to_cell(0,0,0) with pytest.raises(H3ResDomainError): h3.cell_to_parent(h) # Test 2 try: h3.cell_to_parent(h) except Exception as e: msg = str(e) # todo: revist this weird formatting stuff expected = 'Invalid parent resolution -1 for cell {}.' expected = expected.format(hex(h3.str_to_int(h))) assert msg == expected def test_children(): h = '8928308280fffff' with pytest.raises(H3ResDomainError): h3.cell_to_children(h, 8) # same resolution is set of just cell itself out = h3.cell_to_children(h, 9) assert out == [h] # one below should give children expected = [ '8a28308280c7fff', '8a28308280cffff', '8a28308280d7fff', '8a28308280dffff', '8a28308280e7fff', '8a28308280effff', '8a28308280f7fff' ] out = h3.cell_to_children(h, 10) assert u.same_set(out, expected) # finest resolution cell should return error for children h = '8f04ccb2c45e225' with pytest.raises(H3ResDomainError): h3.cell_to_children(h) def test_center_child(): h = '8928308280fffff' # one above should raise an exception with pytest.raises(H3ResDomainError): h3.cell_to_center_child(h, 8) # same resolution should be same cell assert h3.cell_to_center_child(h, 9) == h # one below should give direct child expected = '8a28308280c7fff' assert h3.cell_to_center_child(h, 10) == expected # finest resolution hex should return error for child h = '8f04ccb2c45e225' with pytest.raises(H3ResDomainError): h3.cell_to_center_child(h) def test_distance(): h = '8a28308280c7fff' assert h3.grid_distance(h, h) == 0 n = h3.grid_ring(h, 1).pop() assert h3.grid_distance(h, n) == 1 n = h3.grid_ring(h, 2).pop() assert h3.grid_distance(h, n) == 2 def test_distance_error(): """ Two valid cells, but they are too far apart compute the distance todo: make sure this raises a E_TOO_FAR error (when we add it in the future) """ h1 = '8353b0fffffffff' h2 = '835804fffffffff' with pytest.raises(H3FailedError): h3.grid_distance(h1, h2) def get_maine_cells(): # lat/lngs for State of Maine poly = h3.LatLngPoly([ (45.137451890638886, -67.13734351262877), (44.8097, -66.96466), (44.3252, -68.03252), (43.98, -69.06), (43.68405, -70.11617), (43.090083319667144, -70.64573401557249), (43.08003225358635, -70.75102474636725), (43.21973948828747, -70.79761105007827), (43.36789581966826, -70.98176001655037), (43.46633942318431, -70.94416541205806), (45.3052400000002, -71.08482), (45.46022288673396, -70.6600225491012), (45.914794623389355, -70.30495378282376), (46.69317088478567, -70.00014034695016), (47.44777598732787, -69.23708614772835), (47.184794623394396, -68.90478084987546), (47.35462921812177, -68.23430497910454), (47.066248887716995, -67.79035274928509), (45.702585354182816, -67.79141211614706), (45.137451890638886, -67.13734351262877) ]) res = 5 cells_uncomp = h3.h3shape_to_cells(poly, res=res) # the expected result from h3.compact_cells(cells_uncomp) cells_comp = ['852b114ffffffff', '852b189bfffffff', '852b1163fffffff', '842ba9bffffffff', '842bad3ffffffff', '852ba9cffffffff', '842badbffffffff', '852b1e8bfffffff', '852a346ffffffff', '842b1e3ffffffff', '852b116ffffffff', '842b185ffffffff', '852b1bdbfffffff', '852bad47fffffff', '852ba9c3fffffff', '852b106bfffffff', '852a30d3fffffff', '842b1edffffffff', '852b12a7fffffff', '852b1027fffffff', '842baddffffffff', '852a349bfffffff', '852b1227fffffff', '852a3473fffffff', '852b117bfffffff', '842ba99ffffffff', '852a341bfffffff', '852ba9d3fffffff', '852b1067fffffff', '852a3463fffffff', '852baca7fffffff', '852b116bfffffff', '852b1c6bfffffff', '852a3493fffffff', '852ba9dbfffffff', '852b180bfffffff', '842bad7ffffffff', '852b1063fffffff', '842ba93ffffffff', '852a3693fffffff', '852ba977fffffff', '852b1e9bfffffff', '852bad53fffffff', '852b100ffffffff', '852b102bfffffff', '852a3413fffffff', '852ba8b7fffffff', '852bad43fffffff', '852b1c6ffffffff', '852a340bfffffff', '852b103bfffffff', '852b1813fffffff', '852b12affffffff', '842a34dffffffff', '852b1873fffffff', '852b106ffffffff', '852b115bfffffff', '852baca3fffffff', '852b114bfffffff', '852b1143fffffff', '852a348bfffffff', '852a30d7fffffff', '852b181bfffffff', '842a345ffffffff', '852b1e8ffffffff', '852b1883fffffff', '852b1147fffffff', '852a3483fffffff', '852b12a3fffffff', '852a346bfffffff', '852ba9d7fffffff', '842b18dffffffff', '852b188bfffffff', '852a36a7fffffff', '852bacb3fffffff', '852b187bfffffff', '852bacb7fffffff', '842b1ebffffffff', '842b1e5ffffffff', '852ba8a7fffffff', '842bad9ffffffff', '852a36b7fffffff', '852a347bfffffff', '832b13fffffffff', '852ba9c7fffffff', '832b1afffffffff', '842ba91ffffffff', '852bad57fffffff', '852ba8affffffff', '852b1803fffffff', '842b1e7ffffffff', '852bad4ffffffff', '852b102ffffffff', '852b1077fffffff', '852b1237fffffff', '852b1153fffffff', '852a3697fffffff', '852a36b3fffffff', '842bad1ffffffff', '842b1e1ffffffff', '852b186bfffffff', '852b1023fffffff'] # noqa return cells_uncomp, cells_comp, res def test_compact_cells(): cells_uncomp, cells_comp, _ = get_maine_cells() out = h3.compact_cells(cells_uncomp) assert u.same_set(out, cells_comp) def test_uncompact_cells(): cells_uncomp, cells_comp, res = get_maine_cells() out = h3.uncompact_cells(cells_comp, res) assert u.same_set(out, cells_uncomp) def test_get_num_cells(): expected = { 0: 122, 1: 842, 2: 5882, 9: 4842432842, 15: 569707381193162, } out = { k: h3.get_num_cells(k) for k in expected } assert expected == out def test_average_hexagon_area(): expected_in_km2 = { 0: 4357449.416078381, 1: 609788.441794133, 2: 86801.780398997, 9: 0.105332513, 15: 8.95311e-07, } out = { k: h3.average_hexagon_area(k, unit='km^2') for k in expected_in_km2 } assert out == pytest.approx(expected_in_km2) def test_average_hexagon_edge_length(): expected_in_km = { 0: 1281.256011, 1: 483.0568391, 2: 182.5129565, 9: 0.200786148, 15: 0.000584169, } out = { res: h3.average_hexagon_edge_length(res, unit='km') for res in expected_in_km } assert out == pytest.approx(expected_in_km) def test_edge(): h1 = '8928308280fffff' h2 = '89283082873ffff' assert not h3.are_neighbor_cells(h1, h1) assert h3.are_neighbor_cells(h1, h2) e = h3.cells_to_directed_edge(h1, h2) assert e == '12928308280fffff' assert h3.is_valid_directed_edge(e) assert not h3.is_valid_cell(e) assert h3.get_directed_edge_origin(e) == h1 assert h3.get_directed_edge_destination(e) == h2 assert h3.directed_edge_to_cells(e) == (h1, h2) def test_origin_to_directed_edges(): h = '8928308280fffff' edges = h3.origin_to_directed_edges(h) destinations = { h3.get_directed_edge_destination(e) for e in edges } neighbors = h3.grid_ring(h, 1) assert u.same_set(neighbors, destinations) def test_edge_boundary(): h1 = '8928308280fffff' h2 = '89283082873ffff' e = h3.cells_to_directed_edge(h1, h2) expected = ( (37.77688044840226, -122.41612835779266), (37.778385004930925, -122.41738797617619) ) out = h3.directed_edge_to_boundary(e) assert out[0] == pytest.approx(expected[0]) assert out[1] == pytest.approx(expected[1]) def test_validation(): h = '8a28308280fffff' # invalid cell with pytest.raises(H3CellInvalidError): h3.get_base_cell_number(h) with pytest.raises(H3CellInvalidError): h3.get_resolution(h) with pytest.raises(H3CellInvalidError): h3.cell_to_parent(h, 9) with pytest.raises(H3CellInvalidError): h3.grid_distance(h, h) with pytest.raises(H3CellInvalidError): h3.grid_disk(h, 1) with pytest.raises(H3CellInvalidError): h3.grid_ring(h, 1) with pytest.raises(H3CellInvalidError): h3.cell_to_children(h, 11) with pytest.raises(H3CellInvalidError): h3.compact_cells({h}) with pytest.raises(H3CellInvalidError): h3.uncompact_cells({h}, 10) def test_validation2(): h = '8928308280fffff' with pytest.raises(H3ResDomainError): h3.cell_to_children(h, 17) assert not h3.are_neighbor_cells(h, h) def test_validation_geo(): h = '8a28308280fffff' # invalid cell with pytest.raises(H3CellInvalidError): h3.cell_to_latlng(h) with pytest.raises(H3ResDomainError): h3.latlng_to_cell(0, 0, 17) with pytest.raises(H3CellInvalidError): h3.cell_to_boundary(h) # note: this won't raise an exception on bad input, but it does # *correctly* say that two invalid indexes are not neighbors assert not h3.are_neighbor_cells(h, h) def test_edges(): h = '8928308280fffff' with pytest.raises(H3NotNeighborsError): h3.cells_to_directed_edge(h, h) h2 = h3.grid_ring(h, 2).pop() with pytest.raises(H3NotNeighborsError): h3.cells_to_directed_edge(h, h2) e_bad = '14928308280ffff1' assert not h3.is_valid_directed_edge(e_bad) # note: won't raise an error on bad input h3.get_directed_edge_origin(e_bad) h3.get_directed_edge_destination(e_bad) h3.directed_edge_to_cells(e_bad) def test_line(): h1 = '8928308280fffff' h2 = '8928308287bffff' out = h3.grid_path_cells(h1, h2) expected = [ '8928308280fffff', '89283082873ffff', '8928308287bffff' ] assert out == expected def test_versions(): from packaging.version import Version v = h3.versions() assert v['python'] == h3.__version__ v_c = Version(v['c']) v_p = Version(v['python']) # For all releases (including pre-release), major version must match assert v_c.release[0] == v_p.release[0] if not v_p.is_prerelease: # pragma: no cover # of X.Y.Z, X and Y must match assert v_c.release[:2] == v_p.release[:2] def test_str_int_convert(): s = '8928308280fffff' i = h3.str_to_int(s) assert h3.int_to_str(i) == s def test_str_to_int_fail(): h_invalid = {} assert not h3.is_valid_cell(h_invalid) def test_edge_is_valid_fail(): e_invalid = {} assert not h3.is_valid_directed_edge(e_invalid) def test_get_pentagons(): out = h3.get_pentagons(0) expected = [ '8009fffffffffff', '801dfffffffffff', '8031fffffffffff', '804dfffffffffff', '8063fffffffffff', '8075fffffffffff', '807ffffffffffff', '8091fffffffffff', '80a7fffffffffff', '80c3fffffffffff', '80d7fffffffffff', '80ebfffffffffff', ] assert u.same_set(out, expected) out = h3.get_pentagons(5) expected = [ '85080003fffffff', '851c0003fffffff', '85300003fffffff', '854c0003fffffff', '85620003fffffff', '85740003fffffff', '857e0003fffffff', '85900003fffffff', '85a60003fffffff', '85c20003fffffff', '85d60003fffffff', '85ea0003fffffff', ] assert u.same_set(out, expected) for i in range(16): assert len(h3.get_pentagons(i)) == 12 def test_uncompact_cell_input(): # `uncompact_cells` takes in a collection of cells, not a single cell. # Since a python string is seen as a Iterable collection, # inputting a single cell string can raise weird errors. # Ensure we get a reasonably helpful answer with pytest.raises(H3CellInvalidError): h3.uncompact_cells('8001fffffffffff', 1) def test_get_res0_cells(): out = h3.get_res0_cells() assert len(out) == 122 # subset pentagons = h3.get_pentagons(0) assert set(pentagons) < set(out) # all valid assert all(map(h3.is_valid_cell, out)) # resolution assert all(map( lambda h: h3.get_resolution(h) == 0, out )) # verify a few concrete cells sub = { '8001fffffffffff', '8003fffffffffff', '8005fffffffffff', } assert sub < set(out) def test_to_local_ij_error(): h = h3.latlng_to_cell(0, 0, 0) # error if we cross a face nb = h3.grid_ring(h, k=2) # todo: should this be the E_TOO_FAR guy? with pytest.raises(H3FailedError): [h3.cell_to_local_ij(h, p) for p in nb] # should be fine if we do not cross a face nb = h3.grid_ring(h, k=1) out = {h3.cell_to_local_ij(h, p) for p in nb} expected = {(-1, 0), (0, -1), (0, 1), (1, 0), (1, 1)} assert out == expected def test_from_local_ij_error(): h = h3.latlng_to_cell(0, 0, 0) baddies = [(1, -1), (-1, 1), (-1, -1)] for i, j in baddies: with pytest.raises(H3FailedError): h3.local_ij_to_cell(h, i, j) # inverting output should give good data nb = h3.grid_ring(h, k=1) goodies = {h3.cell_to_local_ij(h, p) for p in nb} out = { h3.local_ij_to_cell(h, i, j) for i, j in goodies } assert u.same_set(out, nb) def test_to_local_ij_self(): h = h3.latlng_to_cell(0, 0, 9) out = h3.cell_to_local_ij(h, h) assert out == (-858, -2766) def test_is_valid_index(): assert h3.is_valid_index('8001fffffffffff') assert not h3.is_valid_index('8a28308280fffff') assert not h3.is_valid_index(123) assert not h3.is_valid_index('abcd') def test_is_valid_index_2(): import h3.api.basic_int as h3 h = h3.latlng_to_cell(0,0,9) assert h3.is_valid_index(h) assert not h3.is_valid_index(h + 1) assert not h3.is_valid_directed_edge(h) assert not h3.is_valid_vertex(h) e = h3.origin_to_directed_edges(h)[0] assert h3.is_valid_index(e) assert not h3.is_valid_index(e + 1) assert not h3.is_valid_cell(e) assert not h3.is_valid_vertex(e) v = h3.cell_to_vertex(h, 0) assert h3.is_valid_index(v) assert not h3.is_valid_index(v + 1) assert not h3.is_valid_cell(v) assert not h3.is_valid_directed_edge(v) def test_get_index_digit(): assert h3.get_index_digit('822377fffffffff', 1) == 5 assert h3.get_index_digit('822377fffffffff', 2) == 6 assert h3.get_index_digit('822377fffffffff', 3) == 7 assert h3.get_index_digit('822377fffffffff', 15) == 7 with pytest.raises(H3ResDomainError): h3.get_index_digit('822377fffffffff', 16) with pytest.raises(H3ResDomainError): h3.get_index_digit('822377fffffffff', 0) with pytest.raises(H3ResDomainError): h3.get_index_digit('822377fffffffff',-1) def test_get_index_digit_non_cells(): h = h3.construct_cell(111, 3, 6, 4) assert h == '83def4fffffffff' e = h3.origin_to_directed_edges(h)[0] assert e == '113def4fffffffff' assert h3.is_valid_directed_edge(e) assert h3.get_index_digit(e, 1) == 3 assert h3.get_index_digit(e, 2) == 6 assert h3.get_index_digit(e, 3) == 4 e = h3.int_to_str(1 + h3.str_to_int(e)) assert not h3.is_valid_directed_edge(e) assert not h3.is_valid_index(e) with pytest.raises(H3IndexInvalidError): h3.get_index_digit(e, 1) v = h3.cell_to_vertexes(h)[0] assert v == '223de1afffffffff' assert h3.is_valid_vertex(v) assert h3.get_index_digit(v, 1) == 0 assert h3.get_index_digit(v, 2) == 3 assert h3.get_index_digit(v, 3) == 2 v = h3.int_to_str(1 + h3.str_to_int(v)) assert not h3.is_valid_vertex(v) assert not h3.is_valid_index(v) with pytest.raises(H3IndexInvalidError): h3.get_index_digit(v, 1) def test_construct_cell(): bc = 17 assert h3.construct_cell(bc, 5, 6) == '822377fffffffff' assert h3.construct_cell(bc) == '8023fffffffffff' digits = [0] * 15 assert h3.construct_cell(bc, *digits) == '8f2200000000000' with pytest.raises(H3ResDomainError): digits = [0] * 16 h3.construct_cell(bc, *digits) with pytest.raises(ValueError): digits = [1,2,3,4] h3.construct_cell(bc, *digits, res=2) with pytest.raises(H3DigitDomainError): h3.construct_cell(121, 1, 7) # 7 is not a valid digit with pytest.raises(H3DigitDomainError): h3.construct_cell(121, 45) # 45 is not a valid digit with pytest.raises(H3DigitDomainError): h3.construct_cell(121, -1) # -1 is not a valid digit with pytest.raises(H3BaseCellDomainError): h3.construct_cell(122) # one past last base cell number with pytest.raises(H3DeletedDigitError): # 4 is a pentagon base cell. first nonzero digit can't be a 1 h3.construct_cell(4, 1) with pytest.raises(H3DeletedDigitError): # 4 is a pentagon base cell. first nonzero digit can't be a 1 h3.construct_cell(4, 0,0,0, 0, 1) def test_deconstruct_cell(): h = '822377fffffffff' assert h3.deconstruct_cell(h) == [17, 5, 6] def test_construct_cell_inverses(): # demonstrate functions are inverses of each other h = '8ff3ac688d63446' components = [121, 6, 5, 4, 3, 2, 1, 0, 6, 5, 4, 3, 2, 1, 0, 6] assert h3.construct_cell(*components) == h assert h3.deconstruct_cell(h) == components assert h3.construct_cell(*h3.deconstruct_cell(h)) == h assert h3.deconstruct_cell(h3.construct_cell(*components)) == components def test_construct_cell_inverses_int_api(): # same inverse test as above, but in int api import h3.api.basic_int as h3 h = 0x8ff3ac688d63446 components = [121, 6, 5, 4, 3, 2, 1, 0, 6, 5, 4, 3, 2, 1, 0, 6] assert h3.construct_cell(*components) == h assert h3.deconstruct_cell(h) == components assert h3.construct_cell(*h3.deconstruct_cell(h)) == h assert h3.deconstruct_cell(h3.construct_cell(*components)) == components def test_center_child_with_deconstruct(): for h in h3.get_res0_cells(): h3.construct_cell(*h3.deconstruct_cell(h), 0) == h3.cell_to_center_child(h) uber-h3-py-2883945/tests/test_lib/test_error_codes.py000066400000000000000000000034451520643167100225160ustar00rootroot00000000000000import pytest import h3 from h3._cy import ( error_code_to_exception, get_H3_ERROR_END, ) # todo: maybe check the `check_for_error` function behavior directly? h3_exceptions = { # h3.UnknownH3ErrorCode h3.H3BaseException: None, h3.H3GridNavigationError: None, h3.H3MemoryError: None, h3.H3ValueError: None, } for e in range(1, get_H3_ERROR_END()): ex = error_code_to_exception(e) h3_exceptions[ex] = e def test_num_error_codes(): assert get_H3_ERROR_END() >= 20 assert error_code_to_exception(19) == h3.H3DeletedDigitError # H3_ERROR_END (and beyond) shouldn't be a valid error code code = get_H3_ERROR_END() assert isinstance( error_code_to_exception(code), h3.UnknownH3ErrorCode ) code = get_H3_ERROR_END() + 1 assert isinstance( error_code_to_exception(code), h3.UnknownH3ErrorCode ) def test_error_codes_match(): """ Should match the error codes given in `h3api.h.in` in the core C lib. """ for err, code in h3_exceptions.items(): assert err.h3_error_code == code def test_unknown(): weird_code = 1234 with pytest.raises(h3.UnknownH3ErrorCode) as excinfo: raise h3.UnknownH3ErrorCode(weird_code) err = excinfo.value assert isinstance(err, h3.UnknownH3ErrorCode) assert err.args == (weird_code,) def test_attributes(): # errors always have an `h3_error_code` attribute for err in h3_exceptions: x = err.h3_error_code assert (x is None) or (x > 0) # UnknownH3ErrorCode is a bit of a special case weird_code = 1234 with pytest.raises(h3.UnknownH3ErrorCode) as excinfo: raise h3.UnknownH3ErrorCode(weird_code) err = excinfo.value assert err.h3_error_code is None assert err.args == (weird_code,) uber-h3-py-2883945/tests/test_lib/test_h3.py000066400000000000000000000313141520643167100205160ustar00rootroot00000000000000import pytest from pytest import approx import h3 from . import util as u def test_approx_helper(): assert not u.approx2('abc', 'abdc') def test_is_valid_cell(): assert h3.is_valid_cell('85283473fffffff') assert h3.is_valid_cell('850dab63fffffff') assert not h3.is_valid_cell('lolwut') # H3 0.x Addresses are not considered valid assert not h3.is_valid_cell('5004295803a88') for res in range(16): assert h3.is_valid_cell(h3.latlng_to_cell(37, -122, res)) def test_latlng_to_cell(): assert h3.latlng_to_cell(37.3615593, -122.0553238, 5) == '85283473fffffff' def test_get_resolution(): for res in range(16): h = h3.latlng_to_cell(37.3615593, -122.0553238, res) assert h3.get_resolution(h) == res def test_silly_latlng_to_cell(): lat, lng = 37.3615593, -122.0553238 expected0 = '85283473fffffff' out0 = h3.latlng_to_cell(lat, lng, 5) assert out0 == expected0 out1 = h3.latlng_to_cell(lat + 180.0, lng + 360.0, 5) expected1 = '85ca2d53fffffff' assert out1 == expected1 def test_cell_to_latlng(): latlng = h3.cell_to_latlng('85283473fffffff') assert latlng == approx((37.34579337536848, -121.97637597255124)) def test_cell_to_boundary(): out = h3.cell_to_boundary('85283473fffffff') expected = [ [37.271355866731895, -121.91508032705622], [37.353926450852256, -121.86222328902491], [37.42834118609435, -121.9235499963016], [37.42012867767778, -122.0377349642703], [37.33755608435298, -122.09042892904395], [37.26319797461824, -122.02910130919], ] assert len(out) == len(expected) for o, e in zip(out, expected): assert o == approx(e) def test_grid_disk(): h = '8928308280fffff' out = h3.grid_disk(h, 1) assert len(out) == 1 + 6 expected = { '8928308280bffff', '89283082807ffff', '89283082877ffff', h, '89283082803ffff', '89283082873ffff', '8928308283bffff', } assert u.same_set(out, expected) def test_grid_disk2(): h = '8928308280fffff' out = h3.grid_disk(h, 2) assert len(out) == 1 + 6 + 12 expected = { '89283082813ffff', '89283082817ffff', '8928308281bffff', '89283082863ffff', '89283082823ffff', '89283082873ffff', '89283082877ffff', h, '8928308287bffff', '89283082833ffff', '8928308282bffff', '8928308283bffff', '89283082857ffff', '892830828abffff', '89283082847ffff', '89283082867ffff', '89283082803ffff', '89283082807ffff', '8928308280bffff', } assert u.same_set(out, expected) def test_grid_disk_pentagon(): h = '821c07fffffffff' # a pentagon cell out = h3.grid_disk(h, 1) assert len(out) == 1 + 5 expected = { '821c2ffffffffff', '821c27fffffffff', h, '821c17fffffffff', '821c1ffffffffff', '821c37fffffffff', } assert u.same_set(out, expected) def test_grid_ring(): h = '8928308280fffff' out = h3.grid_ring(h, 1) expected = { '8928308280bffff', '89283082807ffff', '89283082877ffff', '89283082803ffff', '89283082873ffff', '8928308283bffff', } assert u.same_set(out, expected) assert u.same_set( out, set(h3.grid_disk(h, 1)) - set(h3.grid_disk(h, 0)) ) def test_grid_ring2(): h = '8928308280fffff' out = h3.grid_ring(h, 2) expected = { '89283082813ffff', '89283082817ffff', '8928308281bffff', '89283082863ffff', '89283082823ffff', '8928308287bffff', '89283082833ffff', '8928308282bffff', '89283082857ffff', '892830828abffff', '89283082847ffff', '89283082867ffff', } assert u.same_set(out, expected) assert u.same_set( out, set(h3.grid_disk(h, 2)) - set(h3.grid_disk(h, 1)) ) def test_grid_ring_pentagon(): h = '821c07fffffffff' out = h3.grid_ring(h, 1) expected = { '821c17fffffffff', '821c1ffffffffff', '821c27fffffffff', '821c2ffffffffff', '821c37fffffffff', } assert u.same_set(out, expected) def test_compact_and_uncompact_cells(): sf_7x7 = [ (37.813318999983238, -122.4089866999972145), (37.7866302000007224, -122.3805436999997056), (37.7198061999978478, -122.3544736999993603), (37.7076131999975672, -122.5123436999983966), (37.7835871999971715, -122.5247187000021967), (37.8151571999998453, -122.4798767000009008), ] poly = h3.LatLngPoly(sf_7x7) cells = h3.h3shape_to_cells(poly, 9) compact_cells = h3.compact_cells(cells) assert len(compact_cells) == 209 uncompact_cells = h3.uncompact_cells(compact_cells, 9) assert len(uncompact_cells) == 1253 assert u.same_set(uncompact_cells, cells) def test_compact_cells_and_uncompact_cells_nothing(): assert h3.compact_cells([]) == [] assert h3.uncompact_cells([], 9) == [] def test_uncompact_cells_error(): hexagons = [h3.latlng_to_cell(37, -122, 10)] with pytest.raises(Exception): h3.uncompact_cells(hexagons, 5) def test_compact_cells_malformed_input(): cells = ['89283082813ffff'] * 13 with pytest.raises(Exception): h3.compact_cells(cells) def test_cell_to_parent(): h = '89283082813ffff' assert h3.cell_to_parent(h, 8) == '8828308281fffff' def test_cell_to_children(): h = '8828308281fffff' children = h3.cell_to_children(h, 9) assert len(children) == 7 def test_average_hexagon_area(): for i in range(0, 15): assert isinstance(h3.average_hexagon_area(i), float) assert isinstance(h3.average_hexagon_area(i, 'm^2'), float) with pytest.raises(ValueError): h3.average_hexagon_area(5, 'ft^2') def test_average_hexagon_edge_length(): for i in range(0, 15): assert isinstance(h3.average_hexagon_edge_length(i), float) assert isinstance(h3.average_hexagon_edge_length(i, 'm'), float) with pytest.raises(ValueError): h3.average_hexagon_edge_length(5, 'ft') def test_get_num_cells(): h0 = 122 assert h3.get_num_cells(0) == h0 for i in range(0, 15): n = h3.get_num_cells(i) * 1.0 / h0 assert 6**i <= n <= 7**i def test_get_base_cell_number(): assert h3.get_base_cell_number('8928308280fffff') == 20 def test_is_res_class_III(): assert h3.is_res_class_III('8928308280fffff') assert not h3.is_res_class_III('8828308280fffff') def test_is_pentagon(): assert h3.is_pentagon('821c07fffffffff') assert not h3.is_pentagon('8928308280fffff') def test_are_neighbor_cells(): assert h3.are_neighbor_cells('8928308280fffff', '8928308280bffff') assert not h3.are_neighbor_cells('821c07fffffffff', '8928308280fffff') def test_cells_to_directed_edge(): out = h3.cells_to_directed_edge('8928308280fffff', '8928308280bffff') assert h3.is_valid_directed_edge(out) with pytest.raises(h3.H3NotNeighborsError): h3.cells_to_directed_edge('821c07fffffffff', '8928308280fffff') def test_is_valid_directed_edge(): assert not h3.is_valid_directed_edge('8928308280fffff') assert h3.is_valid_directed_edge('11928308280fffff') def test_get_directed_edge_origin(): out = h3.get_directed_edge_origin('11928308280fffff') assert out == '8928308280fffff' def test_get_directed_edge_destination(): h = '11928308280fffff' out = h3.get_directed_edge_destination(h) assert out == '8928308283bffff' def test_directed_edge_to_cells(): e = h3.directed_edge_to_cells('11928308280fffff') assert e == ('8928308280fffff', '8928308283bffff') def test_origin_to_directed_edges(): h3_uni_edges = h3.origin_to_directed_edges( '8928308280fffff' ) assert len(h3_uni_edges) == 6 h3_uni_edge_pentagon = h3.origin_to_directed_edges( '821c07fffffffff' ) assert len(h3_uni_edge_pentagon) == 5 def test_grid_distance(): h = '89283082993ffff' assert 0 == h3.grid_distance(h, h) assert 1 == h3.grid_distance(h, '8928308299bffff') assert 5 == h3.grid_distance(h, '89283082827ffff') def test_grid_path_cells(): h1 = '8a2a84730587fff' h2 = '8a2a8471414ffff' out = h3.grid_path_cells(h1, h2) expected = [ h1, '8a2a8473059ffff', '8a2a847304b7fff', '8a2a84730487fff', '8a2a8473049ffff', '8a2a84732b37fff', '8a2a84732b17fff', '8a2a84732baffff', '8a2a84732a37fff', '8a2a84732a17fff', '8a2a84732aaffff', '8a2a84732a8ffff', '8a2a84732327fff', '8a2a8473232ffff', '8a2a8473230ffff', '8a2a84732227fff', '8a2a84732207fff', '8a2a8473221ffff', '8a2a847322e7fff', '8a2a847322c7fff', '8a2a847322dffff', '8a2a84714977fff', '8a2a84714957fff', '8a2a8471495ffff', '8a2a84714877fff', '8a2a84714857fff', '8a2a847148effff', '8a2a847148cffff', '8a2a84714b97fff', '8a2a8471416ffff', h2, ] assert out == expected with pytest.raises(h3.H3ResMismatchError): h3.grid_path_cells(h1, '8001fffffffffff') def test_cell_to_vertex(): # pentagon assert h3.cell_to_vertex('814c3ffffffffff', 0) == '2014c3ffffffffff' assert h3.cell_to_vertex('814c3ffffffffff', 1) == '2114c3ffffffffff' assert h3.cell_to_vertex('814c3ffffffffff', 2) == '2214c3ffffffffff' assert h3.cell_to_vertex('814c3ffffffffff', 3) == '2314c3ffffffffff' assert h3.cell_to_vertex('814c3ffffffffff', 4) == '2414c3ffffffffff' with pytest.raises(h3.H3DomainError): h3.cell_to_vertex('814c3ffffffffff', 5) # hexagon assert h3.cell_to_vertex('814d7ffffffffff', 0) == '2014d7ffffffffff' assert h3.cell_to_vertex('814d7ffffffffff', 1) == '2213abffffffffff' assert h3.cell_to_vertex('814d7ffffffffff', 2) == '2113abffffffffff' assert h3.cell_to_vertex('814d7ffffffffff', 3) == '2414c3ffffffffff' assert h3.cell_to_vertex('814d7ffffffffff', 4) == '2314c3ffffffffff' assert h3.cell_to_vertex('814d7ffffffffff', 5) == '2414cfffffffffff' def test_cell_to_vertexes(): # pentagon assert h3.cell_to_vertexes('814c3ffffffffff') == [ '2014c3ffffffffff', '2114c3ffffffffff', '2214c3ffffffffff', '2314c3ffffffffff', '2414c3ffffffffff', ] # hexagon assert h3.cell_to_vertexes('814d7ffffffffff') == [ '2014d7ffffffffff', '2213abffffffffff', '2113abffffffffff', '2414c3ffffffffff', '2314c3ffffffffff', '2414cfffffffffff', ] def test_vertex_to_latlng(): latlng = h3.vertex_to_latlng('2114c3ffffffffff') assert latlng == approx((24.945215618732814, -70.33904370008679)) def test_is_valid_vertex(): assert h3.is_valid_vertex('2114c3ffffffffff') assert not h3.is_valid_vertex(2455495337847029759) assert not h3.is_valid_vertex('foobar') def test_child_pos(): h = '88283080ddfffff' assert h3.cell_to_child_pos(h, 8) == 0 assert h3.cell_to_child_pos(h, 7) == 6 assert h3.cell_to_child_pos(h, 6) == 41 with pytest.raises(h3.H3BaseException): h3.cell_to_child_pos(h, 9) with pytest.raises(h3.H3BaseException): h3.child_pos_to_cell(h, 9, -1) with pytest.raises(h3.H3BaseException): h3.child_pos_to_cell(h, 9, 10000) def test_child_pos2(): h = '88283080ddfffff' assert h == h3.child_pos_to_cell(h, 8, 0) assert h == h3.child_pos_to_cell(h3.cell_to_parent(h, 7), 8, 6) assert h == h3.child_pos_to_cell(h3.cell_to_parent(h, 6), 8, 41) def test_cell_to_children_size(): h = '8053fffffffffff' # hexagon for r in range(16): assert h3.cell_to_children_size(h, r) == 7**r def test_cell_to_children_size2(): cells = h3.get_res0_cells() for r in range(16): total_cells = 120 * (7**r) + 2 assert total_cells == sum( h3.cell_to_children_size(h, r) for h in cells ) def test_child_pos3(): def cells_at_res(res): cells = h3.get_res0_cells() for parent in cells: yield from h3.cell_to_children(parent, res) def roundtrip(child, res_parent): res_child = h3.get_resolution(child) parent = h3.cell_to_parent(child, res_parent) pos = h3.cell_to_child_pos(child, res_parent) return h3.child_pos_to_cell(parent, res_child, pos) res_pairs = [ (res_parent, res_child) for res_parent in [0, 1] for res_child in [0, 1, 2, 3] if res_parent <= res_child ] for res_parent, res_child in res_pairs: for child in cells_at_res(res_child): assert child == roundtrip(child, res_parent) uber-h3-py-2883945/tests/test_lib/test_length_area.py000066400000000000000000000064611520643167100224620ustar00rootroot00000000000000import h3 import pytest from . import util as u def cell_perimiter1(h, unit='km'): edges = h3.origin_to_directed_edges(h) dists = [ h3.edge_length(e, unit=unit) for e in edges ] assert all(d > 0 for d in dists) return sum(dists) def cell_perimiter2(h, unit='km'): verts = h3.cell_to_boundary(h) N = len(verts) verts += (verts[0],) dists = [ h3.great_circle_distance(verts[i], verts[i + 1], unit=unit) for i in range(N) ] assert all(d > 0 for d in dists) return sum(dists) def test_areas_at_00(): areas_km2 = [ 2.562182162955495529e+06, 4.476842018179409206e+05, 6.596162242711056024e+04, 9.228872919002589697e+03, 1.318694490797110348e+03, 1.879593512281297762e+02, 2.687164354763186225e+01, 3.840848847060638782e+00, 5.486939641329895423e-01, 7.838600808637447015e-02, 1.119834221989390345e-02, 1.599777169186613647e-03, 2.285390931423379875e-04, 3.264850232091780848e-05, 4.664070326136773890e-06, 6.662957615868890711e-07, ] out = [ h3.cell_area(h3.latlng_to_cell(0, 0, r), unit='km^2') for r in range(16) ] assert u.approx2(out, areas_km2) areas_rads2 = [ 6.312389871006786335e-02, 1.102949377223657809e-02, 1.625081476657283096e-03, 2.273696413041990331e-04, 3.248837599063685022e-05, 4.630711750349743332e-06, 6.620305651949173071e-07, 9.462611873890716096e-08, 1.351804829317986891e-08, 1.931178237937334527e-09, 2.758910081529350229e-10, 3.941334595426616175e-11, 5.630465614578665530e-12, 8.043537197853909460e-13, 1.149076389260636790e-13, 1.641537700693487648e-14, ] out = [ h3.cell_area(h3.latlng_to_cell(0, 0, r), unit='rads^2') for r in range(16) ] assert u.approx2(out, areas_rads2) def test_bad_units(): h = '89754e64993ffff' e = '139754e64993ffff' assert h3.is_valid_cell(h) assert h3.is_valid_directed_edge(e) with pytest.raises(ValueError): h3.cell_area(h, unit='foot-pounds') with pytest.raises(ValueError): h3.edge_length(e, unit='foot-pounds') with pytest.raises(ValueError): h3.great_circle_distance((0, 0), (0, 0), unit='foot-pounds') def test_great_circle_distance(): lyon = (45.7597, 4.8422) # (lat, lon) paris = (48.8567, 2.3508) d = h3.great_circle_distance(lyon, paris, unit='rads') assert d == pytest.approx(0.0615628186794217) d = h3.great_circle_distance(lyon, paris, unit='m') assert d == pytest.approx(392217.1598841777) d = h3.great_circle_distance(lyon, paris, unit='km') assert d == pytest.approx(392.21715988417765) # test that 'km' is the default unit dist = h3.great_circle_distance assert dist(lyon, paris, unit='km') == dist(lyon, paris) def test_cell_perimiter_calculations(): resolutions = [0, 1] for r in resolutions: cells = h3.uncompact_cells(h3.get_res0_cells(), r) for h in cells: for unit in ['rads', 'm', 'km']: v1 = cell_perimiter1(h, unit) v2 = cell_perimiter2(h, unit) assert v1 == pytest.approx(v2) uber-h3-py-2883945/tests/test_lib/util.py000066400000000000000000000005641520643167100201250ustar00rootroot00000000000000import pytest def same_set(a, b): """Test if two collections are the same if taken as sets""" set_a = set(a) set_b = set(b) assert len(a) == len(b) == len(set_a) == len(set_b) return set_a == set_b def approx2(a, b): if len(a) != len(b): return False return all( x == pytest.approx(y) for x, y in zip(a, b) )