diff --git a/CHANGELOG.md b/CHANGELOG.md index 37cf91cf..268c7446 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,9 +14,9 @@ Because H3-Py is versioned in lockstep with the H3 core library, please avoid adding features or APIs which do not map onto the [H3 core API](https://uber.github.io/h3/#/documentation/api-reference/). -## Unreleased +## Unreleased - 2024-08-23 -- None +- Added bindings for `cellToVertex`, `cellToVertexes`, `vertexToLatLng`, and `isValidVertex` (#323) ## [4.0.0b5] - 2024-05-19 diff --git a/docs/api_quick.md b/docs/api_quick.md index d73f1144..0753ddb0 100644 --- a/docs/api_quick.md +++ b/docs/api_quick.md @@ -24,6 +24,7 @@ and should be generally aligned with the is_pentagon is_res_class_III is_valid_directed_edge + is_valid_vertex ``` ## Index representation @@ -108,6 +109,17 @@ Functions relating H3 objects to geographic (lat/lng) coordinates. get_directed_edge_origin ``` +## Vertexes + +```{eval-rst} +.. currentmodule:: h3 + +.. autosummary:: + cell_to_vertex + cell_to_vertexes + vertex_to_latlng +``` + ## Polygon interface The ``LatLngPoly`` and ``LatLngMultiPoly`` objects and their related functions allow users to represent (multi)polygons of lat/lng points and convert back and forth between H3 cells. diff --git a/src/h3/_cy/CMakeLists.txt b/src/h3/_cy/CMakeLists.txt index 5d392d2c..52e01561 100644 --- a/src/h3/_cy/CMakeLists.txt +++ b/src/h3/_cy/CMakeLists.txt @@ -24,6 +24,7 @@ add_cython_file(edges) add_cython_file(to_multipoly) add_cython_file(error_system) add_cython_file(memory) +add_cython_file(vertex) # Include pyx and pxd files in distribution for use by Cython API @@ -41,6 +42,8 @@ install( error_system.pyx memory.pxd memory.pyx + vertex.pxd + vertex.pyx DESTINATION src/h3/_cy ) diff --git a/src/h3/_cy/__init__.py b/src/h3/_cy/__init__.py index c208711e..c739bc04 100644 --- a/src/h3/_cy/__init__.py +++ b/src/h3/_cy/__init__.py @@ -60,6 +60,13 @@ great_circle_distance, ) +from .vertex import ( + cell_to_vertex, + cell_to_vertexes, + vertex_to_latlng, + is_valid_vertex, +) + from .to_multipoly import ( cells_to_multi_polygon ) diff --git a/src/h3/_cy/h3lib.pxd b/src/h3/_cy/h3lib.pxd index 00fff601..eae1b853 100644 --- a/src/h3/_cy/h3lib.pxd +++ b/src/h3/_cy/h3lib.pxd @@ -69,6 +69,7 @@ cdef extern from 'h3api.h': int isPentagon(H3int h) nogil int isResClassIII(H3int h) nogil int isValidDirectedEdge(H3int edge) nogil + int isValidVertex(H3int v) nogil double degsToRads(double degrees) nogil double radsToDegs(double radians) nogil @@ -80,6 +81,10 @@ cdef extern from 'h3api.h': H3Error cellToLatLng(H3int h, LatLng *) nogil H3Error gridDistance(H3int h1, H3int h2, int64_t *distance) nogil + H3Error cellToVertex(H3int cell, int vertexNum, H3int *out) nogil + H3Error cellToVertexes(H3int cell, H3int *vertexes) nogil + H3Error vertexToLatLng(H3int vertex, LatLng *coord) nogil + H3Error maxGridDiskSize(int k, int64_t *out) nogil # num/out/N? H3Error gridDisk(H3int h, int k, H3int *out) nogil diff --git a/src/h3/_cy/latlng.pyx b/src/h3/_cy/latlng.pyx index 520b950b..c3e0cdb8 100644 --- a/src/h3/_cy/latlng.pyx +++ b/src/h3/_cy/latlng.pyx @@ -8,7 +8,7 @@ from .util cimport ( check_edge, check_res, deg2coord, - coord2deg, + coord2deg ) from .error_system cimport check_for_error diff --git a/src/h3/_cy/util.pxd b/src/h3/_cy/util.pxd index 15e97ad0..4d7d962c 100644 --- a/src/h3/_cy/util.pxd +++ b/src/h3/_cy/util.pxd @@ -9,4 +9,5 @@ cpdef H3str int_to_str(H3int x) cdef check_cell(H3int h) cdef check_edge(H3int e) cdef check_res(int res) +cdef check_vertex(H3int v) cdef check_distance(int k) diff --git a/src/h3/_cy/util.pyx b/src/h3/_cy/util.pyx index 8bc061db..7b8a9f20 100644 --- a/src/h3/_cy/util.pyx +++ b/src/h3/_cy/util.pyx @@ -1,4 +1,4 @@ -from .h3lib cimport H3int, H3str, isValidCell, isValidDirectedEdge +from .h3lib cimport H3int, H3str, isValidCell, isValidDirectedEdge, isValidVertex cimport h3lib @@ -7,6 +7,7 @@ from .error_system import ( H3DomainError, H3DirEdgeInvalidError, H3CellInvalidError, + H3VertexInvalidError ) cdef h3lib.LatLng deg2coord(double lat, double lng) nogil: @@ -73,6 +74,10 @@ cdef check_edge(H3int e): if isValidDirectedEdge(e) == 0: raise H3DirEdgeInvalidError('Integer is not a valid H3 edge: {}'.format(hex(e))) +cdef check_vertex(H3int v): + if isValidVertex(v) == 0: + raise H3VertexInvalidError('Integer is not a valid H3 vertex: {}'.format(hex(v))) + cdef check_res(int res): if (res < 0) or (res > 15): raise H3ResDomainError(res) diff --git a/src/h3/_cy/vertex.pxd b/src/h3/_cy/vertex.pxd new file mode 100644 index 00000000..98d890ce --- /dev/null +++ b/src/h3/_cy/vertex.pxd @@ -0,0 +1,6 @@ +from .h3lib cimport bool, H3int + +cpdef H3int cell_to_vertex(H3int h, int vertex_num) except 1 +cpdef H3int[:] cell_to_vertexes(H3int h) +cpdef (double, double) vertex_to_latlng(H3int v) except * +cpdef bool is_valid_vertex(H3int v) diff --git a/src/h3/_cy/vertex.pyx b/src/h3/_cy/vertex.pyx new file mode 100644 index 00000000..26a674af --- /dev/null +++ b/src/h3/_cy/vertex.pyx @@ -0,0 +1,54 @@ +cimport h3lib +from h3lib cimport bool, H3int + +from .util cimport ( + check_cell, + check_vertex, + coord2deg +) + +from .error_system cimport check_for_error + +from .memory cimport H3MemoryManager + + +cpdef H3int cell_to_vertex(H3int h, int vertex_num) except 1: + cdef: + H3int out + + check_cell(h) + + check_for_error( + h3lib.cellToVertex(h, vertex_num, &out) + ) + + return out + +cpdef H3int[:] cell_to_vertexes(H3int h): + cdef: + H3int out + + check_cell(h) + + hmm = H3MemoryManager(6) + check_for_error( + h3lib.cellToVertexes(h, hmm.ptr) + ) + mv = hmm.to_mv() + + return mv + +cpdef (double, double) vertex_to_latlng(H3int v) except *: + cdef: + h3lib.LatLng c + + check_vertex(v) + + check_for_error( + h3lib.vertexToLatLng(v, &c) + ) + + return coord2deg(c) + +cpdef bool is_valid_vertex(H3int v): + return h3lib.isValidVertex(v) == 1 diff --git a/src/h3/api/basic_int/__init__.py b/src/h3/api/basic_int/__init__.py index d1122d83..81861897 100644 --- a/src/h3/api/basic_int/__init__.py +++ b/src/h3/api/basic_int/__init__.py @@ -996,3 +996,70 @@ def great_circle_distance(latlng1, latlng2, unit='km'): lat2, lng2, unit = unit ) + + +def cell_to_vertex(h, vertex_num): + """ + Return a (specified) vertex of an H3 cell. + + Parameters + ---------- + h : H3Cell + vertex_num : int + Vertex number (0-5) + + Returns + ------- + The vertex + """ + h = _in_scalar(h) + h = _cy.cell_to_vertex(h, vertex_num) + return _out_scalar(h) + + +def cell_to_vertexes(h): + """ + Return a list of vertexes of an H3 cell. + The list will be of length 5 for pentagons and 6 for hexagons. + + Parameters + ---------- + h : H3Cell + + Returns + ------- + A list of vertexes + """ + h = _in_scalar(h) + mv = _cy.cell_to_vertexes(h) + return _out_collection(mv) + + +def vertex_to_latlng(v): + """ + Return latitude and longitude of a vertex. + + Returns + ------- + lat : float + Latitude + lng : float + Longitude + """ + v = _in_scalar(v) + return _cy.vertex_to_latlng(v) + + +def is_valid_vertex(v): + """ + Validates an H3 vertex. + + Returns + ------- + bool + """ + try: + v = _in_scalar(v) + return _cy.is_valid_vertex(v) + except (ValueError, TypeError): + return False diff --git a/tests/test_h3.py b/tests/test_h3.py index 186145fa..567ba1fa 100644 --- a/tests/test_h3.py +++ b/tests/test_h3.py @@ -387,3 +387,58 @@ def test_grid_path_cells(): with pytest.raises(h3.H3ResMismatchError): h3.grid_path_cells(h1, '8001fffffffffff') + + +def test_cell_to_vertex(): + # pentagon + assert h3.cell_to_vertex('814c3ffffffffff', 0) == '2014c3ffffffffff' + assert h3.cell_to_vertex('814c3ffffffffff', 1) == '2114c3ffffffffff' + assert h3.cell_to_vertex('814c3ffffffffff', 2) == '2214c3ffffffffff' + assert h3.cell_to_vertex('814c3ffffffffff', 3) == '2314c3ffffffffff' + assert h3.cell_to_vertex('814c3ffffffffff', 4) == '2414c3ffffffffff' + try: + h3.cell_to_vertex('814c3ffffffffff', 5) + except h3._cy.error_system.H3DomainError: + pass + else: + assert False + + # hexagon + assert h3.cell_to_vertex('814d7ffffffffff', 0) == '2014d7ffffffffff' + assert h3.cell_to_vertex('814d7ffffffffff', 1) == '2213abffffffffff' + assert h3.cell_to_vertex('814d7ffffffffff', 2) == '2113abffffffffff' + assert h3.cell_to_vertex('814d7ffffffffff', 3) == '2414c3ffffffffff' + assert h3.cell_to_vertex('814d7ffffffffff', 4) == '2314c3ffffffffff' + assert h3.cell_to_vertex('814d7ffffffffff', 5) == '2414cfffffffffff' + + +def test_cell_to_vertexes(): + # pentagon + assert h3.cell_to_vertexes('814c3ffffffffff') == [ + '2014c3ffffffffff', + '2114c3ffffffffff', + '2214c3ffffffffff', + '2314c3ffffffffff', + '2414c3ffffffffff', + ] + + # hexagon + assert h3.cell_to_vertexes('814d7ffffffffff') == [ + '2014d7ffffffffff', + '2213abffffffffff', + '2113abffffffffff', + '2414c3ffffffffff', + '2314c3ffffffffff', + '2414cfffffffffff', + ] + + +def test_vertex_to_latlng(): + latlng = h3.vertex_to_latlng('2114c3ffffffffff') + assert latlng == approx((24.945215618732814, -70.33904370008679)) + + +def test_is_valid_vertex(): + assert h3.is_valid_vertex('2114c3ffffffffff') + assert not h3.is_valid_vertex(2455495337847029759) + assert not h3.is_valid_vertex('foobar')