Skip to content

Commit

Permalink
upgrade to h3 v4.2.0 (#432)
Browse files Browse the repository at this point in the history
* upgrade to h3 v4.2.0

* fix lint

* pr number

* split out experimental function to new function

* docs
  • Loading branch information
isaacbrodsky authored Dec 29, 2024
1 parent e9506ff commit ef6e897
Show file tree
Hide file tree
Showing 10 changed files with 240 additions and 10 deletions.
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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'}
Expand Down
2 changes: 2 additions & 0 deletions src/h3/_cy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
3 changes: 3 additions & 0 deletions src/h3/_cy/h3lib.pxd
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
74 changes: 74 additions & 0 deletions src/h3/_cy/latlng.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
11 changes: 11 additions & 0 deletions src/h3/_h3shape.py
Original file line number Diff line number Diff line change
@@ -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):
Expand Down
82 changes: 80 additions & 2 deletions src/h3/api/basic_int/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from ... import _cy
from ..._h3shape import (
ContainmentMode,
H3Shape,
LatLngPoly,
LatLngMultiPoly,
Expand Down Expand Up @@ -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:
Expand All @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion src/h3lib
Submodule h3lib updated 186 files
62 changes: 62 additions & 0 deletions tests/polyfill/test_h3.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
10 changes: 5 additions & 5 deletions tests/test_cells_and_edges.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down

0 comments on commit ef6e897

Please sign in to comment.