From 298f17faae4d3466fd6fc3b6d64dad107181bb25 Mon Sep 17 00:00:00 2001 From: AJ Friend Date: Thu, 19 May 2022 19:48:25 -0400 Subject: [PATCH 01/21] WIP: Starting work on v4, The Sequel (#238) * cimport for h3fake2 doesn't seem to work * back to where we were * meh, close to what i was hoping for * h3fake2 in github actions * comment out current bindings that consume the h3c v3 library * don't need the coveragerc file at the moment; clear out c files in /tests * sdist needs h3fake2 * try CIBW_BEFORE_BUILD to get h3fake2 * need h3fake2 for cibuildwheel tests * update h3 C library * comment out most of h3lib.pxd * calling from v4 c lib works! (tests dont' work yet tho) * tests shall pass * starting to move over first few functions * little steps * util submodule fully moved over; some hackery with h3fake2 errors * getting even trickier with h3fake2 errors * few more simple functions * geo_to_h3 and h3_to_geo * grid_distance function * grid_disk * parents and kids * compact/uncompact * getNumCells * comment out sdist test temporarily * commenting out `make_sdist` seems to have turned off the other CI jobs * no, it was that we're pushing to the dev branch * keep the sdist job in there, but commented out --- .coveragerc | 3 - .github/workflows/coverage-lint.yml | 5 +- .github/workflows/tests.yml | 5 +- .github/workflows/wheels.yml | 63 ++--- makefile | 2 + src/h3/_cy/CMakeLists.txt | 1 - src/h3/_cy/__init__.py | 41 ++- src/h3/_cy/cells.pxd | 20 +- src/h3/_cy/cells.pyx | 356 ++++++++++++++----------- src/h3/_cy/edges.pxd | 20 +- src/h3/_cy/edges.pyx | 140 +++++----- src/h3/_cy/geo.pxd | 6 +- src/h3/_cy/geo.pyx | 391 ++++++++++++++-------------- src/h3/_cy/h3lib.pxd | 214 +++++++-------- src/h3/_cy/to_multipoly.pyx | 104 ++++---- src/h3/_cy/unstable_vect.pyx | 89 ------- src/h3/_cy/util.pxd | 6 +- src/h3/_cy/util.pyx | 12 +- src/h3/_version.py | 37 +-- src/h3/unstable/__init__.py | 9 - src/h3/unstable/v4.py | 125 --------- src/h3/unstable/vect.py | 105 -------- src/h3lib | 2 +- tests/cython_example.pyx | 28 +- tests/h3fake2_errors.py | 15 ++ tests/test_cells_and_edges.py | 2 +- tests/test_cython.py | 40 +-- tests/test_length_area.py | 2 +- tests/test_polyfill.py | 6 +- tests/test_unstable_vect.py | 69 ----- 30 files changed, 813 insertions(+), 1105 deletions(-) delete mode 100644 .coveragerc delete mode 100644 src/h3/_cy/unstable_vect.pyx delete mode 100644 src/h3/unstable/__init__.py delete mode 100644 src/h3/unstable/v4.py delete mode 100644 src/h3/unstable/vect.py create mode 100644 tests/h3fake2_errors.py delete mode 100644 tests/test_unstable_vect.py diff --git a/.coveragerc b/.coveragerc deleted file mode 100644 index fbf9e0ec9..000000000 --- a/.coveragerc +++ /dev/null @@ -1,3 +0,0 @@ -[run] -omit = - */h3/unstable/* diff --git a/.github/workflows/coverage-lint.yml b/.github/workflows/coverage-lint.yml index e1d0bbdb8..511023632 100644 --- a/.github/workflows/coverage-lint.yml +++ b/.github/workflows/coverage-lint.yml @@ -2,9 +2,9 @@ name: coverage-lint on: push: - branches: [master] + branches: [master, dev_v4] pull_request: - branches: [master] + branches: [master, dev_v4] jobs: tests: @@ -23,6 +23,7 @@ jobs: - name: Install from source run: | pip install --upgrade pip setuptools wheel + pip install git+https://github.com/ajfriend/h3fake2.git pip install .[all] - name: Lint diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index f2c568274..0b9883dd7 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -2,9 +2,9 @@ name: tests on: push: - branches: [master] + branches: [master, dev_v4] pull_request: - branches: [master] + branches: [master, dev_v4] jobs: tests: @@ -44,6 +44,7 @@ jobs: - name: Install from source run: | pip install --upgrade pip setuptools wheel + pip install git+https://github.com/ajfriend/h3fake2.git pip install .[all] - name: Tests diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 5f75525de..71f47d5d4 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -2,9 +2,9 @@ name: wheels on: push: - branches: [master] + branches: [master, dev_v4] pull_request: - branches: [master] + branches: [master, dev_v4] types: # Opened, synchronize, and reopened are the default types # We add ready_for_review to additionally trigger when converting from draft to non-draft @@ -18,33 +18,34 @@ on: - published jobs: - make_sdist: - name: SDist - if: ${{ github.event_name != 'pull_request' || !github.event.pull_request.draft }} - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v3 - with: - submodules: recursive - - - name: Make sdist - run: | - pipx run build --sdist - - - name: Install from sdist - run: | - pip install --upgrade pip setuptools wheel - cp dist/h3-*.tar.gz h3.tar.gz - pip install h3.tar.gz[all] - - - name: Test sdist - run: pytest --cov=h3 --full-trace - - - name: Upload artifacts to GitHub - uses: actions/upload-artifact@v3 - with: - path: ./dist + # make_sdist: + # name: SDist + # if: ${{ github.event_name != 'pull_request' || !github.event.pull_request.draft }} + # runs-on: ubuntu-latest + + # steps: + # - uses: actions/checkout@v3 + # with: + # submodules: recursive + + # - name: Make sdist + # run: | + # pipx run build --sdist + + # - name: Install from sdist + # run: | + # pip install --upgrade pip setuptools wheel + # pip install git+https://github.com/ajfriend/h3fake2.git + # cp dist/h3-*.tar.gz h3.tar.gz + # pip install h3.tar.gz[all] + + # - name: Test sdist + # run: pytest --cov=h3 --full-trace + + # - name: Upload artifacts to GitHub + # uses: actions/upload-artifact@v3 + # with: + # path: ./dist make_cibw_v1_wheels: name: "cibuildwheel v1: ${{ matrix.name }}" @@ -74,6 +75,8 @@ jobs: CIBW_TEST_COMMAND: pytest {project}/tests CIBW_ARCHS_LINUX: auto aarch64 CIBW_BUILD: ${{ matrix.build }} + CIBW_BEFORE_BUILD: pip install git+https://github.com/ajfriend/h3fake2.git + CIBW_BEFORE_TEST: pip install git+https://github.com/ajfriend/h3fake2.git - name: Check with Twine run: | @@ -152,6 +155,8 @@ jobs: CIBW_ARCHS_MACOS: x86_64 arm64 CIBW_BUILD: ${{ matrix.build }} CIBW_SKIP: ${{ matrix.skip }} + CIBW_BEFORE_BUILD: pip install git+https://github.com/ajfriend/h3fake2.git + CIBW_BEFORE_TEST: pip install git+https://github.com/ajfriend/h3fake2.git - name: Check with Twine run: | diff --git a/makefile b/makefile index 6e44c02d0..7c2845e4e 100644 --- a/makefile +++ b/makefile @@ -12,6 +12,7 @@ init: purge git submodule update --init python -m venv env env/bin/pip install --upgrade pip wheel setuptools + env/bin/pip install git+https://github.com/ajfriend/h3fake2.git env/bin/pip install .[all] env/bin/pip install -r requirements.in @@ -23,6 +24,7 @@ clear: -@find . -type d -name '*.egg-info' | xargs rm -r -@find . -type f -name '*.pyc' | xargs rm -r -@find . -type d -name '*.ipynb_checkpoints' | xargs rm -r + -@find ./tests -type f -name '*.c' | xargs rm -r rebuild: clear env/bin/pip install .[all] diff --git a/src/h3/_cy/CMakeLists.txt b/src/h3/_cy/CMakeLists.txt index 0d7e78c9f..386557c5b 100644 --- a/src/h3/_cy/CMakeLists.txt +++ b/src/h3/_cy/CMakeLists.txt @@ -22,7 +22,6 @@ add_cython_file(geo) add_cython_file(cells) add_cython_file(edges) add_cython_file(to_multipoly) -add_cython_file(unstable_vect) # Include pyx and pxd files in distribution for use by Cython API install( diff --git a/src/h3/_cy/__init__.py b/src/h3/_cy/__init__.py index bb4dfc066..6b17b2d08 100644 --- a/src/h3/_cy/__init__.py +++ b/src/h3/_cy/__init__.py @@ -18,27 +18,44 @@ is_pentagon, get_base_cell, resolution, - parent, + is_res_class_iii, distance, disk, - ring, + parent, children, + center_child, compact, uncompact, num_hexagons, +) + +from h3fake2._cy import ( + # is_cell, + # is_pentagon, + # get_base_cell, + # resolution, + # parent, + # distance, + # disk, + ring, + # children, + # compact, + # uncompact, + # num_hexagons, mean_hex_area, cell_area, line, - is_res_class_iii, + # is_res_class_iii, get_pentagon_indexes, get_res0_indexes, - center_child, + # center_child, get_faces, experimental_h3_to_local_ij, experimental_local_ij_to_h3, ) -from .edges import ( +# from .edges import ( +from h3fake2._cy import ( are_neighbors, edge, is_edge, @@ -53,6 +70,17 @@ from .geo import ( geo_to_h3, h3_to_geo, + # polyfill_polygon, + # polyfill_geojson, + # polyfill, + # cell_boundary, + # edge_boundary, + # point_dist, +) + +from h3fake2._cy import ( + # geo_to_h3, + # h3_to_geo, polyfill_polygon, polyfill_geojson, polyfill, @@ -61,7 +89,8 @@ point_dist, ) -from .to_multipoly import ( +# from .to_multipoly import ( +from h3fake2._cy import ( h3_set_to_multi_polygon ) diff --git a/src/h3/_cy/cells.pxd b/src/h3/_cy/cells.pxd index 10719389e..4e2d4411d 100644 --- a/src/h3/_cy/cells.pxd +++ b/src/h3/_cy/cells.pxd @@ -6,20 +6,20 @@ cpdef int get_base_cell(H3int h) except -1 cpdef int resolution(H3int h) except -1 cpdef int distance(H3int h1, H3int h2) except -1 cpdef H3int[:] disk(H3int h, int k) -cpdef H3int[:] _ring_fallback(H3int h, int k) -cpdef H3int[:] ring(H3int h, int k) +# cpdef H3int[:] _ring_fallback(H3int h, int k) +# cpdef H3int[:] ring(H3int h, int k) cpdef H3int parent(H3int h, res=*) except 0 cpdef H3int[:] children(H3int h, res=*) cpdef H3int center_child(H3int h, res=*) except 0 cpdef H3int[:] compact(const H3int[:] hu) cpdef H3int[:] uncompact(const H3int[:] hc, int res) cpdef int64_t num_hexagons(int resolution) except -1 -cpdef double mean_hex_area(int resolution, unit=*) except -1 -cpdef double cell_area(H3int h, unit=*) except -1 -cpdef H3int[:] line(H3int start, H3int end) +# cpdef double mean_hex_area(int resolution, unit=*) except -1 +# cpdef double cell_area(H3int h, unit=*) except -1 +# cpdef H3int[:] line(H3int start, H3int end) cpdef bool is_res_class_iii(H3int h) -cpdef H3int[:] get_pentagon_indexes(int res) -cpdef H3int[:] get_res0_indexes() -cpdef get_faces(H3int h) -cpdef (int, int) experimental_h3_to_local_ij(H3int origin, H3int h) except * -cpdef H3int experimental_local_ij_to_h3(H3int origin, int i, int j) except 0 +# cpdef H3int[:] get_pentagon_indexes(int res) +# cpdef H3int[:] get_res0_indexes() +# cpdef get_faces(H3int h) +# cpdef (int, int) experimental_h3_to_local_ij(H3int origin, H3int h) except * +# cpdef H3int experimental_local_ij_to_h3(H3int origin, int i, int j) except 0 diff --git a/src/h3/_cy/cells.pyx b/src/h3/_cy/cells.pyx index 4206b81d8..265e4c377 100644 --- a/src/h3/_cy/cells.pyx +++ b/src/h3/_cy/cells.pyx @@ -24,15 +24,17 @@ cpdef bool is_cell(H3int h): ------- boolean """ - return h3lib.h3IsValid(h) == 1 + return h3lib.isValidCell(h) == 1 + cpdef bool is_pentagon(H3int h): - return h3lib.h3IsPentagon(h) == 1 + return h3lib.isPentagon(h) == 1 + cpdef int get_base_cell(H3int h) except -1: check_cell(h) - return h3lib.h3GetBaseCell(h) + return h3lib.getBaseCellNumber(h) cpdef int resolution(H3int h) except -1: @@ -41,96 +43,109 @@ cpdef int resolution(H3int h) except -1: """ check_cell(h) - return h3lib.h3GetResolution(h) + return h3lib.getResolution(h) cpdef int distance(H3int h1, H3int h2) except -1: - """ compute the hex-distance between two hexagons + """ Compute the grid distance between two cells """ + cdef: + int64_t distance + h3lib.H3Error err + check_cell(h1) check_cell(h2) - d = h3lib.h3Distance(h1,h2) - - if d < 0: + err = h3lib.gridDistance(h1, h2, &distance) + if err: + # todo: do error handling later s = 'Cells are too far apart to compute distance: {} and {}' s = s.format(hex(h1), hex(h2)) raise H3ValueError(s) - return d + return distance cpdef H3int[:] disk(H3int h, int k): """ Return cells at grid distance `<= k` from `h`. """ + cdef: + int64_t n + h3lib.H3Error err + check_cell(h) check_distance(k) - n = h3lib.maxKringSize(k) + # ignoring error for now + err = h3lib.maxGridDiskSize(k, &n) ptr = create_ptr(n) # todo: return a "smart" pointer that knows its length? - h3lib.kRing(h, k, ptr) + err = h3lib.gridDisk(h, k, ptr) # ignoring error again! mv = create_mv(ptr, n) return mv -cpdef H3int[:] _ring_fallback(H3int h, int k): - """ - `ring` tries to call `h3lib.hexRing` first; if that fails, we call - this function, which relies on `h3lib.kRingDistances`. +# cpdef H3int[:] _ring_fallback(H3int h, int k): +# """ +# `ring` tries to call `h3lib.hexRing` first; if that fails, we call +# this function, which relies on `h3lib.kRingDistances`. - Failures for `h3lib.hexRing` happen when the algortihm runs into a pentagon. - """ - check_cell(h) - check_distance(k) +# Failures for `h3lib.hexRing` happen when the algortihm runs into a pentagon. +# """ +# check_cell(h) +# check_distance(k) - n = h3lib.maxKringSize(k) - # array of h3 cells - ptr = create_ptr(n) +# n = h3lib.maxKringSize(k) +# # array of h3 cells +# ptr = create_ptr(n) - # array of cell distances from `h` - dist_ptr = stdlib.calloc(n, sizeof(int)) - if dist_ptr is NULL: - raise MemoryError() +# # array of cell distances from `h` +# dist_ptr = stdlib.calloc(n, sizeof(int)) +# if dist_ptr is NULL: +# raise MemoryError() - h3lib.kRingDistances(h, k, ptr, dist_ptr) +# h3lib.kRingDistances(h, k, ptr, dist_ptr) - distances = dist_ptr - distances.callback_free_data = stdlib.free +# distances = dist_ptr +# distances.callback_free_data = stdlib.free - for i,v in enumerate(distances): - if v != k: - ptr[i] = 0 +# for i,v in enumerate(distances): +# if v != k: +# ptr[i] = 0 - mv = create_mv(ptr, n) +# mv = create_mv(ptr, n) - return mv +# return mv -cpdef H3int[:] 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) +# cpdef H3int[:] 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 - ptr = create_ptr(n) +# n = 6*k if k > 0 else 1 +# ptr = create_ptr(n) - flag = h3lib.hexRing(h, k, ptr) +# flag = h3lib.hexRing(h, k, ptr) - # if we drop into the failure state, we might be tempted to not create - # this mv, but creating the mv is exactly what guarantees that we'll free - # the memory. context manager would be better here, if we can figure out - # how to do that - mv = create_mv(ptr, n) +# # if we drop into the failure state, we might be tempted to not create +# # this mv, but creating the mv is exactly what guarantees that we'll free +# # the memory. context manager would be better here, if we can figure out +# # how to do that +# mv = create_mv(ptr, n) - if flag != 0: - mv = _ring_fallback(h, k) +# if flag != 0: +# mv = _ring_fallback(h, k) - return mv +# return mv cpdef H3int parent(H3int h, res=None) except 0: + cdef: + H3int parent + h3lib.H3Error err + check_cell(h) if res is None: @@ -141,10 +156,17 @@ cpdef H3int parent(H3int h, res=None) except 0: raise H3ResolutionError(msg) check_res(res) + err = h3lib.cellToParent(h, res, &parent) + + return parent - return h3lib.h3ToParent(h, res) cpdef H3int[:] children(H3int h, res=None): + cdef: + H3int child + h3lib.H3Error err + int64_t N + check_cell(h) if res is None: @@ -155,16 +177,20 @@ cpdef H3int[:] children(H3int h, res=None): raise H3ResolutionError(msg) check_res(res) + err = h3lib.cellToChildrenSize(h, res, &N) - n = h3lib.maxH3ToChildrenSize(h, res) - - ptr = create_ptr(n) - h3lib.h3ToChildren(h, res, ptr) - mv = create_mv(ptr, n) + ptr = create_ptr(N) + err = h3lib.cellToChildren(h, res, ptr) + mv = create_mv(ptr, N) return mv + cpdef H3int center_child(H3int h, res=None) except 0: + cdef: + H3int child + h3lib.H3Error err + check_cell(h) if res is None: @@ -175,8 +201,9 @@ cpdef H3int center_child(H3int h, res=None) except 0: raise H3ResolutionError(msg) check_res(res) + err = h3lib.cellToCenterChild(h, res, &child) - return h3lib.h3ToCenterChild(h, res) + return child @@ -186,6 +213,9 @@ cpdef H3int[:] compact(const H3int[:] hu): # `&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! + cdef: + h3lib.H3Error err + if len(hu) == 0: return empty_memory_view() @@ -193,14 +223,16 @@ cpdef H3int[:] compact(const H3int[:] hu): check_cell(h) ptr = create_ptr(len(hu)) - flag = h3lib.compact(&hu[0], ptr, len(hu)) + err = h3lib.compactCells(&hu[0], ptr, len(hu)) mv = create_mv(ptr, len(hu)) - if flag != 0: + if err: + # todo: additional error processing raise H3ValueError('Could not compact set of hexagons!') 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(const H3int[:] hc, int res): @@ -209,23 +241,30 @@ cpdef H3int[:] uncompact(const H3int[:] hc, int res): # `&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: + h3lib.H3Error err + int64_t N + if len(hc) == 0: return empty_memory_view() for h in hc: check_cell(h) - N = h3lib.maxUncompactSize(&hc[0], len(hc), res) + # ignoring error for now + err = h3lib.uncompactCellsSize(&hc[0], len(hc), res, &N) ptr = create_ptr(N) - flag = h3lib.uncompact( - &hc[0], len(hc), - ptr, N, + err = h3lib.uncompactCells( + &hc[0], + len(hc), + ptr, + N, res ) mv = create_mv(ptr, N) - if flag != 0: + if err: raise H3ValueError('Could not uncompact set of hexagons!') return mv @@ -233,143 +272,148 @@ cpdef H3int[:] uncompact(const H3int[:] hc, int res): cpdef int64_t num_hexagons(int resolution) except -1: check_res(resolution) + cdef: + h3lib.H3Error err + int64_t num_cells - return h3lib.numHexagons(resolution) + err = h3lib.getNumCells(resolution, &num_cells) + return num_cells -cpdef double mean_hex_area(int resolution, unit='km^2') except -1: - check_res(resolution) - area = h3lib.hexAreaKm2(resolution) +# cpdef double mean_hex_area(int resolution, unit='km^2') except -1: +# check_res(resolution) - # todo: multiple units - convert = { - 'km^2': 1.0, - 'm^2': 1000*1000.0 - } +# area = h3lib.hexAreaKm2(resolution) - try: - area *= convert[unit] - except: - raise H3ValueError('Unknown unit: {}'.format(unit)) +# # todo: multiple units +# convert = { +# 'km^2': 1.0, +# 'm^2': 1000*1000.0 +# } - return area +# try: +# area *= convert[unit] +# except: +# raise H3ValueError('Unknown unit: {}'.format(unit)) +# return area -cpdef double cell_area(H3int h, unit='km^2') except -1: - check_cell(h) - if unit == 'rads^2': - area = h3lib.cellAreaRads2(h) - elif unit == 'km^2': - area = h3lib.cellAreaKm2(h) - elif unit == 'm^2': - area = h3lib.cellAreaM2(h) - else: - raise H3ValueError('Unknown unit: {}'.format(unit)) +# cpdef double cell_area(H3int h, unit='km^2') except -1: +# check_cell(h) - return area +# if unit == 'rads^2': +# area = h3lib.cellAreaRads2(h) +# elif unit == 'km^2': +# area = h3lib.cellAreaKm2(h) +# elif unit == 'm^2': +# area = h3lib.cellAreaM2(h) +# else: +# raise H3ValueError('Unknown unit: {}'.format(unit)) +# return area -cpdef H3int[:] line(H3int start, H3int end): - check_cell(start) - check_cell(end) - n = h3lib.h3LineSize(start, end) +# cpdef H3int[:] line(H3int start, H3int end): +# check_cell(start) +# check_cell(end) - if n < 0: - s = "Couldn't find line between cells {} and {}" - s = s.format(hex(start), hex(end)) - raise H3ValueError(s) +# n = h3lib.h3LineSize(start, end) - ptr = create_ptr(n) - flag = h3lib.h3Line(start, end, ptr) - mv = create_mv(ptr, n) +# if n < 0: +# s = "Couldn't find line between cells {} and {}" +# s = s.format(hex(start), hex(end)) +# raise H3ValueError(s) - if flag != 0: - s = "Couldn't find line between cells {} and {}" - s = s.format(hex(start), hex(end)) - raise H3ValueError(s) +# ptr = create_ptr(n) +# flag = h3lib.h3Line(start, end, ptr) +# mv = create_mv(ptr, n) - return mv +# if flag != 0: +# s = "Couldn't find line between cells {} and {}" +# s = s.format(hex(start), hex(end)) +# raise H3ValueError(s) + +# return mv cpdef bool is_res_class_iii(H3int h): - return h3lib.h3IsResClassIII(h) == 1 + return h3lib.isResClassIII(h) == 1 -cpdef H3int[:] get_pentagon_indexes(int res): - check_res(res) +# cpdef H3int[:] get_pentagon_indexes(int res): +# check_res(res) - n = h3lib.pentagonIndexCount() +# n = h3lib.pentagonIndexCount() - ptr = create_ptr(n) - h3lib.getPentagonIndexes(res, ptr) - mv = create_mv(ptr, n) +# ptr = create_ptr(n) +# h3lib.getPentagonIndexes(res, ptr) +# mv = create_mv(ptr, n) - return mv +# return mv -cpdef H3int[:] get_res0_indexes(): - n = h3lib.res0IndexCount() +# cpdef H3int[:] get_res0_indexes(): +# n = h3lib.res0IndexCount() - ptr = create_ptr(n) - h3lib.getRes0Indexes(ptr) - mv = create_mv(ptr, n) +# ptr = create_ptr(n) +# h3lib.getRes0Indexes(ptr) +# mv = create_mv(ptr, n) - return mv +# return mv -cpdef get_faces(H3int h): - check_cell(h) +# cpdef get_faces(H3int h): +# check_cell(h) - n = h3lib.maxFaceCount(h) +# n = h3lib.maxFaceCount(h) - cdef int* ptr = stdlib.calloc(n, sizeof(int)) - if (n > 0) and (not ptr): - raise MemoryError() +# cdef int* ptr = stdlib.calloc(n, sizeof(int)) +# if (n > 0) and (not ptr): +# raise MemoryError() - h3lib.h3GetFaces(h, ptr) +# h3lib.h3GetFaces(h, ptr) - faces = ptr - faces = {f for f in faces if f >= 0} - stdlib.free(ptr) +# faces = ptr +# faces = {f for f in faces if f >= 0} +# stdlib.free(ptr) - return faces +# return faces -cpdef (int, int) experimental_h3_to_local_ij(H3int origin, H3int h) except *: - cdef: - int flag - h3lib.CoordIJ c +# cpdef (int, int) experimental_h3_to_local_ij(H3int origin, H3int h) except *: +# cdef: +# int flag +# h3lib.CoordIJ c - check_cell(origin) - check_cell(h) +# check_cell(origin) +# check_cell(h) - flag = h3lib.experimentalH3ToLocalIj(origin, h, &c) +# flag = h3lib.experimentalH3ToLocalIj(origin, h, &c) - if flag != 0: - s = "Couldn't find local (i,j) between cells {} and {}." - s = s.format(hex(origin), hex(h)) - raise H3ValueError(s) +# if flag != 0: +# s = "Couldn't find local (i,j) between cells {} and {}." +# s = s.format(hex(origin), hex(h)) +# raise H3ValueError(s) - return c.i, c.j +# return c.i, c.j -cpdef H3int experimental_local_ij_to_h3(H3int origin, int i, int j) except 0: - cdef: - int flag - h3lib.CoordIJ c - H3int out +# cpdef H3int experimental_local_ij_to_h3(H3int origin, int i, int j) except 0: +# cdef: +# int flag +# h3lib.CoordIJ c +# H3int out - check_cell(origin) +# check_cell(origin) - c.i, c.j = i, j +# c.i, c.j = i, j - flag = h3lib.experimentalLocalIjToH3(origin, &c, &out) +# flag = h3lib.experimentalLocalIjToH3(origin, &c, &out) - if flag != 0: - s = "Couldn't find cell at local ({},{}) from cell {}." - s = s.format(i, j, hex(origin)) - raise H3ValueError(s) +# if flag != 0: +# s = "Couldn't find cell at local ({},{}) from cell {}." +# s = s.format(i, j, hex(origin)) +# raise H3ValueError(s) - return out +# return out diff --git a/src/h3/_cy/edges.pxd b/src/h3/_cy/edges.pxd index 465b82d8f..cdcf4d029 100644 --- a/src/h3/_cy/edges.pxd +++ b/src/h3/_cy/edges.pxd @@ -1,11 +1,11 @@ -from .h3lib cimport bool, H3int +# from .h3lib cimport bool, H3int -cpdef bool are_neighbors(H3int h1, H3int h2) -cpdef H3int edge(H3int origin, H3int destination) except 1 -cpdef bool is_edge(H3int e) -cpdef H3int edge_origin(H3int e) except 1 -cpdef H3int edge_destination(H3int e) except 1 -cpdef (H3int, H3int) edge_cells(H3int e) except * -cpdef H3int[:] edges_from_cell(H3int origin) -cpdef double mean_edge_length(int resolution, unit=*) except -1 -cpdef double edge_length(H3int e, unit=*) except -1 +# cpdef bool are_neighbors(H3int h1, H3int h2) +# cpdef H3int edge(H3int origin, H3int destination) except 1 +# cpdef bool is_edge(H3int e) +# cpdef H3int edge_origin(H3int e) except 1 +# cpdef H3int edge_destination(H3int e) except 1 +# cpdef (H3int, H3int) edge_cells(H3int e) except * +# cpdef H3int[:] edges_from_cell(H3int origin) +# cpdef double mean_edge_length(int resolution, unit=*) except -1 +# cpdef double edge_length(H3int e, unit=*) except -1 diff --git a/src/h3/_cy/edges.pyx b/src/h3/_cy/edges.pyx index 656000636..27397d957 100644 --- a/src/h3/_cy/edges.pyx +++ b/src/h3/_cy/edges.pyx @@ -1,100 +1,100 @@ -cimport h3lib -from .h3lib cimport bool, H3int +# cimport h3lib +# from .h3lib cimport bool, H3int -from .util cimport ( - check_cell, - check_edge, - check_res, - create_ptr, - create_mv, -) +# from .util cimport ( +# check_cell, +# check_edge, +# check_res, +# create_ptr, +# create_mv, +# ) -from .util import H3ValueError +# from .util import H3ValueError -cpdef bool are_neighbors(H3int h1, H3int h2): - check_cell(h1) - check_cell(h2) +# cpdef bool are_neighbors(H3int h1, H3int h2): +# check_cell(h1) +# check_cell(h2) - return h3lib.h3IndexesAreNeighbors(h1, h2) == 1 +# return h3lib.h3IndexesAreNeighbors(h1, h2) == 1 -cpdef H3int edge(H3int origin, H3int destination) except 1: - check_cell(origin) - check_cell(destination) +# cpdef H3int edge(H3int origin, H3int destination) except 1: +# check_cell(origin) +# check_cell(destination) - if h3lib.h3IndexesAreNeighbors(origin, destination) != 1: - s = 'Cells are not neighbors: {} and {}' - s = s.format(hex(origin), hex(destination)) - raise H3ValueError(s) +# if h3lib.h3IndexesAreNeighbors(origin, destination) != 1: +# s = 'Cells are not neighbors: {} and {}' +# s = s.format(hex(origin), hex(destination)) +# raise H3ValueError(s) - return h3lib.getH3UnidirectionalEdge(origin, destination) +# return h3lib.getH3UnidirectionalEdge(origin, destination) -cpdef bool is_edge(H3int e): - return h3lib.h3UnidirectionalEdgeIsValid(e) == 1 +# cpdef bool is_edge(H3int e): +# return h3lib.h3UnidirectionalEdgeIsValid(e) == 1 -cpdef H3int edge_origin(H3int e) except 1: - # without the check, with an invalid input, the function will just return 0 - check_edge(e) +# cpdef H3int edge_origin(H3int e) except 1: +# # without the check, with an invalid input, the function will just return 0 +# check_edge(e) - return h3lib.getOriginH3IndexFromUnidirectionalEdge(e) +# return h3lib.getOriginH3IndexFromUnidirectionalEdge(e) -cpdef H3int edge_destination(H3int e) except 1: - check_edge(e) +# cpdef H3int edge_destination(H3int e) except 1: +# check_edge(e) - return h3lib.getDestinationH3IndexFromUnidirectionalEdge(e) +# return h3lib.getDestinationH3IndexFromUnidirectionalEdge(e) -cpdef (H3int, H3int) edge_cells(H3int e) except *: - check_edge(e) +# cpdef (H3int, H3int) edge_cells(H3int e) except *: +# check_edge(e) - return edge_origin(e), edge_destination(e) +# return edge_origin(e), edge_destination(e) -cpdef H3int[:] edges_from_cell(H3int origin): - """ Returns the 6 (or 5 for pentagons) directed edges - for the given origin cell - """ - check_cell(origin) +# cpdef H3int[:] edges_from_cell(H3int origin): +# """ Returns the 6 (or 5 for pentagons) directed edges +# for the given origin cell +# """ +# check_cell(origin) - ptr = create_ptr(6) - h3lib.getH3UnidirectionalEdgesFromHexagon(origin, ptr) - mv = create_mv(ptr, 6) +# ptr = create_ptr(6) +# h3lib.getH3UnidirectionalEdgesFromHexagon(origin, ptr) +# mv = create_mv(ptr, 6) - return mv +# return mv -cpdef double mean_edge_length(int resolution, unit='km') except -1: - check_res(resolution) +# cpdef double mean_edge_length(int resolution, unit='km') except -1: +# check_res(resolution) - length = h3lib.edgeLengthKm(resolution) +# length = h3lib.edgeLengthKm(resolution) - # todo: multiple units - convert = { - 'km': 1.0, - 'm': 1000.0 - } +# # todo: multiple units +# convert = { +# 'km': 1.0, +# 'm': 1000.0 +# } - try: - length *= convert[unit] - except: - raise H3ValueError('Unknown unit: {}'.format(unit)) +# try: +# length *= convert[unit] +# except: +# raise H3ValueError('Unknown unit: {}'.format(unit)) - return length +# return length -cpdef double edge_length(H3int e, unit='km') except -1: - check_edge(e) +# cpdef double edge_length(H3int e, unit='km') except -1: +# check_edge(e) - # todo: maybe kick this logic up to the python level - # it might be a little cleaner, because we can do the "switch statement" - # with a dict, but would require exposing more C functions +# # todo: maybe kick this logic up to the python level +# # it might be a little cleaner, because we can do the "switch statement" +# # with a dict, but would require exposing more C functions - if unit == 'rads': - length = h3lib.exactEdgeLengthRads(e) - elif unit == 'km': - length = h3lib.exactEdgeLengthKm(e) - elif unit == 'm': - length = h3lib.exactEdgeLengthM(e) - else: - raise H3ValueError('Unknown unit: {}'.format(unit)) +# if unit == 'rads': +# length = h3lib.exactEdgeLengthRads(e) +# elif unit == 'km': +# length = h3lib.exactEdgeLengthKm(e) +# elif unit == 'm': +# length = h3lib.exactEdgeLengthM(e) +# else: +# raise H3ValueError('Unknown unit: {}'.format(unit)) - return length +# return length diff --git a/src/h3/_cy/geo.pxd b/src/h3/_cy/geo.pxd index 1a2c1c332..37e32a0b5 100644 --- a/src/h3/_cy/geo.pxd +++ b/src/h3/_cy/geo.pxd @@ -2,6 +2,6 @@ from .h3lib cimport H3int cpdef H3int geo_to_h3(double lat, double lng, int res) except 1 cpdef (double, double) h3_to_geo(H3int h) except * -cpdef double point_dist( - double lat1, double lng1, - double lat2, double lng2, unit=*) except -1 +# cpdef double point_dist( +# double lat1, double lng1, +# double lat2, double lng2, unit=*) except -1 diff --git a/src/h3/_cy/geo.pyx b/src/h3/_cy/geo.pyx index dc2c1d7b4..fe6c93b5b 100644 --- a/src/h3/_cy/geo.pyx +++ b/src/h3/_cy/geo.pyx @@ -16,263 +16,268 @@ from .util import H3ValueError cpdef H3int geo_to_h3(double lat, double lng, int res) except 1: cdef: - h3lib.GeoCoord c + h3lib.LatLng c + H3int out + h3lib.H3Error err check_res(res) - c = deg2coord(lat, lng) - return h3lib.geoToH3(&c, res) + # todo: just ignoring the error for now, check in the future + err = h3lib.latLngToCell(&c, res, &out) + + return out cpdef (double, double) h3_to_geo(H3int h) except *: """Map an H3 cell into its centroid geo-coordinate (lat/lng)""" cdef: - h3lib.GeoCoord c + h3lib.LatLng c + h3lib.H3Error err check_cell(h) - h3lib.h3ToGeo(h, &c) + err = h3lib.cellToLatLng(h, &c) return coord2deg(c) -cdef h3lib.Geofence make_geofence(geos, bool lnglat_order=False) except *: - """ +# cdef h3lib.Geofence make_geofence(geos, bool lnglat_order=False) except *: +# """ - The returned `Geofence` must be freed with a call to `free_geofence`. +# The returned `Geofence` must be freed with a call to `free_geofence`. - Parameters - ---------- - geos : list or tuple - GeoFence: 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. - lnglat_order : bool - If True, assume coordinate pairs like (lng, lat) - If False, assume coordinate pairs like (lat, lng) - """ - cdef: - h3lib.Geofence gf +# Parameters +# ---------- +# geos : list or tuple +# GeoFence: 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. +# lnglat_order : bool +# If True, assume coordinate pairs like (lng, lat) +# If False, assume coordinate pairs like (lat, lng) +# """ +# cdef: +# h3lib.Geofence gf - gf.numVerts = len(geos) +# gf.numVerts = len(geos) - gf.verts = stdlib.calloc(gf.numVerts, sizeof(h3lib.GeoCoord)) +# gf.verts = stdlib.calloc(gf.numVerts, sizeof(h3lib.GeoCoord)) - if lnglat_order: - latlng = (g[::-1] for g in geos) - else: - latlng = geos +# if lnglat_order: +# latlng = (g[::-1] for g in geos) +# else: +# latlng = geos - for i, (lat, lng) in enumerate(latlng): - gf.verts[i] = deg2coord(lat, lng) +# for i, (lat, lng) in enumerate(latlng): +# gf.verts[i] = deg2coord(lat, lng) - return gf +# return gf -cdef free_geofence(h3lib.Geofence* gf): - stdlib.free(gf.verts) - gf.verts = NULL +# cdef free_geofence(h3lib.Geofence* gf): +# stdlib.free(gf.verts) +# gf.verts = NULL -cdef class GeoPolygon: - cdef: - h3lib.GeoPolygon gp - - def __cinit__(self, outer, holes=None, bool lnglat_order=False): - """ - - Parameters - ---------- - outer : list or tuple - GeoFence - A GeoFence 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 GeoFences - lnglat_order : bool - If True, assume coordinate pairs like (lng, lat) - If False, assume coordinate pairs like (lat, lng) - - """ - if holes is None: - holes = [] - - self.gp.geofence = make_geofence(outer, lnglat_order) - self.gp.numHoles = len(holes) - self.gp.holes = NULL - - if len(holes) > 0: - self.gp.holes = stdlib.calloc(len(holes), sizeof(h3lib.Geofence)) - for i, hole in enumerate(holes): - self.gp.holes[i] = make_geofence(hole, lnglat_order) - - - def __dealloc__(self): - free_geofence(&self.gp.geofence) - - for i in range(self.gp.numHoles): - free_geofence(&self.gp.holes[i]) - - stdlib.free(self.gp.holes) - - -def polyfill_polygon(outer, int res, holes=None, bool lnglat_order=False): - """ Set of hexagons whose center is contained in a polygon. - - The polygon is defined as in the GeoJson standard, with an exterior - LinearRing `outer` and a list of LinearRings `holes`, which define any - holes in the polygon. - - Each LinearRing 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 will work with any input format. - - Parameters - ---------- - outer : list or tuple - A LinearRing, a sequence of (lat/lng) or (lng/lat) pairs - res : int - The resolution of the output hexagons - holes : list or tuple - A collection of LinearRings, describing any holes in the polygon - lnglat_order : bool - If True, assume coordinate pairs like (lng, lat) - If False, assume coordinate pairs like (lat, lng) - """ +# cdef class GeoPolygon: +# cdef: +# h3lib.GeoPolygon gp - check_res(res) - gp = GeoPolygon(outer, holes=holes, lnglat_order=lnglat_order) +# def __cinit__(self, outer, holes=None, bool lnglat_order=False): +# """ - n = h3lib.maxPolyfillSize(&gp.gp, res) - ptr = create_ptr(n) +# Parameters +# ---------- +# outer : list or tuple +# GeoFence +# A GeoFence 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 GeoFences +# lnglat_order : bool +# If True, assume coordinate pairs like (lng, lat) +# If False, assume coordinate pairs like (lat, lng) - h3lib.polyfill(&gp.gp, res, ptr) - mv = create_mv(ptr, n) +# """ +# if holes is None: +# holes = [] - return mv +# self.gp.geofence = make_geofence(outer, lnglat_order) +# self.gp.numHoles = len(holes) +# self.gp.holes = NULL +# if len(holes) > 0: +# self.gp.holes = stdlib.calloc(len(holes), sizeof(h3lib.Geofence)) +# for i, hole in enumerate(holes): +# self.gp.holes[i] = make_geofence(hole, lnglat_order) -def polyfill_geojson(geojson, int res): - """ Set of hexagons whose center is contained in a GeoJson Polygon object. - The polygon is defined exactly as in the GeoJson standard, so - `geojson` should be a dictionary like: - { - 'type': 'Polygon', - 'coordinates': [...] - } +# def __dealloc__(self): +# free_geofence(&self.gp.geofence) - 'coordinates' should be a list of LinearRings, where the first ring describes - the exterior boundary of the Polygon, and any subsequent LinearRings - describe holes in the polygon. +# for i in range(self.gp.numHoles): +# free_geofence(&self.gp.holes[i]) - Note that we don't provide an option for the order of the coordinates, - as the GeoJson standard requires them to be in lng/lat order. +# stdlib.free(self.gp.holes) - Parameters - ---------- - geojson : dict - res : int - The resolution of the output hexagons - """ - # todo: this one could handle multipolygons... +# def polyfill_polygon(outer, int res, holes=None, bool lnglat_order=False): +# """ Set of hexagons whose center is contained in a polygon. - if geojson['type'] != 'Polygon': - raise ValueError('Only Polygon GeoJSON supported') +# The polygon is defined as in the GeoJson standard, with an exterior +# LinearRing `outer` and a list of LinearRings `holes`, which define any +# holes in the polygon. - coords = geojson['coordinates'] +# Each LinearRing 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 will work with any input format. - out = polyfill_polygon(coords[0], res, holes=coords[1:], lnglat_order=True) +# Parameters +# ---------- +# outer : list or tuple +# A LinearRing, a sequence of (lat/lng) or (lng/lat) pairs +# res : int +# The resolution of the output hexagons +# holes : list or tuple +# A collection of LinearRings, describing any holes in the polygon +# lnglat_order : bool +# If True, assume coordinate pairs like (lng, lat) +# If False, assume coordinate pairs like (lat, lng) +# """ - return out +# check_res(res) +# gp = GeoPolygon(outer, holes=holes, lnglat_order=lnglat_order) +# n = h3lib.maxPolyfillSize(&gp.gp, res) +# ptr = create_ptr(n) -def polyfill(dict geojson, int res, bool geo_json_conformant=False): - """ Light wrapper around `polyfill_geojson` to provide backward compatibility. - """ +# h3lib.polyfill(&gp.gp, res, ptr) +# mv = create_mv(ptr, n) - try: - gj_type = geojson['type'] - except KeyError: - raise KeyError("`geojson` dict must have key 'type'.") from None +# return mv - if gj_type != 'Polygon': - raise ValueError('Only Polygon GeoJSON supported') - if geo_json_conformant: - out = polyfill_geojson(geojson, res) - else: - coords = geojson['coordinates'] - out = polyfill_polygon(coords[0], res, holes=coords[1:], lnglat_order=False) +# def polyfill_geojson(geojson, int res): +# """ Set of hexagons whose center is contained in a GeoJson Polygon object. - return out +# The polygon is defined exactly as in the GeoJson standard, so +# `geojson` should be a dictionary like: +# { +# 'type': 'Polygon', +# 'coordinates': [...] +# } +# 'coordinates' should be a list of LinearRings, where the first ring describes +# the exterior boundary of the Polygon, and any subsequent LinearRings +# describe holes in the polygon. -def cell_boundary(H3int h, bool geo_json=False): - """Compose an array of geo-coordinates that outlines a hexagonal cell""" - cdef: - h3lib.GeoBoundary gb +# Note that we don't provide an option for the order of the coordinates, +# as the GeoJson standard requires them to be in lng/lat order. - check_cell(h) +# Parameters +# ---------- +# geojson : dict +# res : int +# The resolution of the output hexagons +# """ - h3lib.h3ToGeoBoundary(h, &gb) +# # todo: this one could handle multipolygons... - verts = tuple( - coord2deg(gb.verts[i]) - for i in range(gb.num_verts) - ) +# if geojson['type'] != 'Polygon': +# raise ValueError('Only Polygon GeoJSON supported') - if geo_json: - #lat/lng -> lng/lat and last point same as first - verts += (verts[0],) - verts = tuple(v[::-1] for v in verts) +# coords = geojson['coordinates'] - return verts +# out = polyfill_polygon(coords[0], res, holes=coords[1:], lnglat_order=True) +# return out -def edge_boundary(H3int edge, bool geo_json=False): - """ Returns the GeoBoundary containing the coordinates of the edge - """ - cdef: - h3lib.GeoBoundary gb - check_edge(edge) +# def polyfill(dict geojson, int res, bool geo_json_conformant=False): +# """ Light wrapper around `polyfill_geojson` to provide backward compatibility. +# """ + +# try: +# gj_type = geojson['type'] +# except KeyError: +# raise KeyError("`geojson` dict must have key 'type'.") from None + +# if gj_type != 'Polygon': +# raise ValueError('Only Polygon GeoJSON supported') + +# if geo_json_conformant: +# out = polyfill_geojson(geojson, res) +# else: +# coords = geojson['coordinates'] +# out = polyfill_polygon(coords[0], res, holes=coords[1:], lnglat_order=False) + +# return out + + +# def cell_boundary(H3int h, bool geo_json=False): +# """Compose an array of geo-coordinates that outlines a hexagonal cell""" +# cdef: +# h3lib.GeoBoundary gb + +# check_cell(h) + +# h3lib.h3ToGeoBoundary(h, &gb) + +# verts = tuple( +# coord2deg(gb.verts[i]) +# for i in range(gb.num_verts) +# ) + +# if geo_json: +# #lat/lng -> lng/lat and last point same as first +# verts += (verts[0],) +# verts = tuple(v[::-1] for v in verts) + +# return verts + + +# def edge_boundary(H3int edge, bool geo_json=False): +# """ Returns the GeoBoundary containing the coordinates of the edge +# """ +# cdef: +# h3lib.GeoBoundary gb + +# check_edge(edge) - h3lib.getH3UnidirectionalEdgeBoundary(edge, &gb) +# h3lib.getH3UnidirectionalEdgeBoundary(edge, &gb) - # todo: move this verts transform into the GeoBoundary object - verts = tuple( - coord2deg(gb.verts[i]) - for i in range(gb.num_verts) - ) +# # todo: move this verts transform into the GeoBoundary object +# verts = tuple( +# coord2deg(gb.verts[i]) +# for i in range(gb.num_verts) +# ) - if geo_json: - #lat/lng -> lng/lat and last point same as first - verts += (verts[0],) - verts = tuple(v[::-1] for v in verts) +# if geo_json: +# #lat/lng -> lng/lat and last point same as first +# verts += (verts[0],) +# verts = tuple(v[::-1] for v in verts) - return verts +# return verts -cpdef double point_dist( - double lat1, double lng1, - double lat2, double lng2, unit='km') except -1: +# cpdef double point_dist( +# double lat1, double lng1, +# double lat2, double lng2, unit='km') except -1: - a = deg2coord(lat1, lng1) - b = deg2coord(lat2, lng2) +# a = deg2coord(lat1, lng1) +# b = deg2coord(lat2, lng2) - if unit == 'rads': - d = h3lib.pointDistRads(&a, &b) - elif unit == 'km': - d = h3lib.pointDistKm(&a, &b) - elif unit == 'm': - d = h3lib.pointDistM(&a, &b) - else: - raise H3ValueError('Unknown unit: {}'.format(unit)) +# if unit == 'rads': +# d = h3lib.pointDistRads(&a, &b) +# elif unit == 'km': +# d = h3lib.pointDistKm(&a, &b) +# elif unit == 'm': +# d = h3lib.pointDistM(&a, &b) +# else: +# raise H3ValueError('Unknown unit: {}'.format(unit)) - return d +# return d diff --git a/src/h3/_cy/h3lib.pxd b/src/h3/_cy/h3lib.pxd index 5fa05cc0b..633af812a 100644 --- a/src/h3/_cy/h3lib.pxd +++ b/src/h3/_cy/h3lib.pxd @@ -3,6 +3,7 @@ from cpython cimport bool from libc.stdint cimport int64_t ctypedef stdint.uint64_t H3int +ctypedef stdint.uint32_t H3Error ctypedef basestring H3str cdef extern from "h3api.h": @@ -12,157 +13,160 @@ cdef extern from "h3api.h": ctypedef stdint.uint64_t H3Index - ctypedef struct GeoCoord: + ctypedef struct LatLng: double lat # in radians - double lng "lon" # in radians + double lng # in radians - ctypedef struct GeoBoundary: - int num_verts "numVerts" - GeoCoord verts[10] # MAX_CELL_BNDRY_VERTS + int isValidCell(H3Index h) nogil + int isPentagon(H3Index h) nogil + int isResClassIII(H3Index h) nogil + int isValidDirectedEdge(H3Index edge) nogil - ctypedef struct Geofence: - int numVerts - GeoCoord *verts - - ctypedef struct GeoPolygon: - Geofence geofence - int numHoles - Geofence *holes - - ctypedef struct GeoMultiPolygon: - int numPolygons - GeoPolygon *polygons - - ctypedef struct LinkedGeoCoord: - GeoCoord data "vertex" - LinkedGeoCoord *next - - # renaming these for clarity - ctypedef struct LinkedGeoLoop: - LinkedGeoCoord *data "first" - LinkedGeoCoord *_data_last "last" # not needed in Cython bindings - LinkedGeoLoop *next - - ctypedef struct LinkedGeoPolygon: - LinkedGeoLoop *data "first" - LinkedGeoLoop *_data_last "last" # not needed in Cython bindings - LinkedGeoPolygon *next - - ctypedef struct CoordIJ: - int i - int j - - H3Index geoToH3(const GeoCoord *g, int res) nogil - - void h3ToGeo(H3Index h3, GeoCoord *g) nogil - - void h3ToGeoBoundary(H3Index h3, GeoBoundary *gp) - - int maxKringSize(int k) - - int hexRange(H3Index origin, int k, H3Index *out) - - int hexRangeDistances(H3Index origin, int k, H3Index *out, int *distances) + double degsToRads(double degrees) nogil + double radsToDegs(double radians) nogil - int h3Distance(H3Index origin, H3Index h3) + int getResolution(H3Index h) nogil + int getBaseCellNumber(H3Index h) nogil - int hexRanges(H3Index *h3Set, int length, int k, H3Index *out) + H3Error latLngToCell(const LatLng *g, int res, H3Index *out) nogil + H3Error cellToLatLng(H3Index h, LatLng *) nogil + H3Error gridDistance(H3Index h1, H3Index h2, int64_t *distance) nogil - void kRing(H3Index origin, int k, H3Index *out) + H3Error maxGridDiskSize(int k, int64_t *out) nogil # num/out/N? + H3Error gridDisk(H3Index h, int k, H3Index *out) nogil - void kRingDistances(H3Index origin, int k, H3Index *out, int *distances) + H3Error cellToParent( H3Index h, int parentRes, H3Index *parent) nogil + H3Error cellToCenterChild(H3Index h, int childRes, H3Index *child) nogil - int hexRing(H3Index origin, int k, H3Index *out) + H3Error cellToChildrenSize(H3Index h, int childRes, int64_t *num) nogil # num/out/N? + H3Error cellToChildren( H3Index h, int childRes, H3Index *children) nogil - int maxPolyfillSize(const GeoPolygon *geoPolygon, int res) + H3Error compactCells( + const H3Index *cells_u, + H3Index *cells_c, + const int num_u + ) nogil + H3Error uncompactCellsSize( + const H3Index *cells_c, + const int64_t num_c, + const int res, + int64_t *num_u + ) nogil + H3Error uncompactCells( + const H3Index *cells_c, + const int num_c, + H3Index *cells_u, + const int num_u, + const int res + ) nogil - void polyfill(const GeoPolygon *geoPolygon, int res, H3Index *out) + H3Error getNumCells(int res, int64_t *out) - void h3SetToLinkedGeo(const H3Index *h3Set, const int numHexes, LinkedGeoPolygon *out) + # ctypedef struct GeoBoundary: + # int num_verts "numVerts" + # GeoCoord verts[10] # MAX_CELL_BNDRY_VERTS - void destroyLinkedPolygon(LinkedGeoPolygon *polygon) + # ctypedef struct Geofence: + # int numVerts + # GeoCoord *verts - double degsToRads(double degrees) nogil + # ctypedef struct GeoPolygon: + # Geofence geofence + # int numHoles + # Geofence *holes - double radsToDegs(double radians) nogil + # ctypedef struct GeoMultiPolygon: + # int numPolygons + # GeoPolygon *polygons - stdint.int64_t numHexagons(int res) + # ctypedef struct LinkedGeoCoord: + # GeoCoord data "vertex" + # LinkedGeoCoord *next - int h3GetResolution(H3Index h) nogil + # # renaming these for clarity + # ctypedef struct LinkedGeoLoop: + # LinkedGeoCoord *data "first" + # LinkedGeoCoord *_data_last "last" # not needed in Cython bindings + # LinkedGeoLoop *next - int h3GetBaseCell(H3Index h) + # ctypedef struct LinkedGeoPolygon: + # LinkedGeoLoop *data "first" + # LinkedGeoLoop *_data_last "last" # not needed in Cython bindings + # LinkedGeoPolygon *next - H3Index stringToH3(const char *str) + # ctypedef struct CoordIJ: + # int i + # int j - void h3ToString(H3Index h, char *str, size_t sz) + # void h3ToGeoBoundary(H3Index h3, GeoBoundary *gp) - int h3IsValid(H3Index h) + # int hexRange(H3Index origin, int k, H3Index *out) - H3Index h3ToParent(H3Index h, int parentRes) nogil + # int hexRangeDistances(H3Index origin, int k, H3Index *out, int *distances) - int maxH3ToChildrenSize(H3Index h, int childRes) + # int hexRanges(H3Index *h3Set, int length, int k, H3Index *out) - void h3ToChildren(H3Index h, int childRes, H3Index *children) + # void kRingDistances(H3Index origin, int k, H3Index *out, int *distances) - int compact(const H3Index *h3Set, H3Index *compactedSet, const int numHexes) + # int hexRing(H3Index origin, int k, H3Index *out) - int maxUncompactSize(const H3Index *compactedSet, const int numHexes, const int res) + # int maxPolyfillSize(const GeoPolygon *geoPolygon, int res) - int uncompact(const H3Index *compactedSet, const int numHexes, H3Index *h3Set, const int maxHexes, const int res) + # void polyfill(const GeoPolygon *geoPolygon, int res, H3Index *out) - int h3IsResClassIII(H3Index h) + # void h3SetToLinkedGeo(const H3Index *h3Set, const int numHexes, LinkedGeoPolygon *out) - int h3IsPentagon(H3Index h) + # void destroyLinkedPolygon(LinkedGeoPolygon *polygon) - int pentagonIndexCount() + # H3Index stringToH3(const char *str) - void getPentagonIndexes(int res, H3Index *out) + # void h3ToString(H3Index h, char *str, size_t sz) - int res0IndexCount() + # int pentagonIndexCount() - void getRes0Indexes(H3Index *out) + # void getPentagonIndexes(int res, H3Index *out) - H3Index h3ToCenterChild(H3Index h, int res) + # int res0IndexCount() - int h3IndexesAreNeighbors(H3Index origin, H3Index destination) + # void getRes0Indexes(H3Index *out) - H3Index getH3UnidirectionalEdge(H3Index origin, H3Index destination) + # int h3IndexesAreNeighbors(H3Index origin, H3Index destination) - int h3UnidirectionalEdgeIsValid(H3Index edge) + # H3Index getH3UnidirectionalEdge(H3Index origin, H3Index destination) - H3Index getOriginH3IndexFromUnidirectionalEdge(H3Index edge) + # H3Index getOriginH3IndexFromUnidirectionalEdge(H3Index edge) - H3Index getDestinationH3IndexFromUnidirectionalEdge(H3Index edge) + # H3Index getDestinationH3IndexFromUnidirectionalEdge(H3Index edge) - void getH3IndexesFromUnidirectionalEdge(H3Index edge, H3Index *originDestination) + # void getH3IndexesFromUnidirectionalEdge(H3Index edge, H3Index *originDestination) - void getH3UnidirectionalEdgesFromHexagon(H3Index origin, H3Index *edges) + # void getH3UnidirectionalEdgesFromHexagon(H3Index origin, H3Index *edges) - void getH3UnidirectionalEdgeBoundary(H3Index edge, GeoBoundary *gb) + # void getH3UnidirectionalEdgeBoundary(H3Index edge, GeoBoundary *gb) - int h3LineSize(H3Index start, H3Index end) - int h3Line(H3Index start, H3Index end, H3Index *out) + # int h3LineSize(H3Index start, H3Index end) + # int h3Line(H3Index start, H3Index end, H3Index *out) - int maxFaceCount(H3Index h3) - void h3GetFaces(H3Index h3, int *out) + # int maxFaceCount(H3Index h3) + # void h3GetFaces(H3Index h3, int *out) - int experimentalH3ToLocalIj(H3Index origin, H3Index h3, CoordIJ *out) - int experimentalLocalIjToH3(H3Index origin, const CoordIJ *ij, H3Index *out) + # int experimentalH3ToLocalIj(H3Index origin, H3Index h3, CoordIJ *out) + # int experimentalLocalIjToH3(H3Index origin, const CoordIJ *ij, H3Index *out) - double hexAreaKm2(int res) nogil - double hexAreaM2(int res) nogil + # double hexAreaKm2(int res) nogil + # double hexAreaM2(int res) nogil - double cellAreaRads2(H3Index h) nogil - double cellAreaKm2(H3Index h) nogil - double cellAreaM2(H3Index h) nogil + # double cellAreaRads2(H3Index h) nogil + # double cellAreaKm2(H3Index h) nogil + # double cellAreaM2(H3Index h) nogil - double edgeLengthKm(int res) nogil - double edgeLengthM(int res) nogil + # double edgeLengthKm(int res) nogil + # double edgeLengthM(int res) nogil - double exactEdgeLengthRads(H3Index edge) nogil - double exactEdgeLengthKm(H3Index edge) nogil - double exactEdgeLengthM(H3Index edge) nogil + # double exactEdgeLengthRads(H3Index edge) nogil + # double exactEdgeLengthKm(H3Index edge) nogil + # double exactEdgeLengthM(H3Index edge) nogil - double pointDistRads(const GeoCoord *a, const GeoCoord *b) nogil - double pointDistKm(const GeoCoord *a, const GeoCoord *b) nogil - double pointDistM(const GeoCoord *a, const GeoCoord *b) nogil + # double pointDistRads(const GeoCoord *a, const GeoCoord *b) nogil + # double pointDistKm(const GeoCoord *a, const GeoCoord *b) nogil + # double pointDistM(const GeoCoord *a, const GeoCoord *b) nogil diff --git a/src/h3/_cy/to_multipoly.pyx b/src/h3/_cy/to_multipoly.pyx index 8e5ca55b5..296a932f0 100644 --- a/src/h3/_cy/to_multipoly.pyx +++ b/src/h3/_cy/to_multipoly.pyx @@ -1,75 +1,75 @@ -cimport h3lib -from h3lib cimport H3int -from .util cimport check_cell, coord2deg +# cimport h3lib +# from h3lib cimport H3int +# from .util cimport check_cell, coord2deg -# todo: it's driving me crazy that these three functions are all essentially the same linked list walker... -# grumble: no way to do iterators in with cdef functions! -cdef walk_polys(const h3lib.LinkedGeoPolygon* L): - out = [] - while L: - out += [walk_loops(L.data)] - L = L.next +# # todo: it's driving me crazy that these three functions are all essentially the same linked list walker... +# # grumble: no way to do iterators in with cdef functions! +# cdef walk_polys(const h3lib.LinkedGeoPolygon* L): +# out = [] +# while L: +# out += [walk_loops(L.data)] +# L = L.next - return out +# return out -cdef walk_loops(const h3lib.LinkedGeoLoop* L): - out = [] - while L: - out += [walk_coords(L.data)] - L = L.next +# cdef walk_loops(const h3lib.LinkedGeoLoop* L): +# out = [] +# while L: +# out += [walk_coords(L.data)] +# L = L.next - return out +# return out -cdef walk_coords(const h3lib.LinkedGeoCoord* L): - out = [] - while L: - out += [coord2deg(L.data)] - L = L.next +# cdef walk_coords(const h3lib.LinkedGeoCoord* L): +# out = [] +# while L: +# out += [coord2deg(L.data)] +# L = L.next - return out +# return out -# todo: tuples instead of lists? -def _to_multi_polygon(const H3int[:] hexes): - cdef: - h3lib.LinkedGeoPolygon polygon +# # todo: tuples instead of lists? +# def _to_multi_polygon(const H3int[:] hexes): +# cdef: +# h3lib.LinkedGeoPolygon polygon - for h in hexes: - check_cell(h) +# for h in hexes: +# check_cell(h) - h3lib.h3SetToLinkedGeo(&hexes[0], len(hexes), &polygon) +# h3lib.h3SetToLinkedGeo(&hexes[0], len(hexes), &polygon) - out = walk_polys(&polygon) +# out = walk_polys(&polygon) - # we're still responsible for cleaning up the passed in `polygon`, - # but not a problem here, since it is stack allocated - h3lib.destroyLinkedPolygon(&polygon) +# # we're still responsible for cleaning up the passed in `polygon`, +# # but not a problem here, since it is stack allocated +# h3lib.destroyLinkedPolygon(&polygon) - return out +# return out -def _geojson_loop(loop): - """ Swap lat/lng order and close loop. - """ - loop = [e[::-1] for e in loop] - loop += [loop[0]] +# def _geojson_loop(loop): +# """ Swap lat/lng order and close loop. +# """ +# loop = [e[::-1] for e in loop] +# loop += [loop[0]] - return loop +# return loop -def h3_set_to_multi_polygon(const H3int[:] hexes, geo_json=False): - # todo: gotta be a more elegant way to handle these... - if len(hexes) == 0: - return [] +# def h3_set_to_multi_polygon(const H3int[:] hexes, geo_json=False): +# # todo: gotta be a more elegant way to handle these... +# if len(hexes) == 0: +# return [] - multipoly = _to_multi_polygon(hexes) +# multipoly = _to_multi_polygon(hexes) - if geo_json: - multipoly = [ - [_geojson_loop(loop) for loop in poly] - for poly in multipoly - ] +# if geo_json: +# multipoly = [ +# [_geojson_loop(loop) for loop in poly] +# for poly in multipoly +# ] - return multipoly +# return multipoly diff --git a/src/h3/_cy/unstable_vect.pyx b/src/h3/_cy/unstable_vect.pyx deleted file mode 100644 index 316ac12c8..000000000 --- a/src/h3/_cy/unstable_vect.pyx +++ /dev/null @@ -1,89 +0,0 @@ -cimport h3lib -from h3lib cimport H3int -from .util cimport deg2coord - -from cython cimport boundscheck, wraparound -from libc.math cimport sqrt, sin, cos, asin - -cdef double haversineDistance(double th1, double ph1, double th2, double ph2) nogil: - cdef: - double dx, dy, dz - double R = 6371.0088 - - ph1 -= ph2 - - dz = sin(th1) - sin(th2) - dx = cos(ph1) * cos(th1) - cos(th2) - dy = sin(ph1) * cos(th1) - - return asin(sqrt(dx*dx + dy*dy + dz*dz) / 2)*2*R - - -@boundscheck(False) -@wraparound(False) -cpdef void haversine_vect( - const H3int[:] a, - const H3int[:] b, - double[:] out -) nogil: - - cdef h3lib.GeoCoord p1, p2 - - with nogil: - # todo: add these back in when cython 3.0 comes out - #assert len(a) == len(b) - #assert len(a) <= len(out) - - for i in range(len(a)): - h3lib.h3ToGeo(a[i], &p1) - h3lib.h3ToGeo(b[i], &p2) - out[i] = haversineDistance( - p1.lat, p1.lng, - p2.lat, p2.lng - ) - - -@boundscheck(False) -@wraparound(False) -cpdef void geo_to_h3_vect( - const double[:] lat, - const double[:] lng, - int res, - H3int[:] out -) nogil: - - cdef h3lib.GeoCoord c - - with nogil: - for i in range(len(lat)): - c = deg2coord(lat[i], lng[i]) - out[i] = h3lib.geoToH3(&c, res) - - -@boundscheck(False) -@wraparound(False) -cpdef void h3_to_parent_vect( - const H3int[:] h, - int[:] res, - H3int[:] out -) nogil: - - cdef Py_ssize_t i - - with nogil: - for i in range(len(h)): - out[i] = h3lib.h3ToParent(h[i], res[i]) - - -@boundscheck(False) -@wraparound(False) -cpdef void h3_get_resolution_vect( - const H3int[:] h, - int[:] out, -) nogil: - - cdef Py_ssize_t i - - with nogil: - for i in range(len(h)): - out[i] = h3lib.h3GetResolution(h[i]) diff --git a/src/h3/_cy/util.pxd b/src/h3/_cy/util.pxd index 06cc26a73..6a34b751e 100644 --- a/src/h3/_cy/util.pxd +++ b/src/h3/_cy/util.pxd @@ -1,7 +1,7 @@ -from .h3lib cimport H3int, H3str, GeoCoord +from .h3lib cimport H3int, H3str, LatLng -cdef GeoCoord deg2coord(double lat, double lng) nogil -cdef (double, double) coord2deg(GeoCoord c) nogil +cdef LatLng deg2coord(double lat, double lng) nogil +cdef (double, double) coord2deg(LatLng c) nogil cpdef H3int hex2int(H3str h) except? 0 cpdef H3str int2hex(H3int x) diff --git a/src/h3/_cy/util.pyx b/src/h3/_cy/util.pyx index dbf17daac..bbb63e4fb 100644 --- a/src/h3/_cy/util.pyx +++ b/src/h3/_cy/util.pyx @@ -1,13 +1,13 @@ from libc cimport stdlib from cython.view cimport array -from .h3lib cimport H3int, H3str, h3IsValid, h3UnidirectionalEdgeIsValid +from .h3lib cimport H3int, H3str, isValidCell, isValidDirectedEdge cimport h3lib -cdef h3lib.GeoCoord deg2coord(double lat, double lng) nogil: +cdef h3lib.LatLng deg2coord(double lat, double lng) nogil: cdef: - h3lib.GeoCoord c + h3lib.LatLng c c.lat = h3lib.degsToRads(lat) c.lng = h3lib.degsToRads(lng) @@ -15,7 +15,7 @@ cdef h3lib.GeoCoord deg2coord(double lat, double lng) nogil: return c -cdef (double, double) coord2deg(h3lib.GeoCoord c) nogil: +cdef (double, double) coord2deg(h3lib.LatLng c) nogil: return ( h3lib.radsToDegs(c.lat), h3lib.radsToDegs(c.lng) @@ -77,11 +77,11 @@ cdef check_cell(H3int h): is incorrect, but in a format that is easily compared to `str` inputs. """ - if h3IsValid(h) == 0: + if isValidCell(h) == 0: raise H3CellError('Integer is not a valid H3 cell: {}'.format(hex(h))) cdef check_edge(H3int e): - if h3UnidirectionalEdgeIsValid(e) == 0: + if isValidDirectedEdge(e) == 0: raise H3EdgeError('Integer is not a valid H3 edge: {}'.format(hex(e))) cdef check_res(int res): diff --git a/src/h3/_version.py b/src/h3/_version.py index eb5cae01f..41540d5ef 100644 --- a/src/h3/_version.py +++ b/src/h3/_version.py @@ -1,24 +1,25 @@ -__version__ = '3.7.4' +__version__ = '4.0.0' __description__ = 'Hierarchical hexagonal geospatial indexing system' __url__ = 'https://github.com/uber/h3-py' -__license__ = "Apache 2.0 License" +__license__ = 'Apache 2.0 License' __author__ = 'Uber Technologies' __author_email__ = 'AJ Friend ' __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 :: 2.7", - "Programming Language :: Python :: 3.5", - "Programming Language :: Python :: 3.6", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Operating System :: MacOS :: MacOS X", - "Operating System :: POSIX :: Linux", - "Operating System :: Microsoft :: Windows", - "Topic :: Scientific/Engineering :: GIS", + '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 :: 2.7', + 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', + 'Programming Language :: Python :: 3.10', + 'Operating System :: MacOS :: MacOS X', + 'Operating System :: POSIX :: Linux', + 'Operating System :: Microsoft :: Windows', + 'Topic :: Scientific/Engineering :: GIS', ] diff --git a/src/h3/unstable/__init__.py b/src/h3/unstable/__init__.py deleted file mode 100644 index 5c1839c43..000000000 --- a/src/h3/unstable/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -# flake8: noqa - -import warnings -warnings.warn( - 'Modules under `h3.unstable` are experimental, and may change at any time.' -) - -from . import v4 -from . import vect diff --git a/src/h3/unstable/v4.py b/src/h3/unstable/v4.py deleted file mode 100644 index 833cd6e15..000000000 --- a/src/h3/unstable/v4.py +++ /dev/null @@ -1,125 +0,0 @@ -# flake8: noqa - -from ..api.basic_str import ( - # todo: implement is_valid_index - - compact as - compact_cells, - - edge_length as - get_hexagon_edge_length_avg, - - geo_to_h3 as - point_to_cell, - - get_destination_h3_index_from_unidirectional_edge as - get_directed_edge_destination, - - get_h3_indexes_from_unidirectional_edge as - directed_edge_to_cells, - - get_h3_unidirectional_edge as - cells_to_directed_edge, - - get_h3_unidirectional_edge_boundary as - directed_edge_to_boundary, - - get_h3_unidirectional_edges_from_hexagon as - origin_to_directed_edges, - - get_origin_h3_index_from_unidirectional_edge as - get_directed_edge_origin, - - get_pentagon_indexes as - get_pentagons, - - get_res0_indexes as - get_res0_cells, - - h3_distance as - grid_distance, - - h3_get_base_cell as - get_base_cell_number, - - h3_get_faces as - get_icosahedron_faces, - - h3_get_resolution as - get_resolution, - - h3_indexes_are_neighbors as - are_neighbor_cells, - - h3_is_pentagon as - is_pentagon, - - h3_is_res_class_III as - is_res_class_III, - - h3_is_valid as - is_valid_cell, - - h3_line as - grid_path_cells, - - h3_set_to_multi_polygon as - cells_to_multipolygon, - - h3_to_center_child as - cell_to_center_child, - - h3_to_children as - cell_to_children, - - h3_to_geo as - cell_to_point, - - h3_to_geo_boundary as - cell_to_boundary, - - h3_to_parent as - cell_to_parent, - - h3_to_string as - int_to_str, - - h3_unidirectional_edge_is_valid as - is_valid_directed_edge, - - hex_area as - get_hexagon_area_avg, - - # hex_range as _, - # hex_range_distances as _, # not sure we want this one; easy for user to implement - # hex_ranges as _, - - hex_ring as - grid_ring, - - k_ring as - grid_disk, - - k_ring_distances as - grid_disk_distances, - - num_hexagons as - get_num_cells, - - polyfill as - polygon_to_cells, - - polyfill_geojson as - geojson_to_cells, # still need to figure out the final polyfill interface - - #polyfill_polygon as _, - - string_to_h3 as - str_to_int, - - uncompact as - uncompact_cells, - - versions as - versions, -) diff --git a/src/h3/unstable/vect.py b/src/h3/unstable/vect.py deleted file mode 100644 index 122c07d74..000000000 --- a/src/h3/unstable/vect.py +++ /dev/null @@ -1,105 +0,0 @@ -from .._cy import unstable_vect as _vect - -import numpy as np - - -def h3_to_parent(h, res=None): - """ - Get parent of arrays of cells. - - Parameters - ---------- - h : array of H3Cells - res: int or None, optional - The resolution for the parent - If `None`, then `res = resolution(h) - 1` - - Returns - ------- - array of H3Cells - """ - h = np.array(h, dtype=np.uint64) - out = np.zeros(len(h), dtype=np.uint64) - - if res is None: - res = h3_get_resolution(h) - 1 - else: - res = np.full(h.shape, res, dtype=np.intc) - - _vect.h3_to_parent_vect(h, res, out) - - return out - - -def h3_get_resolution(h): - """ - Return the resolution of an array of H3 cells. - - Parameters - ---------- - h : H3Cell - - Returns - ------- - array of int - """ - h = np.array(h, dtype=np.uint64) - out = np.zeros(len(h), dtype=np.intc) - - _vect.h3_get_resolution_vect(h, out) - - return out - - -def geo_to_h3(lats, lngs, res): - """ - Convert arrays describing lat/lng pairs to cells. - - Parameters - ---------- - lats, lngs : arrays of floats - - res: int - Resolution for output cells. - - Returns - ------- - array of H3Cells - """ - lats = np.array(lats, dtype=np.float64) - lngs = np.array(lngs, dtype=np.float64) - - assert len(lats) == len(lngs) - - out = np.zeros(len(lats), dtype='uint64') - - _vect.geo_to_h3_vect(lats, lngs, res, out) - - return out - - -def cell_haversine(a, b): - """ - Compute haversine distance between the centers of cells given in - arrays a and b. - - - Parameters - ---------- - a, b : arrays of H3Cell - - Returns - ------- - float - Haversine distance in kilometers - """ - a = np.array(a, dtype=np.uint64) - b = np.array(b, dtype=np.uint64) - - assert len(a) == len(b) - - out = np.zeros(len(a), dtype='double') - - _vect.haversine_vect(a, b, out) - - return out diff --git a/src/h3lib b/src/h3lib index ee292a969..294c20ed7 160000 --- a/src/h3lib +++ b/src/h3lib @@ -1 +1 @@ -Subproject commit ee292a96906767162f2f530b46fc2428b2555748 +Subproject commit 294c20ed7d447122d5ab37260896d4ef7579ebfb diff --git a/tests/cython_example.pyx b/tests/cython_example.pyx index e8d285947..c19fe21ec 100644 --- a/tests/cython_example.pyx +++ b/tests/cython_example.pyx @@ -1,18 +1,18 @@ -from cython cimport boundscheck, wraparound +# from cython cimport boundscheck, wraparound -from h3._cy.h3lib cimport H3int -from h3._cy.geo cimport geo_to_h3 +# from h3._cy.h3lib cimport H3int +# from h3._cy.geo cimport geo_to_h3 -@boundscheck(False) -@wraparound(False) -cpdef void geo_to_h3_vect( - const double[:] lat, - const double[:] lng, - int res, - H3int[:] out -): +# @boundscheck(False) +# @wraparound(False) +# cpdef void geo_to_h3_vect( +# const double[:] lat, +# const double[:] lng, +# int res, +# H3int[:] out +# ): - cdef Py_ssize_t i +# cdef Py_ssize_t i - for i in range(len(lat)): - out[i] = geo_to_h3(lat[i], lng[i], res) +# for i in range(len(lat)): +# out[i] = geo_to_h3(lat[i], lng[i], res) diff --git a/tests/h3fake2_errors.py b/tests/h3fake2_errors.py new file mode 100644 index 000000000..da5688730 --- /dev/null +++ b/tests/h3fake2_errors.py @@ -0,0 +1,15 @@ +# flake8: noqa +""" +Some of the tests expect to catch certain errors. +As we transition, we'll have errors of each type. +pytest functions can accept tuples of errors to check for either one. +""" + +import h3 +import h3fake2 + +H3ValueError = (h3.H3ValueError, h3fake2.H3ValueError) +H3CellError = (h3.H3CellError, h3fake2.H3CellError) +H3ResolutionError = (h3.H3ResolutionError, h3fake2.H3ResolutionError) +H3EdgeError = (h3.H3EdgeError, h3fake2.H3EdgeError) +H3DistanceError = (h3.H3DistanceError, h3fake2.H3DistanceError) diff --git a/tests/test_cells_and_edges.py b/tests/test_cells_and_edges.py index 173033ac0..014f5193e 100644 --- a/tests/test_cells_and_edges.py +++ b/tests/test_cells_and_edges.py @@ -1,7 +1,7 @@ import h3 import pytest -from h3 import ( +from .h3fake2_errors import ( H3ValueError, H3CellError, H3ResolutionError, diff --git a/tests/test_cython.py b/tests/test_cython.py index 30566b188..0acf3073f 100644 --- a/tests/test_cython.py +++ b/tests/test_cython.py @@ -1,28 +1,28 @@ -import numpy as np +# import numpy as np -# Avoid checking for import-error here because cython_example may not have -# been compiled yet. -try: - from .cython_example import geo_to_h3_vect # pylint: disable=import-error -except ImportError: - geo_to_h3_vect = None +# # Avoid checking for import-error here because cython_example may not have +# # been compiled yet. +# try: +# from .cython_example import geo_to_h3_vect # pylint: disable=import-error +# except ImportError: +# geo_to_h3_vect = None -np.random.seed(0) +# np.random.seed(0) -def test_cython_api(): - if geo_to_h3_vect is None: - print("Not running Cython test because cython example was not compiled") - return +# def test_cython_api(): +# if geo_to_h3_vect is None: +# print("Not running Cython test because cython example was not compiled") +# return - N = 100000 +# N = 100000 - lats, lngs = np.random.uniform(0, 90, N), np.random.uniform(0, 90, N) - res = 9 +# 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") - geo_to_h3_vect(lats, lngs, res, out) +# lats = np.array(lats, dtype=np.float64) +# lngs = np.array(lngs, dtype=np.float64) +# out = np.zeros(len(lats), dtype="uint64") +# geo_to_h3_vect(lats, lngs, res, out) - assert out[0] == 617284541015654399 +# assert out[0] == 617284541015654399 diff --git a/tests/test_length_area.py b/tests/test_length_area.py index 8236199d6..2a1b5d7de 100644 --- a/tests/test_length_area.py +++ b/tests/test_length_area.py @@ -1,7 +1,7 @@ import h3 import pytest -from h3 import H3ValueError +from .h3fake2_errors import H3ValueError def approx2(a, b): diff --git a/tests/test_polyfill.py b/tests/test_polyfill.py index 4739009be..a02560d1e 100644 --- a/tests/test_polyfill.py +++ b/tests/test_polyfill.py @@ -2,6 +2,8 @@ import itertools import pytest +from .h3fake2_errors import H3ResolutionError + def reverse(loop): return list(reversed(loop)) @@ -209,10 +211,10 @@ def test_resolution(): 'coordinates': [[]], } - with pytest.raises(h3.H3ResolutionError): + with pytest.raises(H3ResolutionError): h3.polyfill(d, -1) - with pytest.raises(h3.H3ResolutionError): + with pytest.raises(H3ResolutionError): h3.polyfill(d, 16) diff --git a/tests/test_unstable_vect.py b/tests/test_unstable_vect.py deleted file mode 100644 index a556aae66..000000000 --- a/tests/test_unstable_vect.py +++ /dev/null @@ -1,69 +0,0 @@ -import h3.api.numpy_int as h3 -import numpy as np # only run this test suite if numpy is installed -import pytest - -with pytest.warns( - UserWarning, - match = 'Modules under `h3.unstable` are experimental', -): - import h3.unstable.vect as h3_vect - - -def test_h3_to_parent(): - # At res 9 - h = np.array([617700169958555647, 617700169958555647], np.uint64) - - # Default to res - 1 - arr1 = h3_vect.h3_to_parent(h) - arr2 = h3_vect.h3_to_parent(h, 8) - assert np.array_equal(arr1, arr2) - - # Same as other h3 bindings - arr1 = h3_vect.h3_to_parent(h) - arr2 = np.array(list(map(h3.h3_to_parent, h)), dtype=np.uint64) - assert np.array_equal(arr1, arr2) - - # Test with a number passed to res - arr1 = h3_vect.h3_to_parent(h, 7) - arr2 = np.array([h3.h3_to_parent(c, 7) for c in h], dtype=np.uint64) - assert np.array_equal(arr1, arr2) - - # Test with an array passed to res - arr1 = h3_vect.h3_to_parent(h, np.array([7, 5], np.intc)) - arr2 = h3_vect.h3_to_parent(h, [7, 5]) - - arr3 = [] - for c, res in zip(h, [7, 5]): - arr3.append(h3.h3_to_parent(c, res)) - - assert np.array_equal(arr1, arr2) - assert np.array_equal(arr1, np.array(arr3, dtype=np.uint64)) - - # Test with array-like (but not np.array) cell input - arr1 = h3_vect.h3_to_parent(list(h)) - arr2 = np.array(list(map(h3.h3_to_parent, h)), dtype=np.uint64) - assert np.array_equal(arr1, arr2) - - -def test_h3_to_parent_multiple_res(): - h = np.array([617700169958555647, 613196570331971583], np.uint64) - - # Cells at res 9, 8 - assert list(h3_vect.h3_get_resolution(h)) == [9, 8] - - # Same as other h3 bindings - arr1 = h3_vect.h3_to_parent(h) - arr2 = np.array(list(map(h3.h3_to_parent, h)), dtype=np.uint64) - assert np.array_equal(arr1, arr2) - - # Parent cells are 8, 7 - parents = h3_vect.h3_to_parent(h) - assert list(h3_vect.h3_get_resolution(parents)) == [8, 7] - - -def test_h3_get_resolution(): - h = np.array([617700169958555647], np.uint64) - - arr1 = h3_vect.h3_get_resolution(h) - arr2 = np.array(list(map(h3.h3_get_resolution, h)), dtype=np.intc) - assert np.array_equal(arr1, arr2) From 4ec0a92f109821ad338f701542ab490f585993af Mon Sep 17 00:00:00 2001 From: AJ Friend Date: Tue, 24 May 2022 20:18:44 -0700 Subject: [PATCH 02/21] ajfriend/more v4 funcs (#241) * get_pentagon_indexes and get_res0_indexes * line * cell area functions --- src/h3/_cy/__init__.py | 15 +++-- src/h3/_cy/cells.pxd | 10 +-- src/h3/_cy/cells.pyx | 123 +++++++++++++++++++--------------- src/h3/_cy/h3lib.pxd | 34 +++++----- tests/test_cells_and_edges.py | 10 +-- 5 files changed, 105 insertions(+), 87 deletions(-) diff --git a/src/h3/_cy/__init__.py b/src/h3/_cy/__init__.py index 6b17b2d08..cfe34c742 100644 --- a/src/h3/_cy/__init__.py +++ b/src/h3/_cy/__init__.py @@ -27,6 +27,11 @@ compact, uncompact, num_hexagons, + get_pentagon_indexes, + get_res0_indexes, + line, + mean_hex_area, + cell_area, ) from h3fake2._cy import ( @@ -42,12 +47,12 @@ # compact, # uncompact, # num_hexagons, - mean_hex_area, - cell_area, - line, + # mean_hex_area, + # cell_area, + # line, # is_res_class_iii, - get_pentagon_indexes, - get_res0_indexes, + # get_pentagon_indexes, + # get_res0_indexes, # center_child, get_faces, experimental_h3_to_local_ij, diff --git a/src/h3/_cy/cells.pxd b/src/h3/_cy/cells.pxd index 4e2d4411d..0dd3aa60d 100644 --- a/src/h3/_cy/cells.pxd +++ b/src/h3/_cy/cells.pxd @@ -14,12 +14,12 @@ cpdef H3int center_child(H3int h, res=*) except 0 cpdef H3int[:] compact(const H3int[:] hu) cpdef H3int[:] uncompact(const H3int[:] hc, int res) cpdef int64_t num_hexagons(int resolution) except -1 -# cpdef double mean_hex_area(int resolution, unit=*) except -1 -# cpdef double cell_area(H3int h, unit=*) except -1 -# cpdef H3int[:] line(H3int start, H3int end) +cpdef double mean_hex_area(int resolution, unit=*) except -1 +cpdef double cell_area(H3int h, unit=*) except -1 +cpdef H3int[:] line(H3int start, H3int end) cpdef bool is_res_class_iii(H3int h) -# cpdef H3int[:] get_pentagon_indexes(int res) -# cpdef H3int[:] get_res0_indexes() +cpdef H3int[:] get_pentagon_indexes(int res) +cpdef H3int[:] get_res0_indexes() # cpdef get_faces(H3int h) # cpdef (int, int) experimental_h3_to_local_ij(H3int origin, H3int h) except * # cpdef H3int experimental_local_ij_to_h3(H3int origin, int i, int j) except 0 diff --git a/src/h3/_cy/cells.pyx b/src/h3/_cy/cells.pyx index 265e4c377..9cfcbb241 100644 --- a/src/h3/_cy/cells.pyx +++ b/src/h3/_cy/cells.pyx @@ -281,87 +281,104 @@ cpdef int64_t num_hexagons(int resolution) except -1: return num_cells -# cpdef double mean_hex_area(int resolution, unit='km^2') except -1: -# check_res(resolution) +cpdef double mean_hex_area(int resolution, unit='km^2') except -1: + cdef: + h3lib.H3Error err + double area -# area = h3lib.hexAreaKm2(resolution) + check_res(resolution) -# # todo: multiple units -# convert = { -# 'km^2': 1.0, -# 'm^2': 1000*1000.0 -# } + err = h3lib.getHexagonAreaAvgKm2(resolution, &area) -# try: -# area *= convert[unit] -# except: -# raise H3ValueError('Unknown unit: {}'.format(unit)) + # todo: multiple units + convert = { + 'km^2': 1.0, + 'm^2': 1000*1000.0 + } -# return area + try: + area *= convert[unit] + except: + raise H3ValueError('Unknown unit: {}'.format(unit)) + return area -# cpdef double cell_area(H3int h, unit='km^2') except -1: -# check_cell(h) -# if unit == 'rads^2': -# area = h3lib.cellAreaRads2(h) -# elif unit == 'km^2': -# area = h3lib.cellAreaKm2(h) -# elif unit == 'm^2': -# area = h3lib.cellAreaM2(h) -# else: -# raise H3ValueError('Unknown unit: {}'.format(unit)) +cpdef double cell_area(H3int h, unit='km^2') except -1: + cdef: + h3lib.H3Error err + double area -# return area + check_cell(h) + 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 H3ValueError('Unknown unit: {}'.format(unit)) -# cpdef H3int[:] line(H3int start, H3int end): -# check_cell(start) -# check_cell(end) + return area -# n = h3lib.h3LineSize(start, end) -# if n < 0: -# s = "Couldn't find line between cells {} and {}" -# s = s.format(hex(start), hex(end)) -# raise H3ValueError(s) +cpdef H3int[:] line(H3int start, H3int end): + cdef: + h3lib.H3Error err + int64_t n -# ptr = create_ptr(n) -# flag = h3lib.h3Line(start, end, ptr) -# mv = create_mv(ptr, n) + check_cell(start) + check_cell(end) -# if flag != 0: -# s = "Couldn't find line between cells {} and {}" -# s = s.format(hex(start), hex(end)) -# raise H3ValueError(s) + err = h3lib.gridPathCellsSize(start, end, &n) -# return mv + if err: + s = "Couldn't find line between cells {} and {}" + s = s.format(hex(start), hex(end)) + raise H3ValueError(s) + + ptr = create_ptr(n) + err = h3lib.gridPathCells(start, end, ptr) + mv = create_mv(ptr, n) + + if err: + s = "Couldn't find line between cells {} and {}" + s = s.format(hex(start), hex(end)) + raise H3ValueError(s) + + return mv cpdef bool is_res_class_iii(H3int h): return h3lib.isResClassIII(h) == 1 -# cpdef H3int[:] get_pentagon_indexes(int res): -# check_res(res) +cpdef H3int[:] get_pentagon_indexes(int res): + cdef: + h3lib.H3Error err -# n = h3lib.pentagonIndexCount() + check_res(res) -# ptr = create_ptr(n) -# h3lib.getPentagonIndexes(res, ptr) -# mv = create_mv(ptr, n) + n = h3lib.pentagonCount() -# return mv + ptr = create_ptr(n) + err = h3lib.getPentagons(res, ptr) + mv = create_mv(ptr, n) + return mv -# cpdef H3int[:] get_res0_indexes(): -# n = h3lib.res0IndexCount() -# ptr = create_ptr(n) -# h3lib.getRes0Indexes(ptr) -# mv = create_mv(ptr, n) +cpdef H3int[:] get_res0_indexes(): + cdef: + h3lib.H3Error err -# return mv + n = h3lib.res0CellCount() + ptr = create_ptr(n) + err = h3lib.getRes0Cells(ptr) + mv = create_mv(ptr, n) + + return mv # cpdef get_faces(H3int h): # check_cell(h) diff --git a/src/h3/_cy/h3lib.pxd b/src/h3/_cy/h3lib.pxd index 633af812a..2061b43ff 100644 --- a/src/h3/_cy/h3lib.pxd +++ b/src/h3/_cy/h3lib.pxd @@ -60,7 +60,21 @@ cdef extern from "h3api.h": const int res ) nogil - H3Error getNumCells(int res, int64_t *out) + H3Error getNumCells(int res, int64_t *out) nogil + int pentagonCount() nogil + int res0CellCount() nogil + H3Error getPentagons(int res, H3Index *out) nogil + H3Error getRes0Cells(H3Index *out) nogil + + H3Error gridPathCellsSize(H3Index start, H3Index end, int64_t *size) nogil + H3Error gridPathCells(H3Index start, H3Index end, H3Index *out) nogil + + H3Error getHexagonAreaAvgKm2(int res, double *out) nogil + H3Error getHexagonAreaAvgM2(int res, double *out) nogil + + H3Error cellAreaRads2(H3Index h, double *out) nogil + H3Error cellAreaKm2(H3Index h, double *out) nogil + H3Error cellAreaM2(H3Index h, double *out) nogil # ctypedef struct GeoBoundary: # int num_verts "numVerts" @@ -122,14 +136,6 @@ cdef extern from "h3api.h": # void h3ToString(H3Index h, char *str, size_t sz) - # int pentagonIndexCount() - - # void getPentagonIndexes(int res, H3Index *out) - - # int res0IndexCount() - - # void getRes0Indexes(H3Index *out) - # int h3IndexesAreNeighbors(H3Index origin, H3Index destination) # H3Index getH3UnidirectionalEdge(H3Index origin, H3Index destination) @@ -144,22 +150,12 @@ cdef extern from "h3api.h": # void getH3UnidirectionalEdgeBoundary(H3Index edge, GeoBoundary *gb) - # int h3LineSize(H3Index start, H3Index end) - # int h3Line(H3Index start, H3Index end, H3Index *out) - # int maxFaceCount(H3Index h3) # void h3GetFaces(H3Index h3, int *out) # int experimentalH3ToLocalIj(H3Index origin, H3Index h3, CoordIJ *out) # int experimentalLocalIjToH3(H3Index origin, const CoordIJ *ij, H3Index *out) - # double hexAreaKm2(int res) nogil - # double hexAreaM2(int res) nogil - - # double cellAreaRads2(H3Index h) nogil - # double cellAreaKm2(H3Index h) nogil - # double cellAreaM2(H3Index h) nogil - # double edgeLengthKm(int res) nogil # double edgeLengthM(int res) nogil diff --git a/tests/test_cells_and_edges.py b/tests/test_cells_and_edges.py index 014f5193e..851dda58b 100644 --- a/tests/test_cells_and_edges.py +++ b/tests/test_cells_and_edges.py @@ -269,11 +269,11 @@ def test_num_hexagons(): def test_hex_area(): expected_in_km2 = { - 0: 4250546.848, - 1: 607220.9782, - 2: 86745.85403, - 9: 0.1053325, - 15: 9e-07, + 0: 4357449.416078381, + 1: 609788.441794133, + 2: 86801.780398997, + 9: 0.105332513, + 15: 8.95311e-07, } out = { From f2253cfb30e8b3925f0b3df29e176c8508884a5a Mon Sep 17 00:00:00 2001 From: AJ Friend Date: Wed, 25 May 2022 11:14:55 -0700 Subject: [PATCH 03/21] get_faces (#248) * initial pass * thoughts * the results of thinking * this commit is associated with the previous commits * nogil * i'm a bill * remove the typical nonsense --- src/h3/_cy/__init__.py | 3 ++- src/h3/_cy/cells.pxd | 2 +- src/h3/_cy/cells.pyx | 26 +++++++++++++++----------- src/h3/_cy/h3lib.pxd | 6 +++--- 4 files changed, 21 insertions(+), 16 deletions(-) diff --git a/src/h3/_cy/__init__.py b/src/h3/_cy/__init__.py index cfe34c742..d8460bf78 100644 --- a/src/h3/_cy/__init__.py +++ b/src/h3/_cy/__init__.py @@ -32,6 +32,7 @@ line, mean_hex_area, cell_area, + get_faces, ) from h3fake2._cy import ( @@ -54,7 +55,7 @@ # get_pentagon_indexes, # get_res0_indexes, # center_child, - get_faces, + # get_faces, experimental_h3_to_local_ij, experimental_local_ij_to_h3, ) diff --git a/src/h3/_cy/cells.pxd b/src/h3/_cy/cells.pxd index 0dd3aa60d..8ca4313b7 100644 --- a/src/h3/_cy/cells.pxd +++ b/src/h3/_cy/cells.pxd @@ -20,6 +20,6 @@ cpdef H3int[:] line(H3int start, H3int end) cpdef bool is_res_class_iii(H3int h) cpdef H3int[:] get_pentagon_indexes(int res) cpdef H3int[:] get_res0_indexes() -# cpdef get_faces(H3int h) +cpdef get_faces(H3int h) # cpdef (int, int) experimental_h3_to_local_ij(H3int origin, H3int h) except * # cpdef H3int experimental_local_ij_to_h3(H3int origin, int i, int j) except 0 diff --git a/src/h3/_cy/cells.pyx b/src/h3/_cy/cells.pyx index 9cfcbb241..fdc2e7e83 100644 --- a/src/h3/_cy/cells.pyx +++ b/src/h3/_cy/cells.pyx @@ -380,22 +380,26 @@ cpdef H3int[:] get_res0_indexes(): return mv -# cpdef get_faces(H3int h): -# check_cell(h) +cpdef get_faces(H3int h): + cdef: + h3lib.H3Error err + int n -# n = h3lib.maxFaceCount(h) + check_cell(h) -# cdef int* ptr = stdlib.calloc(n, sizeof(int)) -# if (n > 0) and (not ptr): -# raise MemoryError() + err = h3lib.maxFaceCount(h, &n) #ignore error for now + + cdef int* ptr = stdlib.calloc(n, sizeof(int)) + if (n > 0) and (not ptr): + raise MemoryError() -# h3lib.h3GetFaces(h, ptr) + err = h3lib.getIcosahedronFaces(h, ptr) # handle error? -# faces = ptr -# faces = {f for f in faces if f >= 0} -# stdlib.free(ptr) + faces = ptr + faces = {f for f in faces if f >= 0} + stdlib.free(ptr) -# return faces + return faces # cpdef (int, int) experimental_h3_to_local_ij(H3int origin, H3int h) except *: diff --git a/src/h3/_cy/h3lib.pxd b/src/h3/_cy/h3lib.pxd index 2061b43ff..a5aeb779f 100644 --- a/src/h3/_cy/h3lib.pxd +++ b/src/h3/_cy/h3lib.pxd @@ -76,6 +76,9 @@ cdef extern from "h3api.h": H3Error cellAreaKm2(H3Index h, double *out) nogil H3Error cellAreaM2(H3Index h, double *out) nogil + H3Error maxFaceCount(H3Index h, int *out) nogil + H3Error getIcosahedronFaces(H3Index h3, int *out) nogil + # ctypedef struct GeoBoundary: # int num_verts "numVerts" # GeoCoord verts[10] # MAX_CELL_BNDRY_VERTS @@ -150,9 +153,6 @@ cdef extern from "h3api.h": # void getH3UnidirectionalEdgeBoundary(H3Index edge, GeoBoundary *gb) - # int maxFaceCount(H3Index h3) - # void h3GetFaces(H3Index h3, int *out) - # int experimentalH3ToLocalIj(H3Index origin, H3Index h3, CoordIJ *out) # int experimentalLocalIjToH3(H3Index origin, const CoordIJ *ij, H3Index *out) From 7abaf49b195cea067e10e982b255d7996bc6ba53 Mon Sep 17 00:00:00 2001 From: Isaac Brodsky Date: Wed, 25 May 2022 17:09:36 -0700 Subject: [PATCH 04/21] v4: local ij functions to v4 cython (#240) * move local ij functions to v4 implementation * revert some changes and add gridPathCells * swap implementation * revert gridPathCells --- src/h3/_cy/__init__.py | 6 +++-- src/h3/_cy/cells.pxd | 4 ++-- src/h3/_cy/cells.pyx | 51 +++++++++++++++++++++--------------------- src/h3/_cy/h3lib.pxd | 23 +++++++++---------- 4 files changed, 42 insertions(+), 42 deletions(-) diff --git a/src/h3/_cy/__init__.py b/src/h3/_cy/__init__.py index d8460bf78..d378be877 100644 --- a/src/h3/_cy/__init__.py +++ b/src/h3/_cy/__init__.py @@ -33,6 +33,8 @@ mean_hex_area, cell_area, get_faces, + experimental_h3_to_local_ij, + experimental_local_ij_to_h3, ) from h3fake2._cy import ( @@ -56,8 +58,8 @@ # get_res0_indexes, # center_child, # get_faces, - experimental_h3_to_local_ij, - experimental_local_ij_to_h3, + # experimental_h3_to_local_ij, + # experimental_local_ij_to_h3, ) # from .edges import ( diff --git a/src/h3/_cy/cells.pxd b/src/h3/_cy/cells.pxd index 8ca4313b7..ee6c2a7cf 100644 --- a/src/h3/_cy/cells.pxd +++ b/src/h3/_cy/cells.pxd @@ -21,5 +21,5 @@ cpdef bool is_res_class_iii(H3int h) cpdef H3int[:] get_pentagon_indexes(int res) cpdef H3int[:] get_res0_indexes() cpdef get_faces(H3int h) -# cpdef (int, int) experimental_h3_to_local_ij(H3int origin, H3int h) except * -# cpdef H3int experimental_local_ij_to_h3(H3int origin, int i, int j) except 0 +cpdef (int, int) experimental_h3_to_local_ij(H3int origin, H3int h) except * +cpdef H3int experimental_local_ij_to_h3(H3int origin, int i, int j) except 0 diff --git a/src/h3/_cy/cells.pyx b/src/h3/_cy/cells.pyx index fdc2e7e83..0bd99bd49 100644 --- a/src/h3/_cy/cells.pyx +++ b/src/h3/_cy/cells.pyx @@ -402,39 +402,38 @@ cpdef get_faces(H3int h): return faces -# cpdef (int, int) experimental_h3_to_local_ij(H3int origin, H3int h) except *: -# cdef: -# int flag -# h3lib.CoordIJ c - -# check_cell(origin) -# check_cell(h) +cpdef (int, int) experimental_h3_to_local_ij(H3int origin, H3int h) except *: + cdef: + int flag + h3lib.CoordIJ c -# flag = h3lib.experimentalH3ToLocalIj(origin, h, &c) + check_cell(origin) + check_cell(h) -# if flag != 0: -# s = "Couldn't find local (i,j) between cells {} and {}." -# s = s.format(hex(origin), hex(h)) -# raise H3ValueError(s) + flag = h3lib.cellToLocalIj(origin, h, 0, &c) -# return c.i, c.j + if flag != 0: + s = "Couldn't find local (i,j) between cells {} and {}." + s = s.format(hex(origin), hex(h)) + raise H3ValueError(s) + return c.i, c.j -# cpdef H3int experimental_local_ij_to_h3(H3int origin, int i, int j) except 0: -# cdef: -# int flag -# h3lib.CoordIJ c -# H3int out +cpdef H3int experimental_local_ij_to_h3(H3int origin, int i, int j) except 0: + cdef: + int flag + h3lib.CoordIJ c + H3int out -# check_cell(origin) + check_cell(origin) -# c.i, c.j = i, j + c.i, c.j = i, j -# flag = h3lib.experimentalLocalIjToH3(origin, &c, &out) + flag = h3lib.localIjToCell(origin, &c, 0, &out) -# if flag != 0: -# s = "Couldn't find cell at local ({},{}) from cell {}." -# s = s.format(i, j, hex(origin)) -# raise H3ValueError(s) + if flag != 0: + s = "Couldn't find cell at local ({},{}) from cell {}." + s = s.format(i, j, hex(origin)) + raise H3ValueError(s) -# return out + return out diff --git a/src/h3/_cy/h3lib.pxd b/src/h3/_cy/h3lib.pxd index a5aeb779f..05013b948 100644 --- a/src/h3/_cy/h3lib.pxd +++ b/src/h3/_cy/h3lib.pxd @@ -1,9 +1,8 @@ -from libc cimport stdint from cpython cimport bool -from libc.stdint cimport int64_t +from libc.stdint cimport uint64_t, int64_t, uint32_t -ctypedef stdint.uint64_t H3int -ctypedef stdint.uint32_t H3Error +ctypedef uint64_t H3int +ctypedef uint32_t H3Error ctypedef basestring H3str cdef extern from "h3api.h": @@ -11,12 +10,16 @@ cdef extern from "h3api.h": cdef int H3_VERSION_MINOR cdef int H3_VERSION_PATCH - ctypedef stdint.uint64_t H3Index + ctypedef uint64_t H3Index ctypedef struct LatLng: double lat # in radians double lng # in radians + ctypedef struct CoordIJ: + int i + int j + int isValidCell(H3Index h) nogil int isPentagon(H3Index h) nogil int isResClassIII(H3Index h) nogil @@ -79,6 +82,9 @@ cdef extern from "h3api.h": H3Error maxFaceCount(H3Index h, int *out) nogil H3Error getIcosahedronFaces(H3Index h3, int *out) nogil + H3Error cellToLocalIj(H3Index origin, H3Index h3, uint32_t mode, CoordIJ *out) nogil + H3Error localIjToCell(H3Index origin, const CoordIJ *ij, uint32_t mode, H3Index *out) nogil + # ctypedef struct GeoBoundary: # int num_verts "numVerts" # GeoCoord verts[10] # MAX_CELL_BNDRY_VERTS @@ -111,10 +117,6 @@ cdef extern from "h3api.h": # LinkedGeoLoop *_data_last "last" # not needed in Cython bindings # LinkedGeoPolygon *next - # ctypedef struct CoordIJ: - # int i - # int j - # void h3ToGeoBoundary(H3Index h3, GeoBoundary *gp) # int hexRange(H3Index origin, int k, H3Index *out) @@ -153,9 +155,6 @@ cdef extern from "h3api.h": # void getH3UnidirectionalEdgeBoundary(H3Index edge, GeoBoundary *gb) - # int experimentalH3ToLocalIj(H3Index origin, H3Index h3, CoordIJ *out) - # int experimentalLocalIjToH3(H3Index origin, const CoordIJ *ij, H3Index *out) - # double edgeLengthKm(int res) nogil # double edgeLengthM(int res) nogil From 610d648bfee25e55b96133f1038bffc0e81bff62 Mon Sep 17 00:00:00 2001 From: AJ Friend Date: Wed, 25 May 2022 21:45:26 -0700 Subject: [PATCH 05/21] ring (#251) --- src/h3/_cy/__init__.py | 38 ++++--------------- src/h3/_cy/cells.pxd | 4 +- src/h3/_cy/cells.pyx | 86 ++++++++++++++++++++++-------------------- src/h3/_cy/h3lib.pxd | 7 ++-- 4 files changed, 58 insertions(+), 77 deletions(-) diff --git a/src/h3/_cy/__init__.py b/src/h3/_cy/__init__.py index d378be877..6bce5bf44 100644 --- a/src/h3/_cy/__init__.py +++ b/src/h3/_cy/__init__.py @@ -18,50 +18,26 @@ is_pentagon, get_base_cell, resolution, - is_res_class_iii, + parent, distance, disk, - parent, + ring, children, - center_child, compact, uncompact, num_hexagons, - get_pentagon_indexes, - get_res0_indexes, - line, mean_hex_area, cell_area, + line, + is_res_class_iii, + get_pentagon_indexes, + get_res0_indexes, + center_child, get_faces, experimental_h3_to_local_ij, experimental_local_ij_to_h3, ) -from h3fake2._cy import ( - # is_cell, - # is_pentagon, - # get_base_cell, - # resolution, - # parent, - # distance, - # disk, - ring, - # children, - # compact, - # uncompact, - # num_hexagons, - # mean_hex_area, - # cell_area, - # line, - # is_res_class_iii, - # get_pentagon_indexes, - # get_res0_indexes, - # center_child, - # get_faces, - # experimental_h3_to_local_ij, - # experimental_local_ij_to_h3, -) - # from .edges import ( from h3fake2._cy import ( are_neighbors, diff --git a/src/h3/_cy/cells.pxd b/src/h3/_cy/cells.pxd index ee6c2a7cf..10719389e 100644 --- a/src/h3/_cy/cells.pxd +++ b/src/h3/_cy/cells.pxd @@ -6,8 +6,8 @@ cpdef int get_base_cell(H3int h) except -1 cpdef int resolution(H3int h) except -1 cpdef int distance(H3int h1, H3int h2) except -1 cpdef H3int[:] disk(H3int h, int k) -# cpdef H3int[:] _ring_fallback(H3int h, int k) -# cpdef H3int[:] ring(H3int h, int k) +cpdef H3int[:] _ring_fallback(H3int h, int k) +cpdef H3int[:] ring(H3int h, int k) cpdef H3int parent(H3int h, res=*) except 0 cpdef H3int[:] children(H3int h, res=*) cpdef H3int center_child(H3int h, res=*) except 0 diff --git a/src/h3/_cy/cells.pyx b/src/h3/_cy/cells.pyx index 0bd99bd49..4453e388b 100644 --- a/src/h3/_cy/cells.pyx +++ b/src/h3/_cy/cells.pyx @@ -85,61 +85,67 @@ cpdef H3int[:] disk(H3int h, int k): return mv -# cpdef H3int[:] _ring_fallback(H3int h, int k): -# """ -# `ring` tries to call `h3lib.hexRing` first; if that fails, we call -# this function, which relies on `h3lib.kRingDistances`. +cpdef H3int[:] _ring_fallback(H3int h, int k): + """ + `ring` tries to call `h3lib.hexRing` first; if that fails, we call + this function, which relies on `h3lib.kRingDistances`. -# Failures for `h3lib.hexRing` happen when the algortihm runs into a pentagon. -# """ -# check_cell(h) -# check_distance(k) + Failures for `h3lib.hexRing` happen when the algortihm runs into a pentagon. + """ + cdef: + int64_t n + h3lib.H3Error err -# n = h3lib.maxKringSize(k) -# # array of h3 cells -# ptr = create_ptr(n) + check_cell(h) + check_distance(k) -# # array of cell distances from `h` -# dist_ptr = stdlib.calloc(n, sizeof(int)) -# if dist_ptr is NULL: -# raise MemoryError() + err = h3lib.maxGridDiskSize(k, &n) + # array of h3 cells + ptr = create_ptr(n) + + # array of cell distances from `h` + dist_ptr = stdlib.calloc(n, sizeof(int)) + if dist_ptr is NULL: + raise MemoryError() -# h3lib.kRingDistances(h, k, ptr, dist_ptr) + err = h3lib.gridDiskDistances(h, k, ptr, dist_ptr) -# distances = dist_ptr -# distances.callback_free_data = stdlib.free + distances = dist_ptr + distances.callback_free_data = stdlib.free -# for i,v in enumerate(distances): -# if v != k: -# ptr[i] = 0 + for i,v in enumerate(distances): + if v != k: + ptr[i] = 0 -# mv = create_mv(ptr, n) + mv = create_mv(ptr, n) -# return mv + return mv -# cpdef H3int[:] 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) +cpdef H3int[:] ring(H3int h, int k): + """ Return cells at grid distance `== k` from `h`. + Collection is "hollow" for k >= 1. + """ + cdef: + h3lib.H3Error err -# n = 6*k if k > 0 else 1 -# ptr = create_ptr(n) + check_cell(h) + check_distance(k) -# flag = h3lib.hexRing(h, k, ptr) + n = 6*k if k > 0 else 1 + ptr = create_ptr(n) -# # if we drop into the failure state, we might be tempted to not create -# # this mv, but creating the mv is exactly what guarantees that we'll free -# # the memory. context manager would be better here, if we can figure out -# # how to do that -# mv = create_mv(ptr, n) + err = h3lib.gridRingUnsafe(h, k, ptr) -# if flag != 0: -# mv = _ring_fallback(h, k) + # if we drop into the failure state, we might be tempted to not create + # this mv, but creating the mv is exactly what guarantees that we'll free + # the memory. context manager would be better here, if we can figure out + # how to do that + mv = create_mv(ptr, n) -# return mv + if err: + mv = _ring_fallback(h, k) + return mv cpdef H3int parent(H3int h, res=None) except 0: cdef: diff --git a/src/h3/_cy/h3lib.pxd b/src/h3/_cy/h3lib.pxd index 05013b948..aa84b2bc5 100644 --- a/src/h3/_cy/h3lib.pxd +++ b/src/h3/_cy/h3lib.pxd @@ -85,6 +85,9 @@ cdef extern from "h3api.h": H3Error cellToLocalIj(H3Index origin, H3Index h3, uint32_t mode, CoordIJ *out) nogil H3Error localIjToCell(H3Index origin, const CoordIJ *ij, uint32_t mode, H3Index *out) nogil + H3Error gridDiskDistances(H3Index origin, int k, H3Index *out, int *distances) nogil + H3Error gridRingUnsafe(H3Index origin, int k, H3Index *out) nogil + # ctypedef struct GeoBoundary: # int num_verts "numVerts" # GeoCoord verts[10] # MAX_CELL_BNDRY_VERTS @@ -125,10 +128,6 @@ cdef extern from "h3api.h": # int hexRanges(H3Index *h3Set, int length, int k, H3Index *out) - # void kRingDistances(H3Index origin, int k, H3Index *out, int *distances) - - # int hexRing(H3Index origin, int k, H3Index *out) - # int maxPolyfillSize(const GeoPolygon *geoPolygon, int res) # void polyfill(const GeoPolygon *geoPolygon, int res, H3Index *out) From 51e73fe01b38ae53289d49d692e233e6b16eba3b Mon Sep 17 00:00:00 2001 From: AJ Friend Date: Thu, 26 May 2022 14:15:35 -0400 Subject: [PATCH 06/21] Uncomment cython example, and exclude sdist from wheels test temporarilty (#252) * uncomment cython test * exclude make_sdist temporarily from the wheels tests --- .github/workflows/wheels.yml | 3 ++- tests/cython_example.pyx | 28 ++++++++++++------------- tests/test_cython.py | 40 ++++++++++++++++++------------------ 3 files changed, 36 insertions(+), 35 deletions(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 71f47d5d4..dd6bfbc75 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -168,7 +168,8 @@ jobs: path: wheelhouse/*.whl upload_all: - needs: [make_sdist, make_cibw_v1_wheels, make_cibw_v2_wheels] + # needs: [make_sdist, make_cibw_v1_wheels, make_cibw_v2_wheels] + needs: [make_cibw_v1_wheels, make_cibw_v2_wheels] runs-on: ubuntu-latest if: github.event_name == 'release' && github.event.action == 'published' diff --git a/tests/cython_example.pyx b/tests/cython_example.pyx index c19fe21ec..e8d285947 100644 --- a/tests/cython_example.pyx +++ b/tests/cython_example.pyx @@ -1,18 +1,18 @@ -# from cython cimport boundscheck, wraparound +from cython cimport boundscheck, wraparound -# from h3._cy.h3lib cimport H3int -# from h3._cy.geo cimport geo_to_h3 +from h3._cy.h3lib cimport H3int +from h3._cy.geo cimport geo_to_h3 -# @boundscheck(False) -# @wraparound(False) -# cpdef void geo_to_h3_vect( -# const double[:] lat, -# const double[:] lng, -# int res, -# H3int[:] out -# ): +@boundscheck(False) +@wraparound(False) +cpdef void geo_to_h3_vect( + const double[:] lat, + const double[:] lng, + int res, + H3int[:] out +): -# cdef Py_ssize_t i + cdef Py_ssize_t i -# for i in range(len(lat)): -# out[i] = geo_to_h3(lat[i], lng[i], res) + for i in range(len(lat)): + out[i] = geo_to_h3(lat[i], lng[i], res) diff --git a/tests/test_cython.py b/tests/test_cython.py index 0acf3073f..30566b188 100644 --- a/tests/test_cython.py +++ b/tests/test_cython.py @@ -1,28 +1,28 @@ -# import numpy as np +import numpy as np -# # Avoid checking for import-error here because cython_example may not have -# # been compiled yet. -# try: -# from .cython_example import geo_to_h3_vect # pylint: disable=import-error -# except ImportError: -# geo_to_h3_vect = None +# Avoid checking for import-error here because cython_example may not have +# been compiled yet. +try: + from .cython_example import geo_to_h3_vect # pylint: disable=import-error +except ImportError: + geo_to_h3_vect = None -# np.random.seed(0) +np.random.seed(0) -# def test_cython_api(): -# if geo_to_h3_vect is None: -# print("Not running Cython test because cython example was not compiled") -# return +def test_cython_api(): + if geo_to_h3_vect is None: + print("Not running Cython test because cython example was not compiled") + return -# N = 100000 + N = 100000 -# lats, lngs = np.random.uniform(0, 90, N), np.random.uniform(0, 90, N) -# res = 9 + 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") -# geo_to_h3_vect(lats, lngs, res, out) + lats = np.array(lats, dtype=np.float64) + lngs = np.array(lngs, dtype=np.float64) + out = np.zeros(len(lats), dtype="uint64") + geo_to_h3_vect(lats, lngs, res, out) -# assert out[0] == 617284541015654399 + assert out[0] == 617284541015654399 From c5333321e92b77847685b3dbd80692e5f9d65713 Mon Sep 17 00:00:00 2001 From: Isaac Brodsky Date: Tue, 31 May 2022 19:01:51 -0700 Subject: [PATCH 07/21] v4: polygonToCells (#254) --- src/h3/_cy/__init__.py | 12 +- src/h3/_cy/geo.pyx | 289 +++++++++++++++++++++-------------------- src/h3/_cy/h3lib.pxd | 25 ++-- 3 files changed, 164 insertions(+), 162 deletions(-) diff --git a/src/h3/_cy/__init__.py b/src/h3/_cy/__init__.py index 6bce5bf44..db15af649 100644 --- a/src/h3/_cy/__init__.py +++ b/src/h3/_cy/__init__.py @@ -54,9 +54,9 @@ from .geo import ( geo_to_h3, h3_to_geo, - # polyfill_polygon, - # polyfill_geojson, - # polyfill, + polyfill_polygon, + polyfill_geojson, + polyfill, # cell_boundary, # edge_boundary, # point_dist, @@ -65,9 +65,9 @@ from h3fake2._cy import ( # geo_to_h3, # h3_to_geo, - polyfill_polygon, - polyfill_geojson, - polyfill, + # polyfill_polygon, + # polyfill_geojson, + # polyfill, cell_boundary, edge_boundary, point_dist, diff --git a/src/h3/_cy/geo.pyx b/src/h3/_cy/geo.pyx index fe6c93b5b..870594bcd 100644 --- a/src/h3/_cy/geo.pyx +++ b/src/h3/_cy/geo.pyx @@ -10,6 +10,7 @@ from .util cimport ( coord2deg, ) from libc cimport stdlib +from libc.stdint cimport uint64_t from .util import H3ValueError @@ -42,180 +43,182 @@ cpdef (double, double) h3_to_geo(H3int h) except *: return coord2deg(c) -# cdef h3lib.Geofence make_geofence(geos, bool lnglat_order=False) except *: -# """ +cdef h3lib.GeoLoop make_geoloop(geos, bool lnglat_order=False) except *: + """ -# The returned `Geofence` must be freed with a call to `free_geofence`. - -# Parameters -# ---------- -# geos : list or tuple -# GeoFence: 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. -# lnglat_order : bool -# If True, assume coordinate pairs like (lng, lat) -# If False, assume coordinate pairs like (lat, lng) -# """ -# cdef: -# h3lib.Geofence gf + The returned `GeoLoop` must be freed with a call to `free_geoloop`. + + Parameters + ---------- + geos : 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. + lnglat_order : bool + If True, assume coordinate pairs like (lng, lat) + If False, assume coordinate pairs like (lat, lng) + """ + cdef: + h3lib.GeoLoop gl -# gf.numVerts = len(geos) + gl.numVerts = len(geos) -# gf.verts = stdlib.calloc(gf.numVerts, sizeof(h3lib.GeoCoord)) + gl.verts = stdlib.calloc(gl.numVerts, sizeof(h3lib.LatLng)) -# if lnglat_order: -# latlng = (g[::-1] for g in geos) -# else: -# latlng = geos + if lnglat_order: + latlng = (g[::-1] for g in geos) + else: + latlng = geos -# for i, (lat, lng) in enumerate(latlng): -# gf.verts[i] = deg2coord(lat, lng) + for i, (lat, lng) in enumerate(latlng): + gl.verts[i] = deg2coord(lat, lng) -# return gf + return gl -# cdef free_geofence(h3lib.Geofence* gf): -# stdlib.free(gf.verts) -# gf.verts = NULL +cdef free_geoloop(h3lib.GeoLoop* gl): + stdlib.free(gl.verts) + gl.verts = NULL -# cdef class GeoPolygon: -# cdef: -# h3lib.GeoPolygon gp - -# def __cinit__(self, outer, holes=None, bool lnglat_order=False): -# """ - -# Parameters -# ---------- -# outer : list or tuple -# GeoFence -# A GeoFence 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 GeoFences -# lnglat_order : bool -# If True, assume coordinate pairs like (lng, lat) -# If False, assume coordinate pairs like (lat, lng) - -# """ -# if holes is None: -# holes = [] - -# self.gp.geofence = make_geofence(outer, lnglat_order) -# self.gp.numHoles = len(holes) -# self.gp.holes = NULL - -# if len(holes) > 0: -# self.gp.holes = stdlib.calloc(len(holes), sizeof(h3lib.Geofence)) -# for i, hole in enumerate(holes): -# self.gp.holes[i] = make_geofence(hole, lnglat_order) - - -# def __dealloc__(self): -# free_geofence(&self.gp.geofence) - -# for i in range(self.gp.numHoles): -# free_geofence(&self.gp.holes[i]) - -# stdlib.free(self.gp.holes) - - -# def polyfill_polygon(outer, int res, holes=None, bool lnglat_order=False): -# """ Set of hexagons whose center is contained in a polygon. - -# The polygon is defined as in the GeoJson standard, with an exterior -# LinearRing `outer` and a list of LinearRings `holes`, which define any -# holes in the polygon. - -# Each LinearRing 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 will work with any input format. - -# Parameters -# ---------- -# outer : list or tuple -# A LinearRing, a sequence of (lat/lng) or (lng/lat) pairs -# res : int -# The resolution of the output hexagons -# holes : list or tuple -# A collection of LinearRings, describing any holes in the polygon -# lnglat_order : bool -# If True, assume coordinate pairs like (lng, lat) -# If False, assume coordinate pairs like (lat, lng) -# """ +cdef class GeoPolygon: + cdef: + h3lib.GeoPolygon gp + + def __cinit__(self, outer, holes=None, bool lnglat_order=False): + """ + + 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 + lnglat_order : bool + If True, assume coordinate pairs like (lng, lat) + If False, assume coordinate pairs like (lat, lng) + + """ + if holes is None: + holes = [] + + self.gp.geoloop = make_geoloop(outer, lnglat_order) + self.gp.numHoles = len(holes) + self.gp.holes = NULL + + if len(holes) > 0: + self.gp.holes = stdlib.calloc(len(holes), sizeof(h3lib.GeoLoop)) + for i, hole in enumerate(holes): + self.gp.holes[i] = make_geoloop(hole, lnglat_order) + + + def __dealloc__(self): + free_geoloop(&self.gp.geoloop) + + for i in range(self.gp.numHoles): + free_geoloop(&self.gp.holes[i]) + + stdlib.free(self.gp.holes) + + +def polyfill_polygon(outer, int res, holes=None, bool lnglat_order=False): + """ Set of hexagons whose center is contained in a polygon. + + The polygon is defined as in the GeoJson standard, with an exterior + LinearRing `outer` and a list of LinearRings `holes`, which define any + holes in the polygon. + + Each LinearRing 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 will work with any input format. + + Parameters + ---------- + outer : list or tuple + A LinearRing, a sequence of (lat/lng) or (lng/lat) pairs + res : int + The resolution of the output hexagons + holes : list or tuple + A collection of LinearRings, describing any holes in the polygon + lnglat_order : bool + If True, assume coordinate pairs like (lng, lat) + If False, assume coordinate pairs like (lat, lng) + """ + cdef: + uint64_t n -# check_res(res) -# gp = GeoPolygon(outer, holes=holes, lnglat_order=lnglat_order) + check_res(res) + gp = GeoPolygon(outer, holes=holes, lnglat_order=lnglat_order) -# n = h3lib.maxPolyfillSize(&gp.gp, res) -# ptr = create_ptr(n) + h3lib.maxPolygonToCellsSize(&gp.gp, res, 0, &n) + ptr = create_ptr(n) -# h3lib.polyfill(&gp.gp, res, ptr) -# mv = create_mv(ptr, n) + h3lib.polygonToCells(&gp.gp, res, 0, ptr) + mv = create_mv(ptr, n) -# return mv + return mv -# def polyfill_geojson(geojson, int res): -# """ Set of hexagons whose center is contained in a GeoJson Polygon object. +def polyfill_geojson(geojson, int res): + """ Set of hexagons whose center is contained in a GeoJson Polygon object. -# The polygon is defined exactly as in the GeoJson standard, so -# `geojson` should be a dictionary like: -# { -# 'type': 'Polygon', -# 'coordinates': [...] -# } + The polygon is defined exactly as in the GeoJson standard, so + `geojson` should be a dictionary like: + { + 'type': 'Polygon', + 'coordinates': [...] + } -# 'coordinates' should be a list of LinearRings, where the first ring describes -# the exterior boundary of the Polygon, and any subsequent LinearRings -# describe holes in the polygon. + 'coordinates' should be a list of LinearRings, where the first ring describes + the exterior boundary of the Polygon, and any subsequent LinearRings + describe holes in the polygon. -# Note that we don't provide an option for the order of the coordinates, -# as the GeoJson standard requires them to be in lng/lat order. + Note that we don't provide an option for the order of the coordinates, + as the GeoJson standard requires them to be in lng/lat order. -# Parameters -# ---------- -# geojson : dict -# res : int -# The resolution of the output hexagons -# """ + Parameters + ---------- + geojson : dict + res : int + The resolution of the output hexagons + """ -# # todo: this one could handle multipolygons... + # todo: this one could handle multipolygons... -# if geojson['type'] != 'Polygon': -# raise ValueError('Only Polygon GeoJSON supported') + if geojson['type'] != 'Polygon': + raise ValueError('Only Polygon GeoJSON supported') -# coords = geojson['coordinates'] + coords = geojson['coordinates'] -# out = polyfill_polygon(coords[0], res, holes=coords[1:], lnglat_order=True) + out = polyfill_polygon(coords[0], res, holes=coords[1:], lnglat_order=True) -# return out + return out -# def polyfill(dict geojson, int res, bool geo_json_conformant=False): -# """ Light wrapper around `polyfill_geojson` to provide backward compatibility. -# """ +def polyfill(dict geojson, int res, bool geo_json_conformant=False): + """ Light wrapper around `polyfill_geojson` to provide backward compatibility. + """ -# try: -# gj_type = geojson['type'] -# except KeyError: -# raise KeyError("`geojson` dict must have key 'type'.") from None + try: + gj_type = geojson['type'] + except KeyError: + raise KeyError("`geojson` dict must have key 'type'.") from None -# if gj_type != 'Polygon': -# raise ValueError('Only Polygon GeoJSON supported') + if gj_type != 'Polygon': + raise ValueError('Only Polygon GeoJSON supported') -# if geo_json_conformant: -# out = polyfill_geojson(geojson, res) -# else: -# coords = geojson['coordinates'] -# out = polyfill_polygon(coords[0], res, holes=coords[1:], lnglat_order=False) + if geo_json_conformant: + out = polyfill_geojson(geojson, res) + else: + coords = geojson['coordinates'] + out = polyfill_polygon(coords[0], res, holes=coords[1:], lnglat_order=False) -# return out + return out # def cell_boundary(H3int h, bool geo_json=False): diff --git a/src/h3/_cy/h3lib.pxd b/src/h3/_cy/h3lib.pxd index aa84b2bc5..440391a93 100644 --- a/src/h3/_cy/h3lib.pxd +++ b/src/h3/_cy/h3lib.pxd @@ -20,6 +20,15 @@ cdef extern from "h3api.h": int i int j + ctypedef struct GeoLoop: + int numVerts + LatLng *verts + + ctypedef struct GeoPolygon: + GeoLoop geoloop + int numHoles + GeoLoop *holes + int isValidCell(H3Index h) nogil int isPentagon(H3Index h) nogil int isResClassIII(H3Index h) nogil @@ -88,19 +97,13 @@ cdef extern from "h3api.h": H3Error gridDiskDistances(H3Index origin, int k, H3Index *out, int *distances) nogil H3Error gridRingUnsafe(H3Index origin, int k, H3Index *out) nogil + H3Error maxPolygonToCellsSize(const GeoPolygon *geoPolygon, int res, uint32_t flags, uint64_t *count) + H3Error polygonToCells(const GeoPolygon *geoPolygon, int res, uint32_t flags, H3Index *out) + # ctypedef struct GeoBoundary: # int num_verts "numVerts" # GeoCoord verts[10] # MAX_CELL_BNDRY_VERTS - # ctypedef struct Geofence: - # int numVerts - # GeoCoord *verts - - # ctypedef struct GeoPolygon: - # Geofence geofence - # int numHoles - # Geofence *holes - # ctypedef struct GeoMultiPolygon: # int numPolygons # GeoPolygon *polygons @@ -128,10 +131,6 @@ cdef extern from "h3api.h": # int hexRanges(H3Index *h3Set, int length, int k, H3Index *out) - # int maxPolyfillSize(const GeoPolygon *geoPolygon, int res) - - # void polyfill(const GeoPolygon *geoPolygon, int res, H3Index *out) - # void h3SetToLinkedGeo(const H3Index *h3Set, const int numHexes, LinkedGeoPolygon *out) # void destroyLinkedPolygon(LinkedGeoPolygon *polygon) From dc9c011dde04df701fe22df429e16d9980fd146b Mon Sep 17 00:00:00 2001 From: Isaac Brodsky Date: Wed, 1 Jun 2022 14:42:02 -0700 Subject: [PATCH 08/21] v4: geo boundary, set to multi polygon (#255) * v4: cell boundary * v4: h3_set_to_multi_polygon --- src/h3/_cy/__init__.py | 14 +---- src/h3/_cy/geo.pxd | 6 +-- src/h3/_cy/geo.pyx | 92 +++++++++++++++---------------- src/h3/_cy/h3lib.pxd | 57 ++++++++++---------- src/h3/_cy/to_multipoly.pyx | 104 ++++++++++++++++++------------------ 5 files changed, 132 insertions(+), 141 deletions(-) diff --git a/src/h3/_cy/__init__.py b/src/h3/_cy/__init__.py index db15af649..c684997f2 100644 --- a/src/h3/_cy/__init__.py +++ b/src/h3/_cy/__init__.py @@ -57,24 +57,12 @@ polyfill_polygon, polyfill_geojson, polyfill, - # cell_boundary, - # edge_boundary, - # point_dist, -) - -from h3fake2._cy import ( - # geo_to_h3, - # h3_to_geo, - # polyfill_polygon, - # polyfill_geojson, - # polyfill, cell_boundary, edge_boundary, point_dist, ) -# from .to_multipoly import ( -from h3fake2._cy import ( +from .to_multipoly import ( h3_set_to_multi_polygon ) diff --git a/src/h3/_cy/geo.pxd b/src/h3/_cy/geo.pxd index 37e32a0b5..1a2c1c332 100644 --- a/src/h3/_cy/geo.pxd +++ b/src/h3/_cy/geo.pxd @@ -2,6 +2,6 @@ from .h3lib cimport H3int cpdef H3int geo_to_h3(double lat, double lng, int res) except 1 cpdef (double, double) h3_to_geo(H3int h) except * -# cpdef double point_dist( -# double lat1, double lng1, -# double lat2, double lng2, unit=*) except -1 +cpdef double point_dist( + double lat1, double lng1, + double lat2, double lng2, unit=*) except -1 diff --git a/src/h3/_cy/geo.pyx b/src/h3/_cy/geo.pyx index 870594bcd..7a737e28b 100644 --- a/src/h3/_cy/geo.pyx +++ b/src/h3/_cy/geo.pyx @@ -221,66 +221,66 @@ def polyfill(dict geojson, int res, bool geo_json_conformant=False): return out -# def cell_boundary(H3int h, bool geo_json=False): -# """Compose an array of geo-coordinates that outlines a hexagonal cell""" -# cdef: -# h3lib.GeoBoundary gb +def cell_boundary(H3int h, bool geo_json=False): + """Compose an array of geo-coordinates that outlines a hexagonal cell""" + cdef: + h3lib.CellBoundary gb -# check_cell(h) + check_cell(h) -# h3lib.h3ToGeoBoundary(h, &gb) + h3lib.cellToBoundary(h, &gb) -# verts = tuple( -# coord2deg(gb.verts[i]) -# for i in range(gb.num_verts) -# ) + verts = tuple( + coord2deg(gb.verts[i]) + for i in range(gb.num_verts) + ) -# if geo_json: -# #lat/lng -> lng/lat and last point same as first -# verts += (verts[0],) -# verts = tuple(v[::-1] for v in verts) + if geo_json: + #lat/lng -> lng/lat and last point same as first + verts += (verts[0],) + verts = tuple(v[::-1] for v in verts) -# return verts + return verts -# def edge_boundary(H3int edge, bool geo_json=False): -# """ Returns the GeoBoundary containing the coordinates of the edge -# """ -# cdef: -# h3lib.GeoBoundary gb +def edge_boundary(H3int edge, bool geo_json=False): + """ Returns the CellBoundary containing the coordinates of the edge + """ + cdef: + h3lib.CellBoundary gb -# check_edge(edge) + check_edge(edge) -# h3lib.getH3UnidirectionalEdgeBoundary(edge, &gb) + h3lib.directedEdgeToBoundary(edge, &gb) -# # todo: move this verts transform into the GeoBoundary object -# verts = tuple( -# coord2deg(gb.verts[i]) -# for i in range(gb.num_verts) -# ) + # todo: move this verts transform into the CellBoundary object + verts = tuple( + coord2deg(gb.verts[i]) + for i in range(gb.num_verts) + ) -# if geo_json: -# #lat/lng -> lng/lat and last point same as first -# verts += (verts[0],) -# verts = tuple(v[::-1] for v in verts) + if geo_json: + #lat/lng -> lng/lat and last point same as first + verts += (verts[0],) + verts = tuple(v[::-1] for v in verts) -# return verts + return verts -# cpdef double point_dist( -# double lat1, double lng1, -# double lat2, double lng2, unit='km') except -1: +cpdef double point_dist( + double lat1, double lng1, + double lat2, double lng2, unit='km') except -1: -# a = deg2coord(lat1, lng1) -# b = deg2coord(lat2, lng2) + a = deg2coord(lat1, lng1) + b = deg2coord(lat2, lng2) -# if unit == 'rads': -# d = h3lib.pointDistRads(&a, &b) -# elif unit == 'km': -# d = h3lib.pointDistKm(&a, &b) -# elif unit == 'm': -# d = h3lib.pointDistM(&a, &b) -# else: -# raise H3ValueError('Unknown unit: {}'.format(unit)) + if unit == 'rads': + d = h3lib.distanceRads(&a, &b) + elif unit == 'km': + d = h3lib.distanceKm(&a, &b) + elif unit == 'm': + d = h3lib.distanceM(&a, &b) + else: + raise H3ValueError('Unknown unit: {}'.format(unit)) -# return d + return d diff --git a/src/h3/_cy/h3lib.pxd b/src/h3/_cy/h3lib.pxd index 440391a93..c3efea8bf 100644 --- a/src/h3/_cy/h3lib.pxd +++ b/src/h3/_cy/h3lib.pxd @@ -16,10 +16,29 @@ cdef extern from "h3api.h": 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 LinkedLatLng: + LatLng data "vertex" + LinkedLatLng *next + + # renaming these for clarity + ctypedef struct LinkedGeoLoop: + LinkedLatLng *data "first" + LinkedLatLng *_data_last "last" # not needed in Cython bindings + LinkedGeoLoop *next + + ctypedef struct LinkedGeoPolygon: + LinkedGeoLoop *data "first" + LinkedGeoLoop *_data_last "last" # not needed in Cython bindings + LinkedGeoPolygon *next + ctypedef struct GeoLoop: int numVerts LatLng *verts @@ -97,34 +116,24 @@ cdef extern from "h3api.h": H3Error gridDiskDistances(H3Index origin, int k, H3Index *out, int *distances) nogil H3Error gridRingUnsafe(H3Index origin, int k, H3Index *out) nogil + H3Error cellToBoundary(H3Index h3, CellBoundary *gp) nogil + + H3Error directedEdgeToBoundary(H3Index edge, CellBoundary *gb) nogil + + double distanceRads(const LatLng *a, const LatLng *b) nogil + double distanceKm(const LatLng *a, const LatLng *b) nogil + double distanceM(const LatLng *a, const LatLng *b) nogil + + H3Error cellsToLinkedMultiPolygon(const H3Index *h3Set, const int numHexes, LinkedGeoPolygon *out) + void destroyLinkedMultiPolygon(LinkedGeoPolygon *polygon) + H3Error maxPolygonToCellsSize(const GeoPolygon *geoPolygon, int res, uint32_t flags, uint64_t *count) H3Error polygonToCells(const GeoPolygon *geoPolygon, int res, uint32_t flags, H3Index *out) - # ctypedef struct GeoBoundary: - # int num_verts "numVerts" - # GeoCoord verts[10] # MAX_CELL_BNDRY_VERTS - # ctypedef struct GeoMultiPolygon: # int numPolygons # GeoPolygon *polygons - # ctypedef struct LinkedGeoCoord: - # GeoCoord data "vertex" - # LinkedGeoCoord *next - - # # renaming these for clarity - # ctypedef struct LinkedGeoLoop: - # LinkedGeoCoord *data "first" - # LinkedGeoCoord *_data_last "last" # not needed in Cython bindings - # LinkedGeoLoop *next - - # ctypedef struct LinkedGeoPolygon: - # LinkedGeoLoop *data "first" - # LinkedGeoLoop *_data_last "last" # not needed in Cython bindings - # LinkedGeoPolygon *next - - # void h3ToGeoBoundary(H3Index h3, GeoBoundary *gp) - # int hexRange(H3Index origin, int k, H3Index *out) # int hexRangeDistances(H3Index origin, int k, H3Index *out, int *distances) @@ -151,15 +160,9 @@ cdef extern from "h3api.h": # void getH3UnidirectionalEdgesFromHexagon(H3Index origin, H3Index *edges) - # void getH3UnidirectionalEdgeBoundary(H3Index edge, GeoBoundary *gb) - # double edgeLengthKm(int res) nogil # double edgeLengthM(int res) nogil # double exactEdgeLengthRads(H3Index edge) nogil # double exactEdgeLengthKm(H3Index edge) nogil # double exactEdgeLengthM(H3Index edge) nogil - - # double pointDistRads(const GeoCoord *a, const GeoCoord *b) nogil - # double pointDistKm(const GeoCoord *a, const GeoCoord *b) nogil - # double pointDistM(const GeoCoord *a, const GeoCoord *b) nogil diff --git a/src/h3/_cy/to_multipoly.pyx b/src/h3/_cy/to_multipoly.pyx index 296a932f0..7a421f7c6 100644 --- a/src/h3/_cy/to_multipoly.pyx +++ b/src/h3/_cy/to_multipoly.pyx @@ -1,75 +1,75 @@ -# cimport h3lib -# from h3lib cimport H3int -# from .util cimport check_cell, coord2deg +cimport h3lib +from h3lib cimport H3int +from .util cimport check_cell, coord2deg -# # todo: it's driving me crazy that these three functions are all essentially the same linked list walker... -# # grumble: no way to do iterators in with cdef functions! -# cdef walk_polys(const h3lib.LinkedGeoPolygon* L): -# out = [] -# while L: -# out += [walk_loops(L.data)] -# L = L.next +# todo: it's driving me crazy that these three functions are all essentially the same linked list walker... +# grumble: no way to do iterators in with cdef functions! +cdef walk_polys(const h3lib.LinkedGeoPolygon* L): + out = [] + while L: + out += [walk_loops(L.data)] + L = L.next -# return out + return out -# cdef walk_loops(const h3lib.LinkedGeoLoop* L): -# out = [] -# while L: -# out += [walk_coords(L.data)] -# L = L.next +cdef walk_loops(const h3lib.LinkedGeoLoop* L): + out = [] + while L: + out += [walk_coords(L.data)] + L = L.next -# return out + return out -# cdef walk_coords(const h3lib.LinkedGeoCoord* L): -# out = [] -# while L: -# out += [coord2deg(L.data)] -# L = L.next +cdef walk_coords(const h3lib.LinkedLatLng* L): + out = [] + while L: + out += [coord2deg(L.data)] + L = L.next -# return out + return out -# # todo: tuples instead of lists? -# def _to_multi_polygon(const H3int[:] hexes): -# cdef: -# h3lib.LinkedGeoPolygon polygon +# todo: tuples instead of lists? +def _to_multi_polygon(const H3int[:] hexes): + cdef: + h3lib.LinkedGeoPolygon polygon -# for h in hexes: -# check_cell(h) + for h in hexes: + check_cell(h) -# h3lib.h3SetToLinkedGeo(&hexes[0], len(hexes), &polygon) + h3lib.cellsToLinkedMultiPolygon(&hexes[0], len(hexes), &polygon) -# out = walk_polys(&polygon) + out = walk_polys(&polygon) -# # we're still responsible for cleaning up the passed in `polygon`, -# # but not a problem here, since it is stack allocated -# h3lib.destroyLinkedPolygon(&polygon) + # we're still responsible for cleaning up the passed in `polygon`, + # but not a problem here, since it is stack allocated + h3lib.destroyLinkedMultiPolygon(&polygon) -# return out + return out -# def _geojson_loop(loop): -# """ Swap lat/lng order and close loop. -# """ -# loop = [e[::-1] for e in loop] -# loop += [loop[0]] +def _geojson_loop(loop): + """ Swap lat/lng order and close loop. + """ + loop = [e[::-1] for e in loop] + loop += [loop[0]] -# return loop + return loop -# def h3_set_to_multi_polygon(const H3int[:] hexes, geo_json=False): -# # todo: gotta be a more elegant way to handle these... -# if len(hexes) == 0: -# return [] +def h3_set_to_multi_polygon(const H3int[:] hexes, geo_json=False): + # todo: gotta be a more elegant way to handle these... + if len(hexes) == 0: + return [] -# multipoly = _to_multi_polygon(hexes) + multipoly = _to_multi_polygon(hexes) -# if geo_json: -# multipoly = [ -# [_geojson_loop(loop) for loop in poly] -# for poly in multipoly -# ] + if geo_json: + multipoly = [ + [_geojson_loop(loop) for loop in poly] + for poly in multipoly + ] -# return multipoly + return multipoly From 75fb4c7046a3b14bf6b413dc6a0709579189c1e9 Mon Sep 17 00:00:00 2001 From: Isaac Brodsky Date: Thu, 2 Jun 2022 15:44:24 -0700 Subject: [PATCH 09/21] v4: convert edge from h3fake2 (#253) * v4: convert edge from h3fake2 * on error not neighbors * except * * check for neighbor error --- src/h3/_cy/__init__.py | 3 +- src/h3/_cy/edges.pxd | 20 ++--- src/h3/_cy/edges.pyx | 165 ++++++++++++++++++++++++----------------- src/h3/_cy/h3lib.pxd | 31 ++++---- 4 files changed, 119 insertions(+), 100 deletions(-) diff --git a/src/h3/_cy/__init__.py b/src/h3/_cy/__init__.py index c684997f2..bb4dfc066 100644 --- a/src/h3/_cy/__init__.py +++ b/src/h3/_cy/__init__.py @@ -38,8 +38,7 @@ experimental_local_ij_to_h3, ) -# from .edges import ( -from h3fake2._cy import ( +from .edges import ( are_neighbors, edge, is_edge, diff --git a/src/h3/_cy/edges.pxd b/src/h3/_cy/edges.pxd index cdcf4d029..bf4c5c4af 100644 --- a/src/h3/_cy/edges.pxd +++ b/src/h3/_cy/edges.pxd @@ -1,11 +1,11 @@ -# from .h3lib cimport bool, H3int +from .h3lib cimport bool, H3int -# cpdef bool are_neighbors(H3int h1, H3int h2) -# cpdef H3int edge(H3int origin, H3int destination) except 1 -# cpdef bool is_edge(H3int e) -# cpdef H3int edge_origin(H3int e) except 1 -# cpdef H3int edge_destination(H3int e) except 1 -# cpdef (H3int, H3int) edge_cells(H3int e) except * -# cpdef H3int[:] edges_from_cell(H3int origin) -# cpdef double mean_edge_length(int resolution, unit=*) except -1 -# cpdef double edge_length(H3int e, unit=*) except -1 +cpdef bool are_neighbors(H3int h1, H3int h2) +cpdef H3int edge(H3int origin, H3int destination) except * +cpdef bool is_edge(H3int e) +cpdef H3int edge_origin(H3int e) except 1 +cpdef H3int edge_destination(H3int e) except 1 +cpdef (H3int, H3int) edge_cells(H3int e) except * +cpdef H3int[:] edges_from_cell(H3int origin) +cpdef double mean_edge_length(int resolution, unit=*) except -1 +cpdef double edge_length(H3int e, unit=*) except -1 diff --git a/src/h3/_cy/edges.pyx b/src/h3/_cy/edges.pyx index 27397d957..716c96a59 100644 --- a/src/h3/_cy/edges.pyx +++ b/src/h3/_cy/edges.pyx @@ -1,100 +1,125 @@ -# cimport h3lib -# from .h3lib cimport bool, H3int +cimport h3lib +from .h3lib cimport bool, H3int -# from .util cimport ( -# check_cell, -# check_edge, -# check_res, -# create_ptr, -# create_mv, -# ) +from .util cimport ( + check_cell, + check_edge, + check_res, + create_ptr, + create_mv, +) -# from .util import H3ValueError +from .util import H3ValueError -# cpdef bool are_neighbors(H3int h1, H3int h2): -# check_cell(h1) -# check_cell(h2) +cpdef bool are_neighbors(H3int h1, H3int h2): + cdef: + int out -# return h3lib.h3IndexesAreNeighbors(h1, h2) == 1 + check_cell(h1) + check_cell(h2) + error = h3lib.areNeighborCells(h1, h2, &out) + if error != 0: + return False + return out == 1 -# cpdef H3int edge(H3int origin, H3int destination) except 1: -# check_cell(origin) -# check_cell(destination) -# if h3lib.h3IndexesAreNeighbors(origin, destination) != 1: -# s = 'Cells are not neighbors: {} and {}' -# s = s.format(hex(origin), hex(destination)) -# raise H3ValueError(s) +cpdef H3int edge(H3int origin, H3int destination) except *: + cdef: + int neighbor_out + H3int out -# return h3lib.getH3UnidirectionalEdge(origin, destination) + check_cell(origin) + check_cell(destination) + error = h3lib.areNeighborCells(origin, destination, &neighbor_out) + if error != 0 or neighbor_out != 1: + s = 'Cells are not neighbors: {} and {}' + s = s.format(hex(origin), hex(destination)) + raise H3ValueError(s) -# cpdef bool is_edge(H3int e): -# return h3lib.h3UnidirectionalEdgeIsValid(e) == 1 + h3lib.cellsToDirectedEdge(origin, destination, &out) + return out -# cpdef H3int edge_origin(H3int e) except 1: -# # without the check, with an invalid input, the function will just return 0 -# check_edge(e) -# return h3lib.getOriginH3IndexFromUnidirectionalEdge(e) +cpdef bool is_edge(H3int e): + return h3lib.isValidDirectedEdge(e) == 1 -# cpdef H3int edge_destination(H3int e) except 1: -# check_edge(e) +cpdef H3int edge_origin(H3int e) except 1: + cdef: + H3int out -# return h3lib.getDestinationH3IndexFromUnidirectionalEdge(e) + # without the check, with an invalid input, the function will just return 0 + check_edge(e) -# cpdef (H3int, H3int) edge_cells(H3int e) except *: -# check_edge(e) + h3lib.getDirectedEdgeOrigin(e, &out) + return out -# return edge_origin(e), edge_destination(e) +cpdef H3int edge_destination(H3int e) except 1: + cdef: + H3int out -# cpdef H3int[:] edges_from_cell(H3int origin): -# """ Returns the 6 (or 5 for pentagons) directed edges -# for the given origin cell -# """ -# check_cell(origin) + check_edge(e) -# ptr = create_ptr(6) -# h3lib.getH3UnidirectionalEdgesFromHexagon(origin, ptr) -# mv = create_mv(ptr, 6) + h3lib.getDirectedEdgeDestination(e, &out) + return out -# return mv +cpdef (H3int, H3int) edge_cells(H3int e) except *: + check_edge(e) + return edge_origin(e), edge_destination(e) -# cpdef double mean_edge_length(int resolution, unit='km') except -1: -# check_res(resolution) +cpdef H3int[:] edges_from_cell(H3int origin): + """ Returns the 6 (or 5 for pentagons) directed edges + for the given origin cell + """ + check_cell(origin) -# length = h3lib.edgeLengthKm(resolution) + ptr = create_ptr(6) + h3lib.originToDirectedEdges(origin, ptr) + mv = create_mv(ptr, 6) -# # todo: multiple units -# convert = { -# 'km': 1.0, -# 'm': 1000.0 -# } + return mv -# try: -# length *= convert[unit] -# except: -# raise H3ValueError('Unknown unit: {}'.format(unit)) -# return length +cpdef double mean_edge_length(int resolution, unit='km') except -1: + cdef: + double length + check_res(resolution) -# cpdef double edge_length(H3int e, unit='km') except -1: -# check_edge(e) + h3lib.getHexagonEdgeLengthAvgKm(resolution, &length) -# # todo: maybe kick this logic up to the python level -# # it might be a little cleaner, because we can do the "switch statement" -# # with a dict, but would require exposing more C functions + # todo: multiple units + convert = { + 'km': 1.0, + 'm': 1000.0 + } -# if unit == 'rads': -# length = h3lib.exactEdgeLengthRads(e) -# elif unit == 'km': -# length = h3lib.exactEdgeLengthKm(e) -# elif unit == 'm': -# length = h3lib.exactEdgeLengthM(e) -# else: -# raise H3ValueError('Unknown unit: {}'.format(unit)) + try: + length *= convert[unit] + except: + raise H3ValueError('Unknown unit: {}'.format(unit)) -# return length + return length + + +cpdef double edge_length(H3int e, unit='km') except -1: + cdef: + double length + check_edge(e) + + # todo: maybe kick this logic up to the python level + # it might be a little cleaner, because we can do the "switch statement" + # with a dict, but would require exposing more C functions + + if unit == 'rads': + h3lib.exactEdgeLengthRads(e, &length) + elif unit == 'km': + h3lib.exactEdgeLengthKm(e, &length) + elif unit == 'm': + h3lib.exactEdgeLengthM(e, &length) + else: + raise H3ValueError('Unknown unit: {}'.format(unit)) + + return length diff --git a/src/h3/_cy/h3lib.pxd b/src/h3/_cy/h3lib.pxd index c3efea8bf..ba8a4e2ad 100644 --- a/src/h3/_cy/h3lib.pxd +++ b/src/h3/_cy/h3lib.pxd @@ -116,8 +116,20 @@ cdef extern from "h3api.h": H3Error gridDiskDistances(H3Index origin, int k, H3Index *out, int *distances) nogil H3Error gridRingUnsafe(H3Index origin, int k, H3Index *out) nogil - H3Error cellToBoundary(H3Index h3, CellBoundary *gp) nogil + H3Error areNeighborCells(H3Index origin, H3Index destination, int *out) nogil + H3Error cellsToDirectedEdge(H3Index origin, H3Index destination, H3Index *out) nogil + H3Error getDirectedEdgeOrigin(H3Index edge, H3Index *out) nogil + H3Error getDirectedEdgeDestination(H3Index edge, H3Index *out) nogil + H3Error originToDirectedEdges(H3Index origin, H3Index *edges) nogil + + H3Error getHexagonEdgeLengthAvgKm(int res, double *out) nogil + H3Error getHexagonEdgeLengthAvgM(int res, double *out) nogil + + H3Error exactEdgeLengthRads(H3Index edge, double *out) nogil + H3Error exactEdgeLengthKm(H3Index edge, double *out) nogil + H3Error exactEdgeLengthM(H3Index edge, double *out) nogil + H3Error cellToBoundary(H3Index h3, CellBoundary *gp) nogil H3Error directedEdgeToBoundary(H3Index edge, CellBoundary *gb) nogil double distanceRads(const LatLng *a, const LatLng *b) nogil @@ -148,21 +160,4 @@ cdef extern from "h3api.h": # void h3ToString(H3Index h, char *str, size_t sz) - # int h3IndexesAreNeighbors(H3Index origin, H3Index destination) - - # H3Index getH3UnidirectionalEdge(H3Index origin, H3Index destination) - - # H3Index getOriginH3IndexFromUnidirectionalEdge(H3Index edge) - - # H3Index getDestinationH3IndexFromUnidirectionalEdge(H3Index edge) - # void getH3IndexesFromUnidirectionalEdge(H3Index edge, H3Index *originDestination) - - # void getH3UnidirectionalEdgesFromHexagon(H3Index origin, H3Index *edges) - - # double edgeLengthKm(int res) nogil - # double edgeLengthM(int res) nogil - - # double exactEdgeLengthRads(H3Index edge) nogil - # double exactEdgeLengthKm(H3Index edge) nogil - # double exactEdgeLengthM(H3Index edge) nogil From 075da765394683988610a6fb02415acf24e5f490 Mon Sep 17 00:00:00 2001 From: AJ Friend Date: Thu, 2 Jun 2022 20:10:41 -0400 Subject: [PATCH 10/21] Fix sdist build (#239) * make sure `make_sdist` CI job still broken * reproduce locally * update h3lib * still seems broken * try sdist on mac and ubuntu * pyest! * debugging * ensurepath * Disable building docs * just build sdist once * colon Co-authored-by: Isaac Brodsky --- .github/workflows/wheels.yml | 68 ++++++++++++++++++++---------------- CMakeLists.txt | 1 + makefile | 1 - src/h3lib | 2 +- 4 files changed, 40 insertions(+), 32 deletions(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index dd6bfbc75..a6607e47d 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -18,34 +18,43 @@ on: - published jobs: - # make_sdist: - # name: SDist - # if: ${{ github.event_name != 'pull_request' || !github.event.pull_request.draft }} - # runs-on: ubuntu-latest - - # steps: - # - uses: actions/checkout@v3 - # with: - # submodules: recursive - - # - name: Make sdist - # run: | - # pipx run build --sdist - - # - name: Install from sdist - # run: | - # pip install --upgrade pip setuptools wheel - # pip install git+https://github.com/ajfriend/h3fake2.git - # cp dist/h3-*.tar.gz h3.tar.gz - # pip install h3.tar.gz[all] - - # - name: Test sdist - # run: pytest --cov=h3 --full-trace - - # - name: Upload artifacts to GitHub - # uses: actions/upload-artifact@v3 - # with: - # path: ./dist + make_sdist: + name: "SDist: ${{ matrix.os }}" + if: ${{ github.event_name != 'pull_request' || !github.event.pull_request.draft }} + runs-on: ${{ matrix.os }} + + strategy: + matrix: + os: [ubuntu-latest] + + steps: + - uses: actions/checkout@v3 + with: + submodules: recursive + + - name: Setup Python + uses: actions/setup-python@v3.1.2 + + - name: Make sdist + run: | + python --version + pipx --version + pipx run build --sdist + + - name: Install from sdist + run: | + pip install --upgrade pip setuptools wheel + pip install git+https://github.com/ajfriend/h3fake2.git pytest + cp dist/h3-*.tar.gz h3.tar.gz + pip install h3.tar.gz[all] + + - name: Test sdist + run: pytest --cov=h3 --full-trace + + - name: Upload artifacts to GitHub + uses: actions/upload-artifact@v3 + with: + path: ./dist make_cibw_v1_wheels: name: "cibuildwheel v1: ${{ matrix.name }}" @@ -168,8 +177,7 @@ jobs: path: wheelhouse/*.whl upload_all: - # needs: [make_sdist, make_cibw_v1_wheels, make_cibw_v2_wheels] - needs: [make_cibw_v1_wheels, make_cibw_v2_wheels] + needs: [make_sdist, make_cibw_v1_wheels, make_cibw_v2_wheels] runs-on: ubuntu-latest if: github.event_name == 'release' && github.event.action == 'published' diff --git a/CMakeLists.txt b/CMakeLists.txt index 00d9fe444..436ea65c3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -11,6 +11,7 @@ set(BUILD_BENCHMARKS OFF CACHE BOOL "" FORCE) set(BUILD_FILTERS OFF CACHE BOOL "" FORCE) set(BUILD_GENERATORS OFF CACHE BOOL "" FORCE) set(BUILD_TESTING OFF CACHE BOOL "" FORCE) +set(ENABLE_DOCS OFF CACHE BOOL "" FORCE) # Build the core library as static set(BUILD_SHARED_LIBS OFF) diff --git a/makefile b/makefile index 7c2845e4e..ac2e4634c 100644 --- a/makefile +++ b/makefile @@ -7,7 +7,6 @@ build-docs: open: open docs/_build/html/index.html - init: purge git submodule update --init python -m venv env diff --git a/src/h3lib b/src/h3lib index 294c20ed7..a36c614b4 160000 --- a/src/h3lib +++ b/src/h3lib @@ -1 +1 @@ -Subproject commit 294c20ed7d447122d5ab37260896d4ef7579ebfb +Subproject commit a36c614b477fb5409a0a87e0111d347ce550e40d From cfbd892780978dfba70747465069c0b28ca3f22f Mon Sep 17 00:00:00 2001 From: AJ Friend Date: Thu, 2 Jun 2022 21:11:17 -0400 Subject: [PATCH 11/21] remove h3fake2 transitional library (#257) --- .github/workflows/coverage-lint.yml | 1 - .github/workflows/tests.yml | 1 - .github/workflows/wheels.yml | 6 +----- makefile | 1 - tests/h3fake2_errors.py | 15 --------------- tests/test_cells_and_edges.py | 2 +- tests/test_length_area.py | 2 +- tests/test_polyfill.py | 2 +- 8 files changed, 4 insertions(+), 26 deletions(-) delete mode 100644 tests/h3fake2_errors.py diff --git a/.github/workflows/coverage-lint.yml b/.github/workflows/coverage-lint.yml index 511023632..6d33368be 100644 --- a/.github/workflows/coverage-lint.yml +++ b/.github/workflows/coverage-lint.yml @@ -23,7 +23,6 @@ jobs: - name: Install from source run: | pip install --upgrade pip setuptools wheel - pip install git+https://github.com/ajfriend/h3fake2.git pip install .[all] - name: Lint diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 0b9883dd7..072e21ca3 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -44,7 +44,6 @@ jobs: - name: Install from source run: | pip install --upgrade pip setuptools wheel - pip install git+https://github.com/ajfriend/h3fake2.git pip install .[all] - name: Tests diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index a6607e47d..a02f51d56 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -44,7 +44,7 @@ jobs: - name: Install from sdist run: | pip install --upgrade pip setuptools wheel - pip install git+https://github.com/ajfriend/h3fake2.git pytest + pip install pytest cp dist/h3-*.tar.gz h3.tar.gz pip install h3.tar.gz[all] @@ -84,8 +84,6 @@ jobs: CIBW_TEST_COMMAND: pytest {project}/tests CIBW_ARCHS_LINUX: auto aarch64 CIBW_BUILD: ${{ matrix.build }} - CIBW_BEFORE_BUILD: pip install git+https://github.com/ajfriend/h3fake2.git - CIBW_BEFORE_TEST: pip install git+https://github.com/ajfriend/h3fake2.git - name: Check with Twine run: | @@ -164,8 +162,6 @@ jobs: CIBW_ARCHS_MACOS: x86_64 arm64 CIBW_BUILD: ${{ matrix.build }} CIBW_SKIP: ${{ matrix.skip }} - CIBW_BEFORE_BUILD: pip install git+https://github.com/ajfriend/h3fake2.git - CIBW_BEFORE_TEST: pip install git+https://github.com/ajfriend/h3fake2.git - name: Check with Twine run: | diff --git a/makefile b/makefile index ac2e4634c..44eceb807 100644 --- a/makefile +++ b/makefile @@ -11,7 +11,6 @@ init: purge git submodule update --init python -m venv env env/bin/pip install --upgrade pip wheel setuptools - env/bin/pip install git+https://github.com/ajfriend/h3fake2.git env/bin/pip install .[all] env/bin/pip install -r requirements.in diff --git a/tests/h3fake2_errors.py b/tests/h3fake2_errors.py deleted file mode 100644 index da5688730..000000000 --- a/tests/h3fake2_errors.py +++ /dev/null @@ -1,15 +0,0 @@ -# flake8: noqa -""" -Some of the tests expect to catch certain errors. -As we transition, we'll have errors of each type. -pytest functions can accept tuples of errors to check for either one. -""" - -import h3 -import h3fake2 - -H3ValueError = (h3.H3ValueError, h3fake2.H3ValueError) -H3CellError = (h3.H3CellError, h3fake2.H3CellError) -H3ResolutionError = (h3.H3ResolutionError, h3fake2.H3ResolutionError) -H3EdgeError = (h3.H3EdgeError, h3fake2.H3EdgeError) -H3DistanceError = (h3.H3DistanceError, h3fake2.H3DistanceError) diff --git a/tests/test_cells_and_edges.py b/tests/test_cells_and_edges.py index 851dda58b..a90bb0af9 100644 --- a/tests/test_cells_and_edges.py +++ b/tests/test_cells_and_edges.py @@ -1,7 +1,7 @@ import h3 import pytest -from .h3fake2_errors import ( +from h3 import ( H3ValueError, H3CellError, H3ResolutionError, diff --git a/tests/test_length_area.py b/tests/test_length_area.py index 2a1b5d7de..8236199d6 100644 --- a/tests/test_length_area.py +++ b/tests/test_length_area.py @@ -1,7 +1,7 @@ import h3 import pytest -from .h3fake2_errors import H3ValueError +from h3 import H3ValueError def approx2(a, b): diff --git a/tests/test_polyfill.py b/tests/test_polyfill.py index a02560d1e..7c35bb622 100644 --- a/tests/test_polyfill.py +++ b/tests/test_polyfill.py @@ -2,7 +2,7 @@ import itertools import pytest -from .h3fake2_errors import H3ResolutionError +from h3 import H3ResolutionError def reverse(loop): From 95c9affd90a141a1959bf9313aba94bd4ef103bc Mon Sep 17 00:00:00 2001 From: AJ Friend Date: Mon, 6 Jun 2022 19:37:42 -0400 Subject: [PATCH 12/21] Remove more sdist files (#259) * remove scripts/examples/apps * BUILD_FUZZERS OFF * ENABLE_FORMAT off * turn off all the things (and sort options) * mac attack * see if there's a build time difference * back to the future --- CMakeLists.txt | 21 ++++++++++++++++----- MANIFEST.in | 3 --- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 436ea65c3..85ea71cc6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -7,11 +7,22 @@ set(CMAKE_POSITION_INDEPENDENT_CODE ON) set(CMAKE_BUILD_TYPE Release) # Avoid building tooling we won't need for release -set(BUILD_BENCHMARKS OFF CACHE BOOL "" FORCE) -set(BUILD_FILTERS OFF CACHE BOOL "" FORCE) -set(BUILD_GENERATORS OFF CACHE BOOL "" FORCE) -set(BUILD_TESTING OFF CACHE BOOL "" FORCE) -set(ENABLE_DOCS OFF CACHE BOOL "" FORCE) +# 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) diff --git a/MANIFEST.in b/MANIFEST.in index 47de93936..6ccde1f68 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -19,9 +19,6 @@ include src/h3lib/README.md include src/h3lib/VERSION include src/h3lib/CMakeLists.txt -graft src/h3lib/scripts -graft src/h3lib/examples graft src/h3lib/cmake graft src/h3lib/src/h3lib -graft src/h3lib/src/apps exclude MANIFEST.in From 20a6a29dae19bb22c6b61b24d77f56b70c00d63a Mon Sep 17 00:00:00 2001 From: AJ Friend Date: Thu, 28 Jul 2022 15:47:09 -0400 Subject: [PATCH 13/21] New error system (#260) * starting to play around with new error system * move enum to h3lib.pxd * this structure is weird :( * error system gets its own file * go back to using H3ErrorCodes * extern the H3ErrorCodes * remove H3Index in favor of just H3int * " to ' * import ordering * something * exception hierarchy * formatting * errors list * remove H3ResolutionError * remove H3EdgeError * remove H3CellError * remove H3DistanceError * H3FailedError * domain error * H3UnrecognizedException * most of em done * remove H3ValueError * remove some checks * ideas ideas * document what the function raises? * thoughts * lint * one half idea for DRYing (kinda) * potentially make the E_SUCCESS path faster * typos * maple syrup * cimport H3ErrorCodes as ec * clean up * whitespace * clarify cases * trying new error idiom * thanks comment * notes * applied idiom a few times now * alternative idiom? * handling some more potential errors * idioms and ideas * clean * removing check * interesting allocation example * lint * just trying to beat 2.7 for sport right now * works? * bow to lint * using enum values directly! * now that's a hierarchy * remove raise_with_msg * ij funcs * lint * remove some checks * remove err * comment * cleaned up `ring`, and i think i fixed a memory leak bug * think i fixed another memory leak bug * another memory leak bug * memory leaks! * clean * removing some comments * meh * H3Exception shouldn't be appearing as a concrete object * renaming some base exceptions * clean up * don't have a test for this anyway * documented * puttering around * words * backing up this nonsense with an airtight argument * air toight * todos * more words * skepticism * rewording * editing * ordering * tests on the error codes * more error code tests * note * address some comments * we raise! * lint * comments --- src/h3/__init__.py | 25 +++- src/h3/_cy/CMakeLists.txt | 2 + src/h3/_cy/__init__.py | 28 ++++- src/h3/_cy/cells.pyx | 202 ++++++++++++++--------------- src/h3/_cy/edges.pyx | 55 ++++---- src/h3/_cy/error_system.pxd | 3 + src/h3/_cy/error_system.pyx | 231 ++++++++++++++++++++++++++++++++++ src/h3/_cy/geo.pyx | 18 +-- src/h3/_cy/h3lib.pxd | 143 +++++++++++---------- src/h3/_cy/util.pyx | 30 ++--- src/h3/api/_api_template.py | 2 +- tests/test_cells_and_edges.py | 116 +++++++++-------- tests/test_error_codes.py | 67 ++++++++++ tests/test_h3.py | 5 +- tests/test_length_area.py | 10 +- tests/test_polyfill.py | 6 +- 16 files changed, 643 insertions(+), 300 deletions(-) create mode 100644 src/h3/_cy/error_system.pxd create mode 100644 src/h3/_cy/error_system.pyx create mode 100644 tests/test_error_codes.py diff --git a/src/h3/__init__.py b/src/h3/__init__.py index 717f9cb8f..a507e3b56 100644 --- a/src/h3/__init__.py +++ b/src/h3/__init__.py @@ -8,9 +8,26 @@ from ._cy import ( + UnknownH3ErrorCode, + H3BaseException, + + H3GridNavigationError, + H3MemoryError, H3ValueError, - H3CellError, - H3ResolutionError, - H3EdgeError, - H3DistanceError, + + H3FailedError, + H3DomainError, + H3LatLngDomainError, + H3ResDomainError, + H3CellInvalidError, + H3DirEdgeInvalidError, + H3UndirEdgeInvalidError, + H3VertexInvalidError, + H3PentagonError, + H3DuplicateInputError, + H3NotNeighborsError, + H3ResMismatchError, + H3MemoryAllocError, + H3MemoryBoundsError, + H3OptionInvalidError, ) diff --git a/src/h3/_cy/CMakeLists.txt b/src/h3/_cy/CMakeLists.txt index 386557c5b..57a3b21c5 100644 --- a/src/h3/_cy/CMakeLists.txt +++ b/src/h3/_cy/CMakeLists.txt @@ -22,6 +22,7 @@ add_cython_file(geo) add_cython_file(cells) add_cython_file(edges) add_cython_file(to_multipoly) +add_cython_file(error_system) # Include pyx and pxd files in distribution for use by Cython API install( @@ -35,6 +36,7 @@ install( h3lib.pxd util.pxd util.pyx + error_system.pyx DESTINATION src/h3/_cy ) diff --git a/src/h3/_cy/__init__.py b/src/h3/_cy/__init__.py index bb4dfc066..8496c05e3 100644 --- a/src/h3/_cy/__init__.py +++ b/src/h3/_cy/__init__.py @@ -70,9 +70,29 @@ hex2int, int2hex, from_iter, +) + +from .error_system import ( + UnknownH3ErrorCode, + H3BaseException, + + H3GridNavigationError, + H3MemoryError, H3ValueError, - H3CellError, - H3ResolutionError, - H3EdgeError, - H3DistanceError, + + H3FailedError, + H3DomainError, + H3LatLngDomainError, + H3ResDomainError, + H3CellInvalidError, + H3DirEdgeInvalidError, + H3UndirEdgeInvalidError, + H3VertexInvalidError, + H3PentagonError, + H3DuplicateInputError, + H3NotNeighborsError, + H3ResMismatchError, + H3MemoryAllocError, + H3MemoryBoundsError, + H3OptionInvalidError, ) diff --git a/src/h3/_cy/cells.pyx b/src/h3/_cy/cells.pyx index 4453e388b..f6abe997f 100644 --- a/src/h3/_cy/cells.pyx +++ b/src/h3/_cy/cells.pyx @@ -1,5 +1,5 @@ cimport h3lib -from .h3lib cimport bool, int64_t, H3int +from .h3lib cimport bool, int64_t, H3int, H3ErrorCodes from libc cimport stdlib from .util cimport ( @@ -11,7 +11,10 @@ from .util cimport ( empty_memory_view, # want to drop this import if possible ) -from .util import H3ValueError, H3ResolutionError +from .error_system cimport ( + check_for_error, + check_for_error_msg, +) # todo: add notes about Cython exception handling @@ -51,17 +54,15 @@ cpdef int distance(H3int h1, H3int h2) except -1: """ cdef: int64_t distance - h3lib.H3Error err check_cell(h1) check_cell(h2) - err = h3lib.gridDistance(h1, h2, &distance) - if err: - # todo: do error handling later - s = 'Cells are too far apart to compute distance: {} and {}' - s = s.format(hex(h1), hex(h2)) - raise H3ValueError(s) + check_for_error( + h3lib.gridDistance(h1, h2, &distance) + ) # todo: should this raise a distance error? + + # s = 'Cells are too far apart to compute distance: {} and {}' return distance @@ -70,17 +71,19 @@ cpdef H3int[:] disk(H3int h, int k): """ cdef: int64_t n - h3lib.H3Error err check_cell(h) check_distance(k) - # ignoring error for now - err = h3lib.maxGridDiskSize(k, &n) + check_for_error( + h3lib.maxGridDiskSize(k, &n) + ) ptr = create_ptr(n) # todo: return a "smart" pointer that knows its length? - err = h3lib.gridDisk(h, k, ptr) # ignoring error again! + + err = h3lib.gridDisk(h, k, ptr) mv = create_mv(ptr, n) + check_for_error(err) # needs to be after, to ensure we clean up memory return mv @@ -94,19 +97,20 @@ cpdef H3int[:] _ring_fallback(H3int h, int k): """ cdef: int64_t n - h3lib.H3Error err check_cell(h) check_distance(k) - err = h3lib.maxGridDiskSize(k, &n) + check_for_error( + h3lib.maxGridDiskSize(k, &n) + ) # array of h3 cells ptr = create_ptr(n) # array of cell distances from `h` dist_ptr = stdlib.calloc(n, sizeof(int)) if dist_ptr is NULL: - raise MemoryError() + raise MemoryError() #todo: H3 memory error? err = h3lib.gridDiskDistances(h, k, ptr, dist_ptr) @@ -119,15 +123,15 @@ cpdef H3int[:] _ring_fallback(H3int h, int k): mv = create_mv(ptr, n) + check_for_error(err) # note: need to move this down here to make sure ptr gets freed + # again, easier with a memory manager object + return mv cpdef H3int[:] ring(H3int h, int k): """ Return cells at grid distance `== k` from `h`. Collection is "hollow" for k >= 1. """ - cdef: - h3lib.H3Error err - check_cell(h) check_distance(k) @@ -136,33 +140,33 @@ cpdef H3int[:] ring(H3int h, int k): err = h3lib.gridRingUnsafe(h, k, ptr) - # if we drop into the failure state, we might be tempted to not create - # this mv, but creating the mv is exactly what guarantees that we'll free - # the memory. context manager would be better here, if we can figure out - # how to do that - mv = create_mv(ptr, n) - - if err: + if err == H3ErrorCodes.E_SUCCESS: + mv = create_mv(ptr, n) + elif err == H3ErrorCodes.E_PENTAGON: + mv = create_mv(ptr, n) # need to create this to guarantee memory is freed. + # better done with a Cython object, i think. mv = _ring_fallback(h, k) + else: + mv = create_mv(ptr, n) # need to create this to guarantee memory is freed. + # definitely better done with a Cython "memory manager" object. + check_for_error(err) return mv cpdef H3int parent(H3int h, res=None) except 0: cdef: H3int parent - h3lib.H3Error err - check_cell(h) + check_cell(h) # todo: do we want to check for validity here? or leave correctness to the user? if res is None: res = resolution(h) - 1 - if res > resolution(h): - msg = 'Invalid parent resolution {} for cell {}.' - msg = msg.format(res, hex(h)) - raise H3ResolutionError(msg) - check_res(res) 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 @@ -170,24 +174,23 @@ cpdef H3int parent(H3int h, res=None) except 0: cpdef H3int[:] children(H3int h, res=None): cdef: H3int child - h3lib.H3Error err int64_t N check_cell(h) if res is None: res = resolution(h) + 1 - if res < resolution(h): - msg = 'Invalid child resolution {} for cell {}.' - msg = msg.format(res, hex(h)) - raise H3ResolutionError(msg) - check_res(res) 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) ptr = create_ptr(N) err = h3lib.cellToChildren(h, res, ptr) mv = create_mv(ptr, N) + check_for_error(err) # needs to be after, to ensure memory is freed! return mv @@ -195,19 +198,17 @@ cpdef H3int[:] children(H3int h, res=None): cpdef H3int center_child(H3int h, res=None) except 0: cdef: H3int child - h3lib.H3Error err check_cell(h) if res is None: res = resolution(h) + 1 - if res < resolution(h): - msg = 'Invalid child resolution {} for cell {}.' - msg = msg.format(res, hex(h)) - raise H3ResolutionError(msg) - check_res(res) 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 @@ -219,8 +220,6 @@ cpdef H3int[:] compact(const H3int[:] hu): # `&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! - cdef: - h3lib.H3Error err if len(hu) == 0: return empty_memory_view() @@ -232,9 +231,8 @@ cpdef H3int[:] compact(const H3int[:] hu): err = h3lib.compactCells(&hu[0], ptr, len(hu)) mv = create_mv(ptr, len(hu)) - if err: - # todo: additional error processing - raise H3ValueError('Could not compact set of hexagons!') + # todo: fix weird error ordering due to memory freeing... + check_for_error(err) return mv @@ -248,7 +246,6 @@ cpdef H3int[:] uncompact(const H3int[:] hc, int res): # length of zero and returning early seems like the easiest solution. # note: open to better ideas! cdef: - h3lib.H3Error err int64_t N if len(hc) == 0: @@ -270,31 +267,30 @@ cpdef H3int[:] uncompact(const H3int[:] hc, int res): ) mv = create_mv(ptr, N) - if err: - raise H3ValueError('Could not uncompact set of hexagons!') + # todo: fix weird error ordering due to memory freeing... + check_for_error(err) return mv cpdef int64_t num_hexagons(int resolution) except -1: - check_res(resolution) cdef: - h3lib.H3Error err int64_t num_cells - err = h3lib.getNumCells(resolution, &num_cells) + check_for_error( + h3lib.getNumCells(resolution, &num_cells) + ) return num_cells cpdef double mean_hex_area(int resolution, unit='km^2') except -1: cdef: - h3lib.H3Error err double area - check_res(resolution) - - err = h3lib.getHexagonAreaAvgKm2(resolution, &area) + check_for_error( + h3lib.getHexagonAreaAvgKm2(resolution, &area) + ) # todo: multiple units convert = { @@ -305,18 +301,15 @@ cpdef double mean_hex_area(int resolution, unit='km^2') except -1: try: area *= convert[unit] except: - raise H3ValueError('Unknown unit: {}'.format(unit)) + raise ValueError('Unknown unit: {}'.format(unit)) return area cpdef double cell_area(H3int h, unit='km^2') except -1: cdef: - h3lib.H3Error err double area - check_cell(h) - if unit == 'rads^2': err = h3lib.cellAreaRads2(h, &area) elif unit == 'km^2': @@ -324,34 +317,35 @@ cpdef double cell_area(H3int h, unit='km^2') except -1: elif unit == 'm^2': err = h3lib.cellAreaM2(h, &area) else: - raise H3ValueError('Unknown unit: {}'.format(unit)) + 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[:] line(H3int start, H3int end): cdef: - h3lib.H3Error err int64_t n - check_cell(start) - check_cell(end) - + # 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) - if err: - s = "Couldn't find line between cells {} and {}" - s = s.format(hex(start), hex(end)) - raise H3ValueError(s) + could_not_find_line(err, start, end) ptr = create_ptr(n) err = h3lib.gridPathCells(start, end, ptr) mv = create_mv(ptr, n) - if err: - s = "Couldn't find line between cells {} and {}" - s = s.format(hex(start), hex(end)) - raise H3ValueError(s) + could_not_find_line(err, start, end) return mv @@ -360,46 +354,45 @@ cpdef bool is_res_class_iii(H3int h): cpdef H3int[:] get_pentagon_indexes(int res): - cdef: - h3lib.H3Error err - - check_res(res) - n = h3lib.pentagonCount() + # todo note: this is the tricky situation where we need the memory manager ptr = create_ptr(n) err = h3lib.getPentagons(res, ptr) mv = create_mv(ptr, n) + check_for_error(err) + return mv cpdef H3int[:] get_res0_indexes(): - cdef: - h3lib.H3Error err - n = h3lib.res0CellCount() + # todo note: this is the tricky situation where we need the memory manager ptr = create_ptr(n) err = h3lib.getRes0Cells(ptr) mv = create_mv(ptr, n) + check_for_error(err) + return mv cpdef get_faces(H3int h): cdef: - h3lib.H3Error err int n - check_cell(h) - - err = h3lib.maxFaceCount(h, &n) #ignore error for now + check_for_error( + h3lib.maxFaceCount(h, &n) + ) cdef int* ptr = stdlib.calloc(n, sizeof(int)) if (n > 0) and (not ptr): raise MemoryError() - err = h3lib.getIcosahedronFaces(h, ptr) # handle error? + check_for_error( + h3lib.getIcosahedronFaces(h, ptr) + ) faces = ptr faces = {f for f in faces if f >= 0} @@ -410,36 +403,27 @@ cpdef get_faces(H3int h): cpdef (int, int) experimental_h3_to_local_ij(H3int origin, H3int h) except *: cdef: - int flag h3lib.CoordIJ c - check_cell(origin) - check_cell(h) - - flag = h3lib.cellToLocalIj(origin, h, 0, &c) - - if flag != 0: - s = "Couldn't find local (i,j) between cells {} and {}." - s = s.format(hex(origin), hex(h)) - raise H3ValueError(s) + 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 experimental_local_ij_to_h3(H3int origin, int i, int j) except 0: cdef: - int flag h3lib.CoordIJ c H3int out - check_cell(origin) - c.i, c.j = i, j - flag = h3lib.localIjToCell(origin, &c, 0, &out) - - if flag != 0: - s = "Couldn't find cell at local ({},{}) from cell {}." - s = s.format(i, j, hex(origin)) - raise H3ValueError(s) + 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 diff --git a/src/h3/_cy/edges.pyx b/src/h3/_cy/edges.pyx index 716c96a59..2df0795c7 100644 --- a/src/h3/_cy/edges.pyx +++ b/src/h3/_cy/edges.pyx @@ -2,25 +2,25 @@ cimport h3lib from .h3lib cimport bool, H3int from .util cimport ( - check_cell, - check_edge, - check_res, create_ptr, create_mv, ) -from .util import H3ValueError +from .error_system cimport check_for_error +# todo: make bint cpdef bool are_neighbors(H3int h1, H3int h2): cdef: int out - check_cell(h1) - check_cell(h2) + err = h3lib.areNeighborCells(h1, h2, &out) - error = h3lib.areNeighborCells(h1, h2, &out) - if error != 0: + # 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 @@ -29,16 +29,10 @@ cpdef H3int edge(H3int origin, H3int destination) except *: int neighbor_out H3int out - check_cell(origin) - check_cell(destination) - - error = h3lib.areNeighborCells(origin, destination, &neighbor_out) - if error != 0 or neighbor_out != 1: - s = 'Cells are not neighbors: {} and {}' - s = s.format(hex(origin), hex(destination)) - raise H3ValueError(s) + check_for_error( + h3lib.cellsToDirectedEdge(origin, destination, &out) + ) - h3lib.cellsToDirectedEdge(origin, destination, &out) return out @@ -49,34 +43,36 @@ cpdef H3int edge_origin(H3int e) except 1: cdef: H3int out - # without the check, with an invalid input, the function will just return 0 - check_edge(e) + check_for_error( + h3lib.getDirectedEdgeOrigin(e, &out) + ) - h3lib.getDirectedEdgeOrigin(e, &out) return out cpdef H3int edge_destination(H3int e) except 1: cdef: H3int out - check_edge(e) + check_for_error( + h3lib.getDirectedEdgeDestination(e, &out) + ) - h3lib.getDirectedEdgeDestination(e, &out) return out cpdef (H3int, H3int) edge_cells(H3int e) except *: - check_edge(e) - return edge_origin(e), edge_destination(e) cpdef H3int[:] edges_from_cell(H3int origin): """ Returns the 6 (or 5 for pentagons) directed edges for the given origin cell """ - check_cell(origin) ptr = create_ptr(6) - h3lib.originToDirectedEdges(origin, ptr) + + check_for_error( + h3lib.originToDirectedEdges(origin, ptr) + ) + mv = create_mv(ptr, 6) return mv @@ -86,8 +82,6 @@ cpdef double mean_edge_length(int resolution, unit='km') except -1: cdef: double length - check_res(resolution) - h3lib.getHexagonEdgeLengthAvgKm(resolution, &length) # todo: multiple units @@ -99,7 +93,7 @@ cpdef double mean_edge_length(int resolution, unit='km') except -1: try: length *= convert[unit] except: - raise H3ValueError('Unknown unit: {}'.format(unit)) + raise ValueError('Unknown unit: {}'.format(unit)) return length @@ -107,7 +101,6 @@ cpdef double mean_edge_length(int resolution, unit='km') except -1: cpdef double edge_length(H3int e, unit='km') except -1: cdef: double length - check_edge(e) # todo: maybe kick this logic up to the python level # it might be a little cleaner, because we can do the "switch statement" @@ -120,6 +113,6 @@ cpdef double edge_length(H3int e, unit='km') except -1: elif unit == 'm': h3lib.exactEdgeLengthM(e, &length) else: - raise H3ValueError('Unknown unit: {}'.format(unit)) + raise ValueError('Unknown unit: {}'.format(unit)) return length diff --git a/src/h3/_cy/error_system.pxd b/src/h3/_cy/error_system.pxd new file mode 100644 index 000000000..af8c709bc --- /dev/null +++ b/src/h3/_cy/error_system.pxd @@ -0,0 +1,3 @@ +from .h3lib cimport H3Error +cdef check_for_error(H3Error err) +cdef check_for_error_msg(H3Error err, str msg) diff --git a/src/h3/_cy/error_system.pyx b/src/h3/_cy/error_system.pyx new file mode 100644 index 000000000..799e4f220 --- /dev/null +++ b/src/h3/_cy/error_system.pyx @@ -0,0 +1,231 @@ +""" +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 + + +# 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, + E_MEMORY_BOUNDS, + E_OPTION_INVALID, +) + +@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): ... + + +""" +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: H3MemoryAllocError, + E_MEMORY_BOUNDS: H3MemoryBoundsError, + E_OPTION_INVALID: H3OptionInvalidError, +} + +# 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? + +cdef code_to_exception(H3Error err): + if err == E_SUCCESS: + return None + elif err in error_mapping: + return error_mapping[err] + else: + raise UnknownH3ErrorCode(err) + +cdef check_for_error(H3Error err): + ex = code_to_exception(err) + if ex: + raise ex + +# 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 = code_to_exception(err) + if ex: + raise ex(msg) diff --git a/src/h3/_cy/geo.pyx b/src/h3/_cy/geo.pyx index 7a737e28b..b40039b34 100644 --- a/src/h3/_cy/geo.pyx +++ b/src/h3/_cy/geo.pyx @@ -12,20 +12,19 @@ from .util cimport ( from libc cimport stdlib from libc.stdint cimport uint64_t -from .util import H3ValueError +from .error_system cimport check_for_error cpdef H3int geo_to_h3(double lat, double lng, int res) except 1: cdef: h3lib.LatLng c H3int out - h3lib.H3Error err - check_res(res) c = deg2coord(lat, lng) - # todo: just ignoring the error for now, check in the future - err = h3lib.latLngToCell(&c, res, &out) + check_for_error( + h3lib.latLngToCell(&c, res, &out) + ) return out @@ -34,11 +33,14 @@ cpdef (double, double) h3_to_geo(H3int h) except *: """Map an H3 cell into its centroid geo-coordinate (lat/lng)""" cdef: h3lib.LatLng c - h3lib.H3Error err 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? - err = h3lib.cellToLatLng(h, &c) + check_for_error( + h3lib.cellToLatLng(h, &c) + ) return coord2deg(c) @@ -281,6 +283,6 @@ cpdef double point_dist( elif unit == 'm': d = h3lib.distanceM(&a, &b) else: - raise H3ValueError('Unknown unit: {}'.format(unit)) + raise ValueError('Unknown unit: {}'.format(unit)) return d diff --git a/src/h3/_cy/h3lib.pxd b/src/h3/_cy/h3lib.pxd index ba8a4e2ad..0ff65e31f 100644 --- a/src/h3/_cy/h3lib.pxd +++ b/src/h3/_cy/h3lib.pxd @@ -1,23 +1,40 @@ from cpython cimport bool -from libc.stdint cimport uint64_t, int64_t, uint32_t +from libc.stdint cimport uint32_t, uint64_t, int64_t -ctypedef uint64_t H3int -ctypedef uint32_t H3Error -ctypedef basestring H3str +ctypedef basestring H3str # todo: do we really need this str one? -cdef extern from "h3api.h": +cdef extern from 'h3api.h': cdef int H3_VERSION_MAJOR cdef int H3_VERSION_MINOR cdef int H3_VERSION_PATCH - ctypedef uint64_t H3Index + 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 = 13 + E_MEMORY_BOUNDS = 14 + E_OPTION_INVALID = 15 ctypedef struct LatLng: double lat # in radians double lng # in radians ctypedef struct CellBoundary: - int num_verts "numVerts" + int num_verts 'numVerts' LatLng verts[10] # MAX_CELL_BNDRY_VERTS ctypedef struct CoordIJ: @@ -25,18 +42,18 @@ cdef extern from "h3api.h": int j ctypedef struct LinkedLatLng: - LatLng data "vertex" + LatLng data 'vertex' LinkedLatLng *next # renaming these for clarity ctypedef struct LinkedGeoLoop: - LinkedLatLng *data "first" - LinkedLatLng *_data_last "last" # not needed in Cython bindings + LinkedLatLng *data 'first' + LinkedLatLng *_data_last 'last' # not needed in Cython bindings LinkedGeoLoop *next ctypedef struct LinkedGeoPolygon: - LinkedGeoLoop *data "first" - LinkedGeoLoop *_data_last "last" # not needed in Cython bindings + LinkedGeoLoop *data 'first' + LinkedGeoLoop *_data_last 'last' # not needed in Cython bindings LinkedGeoPolygon *next ctypedef struct GeoLoop: @@ -48,45 +65,45 @@ cdef extern from "h3api.h": int numHoles GeoLoop *holes - int isValidCell(H3Index h) nogil - int isPentagon(H3Index h) nogil - int isResClassIII(H3Index h) nogil - int isValidDirectedEdge(H3Index edge) nogil + int isValidCell(H3int h) nogil + int isPentagon(H3int h) nogil + int isResClassIII(H3int h) nogil + int isValidDirectedEdge(H3int edge) nogil double degsToRads(double degrees) nogil double radsToDegs(double radians) nogil - int getResolution(H3Index h) nogil - int getBaseCellNumber(H3Index h) nogil + int getResolution(H3int h) nogil + int getBaseCellNumber(H3int h) nogil - H3Error latLngToCell(const LatLng *g, int res, H3Index *out) nogil - H3Error cellToLatLng(H3Index h, LatLng *) nogil - H3Error gridDistance(H3Index h1, H3Index h2, int64_t *distance) 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 maxGridDiskSize(int k, int64_t *out) nogil # num/out/N? - H3Error gridDisk(H3Index h, int k, H3Index *out) nogil + H3Error gridDisk(H3int h, int k, H3int *out) nogil - H3Error cellToParent( H3Index h, int parentRes, H3Index *parent) nogil - H3Error cellToCenterChild(H3Index h, int childRes, H3Index *child) nogil + H3Error cellToParent( H3int h, int parentRes, H3int *parent) nogil + H3Error cellToCenterChild(H3int h, int childRes, H3int *child) nogil - H3Error cellToChildrenSize(H3Index h, int childRes, int64_t *num) nogil # num/out/N? - H3Error cellToChildren( H3Index h, int childRes, H3Index *children) 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 H3Index *cells_u, - H3Index *cells_c, + const H3int *cells_u, + H3int *cells_c, const int num_u ) nogil H3Error uncompactCellsSize( - const H3Index *cells_c, + const H3int *cells_c, const int64_t num_c, const int res, int64_t *num_u ) nogil H3Error uncompactCells( - const H3Index *cells_c, + const H3int *cells_c, const int num_c, - H3Index *cells_u, + H3int *cells_u, const int num_u, const int res ) nogil @@ -94,70 +111,70 @@ cdef extern from "h3api.h": H3Error getNumCells(int res, int64_t *out) nogil int pentagonCount() nogil int res0CellCount() nogil - H3Error getPentagons(int res, H3Index *out) nogil - H3Error getRes0Cells(H3Index *out) nogil + H3Error getPentagons(int res, H3int *out) nogil + H3Error getRes0Cells(H3int *out) nogil - H3Error gridPathCellsSize(H3Index start, H3Index end, int64_t *size) nogil - H3Error gridPathCells(H3Index start, H3Index end, H3Index *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(H3Index h, double *out) nogil - H3Error cellAreaKm2(H3Index h, double *out) nogil - H3Error cellAreaM2(H3Index h, 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(H3Index h, int *out) nogil - H3Error getIcosahedronFaces(H3Index h3, int *out) nogil + H3Error maxFaceCount(H3int h, int *out) nogil + H3Error getIcosahedronFaces(H3int h3, int *out) nogil - H3Error cellToLocalIj(H3Index origin, H3Index h3, uint32_t mode, CoordIJ *out) nogil - H3Error localIjToCell(H3Index origin, const CoordIJ *ij, uint32_t mode, H3Index *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(H3Index origin, int k, H3Index *out, int *distances) nogil - H3Error gridRingUnsafe(H3Index origin, int k, H3Index *out) nogil + H3Error gridDiskDistances(H3int origin, int k, H3int *out, int *distances) nogil + H3Error gridRingUnsafe(H3int origin, int k, H3int *out) nogil - H3Error areNeighborCells(H3Index origin, H3Index destination, int *out) nogil - H3Error cellsToDirectedEdge(H3Index origin, H3Index destination, H3Index *out) nogil - H3Error getDirectedEdgeOrigin(H3Index edge, H3Index *out) nogil - H3Error getDirectedEdgeDestination(H3Index edge, H3Index *out) nogil - H3Error originToDirectedEdges(H3Index origin, H3Index *edges) 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 H3Error getHexagonEdgeLengthAvgKm(int res, double *out) nogil H3Error getHexagonEdgeLengthAvgM(int res, double *out) nogil - H3Error exactEdgeLengthRads(H3Index edge, double *out) nogil - H3Error exactEdgeLengthKm(H3Index edge, double *out) nogil - H3Error exactEdgeLengthM(H3Index edge, double *out) nogil + H3Error exactEdgeLengthRads(H3int edge, double *out) nogil + H3Error exactEdgeLengthKm(H3int edge, double *out) nogil + H3Error exactEdgeLengthM(H3int edge, double *out) nogil - H3Error cellToBoundary(H3Index h3, CellBoundary *gp) nogil - H3Error directedEdgeToBoundary(H3Index edge, CellBoundary *gb) nogil + H3Error cellToBoundary(H3int h3, CellBoundary *gp) nogil + H3Error directedEdgeToBoundary(H3int edge, CellBoundary *gb) nogil double distanceRads(const LatLng *a, const LatLng *b) nogil double distanceKm(const LatLng *a, const LatLng *b) nogil double distanceM(const LatLng *a, const LatLng *b) nogil - H3Error cellsToLinkedMultiPolygon(const H3Index *h3Set, const int numHexes, LinkedGeoPolygon *out) + H3Error cellsToLinkedMultiPolygon(const H3int *h3Set, const int numHexes, LinkedGeoPolygon *out) void destroyLinkedMultiPolygon(LinkedGeoPolygon *polygon) H3Error maxPolygonToCellsSize(const GeoPolygon *geoPolygon, int res, uint32_t flags, uint64_t *count) - H3Error polygonToCells(const GeoPolygon *geoPolygon, int res, uint32_t flags, H3Index *out) + H3Error polygonToCells(const GeoPolygon *geoPolygon, int res, uint32_t flags, H3int *out) # ctypedef struct GeoMultiPolygon: # int numPolygons # GeoPolygon *polygons - # int hexRange(H3Index origin, int k, H3Index *out) + # int hexRange(H3int origin, int k, H3int *out) - # int hexRangeDistances(H3Index origin, int k, H3Index *out, int *distances) + # int hexRangeDistances(H3int origin, int k, H3int *out, int *distances) - # int hexRanges(H3Index *h3Set, int length, int k, H3Index *out) + # int hexRanges(H3int *h3Set, int length, int k, H3int *out) - # void h3SetToLinkedGeo(const H3Index *h3Set, const int numHexes, LinkedGeoPolygon *out) + # void h3SetToLinkedGeo(const H3int *h3Set, const int numHexes, LinkedGeoPolygon *out) # void destroyLinkedPolygon(LinkedGeoPolygon *polygon) - # H3Index stringToH3(const char *str) + # H3int stringToH3(const char *str) - # void h3ToString(H3Index h, char *str, size_t sz) + # void h3ToString(H3int h, char *str, size_t sz) - # void getH3IndexesFromUnidirectionalEdge(H3Index edge, H3Index *originDestination) + # void getH3intesFromUnidirectionalEdge(H3int edge, H3int *originDestination) diff --git a/src/h3/_cy/util.pyx b/src/h3/_cy/util.pyx index bbb63e4fb..fff021641 100644 --- a/src/h3/_cy/util.pyx +++ b/src/h3/_cy/util.pyx @@ -4,6 +4,13 @@ from .h3lib cimport H3int, H3str, isValidCell, isValidDirectedEdge cimport h3lib +from .error_system import ( + H3ResDomainError, + H3DomainError, + H3DirEdgeInvalidError, + H3CellInvalidError, +) + cdef h3lib.LatLng deg2coord(double lat, double lng) nogil: cdef: @@ -48,21 +55,6 @@ cpdef H3str int2hex(H3int x): return '{:x}'.format(x) -class H3ValueError(ValueError): - pass - -class H3CellError(H3ValueError): - pass - -class H3EdgeError(H3ValueError): - pass - -class H3ResolutionError(H3ValueError): - pass - -class H3DistanceError(H3ValueError): - pass - cdef check_cell(H3int h): """ Check if valid H3 "cell" (hexagon or pentagon). @@ -78,19 +70,19 @@ cdef check_cell(H3int h): `str` inputs. """ if isValidCell(h) == 0: - raise H3CellError('Integer is not a valid H3 cell: {}'.format(hex(h))) + raise H3CellInvalidError('Integer is not a valid H3 cell: {}'.format(hex(h))) cdef check_edge(H3int e): if isValidDirectedEdge(e) == 0: - raise H3EdgeError('Integer is not a valid H3 edge: {}'.format(hex(e))) + raise H3DirEdgeInvalidError('Integer is not a valid H3 edge: {}'.format(hex(e))) cdef check_res(int res): if (res < 0) or (res > 15): - raise H3ResolutionError(res) + raise H3ResDomainError(res) cdef check_distance(int k): if k < 0: - raise H3DistanceError( + raise H3DomainError( 'Grid distances must be nonnegative. Received: {}'.format(k) ) diff --git a/src/h3/api/_api_template.py b/src/h3/api/_api_template.py index 499c0403f..fa9082ea3 100644 --- a/src/h3/api/_api_template.py +++ b/src/h3/api/_api_template.py @@ -254,7 +254,7 @@ def h3_distance(self, h1, h2): path between the cells in the graph formed by connecting adjacent cells. - This function will return an H3ValueError if the + This function will raise an exception if the cells are too far apart to compute the distance. Parameters diff --git a/tests/test_cells_and_edges.py b/tests/test_cells_and_edges.py index a90bb0af9..ccca3421b 100644 --- a/tests/test_cells_and_edges.py +++ b/tests/test_cells_and_edges.py @@ -2,11 +2,12 @@ import pytest from h3 import ( - H3ValueError, - H3CellError, - H3ResolutionError, - H3EdgeError, - H3DistanceError, + H3FailedError, + H3ResDomainError, + H3DomainError, + H3ResMismatchError, + H3CellInvalidError, + H3NotNeighborsError, ) @@ -61,12 +62,12 @@ def test4(): def test_k_ring_distance(): - with pytest.raises(H3DistanceError): + with pytest.raises(H3DomainError): h3.k_ring('8928308280fffff', -10) def test_hex_ring_distance(): - with pytest.raises(H3DistanceError): + with pytest.raises(H3DomainError): h3.hex_ring('8928308280fffff', -10) @@ -114,7 +115,7 @@ def test8(): assert not h3.h3_is_valid(h_bad) # other methods should validate and raise exception if bad input - with pytest.raises(H3CellError): + with pytest.raises(H3CellInvalidError): h3.h3_get_resolution(h_bad) @@ -130,15 +131,34 @@ def test_parent(): assert h3.h3_to_parent(h, 8) == '8828308281fffff' assert h3.h3_to_parent(h, 9) == h - with pytest.raises(H3ResolutionError): + with pytest.raises(H3ResMismatchError): h3.h3_to_parent(h, 10) +def test_parent_err(): + # Test 1 + h = '8075fffffffffff' # geo_to_h3(0,0,0) + + with pytest.raises(H3ResDomainError): + h3.h3_to_parent(h) + + # Test 2 + try: + h3.h3_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.string_to_h3(h))) + + assert msg == expected + + def test_children(): h = '8928308280fffff' - # one above should raise an exception - with pytest.raises(H3ResolutionError): + with pytest.raises(H3ResDomainError): h3.h3_to_children(h, 8) # same resolution is set of just cell itself @@ -160,7 +180,7 @@ def test_children(): # finest resolution cell should return error for children h = '8f04ccb2c45e225' - with pytest.raises(H3ResolutionError): + with pytest.raises(H3ResDomainError): h3.h3_to_children(h) @@ -168,7 +188,7 @@ def test_center_child(): h = '8928308280fffff' # one above should raise an exception - with pytest.raises(H3ResolutionError): + with pytest.raises(H3ResDomainError): h3.h3_to_center_child(h, 8) # same resolution should be same cell @@ -180,7 +200,7 @@ def test_center_child(): # finest resolution hex should return error for child h = '8f04ccb2c45e225' - with pytest.raises(H3ResolutionError): + with pytest.raises(H3ResDomainError): h3.h3_to_center_child(h) @@ -196,10 +216,14 @@ def test_distance(): 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(H3ValueError): + with pytest.raises(H3FailedError): h3.h3_distance(h1, h2) @@ -351,38 +375,38 @@ def test_edge_boundary(): def test_validation(): h = '8a28308280fffff' # invalid cell - with pytest.raises(H3CellError): + with pytest.raises(H3CellInvalidError): h3.h3_get_base_cell(h) - with pytest.raises(H3CellError): + with pytest.raises(H3CellInvalidError): h3.h3_get_resolution(h) - with pytest.raises(H3CellError): + with pytest.raises(H3CellInvalidError): h3.h3_to_parent(h, 9) - with pytest.raises(H3CellError): + with pytest.raises(H3CellInvalidError): h3.h3_distance(h, h) - with pytest.raises(H3CellError): + with pytest.raises(H3CellInvalidError): h3.k_ring(h, 1) - with pytest.raises(H3CellError): + with pytest.raises(H3CellInvalidError): h3.hex_ring(h, 1) - with pytest.raises(H3CellError): + with pytest.raises(H3CellInvalidError): h3.h3_to_children(h, 11) - with pytest.raises(H3CellError): + with pytest.raises(H3CellInvalidError): h3.compact({h}) - with pytest.raises(H3CellError): + with pytest.raises(H3CellInvalidError): h3.uncompact({h}, 10) def test_validation2(): h = '8928308280fffff' - with pytest.raises(H3ResolutionError): + with pytest.raises(H3ResDomainError): h3.h3_to_children(h, 17) assert not h3.h3_indexes_are_neighbors(h, h) @@ -391,40 +415,37 @@ def test_validation2(): def test_validation_geo(): h = '8a28308280fffff' # invalid cell - with pytest.raises(H3CellError): + with pytest.raises(H3CellInvalidError): h3.h3_to_geo(h) - with pytest.raises(H3ResolutionError): + with pytest.raises(H3ResDomainError): h3.geo_to_h3(0, 0, 17) - with pytest.raises(H3CellError): + with pytest.raises(H3CellInvalidError): h3.h3_to_geo_boundary(h) - with pytest.raises(H3CellError): - h3.h3_indexes_are_neighbors(h, 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.h3_indexes_are_neighbors(h, h) def test_edges(): h = '8928308280fffff' - with pytest.raises(H3ValueError): + with pytest.raises(H3NotNeighborsError): h3.get_h3_unidirectional_edge(h, h) h2 = h3.hex_ring(h, 2).pop() - with pytest.raises(H3ValueError): + with pytest.raises(H3NotNeighborsError): h3.get_h3_unidirectional_edge(h, h2) e_bad = '14928308280ffff1' assert not h3.h3_unidirectional_edge_is_valid(e_bad) - with pytest.raises(H3EdgeError): - h3.get_origin_h3_index_from_unidirectional_edge(e_bad) - - with pytest.raises(H3EdgeError): - h3.get_destination_h3_index_from_unidirectional_edge(e_bad) - - with pytest.raises(H3EdgeError): - h3.get_h3_indexes_from_unidirectional_edge(e_bad) + # note: won't raise an error on bad input + h3.get_origin_h3_index_from_unidirectional_edge(e_bad) + h3.get_destination_h3_index_from_unidirectional_edge(e_bad) + h3.get_h3_indexes_from_unidirectional_edge(e_bad) def test_line(): @@ -523,7 +544,7 @@ def test_uncompact_cell_input(): # inputting a single cell string can raise weird errors. # Ensure we get a reasonably helpful answer - with pytest.raises(H3CellError): + with pytest.raises(H3CellInvalidError): h3.uncompact('8001fffffffffff', 1) @@ -554,13 +575,6 @@ def test_get_res0_indexes(): assert sub < out -def test_get_faces_invalid(): - h = '8a28308280fffff' # invalid cell - - with pytest.raises(H3CellError): - h3.h3_get_faces(h) - - def test_get_faces(): h = '804dfffffffffff' expected = {2, 3, 7, 8, 12} @@ -583,7 +597,9 @@ def test_to_local_ij_error(): # error if we cross a face nb = h3.hex_ring(h, k=2) - with pytest.raises(H3ValueError): + + # todo: should this be the E_TOO_FAR guy? + with pytest.raises(H3FailedError): [h3.experimental_h3_to_local_ij(h, p) for p in nb] # should be fine if we do not cross a face @@ -599,7 +615,7 @@ def test_from_local_ij_error(): baddies = [(1, -1), (-1, 1), (-1, -1)] for i, j in baddies: - with pytest.raises(H3ValueError): + with pytest.raises(H3FailedError): h3.experimental_local_ij_to_h3(h, i, j) # inverting output should give good data diff --git a/tests/test_error_codes.py b/tests/test_error_codes.py new file mode 100644 index 000000000..d8229b320 --- /dev/null +++ b/tests/test_error_codes.py @@ -0,0 +1,67 @@ +import pytest + +import h3 + +# 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, + + h3.H3FailedError: 1, + h3.H3DomainError: 2, + h3.H3LatLngDomainError: 3, + h3.H3ResDomainError: 4, + h3.H3CellInvalidError: 5, + h3.H3DirEdgeInvalidError: 6, + h3.H3UndirEdgeInvalidError: 7, + h3.H3VertexInvalidError: 8, + h3.H3PentagonError: 9, + h3.H3DuplicateInputError: 10, + h3.H3NotNeighborsError: 11, + h3.H3ResMismatchError: 12, + h3.H3MemoryAllocError: 13, + h3.H3MemoryBoundsError: 14, + h3.H3OptionInvalidError: 15, +} + + +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_atributes(): + # 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,) diff --git a/tests/test_h3.py b/tests/test_h3.py index 09a1e7dbf..73b18692e 100644 --- a/tests/test_h3.py +++ b/tests/test_h3.py @@ -877,6 +877,7 @@ def test_h3_is_pentagon(): def test_h3_indexes_are_neighbors(): assert h3.h3_indexes_are_neighbors('8928308280fffff', '8928308280bffff') + assert not h3.h3_indexes_are_neighbors('821c07fffffffff', '8928308280fffff') @@ -884,7 +885,7 @@ def test_get_h3_unidirectional_edge(): out = h3.get_h3_unidirectional_edge('8928308280fffff', '8928308280bffff') assert h3.h3_unidirectional_edge_is_valid(out) - with pytest.raises(ValueError): + with pytest.raises(h3.H3NotNeighborsError): h3.get_h3_unidirectional_edge('821c07fffffffff', '8928308280fffff') @@ -982,5 +983,5 @@ def test_h3_line(): assert out == expected - with pytest.raises(ValueError): + with pytest.raises(h3.H3ResMismatchError): h3.h3_line(h1, '8001fffffffffff') diff --git a/tests/test_length_area.py b/tests/test_length_area.py index 8236199d6..b16ef9076 100644 --- a/tests/test_length_area.py +++ b/tests/test_length_area.py @@ -1,8 +1,6 @@ import h3 import pytest -from h3 import H3ValueError - def approx2(a, b): if len(a) != len(b): @@ -103,13 +101,13 @@ def test_bad_units(): assert h3.h3_is_valid(h) assert h3.h3_unidirectional_edge_is_valid(e) - with pytest.raises(H3ValueError): + with pytest.raises(ValueError): h3.cell_area(h, unit='foot-pounds') - with pytest.raises(H3ValueError): - h3.exact_edge_length(h, unit='foot-pounds') + with pytest.raises(ValueError): + h3.exact_edge_length(e, unit='foot-pounds') - with pytest.raises(H3ValueError): + with pytest.raises(ValueError): h3.point_dist((0, 0), (0, 0), unit='foot-pounds') diff --git a/tests/test_polyfill.py b/tests/test_polyfill.py index 7c35bb622..58faaffcc 100644 --- a/tests/test_polyfill.py +++ b/tests/test_polyfill.py @@ -2,7 +2,7 @@ import itertools import pytest -from h3 import H3ResolutionError +from h3 import H3ResDomainError def reverse(loop): @@ -211,10 +211,10 @@ def test_resolution(): 'coordinates': [[]], } - with pytest.raises(H3ResolutionError): + with pytest.raises(H3ResDomainError): h3.polyfill(d, -1) - with pytest.raises(H3ResolutionError): + with pytest.raises(H3ResDomainError): h3.polyfill(d, 16) From 4be0de9c87ed907ef41424f38c7a779abe708ccc Mon Sep 17 00:00:00 2001 From: AJ Friend Date: Wed, 10 Aug 2022 03:49:23 -0400 Subject: [PATCH 14/21] Squash #261: Use new H3MemoryManager system (#268) * Squash #261: Use new H3MemoryManager system Co-authored-by: AJ Friend * add some error checks * comments * comments * clearer logical flow * arrays are different from memoryviews! * little cleaner * Revert "comments" This reverts commit 80c3f4d38213bae0b497db702233bc2ee5bb2f2e. * Revert "Revert "comments"" This reverts commit a9d6590b04eeeffedf765c9011ae5e20484a8651. * Revert "comments" This reverts commit 24904bdceaec46417a39df271b3b7e77ea7cbfcc. * comments * arrays are different from memoryviews! * try _copy_to_mv * i don't get enough opportunities to consider using the word "penultimate" * ugh, and there i go using it incorrectly... * hmmm. was this really all it was? * That's what it was! This reverts commit 7e72adb59036fd783eaa452ba05f9a65fbaae591. * clarify branches * remove simple_mv * moving around * clean up * comments * comments * comments Co-authored-by: Isaac Brodsky --- src/h3/_cy/CMakeLists.txt | 4 + src/h3/_cy/__init__.py | 5 +- src/h3/_cy/cells.pyx | 182 +++++++++++------------ src/h3/_cy/edges.pyx | 31 ++-- src/h3/_cy/geo.pyx | 38 +++-- src/h3/_cy/h3lib.pxd | 2 +- src/h3/_cy/memory.pxd | 12 ++ src/h3/_cy/memory.pyx | 248 +++++++++++++++++++++++++++++++ src/h3/_cy/util.pxd | 6 - src/h3/_cy/util.pyx | 105 ------------- src/h3/api/basic_int/_binding.py | 2 +- src/h3/api/basic_str/_binding.py | 2 +- 12 files changed, 396 insertions(+), 241 deletions(-) create mode 100644 src/h3/_cy/memory.pxd create mode 100644 src/h3/_cy/memory.pyx diff --git a/src/h3/_cy/CMakeLists.txt b/src/h3/_cy/CMakeLists.txt index 57a3b21c5..3cf59d595 100644 --- a/src/h3/_cy/CMakeLists.txt +++ b/src/h3/_cy/CMakeLists.txt @@ -23,6 +23,8 @@ add_cython_file(cells) add_cython_file(edges) add_cython_file(to_multipoly) add_cython_file(error_system) +add_cython_file(memory) + # Include pyx and pxd files in distribution for use by Cython API install( @@ -37,6 +39,8 @@ install( util.pxd util.pyx error_system.pyx + memory.pxd + memory.pyx DESTINATION src/h3/_cy ) diff --git a/src/h3/_cy/__init__.py b/src/h3/_cy/__init__.py index 8496c05e3..e61bb4e1a 100644 --- a/src/h3/_cy/__init__.py +++ b/src/h3/_cy/__init__.py @@ -69,7 +69,10 @@ c_version, hex2int, int2hex, - from_iter, +) + +from .memory import ( + iter_to_mv, ) from .error_system import ( diff --git a/src/h3/_cy/cells.pyx b/src/h3/_cy/cells.pyx index f6abe997f..de6bc6b14 100644 --- a/src/h3/_cy/cells.pyx +++ b/src/h3/_cy/cells.pyx @@ -1,14 +1,10 @@ cimport h3lib from .h3lib cimport bool, int64_t, H3int, H3ErrorCodes -from libc cimport stdlib from .util cimport ( check_cell, check_res, check_distance, - create_ptr, - create_mv, - empty_memory_view, # want to drop this import if possible ) from .error_system cimport ( @@ -16,6 +12,11 @@ from .error_system cimport ( check_for_error_msg, ) +from .memory cimport ( + H3MemoryManager, + int_mv, +) + # todo: add notes about Cython exception handling @@ -60,9 +61,7 @@ cpdef int distance(H3int h1, H3int h2) except -1: check_for_error( h3lib.gridDistance(h1, h2, &distance) - ) # todo: should this raise a distance error? - - # s = 'Cells are too far apart to compute distance: {} and {}' + ) return distance @@ -79,11 +78,11 @@ cpdef H3int[:] disk(H3int h, int k): h3lib.maxGridDiskSize(k, &n) ) - ptr = create_ptr(n) # todo: return a "smart" pointer that knows its length? - - err = h3lib.gridDisk(h, k, ptr) - mv = create_mv(ptr, n) - check_for_error(err) # needs to be after, to ensure we clean up memory + hmm = H3MemoryManager(n) + check_for_error( + h3lib.gridDisk(h, k, hmm.ptr) + ) + mv = hmm.to_mv() return mv @@ -93,10 +92,11 @@ cpdef H3int[:] _ring_fallback(H3int h, int k): `ring` tries to call `h3lib.hexRing` first; if that fails, we call this function, which relies on `h3lib.kRingDistances`. - Failures for `h3lib.hexRing` happen when the algortihm runs into a pentagon. + Failures for `h3lib.hexRing` happen when the algorithm runs into a pentagon. """ cdef: int64_t n + int[:] distances ## todo: weird, this needs to be specified to avoid errors. cython bug? check_cell(h) check_distance(k) @@ -104,27 +104,21 @@ cpdef H3int[:] _ring_fallback(H3int h, int k): check_for_error( h3lib.maxGridDiskSize(k, &n) ) - # array of h3 cells - ptr = create_ptr(n) - - # array of cell distances from `h` - dist_ptr = stdlib.calloc(n, sizeof(int)) - if dist_ptr is NULL: - raise MemoryError() #todo: H3 memory error? + hmm = H3MemoryManager(n) - err = h3lib.gridDiskDistances(h, k, ptr, dist_ptr) - - distances = dist_ptr - distances.callback_free_data = stdlib.free + # parallel array of cell distances from `h`. + # idea: instead of a memoryview object, have a my_mv object that can return a ptr, but does the correct logic when it has length 0 + # then can always just return the pointer, instead of the weird &mv[0] syntax + distances = int_mv(n) + check_for_error( + h3lib.gridDiskDistances(h, k, hmm.ptr, &distances[0]) + ) for i,v in enumerate(distances): if v != k: - ptr[i] = 0 - - mv = create_mv(ptr, n) + hmm.ptr[i] = 0 - check_for_error(err) # note: need to move this down here to make sure ptr gets freed - # again, easier with a memory manager object + mv = hmm.to_mv() return mv @@ -136,20 +130,12 @@ cpdef H3int[:] ring(H3int h, int k): check_distance(k) n = 6*k if k > 0 else 1 - ptr = create_ptr(n) - - err = h3lib.gridRingUnsafe(h, k, ptr) - - if err == H3ErrorCodes.E_SUCCESS: - mv = create_mv(ptr, n) - elif err == H3ErrorCodes.E_PENTAGON: - mv = create_mv(ptr, n) # need to create this to guarantee memory is freed. - # better done with a Cython object, i think. + hmm = H3MemoryManager(n) + err = h3lib.gridRingUnsafe(h, k, hmm.ptr) + if err: mv = _ring_fallback(h, k) else: - mv = create_mv(ptr, n) # need to create this to guarantee memory is freed. - # definitely better done with a Cython "memory manager" object. - check_for_error(err) + mv = hmm.to_mv() return mv @@ -173,24 +159,24 @@ cpdef H3int parent(H3int h, res=None) except 0: cpdef H3int[:] children(H3int h, res=None): cdef: - H3int child - int64_t N + int64_t n check_cell(h) if res is None: res = resolution(h) + 1 - err = h3lib.cellToChildrenSize(h, res, &N) + 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) - ptr = create_ptr(N) - err = h3lib.cellToChildren(h, res, ptr) - mv = create_mv(ptr, N) - check_for_error(err) # needs to be after, to ensure memory is freed! + hmm = H3MemoryManager(n) + check_for_error( + h3lib.cellToChildren(h, res, hmm.ptr) + ) + mv = hmm.to_mv() return mv @@ -215,6 +201,8 @@ cpdef H3int center_child(H3int h, res=None) except 0: cpdef H3int[:] compact(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 @@ -222,17 +210,17 @@ cpdef H3int[:] compact(const H3int[:] hu): # note: open to better ideas! if len(hu) == 0: - return empty_memory_view() + return H3MemoryManager(0).to_mv() for h in hu: ## todo: should we have an array version? would that be faster? check_cell(h) - ptr = create_ptr(len(hu)) - err = h3lib.compactCells(&hu[0], ptr, len(hu)) - mv = create_mv(ptr, len(hu)) - - # todo: fix weird error ordering due to memory freeing... - check_for_error(err) + 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 @@ -246,29 +234,31 @@ cpdef H3int[:] uncompact(const H3int[:] hc, int res): # length of zero and returning early seems like the easiest solution. # note: open to better ideas! cdef: - int64_t N + int64_t n + if len(hc) == 0: - return empty_memory_view() + return H3MemoryManager(0).to_mv() for h in hc: check_cell(h) - # ignoring error for now - err = h3lib.uncompactCellsSize(&hc[0], len(hc), res, &N) + check_for_error( + h3lib.uncompactCellsSize(&hc[0], len(hc), res, &n) + ) - ptr = create_ptr(N) - err = h3lib.uncompactCells( - &hc[0], - len(hc), - ptr, - N, - res + 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 = create_mv(ptr, N) - # todo: fix weird error ordering due to memory freeing... - check_for_error(err) + mv = hmm.to_mv() return mv @@ -324,7 +314,7 @@ cpdef double cell_area(H3int h, unit='km^2') except -1: return area -cdef could_not_find_line(err, start, end): +cdef _could_not_find_line(err, start, end): msg = "Couldn't find line between cells {} and {}" msg = msg.format(hex(start), hex(end)) @@ -339,13 +329,15 @@ cpdef H3int[:] line(H3int start, H3int end): # probably applies to all size/work pairs of functions... err = h3lib.gridPathCellsSize(start, end, &n) - could_not_find_line(err, start, end) + _could_not_find_line(err, start, end) + + hmm = H3MemoryManager(n) + err = h3lib.gridPathCells(start, end, hmm.ptr) - ptr = create_ptr(n) - err = h3lib.gridPathCells(start, end, ptr) - mv = create_mv(ptr, n) + _could_not_find_line(err, start, end) - could_not_find_line(err, start, end) + # todo: probably here too? + mv = hmm.to_mv() return mv @@ -356,12 +348,11 @@ cpdef bool is_res_class_iii(H3int h): cpdef H3int[:] get_pentagon_indexes(int res): n = h3lib.pentagonCount() - # todo note: this is the tricky situation where we need the memory manager - ptr = create_ptr(n) - err = h3lib.getPentagons(res, ptr) - mv = create_mv(ptr, n) - - check_for_error(err) + hmm = H3MemoryManager(n) + check_for_error( + h3lib.getPentagons(res, hmm.ptr) + ) + mv = hmm.to_mv() return mv @@ -369,36 +360,35 @@ cpdef H3int[:] get_pentagon_indexes(int res): cpdef H3int[:] get_res0_indexes(): n = h3lib.res0CellCount() - # todo note: this is the tricky situation where we need the memory manager - ptr = create_ptr(n) - err = h3lib.getRes0Cells(ptr) - mv = create_mv(ptr, n) - - check_for_error(err) + 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_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) ) - cdef int* ptr = stdlib.calloc(n, sizeof(int)) - if (n > 0) and (not ptr): - raise MemoryError() - + faces = int_mv(n) check_for_error( - h3lib.getIcosahedronFaces(h, ptr) + h3lib.getIcosahedronFaces(h, &faces[0]) ) - faces = ptr - faces = {f for f in faces if f >= 0} - stdlib.free(ptr) + # 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 faces + return out cpdef (int, int) experimental_h3_to_local_ij(H3int origin, H3int h) except *: diff --git a/src/h3/_cy/edges.pyx b/src/h3/_cy/edges.pyx index 2df0795c7..9a4fd1520 100644 --- a/src/h3/_cy/edges.pyx +++ b/src/h3/_cy/edges.pyx @@ -1,13 +1,10 @@ cimport h3lib from .h3lib cimport bool, H3int -from .util cimport ( - create_ptr, - create_mv, -) - from .error_system cimport check_for_error +from .memory cimport H3MemoryManager + # todo: make bint cpdef bool are_neighbors(H3int h1, H3int h2): cdef: @@ -67,13 +64,11 @@ cpdef H3int[:] edges_from_cell(H3int origin): for the given origin cell """ - ptr = create_ptr(6) - + hmm = H3MemoryManager(6) check_for_error( - h3lib.originToDirectedEdges(origin, ptr) + h3lib.originToDirectedEdges(origin, hmm.ptr) ) - - mv = create_mv(ptr, 6) + mv = hmm.to_mv() return mv @@ -82,7 +77,9 @@ cpdef double mean_edge_length(int resolution, unit='km') except -1: cdef: double length - h3lib.getHexagonEdgeLengthAvgKm(resolution, &length) + check_for_error( + h3lib.getHexagonEdgeLengthAvgKm(resolution, &length) + ) # todo: multiple units convert = { @@ -102,17 +99,15 @@ cpdef double edge_length(H3int e, unit='km') except -1: cdef: double length - # todo: maybe kick this logic up to the python level - # it might be a little cleaner, because we can do the "switch statement" - # with a dict, but would require exposing more C functions - if unit == 'rads': - h3lib.exactEdgeLengthRads(e, &length) + err = h3lib.exactEdgeLengthRads(e, &length) elif unit == 'km': - h3lib.exactEdgeLengthKm(e, &length) + err = h3lib.exactEdgeLengthKm(e, &length) elif unit == 'm': - h3lib.exactEdgeLengthM(e, &length) + err = h3lib.exactEdgeLengthM(e, &length) else: raise ValueError('Unknown unit: {}'.format(unit)) + check_for_error(err) + return length diff --git a/src/h3/_cy/geo.pyx b/src/h3/_cy/geo.pyx index b40039b34..f6a52215d 100644 --- a/src/h3/_cy/geo.pyx +++ b/src/h3/_cy/geo.pyx @@ -1,19 +1,28 @@ +from libc.stdint cimport uint64_t + cimport h3lib from h3lib cimport bool, H3int + from .util cimport ( check_cell, check_edge, check_res, - create_ptr, - create_mv, deg2coord, coord2deg, ) -from libc cimport stdlib -from libc.stdint cimport uint64_t 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 geo_to_h3(double lat, double lng, int res) except 1: cdef: @@ -65,7 +74,8 @@ cdef h3lib.GeoLoop make_geoloop(geos, bool lnglat_order=False) except *: gl.numVerts = len(geos) - gl.verts = stdlib.calloc(gl.numVerts, sizeof(h3lib.LatLng)) + # todo: need for memory management + gl.verts = h3_calloc(gl.numVerts, sizeof(h3lib.LatLng)) if lnglat_order: latlng = (g[::-1] for g in geos) @@ -79,7 +89,7 @@ cdef h3lib.GeoLoop make_geoloop(geos, bool lnglat_order=False) except *: cdef free_geoloop(h3lib.GeoLoop* gl): - stdlib.free(gl.verts) + h3_free(gl.verts) gl.verts = NULL @@ -112,7 +122,7 @@ cdef class GeoPolygon: self.gp.holes = NULL if len(holes) > 0: - self.gp.holes = stdlib.calloc(len(holes), sizeof(h3lib.GeoLoop)) + self.gp.holes = h3_calloc(len(holes), sizeof(h3lib.GeoLoop)) for i, hole in enumerate(holes): self.gp.holes[i] = make_geoloop(hole, lnglat_order) @@ -123,7 +133,7 @@ cdef class GeoPolygon: for i in range(self.gp.numHoles): free_geoloop(&self.gp.holes[i]) - stdlib.free(self.gp.holes) + h3_free(self.gp.holes) def polyfill_polygon(outer, int res, holes=None, bool lnglat_order=False): @@ -157,11 +167,15 @@ def polyfill_polygon(outer, int res, holes=None, bool lnglat_order=False): check_res(res) gp = GeoPolygon(outer, holes=holes, lnglat_order=lnglat_order) - h3lib.maxPolygonToCellsSize(&gp.gp, res, 0, &n) - ptr = create_ptr(n) + check_for_error( + h3lib.maxPolygonToCellsSize(&gp.gp, res, 0, &n) + ) - h3lib.polygonToCells(&gp.gp, res, 0, ptr) - mv = create_mv(ptr, n) + hmm = H3MemoryManager(n) + check_for_error( + h3lib.polygonToCells(&gp.gp, res, 0, hmm.ptr) + ) + mv = hmm.to_mv() return mv diff --git a/src/h3/_cy/h3lib.pxd b/src/h3/_cy/h3lib.pxd index 0ff65e31f..ec225bd34 100644 --- a/src/h3/_cy/h3lib.pxd +++ b/src/h3/_cy/h3lib.pxd @@ -1,7 +1,7 @@ from cpython cimport bool from libc.stdint cimport uint32_t, uint64_t, int64_t -ctypedef basestring H3str # todo: do we really need this str one? +ctypedef basestring H3str cdef extern from 'h3api.h': cdef int H3_VERSION_MAJOR diff --git a/src/h3/_cy/memory.pxd b/src/h3/_cy/memory.pxd new file mode 100644 index 000000000..6068e6159 --- /dev/null +++ b/src/h3/_cy/memory.pxd @@ -0,0 +1,12 @@ +from .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(hexes) diff --git a/src/h3/_cy/memory.pyx b/src/h3/_cy/memory.pyx new file mode 100644 index 000000000..8c013b00c --- /dev/null +++ b/src/h3/_cy/memory.pyx @@ -0,0 +1,248 @@ +from 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(hexes): + """ hexes 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(hexes) + mv = H3MemoryManager(n).to_mv_keep_zeros() + + for i,h in enumerate(hexes): + mv[i] = h + + return mv diff --git a/src/h3/_cy/util.pxd b/src/h3/_cy/util.pxd index 6a34b751e..750316bea 100644 --- a/src/h3/_cy/util.pxd +++ b/src/h3/_cy/util.pxd @@ -6,13 +6,7 @@ cdef (double, double) coord2deg(LatLng c) nogil cpdef H3int hex2int(H3str h) except? 0 cpdef H3str int2hex(H3int x) -cdef H3int* create_ptr(size_t n) except? NULL -cdef H3int[:] create_mv(H3int* ptr, size_t n) - - cdef check_cell(H3int h) cdef check_edge(H3int e) cdef check_res(int res) cdef check_distance(int k) - -cdef H3int[:] empty_memory_view() # want to drop this if possible diff --git a/src/h3/_cy/util.pyx b/src/h3/_cy/util.pyx index fff021641..16c6df431 100644 --- a/src/h3/_cy/util.pyx +++ b/src/h3/_cy/util.pyx @@ -1,5 +1,3 @@ -from libc cimport stdlib -from cython.view cimport array from .h3lib cimport H3int, H3str, isValidCell, isValidDirectedEdge cimport h3lib @@ -11,7 +9,6 @@ from .error_system import ( H3CellInvalidError, ) - cdef h3lib.LatLng deg2coord(double lat, double lng) nogil: cdef: h3lib.LatLng c @@ -85,105 +82,3 @@ cdef check_distance(int k): raise H3DomainError( 'Grid distances must be nonnegative. Received: {}'.format(k) ) - - -## todo: can i turn these two into a context manager? -cdef H3int* create_ptr(size_t n) except? NULL: - cdef H3int* ptr = stdlib.calloc(n, sizeof(H3int)) - if (n > 0) and (not ptr): - raise MemoryError() - - return ptr - - -cdef H3int[:] create_mv(H3int* ptr, size_t n): - cdef: - array x - - n = move_nonzeros(ptr, n) - if n <= 0: - stdlib.free(ptr) - return empty_memory_view() - - ptr = stdlib.realloc(ptr, n*sizeof(H3int)) - - if ptr is NULL: - raise MemoryError() - - x = ptr - x.callback_free_data = stdlib.free - - return x - - -cpdef H3int[:] from_iter(hexes): - """ hexes 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: - array x - size_t n - n = len(hexes) - - if n == 0: - return empty_memory_view() - - x = create_ptr(n) - x.callback_free_data = stdlib.free - - for i,h in enumerate(hexes): - x[i] = h - - return x - - -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(): - # there's gotta be a better way to do this... - # create an empty cython.view.array? - cdef: - H3int a[1] - - return (a)[:0] diff --git a/src/h3/api/basic_int/_binding.py b/src/h3/api/basic_int/_binding.py index a232287c8..adfdb6e65 100644 --- a/src/h3/api/basic_int/_binding.py +++ b/src/h3/api/basic_int/_binding.py @@ -24,7 +24,7 @@ def _id(x): def _in_collection(hexes): it = list(hexes) - return _cy.from_iter(it) + return _cy.iter_to_mv(it) _binding = _API_FUNCTIONS( diff --git a/src/h3/api/basic_str/_binding.py b/src/h3/api/basic_str/_binding.py index 765fdbb51..e5b563b33 100644 --- a/src/h3/api/basic_str/_binding.py +++ b/src/h3/api/basic_str/_binding.py @@ -21,7 +21,7 @@ def _in_collection(hexes): it = [_cy.hex2int(h) for h in hexes] - return _cy.from_iter(it) + return _cy.iter_to_mv(it) def _out_unordered(mv): From 9e268cf3e792a6af0da2c928ffd0af6f1746782e Mon Sep 17 00:00:00 2001 From: AJ Friend Date: Wed, 10 Aug 2022 16:35:19 -0400 Subject: [PATCH 15/21] remove: from h3 import h3 (#270) --- src/h3/__init__.py | 4 ---- tests/test_h3.py | 8 -------- 2 files changed, 12 deletions(-) diff --git a/src/h3/__init__.py b/src/h3/__init__.py index a507e3b56..4f7cb32f1 100644 --- a/src/h3/__init__.py +++ b/src/h3/__init__.py @@ -3,10 +3,6 @@ from .api.basic_str import * from ._version import __version__ -#todo: remove in 4.0; only here for backward compatibility -from .api import basic_str as h3 - - from ._cy import ( UnknownH3ErrorCode, H3BaseException, diff --git a/tests/test_h3.py b/tests/test_h3.py index 73b18692e..a04da6ba4 100644 --- a/tests/test_h3.py +++ b/tests/test_h3.py @@ -4,14 +4,6 @@ import h3 -def test_nested_import(): - """ Test that we can import `h3.h3` - For backwards-compatibility - """ - from h3 import h3 - assert h3.geo_to_h3(37.3615593, -122.0553238, 5) == '85283473fffffff' - - def shift_circular_list(start_element, elements_list): # We shift the circular list so that it starts from start_element, start_index = elements_list.index(start_element) From dfdf40ba52afcdf69c51f589e5c8602e7083e41e Mon Sep 17 00:00:00 2001 From: AJ Friend Date: Wed, 10 Aug 2022 16:36:18 -0400 Subject: [PATCH 16/21] is_valid_cell (#269) --- docs/api_reference.md | 2 +- src/h3/_cy/__init__.py | 2 +- src/h3/_cy/cells.pxd | 2 +- src/h3/_cy/cells.pyx | 2 +- src/h3/api/_api_template.py | 6 +++--- src/h3/api/basic_int/_public_api.py | 2 +- tests/test_cells_and_edges.py | 12 ++++++------ tests/test_h3.py | 12 ++++++------ tests/test_length_area.py | 2 +- 9 files changed, 21 insertions(+), 21 deletions(-) diff --git a/docs/api_reference.md b/docs/api_reference.md index fee1b4276..06c2b9d46 100644 --- a/docs/api_reference.md +++ b/docs/api_reference.md @@ -19,7 +19,7 @@ but we'll try to group functions in a reasonably logical manner. .. currentmodule:: h3 .. autosummary:: - h3_is_valid + is_valid_cell h3_is_pentagon h3_is_res_class_III h3_is_res_class_iii diff --git a/src/h3/_cy/__init__.py b/src/h3/_cy/__init__.py index e61bb4e1a..303febb61 100644 --- a/src/h3/_cy/__init__.py +++ b/src/h3/_cy/__init__.py @@ -14,7 +14,7 @@ """ from .cells import ( - is_cell, + is_valid_cell, is_pentagon, get_base_cell, resolution, diff --git a/src/h3/_cy/cells.pxd b/src/h3/_cy/cells.pxd index 10719389e..7c8c078b0 100644 --- a/src/h3/_cy/cells.pxd +++ b/src/h3/_cy/cells.pxd @@ -1,6 +1,6 @@ from .h3lib cimport bool, int64_t, H3int -cpdef bool is_cell(H3int h) +cpdef bool is_valid_cell(H3int h) cpdef bool is_pentagon(H3int h) cpdef int get_base_cell(H3int h) except -1 cpdef int resolution(H3int h) except -1 diff --git a/src/h3/_cy/cells.pyx b/src/h3/_cy/cells.pyx index de6bc6b14..bdd31efa0 100644 --- a/src/h3/_cy/cells.pyx +++ b/src/h3/_cy/cells.pyx @@ -21,7 +21,7 @@ from .memory cimport ( # bool is a python type, so we don't need the except clause -cpdef bool is_cell(H3int h): +cpdef bool is_valid_cell(H3int h): """Validates an H3 cell (hexagon or pentagon) Returns diff --git a/src/h3/api/_api_template.py b/src/h3/api/_api_template.py index fa9082ea3..c8d06bb2c 100644 --- a/src/h3/api/_api_template.py +++ b/src/h3/api/_api_template.py @@ -153,7 +153,7 @@ def edge_length(resolution, unit='km'): # todo: `mean_edge_length` in 4.0 return _cy.mean_edge_length(resolution, unit) - def h3_is_valid(self, h): + def is_valid_cell(self, h): """ Validates an H3 cell (hexagon or pentagon). @@ -163,7 +163,7 @@ def h3_is_valid(self, h): """ try: h = self._in_scalar(h) - return _cy.is_cell(h) + return _cy.is_valid_cell(h) except (ValueError, TypeError): return False @@ -546,7 +546,7 @@ def h3_is_pentagon(self, h): Notes ----- - A pentagon should *also* pass ``h3_is_cell()``. + A pentagon should *also* pass ``is_valid_cell()``. Will return ``False`` for valid H3Edge. """ return _cy.is_pentagon(self._in_scalar(h)) diff --git a/src/h3/api/basic_int/_public_api.py b/src/h3/api/basic_int/_public_api.py index 50b51fcaa..bf8b5b299 100644 --- a/src/h3/api/basic_int/_public_api.py +++ b/src/h3/api/basic_int/_public_api.py @@ -39,7 +39,7 @@ h3_is_pentagon = _binding.h3_is_pentagon h3_is_res_class_III = _binding.h3_is_res_class_III h3_is_res_class_iii = _binding.h3_is_res_class_iii -h3_is_valid = _binding.h3_is_valid +is_valid_cell = _binding.is_valid_cell h3_line = _binding.h3_line h3_set_to_multi_polygon = _binding.h3_set_to_multi_polygon h3_to_center_child = _binding.h3_to_center_child diff --git a/tests/test_cells_and_edges.py b/tests/test_cells_and_edges.py index ccca3421b..194b05b46 100644 --- a/tests/test_cells_and_edges.py +++ b/tests/test_cells_and_edges.py @@ -107,12 +107,12 @@ def test7(): def test8(): - assert h3.h3_is_valid('89283082803ffff') - assert not h3.h3_is_valid('abc') + 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.h3_is_valid(h_bad) + assert not h3.is_valid_cell(h_bad) # other methods should validate and raise exception if bad input with pytest.raises(H3CellInvalidError): @@ -336,7 +336,7 @@ def test_edge(): assert e == '12928308280fffff' assert h3.h3_unidirectional_edge_is_valid(e) - assert not h3.h3_is_valid(e) + assert not h3.is_valid_cell(e) assert h3.get_origin_h3_index_from_unidirectional_edge(e) == h1 assert h3.get_destination_h3_index_from_unidirectional_edge(e) == h2 @@ -487,7 +487,7 @@ def test_str_int_convert(): def test_hex2int_fail(): h_invalid = {} - assert not h3.h3_is_valid(h_invalid) + assert not h3.is_valid_cell(h_invalid) def test_edge_is_valid_fail(): @@ -558,7 +558,7 @@ def test_get_res0_indexes(): assert pentagons < out # all valid - assert all(map(h3.h3_is_valid, out)) + assert all(map(h3.is_valid_cell, out)) # resolution assert all(map( diff --git a/tests/test_h3.py b/tests/test_h3.py index a04da6ba4..cebf6a49a 100644 --- a/tests/test_h3.py +++ b/tests/test_h3.py @@ -10,16 +10,16 @@ def shift_circular_list(start_element, elements_list): return elements_list[start_index:] + elements_list[:start_index] -def test_h3_is_valid(): - assert h3.h3_is_valid('85283473fffffff') - assert h3.h3_is_valid('850dab63fffffff') - assert not h3.h3_is_valid('lolwut') +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.h3_is_valid('5004295803a88') + assert not h3.is_valid_cell('5004295803a88') for res in range(16): - assert h3.h3_is_valid(h3.geo_to_h3(37, -122, res)) + assert h3.is_valid_cell(h3.geo_to_h3(37, -122, res)) def test_geo_to_h3(): diff --git a/tests/test_length_area.py b/tests/test_length_area.py index b16ef9076..8c15f1362 100644 --- a/tests/test_length_area.py +++ b/tests/test_length_area.py @@ -98,7 +98,7 @@ def test_bad_units(): h = '89754e64993ffff' e = '139754e64993ffff' - assert h3.h3_is_valid(h) + assert h3.is_valid_cell(h) assert h3.h3_unidirectional_edge_is_valid(e) with pytest.raises(ValueError): From e7e8664c4122587a83cc4e08228203813d57224b Mon Sep 17 00:00:00 2001 From: AJ Friend Date: Sat, 13 Aug 2022 00:07:27 -0400 Subject: [PATCH 17/21] Remove alias functions (#271) * remove alias function hex_range * note test_k_ring and test_hex_range are the same * remove test_hex_range * duplicate tests * remove test_hex_range2 * same test * remove test_hex_range_pentagon * remove alias: k_ring_distances * tests are the same! * same test * combine tests * remove alias: h3_is_res_class_iii * lint --- docs/api_reference.md | 3 - src/h3/api/_api_template.py | 21 ----- src/h3/api/basic_int/_public_api.py | 3 - tests/test_h3.py | 125 +++++----------------------- 4 files changed, 19 insertions(+), 133 deletions(-) diff --git a/docs/api_reference.md b/docs/api_reference.md index 06c2b9d46..b48543aee 100644 --- a/docs/api_reference.md +++ b/docs/api_reference.md @@ -22,7 +22,6 @@ but we'll try to group functions in a reasonably logical manner. is_valid_cell h3_is_pentagon h3_is_res_class_III - h3_is_res_class_iii h3_unidirectional_edge_is_valid versions ``` @@ -83,12 +82,10 @@ Functions relating H3 objects to geographic (lat/lng) coordinates. .. currentmodule:: h3 .. autosummary:: - hex_range hex_range_distances hex_ranges hex_ring k_ring - k_ring_distances h3_distance h3_indexes_are_neighbors h3_line diff --git a/src/h3/api/_api_template.py b/src/h3/api/_api_template.py index c8d06bb2c..0274e7fac 100644 --- a/src/h3/api/_api_template.py +++ b/src/h3/api/_api_template.py @@ -312,19 +312,6 @@ def k_ring(self, h, k=1): return self._out_unordered(mv) - def hex_range(self, h, k=1): - """ - Alias for `k_ring`. - "Filled-in" disk. - - Notes - ----- - This name differs from the C API. - """ - mv = _cy.disk(self._in_scalar(h), k) - - return self._out_unordered(mv) - def hex_ring(self, h, k=1): """ Return unordered set of cells with H3 distance ``== k`` from ``h``. @@ -382,10 +369,6 @@ def hex_ranges(self, hexes, K): return out - def k_ring_distances(self, h, K): - """Alias for `hex_range_distances`.""" - return self.hex_range_distances(h, K) - def h3_to_children(self, h, res=None): """ Children of a hexagon. @@ -743,10 +726,6 @@ def h3_is_res_class_III(self, h): """ return _cy.is_res_class_iii(self._in_scalar(h)) - def h3_is_res_class_iii(self, h): - """Alias for `h3_is_res_class_III`.""" - return self.h3_is_res_class_III(h) - def get_pentagon_indexes(self, resolution): """ Return all pentagons at a given resolution. diff --git a/src/h3/api/basic_int/_public_api.py b/src/h3/api/basic_int/_public_api.py index bf8b5b299..b25064e28 100644 --- a/src/h3/api/basic_int/_public_api.py +++ b/src/h3/api/basic_int/_public_api.py @@ -38,7 +38,6 @@ h3_indexes_are_neighbors = _binding.h3_indexes_are_neighbors h3_is_pentagon = _binding.h3_is_pentagon h3_is_res_class_III = _binding.h3_is_res_class_III -h3_is_res_class_iii = _binding.h3_is_res_class_iii is_valid_cell = _binding.is_valid_cell h3_line = _binding.h3_line h3_set_to_multi_polygon = _binding.h3_set_to_multi_polygon @@ -50,12 +49,10 @@ h3_to_string = _binding.h3_to_string h3_unidirectional_edge_is_valid = _binding.h3_unidirectional_edge_is_valid hex_area = _binding.hex_area -hex_range = _binding.hex_range hex_range_distances = _binding.hex_range_distances hex_ranges = _binding.hex_ranges hex_ring = _binding.hex_ring k_ring = _binding.k_ring -k_ring_distances = _binding.k_ring_distances num_hexagons = _binding.num_hexagons point_dist = _binding.point_dist polyfill = _binding.polyfill diff --git a/tests/test_h3.py b/tests/test_h3.py index cebf6a49a..333860642 100644 --- a/tests/test_h3.py +++ b/tests/test_h3.py @@ -154,31 +154,6 @@ def test_k_ring_pentagon(): assert out == expected -def test_k_ring_distances(): - h = '8928308280fffff' - out = h3.k_ring_distances(h, 1) - - assert [len(x) for x in out] == [1, 6] - - expected = [ - {h}, - { - '8928308280bffff', - '89283082807ffff', - '89283082877ffff', - '89283082803ffff', - '89283082873ffff', - '8928308283bffff', - } - ] - - assert out == expected - - out = h3.k_ring_distances('870800003ffffff', 2) - - assert [len(x) for x in out] == [1, 6, 11] - - def test_polyfill(): geo = { 'type': 'Polygon', @@ -656,69 +631,21 @@ def test_h3_to_children(): assert len(children) == 7 -def test_hex_range(): - h = '8928308280fffff' - out = h3.hex_range(h, 1) - assert len(out) == 1 + 6 - - expected = { - '8928308280bffff', - '89283082807ffff', - h, - '89283082877ffff', - '89283082803ffff', - '89283082873ffff', - '8928308283bffff', - } - - assert out == expected - - -def test_hex_range2(): - h = '8928308280fffff' - out = h3.hex_range(h, 2) - - assert len(out) == 1 + 6 + 12 - - expected = { - '89283082813ffff', - '89283082817ffff', - '8928308281bffff', - '89283082863ffff', - '89283082823ffff', - '89283082873ffff', - '89283082877ffff', - '8928308287bffff', - '89283082833ffff', - '8928308282bffff', - '8928308283bffff', - '89283082857ffff', - '892830828abffff', - '89283082847ffff', - '89283082867ffff', - '89283082803ffff', - h, - '89283082807ffff', - '8928308280bffff', - } - - assert out == expected - - -def test_hex_range_pentagon(): - h = '821c07fffffffff' # a pentagon +def test_hex_range_distances_pentagon(): - # should consist of `h` and it's 5 neighbors - out = h3.hex_range(h, 1) + h = '821c07fffffffff' + out = h3.hex_range_distances(h, 1) - expected = { - h, - '821c17fffffffff', - '821c1ffffffffff', - '821c27fffffffff', - '821c2ffffffffff', - '821c37fffffffff', - } + expected = [ + {h}, + { + '821c17fffffffff', + '821c1ffffffffff', + '821c27fffffffff', + '821c2ffffffffff', + '821c37fffffffff', + } + ] assert out == expected @@ -729,6 +656,8 @@ def test_hex_range_distances(): # should consist of `h` and it's 5 neighbors out = h3.hex_range_distances(h, 1) + assert [len(x) for x in out] == [1, 6] + expected = [ {h}, { @@ -743,24 +672,9 @@ def test_hex_range_distances(): assert out == expected + out = h3.hex_range_distances('870800003ffffff', 2) -def test_hex_range_distances_pentagon(): - - h = '821c07fffffffff' - out = h3.hex_range_distances(h, 1) - - expected = [ - {h}, - { - '821c17fffffffff', - '821c1ffffffffff', - '821c27fffffffff', - '821c2ffffffffff', - '821c37fffffffff', - } - ] - - assert out == expected + assert [len(x) for x in out] == [1, 6, 11] def test_hex_ranges(): @@ -856,10 +770,9 @@ def test_h3_get_base_cell(): assert h3.h3_get_base_cell('8928308280fffff') == 20 -def test_h3_is_res_class_iiiIII(): - assert h3.h3_is_res_class_iii('8928308280fffff') - assert not h3.h3_is_res_class_iii('8828308280fffff') +def test_h3_is_res_class_III(): assert h3.h3_is_res_class_III('8928308280fffff') + assert not h3.h3_is_res_class_III('8828308280fffff') def test_h3_is_pentagon(): From c3fc0a2a16fd571e5b5692dbdefd5b21c981ac3e Mon Sep 17 00:00:00 2001 From: AJ Friend Date: Tue, 16 Aug 2022 19:52:24 -0400 Subject: [PATCH 18/21] Update most functions to v4.0 names (#272) * grid_distance * cell_to_parent * get_resolution * is_pentagon * is_res_class_III * get_pentagons * get_res0_cells * get_base_cell_number * compact and uncompact * get_faces * cell_to_boundary * cell_to_latlng * latlng_to_cell * cell_to_children * int_to_string and string_to_int * cell_to_center_child * moving things around * is_valid_directed_edge * grid_path_cells * moving around * cells_to_multi_polygon * are_neighbor_cells * cell_to_local_ij and local_ij_to_cell * get_icosahedron_faces * great_circle_distance * get_num_cells * note * remove hex_range_distances and hex_ranges (easily implementable by user) * grid_ring * grid_disk * moving around * origin_to_directed_edges * directed_edge_to_boundary * cells_to_directed_edge * directed_edge_to_cells * todo: directedEdgeToCells * get_directed_edge_origin * get_directed_edge_destination * grouping functions * more grouping * _binding to _b * todos * average_hexagon_area * average_edge_length * average_edge_length and edge_length * average_hexagon_edge_length * ordering * more ordering --- docs/api_comparison.md | 32 +-- docs/api_reference.md | 74 +++--- readme.md | 2 +- src/h3/_cy/__init__.py | 64 +++--- src/h3/_cy/cells.pxd | 37 ++- src/h3/_cy/cells.pyx | 42 ++-- src/h3/_cy/edges.pxd | 16 +- src/h3/_cy/edges.pyx | 19 +- src/h3/_cy/geo.pxd | 6 +- src/h3/_cy/geo.pyx | 10 +- src/h3/_cy/h3lib.pxd | 1 + src/h3/_cy/to_multipoly.pyx | 2 +- src/h3/api/_api_template.py | 188 ++++++--------- src/h3/api/basic_int/_public_api.py | 111 ++++----- tests/cython_example.pyx | 6 +- tests/test_basic_int.py | 22 +- tests/test_basic_str.py | 5 +- tests/test_cells_and_edges.py | 223 +++++++++--------- tests/test_collection_inputs.py | 36 +-- tests/test_cython.py | 8 +- tests/test_h3.py | 339 ++++++++++------------------ tests/test_length_area.py | 31 +-- tests/test_memview_int.py | 15 +- tests/test_numpy_int.py | 19 +- tests/test_to_multipoly.py | 18 +- 25 files changed, 593 insertions(+), 733 deletions(-) diff --git a/docs/api_comparison.md b/docs/api_comparison.md index 3700f1c98..2378702c5 100644 --- a/docs/api_comparison.md +++ b/docs/api_comparison.md @@ -37,7 +37,7 @@ 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 `h3_to_string` (links!) and `string_to_h3`. +between them with functions like `int_to_string` (links!) and `string_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` @@ -54,11 +54,11 @@ using `list` and `set` for collections. ```python >>> import h3 ->>> h = h3.geo_to_h3(0, 0, 0) +>>> h = h3.latlng_to_cell(0, 0, 0) >>> h '8075fffffffffff' ->>> h3.hex_ring(h, 1) +>>> h3.grid_ring(h, 1) {'8055fffffffffff', '8059fffffffffff', '807dfffffffffff', @@ -87,11 +87,11 @@ H3 indexes are represented as Python `int`s, using `list` and `set` for collecti ```python >>> import h3.api.basic_int as h3 ->>> h = h3.geo_to_h3(0, 0, 0) +>>> h = h3.latlng_to_cell(0, 0, 0) >>> h 578536630256664575 ->>> h3.hex_ring(h, 1) +>>> h3.grid_ring(h, 1) {577973680303243263, 578044049047420927, 578677367745019903, @@ -110,11 +110,11 @@ no-copy `numpy` arrays instead of Python `list`s and `set`s. ```python >>> import h3.api.numpy_int as h3 ->>> h = h3.geo_to_h3(0, 0, 0) +>>> h = h3.latlng_to_cell(0, 0, 0) >>> h 578536630256664575 ->>> h3.hex_ring(h, 1) +>>> h3.grid_ring(h, 1) array([578782920861286399, 578044049047420927, 577973680303243263, 578677367745019903, 579169948954263551], dtype=uint64) ``` @@ -141,11 +141,11 @@ In fact, `h3.api.numpy_int` is essentially just a light wrapper around ```python >>> import h3.api.memview_int as h3 ->>> h = h3.geo_to_h3(0, 0, 0) +>>> h = h3.latlng_to_cell(0, 0, 0) >>> h 578536630256664575 ->>> mv = h3.hex_ring(h, 1) +>>> mv = h3.grid_ring(h, 1) >>> mv @@ -175,8 +175,8 @@ For example, consider the setup: ```python >>> import h3.api.memview_int as h3 >>> import numpy as np ->>> h = h3.geo_to_h3(0, 0, 0) ->>> mv = h3.hex_ring(h, 1) +>>> h = h3.latlng_to_cell(0, 0, 0) +>>> mv = h3.grid_ring(h, 1) >>> list(mv) [578782920861286399, 578044049047420927, @@ -200,7 +200,7 @@ Running `a = np.asarray(mv)` **does not create a copy**, so modifying `mv` also modifies `a`: ```python ->>> mv = h3.hex_ring(h, 1) +>>> mv = h3.grid_ring(h, 1) >>> a = np.asarray(mv) >>> mv[0] = 0 >>> a @@ -228,15 +228,15 @@ import h3.api.numpy_int def compute(h3_lib, N=100): - h = h3_lib.geo_to_h3(0, 0, 9) - out = h3_lib.k_ring(h, N) - out = h3_lib.compact(out) + 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.h3_to_string(h) for h in out] + out = [h3.int_to_string(h) for h in out] return out ``` diff --git a/docs/api_reference.md b/docs/api_reference.md index b48543aee..efdc1152f 100644 --- a/docs/api_reference.md +++ b/docs/api_reference.md @@ -20,9 +20,9 @@ but we'll try to group functions in a reasonably logical manner. .. autosummary:: is_valid_cell - h3_is_pentagon - h3_is_res_class_III - h3_unidirectional_edge_is_valid + is_pentagon + is_res_class_III + is_valid_directed_edge versions ``` @@ -32,16 +32,16 @@ but we'll try to group functions in a reasonably logical manner. .. currentmodule:: h3 .. autosummary:: - geo_to_h3 - h3_to_geo - h3_to_string - string_to_h3 - get_res0_indexes - get_pentagon_indexes - num_hexagons - h3_get_resolution - compact - uncompact + latlng_to_cell + cell_to_latlng + int_to_string + string_to_int + get_res0_cells + get_pentagons + get_num_cells + get_resolution + compact_cells + uncompact_cells ``` ### Geographic coordinates @@ -52,17 +52,17 @@ Functions relating H3 objects to geographic (lat/lng) coordinates. .. currentmodule:: h3 .. autosummary:: - point_dist - hex_area + great_circle_distance + average_hexagon_area cell_area edge_length - exact_edge_length - h3_to_geo_boundary - get_h3_unidirectional_edge_boundary + average_hexagon_edge_length + cell_to_boundary + directed_edge_to_boundary polyfill polyfill_geojson polyfill_polygon - h3_set_to_multi_polygon + cells_to_multi_polygon ``` ### Hierarchical relationships @@ -71,9 +71,9 @@ Functions relating H3 objects to geographic (lat/lng) coordinates. .. currentmodule:: h3 .. autosummary:: - h3_to_parent - h3_to_children - h3_to_center_child + cell_to_parent + cell_to_children + cell_to_center_child ``` ### Cell grid relationships @@ -82,13 +82,11 @@ Functions relating H3 objects to geographic (lat/lng) coordinates. .. currentmodule:: h3 .. autosummary:: - hex_range_distances - hex_ranges - hex_ring - k_ring - h3_distance - h3_indexes_are_neighbors - h3_line + grid_ring + grid_disk + grid_distance + are_neighbor_cells + grid_path_cells ``` ### Edges @@ -97,11 +95,11 @@ Functions relating H3 objects to geographic (lat/lng) coordinates. .. currentmodule:: h3 .. autosummary:: - get_h3_unidirectional_edge - get_destination_h3_index_from_unidirectional_edge - get_h3_indexes_from_unidirectional_edge - get_h3_unidirectional_edges_from_hexagon - get_origin_h3_index_from_unidirectional_edge + cells_to_directed_edge + get_directed_edge_destination + directed_edge_to_cells + origin_to_directed_edges + get_directed_edge_origin ``` ### IJ-indexing @@ -110,10 +108,10 @@ Functions relating H3 objects to geographic (lat/lng) coordinates. .. currentmodule:: h3 .. autosummary:: - h3_get_base_cell - h3_get_faces - experimental_h3_to_local_ij - experimental_local_ij_to_h3 + get_base_cell_number + get_icosahedron_faces + cell_to_local_ij + local_ij_to_cell ``` diff --git a/readme.md b/readme.md index e060f059f..3ab380367 100644 --- a/readme.md +++ b/readme.md @@ -41,7 +41,7 @@ conda install h3-py >>> import h3 >>> lat, lng = 37.769377, -122.388903 >>> resolution = 9 ->>> h3.geo_to_h3(lat, lng, resolution) +>>> h3.latlng_to_cell(lat, lng, resolution) '89283082e73ffff' ``` diff --git a/src/h3/_cy/__init__.py b/src/h3/_cy/__init__.py index 303febb61..e341b3a96 100644 --- a/src/h3/_cy/__init__.py +++ b/src/h3/_cy/__init__.py @@ -16,53 +16,53 @@ from .cells import ( is_valid_cell, is_pentagon, - get_base_cell, - resolution, - parent, - distance, - disk, - ring, - children, - compact, - uncompact, - num_hexagons, - mean_hex_area, + get_base_cell_number, + get_resolution, + cell_to_parent, + grid_distance, + grid_disk, + grid_ring, + cell_to_children, + compact_cells, + uncompact_cells, + get_num_cells, + average_hexagon_area, cell_area, - line, + grid_path_cells, is_res_class_iii, - get_pentagon_indexes, - get_res0_indexes, - center_child, - get_faces, - experimental_h3_to_local_ij, - experimental_local_ij_to_h3, + get_pentagons, + get_res0_cells, + cell_to_center_child, + get_icosahedron_faces, + cell_to_local_ij, + local_ij_to_cell, ) from .edges import ( - are_neighbors, - edge, - is_edge, - edge_origin, - edge_destination, - edge_cells, - edges_from_cell, - mean_edge_length, + 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 .geo import ( - geo_to_h3, - h3_to_geo, + latlng_to_cell, + cell_to_latlng, polyfill_polygon, polyfill_geojson, polyfill, - cell_boundary, - edge_boundary, - point_dist, + cell_to_boundary, + directed_edge_to_boundary, + great_circle_distance, ) from .to_multipoly import ( - h3_set_to_multi_polygon + cells_to_multi_polygon ) from .util import ( diff --git a/src/h3/_cy/cells.pxd b/src/h3/_cy/cells.pxd index 7c8c078b0..b6a36d180 100644 --- a/src/h3/_cy/cells.pxd +++ b/src/h3/_cy/cells.pxd @@ -2,24 +2,23 @@ from .h3lib cimport bool, int64_t, H3int cpdef bool is_valid_cell(H3int h) cpdef bool is_pentagon(H3int h) -cpdef int get_base_cell(H3int h) except -1 -cpdef int resolution(H3int h) except -1 -cpdef int distance(H3int h1, H3int h2) except -1 -cpdef H3int[:] disk(H3int h, int k) -cpdef H3int[:] _ring_fallback(H3int h, int k) -cpdef H3int[:] ring(H3int h, int k) -cpdef H3int parent(H3int h, res=*) except 0 -cpdef H3int[:] children(H3int h, res=*) -cpdef H3int center_child(H3int h, res=*) except 0 -cpdef H3int[:] compact(const H3int[:] hu) -cpdef H3int[:] uncompact(const H3int[:] hc, int res) -cpdef int64_t num_hexagons(int resolution) except -1 -cpdef double mean_hex_area(int resolution, unit=*) except -1 +cpdef int get_base_cell_number(H3int h) except -1 +cpdef int get_resolution(H3int h) except -1 +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 H3int[:] cell_to_children(H3int h, res=*) +cpdef H3int cell_to_center_child(H3int h, res=*) 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[:] line(H3int start, H3int end) +cpdef H3int[:] grid_path_cells(H3int start, H3int end) cpdef bool is_res_class_iii(H3int h) -cpdef H3int[:] get_pentagon_indexes(int res) -cpdef H3int[:] get_res0_indexes() -cpdef get_faces(H3int h) -cpdef (int, int) experimental_h3_to_local_ij(H3int origin, H3int h) except * -cpdef H3int experimental_local_ij_to_h3(H3int origin, int i, int j) except 0 +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 diff --git a/src/h3/_cy/cells.pyx b/src/h3/_cy/cells.pyx index bdd31efa0..6a2157697 100644 --- a/src/h3/_cy/cells.pyx +++ b/src/h3/_cy/cells.pyx @@ -35,13 +35,13 @@ cpdef bool is_pentagon(H3int h): return h3lib.isPentagon(h) == 1 -cpdef int get_base_cell(H3int h) except -1: +cpdef int get_base_cell_number(H3int h) except -1: check_cell(h) return h3lib.getBaseCellNumber(h) -cpdef int resolution(H3int h) except -1: +cpdef int get_resolution(H3int h) except -1: """Returns the resolution of an H3 Index 0--15 """ @@ -50,7 +50,7 @@ cpdef int resolution(H3int h) except -1: return h3lib.getResolution(h) -cpdef int distance(H3int h1, H3int h2) except -1: +cpdef int grid_distance(H3int h1, H3int h2) except -1: """ Compute the grid distance between two cells """ cdef: @@ -65,7 +65,7 @@ cpdef int distance(H3int h1, H3int h2) except -1: return distance -cpdef H3int[:] disk(H3int h, int k): +cpdef H3int[:] grid_disk(H3int h, int k): """ Return cells at grid distance `<= k` from `h`. """ cdef: @@ -122,7 +122,7 @@ cpdef H3int[:] _ring_fallback(H3int h, int k): return mv -cpdef H3int[:] ring(H3int h, int k): +cpdef H3int[:] grid_ring(H3int h, int k): """ Return cells at grid distance `== k` from `h`. Collection is "hollow" for k >= 1. """ @@ -139,14 +139,14 @@ cpdef H3int[:] ring(H3int h, int k): return mv -cpdef H3int parent(H3int h, res=None) except 0: +cpdef H3int cell_to_parent(H3int h, res=None) except 0: cdef: H3int parent check_cell(h) # todo: do we want to check for validity here? or leave correctness to the user? if res is None: - res = resolution(h) - 1 + res = get_resolution(h) - 1 err = h3lib.cellToParent(h, res, &parent) if err: @@ -157,14 +157,14 @@ cpdef H3int parent(H3int h, res=None) except 0: return parent -cpdef H3int[:] children(H3int h, res=None): +cpdef H3int[:] cell_to_children(H3int h, res=None): cdef: int64_t n check_cell(h) if res is None: - res = resolution(h) + 1 + res = get_resolution(h) + 1 err = h3lib.cellToChildrenSize(h, res, &n) if err: @@ -181,14 +181,14 @@ cpdef H3int[:] children(H3int h, res=None): return mv -cpdef H3int center_child(H3int h, res=None) except 0: +cpdef H3int cell_to_center_child(H3int h, res=None) except 0: cdef: H3int child check_cell(h) if res is None: - res = resolution(h) + 1 + res = get_resolution(h) + 1 err = h3lib.cellToCenterChild(h, res, &child) if err: @@ -200,7 +200,7 @@ cpdef H3int center_child(H3int h, res=None) except 0: -cpdef H3int[:] compact(const H3int[:] hu): +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** @@ -227,7 +227,7 @@ cpdef H3int[:] compact(const H3int[:] hu): # 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(const H3int[:] hc, int res): +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 @@ -263,7 +263,7 @@ cpdef H3int[:] uncompact(const H3int[:] hc, int res): return mv -cpdef int64_t num_hexagons(int resolution) except -1: +cpdef int64_t get_num_cells(int resolution) except -1: cdef: int64_t num_cells @@ -274,7 +274,7 @@ cpdef int64_t num_hexagons(int resolution) except -1: return num_cells -cpdef double mean_hex_area(int resolution, unit='km^2') except -1: +cpdef double average_hexagon_area(int resolution, unit='km^2') except -1: cdef: double area @@ -320,7 +320,7 @@ cdef _could_not_find_line(err, start, end): check_for_error_msg(err, msg) -cpdef H3int[:] line(H3int start, H3int end): +cpdef H3int[:] grid_path_cells(H3int start, H3int end): cdef: int64_t n @@ -345,7 +345,7 @@ cpdef bool is_res_class_iii(H3int h): return h3lib.isResClassIII(h) == 1 -cpdef H3int[:] get_pentagon_indexes(int res): +cpdef H3int[:] get_pentagons(int res): n = h3lib.pentagonCount() hmm = H3MemoryManager(n) @@ -357,7 +357,7 @@ cpdef H3int[:] get_pentagon_indexes(int res): return mv -cpdef H3int[:] get_res0_indexes(): +cpdef H3int[:] get_res0_cells(): n = h3lib.res0CellCount() hmm = H3MemoryManager(n) @@ -370,7 +370,7 @@ cpdef H3int[:] get_res0_indexes(): # oh, this is returning a set?? # todo: convert to int[:]? -cpdef get_faces(H3int h): +cpdef get_icosahedron_faces(H3int h): cdef: int n int[:] faces ## todo: weird, this needs to be specified to avoid errors. cython bug? @@ -391,7 +391,7 @@ cpdef get_faces(H3int h): return out -cpdef (int, int) experimental_h3_to_local_ij(H3int origin, H3int h) except *: +cpdef (int, int) cell_to_local_ij(H3int origin, H3int h) except *: cdef: h3lib.CoordIJ c @@ -403,7 +403,7 @@ cpdef (int, int) experimental_h3_to_local_ij(H3int origin, H3int h) except *: return c.i, c.j -cpdef H3int experimental_local_ij_to_h3(H3int origin, int i, int j) except 0: +cpdef H3int local_ij_to_cell(H3int origin, int i, int j) except 0: cdef: h3lib.CoordIJ c H3int out diff --git a/src/h3/_cy/edges.pxd b/src/h3/_cy/edges.pxd index bf4c5c4af..b7fcf8499 100644 --- a/src/h3/_cy/edges.pxd +++ b/src/h3/_cy/edges.pxd @@ -1,11 +1,11 @@ from .h3lib cimport bool, H3int -cpdef bool are_neighbors(H3int h1, H3int h2) -cpdef H3int edge(H3int origin, H3int destination) except * -cpdef bool is_edge(H3int e) -cpdef H3int edge_origin(H3int e) except 1 -cpdef H3int edge_destination(H3int e) except 1 -cpdef (H3int, H3int) edge_cells(H3int e) except * -cpdef H3int[:] edges_from_cell(H3int origin) -cpdef double mean_edge_length(int resolution, unit=*) except -1 +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 diff --git a/src/h3/_cy/edges.pyx b/src/h3/_cy/edges.pyx index 9a4fd1520..3503e7582 100644 --- a/src/h3/_cy/edges.pyx +++ b/src/h3/_cy/edges.pyx @@ -6,7 +6,7 @@ from .error_system cimport check_for_error from .memory cimport H3MemoryManager # todo: make bint -cpdef bool are_neighbors(H3int h1, H3int h2): +cpdef bool are_neighbor_cells(H3int h1, H3int h2): cdef: int out @@ -21,7 +21,7 @@ cpdef bool are_neighbors(H3int h1, H3int h2): return out == 1 -cpdef H3int edge(H3int origin, H3int destination) except *: +cpdef H3int cells_to_directed_edge(H3int origin, H3int destination) except *: cdef: int neighbor_out H3int out @@ -33,10 +33,10 @@ cpdef H3int edge(H3int origin, H3int destination) except *: return out -cpdef bool is_edge(H3int e): +cpdef bool is_valid_directed_edge(H3int e): return h3lib.isValidDirectedEdge(e) == 1 -cpdef H3int edge_origin(H3int e) except 1: +cpdef H3int get_directed_edge_origin(H3int e) except 1: cdef: H3int out @@ -46,7 +46,7 @@ cpdef H3int edge_origin(H3int e) except 1: return out -cpdef H3int edge_destination(H3int e) except 1: +cpdef H3int get_directed_edge_destination(H3int e) except 1: cdef: H3int out @@ -56,10 +56,11 @@ cpdef H3int edge_destination(H3int e) except 1: return out -cpdef (H3int, H3int) edge_cells(H3int e) except *: - return edge_origin(e), edge_destination(e) +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[:] edges_from_cell(H3int origin): +cpdef H3int[:] origin_to_directed_edges(H3int origin): """ Returns the 6 (or 5 for pentagons) directed edges for the given origin cell """ @@ -73,7 +74,7 @@ cpdef H3int[:] edges_from_cell(H3int origin): return mv -cpdef double mean_edge_length(int resolution, unit='km') except -1: +cpdef double average_hexagon_edge_length(int resolution, unit='km') except -1: cdef: double length diff --git a/src/h3/_cy/geo.pxd b/src/h3/_cy/geo.pxd index 1a2c1c332..40dfc3490 100644 --- a/src/h3/_cy/geo.pxd +++ b/src/h3/_cy/geo.pxd @@ -1,7 +1,7 @@ from .h3lib cimport H3int -cpdef H3int geo_to_h3(double lat, double lng, int res) except 1 -cpdef (double, double) h3_to_geo(H3int h) except * -cpdef double point_dist( +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 diff --git a/src/h3/_cy/geo.pyx b/src/h3/_cy/geo.pyx index f6a52215d..001aca29c 100644 --- a/src/h3/_cy/geo.pyx +++ b/src/h3/_cy/geo.pyx @@ -24,7 +24,7 @@ from libc.stdlib cimport ( ) -cpdef H3int geo_to_h3(double lat, double lng, int res) except 1: +cpdef H3int latlng_to_cell(double lat, double lng, int res) except 1: cdef: h3lib.LatLng c H3int out @@ -38,7 +38,7 @@ cpdef H3int geo_to_h3(double lat, double lng, int res) except 1: return out -cpdef (double, double) h3_to_geo(H3int h) except *: +cpdef (double, double) cell_to_latlng(H3int h) except *: """Map an H3 cell into its centroid geo-coordinate (lat/lng)""" cdef: h3lib.LatLng c @@ -237,7 +237,7 @@ def polyfill(dict geojson, int res, bool geo_json_conformant=False): return out -def cell_boundary(H3int h, bool geo_json=False): +def cell_to_boundary(H3int h, bool geo_json=False): """Compose an array of geo-coordinates that outlines a hexagonal cell""" cdef: h3lib.CellBoundary gb @@ -259,7 +259,7 @@ def cell_boundary(H3int h, bool geo_json=False): return verts -def edge_boundary(H3int edge, bool geo_json=False): +def directed_edge_to_boundary(H3int edge, bool geo_json=False): """ Returns the CellBoundary containing the coordinates of the edge """ cdef: @@ -283,7 +283,7 @@ def edge_boundary(H3int edge, bool geo_json=False): return verts -cpdef double point_dist( +cpdef double great_circle_distance( double lat1, double lng1, double lat2, double lng2, unit='km') except -1: diff --git a/src/h3/_cy/h3lib.pxd b/src/h3/_cy/h3lib.pxd index ec225bd34..150b549bf 100644 --- a/src/h3/_cy/h3lib.pxd +++ b/src/h3/_cy/h3lib.pxd @@ -138,6 +138,7 @@ cdef extern from 'h3api.h': 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 diff --git a/src/h3/_cy/to_multipoly.pyx b/src/h3/_cy/to_multipoly.pyx index 7a421f7c6..588e28388 100644 --- a/src/h3/_cy/to_multipoly.pyx +++ b/src/h3/_cy/to_multipoly.pyx @@ -59,7 +59,7 @@ def _geojson_loop(loop): return loop -def h3_set_to_multi_polygon(const H3int[:] hexes, geo_json=False): +def cells_to_multi_polygon(const H3int[:] hexes, geo_json=False): # todo: gotta be a more elegant way to handle these... if len(hexes) == 0: return [] diff --git a/src/h3/api/_api_template.py b/src/h3/api/_api_template.py index 0274e7fac..bc837bcf7 100644 --- a/src/h3/api/_api_template.py +++ b/src/h3/api/_api_template.py @@ -78,7 +78,7 @@ def versions(): return v @staticmethod - def string_to_h3(h): + def string_to_int(h): """ Converts a hexadecimal string to an H3 64-bit integer index. @@ -95,7 +95,7 @@ def string_to_h3(h): return _cy.hex2int(h) @staticmethod - def h3_to_string(x): + def int_to_string(x): """ Converts an H3 64-bit integer index to a hexadecimal string. @@ -112,7 +112,7 @@ def h3_to_string(x): return _cy.int2hex(x) @staticmethod - def num_hexagons(resolution): + def get_num_cells(resolution): """ Return the total number of *cells* (hexagons and pentagons) for the given resolution. @@ -121,10 +121,10 @@ def num_hexagons(resolution): ------- int """ - return _cy.num_hexagons(resolution) + return _cy.get_num_cells(resolution) @staticmethod - def hex_area(resolution, unit='km^2'): + def average_hexagon_area(resolution, unit='km^2'): """ Return the average area of an H3 *hexagon* for the given resolution. @@ -135,11 +135,10 @@ def hex_area(resolution, unit='km^2'): ------- float """ - # todo: `mean_hex_area` in 4.0 - return _cy.mean_hex_area(resolution, unit) + return _cy.average_hexagon_area(resolution, unit) @staticmethod - def edge_length(resolution, unit='km'): + def average_hexagon_edge_length(resolution, unit='km'): """ Return the average *hexagon* edge length for the given resolution. @@ -150,8 +149,7 @@ def edge_length(resolution, unit='km'): ------- float """ - # todo: `mean_edge_length` in 4.0 - return _cy.mean_edge_length(resolution, unit) + return _cy.average_hexagon_edge_length(resolution, unit) def is_valid_cell(self, h): """ @@ -167,7 +165,7 @@ def is_valid_cell(self, h): except (ValueError, TypeError): return False - def h3_unidirectional_edge_is_valid(self, edge): + def is_valid_directed_edge(self, edge): """ Validates an H3 unidirectional edge. @@ -177,11 +175,11 @@ def h3_unidirectional_edge_is_valid(self, edge): """ try: e = self._in_scalar(edge) - return _cy.is_edge(e) + return _cy.is_valid_directed_edge(e) except (ValueError, TypeError): return False - def geo_to_h3(self, lat, lng, resolution): + def latlng_to_cell(self, lat, lng, resolution): """ Return the cell containing the (lat, lng) point for a given resolution. @@ -191,9 +189,9 @@ def geo_to_h3(self, lat, lng, resolution): H3Cell """ - return self._out_scalar(_cy.geo_to_h3(lat, lng, resolution)) + return self._out_scalar(_cy.latlng_to_cell(lat, lng, resolution)) - def h3_to_geo(self, h): + def cell_to_latlng(self, h): """ Return the center point of an H3 cell as a lat/lng pair. @@ -208,9 +206,9 @@ def h3_to_geo(self, h): lng : float Longitude """ - return _cy.h3_to_geo(self._in_scalar(h)) + return _cy.cell_to_latlng(self._in_scalar(h)) - def h3_get_resolution(self, h): + def get_resolution(self, h): """ Return the resolution of an H3 cell. @@ -223,9 +221,9 @@ def h3_get_resolution(self, h): int """ # todo: could also work for edges - return _cy.resolution(self._in_scalar(h)) + return _cy.get_resolution(self._in_scalar(h)) - def h3_to_parent(self, h, res=None): + def cell_to_parent(self, h, res=None): """ Get the parent of a cell. @@ -241,12 +239,12 @@ def h3_to_parent(self, h, res=None): H3Cell """ h = self._in_scalar(h) - p = _cy.parent(h, res) + p = _cy.cell_to_parent(h, res) p = self._out_scalar(p) return p - def h3_distance(self, h1, h2): + def grid_distance(self, h1, h2): """ Compute the H3 distance between two cells. @@ -269,11 +267,11 @@ def h3_distance(self, h1, h2): h1 = self._in_scalar(h1) h2 = self._in_scalar(h2) - d = _cy.distance(h1, h2) + d = _cy.grid_distance(h1, h2) return d - def h3_to_geo_boundary(self, h, geo_json=False): + def cell_to_boundary(self, h, geo_json=False): """ Return tuple of lat/lng pairs describing the cell boundary. @@ -291,9 +289,9 @@ def h3_to_geo_boundary(self, h, geo_json=False): ------- tuple of (float, float) tuples """ - return _cy.cell_boundary(self._in_scalar(h), geo_json) + return _cy.cell_to_boundary(self._in_scalar(h), geo_json) - def k_ring(self, h, k=1): + def grid_disk(self, h, k=1): """ Return unordered set of cells with H3 distance ``<= k`` from ``h``. That is, the "filled-in" disk. @@ -308,11 +306,11 @@ def k_ring(self, h, k=1): ------- unordered collection of H3Cell """ - mv = _cy.disk(self._in_scalar(h), k) + mv = _cy.grid_disk(self._in_scalar(h), k) return self._out_unordered(mv) - def hex_ring(self, h, k=1): + def grid_ring(self, h, k=1): """ Return unordered set of cells with H3 distance ``== k`` from ``h``. That is, the "hollow" ring. @@ -327,49 +325,11 @@ def hex_ring(self, h, k=1): ------- unordered collection of H3Cell """ - mv = _cy.ring(self._in_scalar(h), k) + mv = _cy.grid_ring(self._in_scalar(h), k) return self._out_unordered(mv) - def hex_range_distances(self, h, K): - """ - Ordered list of the "hollow" rings around ``h``, - up to and including distance ``K``. - - Parameters - ---------- - h : H3Cell - K : int - Largest distance considered. - - Returns - ------- - ordered collection of (unordered collection of H3Cell) - """ - h = self._in_scalar(h) - - out = [ - self._out_unordered(_cy.ring(h, k)) - for k in range(K + 1) - ] - - return out - - def hex_ranges(self, hexes, K): - """ - Returns the dictionary ``{h: hex_range_distances(h, K) for h in hexes}`` - - Returns - ------- - Dict[H3Cell, List[ UnorderedCollection[H3Cell] ]] - """ - # todo: can we drop this function? the user can implement if needed. - # TODO: should we call `out_scalar` on the dict keys? - out = {h: self.hex_range_distances(h, K) for h in hexes} - - return out - - def h3_to_children(self, h, res=None): + def cell_to_children(self, h, res=None): """ Children of a hexagon. @@ -384,12 +344,12 @@ def h3_to_children(self, h, res=None): ------- unordered collection of H3Cell """ - mv = _cy.children(self._in_scalar(h), res) + mv = _cy.cell_to_children(self._in_scalar(h), res) return self._out_unordered(mv) # todo: nogil for expensive C operation? - def compact(self, hexes): + def compact_cells(self, hexes): """ Compact a collection of H3 cells by combining smaller cells into larger cells, if all child cells @@ -403,15 +363,15 @@ def compact(self, hexes): ------- unordered collection of H3Cell """ - # todo: does compact work on mixed-resolution collections? + # todo: does compact_cells work on mixed-resolution collections? hu = self._in_collection(hexes) - hc = _cy.compact(hu) + hc = _cy.compact_cells(hu) return self._out_unordered(hc) - def uncompact(self, hexes, res): + def uncompact_cells(self, hexes, res): """ - Reverse the `compact` operation. + Reverse the `compact_cells` operation. Return a collection of H3 cells, all of resolution ``res``. @@ -432,11 +392,11 @@ def uncompact(self, hexes, res): https://github.com/uber/h3/blob/master/src/h3lib/lib/h3Index.c#L425 """ hc = self._in_collection(hexes) - hu = _cy.uncompact(hc, res) + hu = _cy.uncompact_cells(hc, res) return self._out_unordered(hu) - def h3_set_to_multi_polygon(self, hexes, geo_json=False): + def cells_to_multi_polygon(self, hexes, geo_json=False): """ Get GeoJSON-like MultiPolygon describing the outline of the area covered by a set of H3 cells. @@ -463,7 +423,7 @@ def h3_set_to_multi_polygon(self, hexes, geo_json=False): # This function returns a list of polygons, while `polyfill` returns # a GeoJSON-like dictionary object. hexes = self._in_collection(hexes) - return _cy.h3_set_to_multi_polygon(hexes, geo_json=geo_json) + return _cy.cells_to_multi_polygon(hexes, geo_json=geo_json) def polyfill_polygon(self, outer, res, holes=None, lnglat_order=False): mv = _cy.polyfill_polygon(outer, res, holes=holes, lnglat_order=lnglat_order) @@ -514,7 +474,7 @@ def polyfill(self, geojson, res, geo_json_conformant=False): return self._out_unordered(mv) - def h3_is_pentagon(self, h): + def is_pentagon(self, h): """ Identify if an H3 cell is a pentagon. @@ -534,7 +494,7 @@ def h3_is_pentagon(self, h): """ return _cy.is_pentagon(self._in_scalar(h)) - def h3_get_base_cell(self, h): + def get_base_cell_number(self, h): """ Return the base cell *number* (``0`` to ``121``) of the given cell. @@ -554,9 +514,9 @@ def h3_get_base_cell(self, h): ------- int """ - return _cy.get_base_cell(self._in_scalar(h)) + return _cy.get_base_cell_number(self._in_scalar(h)) - def h3_indexes_are_neighbors(self, h1, h2): + def are_neighbor_cells(self, h1, h2): """ Returns ``True`` if ``h1`` and ``h2`` are neighboring cells. @@ -572,9 +532,9 @@ def h3_indexes_are_neighbors(self, h1, h2): h1 = self._in_scalar(h1) h2 = self._in_scalar(h2) - return _cy.are_neighbors(h1, h2) + return _cy.are_neighbor_cells(h1, h2) - def get_h3_unidirectional_edge(self, origin, destination): + def cells_to_directed_edge(self, origin, destination): """ Create an H3 Index denoting a unidirectional edge. @@ -597,12 +557,12 @@ def get_h3_unidirectional_edge(self, origin, destination): """ o = self._in_scalar(origin) d = self._in_scalar(destination) - e = _cy.edge(o, d) + e = _cy.cells_to_directed_edge(o, d) e = self._out_scalar(e) return e - def get_origin_h3_index_from_unidirectional_edge(self, e): + def get_directed_edge_origin(self, e): """ Origin cell from an H3 directed edge. @@ -615,12 +575,12 @@ def get_origin_h3_index_from_unidirectional_edge(self, e): H3Cell """ e = self._in_scalar(e) - o = _cy.edge_origin(e) + o = _cy.get_directed_edge_origin(e) o = self._out_scalar(o) return o - def get_destination_h3_index_from_unidirectional_edge(self, e): + def get_directed_edge_destination(self, e): """ Destination cell from an H3 directed edge. @@ -633,12 +593,12 @@ def get_destination_h3_index_from_unidirectional_edge(self, e): H3Cell """ e = self._in_scalar(e) - d = _cy.edge_destination(e) + d = _cy.get_directed_edge_destination(e) d = self._out_scalar(d) return d - def get_h3_indexes_from_unidirectional_edge(self, e): + def directed_edge_to_cells(self, e): """ Return (origin, destination) tuple from H3 directed edge @@ -654,12 +614,12 @@ def get_h3_indexes_from_unidirectional_edge(self, e): Destination cell of edge """ e = self._in_scalar(e) - o, d = _cy.edge_cells(e) + o, d = _cy.directed_edge_to_cells(e) o, d = self._out_scalar(o), self._out_scalar(d) return o, d - def get_h3_unidirectional_edges_from_hexagon(self, origin): + def origin_to_directed_edges(self, origin): """ Return all directed edges starting from ``origin`` cell. @@ -671,14 +631,14 @@ def get_h3_unidirectional_edges_from_hexagon(self, origin): ------- unordered collection of H3Edge """ - mv = _cy.edges_from_cell(self._in_scalar(origin)) + mv = _cy.origin_to_directed_edges(self._in_scalar(origin)) return self._out_unordered(mv) - def get_h3_unidirectional_edge_boundary(self, edge, geo_json=False): - return _cy.edge_boundary(self._in_scalar(edge), geo_json=geo_json) + def directed_edge_to_boundary(self, edge, geo_json=False): + return _cy.directed_edge_to_boundary(self._in_scalar(edge), geo_json=geo_json) - def h3_line(self, start, end): + def grid_path_cells(self, start, end): """ Returns the ordered collection of cells denoting a minimum-length non-unique path between cells. @@ -693,11 +653,11 @@ def h3_line(self, start, end): ordered collection of H3Cell Starting with ``start``, and ending with ``end``. """ - mv = _cy.line(self._in_scalar(start), self._in_scalar(end)) + mv = _cy.grid_path_cells(self._in_scalar(start), self._in_scalar(end)) return self._out_ordered(mv) - def h3_is_res_class_III(self, h): + def is_res_class_III(self, h): """ Determine if cell has orientation "Class II" or "Class III". @@ -726,7 +686,7 @@ def h3_is_res_class_III(self, h): """ return _cy.is_res_class_iii(self._in_scalar(h)) - def get_pentagon_indexes(self, resolution): + def get_pentagons(self, resolution): """ Return all pentagons at a given resolution. @@ -738,11 +698,11 @@ def get_pentagon_indexes(self, resolution): ------- unordered collection of H3Cell """ - mv = _cy.get_pentagon_indexes(resolution) + mv = _cy.get_pentagons(resolution) return self._out_unordered(mv) - def get_res0_indexes(self): + def get_res0_cells(self): """ Return all cells at resolution 0. @@ -754,11 +714,11 @@ def get_res0_indexes(self): ------- unordered collection of H3Cell """ - mv = _cy.get_res0_indexes() + mv = _cy.get_res0_cells() return self._out_unordered(mv) - def h3_to_center_child(self, h, res=None): + def cell_to_center_child(self, h, res=None): """ Get the center child of a cell at some finer resolution. @@ -774,12 +734,12 @@ def h3_to_center_child(self, h, res=None): H3Cell """ h = self._in_scalar(h) - p = _cy.center_child(h, res) + p = _cy.cell_to_center_child(h, res) p = self._out_scalar(p) return p - def h3_get_faces(self, h): + def get_icosahedron_faces(self, h): """ Return icosahedron faces intersecting a given H3 cell. @@ -796,11 +756,11 @@ def h3_get_faces(self, h): Python ``set`` of ``int`` """ h = self._in_scalar(h) - faces = _cy.get_faces(h) + faces = _cy.get_icosahedron_faces(h) return faces - def experimental_h3_to_local_ij(self, origin, h): + def cell_to_local_ij(self, origin, h): """ Return local (i,j) coordinates of cell ``h`` in relation to ``origin`` cell @@ -834,11 +794,11 @@ def experimental_h3_to_local_ij(self, origin, h): origin = self._in_scalar(origin) h = self._in_scalar(h) - i, j = _cy.experimental_h3_to_local_ij(origin, h) + i, j = _cy.cell_to_local_ij(origin, h) return i, j - def experimental_local_ij_to_h3(self, origin, i, j): + def local_ij_to_cell(self, origin, i, j): """ Return cell at local (i,j) position relative to the ``origin`` cell. @@ -869,7 +829,7 @@ def experimental_local_ij_to_h3(self, origin, i, j): """ origin = self._in_scalar(origin) - h = _cy.experimental_local_ij_to_h3(origin, i, j) + h = _cy.local_ij_to_cell(origin, i, j) h = self._out_scalar(h) return h @@ -895,13 +855,13 @@ def cell_area(self, h, unit='km^2'): This function breaks the cell into spherical triangles, and computes their spherical area. The function uses the spherical distance calculation given by - `point_dist`. + `great_circle_distance`. """ h = self._in_scalar(h) return _cy.cell_area(h, unit=unit) - def exact_edge_length(self, e, unit='km'): + def edge_length(self, e, unit='km'): """ Compute the spherical length of a specific H3 edge. @@ -920,14 +880,14 @@ def exact_edge_length(self, e, unit='km'): Notes ----- This function uses the spherical distance calculation given by - `point_dist`. + `great_circle_distance`. """ e = self._in_scalar(e) return _cy.edge_length(e, unit=unit) @staticmethod - def point_dist(point1, point2, unit='km'): + def great_circle_distance(point1, point2, unit='km'): """ Compute the spherical distance between two (lat, lng) points. @@ -935,6 +895,8 @@ def point_dist(point1, point2, unit='km'): about (lat1, lng1, lat2, lng2) as the input? How will this work for vectorized versions? + todo: overload to allow two cell inputs? + Parameters ---------- point1 : tuple @@ -952,7 +914,7 @@ def point_dist(point1, point2, unit='km'): lat1, lng1 = point1 lat2, lng2 = point2 - return _cy.point_dist( + return _cy.great_circle_distance( lat1, lng1, lat2, lng2, unit=unit diff --git a/src/h3/api/basic_int/_public_api.py b/src/h3/api/basic_int/_public_api.py index b25064e28..56c6a43fd 100644 --- a/src/h3/api/basic_int/_public_api.py +++ b/src/h3/api/basic_int/_public_api.py @@ -6,58 +6,59 @@ This file exists to avoid dynamically modifying `globals` and support static tooling. """ -from ._binding import _binding - -cell_area = _binding.cell_area -compact = _binding.compact -edge_length = _binding.edge_length -exact_edge_length = _binding.exact_edge_length -experimental_h3_to_local_ij = _binding.experimental_h3_to_local_ij -experimental_local_ij_to_h3 = _binding.experimental_local_ij_to_h3 -geo_to_h3 = _binding.geo_to_h3 -get_destination_h3_index_from_unidirectional_edge = ( - _binding.get_destination_h3_index_from_unidirectional_edge -) -get_h3_indexes_from_unidirectional_edge = ( - _binding.get_h3_indexes_from_unidirectional_edge -) -get_h3_unidirectional_edge = _binding.get_h3_unidirectional_edge -get_h3_unidirectional_edge_boundary = _binding.get_h3_unidirectional_edge_boundary -get_h3_unidirectional_edges_from_hexagon = ( - _binding.get_h3_unidirectional_edges_from_hexagon -) -get_origin_h3_index_from_unidirectional_edge = ( - _binding.get_origin_h3_index_from_unidirectional_edge -) -get_pentagon_indexes = _binding.get_pentagon_indexes -get_res0_indexes = _binding.get_res0_indexes -h3_distance = _binding.h3_distance -h3_get_base_cell = _binding.h3_get_base_cell -h3_get_faces = _binding.h3_get_faces -h3_get_resolution = _binding.h3_get_resolution -h3_indexes_are_neighbors = _binding.h3_indexes_are_neighbors -h3_is_pentagon = _binding.h3_is_pentagon -h3_is_res_class_III = _binding.h3_is_res_class_III -is_valid_cell = _binding.is_valid_cell -h3_line = _binding.h3_line -h3_set_to_multi_polygon = _binding.h3_set_to_multi_polygon -h3_to_center_child = _binding.h3_to_center_child -h3_to_children = _binding.h3_to_children -h3_to_geo = _binding.h3_to_geo -h3_to_geo_boundary = _binding.h3_to_geo_boundary -h3_to_parent = _binding.h3_to_parent -h3_to_string = _binding.h3_to_string -h3_unidirectional_edge_is_valid = _binding.h3_unidirectional_edge_is_valid -hex_area = _binding.hex_area -hex_range_distances = _binding.hex_range_distances -hex_ranges = _binding.hex_ranges -hex_ring = _binding.hex_ring -k_ring = _binding.k_ring -num_hexagons = _binding.num_hexagons -point_dist = _binding.point_dist -polyfill = _binding.polyfill -polyfill_geojson = _binding.polyfill_geojson -polyfill_polygon = _binding.polyfill_polygon -string_to_h3 = _binding.string_to_h3 -uncompact = _binding.uncompact -versions = _binding.versions +from ._binding import _binding as _b + + +is_valid_cell = _b.is_valid_cell +is_pentagon = _b.is_pentagon +is_valid_directed_edge = _b.is_valid_directed_edge +is_res_class_III = _b.is_res_class_III + +int_to_string = _b.int_to_string +string_to_int = _b.string_to_int + +cell_area = _b.cell_area +edge_length = _b.edge_length +great_circle_distance = _b.great_circle_distance +average_hexagon_area = _b.average_hexagon_area +average_hexagon_edge_length = _b.average_hexagon_edge_length + +latlng_to_cell = _b.latlng_to_cell +cell_to_latlng = _b.cell_to_latlng +cell_to_boundary = _b.cell_to_boundary +cell_to_local_ij = _b.cell_to_local_ij +local_ij_to_cell = _b.local_ij_to_cell + +grid_ring = _b.grid_ring +grid_disk = _b.grid_disk +grid_distance = _b.grid_distance +grid_path_cells = _b.grid_path_cells + +get_num_cells = _b.get_num_cells +get_pentagons = _b.get_pentagons +get_res0_cells = _b.get_res0_cells +get_resolution = _b.get_resolution +get_base_cell_number = _b.get_base_cell_number +get_icosahedron_faces = _b.get_icosahedron_faces + +cell_to_parent = _b.cell_to_parent +cell_to_children = _b.cell_to_children +cell_to_center_child = _b.cell_to_center_child +compact_cells = _b.compact_cells +uncompact_cells = _b.uncompact_cells + +# todo: think through polyfill functions +polyfill = _b.polyfill +polyfill_geojson = _b.polyfill_geojson +polyfill_polygon = _b.polyfill_polygon +cells_to_multi_polygon = _b.cells_to_multi_polygon + +are_neighbor_cells = _b.are_neighbor_cells +cells_to_directed_edge = _b.cells_to_directed_edge +directed_edge_to_cells = _b.directed_edge_to_cells +origin_to_directed_edges = _b.origin_to_directed_edges +get_directed_edge_origin = _b.get_directed_edge_origin +get_directed_edge_destination = _b.get_directed_edge_destination +directed_edge_to_boundary = _b.directed_edge_to_boundary + +versions = _b.versions diff --git a/tests/cython_example.pyx b/tests/cython_example.pyx index e8d285947..a465bd6ee 100644 --- a/tests/cython_example.pyx +++ b/tests/cython_example.pyx @@ -1,11 +1,11 @@ from cython cimport boundscheck, wraparound from h3._cy.h3lib cimport H3int -from h3._cy.geo cimport geo_to_h3 +from h3._cy.geo cimport latlng_to_cell @boundscheck(False) @wraparound(False) -cpdef void geo_to_h3_vect( +cpdef void latlng_to_cell_vect( const double[:] lat, const double[:] lng, int res, @@ -15,4 +15,4 @@ cpdef void geo_to_h3_vect( cdef Py_ssize_t i for i in range(len(lat)): - out[i] = geo_to_h3(lat[i], lng[i], res) + out[i] = latlng_to_cell(lat[i], lng[i], res) diff --git a/tests/test_basic_int.py b/tests/test_basic_int.py index d5c47c02e..1b8b8b316 100644 --- a/tests/test_basic_int.py +++ b/tests/test_basic_int.py @@ -5,11 +5,11 @@ def test_int_output(): lat = 37.7752702151959 lng = -122.418307270836 - assert h3.geo_to_h3(lat, lng, 9) == 617700169958293503 - assert h3.geo_to_h3(lat, lng, 9) == 0x8928308280fffff + assert h3.latlng_to_cell(lat, lng, 9) == 617700169958293503 + assert h3.latlng_to_cell(lat, lng, 9) == 0x8928308280fffff -def test_k_ring(): +def test_grid_disk(): expected = { 617700169957507071, 617700169957769215, @@ -20,29 +20,29 @@ def test_k_ring(): 617700169965109247, } - out = h3.k_ring(617700169958293503, 1) + out = h3.grid_disk(617700169958293503, 1) assert out == expected -def test_compact(): +def test_compact_cells(): h = 617700169958293503 - hexes = h3.h3_to_children(h) + hexes = h3.cell_to_children(h) - assert h3.compact(hexes) == {h} + assert h3.compact_cells(hexes) == {h} -def test_get_faces(): +def test_get_icosahedron_faces(): h = 577832942814887935 expected = {2, 3, 7, 8, 12} - out = h3.h3_get_faces(h) + out = h3.get_icosahedron_faces(h) assert out == expected h = 579873636396040191 expected = {13} - out = h3.h3_get_faces(h) + out = h3.get_icosahedron_faces(h) assert out == expected h = 579768083279773695 expected = {16, 15} - out = h3.h3_get_faces(h) + out = h3.get_icosahedron_faces(h) assert out == expected diff --git a/tests/test_basic_str.py b/tests/test_basic_str.py index f76567849..eaae35bb0 100644 --- a/tests/test_basic_str.py +++ b/tests/test_basic_str.py @@ -2,7 +2,8 @@ def test1(): - assert h3.geo_to_h3(37.7752702151959, -122.418307270836, 9) == '8928308280fffff' + lat, lng = 37.7752702151959, -122.418307270836 + assert h3.latlng_to_cell(lat, lng, 9) == '8928308280fffff' def test5(): @@ -16,5 +17,5 @@ def test5(): '89283082803ffff' } - out = h3.k_ring('8928308280fffff', 1) + out = h3.grid_disk('8928308280fffff', 1) assert out == expected diff --git a/tests/test_cells_and_edges.py b/tests/test_cells_and_edges.py index 194b05b46..cfe6f178e 100644 --- a/tests/test_cells_and_edges.py +++ b/tests/test_cells_and_edges.py @@ -22,14 +22,15 @@ def approx2(a, b): def test1(): - assert h3.geo_to_h3(37.7752702151959, -122.418307270836, 9) == '8928308280fffff' + 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.h3_to_geo(h) == pytest.approx(expected) + assert h3.cell_to_latlng(h) == pytest.approx(expected) def test3(): @@ -42,7 +43,7 @@ def test3(): (37.775019673792606, -122.4195306280734), ) - out = h3.h3_to_geo_boundary('8928308280fffff') + out = h3.cell_to_boundary('8928308280fffff') assert approx2(out, expected) @@ -57,18 +58,18 @@ def test4(): (-122.41719971841658, 37.775197782893386) ) - out = h3.h3_to_geo_boundary('8928308280fffff', geo_json=True) + out = h3.cell_to_boundary('8928308280fffff', geo_json=True) assert approx2(out, expected) -def test_k_ring_distance(): +def test_grid_disk_distance(): with pytest.raises(H3DomainError): - h3.k_ring('8928308280fffff', -10) + h3.grid_disk('8928308280fffff', -10) -def test_hex_ring_distance(): +def test_grid_ring_distance(): with pytest.raises(H3DomainError): - h3.hex_ring('8928308280fffff', -10) + h3.grid_ring('8928308280fffff', -10) def test5(): @@ -82,13 +83,13 @@ def test5(): '89283082803ffff' } - out = h3.k_ring('8928308280fffff', 1) + out = h3.grid_disk('8928308280fffff', 1) assert out == expected def test6(): expected = {'8928308280fffff'} - out = h3.hex_ring('8928308280fffff', 0) + out = h3.grid_ring('8928308280fffff', 0) assert out == expected @@ -102,7 +103,7 @@ def test7(): '89283082877ffff' } - out = h3.hex_ring('8928308280fffff', 1) + out = h3.grid_ring('8928308280fffff', 1) assert out == expected @@ -116,41 +117,41 @@ def test8(): # other methods should validate and raise exception if bad input with pytest.raises(H3CellInvalidError): - h3.h3_get_resolution(h_bad) + h3.get_resolution(h_bad) def test9(): - assert h3.h3_get_resolution('8928308280fffff') == 9 - assert h3.h3_get_resolution('8a28308280f7fff') == 10 + assert h3.get_resolution('8928308280fffff') == 9 + assert h3.get_resolution('8a28308280f7fff') == 10 def test_parent(): h = '8928308280fffff' - assert h3.h3_to_parent(h, 7) == '872830828ffffff' - assert h3.h3_to_parent(h, 8) == '8828308281fffff' - assert h3.h3_to_parent(h, 9) == h + 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.h3_to_parent(h, 10) + h3.cell_to_parent(h, 10) def test_parent_err(): # Test 1 - h = '8075fffffffffff' # geo_to_h3(0,0,0) + h = '8075fffffffffff' # latlng_to_cell(0,0,0) with pytest.raises(H3ResDomainError): - h3.h3_to_parent(h) + h3.cell_to_parent(h) # Test 2 try: - h3.h3_to_parent(h) + 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.string_to_h3(h))) + expected = expected.format(hex(h3.string_to_int(h))) assert msg == expected @@ -159,10 +160,10 @@ def test_children(): h = '8928308280fffff' with pytest.raises(H3ResDomainError): - h3.h3_to_children(h, 8) + h3.cell_to_children(h, 8) # same resolution is set of just cell itself - out = h3.h3_to_children(h, 9) + out = h3.cell_to_children(h, 9) assert out == {h} # one below should give children @@ -175,13 +176,13 @@ def test_children(): '8a28308280effff', '8a28308280f7fff' } - out = h3.h3_to_children(h, 10) + out = h3.cell_to_children(h, 10) assert out == expected # finest resolution cell should return error for children h = '8f04ccb2c45e225' with pytest.raises(H3ResDomainError): - h3.h3_to_children(h) + h3.cell_to_children(h) def test_center_child(): @@ -189,30 +190,30 @@ def test_center_child(): # one above should raise an exception with pytest.raises(H3ResDomainError): - h3.h3_to_center_child(h, 8) + h3.cell_to_center_child(h, 8) # same resolution should be same cell - assert h3.h3_to_center_child(h, 9) == h + assert h3.cell_to_center_child(h, 9) == h # one below should give direct child expected = '8a28308280c7fff' - assert h3.h3_to_center_child(h, 10) == expected + assert h3.cell_to_center_child(h, 10) == expected # finest resolution hex should return error for child h = '8f04ccb2c45e225' with pytest.raises(H3ResDomainError): - h3.h3_to_center_child(h) + h3.cell_to_center_child(h) def test_distance(): h = '8a28308280c7fff' - assert h3.h3_distance(h, h) == 0 + assert h3.grid_distance(h, h) == 0 - n = h3.hex_ring(h, 1).pop() - assert h3.h3_distance(h, n) == 1 + n = h3.grid_ring(h, 1).pop() + assert h3.grid_distance(h, n) == 1 - n = h3.hex_ring(h, 2).pop() - assert h3.h3_distance(h, n) == 2 + n = h3.grid_ring(h, 2).pop() + assert h3.grid_distance(h, n) == 2 def test_distance_error(): @@ -224,10 +225,10 @@ def test_distance_error(): h2 = '835804fffffffff' with pytest.raises(H3FailedError): - h3.h3_distance(h1, h2) + h3.grid_distance(h1, h2) -def test_compact(): +def test_compact_cells(): # lat/lngs for State of Maine maine = [ @@ -256,7 +257,7 @@ def test_compact(): res = 5 h_uncomp = h3.polyfill_polygon(maine, res) - h_comp = h3.compact(h_uncomp) + h_comp = h3.compact_cells(h_uncomp) expected = {'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 @@ -265,16 +266,16 @@ def test_compact(): return h_uncomp, h_comp, res -def test_uncompact(): +def test_uncompact_cells(): - h_uncomp, h_comp, res = test_compact() + h_uncomp, h_comp, res = test_compact_cells() - out = h3.uncompact(h_comp, res) + out = h3.uncompact_cells(h_comp, res) assert out == h_uncomp -def test_num_hexagons(): +def test_get_num_cells(): expected = { 0: 122, 1: 842, @@ -284,14 +285,14 @@ def test_num_hexagons(): } out = { - k: h3.num_hexagons(k) + k: h3.get_num_cells(k) for k in expected } assert expected == out -def test_hex_area(): +def test_average_hexagon_area(): expected_in_km2 = { 0: 4357449.416078381, 1: 609788.441794133, @@ -301,14 +302,14 @@ def test_hex_area(): } out = { - k: h3.hex_area(k, unit='km^2') + k: h3.average_hexagon_area(k, unit='km^2') for k in expected_in_km2 } assert out == pytest.approx(expected_in_km2) -def test_hex_edge_length(): +def test_average_hexagon_edge_length(): expected_in_km = { 0: 1107.712591000, 1: 418.676005500, @@ -318,7 +319,7 @@ def test_hex_edge_length(): } out = { - res: h3.edge_length(res, unit='km') + res: h3.average_hexagon_edge_length(res, unit='km') for res in expected_in_km } @@ -329,29 +330,29 @@ def test_edge(): h1 = '8928308280fffff' h2 = '89283082873ffff' - assert not h3.h3_indexes_are_neighbors(h1, h1) - assert h3.h3_indexes_are_neighbors(h1, h2) + assert not h3.are_neighbor_cells(h1, h1) + assert h3.are_neighbor_cells(h1, h2) - e = h3.get_h3_unidirectional_edge(h1, h2) + e = h3.cells_to_directed_edge(h1, h2) assert e == '12928308280fffff' - assert h3.h3_unidirectional_edge_is_valid(e) + assert h3.is_valid_directed_edge(e) assert not h3.is_valid_cell(e) - assert h3.get_origin_h3_index_from_unidirectional_edge(e) == h1 - assert h3.get_destination_h3_index_from_unidirectional_edge(e) == h2 + assert h3.get_directed_edge_origin(e) == h1 + assert h3.get_directed_edge_destination(e) == h2 - assert h3.get_h3_indexes_from_unidirectional_edge(e) == (h1, h2) + assert h3.directed_edge_to_cells(e) == (h1, h2) -def test_edges_from_cell(): +def test_origin_to_directed_edges(): h = '8928308280fffff' - edges = h3.get_h3_unidirectional_edges_from_hexagon(h) + edges = h3.origin_to_directed_edges(h) destinations = { - h3.get_destination_h3_index_from_unidirectional_edge(e) + h3.get_directed_edge_destination(e) for e in edges } - neighbors = h3.hex_ring(h, 1) + neighbors = h3.grid_ring(h, 1) assert neighbors == destinations @@ -359,14 +360,14 @@ def test_edges_from_cell(): def test_edge_boundary(): h1 = '8928308280fffff' h2 = '89283082873ffff' - e = h3.get_h3_unidirectional_edge(h1, h2) + e = h3.cells_to_directed_edge(h1, h2) expected = ( (37.77688044840226, -122.41612835779266), (37.778385004930925, -122.41738797617619) ) - out = h3.get_h3_unidirectional_edge_boundary(e) + out = h3.directed_edge_to_boundary(e) assert out[0] == pytest.approx(expected[0]) assert out[1] == pytest.approx(expected[1]) @@ -376,83 +377,83 @@ def test_validation(): h = '8a28308280fffff' # invalid cell with pytest.raises(H3CellInvalidError): - h3.h3_get_base_cell(h) + h3.get_base_cell_number(h) with pytest.raises(H3CellInvalidError): - h3.h3_get_resolution(h) + h3.get_resolution(h) with pytest.raises(H3CellInvalidError): - h3.h3_to_parent(h, 9) + h3.cell_to_parent(h, 9) with pytest.raises(H3CellInvalidError): - h3.h3_distance(h, h) + h3.grid_distance(h, h) with pytest.raises(H3CellInvalidError): - h3.k_ring(h, 1) + h3.grid_disk(h, 1) with pytest.raises(H3CellInvalidError): - h3.hex_ring(h, 1) + h3.grid_ring(h, 1) with pytest.raises(H3CellInvalidError): - h3.h3_to_children(h, 11) + h3.cell_to_children(h, 11) with pytest.raises(H3CellInvalidError): - h3.compact({h}) + h3.compact_cells({h}) with pytest.raises(H3CellInvalidError): - h3.uncompact({h}, 10) + h3.uncompact_cells({h}, 10) def test_validation2(): h = '8928308280fffff' with pytest.raises(H3ResDomainError): - h3.h3_to_children(h, 17) + h3.cell_to_children(h, 17) - assert not h3.h3_indexes_are_neighbors(h, h) + assert not h3.are_neighbor_cells(h, h) def test_validation_geo(): h = '8a28308280fffff' # invalid cell with pytest.raises(H3CellInvalidError): - h3.h3_to_geo(h) + h3.cell_to_latlng(h) with pytest.raises(H3ResDomainError): - h3.geo_to_h3(0, 0, 17) + h3.latlng_to_cell(0, 0, 17) with pytest.raises(H3CellInvalidError): - h3.h3_to_geo_boundary(h) + 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.h3_indexes_are_neighbors(h, h) + assert not h3.are_neighbor_cells(h, h) def test_edges(): h = '8928308280fffff' with pytest.raises(H3NotNeighborsError): - h3.get_h3_unidirectional_edge(h, h) + h3.cells_to_directed_edge(h, h) - h2 = h3.hex_ring(h, 2).pop() + h2 = h3.grid_ring(h, 2).pop() with pytest.raises(H3NotNeighborsError): - h3.get_h3_unidirectional_edge(h, h2) + h3.cells_to_directed_edge(h, h2) e_bad = '14928308280ffff1' - assert not h3.h3_unidirectional_edge_is_valid(e_bad) + assert not h3.is_valid_directed_edge(e_bad) # note: won't raise an error on bad input - h3.get_origin_h3_index_from_unidirectional_edge(e_bad) - h3.get_destination_h3_index_from_unidirectional_edge(e_bad) - h3.get_h3_indexes_from_unidirectional_edge(e_bad) + 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.h3_line(h1, h2) + out = h3.grid_path_cells(h1, h2) expected = [ '8928308280fffff', @@ -479,9 +480,9 @@ def test_versions(): def test_str_int_convert(): s = '8928308280fffff' - i = h3.string_to_h3(s) + i = h3.string_to_int(s) - assert h3.h3_to_string(i) == s + assert h3.int_to_string(i) == s def test_hex2int_fail(): @@ -492,11 +493,11 @@ def test_hex2int_fail(): def test_edge_is_valid_fail(): e_invalid = {} - assert not h3.h3_unidirectional_edge_is_valid(e_invalid) + assert not h3.is_valid_directed_edge(e_invalid) def test_get_pentagons(): - out = h3.get_pentagon_indexes(0) + out = h3.get_pentagons(0) expected = { '8009fffffffffff', @@ -515,7 +516,7 @@ def test_get_pentagons(): assert out == expected - out = h3.get_pentagon_indexes(5) + out = h3.get_pentagons(5) expected = { '85080003fffffff', @@ -535,26 +536,26 @@ def test_get_pentagons(): assert out == expected for i in range(16): - assert len(h3.get_pentagon_indexes(i)) == 12 + assert len(h3.get_pentagons(i)) == 12 def test_uncompact_cell_input(): - # `uncompact` takes in a collection of cells, not a single cell. + # `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('8001fffffffffff', 1) + h3.uncompact_cells('8001fffffffffff', 1) -def test_get_res0_indexes(): - out = h3.get_res0_indexes() +def test_get_res0_cells(): + out = h3.get_res0_cells() assert len(out) == 122 # subset - pentagons = h3.get_pentagon_indexes(0) + pentagons = h3.get_pentagons(0) assert pentagons < out # all valid @@ -562,7 +563,7 @@ def test_get_res0_indexes(): # resolution assert all(map( - lambda h: h3.h3_get_resolution(h) == 0, + lambda h: h3.get_resolution(h) == 0, out )) @@ -575,55 +576,55 @@ def test_get_res0_indexes(): assert sub < out -def test_get_faces(): +def test_get_icosahedron_faces(): h = '804dfffffffffff' expected = {2, 3, 7, 8, 12} - out = h3.h3_get_faces(h) + out = h3.get_icosahedron_faces(h) assert out == expected h = '80c1fffffffffff' expected = {13} - out = h3.h3_get_faces(h) + out = h3.get_icosahedron_faces(h) assert out == expected h = '80bbfffffffffff' expected = {16, 15} - out = h3.h3_get_faces(h) + out = h3.get_icosahedron_faces(h) assert out == expected def test_to_local_ij_error(): - h = h3.geo_to_h3(0, 0, 0) + h = h3.latlng_to_cell(0, 0, 0) # error if we cross a face - nb = h3.hex_ring(h, k=2) + nb = h3.grid_ring(h, k=2) # todo: should this be the E_TOO_FAR guy? with pytest.raises(H3FailedError): - [h3.experimental_h3_to_local_ij(h, p) for p in nb] + [h3.cell_to_local_ij(h, p) for p in nb] # should be fine if we do not cross a face - nb = h3.hex_ring(h, k=1) - out = {h3.experimental_h3_to_local_ij(h, p) for p in nb} + 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.geo_to_h3(0, 0, 0) + 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.experimental_local_ij_to_h3(h, i, j) + h3.local_ij_to_cell(h, i, j) # inverting output should give good data - nb = h3.hex_ring(h, k=1) - goodies = {h3.experimental_h3_to_local_ij(h, p) for p in nb} + nb = h3.grid_ring(h, k=1) + goodies = {h3.cell_to_local_ij(h, p) for p in nb} out = { - h3.experimental_local_ij_to_h3(h, i, j) + h3.local_ij_to_cell(h, i, j) for i, j in goodies } @@ -631,7 +632,7 @@ def test_from_local_ij_error(): def test_to_local_ij_self(): - h = h3.geo_to_h3(0, 0, 9) - out = h3.experimental_h3_to_local_ij(h, h) + h = h3.latlng_to_cell(0, 0, 9) + out = h3.cell_to_local_ij(h, h) assert out == (-858, -2766) diff --git a/tests/test_collection_inputs.py b/tests/test_collection_inputs.py index aa09c4fd7..92a8ba541 100644 --- a/tests/test_collection_inputs.py +++ b/tests/test_collection_inputs.py @@ -22,15 +22,15 @@ def test_set(): h = 614553222213795839 - assert basic_int.compact(ints) == {h} + assert basic_int.compact_cells(ints) == {h} with pytest.raises(TypeError): # numpy can't convert from a set - numpy_int.compact(ints) + numpy_int.compact_cells(ints) with pytest.raises(TypeError): # set isn't a memoryview - memview_int.compact(ints) + memview_int.compact_cells(ints) def test_list(): @@ -46,19 +46,19 @@ def test_list(): h = 614553222213795839 - assert basic_int.compact(ints) == {h} + 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(ints) == np.array([h], dtype='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(ints) == np.array([h]) - assert not numpy_int.compact(ints).dtype == np.array([h]).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(ints) + memview_int.compact_cells(ints) def test_np_array(): @@ -74,12 +74,12 @@ def test_np_array(): h = 614553222213795839 - assert basic_int.compact(ints) == {h} + assert basic_int.compact_cells(ints) == {h} - assert numpy_int.compact(ints) == np.array([h], dtype='uint64') - assert numpy_int.compact(ints).dtype == np.dtype('uint64') + 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(ints) + out = memview_int.compact_cells(ints) assert len(out) == 1 assert out[0] == h @@ -99,15 +99,15 @@ def test_list_to_array(): h = 614553222213795839 - assert basic_int.compact(ints) == {h} - assert numpy_int.compact(ints) == np.array([h], dtype='uint64') + 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(ints) + memview_int.compact_cells(ints) def test_iterator(): @@ -128,14 +128,14 @@ def foo(): h = 614553222213795839 ints = foo() - assert basic_int.compact(ints) == {h} + 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(ints) + numpy_int.compact_cells(ints) ints = foo() with pytest.raises(TypeError): # requires a bytes-like input - memview_int.compact(ints) + memview_int.compact_cells(ints) diff --git a/tests/test_cython.py b/tests/test_cython.py index 30566b188..89aa06467 100644 --- a/tests/test_cython.py +++ b/tests/test_cython.py @@ -3,15 +3,15 @@ # Avoid checking for import-error here because cython_example may not have # been compiled yet. try: - from .cython_example import geo_to_h3_vect # pylint: disable=import-error + from .cython_example import latlng_to_cell_vect # pylint: disable=import-error except ImportError: - geo_to_h3_vect = None + latlng_to_cell_vect = None np.random.seed(0) def test_cython_api(): - if geo_to_h3_vect is None: + if latlng_to_cell_vect is None: print("Not running Cython test because cython example was not compiled") return @@ -23,6 +23,6 @@ def test_cython_api(): lats = np.array(lats, dtype=np.float64) lngs = np.array(lngs, dtype=np.float64) out = np.zeros(len(lats), dtype="uint64") - geo_to_h3_vect(lats, lngs, res, out) + latlng_to_cell_vect(lats, lngs, res, out) assert out[0] == 617284541015654399 diff --git a/tests/test_h3.py b/tests/test_h3.py index 333860642..3cf5d8f35 100644 --- a/tests/test_h3.py +++ b/tests/test_h3.py @@ -19,38 +19,38 @@ def test_is_valid_cell(): assert not h3.is_valid_cell('5004295803a88') for res in range(16): - assert h3.is_valid_cell(h3.geo_to_h3(37, -122, res)) + assert h3.is_valid_cell(h3.latlng_to_cell(37, -122, res)) -def test_geo_to_h3(): - assert h3.geo_to_h3(37.3615593, -122.0553238, 5) == '85283473fffffff' +def test_latlng_to_cell(): + assert h3.latlng_to_cell(37.3615593, -122.0553238, 5) == '85283473fffffff' -def test_h3_get_resolution(): +def test_get_resolution(): for res in range(16): - h = h3.geo_to_h3(37.3615593, -122.0553238, res) - assert h3.h3_get_resolution(h) == res + h = h3.latlng_to_cell(37.3615593, -122.0553238, res) + assert h3.get_resolution(h) == res -def test_silly_geo_to_h3(): +def test_silly_latlng_to_cell(): lat, lng = 37.3615593, -122.0553238 expected0 = '85283473fffffff' - out0 = h3.geo_to_h3(lat, lng, 5) + out0 = h3.latlng_to_cell(lat, lng, 5) assert out0 == expected0 - out1 = h3.geo_to_h3(lat + 180.0, lng + 360.0, 5) + out1 = h3.latlng_to_cell(lat + 180.0, lng + 360.0, 5) expected1 = '85ca2d53fffffff' assert out1 == expected1 -def test_h3_to_geo(): - latlng = h3.h3_to_geo('85283473fffffff') +def test_cell_to_latlng(): + latlng = h3.cell_to_latlng('85283473fffffff') assert latlng == approx((37.34579337536848, -121.97637597255124)) -def test_h3_to_geo_boundary(): - out = h3.h3_to_geo_boundary('85283473fffffff') +def test_cell_to_boundary(): + out = h3.cell_to_boundary('85283473fffffff') expected = [ [37.271355866731895, -121.91508032705622], @@ -67,8 +67,8 @@ def test_h3_to_geo_boundary(): assert o == approx(e) -def test_h3_to_geo_boundary_geo_json(): - out = h3.h3_to_geo_boundary('85283473fffffff', True) +def test_cell_to_boundary_geo_json(): + out = h3.cell_to_boundary('85283473fffffff', True) expected = [ [-121.91508032705622, 37.271355866731895], @@ -86,9 +86,9 @@ def test_h3_to_geo_boundary_geo_json(): assert o == approx(e) -def test_k_ring(): +def test_grid_disk(): h = '8928308280fffff' - out = h3.k_ring(h, 1) + out = h3.grid_disk(h, 1) assert len(out) == 1 + 6 @@ -105,9 +105,9 @@ def test_k_ring(): assert out == expected -def test_k_ring2(): +def test_grid_disk2(): h = '8928308280fffff' - out = h3.k_ring(h, 2) + out = h3.grid_disk(h, 2) assert len(out) == 1 + 6 + 12 @@ -136,9 +136,9 @@ def test_k_ring2(): assert out == expected -def test_k_ring_pentagon(): +def test_grid_disk_pentagon(): h = '821c07fffffffff' # a pentagon cell - out = h3.k_ring(h, 1) + out = h3.grid_disk(h, 1) assert len(out) == 1 + 5 @@ -349,18 +349,18 @@ def test_polyfill_null_island(): assert len(out) > 10 -def test_h3_set_to_multi_polygon_empty(): - out = h3.h3_set_to_multi_polygon([]) +def test_cells_to_multi_polygon_empty(): + out = h3.cells_to_multi_polygon([]) assert out == [] -def test_h3_set_to_multi_polygon_single(): +def test_cells_to_multi_polygon_single(): h = '89283082837ffff' hexes = {h} # multi_polygon - mp = h3.h3_set_to_multi_polygon(hexes) - vertices = h3.h3_to_geo_boundary(h) + mp = h3.cells_to_multi_polygon(hexes) + vertices = h3.cell_to_boundary(h) # We shift the expected circular list so that it starts from # multi_polygon[0][0][0], since output starting from any vertex @@ -382,10 +382,10 @@ def test_h3_set_to_multi_polygon_single(): assert mp == expected -def test_h3_set_to_multi_polygon_single_geo_json(): +def test_cells_to_multi_polygon_single_geo_json(): hexes = ['89283082837ffff'] - mp = h3.h3_set_to_multi_polygon(hexes, True) - vertices = h3.h3_to_geo_boundary(hexes[0], True) + mp = h3.cells_to_multi_polygon(hexes, True) + vertices = h3.cell_to_boundary(hexes[0], True) # We shift the expected circular list so that it starts from # multi_polygon[0][0][0], since output starting from any vertex @@ -426,14 +426,14 @@ def test_h3_set_to_multi_polygon_single_geo_json(): assert mp == expected -def test_h3_set_to_multi_polygon_contiguous(): +def test_cells_to_multi_polygon_contiguous(): # the second hexagon shares v0 and v1 with the first hexes = ['89283082837ffff', '89283082833ffff'] # multi_polygon - mp = h3.h3_set_to_multi_polygon(hexes) - vertices0 = h3.h3_to_geo_boundary(hexes[0]) - vertices1 = h3.h3_to_geo_boundary(hexes[1]) + mp = h3.cells_to_multi_polygon(hexes) + vertices0 = h3.cell_to_boundary(hexes[0]) + vertices1 = h3.cell_to_boundary(hexes[1]) # We shift the expected circular list so that it starts from # multi_polygon[0][0][0], since output starting from any vertex @@ -463,11 +463,11 @@ def test_h3_set_to_multi_polygon_contiguous(): assert mp == expected -def test_h3_set_to_multi_polygon_non_contiguous(): +def test_cells_to_multi_polygon_non_contiguous(): # the second hexagon does not touch the first hexes = {'89283082837ffff', '8928308280fffff'} # multi_polygon - mp = h3.h3_set_to_multi_polygon(hexes) + mp = h3.cells_to_multi_polygon(hexes) assert len(mp) == 2 # polygon count matches expected assert len(mp[0]) == 1 # loop count matches expected @@ -475,13 +475,13 @@ def test_h3_set_to_multi_polygon_non_contiguous(): assert len(mp[1][0]) == 6 # coord count 2 matches expected -def test_h3_set_to_multi_polygon_hole(): +def test_cells_to_multi_polygon_hole(): # Six hexagons in a ring around a hole hexes = [ '892830828c7ffff', '892830828d7ffff', '8928308289bffff', '89283082813ffff', '8928308288fffff', '89283082883ffff', ] - mp = h3.h3_set_to_multi_polygon(hexes) + mp = h3.cells_to_multi_polygon(hexes) assert len(mp) == 1 # polygon count matches expected assert len(mp[0]) == 2 # loop count matches expected @@ -489,11 +489,11 @@ def test_h3_set_to_multi_polygon_hole(): assert len(mp[0][1]) == 6 # inner coord count matches expected -def test_h3_set_to_multi_polygon_2k_ring(): +def test_cells_to_multi_polygon_2grid_disk(): h = '8930062838bffff' - hexes = h3.k_ring(h, 2) + hexes = h3.grid_disk(h, 2) # multi_polygon - mp = h3.h3_set_to_multi_polygon(hexes) + mp = h3.cells_to_multi_polygon(hexes) assert len(mp) == 1 # polygon count matches expected assert len(mp[0]) == 1 # loop count matches expected @@ -509,22 +509,22 @@ def test_h3_set_to_multi_polygon_2k_ring(): '893006283c7ffff' } - mp2 = h3.h3_set_to_multi_polygon(hexes2) + mp2 = h3.cells_to_multi_polygon(hexes2) assert len(mp2) == 1 # polygon count matches expected assert len(mp2[0]) == 1 # loop count matches expected assert len(mp2[0][0]) == 6 * (2 * 2 + 1) # coord count matches expected - hexes3 = list(h3.k_ring(h, 6)) + hexes3 = list(h3.grid_disk(h, 6)) hexes3.sort() - mp3 = h3.h3_set_to_multi_polygon(hexes3) + mp3 = h3.cells_to_multi_polygon(hexes3) assert len(mp3[0]) == 1 # loop count matches expected -def test_hex_ring(): +def test_grid_ring(): h = '8928308280fffff' - out = h3.hex_ring(h, 1) + out = h3.grid_ring(h, 1) expected = { '8928308280bffff', '89283082807ffff', @@ -535,12 +535,12 @@ def test_hex_ring(): } assert out == expected - assert out == h3.k_ring(h, 1) - h3.k_ring(h, 0) + assert out == h3.grid_disk(h, 1) - h3.grid_disk(h, 0) -def test_hex_ring2(): +def test_grid_ring2(): h = '8928308280fffff' - out = h3.hex_ring(h, 2) + out = h3.grid_ring(h, 2) expected = { '89283082813ffff', @@ -558,12 +558,12 @@ def test_hex_ring2(): } assert out == expected - assert out == h3.k_ring(h, 2) - h3.k_ring(h, 1) + assert out == h3.grid_disk(h, 2) - h3.grid_disk(h, 1) -def test_hex_ring_pentagon(): +def test_grid_ring_pentagon(): h = '821c07fffffffff' - out = h3.hex_ring(h, 1) + out = h3.grid_ring(h, 1) expected = { '821c17fffffffff', @@ -576,7 +576,7 @@ def test_hex_ring_pentagon(): assert out == expected -def test_compact_and_uncompact(): +def test_compact_and_uncompact_cells(): geo = { 'type': 'Polygon', 'coordinates': [ @@ -593,264 +593,157 @@ def test_compact_and_uncompact(): hexes = h3.polyfill(geo, 9) - compact_hexes = h3.compact(hexes) - assert len(compact_hexes) == 209 + compact_cells = h3.compact_cells(hexes) + assert len(compact_cells) == 209 - uncompact_hexes = h3.uncompact(compact_hexes, 9) - assert len(uncompact_hexes) == 1253 + uncompact_cells = h3.uncompact_cells(compact_cells, 9) + assert len(uncompact_cells) == 1253 -def test_compact_and_uncompact_nothing(): - assert h3.compact([]) == set() - assert h3.uncompact([], 9) == set() +def test_compact_cells_and_uncompact_cells_nothing(): + assert h3.compact_cells([]) == set() + assert h3.uncompact_cells([], 9) == set() -def test_uncompact_error(): - hexagons = [h3.geo_to_h3(37, -122, 10)] +def test_uncompact_cells_error(): + hexagons = [h3.latlng_to_cell(37, -122, 10)] with pytest.raises(Exception): - h3.uncompact(hexagons, 5) + h3.uncompact_cells(hexagons, 5) -def test_compact_malformed_input(): +def test_compact_cells_malformed_input(): hexes = ['89283082813ffff'] * 13 with pytest.raises(Exception): - h3.compact(hexes) + h3.compact_cells(hexes) -def test_h3_to_parent(): +def test_cell_to_parent(): h = '89283082813ffff' - assert h3.h3_to_parent(h, 8) == '8828308281fffff' + assert h3.cell_to_parent(h, 8) == '8828308281fffff' -def test_h3_to_children(): +def test_cell_to_children(): h = '8828308281fffff' - children = h3.h3_to_children(h, 9) + children = h3.cell_to_children(h, 9) assert len(children) == 7 -def test_hex_range_distances_pentagon(): - - h = '821c07fffffffff' - out = h3.hex_range_distances(h, 1) - - expected = [ - {h}, - { - '821c17fffffffff', - '821c1ffffffffff', - '821c27fffffffff', - '821c2ffffffffff', - '821c37fffffffff', - } - ] - - assert out == expected - - -def test_hex_range_distances(): - h = '8928308280fffff' - - # should consist of `h` and it's 5 neighbors - out = h3.hex_range_distances(h, 1) - - assert [len(x) for x in out] == [1, 6] - - expected = [ - {h}, - { - '8928308280bffff', - '89283082807ffff', - '89283082877ffff', - '89283082803ffff', - '89283082873ffff', - '8928308283bffff', - } - ] - - assert out == expected - - out = h3.hex_range_distances('870800003ffffff', 2) - - assert [len(x) for x in out] == [1, 6, 11] - - -def test_hex_ranges(): - h = '8928308280fffff' - out = h3.hex_ranges([h], 1) - - assert set(out.keys()) == {h} - - expected = [ - {h}, - { - '8928308280bffff', - '89283082807ffff', - '89283082877ffff', - '89283082803ffff', - '89283082873ffff', - '8928308283bffff', - } - ] - - assert out[h] == expected - - -def test_hex_ranges_pentagon(): - h = '821c07fffffffff' - out = h3.hex_ranges([h], 1) - - expected = { - h: [ - {h}, - { - '821c17fffffffff', - '821c1ffffffffff', - '821c27fffffffff', - '821c2ffffffffff', - '821c37fffffffff' - } - ] - } - - assert out == expected - - -def test_many_hex_ranges(): - hexes = h3.k_ring('8928308280fffff', 2) - out = h3.hex_ranges(hexes, 2) - - assert len(out) == 19 - - hexes = out['8928308280fffff'] - assert [len(x) for x in hexes] == [1, 6, 12] - - -def test_many_hex_ranges2(): - hexes = h3.k_ring('8928308280fffff', 5) - out = h3.hex_ranges(hexes, 5) - assert len(out) == 91 - - hexes = out['8928308280fffff'] - - assert [len(x) for x in hexes] == [1, 6, 12, 18, 24, 30] - - -def test_hex_area(): +def test_average_hexagon_area(): for i in range(0, 15): - assert isinstance(h3.hex_area(i), float) - assert isinstance(h3.hex_area(i, 'm^2'), float) + assert isinstance(h3.average_hexagon_area(i), float) + assert isinstance(h3.average_hexagon_area(i, 'm^2'), float) with pytest.raises(ValueError): - h3.hex_area(5, 'ft^2') + h3.average_hexagon_area(5, 'ft^2') -def test_edge_length(): +def test_average_hexagon_edge_length(): for i in range(0, 15): - assert isinstance(h3.edge_length(i), float) - assert isinstance(h3.edge_length(i, 'm'), float) + assert isinstance(h3.average_hexagon_edge_length(i), float) + assert isinstance(h3.average_hexagon_edge_length(i, 'm'), float) with pytest.raises(ValueError): - h3.edge_length(5, 'ft') + h3.average_hexagon_edge_length(5, 'ft') -def test_num_hexagons(): +def test_get_num_cells(): h0 = 122 - assert h3.num_hexagons(0) == h0 + assert h3.get_num_cells(0) == h0 for i in range(0, 15): - n = h3.num_hexagons(i) * 1.0 / h0 + n = h3.get_num_cells(i) * 1.0 / h0 assert 6**i <= n <= 7**i -def test_h3_get_base_cell(): - assert h3.h3_get_base_cell('8928308280fffff') == 20 +def test_get_base_cell_number(): + assert h3.get_base_cell_number('8928308280fffff') == 20 -def test_h3_is_res_class_III(): - assert h3.h3_is_res_class_III('8928308280fffff') - assert not h3.h3_is_res_class_III('8828308280fffff') +def test_is_res_class_III(): + assert h3.is_res_class_III('8928308280fffff') + assert not h3.is_res_class_III('8828308280fffff') -def test_h3_is_pentagon(): - assert h3.h3_is_pentagon('821c07fffffffff') - assert not h3.h3_is_pentagon('8928308280fffff') +def test_is_pentagon(): + assert h3.is_pentagon('821c07fffffffff') + assert not h3.is_pentagon('8928308280fffff') -def test_h3_indexes_are_neighbors(): - assert h3.h3_indexes_are_neighbors('8928308280fffff', '8928308280bffff') +def test_are_neighbor_cells(): + assert h3.are_neighbor_cells('8928308280fffff', '8928308280bffff') - assert not h3.h3_indexes_are_neighbors('821c07fffffffff', '8928308280fffff') + assert not h3.are_neighbor_cells('821c07fffffffff', '8928308280fffff') -def test_get_h3_unidirectional_edge(): - out = h3.get_h3_unidirectional_edge('8928308280fffff', '8928308280bffff') - assert h3.h3_unidirectional_edge_is_valid(out) +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.get_h3_unidirectional_edge('821c07fffffffff', '8928308280fffff') + h3.cells_to_directed_edge('821c07fffffffff', '8928308280fffff') -def test_h3_unidirectional_edge_is_valid(): - assert not h3.h3_unidirectional_edge_is_valid('8928308280fffff') - assert h3.h3_unidirectional_edge_is_valid('11928308280fffff') +def test_is_valid_directed_edge(): + assert not h3.is_valid_directed_edge('8928308280fffff') + assert h3.is_valid_directed_edge('11928308280fffff') -def test_get_origin_h3_index_from_unidirectional_edge(): - out = h3.get_origin_h3_index_from_unidirectional_edge('11928308280fffff') +def test_get_directed_edge_origin(): + out = h3.get_directed_edge_origin('11928308280fffff') assert out == '8928308280fffff' -def test_get_destination_h3_index_from_unidirectional_edge(): +def test_get_directed_edge_destination(): h = '11928308280fffff' - out = h3.get_destination_h3_index_from_unidirectional_edge(h) + out = h3.get_directed_edge_destination(h) assert out == '8928308283bffff' -def test_get_h3_indexes_from_unidirectional_edge(): - e = h3.get_h3_indexes_from_unidirectional_edge('11928308280fffff') +def test_directed_edge_to_cells(): + e = h3.directed_edge_to_cells('11928308280fffff') assert e == ('8928308280fffff', '8928308283bffff') -def test_get_h3_unidirectional_edges_from_hexagon(): - h3_uni_edges = h3.get_h3_unidirectional_edges_from_hexagon( +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.get_h3_unidirectional_edges_from_hexagon( + h3_uni_edge_pentagon = h3.origin_to_directed_edges( '821c07fffffffff' ) assert len(h3_uni_edge_pentagon) == 5 -def test_get_h3_unidirectional_edge_boundary(): +def test_directed_edge_to_boundary(): e = '11928308280fffff' - boundary = h3.get_h3_unidirectional_edge_boundary(e) + boundary = h3.directed_edge_to_boundary(e) assert len(boundary) == 2 - boundary_geo_json = h3.get_h3_unidirectional_edge_boundary(e, True) + boundary_geo_json = h3.directed_edge_to_boundary(e, True) assert len(boundary_geo_json) == 3 -def test_h3_distance(): +def test_grid_distance(): h = '89283082993ffff' - assert 0 == h3.h3_distance(h, h) - assert 1 == h3.h3_distance(h, '8928308299bffff') - assert 5 == h3.h3_distance(h, '89283082827ffff') + assert 0 == h3.grid_distance(h, h) + assert 1 == h3.grid_distance(h, '8928308299bffff') + assert 5 == h3.grid_distance(h, '89283082827ffff') -def test_h3_line(): +def test_grid_path_cells(): h1 = '8a2a84730587fff' h2 = '8a2a8471414ffff' - out = h3.h3_line(h1, h2) + out = h3.grid_path_cells(h1, h2) expected = [ h1, @@ -889,4 +782,4 @@ def test_h3_line(): assert out == expected with pytest.raises(h3.H3ResMismatchError): - h3.h3_line(h1, '8001fffffffffff') + h3.grid_path_cells(h1, '8001fffffffffff') diff --git a/tests/test_length_area.py b/tests/test_length_area.py index 8c15f1362..8851a4b47 100644 --- a/tests/test_length_area.py +++ b/tests/test_length_area.py @@ -13,10 +13,10 @@ def approx2(a, b): def cell_perimiter1(h, unit='km'): - edges = h3.get_h3_unidirectional_edges_from_hexagon(h) + edges = h3.origin_to_directed_edges(h) dists = [ - h3.exact_edge_length(e, unit=unit) + h3.edge_length(e, unit=unit) for e in edges ] @@ -26,12 +26,12 @@ def cell_perimiter1(h, unit='km'): def cell_perimiter2(h, unit='km'): - verts = h3.h3_to_geo_boundary(h) + verts = h3.cell_to_boundary(h) N = len(verts) verts += (verts[0],) dists = [ - h3.point_dist(verts[i], verts[i + 1], unit=unit) + h3.great_circle_distance(verts[i], verts[i + 1], unit=unit) for i in range(N) ] @@ -61,7 +61,7 @@ def test_areas_at_00(): ] out = [ - h3.cell_area(h3.geo_to_h3(0, 0, r), unit='km^2') + h3.cell_area(h3.latlng_to_cell(0, 0, r), unit='km^2') for r in range(16) ] @@ -87,7 +87,7 @@ def test_areas_at_00(): ] out = [ - h3.cell_area(h3.geo_to_h3(0, 0, r), unit='rads^2') + h3.cell_area(h3.latlng_to_cell(0, 0, r), unit='rads^2') for r in range(16) ] @@ -99,40 +99,41 @@ def test_bad_units(): e = '139754e64993ffff' assert h3.is_valid_cell(h) - assert h3.h3_unidirectional_edge_is_valid(e) + assert h3.is_valid_directed_edge(e) with pytest.raises(ValueError): h3.cell_area(h, unit='foot-pounds') with pytest.raises(ValueError): - h3.exact_edge_length(e, unit='foot-pounds') + h3.edge_length(e, unit='foot-pounds') with pytest.raises(ValueError): - h3.point_dist((0, 0), (0, 0), unit='foot-pounds') + h3.great_circle_distance((0, 0), (0, 0), unit='foot-pounds') -def test_point_dist(): +def test_great_circle_distance(): lyon = (45.7597, 4.8422) # (lat, lon) paris = (48.8567, 2.3508) - d = h3.point_dist(lyon, paris, unit='rads') + d = h3.great_circle_distance(lyon, paris, unit='rads') assert d == pytest.approx(0.0615628186794217) - d = h3.point_dist(lyon, paris, unit='m') + d = h3.great_circle_distance(lyon, paris, unit='m') assert d == pytest.approx(392217.1598841777) - d = h3.point_dist(lyon, paris, unit='km') + d = h3.great_circle_distance(lyon, paris, unit='km') assert d == pytest.approx(392.21715988417765) # test that 'km' is the default unit - assert h3.point_dist(lyon, paris, unit='km') == h3.point_dist(lyon, paris) + 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(h3.get_res0_indexes(), r) + cells = h3.uncompact_cells(h3.get_res0_cells(), r) for h in cells: for unit in ['rads', 'm', 'km']: v1 = cell_perimiter1(h, unit) diff --git a/tests/test_memview_int.py b/tests/test_memview_int.py index 9923b7509..3cd2c4501 100644 --- a/tests/test_memview_int.py +++ b/tests/test_memview_int.py @@ -2,15 +2,16 @@ def test1(): - assert h3.geo_to_h3(37.7752702151959, -122.418307270836, 9) == 617700169958293503 + 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.string_to_h3(h1), h3.string_to_h3(h2) + h1, h2 = h3.string_to_int(h1), h3.string_to_int(h2) - out = h3.h3_line(h1, 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)) @@ -24,18 +25,18 @@ def test_line(): assert list(out) == expected -def test_get_faces(): +def test_get_icosahedron_faces(): h = 577832942814887935 expected = {2, 3, 7, 8, 12} - out = h3.h3_get_faces(h) + out = h3.get_icosahedron_faces(h) assert out == expected h = 579873636396040191 expected = {13} - out = h3.h3_get_faces(h) + out = h3.get_icosahedron_faces(h) assert out == expected h = 579768083279773695 expected = {16, 15} - out = h3.h3_get_faces(h) + out = h3.get_icosahedron_faces(h) assert out == expected diff --git a/tests/test_numpy_int.py b/tests/test_numpy_int.py index 8169772cf..5ebcd44ac 100644 --- a/tests/test_numpy_int.py +++ b/tests/test_numpy_int.py @@ -3,7 +3,8 @@ def test1(): - assert h3.geo_to_h3(37.7752702151959, -122.418307270836, 9) == 617700169958293503 + lat, lng = 37.7752702151959, -122.418307270836, + assert h3.latlng_to_cell(lat, lng, 9) == 617700169958293503 def test5(): @@ -17,31 +18,31 @@ def test5(): 617700169965109247, } - out = h3.k_ring(617700169958293503, 1) + out = h3.grid_disk(617700169958293503, 1) assert isinstance(out, np.ndarray) assert set(out) == expected -def test_compact(): +def test_compact_cells(): h = 617700169958293503 - hexes = h3.h3_to_children(h) + hexes = h3.cell_to_children(h) assert isinstance(hexes, np.ndarray) - assert set(h3.compact(hexes)) == {h} + assert set(h3.compact_cells(hexes)) == {h} -def test_get_faces(): +def test_get_icosahedron_faces(): h = 577832942814887935 expected = {2, 3, 7, 8, 12} - out = h3.h3_get_faces(h) + out = h3.get_icosahedron_faces(h) assert out == expected h = 579873636396040191 expected = {13} - out = h3.h3_get_faces(h) + out = h3.get_icosahedron_faces(h) assert out == expected h = 579768083279773695 expected = {16, 15} - out = h3.h3_get_faces(h) + out = h3.get_icosahedron_faces(h) assert out == expected diff --git a/tests/test_to_multipoly.py b/tests/test_to_multipoly.py index 0b899b935..3de1360de 100644 --- a/tests/test_to_multipoly.py +++ b/tests/test_to_multipoly.py @@ -1,11 +1,11 @@ import h3 -def test_h3_set_to_multi_polygon(): +def test_cells_to_multi_polygon(): h = '8928308280fffff' - hexes = h3.k_ring(h, 1) + hexes = h3.grid_disk(h, 1) - mpoly = h3.h3_set_to_multi_polygon(hexes) + mpoly = h3.cells_to_multi_polygon(hexes) out = h3.polyfill_polygon(mpoly[0][0], 9, holes=None, lnglat_order=False) @@ -14,14 +14,14 @@ def test_h3_set_to_multi_polygon(): def test_2_polys(): h = '8928308280fffff' - hexes = h3.hex_ring(h, 2) + hexes = h3.grid_ring(h, 2) hexes = hexes | {h} # hexes should be a center hex, and the 2-ring around it # (with the 1-ring being absent) out = [ h3.polyfill_polygon(poly[0], 9, holes=poly[1:], lnglat_order=False) - for poly in h3.h3_set_to_multi_polygon(hexes, geo_json=False) + for poly in h3.cells_to_multi_polygon(hexes, geo_json=False) ] assert set.union(*out) == hexes @@ -29,13 +29,13 @@ def test_2_polys(): def test_2_polys_json(): h = '8928308280fffff' - hexes = h3.hex_ring(h, 2) + hexes = h3.grid_ring(h, 2) hexes = hexes | {h} # hexes should be a center hex, and the 2-ring around it # (with the 1-ring being absent) # not deterministic which poly is first.. - poly1, poly2 = h3.h3_set_to_multi_polygon(hexes, geo_json=True) + poly1, poly2 = h3.cells_to_multi_polygon(hexes, geo_json=True) assert {len(poly1), len(poly2)} == {1, 2} @@ -46,13 +46,13 @@ def test_2_polys_json(): def test_2_polys_not_json(): h = '8928308280fffff' - hexes = h3.hex_ring(h, 2) + hexes = h3.grid_ring(h, 2) hexes = hexes | {h} # hexes should be a center hex, and the 2-ring around it # (with the 1-ring being absent) # not deterministic which poly is first.. - poly1, poly2 = h3.h3_set_to_multi_polygon(hexes, geo_json=False) + poly1, poly2 = h3.cells_to_multi_polygon(hexes, geo_json=False) assert {len(poly1), len(poly2)} == {1, 2} From cb74f58c5e75c560fe93c631eaa10e535cb0a0a7 Mon Sep 17 00:00:00 2001 From: David Ellis Date: Thu, 18 Aug 2022 13:06:10 -0500 Subject: [PATCH 19/21] Get the makefile working on Linux (#274) --- makefile | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/makefile b/makefile index 44eceb807..cf7186030 100644 --- a/makefile +++ b/makefile @@ -1,21 +1,23 @@ .PHONY: init clear rebuild purge test lint lab ipython +PYTHON=$(shell command -v python || command -v python3) + build-docs: - env/bin/jupyter-book build docs/ --warningiserror --keep-going --all + ./env/bin/jupyter-book build docs/ --warningiserror --keep-going --all open: open docs/_build/html/index.html init: purge git submodule update --init - python -m venv env - env/bin/pip install --upgrade pip wheel setuptools - env/bin/pip install .[all] - env/bin/pip install -r requirements.in + $(PYTHON) -m venv env + ./env/bin/pip install --upgrade pip wheel setuptools + ./env/bin/pip install .[all] + ./env/bin/pip install -r requirements.in clear: - -env/bin/pip uninstall -y h3 + -./env/bin/pip uninstall -y h3 -@rm -rf MANIFEST -@rm -rf annotations -@rm -rf .pytest_cache tests/__pycache__ __pycache__ _skbuild dist .coverage @@ -25,7 +27,7 @@ clear: -@find ./tests -type f -name '*.c' | xargs rm -r rebuild: clear - env/bin/pip install .[all] + ./env/bin/pip install .[all] purge: clear -@rm -rf env @@ -35,13 +37,13 @@ annotations: rebuild cp _skbuild/*/cmake-build/src/h3/_cy/*.html ./annotations test: - env/bin/cythonize -i tests/cython_example.pyx - env/bin/pytest tests --cov=h3 --cov-report term-missing --durations=10 + ./env/bin/cythonize -i tests/cython_example.pyx + ./env/bin/pytest tests --cov=h3 --cov-report term-missing --durations=10 lint: - env/bin/flake8 src/h3 setup.py tests - env/bin/pylint --disable=all --enable=import-error tests/ + ./env/bin/flake8 src/h3 setup.py tests + ./env/bin/pylint --disable=all --enable=import-error tests/ lab: - env/bin/pip install jupyterlab - env/bin/jupyter lab + ./env/bin/pip install jupyterlab + ./env/bin/jupyter lab From 6219742c4b7ead50111114f254f42f3dfd69befc Mon Sep 17 00:00:00 2001 From: AJ Friend Date: Sat, 20 Aug 2022 18:54:25 -0400 Subject: [PATCH 20/21] bump core lib to 4.0 rc5 (#275) * bump core lib to 4.0 rc5 * more pythonic names for string<->int conversion functions * cython hex2int to str_to_int to match the python functions * better argument names for great_circle_distance * great_circle_distance comments * revert great_circle_distance tuple unpacking since it breaks python2. drop python 2 before release --- CHANGELOG.md | 2 +- docs/api_comparison.md | 4 ++-- docs/api_reference.md | 4 ++-- src/h3/_cy/__init__.py | 4 ++-- src/h3/_cy/edges.pyx | 6 +++--- src/h3/_cy/error_system.pyx | 4 ++-- src/h3/_cy/geo.pyx | 6 +++--- src/h3/_cy/h3lib.pxd | 14 +++++++------- src/h3/_cy/util.pxd | 4 ++-- src/h3/_cy/util.pyx | 6 +++--- src/h3/api/_api_template.py | 28 ++++++++++++---------------- src/h3/api/basic_int/_public_api.py | 4 ++-- src/h3/api/basic_str/_binding.py | 10 +++++----- src/h3lib | 2 +- tests/test_cells_and_edges.py | 8 ++++---- tests/test_memview_int.py | 2 +- 16 files changed, 52 insertions(+), 56 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7bf87cf87..cbab906aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -65,7 +65,7 @@ avoid adding features or APIs which do not map onto the ## [3.6.2] - 2020-06-02 -- Improve error reporting on `hex2int` (https://github.com/uber/h3-py/pull/127) +- 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 diff --git a/docs/api_comparison.md b/docs/api_comparison.md index 2378702c5..e3ce64ba9 100644 --- a/docs/api_comparison.md +++ b/docs/api_comparison.md @@ -37,7 +37,7 @@ 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_string` (links!) and `string_to_int`. +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` @@ -236,7 +236,7 @@ def compute(h3_lib, N=100): def compute_and_convert(h3_lib, N=100): out = compute(h3_lib, N) - out = [h3.int_to_string(h) for h in out] + out = [h3.int_to_str(h) for h in out] return out ``` diff --git a/docs/api_reference.md b/docs/api_reference.md index efdc1152f..c55099ab9 100644 --- a/docs/api_reference.md +++ b/docs/api_reference.md @@ -34,8 +34,8 @@ but we'll try to group functions in a reasonably logical manner. .. autosummary:: latlng_to_cell cell_to_latlng - int_to_string - string_to_int + int_to_str + str_to_int get_res0_cells get_pentagons get_num_cells diff --git a/src/h3/_cy/__init__.py b/src/h3/_cy/__init__.py index e341b3a96..d57519a86 100644 --- a/src/h3/_cy/__init__.py +++ b/src/h3/_cy/__init__.py @@ -67,8 +67,8 @@ from .util import ( c_version, - hex2int, - int2hex, + str_to_int, + int_to_str, ) from .memory import ( diff --git a/src/h3/_cy/edges.pyx b/src/h3/_cy/edges.pyx index 3503e7582..bd0aa7c00 100644 --- a/src/h3/_cy/edges.pyx +++ b/src/h3/_cy/edges.pyx @@ -101,11 +101,11 @@ cpdef double edge_length(H3int e, unit='km') except -1: double length if unit == 'rads': - err = h3lib.exactEdgeLengthRads(e, &length) + err = h3lib.edgeLengthRads(e, &length) elif unit == 'km': - err = h3lib.exactEdgeLengthKm(e, &length) + err = h3lib.edgeLengthKm(e, &length) elif unit == 'm': - err = h3lib.exactEdgeLengthM(e, &length) + err = h3lib.edgeLengthM(e, &length) else: raise ValueError('Unknown unit: {}'.format(unit)) diff --git a/src/h3/_cy/error_system.pyx b/src/h3/_cy/error_system.pyx index 799e4f220..fbdd91854 100644 --- a/src/h3/_cy/error_system.pyx +++ b/src/h3/_cy/error_system.pyx @@ -84,7 +84,7 @@ from .h3lib cimport ( E_DUPLICATE_INPUT, E_NOT_NEIGHBORS, E_RES_MISMATCH, - E_MEMORY, + E_MEMORY_ALLOC, E_MEMORY_BOUNDS, E_OPTION_INVALID, ) @@ -189,7 +189,7 @@ error_mapping = { E_DUPLICATE_INPUT: H3DuplicateInputError, E_NOT_NEIGHBORS: H3NotNeighborsError, E_RES_MISMATCH: H3ResMismatchError, - E_MEMORY: H3MemoryAllocError, + E_MEMORY_ALLOC: H3MemoryAllocError, E_MEMORY_BOUNDS: H3MemoryBoundsError, E_OPTION_INVALID: H3OptionInvalidError, } diff --git a/src/h3/_cy/geo.pyx b/src/h3/_cy/geo.pyx index 001aca29c..c2579525e 100644 --- a/src/h3/_cy/geo.pyx +++ b/src/h3/_cy/geo.pyx @@ -291,11 +291,11 @@ cpdef double great_circle_distance( b = deg2coord(lat2, lng2) if unit == 'rads': - d = h3lib.distanceRads(&a, &b) + d = h3lib.greatCircleDistanceRads(&a, &b) elif unit == 'km': - d = h3lib.distanceKm(&a, &b) + d = h3lib.greatCircleDistanceKm(&a, &b) elif unit == 'm': - d = h3lib.distanceM(&a, &b) + d = h3lib.greatCircleDistanceM(&a, &b) else: raise ValueError('Unknown unit: {}'.format(unit)) diff --git a/src/h3/_cy/h3lib.pxd b/src/h3/_cy/h3lib.pxd index 150b549bf..92b68ce8b 100644 --- a/src/h3/_cy/h3lib.pxd +++ b/src/h3/_cy/h3lib.pxd @@ -25,7 +25,7 @@ cdef extern from 'h3api.h': E_DUPLICATE_INPUT = 10 E_NOT_NEIGHBORS = 11 E_RES_MISMATCH = 12 - E_MEMORY = 13 + E_MEMORY_ALLOC = 13 E_MEMORY_BOUNDS = 14 E_OPTION_INVALID = 15 @@ -143,16 +143,16 @@ cdef extern from 'h3api.h': H3Error getHexagonEdgeLengthAvgKm(int res, double *out) nogil H3Error getHexagonEdgeLengthAvgM(int res, double *out) nogil - H3Error exactEdgeLengthRads(H3int edge, double *out) nogil - H3Error exactEdgeLengthKm(H3int edge, double *out) nogil - H3Error exactEdgeLengthM(H3int edge, 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 distanceRads(const LatLng *a, const LatLng *b) nogil - double distanceKm(const LatLng *a, const LatLng *b) nogil - double distanceM(const LatLng *a, const LatLng *b) 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 cellsToLinkedMultiPolygon(const H3int *h3Set, const int numHexes, LinkedGeoPolygon *out) void destroyLinkedMultiPolygon(LinkedGeoPolygon *polygon) diff --git a/src/h3/_cy/util.pxd b/src/h3/_cy/util.pxd index 750316bea..15e97ad07 100644 --- a/src/h3/_cy/util.pxd +++ b/src/h3/_cy/util.pxd @@ -3,8 +3,8 @@ from .h3lib cimport H3int, H3str, LatLng cdef LatLng deg2coord(double lat, double lng) nogil cdef (double, double) coord2deg(LatLng c) nogil -cpdef H3int hex2int(H3str h) except? 0 -cpdef H3str int2hex(H3int x) +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) diff --git a/src/h3/_cy/util.pyx b/src/h3/_cy/util.pyx index 16c6df431..8bc061db5 100644 --- a/src/h3/_cy/util.pyx +++ b/src/h3/_cy/util.pyx @@ -36,11 +36,11 @@ cpdef basestring c_version(): return '{}.{}.{}'.format(*v) -cpdef H3int hex2int(H3str h) except? 0: +cpdef H3int str_to_int(H3str h) except? 0: return int(h, 16) -cpdef H3str int2hex(H3int x): +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 @@ -61,7 +61,7 @@ cdef check_cell(H3int h): we want the error message to be informative to the user in either case. - We use the builtin `hex` function instead of `int2hex` to + 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. diff --git a/src/h3/api/_api_template.py b/src/h3/api/_api_template.py index bc837bcf7..70ac0515d 100644 --- a/src/h3/api/_api_template.py +++ b/src/h3/api/_api_template.py @@ -78,7 +78,7 @@ def versions(): return v @staticmethod - def string_to_int(h): + def str_to_int(h): """ Converts a hexadecimal string to an H3 64-bit integer index. @@ -92,10 +92,10 @@ def string_to_int(h): int Unsigned 64-bit integer """ - return _cy.hex2int(h) + return _cy.str_to_int(h) @staticmethod - def int_to_string(x): + def int_to_str(x): """ Converts an H3 64-bit integer index to a hexadecimal string. @@ -109,7 +109,7 @@ def int_to_string(x): str Hexadecimal string like ``'89754e64993ffff'`` """ - return _cy.int2hex(x) + return _cy.int_to_str(x) @staticmethod def get_num_cells(resolution): @@ -887,21 +887,18 @@ def edge_length(self, e, unit='km'): return _cy.edge_length(e, unit=unit) @staticmethod - def great_circle_distance(point1, point2, unit='km'): + def great_circle_distance(latlng1, latlng2, unit='km'): """ Compute the spherical distance between two (lat, lng) points. - - todo: do we handle lat/lng points consistently in the api? what - about (lat1, lng1, lat2, lng2) as the input? How will this work - for vectorized versions? + AKA: great circle distance or "haversine" distance. todo: overload to allow two cell inputs? Parameters ---------- - point1 : tuple + latlng1 : tuple (lat, lng) tuple in degrees - point2 : tuple + latlng2 : tuple (lat, lng) tuple in degrees unit: str Unit for distance result ('km', 'm', or 'rads') @@ -909,13 +906,12 @@ def great_circle_distance(point1, point2, unit='km'): Returns ------- - Spherical (or "haversine") distance between the points + The spherical distance between the points in the given units """ - lat1, lng1 = point1 - lat2, lng2 = point2 - + lat1, lng1 = latlng1 + lat2, lng2 = latlng2 return _cy.great_circle_distance( lat1, lng1, lat2, lng2, - unit=unit + unit = unit ) diff --git a/src/h3/api/basic_int/_public_api.py b/src/h3/api/basic_int/_public_api.py index 56c6a43fd..9bb32ea38 100644 --- a/src/h3/api/basic_int/_public_api.py +++ b/src/h3/api/basic_int/_public_api.py @@ -14,8 +14,8 @@ is_valid_directed_edge = _b.is_valid_directed_edge is_res_class_III = _b.is_res_class_III -int_to_string = _b.int_to_string -string_to_int = _b.string_to_int +int_to_str = _b.int_to_str +str_to_int = _b.str_to_int cell_area = _b.cell_area edge_length = _b.edge_length diff --git a/src/h3/api/basic_str/_binding.py b/src/h3/api/basic_str/_binding.py index e5b563b33..ed6333e63 100644 --- a/src/h3/api/basic_str/_binding.py +++ b/src/h3/api/basic_str/_binding.py @@ -19,24 +19,24 @@ def _in_collection(hexes): - it = [_cy.hex2int(h) for h in hexes] + it = [_cy.str_to_int(h) for h in hexes] return _cy.iter_to_mv(it) def _out_unordered(mv): # todo: should this be an (immutable) frozenset? - return set(_cy.int2hex(h) for h in mv) + return set(_cy.int_to_str(h) for h in mv) def _out_ordered(mv): # todo: should this be an (immutable) tuple? - return list(_cy.int2hex(h) for h in mv) + return list(_cy.int_to_str(h) for h in mv) _binding = _API_FUNCTIONS( - _in_scalar = _cy.hex2int, - _out_scalar = _cy.int2hex, + _in_scalar = _cy.str_to_int, + _out_scalar = _cy.int_to_str, _in_collection = _in_collection, _out_unordered = _out_unordered, _out_ordered = _out_ordered, diff --git a/src/h3lib b/src/h3lib index a36c614b4..46a581c90 160000 --- a/src/h3lib +++ b/src/h3lib @@ -1 +1 @@ -Subproject commit a36c614b477fb5409a0a87e0111d347ce550e40d +Subproject commit 46a581c905b2747c861aa1683125276501f68a3b diff --git a/tests/test_cells_and_edges.py b/tests/test_cells_and_edges.py index cfe6f178e..fd3871c61 100644 --- a/tests/test_cells_and_edges.py +++ b/tests/test_cells_and_edges.py @@ -151,7 +151,7 @@ def test_parent_err(): # todo: revist this weird formatting stuff expected = 'Invalid parent resolution -1 for cell {}.' - expected = expected.format(hex(h3.string_to_int(h))) + expected = expected.format(hex(h3.str_to_int(h))) assert msg == expected @@ -480,12 +480,12 @@ def test_versions(): def test_str_int_convert(): s = '8928308280fffff' - i = h3.string_to_int(s) + i = h3.str_to_int(s) - assert h3.int_to_string(i) == s + assert h3.int_to_str(i) == s -def test_hex2int_fail(): +def test_str_to_int_fail(): h_invalid = {} assert not h3.is_valid_cell(h_invalid) diff --git a/tests/test_memview_int.py b/tests/test_memview_int.py index 3cd2c4501..0e7e56e43 100644 --- a/tests/test_memview_int.py +++ b/tests/test_memview_int.py @@ -9,7 +9,7 @@ def test1(): def test_line(): h1 = '8928308280fffff' h2 = '8928308287bffff' - h1, h2 = h3.string_to_int(h1), h3.string_to_int(h2) + h1, h2 = h3.str_to_int(h1), h3.str_to_int(h2) out = h3.grid_path_cells(h1, h2) From 1c6b82880296d6290594cd1188d524e6477e7956 Mon Sep 17 00:00:00 2001 From: AJ Friend Date: Tue, 23 Aug 2022 01:42:28 -0400 Subject: [PATCH 21/21] polygon_to_cells (#276) * start converting tests over * converted main tests * linting * better null island test * test_compact_and_uncompact_cells * convert a few more tests * add h3.Polygon class * lint * migrating some tests * this shift_circular_list thing makes these tests too hard to understand... * remove shift_circular_list and convert a few more tests * last of the tests in test_h3.py converted * last of the tests converted * note for polygons_to_cells in the future * Try out a __repr__ for h3.Polygon * test_polygon_class.py * use `cells` instead of `hexes` throughout the library and tests * docstring for Polygon * docstrings for functions * use `res` convention for resolution parameters --- .flake8 | 2 + docs/api_reference.md | 6 +- src/h3/__init__.py | 1 + src/h3/_cy/__init__.py | 4 +- src/h3/_cy/geo.pyx | 117 ++---- src/h3/_cy/h3lib.pxd | 4 +- src/h3/_cy/memory.pxd | 2 +- src/h3/_cy/memory.pyx | 8 +- src/h3/_cy/to_multipoly.pyx | 12 +- src/h3/_polygon.py | 52 +++ src/h3/api/_api_template.py | 145 ++++---- src/h3/api/basic_int/_binding.py | 4 +- src/h3/api/basic_int/_public_api.py | 7 +- src/h3/api/basic_str/_binding.py | 4 +- tests/test_basic_int.py | 4 +- tests/test_cells_and_edges.py | 7 +- tests/test_h3.py | 530 +++++++++++----------------- tests/test_numpy_int.py | 6 +- tests/test_polyfill.py | 148 ++++---- tests/test_polygon_class.py | 27 ++ tests/test_to_multipoly.py | 61 +--- 21 files changed, 488 insertions(+), 663 deletions(-) create mode 100644 src/h3/_polygon.py create mode 100644 tests/test_polygon_class.py diff --git a/.flake8 b/.flake8 index 0729f412c..fedf9fd97 100644 --- a/.flake8 +++ b/.flake8 @@ -12,3 +12,5 @@ ignore = E201, # E241 multiple spaces after ',' E241, + # E731 do not assign a lambda expression, use a def + E731, diff --git a/docs/api_reference.md b/docs/api_reference.md index c55099ab9..526c85bde 100644 --- a/docs/api_reference.md +++ b/docs/api_reference.md @@ -59,10 +59,8 @@ Functions relating H3 objects to geographic (lat/lng) coordinates. average_hexagon_edge_length cell_to_boundary directed_edge_to_boundary - polyfill - polyfill_geojson - polyfill_polygon - cells_to_multi_polygon + polygon_to_cells + cells_to_polygons ``` ### Hierarchical relationships diff --git a/src/h3/__init__.py b/src/h3/__init__.py index 4f7cb32f1..366ab02a8 100644 --- a/src/h3/__init__.py +++ b/src/h3/__init__.py @@ -2,6 +2,7 @@ from .api.basic_str import * from ._version import __version__ +from ._polygon import Polygon from ._cy import ( UnknownH3ErrorCode, diff --git a/src/h3/_cy/__init__.py b/src/h3/_cy/__init__.py index d57519a86..b3647aa4e 100644 --- a/src/h3/_cy/__init__.py +++ b/src/h3/_cy/__init__.py @@ -53,9 +53,7 @@ from .geo import ( latlng_to_cell, cell_to_latlng, - polyfill_polygon, - polyfill_geojson, - polyfill, + polygon_to_cells, cell_to_boundary, directed_edge_to_boundary, great_circle_distance, diff --git a/src/h3/_cy/geo.pyx b/src/h3/_cy/geo.pyx index c2579525e..a606c418b 100644 --- a/src/h3/_cy/geo.pyx +++ b/src/h3/_cy/geo.pyx @@ -54,35 +54,27 @@ cpdef (double, double) cell_to_latlng(H3int h) except *: return coord2deg(c) -cdef h3lib.GeoLoop make_geoloop(geos, bool lnglat_order=False) except *: +cdef h3lib.GeoLoop make_geoloop(latlngs) except *: """ - The returned `GeoLoop` must be freed with a call to `free_geoloop`. Parameters ---------- - geos : list or tuple + 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. - lnglat_order : bool - If True, assume coordinate pairs like (lng, lat) - If False, assume coordinate pairs like (lat, lng) """ cdef: h3lib.GeoLoop gl - gl.numVerts = len(geos) + gl.numVerts = len(latlngs) # todo: need for memory management + # can automatically free? gl.verts = h3_calloc(gl.numVerts, sizeof(h3lib.LatLng)) - if lnglat_order: - latlng = (g[::-1] for g in geos) - else: - latlng = geos - - for i, (lat, lng) in enumerate(latlng): + for i, (lat, lng) in enumerate(latlngs): gl.verts[i] = deg2coord(lat, lng) return gl @@ -97,7 +89,7 @@ cdef class GeoPolygon: cdef: h3lib.GeoPolygon gp - def __cinit__(self, outer, holes=None, bool lnglat_order=False): + def __cinit__(self, outer, holes=None): """ Parameters @@ -109,22 +101,18 @@ cdef class GeoPolygon: The order of the pairs may be either clockwise or counterclockwise. holes : list or tuple A sequence of GeoLoops - lnglat_order : bool - If True, assume coordinate pairs like (lng, lat) - If False, assume coordinate pairs like (lat, lng) - """ if holes is None: holes = [] - self.gp.geoloop = make_geoloop(outer, lnglat_order) + 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, lnglat_order) + self.gp.holes[i] = make_geoloop(hole) def __dealloc__(self): @@ -136,36 +124,40 @@ cdef class GeoPolygon: h3_free(self.gp.holes) -def polyfill_polygon(outer, int res, holes=None, bool lnglat_order=False): - """ Set of hexagons whose center is contained in a polygon. +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 as in the GeoJson standard, with an exterior - LinearRing `outer` and a list of LinearRings `holes`, which define any - holes in the 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 LinearRing may be in clockwise or counter-clockwise order + 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 will work with any input format. + 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 LinearRing, a sequence of (lat/lng) or (lng/lat) pairs + 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 LinearRings, describing any holes in the polygon - lnglat_order : bool - If True, assume coordinate pairs like (lng, lat) - If False, assume coordinate pairs like (lat, lng) + 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) - gp = GeoPolygon(outer, holes=holes, lnglat_order=lnglat_order) + gp = GeoPolygon(outer, holes=holes) check_for_error( h3lib.maxPolygonToCellsSize(&gp.gp, res, 0, &n) @@ -180,63 +172,6 @@ def polyfill_polygon(outer, int res, holes=None, bool lnglat_order=False): return mv -def polyfill_geojson(geojson, int res): - """ Set of hexagons whose center is contained in a GeoJson Polygon object. - - The polygon is defined exactly as in the GeoJson standard, so - `geojson` should be a dictionary like: - { - 'type': 'Polygon', - 'coordinates': [...] - } - - 'coordinates' should be a list of LinearRings, where the first ring describes - the exterior boundary of the Polygon, and any subsequent LinearRings - describe holes in the polygon. - - Note that we don't provide an option for the order of the coordinates, - as the GeoJson standard requires them to be in lng/lat order. - - Parameters - ---------- - geojson : dict - res : int - The resolution of the output hexagons - """ - - # todo: this one could handle multipolygons... - - if geojson['type'] != 'Polygon': - raise ValueError('Only Polygon GeoJSON supported') - - coords = geojson['coordinates'] - - out = polyfill_polygon(coords[0], res, holes=coords[1:], lnglat_order=True) - - return out - - -def polyfill(dict geojson, int res, bool geo_json_conformant=False): - """ Light wrapper around `polyfill_geojson` to provide backward compatibility. - """ - - try: - gj_type = geojson['type'] - except KeyError: - raise KeyError("`geojson` dict must have key 'type'.") from None - - if gj_type != 'Polygon': - raise ValueError('Only Polygon GeoJSON supported') - - if geo_json_conformant: - out = polyfill_geojson(geojson, res) - else: - coords = geojson['coordinates'] - out = polyfill_polygon(coords[0], res, holes=coords[1:], lnglat_order=False) - - return out - - def cell_to_boundary(H3int h, bool geo_json=False): """Compose an array of geo-coordinates that outlines a hexagonal cell""" cdef: diff --git a/src/h3/_cy/h3lib.pxd b/src/h3/_cy/h3lib.pxd index 92b68ce8b..00fff6019 100644 --- a/src/h3/_cy/h3lib.pxd +++ b/src/h3/_cy/h3lib.pxd @@ -154,7 +154,7 @@ cdef extern from 'h3api.h': double greatCircleDistanceKm(const LatLng *a, const LatLng *b) nogil double greatCircleDistanceM(const LatLng *a, const LatLng *b) nogil - H3Error cellsToLinkedMultiPolygon(const H3int *h3Set, const int numHexes, LinkedGeoPolygon *out) + H3Error cellsToLinkedMultiPolygon(const H3int *h3Set, const int numCells, LinkedGeoPolygon *out) void destroyLinkedMultiPolygon(LinkedGeoPolygon *polygon) H3Error maxPolygonToCellsSize(const GeoPolygon *geoPolygon, int res, uint32_t flags, uint64_t *count) @@ -170,7 +170,7 @@ cdef extern from 'h3api.h': # int hexRanges(H3int *h3Set, int length, int k, H3int *out) - # void h3SetToLinkedGeo(const H3int *h3Set, const int numHexes, LinkedGeoPolygon *out) + # void h3SetToLinkedGeo(const H3int *h3Set, const int numCells, LinkedGeoPolygon *out) # void destroyLinkedPolygon(LinkedGeoPolygon *polygon) diff --git a/src/h3/_cy/memory.pxd b/src/h3/_cy/memory.pxd index 6068e6159..5e259e256 100644 --- a/src/h3/_cy/memory.pxd +++ b/src/h3/_cy/memory.pxd @@ -9,4 +9,4 @@ cdef class H3MemoryManager: cdef H3int[:] to_mv_keep_zeros(self) cdef int[:] int_mv(size_t n) -cpdef H3int[:] iter_to_mv(hexes) +cpdef H3int[:] iter_to_mv(cells) diff --git a/src/h3/_cy/memory.pyx b/src/h3/_cy/memory.pyx index 8c013b00c..0852105fe 100644 --- a/src/h3/_cy/memory.pyx +++ b/src/h3/_cy/memory.pyx @@ -232,17 +232,17 @@ cdef int[:] int_mv(size_t n): return arr -cpdef H3int[:] iter_to_mv(hexes): - """ hexes needs to be an iterable that knows its size... +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(hexes) + n = len(cells) mv = H3MemoryManager(n).to_mv_keep_zeros() - for i,h in enumerate(hexes): + for i,h in enumerate(cells): mv[i] = h return mv diff --git a/src/h3/_cy/to_multipoly.pyx b/src/h3/_cy/to_multipoly.pyx index 588e28388..d19bda827 100644 --- a/src/h3/_cy/to_multipoly.pyx +++ b/src/h3/_cy/to_multipoly.pyx @@ -32,14 +32,14 @@ cdef walk_coords(const h3lib.LinkedLatLng* L): return out # todo: tuples instead of lists? -def _to_multi_polygon(const H3int[:] hexes): +def _to_multi_polygon(const H3int[:] cells): cdef: h3lib.LinkedGeoPolygon polygon - for h in hexes: + for h in cells: check_cell(h) - h3lib.cellsToLinkedMultiPolygon(&hexes[0], len(hexes), &polygon) + h3lib.cellsToLinkedMultiPolygon(&cells[0], len(cells), &polygon) out = walk_polys(&polygon) @@ -59,12 +59,12 @@ def _geojson_loop(loop): return loop -def cells_to_multi_polygon(const H3int[:] hexes, geo_json=False): +def cells_to_multi_polygon(const H3int[:] cells, geo_json=False): # todo: gotta be a more elegant way to handle these... - if len(hexes) == 0: + if len(cells) == 0: return [] - multipoly = _to_multi_polygon(hexes) + multipoly = _to_multi_polygon(cells) if geo_json: multipoly = [ diff --git a/src/h3/_polygon.py b/src/h3/_polygon.py new file mode 100644 index 000000000..6ef2e9475 --- /dev/null +++ b/src/h3/_polygon.py @@ -0,0 +1,52 @@ +class Polygon: + """ + Container for loops of lat/lng points describing a polygon. + + 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: + + >>> h3.Polygon( + ... [(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: + + >>> h3.Polygon( + ... [(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: + + >>> h3.Polygon( + ... [(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): + self.outer = outer + self.holes = holes + + def __repr__(self): + s = ''.format( + len(self.outer), + tuple(map(len, self.holes)), + ) + + return s diff --git a/src/h3/api/_api_template.py b/src/h3/api/_api_template.py index 70ac0515d..7baa27eab 100644 --- a/src/h3/api/_api_template.py +++ b/src/h3/api/_api_template.py @@ -38,6 +38,7 @@ """ from .. import _cy +from .._polygon import Polygon class _API_FUNCTIONS(object): @@ -112,7 +113,7 @@ def int_to_str(x): return _cy.int_to_str(x) @staticmethod - def get_num_cells(resolution): + def get_num_cells(res): """ Return the total number of *cells* (hexagons and pentagons) for the given resolution. @@ -121,10 +122,10 @@ def get_num_cells(resolution): ------- int """ - return _cy.get_num_cells(resolution) + return _cy.get_num_cells(res) @staticmethod - def average_hexagon_area(resolution, unit='km^2'): + def average_hexagon_area(res, unit='km^2'): """ Return the average area of an H3 *hexagon* for the given resolution. @@ -135,10 +136,10 @@ def average_hexagon_area(resolution, unit='km^2'): ------- float """ - return _cy.average_hexagon_area(resolution, unit) + return _cy.average_hexagon_area(res, unit) @staticmethod - def average_hexagon_edge_length(resolution, unit='km'): + def average_hexagon_edge_length(res, unit='km'): """ Return the average *hexagon* edge length for the given resolution. @@ -149,7 +150,7 @@ def average_hexagon_edge_length(resolution, unit='km'): ------- float """ - return _cy.average_hexagon_edge_length(resolution, unit) + return _cy.average_hexagon_edge_length(res, unit) def is_valid_cell(self, h): """ @@ -179,7 +180,7 @@ def is_valid_directed_edge(self, edge): except (ValueError, TypeError): return False - def latlng_to_cell(self, lat, lng, resolution): + def latlng_to_cell(self, lat, lng, res): """ Return the cell containing the (lat, lng) point for a given resolution. @@ -189,7 +190,7 @@ def latlng_to_cell(self, lat, lng, resolution): H3Cell """ - return self._out_scalar(_cy.latlng_to_cell(lat, lng, resolution)) + return self._out_scalar(_cy.latlng_to_cell(lat, lng, res)) def cell_to_latlng(self, h): """ @@ -331,7 +332,7 @@ def grid_ring(self, h, k=1): def cell_to_children(self, h, res=None): """ - Children of a hexagon. + Children of a cell. Parameters ---------- @@ -349,7 +350,7 @@ def cell_to_children(self, h, res=None): return self._out_unordered(mv) # todo: nogil for expensive C operation? - def compact_cells(self, hexes): + def compact_cells(self, cells): """ Compact a collection of H3 cells by combining smaller cells into larger cells, if all child cells @@ -357,19 +358,19 @@ def compact_cells(self, hexes): Parameters ---------- - hexes : iterable of H3Cell + cells : iterable of H3 Cells Returns ------- unordered collection of H3Cell """ # todo: does compact_cells work on mixed-resolution collections? - hu = self._in_collection(hexes) + hu = self._in_collection(cells) hc = _cy.compact_cells(hu) return self._out_unordered(hc) - def uncompact_cells(self, hexes, res): + def uncompact_cells(self, cells, res): """ Reverse the `compact_cells` operation. @@ -377,7 +378,7 @@ def uncompact_cells(self, hexes, res): Parameters ---------- - hexes : iterable of H3Cell + cells : iterable of H3Cell res : int Resolution of desired output cells. @@ -388,91 +389,80 @@ def uncompact_cells(self, hexes, res): Raises ------ todo: add test to make sure an error is returned when input - contains hex smaller than output res. + contains cell smaller than output res. https://github.com/uber/h3/blob/master/src/h3lib/lib/h3Index.c#L425 """ - hc = self._in_collection(hexes) + hc = self._in_collection(cells) hu = _cy.uncompact_cells(hc, res) return self._out_unordered(hu) - def cells_to_multi_polygon(self, hexes, geo_json=False): + def polygon_to_cells(self, polygon, res): """ - Get GeoJSON-like MultiPolygon describing the outline of the area - covered by a set of H3 cells. + Return the set of H3 cells at a given resolution whose center points + are contained within a `h3.Polygon` Parameters ---------- - hexes : unordered collection of H3Cell - geo_json : bool, optional - If `True`, output geo sequences will be lng/lat pairs, with the - last the same as the first. - If `False`, output geo sequences will be lat/lng pairs, with the - last distinct from the first. - Defaults to `False` - - Returns - ------- - list - List of "polygons". - Each polygon is a list of "geo sequences" like - ``[outer, hole1, hole2, ...]``. The holes may not be present. - Each geo sequence is a list of lat/lng or lng/lat pairs. - """ - # todo: this function output does not match with `polyfill`. - # This function returns a list of polygons, while `polyfill` returns - # a GeoJSON-like dictionary object. - hexes = self._in_collection(hexes) - return _cy.cells_to_multi_polygon(hexes, geo_json=geo_json) + Polygon : h3.Polygon + A polygon described by an outer ring and optional holes - def polyfill_polygon(self, outer, res, holes=None, lnglat_order=False): - mv = _cy.polyfill_polygon(outer, res, holes=holes, lnglat_order=lnglat_order) + res : int + Resolution of the output cells + + Examples + -------- + + >>> poly = h3.Polygon( + ... [(37.68, -122.54), (37.68, -122.34), (37.82, -122.34), + ... (37.82, -122.54)], + ... ) + >>> h3.polygon_to_cells(poly, 6) + {'862830807ffffff', + '862830827ffffff', + '86283082fffffff', + '862830877ffffff', + '862830947ffffff', + '862830957ffffff', + '86283095fffffff'} + """ + mv = _cy.polygon_to_cells(polygon.outer, res, holes=polygon.holes) return self._out_unordered(mv) - def polyfill_geojson(self, geojson, res): - mv = _cy.polyfill_geojson(geojson, res) - - return self._out_unordered(mv) + # def polygons_to_cells(self, polygons, res): + # # todo: have to figure out how to concat memoryviews cleanly + # # or some other approach + # pass - def polyfill(self, geojson, res, geo_json_conformant=False): + def cells_to_polygons(self, cells): """ - Get set of hexagons whose *centers* are contained within - a GeoJSON-style polygon. + Return a list of h3.Polygon objects describing the area + covered by a set of H3 cells. Parameters ---------- - geojson : dict - GeoJSON-style input dictionary describing a polygon (optionally - including holes). - - Dictionary should be formatted like: + cells : iterable of H3 cells - .. code-block:: text + Returns + ------- + list[h3.Polygon] + List of h3.Polygon objects - { - 'type': 'Polygon', - 'coordinates': [outer, hole1, hole2, ...], - } + Examples + -------- - `outer`, `hole1`, etc., are lists of geo coordinate tuples. - The holes are optional. + >>> cells = ['8428309ffffffff', '842830dffffffff'] + >>> h3.cells_to_polygons(cells) + [] - res : int - Desired output resolution for cells. - geo_json_conformant : bool, optional - When ``True``, ``outer``, ``hole1``, etc. must be sequences of - lng/lat pairs, with the last the same as the first. - When ``False``, they must be sequences of lat/lng pairs, - with the last not needing to match the first. - - Returns - ------- - unordered collection of H3Cell """ - mv = _cy.polyfill(geojson, res, geo_json_conformant=geo_json_conformant) + cells = self._in_collection(cells) + geos = _cy.cells_to_multi_polygon(cells) - return self._out_unordered(mv) + polys = [Polygon(*geo) for geo in geos] + + return polys def is_pentagon(self, h): """ @@ -686,19 +676,20 @@ def is_res_class_III(self, h): """ return _cy.is_res_class_iii(self._in_scalar(h)) - def get_pentagons(self, resolution): + def get_pentagons(self, res): """ Return all pentagons at a given resolution. Parameters ---------- - resolution : int + res : int + Resolution of the pentagons Returns ------- unordered collection of H3Cell """ - mv = _cy.get_pentagons(resolution) + mv = _cy.get_pentagons(res) return self._out_unordered(mv) diff --git a/src/h3/api/basic_int/_binding.py b/src/h3/api/basic_int/_binding.py index adfdb6e65..28002dd93 100644 --- a/src/h3/api/basic_int/_binding.py +++ b/src/h3/api/basic_int/_binding.py @@ -21,8 +21,8 @@ def _id(x): return x -def _in_collection(hexes): - it = list(hexes) +def _in_collection(cells): + it = list(cells) return _cy.iter_to_mv(it) diff --git a/src/h3/api/basic_int/_public_api.py b/src/h3/api/basic_int/_public_api.py index 9bb32ea38..6f1b55277 100644 --- a/src/h3/api/basic_int/_public_api.py +++ b/src/h3/api/basic_int/_public_api.py @@ -47,11 +47,8 @@ compact_cells = _b.compact_cells uncompact_cells = _b.uncompact_cells -# todo: think through polyfill functions -polyfill = _b.polyfill -polyfill_geojson = _b.polyfill_geojson -polyfill_polygon = _b.polyfill_polygon -cells_to_multi_polygon = _b.cells_to_multi_polygon +polygon_to_cells = _b.polygon_to_cells +cells_to_polygons = _b.cells_to_polygons are_neighbor_cells = _b.are_neighbor_cells cells_to_directed_edge = _b.cells_to_directed_edge diff --git a/src/h3/api/basic_str/_binding.py b/src/h3/api/basic_str/_binding.py index ed6333e63..4ebd5fb1d 100644 --- a/src/h3/api/basic_str/_binding.py +++ b/src/h3/api/basic_str/_binding.py @@ -18,8 +18,8 @@ from .._api_template import _API_FUNCTIONS -def _in_collection(hexes): - it = [_cy.str_to_int(h) for h in hexes] +def _in_collection(cells): + it = [_cy.str_to_int(h) for h in cells] return _cy.iter_to_mv(it) diff --git a/tests/test_basic_int.py b/tests/test_basic_int.py index 1b8b8b316..99452feb5 100644 --- a/tests/test_basic_int.py +++ b/tests/test_basic_int.py @@ -26,9 +26,9 @@ def test_grid_disk(): def test_compact_cells(): h = 617700169958293503 - hexes = h3.cell_to_children(h) + cells = h3.cell_to_children(h) - assert h3.compact_cells(hexes) == {h} + assert h3.compact_cells(cells) == {h} def test_get_icosahedron_faces(): diff --git a/tests/test_cells_and_edges.py b/tests/test_cells_and_edges.py index fd3871c61..474656958 100644 --- a/tests/test_cells_and_edges.py +++ b/tests/test_cells_and_edges.py @@ -231,7 +231,7 @@ def test_distance_error(): def test_compact_cells(): # lat/lngs for State of Maine - maine = [ + maine = h3.Polygon([ (45.137451890638886, -67.13734351262877), (44.8097, -66.96466), (44.3252, -68.03252), @@ -252,11 +252,10 @@ def test_compact_cells(): (47.066248887716995, -67.79035274928509), (45.702585354182816, -67.79141211614706), (45.137451890638886, -67.13734351262877) - ] + ]) res = 5 - - h_uncomp = h3.polyfill_polygon(maine, res) + h_uncomp = h3.polygon_to_cells(maine, res=res) h_comp = h3.compact_cells(h_uncomp) expected = {'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 diff --git a/tests/test_h3.py b/tests/test_h3.py index 3cf5d8f35..5900db989 100644 --- a/tests/test_h3.py +++ b/tests/test_h3.py @@ -4,12 +4,6 @@ import h3 -def shift_circular_list(start_element, elements_list): - # We shift the circular list so that it starts from start_element, - start_index = elements_list.index(start_element) - return elements_list[start_index:] + elements_list[:start_index] - - def test_is_valid_cell(): assert h3.is_valid_cell('85283473fffffff') assert h3.is_valid_cell('850dab63fffffff') @@ -154,372 +148,253 @@ def test_grid_disk_pentagon(): assert out == expected +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), +] + +sf_hole1 = [ + (37.7869802, -122.4471197), + (37.7664102, -122.4590777), + (37.7710682, -122.4137097), +] + +sf_hole2 = [ + (37.747976, -122.490025), + (37.731550, -122.503758), + (37.725440, -122.452603), +] + + def test_polyfill(): - geo = { - 'type': 'Polygon', - 'coordinates': [ - [ - [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.Polygon(sf_7x7) + out = h3.polygon_to_cells(poly, res=9) - out = h3.polyfill(geo, 9) - assert len(out) > 1000 + assert len(out) == 1253 + assert '89283080527ffff' in out + assert '89283095edbffff' in out -def test_polyfill_bogus_geo_json(): - with pytest.raises(ValueError): - bad_geo = {'type': 'whatwhat'} - h3.polyfill(bad_geo, 9) +# def test_polyfill_bogus_geo_json(): +# with pytest.raises(ValueError): +# bad_geo = {'type': 'whatwhat'} +# h3.polyfill(bad_geo, 9) def test_polyfill_with_hole(): - geo = { - 'type': 'Polygon', - 'coordinates': [ - [ - [37.813318999983238, -122.4089866999972145], - [37.7866302000007224, -122.3805436999997056], - [37.7198061999978478, -122.3544736999993603], - [37.7076131999975672, -122.5123436999983966], - [37.7835871999971715, -122.5247187000021967], - [37.8151571999998453, -122.4798767000009008], - ], - [ - [37.7869802, -122.4471197], - [37.7664102, -122.4590777], - [37.7710682, -122.4137097], - ] - ] - } + poly = h3.Polygon(sf_7x7, sf_hole1) - out = h3.polyfill(geo, 9) - assert len(out) > 1000 + out = h3.polygon_to_cells(poly, res=9) + assert len(out) == 1214 + + foo = lambda x: h3.polygon_to_cells(h3.Polygon(x), 9) + # todo: foo = lambda x: h3.Polygon(x).to_cells(9) + assert out == foo(sf_7x7) - foo(sf_hole1) def test_polyfill_with_two_holes(): - geo = { - 'type': 'Polygon', - 'coordinates': [ - [ - [37.813318999983238, -122.4089866999972145], - [37.7866302000007224, -122.3805436999997056], - [37.7198061999978478, -122.3544736999993603], - [37.7076131999975672, -122.5123436999983966], - [37.7835871999971715, -122.5247187000021967], - [37.8151571999998453, -122.4798767000009008], - ], - [ - [37.7869802, -122.4471197], - [37.7664102, -122.4590777], - [37.7710682, -122.4137097], - ], - [ - [37.747976, -122.490025], - [37.731550, -122.503758], - [37.725440, -122.452603], - ] - ] - } - out = h3.polyfill(geo, 9) - assert len(out) > 1000 - - -def test_polyfill_geo_json_compliant(): - geo = { - 'type': 'Polygon', - 'coordinates': [ - [ - [-122.4089866999972145, 37.813318999983238], - [-122.3805436999997056, 37.7866302000007224], - [-122.3544736999993603, 37.7198061999978478], - [-122.5123436999983966, 37.7076131999975672], - [-122.5247187000021967, 37.7835871999971715], - [-122.4798767000009008, 37.8151571999998453], - ] - ] - } + poly = h3.Polygon(sf_7x7, sf_hole1, sf_hole2) + out = h3.polygon_to_cells(poly, 9) + assert len(out) == 1172 + + foo = lambda x: h3.polygon_to_cells(h3.Polygon(x), 9) + assert out == foo(sf_7x7) - (foo(sf_hole1) | foo(sf_hole2)) + +# def test_polyfill_geo_json_compliant(): +# geo = { +# 'type': 'Polygon', +# 'coordinates': [ +# [ +# [-122.4089866999972145, 37.813318999983238], +# [-122.3805436999997056, 37.7866302000007224], +# [-122.3544736999993603, 37.7198061999978478], +# [-122.5123436999983966, 37.7076131999975672], +# [-122.5247187000021967, 37.7835871999971715], +# [-122.4798767000009008, 37.8151571999998453], +# ] +# ] +# } - out = h3.polyfill(geo, 9, True) - assert len(out) > 1000 +# out = h3.polyfill(geo, 9, True) +# assert len(out) > 1000 def test_polyfill_down_under(): - geo = { - 'type': 'Polygon', - 'coordinates': [ - [ - [151.1979259, -33.8555555], - [151.2074556, -33.8519779], - [151.224743, -33.8579597], - [151.2254986, -33.8582212], - [151.235313348, -33.8564183032], - [151.234799568, -33.8594049408], - [151.233485084, -33.8641069037], - [151.233181742, -33.8715791334], - [151.223980353, -33.8876967719], - [151.219388501, -33.8873877027], - [151.2189209, -33.8869995], - [151.2181177, -33.886283399999996], - [151.2157995, -33.8851287], - [151.2156925, -33.8852471], - [151.2141233, -33.8851287], - [151.2116267, -33.8847438], - [151.2083456, -33.8834707], - [151.2080246, -33.8827601], - [151.2059204, -33.8816053], - [151.2043868, -33.8827601], - [151.2028176, -33.8838556], - [151.2022826, -33.8839148], - [151.2011057, -33.8842405], - [151.1986114, -33.8842819], - [151.1986091, -33.8842405], - [151.1948287, -33.8773416], - [151.1923322, -33.8740845], - [151.1850566, -33.8697019], - [151.1902636, -33.8625354], - [151.1986805, -33.8612915], - [151.1979259, -33.8555555], - ] - ] - } + sydney = [ + (-33.8555555, 151.1979259), + (-33.8519779, 151.2074556), + (-33.8579597, 151.224743), + (-33.8582212, 151.2254986), + (-33.8564183032, 151.235313348), + (-33.8594049408, 151.234799568), + (-33.8641069037, 151.233485084), + (-33.8715791334, 151.233181742), + (-33.8876967719, 151.223980353), + (-33.8873877027, 151.219388501), + (-33.8869995, 151.2189209), + (-33.886283399999996, 151.2181177), + (-33.8851287, 151.2157995), + (-33.8852471, 151.2156925), + (-33.8851287, 151.2141233), + (-33.8847438, 151.2116267), + (-33.8834707, 151.2083456), + (-33.8827601, 151.2080246), + (-33.8816053, 151.2059204), + (-33.8827601, 151.2043868), + (-33.8838556, 151.2028176), + (-33.8839148, 151.2022826), + (-33.8842405, 151.2011057), + (-33.8842819, 151.1986114), + (-33.8842405, 151.1986091), + (-33.8773416, 151.1948287), + (-33.8740845, 151.1923322), + (-33.8697019, 151.1850566), + (-33.8625354, 151.1902636), + (-33.8612915, 151.1986805), + (-33.8555555, 151.1979259), + ] - out = h3.polyfill(geo, 9, True) - assert len(out) > 10 + poly = h3.Polygon(sydney) + out = h3.polygon_to_cells(poly, 9) + assert len(out) == 92 + assert '89be0e34207ffff' in out + assert '89be0e35ddbffff' in out def test_polyfill_far_east(): - geo = { - 'type': 'Polygon', - 'coordinates': [ - [ - [142.86483764648438, 41.92578147109541], - [142.86483764648438, 42.29965889253408], - [143.41552734375, 42.29965889253408], - [143.41552734375, 41.92578147109541], - [142.86483764648438, 41.92578147109541], - ] - ] - } + geo = [ + (41.92578147109541, 142.86483764648438), + (42.29965889253408, 142.86483764648438), + (42.29965889253408, 143.41552734375), + (41.92578147109541, 143.41552734375), + (41.92578147109541, 142.86483764648438), + ] - out = h3.polyfill(geo, 9, True) - assert len(out) > 10 + poly = h3.Polygon(geo) + out = h3.polygon_to_cells(poly, 9) + assert len(out) == 18507 + assert '892e18d16c3ffff' in out + assert '892e1ebb5a7ffff' in out def test_polyfill_southern_tip(): - geo = { - 'type': 'Polygon', - 'coordinates': [ - [ - [-67.642822265625, -55.41654360858007], - [-67.642822265625, -54.354955689554096], - [-64.742431640625, -54.354955689554096], - [-64.742431640625, -55.41654360858007], - [-67.642822265625, -55.41654360858007], - ] - ] - } + geo = [ + (-55.41654360858007, -67.642822265625), + (-54.354955689554096, -67.642822265625), + (-54.354955689554096, -64.742431640625), + (-55.41654360858007, -64.742431640625), + (-55.41654360858007, -67.642822265625), + ] - out = h3.polyfill(geo, 9, True) - assert len(out) > 10 + poly = h3.Polygon(geo) + out = h3.polygon_to_cells(poly, 9) + assert len(out) == 223247 + assert '89df4000003ffff' in out + assert '89df4636b27ffff' in out def test_polyfill_null_island(): - geo = { - "type": "Polygon", - "coordinates": [ - [ - [-3.218994140625, -3.0856655287215378], - [-3.218994140625, 3.6888551431470478], - [3.5815429687499996, 3.6888551431470478], - [3.5815429687499996, -3.0856655287215378], - [-3.218994140625, -3.0856655287215378], - ] - ] - } + geo = [ + (-3, -3), + (+3, -3), + (+3, +3), + (-3, +3), + (-3, -3), + ] - out = h3.polyfill(geo, 4, True) - assert len(out) > 10 + poly = h3.Polygon(geo) + out = h3.polygon_to_cells(poly, 4) + assert len(out) == 345 + assert '847421bffffffff' in out + assert '84825ddffffffff' in out -def test_cells_to_multi_polygon_empty(): - out = h3.cells_to_multi_polygon([]) - assert out == [] +def test_cells_to_polygons_empty(): + polys = h3.cells_to_polygons([]) + assert polys == [] -def test_cells_to_multi_polygon_single(): +def test_cells_to_polygons_single(): h = '89283082837ffff' - hexes = {h} + cells = {h} + + polys = h3.cells_to_polygons(cells) + assert len(polys) == 1 + poly = polys[0] - # multi_polygon - mp = h3.cells_to_multi_polygon(hexes) vertices = h3.cell_to_boundary(h) + expected_poly = h3.Polygon(vertices) - # We shift the expected circular list so that it starts from - # multi_polygon[0][0][0], since output starting from any vertex - # would be correct as long as it's in order. - expected_coords = shift_circular_list( - mp[0][0][0], - [ - vertices[2], - vertices[3], - vertices[4], - vertices[5], - vertices[0], - vertices[1], - ] - ) + assert set(poly.outer) == set(expected_poly.outer) + assert poly.holes == expected_poly.holes == () - expected = [[expected_coords]] - - assert mp == expected - - -def test_cells_to_multi_polygon_single_geo_json(): - hexes = ['89283082837ffff'] - mp = h3.cells_to_multi_polygon(hexes, True) - vertices = h3.cell_to_boundary(hexes[0], True) - - # We shift the expected circular list so that it starts from - # multi_polygon[0][0][0], since output starting from any vertex - # would be correct as long as it's in order. - expected_coords = shift_circular_list( - mp[0][0][0], - [ - vertices[2], - vertices[3], - vertices[4], - vertices[5], - vertices[0], - vertices[1] - ] - ) - expected = [[expected_coords]] - - # polygon count matches expected - assert len(mp) == 1 - - # loop count matches expected - assert len(mp[0]) == 1 - - # coord count 7 matches expected according to geojson format - assert len(mp[0][0]) == 7 - - # first coord should be the same as last coord according to geojson format - assert mp[0] == mp[-1] - - # the coord should be (lng, lat) according to geojson format - assert mp[0][0][0][0] == approx(-122.42778275313199) - assert mp[0][0][0][1] == approx(37.77598951883773) - - # Discard last coord for testing below, since last coord is - # the same as the first one - mp[0][0].pop() - assert mp == expected - - -def test_cells_to_multi_polygon_contiguous(): - # the second hexagon shares v0 and v1 with the first - hexes = ['89283082837ffff', '89283082833ffff'] - - # multi_polygon - mp = h3.cells_to_multi_polygon(hexes) - vertices0 = h3.cell_to_boundary(hexes[0]) - vertices1 = h3.cell_to_boundary(hexes[1]) - - # We shift the expected circular list so that it starts from - # multi_polygon[0][0][0], since output starting from any vertex - # would be correct as long as it's in order. - expected_coords = shift_circular_list( - mp[0][0][0], - [ - vertices1[0], - vertices1[1], - vertices1[2], - vertices0[1], - vertices0[2], - vertices0[3], - vertices0[4], - vertices0[5], - vertices1[4], - vertices1[5], - ] - ) +def test_cells_to_polygons_contiguous(): + a = '89283082837ffff' + b = '89283082833ffff' + assert h3.are_neighbor_cells(a, b) + + polys = h3.cells_to_polygons([a, b]) + assert len(polys) == 1 + poly = polys[0] - expected = [[expected_coords]] + assert len(poly.outer) == 10 + assert poly.holes == () - assert len(mp) == 1 # polygon count matches expected - assert len(mp[0]) == 1 # loop count matches expected - assert len(mp[0][0]) == 10 # coord count matches expected + verts_a = h3.cell_to_boundary(a) + verts_b = h3.cell_to_boundary(b) + assert set(poly.outer) == set(verts_a) | set(verts_b) - assert mp == expected +def test_cells_to_polygons_non_contiguous(): + a = '89283082837ffff' + b = '8928308280fffff' + assert not h3.are_neighbor_cells(a, b) -def test_cells_to_multi_polygon_non_contiguous(): - # the second hexagon does not touch the first - hexes = {'89283082837ffff', '8928308280fffff'} - # multi_polygon - mp = h3.cells_to_multi_polygon(hexes) + polys = h3.cells_to_polygons([a, b]) + assert len(polys) == 2 - assert len(mp) == 2 # polygon count matches expected - assert len(mp[0]) == 1 # loop count matches expected - assert len(mp[0][0]) == 6 # coord count 1 matches expected - assert len(mp[1][0]) == 6 # coord count 2 matches expected + assert all(poly.holes == () for poly in polys) + assert all(len(poly.outer) == 6 for poly in polys) + verts_a = h3.cell_to_boundary(a) + verts_b = h3.cell_to_boundary(b) -def test_cells_to_multi_polygon_hole(): + verts_both = set.union(*[set(poly.outer) for poly in polys]) + assert verts_both == set(verts_a) | set(verts_b) + + +def test_cells_to_polygons_hole(): # Six hexagons in a ring around a hole - hexes = [ + cells = [ '892830828c7ffff', '892830828d7ffff', '8928308289bffff', '89283082813ffff', '8928308288fffff', '89283082883ffff', ] - mp = h3.cells_to_multi_polygon(hexes) - - assert len(mp) == 1 # polygon count matches expected - assert len(mp[0]) == 2 # loop count matches expected - assert len(mp[0][0]) == 6 * 3 # outer coord count matches expected - assert len(mp[0][1]) == 6 # inner coord count matches expected + polys = h3.cells_to_polygons(cells) + assert len(polys) == 1 + poly = polys[0] -def test_cells_to_multi_polygon_2grid_disk(): - h = '8930062838bffff' - hexes = h3.grid_disk(h, 2) - # multi_polygon - mp = h3.cells_to_multi_polygon(hexes) - - assert len(mp) == 1 # polygon count matches expected - assert len(mp[0]) == 1 # loop count matches expected - assert len(mp[0][0]) == 6 * (2 * 2 + 1) # coord count matches expected - - hexes2 = { - '89300628393ffff', '89300628383ffff', '89300628397ffff', - '89300628067ffff', '89300628387ffff', '893006283bbffff', - '89300628313ffff', '893006283cfffff', '89300628303ffff', - '89300628317ffff', '8930062839bffff', h, - '8930062806fffff', '8930062838fffff', '893006283d3ffff', - '893006283c3ffff', '8930062831bffff', '893006283d7ffff', - '893006283c7ffff' - } + assert len(poly.holes) == 1 + assert len(poly.holes[0]) == 6 + assert len(poly.outer) == 6 * 3 - mp2 = h3.cells_to_multi_polygon(hexes2) - assert len(mp2) == 1 # polygon count matches expected - assert len(mp2[0]) == 1 # loop count matches expected - assert len(mp2[0][0]) == 6 * (2 * 2 + 1) # coord count matches expected +def test_cells_to_polygons_2grid_disk(): + h = '8930062838bffff' + cells = h3.grid_disk(h, 2) + polys = h3.cells_to_polygons(cells) - hexes3 = list(h3.grid_disk(h, 6)) - hexes3.sort() - mp3 = h3.cells_to_multi_polygon(hexes3) + assert len(polys) == 1 + poly = polys[0] - assert len(mp3[0]) == 1 # loop count matches expected + assert len(poly.holes) == 0 + assert len(poly.outer) == 6 * (2 * 2 + 1) def test_grid_ring(): @@ -577,28 +452,17 @@ def test_grid_ring_pentagon(): def test_compact_and_uncompact_cells(): - geo = { - 'type': 'Polygon', - 'coordinates': [ - [ - [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.Polygon(sf_7x7) + cells = h3.polygon_to_cells(poly, 9) - hexes = h3.polyfill(geo, 9) - - compact_cells = h3.compact_cells(hexes) + 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 uncompact_cells == cells + def test_compact_cells_and_uncompact_cells_nothing(): assert h3.compact_cells([]) == set() @@ -613,10 +477,10 @@ def test_uncompact_cells_error(): def test_compact_cells_malformed_input(): - hexes = ['89283082813ffff'] * 13 + cells = ['89283082813ffff'] * 13 with pytest.raises(Exception): - h3.compact_cells(hexes) + h3.compact_cells(cells) def test_cell_to_parent(): diff --git a/tests/test_numpy_int.py b/tests/test_numpy_int.py index 5ebcd44ac..c565518dc 100644 --- a/tests/test_numpy_int.py +++ b/tests/test_numpy_int.py @@ -25,10 +25,10 @@ def test5(): def test_compact_cells(): h = 617700169958293503 - hexes = h3.cell_to_children(h) - assert isinstance(hexes, np.ndarray) + cells = h3.cell_to_children(h) + assert isinstance(cells, np.ndarray) - assert set(h3.compact_cells(hexes)) == {h} + assert set(h3.compact_cells(cells)) == {h} def test_get_icosahedron_faces(): diff --git a/tests/test_polyfill.py b/tests/test_polyfill.py index 58faaffcc..6e2c55096 100644 --- a/tests/test_polyfill.py +++ b/tests/test_polyfill.py @@ -31,21 +31,22 @@ def chain_toggle_map(func, seq): return seq -def input_permutations(poly, res=5): - g = [poly] +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: - hexes = h3.polyfill_polygon(p[0], res=res, holes=p[1:]) - yield hexes + poly = h3.Polygon(*p) + cells = h3.polygon_to_cells(poly, res=res) + yield cells def swap_element_order(seq): return [e[::-1] for e in seq] -def get_us_box_coords(order='latlng'): +def get_us_box_coords(): # big center chunk of the US in lat/lng order outer = [ @@ -71,13 +72,10 @@ def get_us_box_coords(order='latlng'): [41.37, -98.61] ] - if order == 'lnglat': - outer, hole1, hole2 = map(swap_element_order, [outer, hole1, hole2]) - return outer, hole1, hole2 -def test_polyfill_polygon(): +def test_polygon_to_cells(): # lat/lngs for State of Maine maine = [ @@ -113,72 +111,74 @@ def test_polyfill_polygon(): '832badfffffffff' } - out = h3.polyfill_polygon(maine, 3) + poly = h3.Polygon(maine) + out = h3.polygon_to_cells(poly, 3) assert out == expected -def test_polyfill_polygon_order(): - lnglat, _, _ = get_us_box_coords(order='lnglat') +def test_polygon_to_cells2(): + lnglat, _, _ = get_us_box_coords() - out = h3.polyfill_polygon(lnglat, 5, lnglat_order=True) + poly = h3.Polygon(lnglat) + out = h3.polygon_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 +# 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_polyfill_polygon_holes(): +def test_polygon_to_cells_holes(): outer, hole1, hole2 = get_us_box_coords() assert 7063 == len( - h3.polyfill_polygon(outer, 5) + h3.polygon_to_cells(h3.Polygon(outer), 5) ) for res in 1, 2, 3, 4, 5: - hexes_all = h3.polyfill_polygon(outer, res) - hexes_holes = h3.polyfill_polygon(outer, res, [hole1, hole2]) + cells_all = h3.polygon_to_cells(h3.Polygon(outer), res) + cells_holes = h3.polygon_to_cells(h3.Polygon(outer, hole1, hole2), res=res) - hexes_1 = h3.polyfill_polygon(hole1, res) - hexes_2 = h3.polyfill_polygon(hole2, res) + cells_1 = h3.polygon_to_cells(h3.Polygon(hole1), res) + cells_2 = h3.polygon_to_cells(h3.Polygon(hole2), res) - assert len(hexes_all) == len(hexes_holes) + len(hexes_1) + len(hexes_2) - assert hexes_all == set.union(hexes_holes, hexes_1, hexes_2) + assert len(cells_all) == len(cells_holes) + len(cells_1) + len(cells_2) + assert cells_all == set.union(cells_holes, cells_1, cells_2) -def test_polyfill_geojson(): - outer, hole1, hole2 = get_us_box_coords(order='lnglat') +# def test_polyfill_geojson(): +# outer, hole1, hole2 = get_us_box_coords(order='lnglat') - d = { - 'type': 'Polygon', - 'coordinates': [outer], - } +# d = { +# 'type': 'Polygon', +# 'coordinates': [outer], +# } - out = h3.polyfill_geojson(d, 5) +# out = h3.polyfill_geojson(d, 5) - assert len(out) == 7063 +# assert len(out) == 7063 -def test_polyfill(): - outer, hole1, hole2 = get_us_box_coords(order='lnglat') +# def test_polyfill(): +# outer, hole1, hole2 = get_us_box_coords(order='lnglat') - d = { - 'type': 'Polygon', - 'coordinates': [outer], - } +# d = { +# 'type': 'Polygon', +# 'coordinates': [outer], +# } - out = h3.polyfill(d, 5, geo_json_conformant=True) +# out = h3.polyfill(d, 5, geo_json_conformant=True) - assert len(out) == 7063 +# assert len(out) == 7063 def test_input_format(): - """ Test that `polyfill_polygon` can take in polygon inputs + """ Test that `polygon_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). @@ -188,34 +188,34 @@ def test_input_format(): may follow a different subset of rules. """ - poly = get_us_box_coords(order='latlng') + geo = get_us_box_coords() - assert len(poly) == 3 + assert len(geo) == 3 # two holes - for hexes in input_permutations(poly[:3]): - assert len(hexes) == 5437 + for cells in input_permutations(geo[:3]): + assert len(cells) == 5437 # one hole - for hexes in input_permutations(poly[:2]): - assert len(hexes) == 5726 + for cells in input_permutations(geo[:2]): + assert len(cells) == 5726 # zero holes - for hexes in input_permutations(poly[:1]): - assert len(hexes) == 7063 + for cells in input_permutations(geo[:1]): + assert len(cells) == 7063 def test_resolution(): - d = { - 'type': 'Polygon', - 'coordinates': [[]], - } + poly = h3.Polygon([]) + + assert h3.polygon_to_cells(poly, 0) == set() + assert h3.polygon_to_cells(poly, 15) == set() with pytest.raises(H3ResDomainError): - h3.polyfill(d, -1) + h3.polygon_to_cells(poly, -1) with pytest.raises(H3ResDomainError): - h3.polyfill(d, 16) + h3.polygon_to_cells(poly, 16) def test_invalid_polygon(): @@ -224,27 +224,17 @@ def test_invalid_polygon(): this because we weren't raising errors inside some `cdef` functions. """ - - # one - d = { - 'type': 'Polygon', - 'coordinates': [1, 2, 3], - } - with pytest.raises(TypeError): - h3.polyfill(d, 4) - - # two - d = { - 'type': 'Polygon', - 'coordinates': [[1, 2, 3]], - } - with pytest.raises(TypeError): - h3.polyfill(d, 4) - - # three - d = { - 'type': 'Polygon', - 'coordinates': [(1, 2), (2, 2), (2, 1), (1, 2)], - } with pytest.raises(TypeError): - h3.polyfill(d, 4) + poly = h3.Polygon([1, 2, 3]) + h3.polygon_to_cells(poly, 4) + + with pytest.raises(ValueError): + poly = h3.Polygon([[1, 2, 3]]) + h3.polygon_to_cells(poly, 4) + + # d = { + # 'type': 'Polygon', + # 'coordinates': [(1, 2), (2, 2), (2, 1), (1, 2)], + # } + # with pytest.raises(TypeError): + # h3.polyfill(d, 4) diff --git a/tests/test_polygon_class.py b/tests/test_polygon_class.py new file mode 100644 index 000000000..d14b62aa3 --- /dev/null +++ b/tests/test_polygon_class.py @@ -0,0 +1,27 @@ +import h3 + + +def test_repr(): + a = '8928308280fffff' + b = h3.grid_ring(a, 5).pop() + + cells1 = h3.grid_ring(b, 2) | {a} + cells2 = cells1 | {b} + + poly1 = h3.cells_to_polygons(cells1) + poly2 = h3.cells_to_polygons(cells2) + + # unfortunately output order is nondeterministic + poly1 = sorted(map(str, poly1)) + poly2 = sorted(map(str, poly2)) + + assert poly1 == [ + '', + '', + ] + + assert poly2 == [ + '', + '', + '', + ] diff --git a/tests/test_to_multipoly.py b/tests/test_to_multipoly.py index 3de1360de..be2b94860 100644 --- a/tests/test_to_multipoly.py +++ b/tests/test_to_multipoly.py @@ -1,61 +1,32 @@ import h3 -def test_cells_to_multi_polygon(): +def test_cells_to_polygons(): h = '8928308280fffff' - hexes = h3.grid_disk(h, 1) + cells = h3.grid_disk(h, 1) - mpoly = h3.cells_to_multi_polygon(hexes) + polys = h3.cells_to_polygons(cells) + poly = polys[0] - out = h3.polyfill_polygon(mpoly[0][0], 9, holes=None, lnglat_order=False) + poly2 = h3.Polygon(poly.outer, *poly.holes) + out = h3.polygon_to_cells(poly2, 9) - assert out == hexes + assert out == cells def test_2_polys(): h = '8928308280fffff' - hexes = h3.grid_ring(h, 2) - hexes = hexes | {h} - # hexes should be a center hex, and the 2-ring around it + 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) + polys = h3.cells_to_polygons(cells) + out = [ - h3.polyfill_polygon(poly[0], 9, holes=poly[1:], lnglat_order=False) - for poly in h3.cells_to_multi_polygon(hexes, geo_json=False) + h3.polygon_to_cells(poly, 9) + for poly in polys ] - assert set.union(*out) == hexes - - -def test_2_polys_json(): - h = '8928308280fffff' - hexes = h3.grid_ring(h, 2) - hexes = hexes | {h} - # hexes should be a center hex, and the 2-ring around it - # (with the 1-ring being absent) - - # not deterministic which poly is first.. - poly1, poly2 = h3.cells_to_multi_polygon(hexes, geo_json=True) - - assert {len(poly1), len(poly2)} == {1, 2} - - for poly in poly1, poly2: - for loop in poly: - assert loop[0] == loop[-1] - - -def test_2_polys_not_json(): - h = '8928308280fffff' - hexes = h3.grid_ring(h, 2) - hexes = hexes | {h} - # hexes should be a center hex, and the 2-ring around it - # (with the 1-ring being absent) - - # not deterministic which poly is first.. - poly1, poly2 = h3.cells_to_multi_polygon(hexes, geo_json=False) - - assert {len(poly1), len(poly2)} == {1, 2} - - for poly in poly1, poly2: - for loop in poly: - assert loop[0] != loop[-1] + assert set.union(*out) == cells + assert set(map(len, out)) == {1, 12}