diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ec73962..4c60e19f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,7 +16,7 @@ avoid adding features or APIs which do not map onto the ## Unreleased -None. +- Update to v4.2.0. (#432) ## [4.1.2] - 2024-10-26 diff --git a/pyproject.toml b/pyproject.toml index 50b5dc61..93cb37d3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = 'scikit_build_core.build' [project] name = 'h3' -version = '4.1.2' +version = '4.2.0' description = "Uber's hierarchical hexagonal geospatial indexing system" readme = 'readme.md' license = {file = 'LICENSE'} diff --git a/src/h3/_cy/__init__.py b/src/h3/_cy/__init__.py index 804b6b0e..627f3b97 100644 --- a/src/h3/_cy/__init__.py +++ b/src/h3/_cy/__init__.py @@ -58,6 +58,8 @@ cell_to_latlng, polygon_to_cells, polygons_to_cells, + polygon_to_cells_experimental, + polygons_to_cells_experimental, cell_to_boundary, directed_edge_to_boundary, great_circle_distance, diff --git a/src/h3/_cy/h3lib.pxd b/src/h3/_cy/h3lib.pxd index e91cc2df..4a483b2e 100644 --- a/src/h3/_cy/h3lib.pxd +++ b/src/h3/_cy/h3lib.pxd @@ -168,6 +168,9 @@ cdef extern from 'h3api.h': H3Error maxPolygonToCellsSize(const GeoPolygon *geoPolygon, int res, uint32_t flags, uint64_t *count) H3Error polygonToCells(const GeoPolygon *geoPolygon, int res, uint32_t flags, H3int *out) + H3Error maxPolygonToCellsSizeExperimental(const GeoPolygon *geoPolygon, int res, uint32_t flags, uint64_t *count) + H3Error polygonToCellsExperimental(const GeoPolygon *geoPolygon, int res, uint32_t flags, uint64_t sz, H3int *out) + # ctypedef struct GeoMultiPolygon: # int numPolygons # GeoPolygon *polygons diff --git a/src/h3/_cy/latlng.pyx b/src/h3/_cy/latlng.pyx index 493b85ac..60b9053f 100644 --- a/src/h3/_cy/latlng.pyx +++ b/src/h3/_cy/latlng.pyx @@ -196,6 +196,80 @@ def polygons_to_cells(polygons, int res): return hmm.to_mv() +def polygon_to_cells_experimental(outer, int res, int flags, holes=None): + """ Get the set of cells whose center is contained in a polygon. + + The polygon is defined similarity to the GeoJson standard, with an exterior + `outer` ring of lat/lng points, and a list of `holes`, each of which are also + rings of lat/lng points. + + Each ring may be in clockwise or counter-clockwise order + (right-hand rule or not), and may or may not be a closed loop (where the last + element is equal to the first). + The GeoJSON spec requires the right-hand rule and a closed loop, but + this function relaxes those constraints. + + Unlike the GeoJson standard, the elements of the lat/lng pairs of each + ring are in lat/lng order, instead of lng/lat order. + + We'll handle translation to different formats in the Python code, + rather than the Cython code. + + Parameters + ---------- + outer : list or tuple + A ring given by a sequence of lat/lng pairs. + res : int + The resolution of the output hexagons + flags : int + Polygon to cells flags, such as containment mode. + holes : list or tuple + A collection of rings, each given by a sequence of lat/lng pairs. + These describe any the "holes" in the polygon. + """ + cdef: + uint64_t n + + check_res(res) + + if not outer: + return H3MemoryManager(0).to_mv() + + gp = GeoPolygon(outer, holes=holes) + + check_for_error( + h3lib.maxPolygonToCellsSizeExperimental(&gp.gp, res, flags, &n) + ) + + hmm = H3MemoryManager(n) + check_for_error( + h3lib.polygonToCellsExperimental(&gp.gp, res, flags, n, hmm.ptr) + ) + mv = hmm.to_mv() + + return mv + + +def polygons_to_cells_experimental(polygons, int res, int flags): + mvs = [ + polygon_to_cells_experimental(outer=poly.outer, res=res, holes=poly.holes, flags=flags) + for poly in polygons + ] + + n = sum(map(len, mvs)) + hmm = H3MemoryManager(n) + + # probably super inefficient, but it is working! + # tood: move this to C + k = 0 + for mv in mvs: + for v in mv: + hmm.ptr[k] = v + k += 1 + + return hmm.to_mv() + + def cell_to_boundary(H3int h): """Compose an array of geo-coordinates that outlines a hexagonal cell""" cdef: diff --git a/src/h3/_h3shape.py b/src/h3/_h3shape.py index 5c362951..261dfa92 100644 --- a/src/h3/_h3shape.py +++ b/src/h3/_h3shape.py @@ -1,4 +1,15 @@ from abc import ABCMeta, abstractmethod +from enum import Enum + + +class ContainmentMode(int, Enum): + """ + Containment modes for use with ``polygon_to_cells_experimental``. + """ + containment_center = 0 + containment_full = 1 + containment_overlapping = 2 + containment_overlapping_bbox = 3 class H3Shape(metaclass=ABCMeta): diff --git a/src/h3/api/basic_int/__init__.py b/src/h3/api/basic_int/__init__.py index 60359662..cf450c89 100644 --- a/src/h3/api/basic_int/__init__.py +++ b/src/h3/api/basic_int/__init__.py @@ -2,6 +2,7 @@ from ... import _cy from ..._h3shape import ( + ContainmentMode, H3Shape, LatLngPoly, LatLngMultiPoly, @@ -506,10 +507,10 @@ def h3shape_to_cells(h3shape, res): # todo: not sure if i want this dispatch logic here. maybe in the objects? if isinstance(h3shape, LatLngPoly): poly = h3shape - mv = _cy.polygon_to_cells(poly.outer, res, holes=poly.holes) + mv = _cy.polygon_to_cells(poly.outer, res=res, holes=poly.holes) elif isinstance(h3shape, LatLngMultiPoly): mpoly = h3shape - mv = _cy.polygons_to_cells(mpoly.polys, res) + mv = _cy.polygons_to_cells(mpoly.polys, res=res) elif isinstance(h3shape, H3Shape): raise ValueError('Unrecognized H3Shape: ' + str(h3shape)) else: @@ -525,6 +526,83 @@ def polygon_to_cells(h3shape, res): return h3shape_to_cells(h3shape, res) +def h3shape_to_cells_experimental(h3shape, res, flags=0): + """ + Return the collection of H3 cells at a given resolution whose center points + are contained within an ``LatLngPoly`` or ``LatLngMultiPoly``. + + Parameters + ---------- + h3shape : ``H3Shape`` + res : int + Resolution of the output cells + flags : ``ContainmentMode``, int, or string + Containment mode flags + + Returns + ------- + list of H3Cell + + Examples + -------- + + >>> poly = LatLngPoly( + ... [(37.68, -122.54), (37.68, -122.34), (37.82, -122.34), + ... (37.82, -122.54)], + ... ) + >>> h3.h3shape_to_cells_experimental(poly, 6, h3.ContainmentMode.containment_center) + ['862830807ffffff', + '862830827ffffff', + '86283082fffffff', + '862830877ffffff', + '862830947ffffff', + '862830957ffffff', + '86283095fffffff'] + + Notes + ----- + There is currently no guaranteed order of the output cells. + """ + + if isinstance(flags, str): + try: + flags = ContainmentMode[flags] + except KeyError as e: + raise ValueError('Unrecognized flags: ' + flags) from e + if isinstance(flags, ContainmentMode): + flags = int(flags) + if not isinstance(flags, int): + raise ValueError( + 'Flags should be ContainmentMode, str, or int, but got: ' + str(type(flags)) + ) + + # todo: not sure if i want this dispatch logic here. maybe in the objects? + if isinstance(h3shape, LatLngPoly): + poly = h3shape + mv = _cy.polygon_to_cells_experimental( + poly.outer, + res=res, + holes=poly.holes, + flags=flags + ) + elif isinstance(h3shape, LatLngMultiPoly): + mpoly = h3shape + mv = _cy.polygons_to_cells_experimental(mpoly.polys, res=res, flags=flags) + elif isinstance(h3shape, H3Shape): + raise ValueError('Unrecognized H3Shape: ' + str(h3shape)) + else: + raise ValueError('Unrecognized type: ' + str(type(h3shape))) + + return _out_collection(mv) + + +def polygon_to_cells_experimental(h3shape, res, flags=0): + """ + Alias for ``h3shape_to_cells_experimental``. + """ + return h3shape_to_cells_experimental(h3shape, res, flags=flags) + + def cells_to_h3shape(cells, *, tight=True): """ Return an ``H3Shape`` describing the area covered by a collection of H3 cells. diff --git a/src/h3lib b/src/h3lib index 5c911491..2d8a62d5 160000 --- a/src/h3lib +++ b/src/h3lib @@ -1 +1 @@ -Subproject commit 5c91149104ac02c4f06faa4fc557e69cf6b131ef +Subproject commit 2d8a62d5fe456bba2af7ca0be7efe9754f1dd25b diff --git a/tests/polyfill/test_h3.py b/tests/polyfill/test_h3.py index 8d9d9480..50728d35 100644 --- a/tests/polyfill/test_h3.py +++ b/tests/polyfill/test_h3.py @@ -117,6 +117,68 @@ def test_polygon_to_cells(): assert '89283095edbffff' in out +def test_polygon_to_cells_experimental(): + poly = h3.LatLngPoly(sf_7x7) + for flags in [0, 'containment_center', h3.ContainmentMode.containment_center]: + # Note that `polygon_to_cells` is an alias for `h3shape_to_cells` + out = h3.polygon_to_cells_experimental(poly, res=9, flags=flags) + + assert len(out) == 1253 + assert '89283080527ffff' in out + assert '89283095edbffff' in out + + +def test_polygon_to_cells_experimental_full(): + poly = h3.LatLngPoly(sf_7x7) + for flags in [1, 'containment_full', h3.ContainmentMode.containment_full]: + # Note that `polygon_to_cells` is an alias for `h3shape_to_cells` + out = h3.polygon_to_cells_experimental(poly, res=9, flags=flags) + + assert len(out) == 1175 + assert '89283082a1bffff' in out + assert '89283080527ffff' not in out + assert '89283095edbffff' in out + + +def test_polygon_to_cells_experimental_overlapping(): + poly = h3.LatLngPoly(sf_7x7) + for flags in [ + 2, + 'containment_overlapping', + h3.ContainmentMode.containment_overlapping + ]: + # Note that `polygon_to_cells` is an alias for `h3shape_to_cells` + out = h3.polygon_to_cells_experimental(poly, res=9, flags=flags) + + assert len(out) == 1334 + assert '89283080527ffff' in out + assert '89283095edbffff' in out + + +def test_polygon_to_cells_experimental_overlapping_bbox(): + poly = h3.LatLngPoly(sf_7x7) + for flags in [ + 3, + 'containment_overlapping_bbox', + h3.ContainmentMode.containment_overlapping_bbox + ]: + # Note that `polygon_to_cells` is an alias for `h3shape_to_cells` + out = h3.polygon_to_cells_experimental(poly, res=9, flags=flags) + + assert len(out) == 1416 + assert '89283080527ffff' in out + assert '89283095edbffff' in out + + +def test_polygon_to_cells_experimental_invalid_mode(): + poly = h3.LatLngPoly(sf_7x7) + for flags in [1.0, 'containment_overlapping_bbox_abc', None]: + with pytest.raises(ValueError): + print(flags) + # Note that `polygon_to_cells` is an alias for `h3shape_to_cells` + h3.polygon_to_cells_experimental(poly, res=9, flags=flags) + + def test_polyfill_with_hole(): poly = h3.LatLngPoly(sf_7x7, sf_hole1) diff --git a/tests/test_cells_and_edges.py b/tests/test_cells_and_edges.py index d23ff4d6..f918019a 100644 --- a/tests/test_cells_and_edges.py +++ b/tests/test_cells_and_edges.py @@ -289,11 +289,11 @@ def test_average_hexagon_area(): def test_average_hexagon_edge_length(): expected_in_km = { - 0: 1107.712591000, - 1: 418.676005500, - 2: 158.244655800, - 9: 0.174375668, - 15: 0.000509713, + 0: 1281.256011, + 1: 483.0568391, + 2: 182.5129565, + 9: 0.200786148, + 15: 0.000584169, } out = {