Skip to content

Commit ef6e897

Browse files
authored
upgrade to h3 v4.2.0 (#432)
* upgrade to h3 v4.2.0 * fix lint * pr number * split out experimental function to new function * docs
1 parent e9506ff commit ef6e897

File tree

10 files changed

+240
-10
lines changed

10 files changed

+240
-10
lines changed

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ avoid adding features or APIs which do not map onto the
1616

1717
## Unreleased
1818

19-
None.
19+
- Update to v4.2.0. (#432)
2020

2121
## [4.1.2] - 2024-10-26
2222

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = 'scikit_build_core.build'
44

55
[project]
66
name = 'h3'
7-
version = '4.1.2'
7+
version = '4.2.0'
88
description = "Uber's hierarchical hexagonal geospatial indexing system"
99
readme = 'readme.md'
1010
license = {file = 'LICENSE'}

src/h3/_cy/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@
5858
cell_to_latlng,
5959
polygon_to_cells,
6060
polygons_to_cells,
61+
polygon_to_cells_experimental,
62+
polygons_to_cells_experimental,
6163
cell_to_boundary,
6264
directed_edge_to_boundary,
6365
great_circle_distance,

src/h3/_cy/h3lib.pxd

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,9 @@ cdef extern from 'h3api.h':
168168
H3Error maxPolygonToCellsSize(const GeoPolygon *geoPolygon, int res, uint32_t flags, uint64_t *count)
169169
H3Error polygonToCells(const GeoPolygon *geoPolygon, int res, uint32_t flags, H3int *out)
170170

171+
H3Error maxPolygonToCellsSizeExperimental(const GeoPolygon *geoPolygon, int res, uint32_t flags, uint64_t *count)
172+
H3Error polygonToCellsExperimental(const GeoPolygon *geoPolygon, int res, uint32_t flags, uint64_t sz, H3int *out)
173+
171174
# ctypedef struct GeoMultiPolygon:
172175
# int numPolygons
173176
# GeoPolygon *polygons

src/h3/_cy/latlng.pyx

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,80 @@ def polygons_to_cells(polygons, int res):
196196
return hmm.to_mv()
197197

198198

199+
def polygon_to_cells_experimental(outer, int res, int flags, holes=None):
200+
""" Get the set of cells whose center is contained in a polygon.
201+
202+
The polygon is defined similarity to the GeoJson standard, with an exterior
203+
`outer` ring of lat/lng points, and a list of `holes`, each of which are also
204+
rings of lat/lng points.
205+
206+
Each ring may be in clockwise or counter-clockwise order
207+
(right-hand rule or not), and may or may not be a closed loop (where the last
208+
element is equal to the first).
209+
The GeoJSON spec requires the right-hand rule and a closed loop, but
210+
this function relaxes those constraints.
211+
212+
Unlike the GeoJson standard, the elements of the lat/lng pairs of each
213+
ring are in lat/lng order, instead of lng/lat order.
214+
215+
We'll handle translation to different formats in the Python code,
216+
rather than the Cython code.
217+
218+
Parameters
219+
----------
220+
outer : list or tuple
221+
A ring given by a sequence of lat/lng pairs.
222+
res : int
223+
The resolution of the output hexagons
224+
flags : int
225+
Polygon to cells flags, such as containment mode.
226+
holes : list or tuple
227+
A collection of rings, each given by a sequence of lat/lng pairs.
228+
These describe any the "holes" in the polygon.
229+
"""
230+
cdef:
231+
uint64_t n
232+
233+
check_res(res)
234+
235+
if not outer:
236+
return H3MemoryManager(0).to_mv()
237+
238+
gp = GeoPolygon(outer, holes=holes)
239+
240+
check_for_error(
241+
h3lib.maxPolygonToCellsSizeExperimental(&gp.gp, res, flags, &n)
242+
)
243+
244+
hmm = H3MemoryManager(n)
245+
check_for_error(
246+
h3lib.polygonToCellsExperimental(&gp.gp, res, flags, n, hmm.ptr)
247+
)
248+
mv = hmm.to_mv()
249+
250+
return mv
251+
252+
253+
def polygons_to_cells_experimental(polygons, int res, int flags):
254+
mvs = [
255+
polygon_to_cells_experimental(outer=poly.outer, res=res, holes=poly.holes, flags=flags)
256+
for poly in polygons
257+
]
258+
259+
n = sum(map(len, mvs))
260+
hmm = H3MemoryManager(n)
261+
262+
# probably super inefficient, but it is working!
263+
# tood: move this to C
264+
k = 0
265+
for mv in mvs:
266+
for v in mv:
267+
hmm.ptr[k] = v
268+
k += 1
269+
270+
return hmm.to_mv()
271+
272+
199273
def cell_to_boundary(H3int h):
200274
"""Compose an array of geo-coordinates that outlines a hexagonal cell"""
201275
cdef:

src/h3/_h3shape.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,15 @@
11
from abc import ABCMeta, abstractmethod
2+
from enum import Enum
3+
4+
5+
class ContainmentMode(int, Enum):
6+
"""
7+
Containment modes for use with ``polygon_to_cells_experimental``.
8+
"""
9+
containment_center = 0
10+
containment_full = 1
11+
containment_overlapping = 2
12+
containment_overlapping_bbox = 3
213

314

415
class H3Shape(metaclass=ABCMeta):

src/h3/api/basic_int/__init__.py

Lines changed: 80 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from ... import _cy
44
from ..._h3shape import (
5+
ContainmentMode,
56
H3Shape,
67
LatLngPoly,
78
LatLngMultiPoly,
@@ -506,10 +507,10 @@ def h3shape_to_cells(h3shape, res):
506507
# todo: not sure if i want this dispatch logic here. maybe in the objects?
507508
if isinstance(h3shape, LatLngPoly):
508509
poly = h3shape
509-
mv = _cy.polygon_to_cells(poly.outer, res, holes=poly.holes)
510+
mv = _cy.polygon_to_cells(poly.outer, res=res, holes=poly.holes)
510511
elif isinstance(h3shape, LatLngMultiPoly):
511512
mpoly = h3shape
512-
mv = _cy.polygons_to_cells(mpoly.polys, res)
513+
mv = _cy.polygons_to_cells(mpoly.polys, res=res)
513514
elif isinstance(h3shape, H3Shape):
514515
raise ValueError('Unrecognized H3Shape: ' + str(h3shape))
515516
else:
@@ -525,6 +526,83 @@ def polygon_to_cells(h3shape, res):
525526
return h3shape_to_cells(h3shape, res)
526527

527528

529+
def h3shape_to_cells_experimental(h3shape, res, flags=0):
530+
"""
531+
Return the collection of H3 cells at a given resolution whose center points
532+
are contained within an ``LatLngPoly`` or ``LatLngMultiPoly``.
533+
534+
Parameters
535+
----------
536+
h3shape : ``H3Shape``
537+
res : int
538+
Resolution of the output cells
539+
flags : ``ContainmentMode``, int, or string
540+
Containment mode flags
541+
542+
Returns
543+
-------
544+
list of H3Cell
545+
546+
Examples
547+
--------
548+
549+
>>> poly = LatLngPoly(
550+
... [(37.68, -122.54), (37.68, -122.34), (37.82, -122.34),
551+
... (37.82, -122.54)],
552+
... )
553+
>>> h3.h3shape_to_cells_experimental(poly, 6, h3.ContainmentMode.containment_center)
554+
['862830807ffffff',
555+
'862830827ffffff',
556+
'86283082fffffff',
557+
'862830877ffffff',
558+
'862830947ffffff',
559+
'862830957ffffff',
560+
'86283095fffffff']
561+
562+
Notes
563+
-----
564+
There is currently no guaranteed order of the output cells.
565+
"""
566+
567+
if isinstance(flags, str):
568+
try:
569+
flags = ContainmentMode[flags]
570+
except KeyError as e:
571+
raise ValueError('Unrecognized flags: ' + flags) from e
572+
if isinstance(flags, ContainmentMode):
573+
flags = int(flags)
574+
if not isinstance(flags, int):
575+
raise ValueError(
576+
'Flags should be ContainmentMode, str, or int, but got: ' + str(type(flags))
577+
)
578+
579+
# todo: not sure if i want this dispatch logic here. maybe in the objects?
580+
if isinstance(h3shape, LatLngPoly):
581+
poly = h3shape
582+
mv = _cy.polygon_to_cells_experimental(
583+
poly.outer,
584+
res=res,
585+
holes=poly.holes,
586+
flags=flags
587+
)
588+
elif isinstance(h3shape, LatLngMultiPoly):
589+
mpoly = h3shape
590+
mv = _cy.polygons_to_cells_experimental(mpoly.polys, res=res, flags=flags)
591+
elif isinstance(h3shape, H3Shape):
592+
raise ValueError('Unrecognized H3Shape: ' + str(h3shape))
593+
else:
594+
raise ValueError('Unrecognized type: ' + str(type(h3shape)))
595+
596+
return _out_collection(mv)
597+
598+
599+
def polygon_to_cells_experimental(h3shape, res, flags=0):
600+
"""
601+
Alias for ``h3shape_to_cells_experimental``.
602+
"""
603+
return h3shape_to_cells_experimental(h3shape, res, flags=flags)
604+
605+
528606
def cells_to_h3shape(cells, *, tight=True):
529607
"""
530608
Return an ``H3Shape`` describing the area covered by a collection of H3 cells.

src/h3lib

Submodule h3lib updated 186 files

tests/polyfill/test_h3.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,68 @@ def test_polygon_to_cells():
117117
assert '89283095edbffff' in out
118118

119119

120+
def test_polygon_to_cells_experimental():
121+
poly = h3.LatLngPoly(sf_7x7)
122+
for flags in [0, 'containment_center', h3.ContainmentMode.containment_center]:
123+
# Note that `polygon_to_cells` is an alias for `h3shape_to_cells`
124+
out = h3.polygon_to_cells_experimental(poly, res=9, flags=flags)
125+
126+
assert len(out) == 1253
127+
assert '89283080527ffff' in out
128+
assert '89283095edbffff' in out
129+
130+
131+
def test_polygon_to_cells_experimental_full():
132+
poly = h3.LatLngPoly(sf_7x7)
133+
for flags in [1, 'containment_full', h3.ContainmentMode.containment_full]:
134+
# Note that `polygon_to_cells` is an alias for `h3shape_to_cells`
135+
out = h3.polygon_to_cells_experimental(poly, res=9, flags=flags)
136+
137+
assert len(out) == 1175
138+
assert '89283082a1bffff' in out
139+
assert '89283080527ffff' not in out
140+
assert '89283095edbffff' in out
141+
142+
143+
def test_polygon_to_cells_experimental_overlapping():
144+
poly = h3.LatLngPoly(sf_7x7)
145+
for flags in [
146+
2,
147+
'containment_overlapping',
148+
h3.ContainmentMode.containment_overlapping
149+
]:
150+
# Note that `polygon_to_cells` is an alias for `h3shape_to_cells`
151+
out = h3.polygon_to_cells_experimental(poly, res=9, flags=flags)
152+
153+
assert len(out) == 1334
154+
assert '89283080527ffff' in out
155+
assert '89283095edbffff' in out
156+
157+
158+
def test_polygon_to_cells_experimental_overlapping_bbox():
159+
poly = h3.LatLngPoly(sf_7x7)
160+
for flags in [
161+
3,
162+
'containment_overlapping_bbox',
163+
h3.ContainmentMode.containment_overlapping_bbox
164+
]:
165+
# Note that `polygon_to_cells` is an alias for `h3shape_to_cells`
166+
out = h3.polygon_to_cells_experimental(poly, res=9, flags=flags)
167+
168+
assert len(out) == 1416
169+
assert '89283080527ffff' in out
170+
assert '89283095edbffff' in out
171+
172+
173+
def test_polygon_to_cells_experimental_invalid_mode():
174+
poly = h3.LatLngPoly(sf_7x7)
175+
for flags in [1.0, 'containment_overlapping_bbox_abc', None]:
176+
with pytest.raises(ValueError):
177+
print(flags)
178+
# Note that `polygon_to_cells` is an alias for `h3shape_to_cells`
179+
h3.polygon_to_cells_experimental(poly, res=9, flags=flags)
180+
181+
120182
def test_polyfill_with_hole():
121183
poly = h3.LatLngPoly(sf_7x7, sf_hole1)
122184

tests/test_cells_and_edges.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -289,11 +289,11 @@ def test_average_hexagon_area():
289289

290290
def test_average_hexagon_edge_length():
291291
expected_in_km = {
292-
0: 1107.712591000,
293-
1: 418.676005500,
294-
2: 158.244655800,
295-
9: 0.174375668,
296-
15: 0.000509713,
292+
0: 1281.256011,
293+
1: 483.0568391,
294+
2: 182.5129565,
295+
9: 0.200786148,
296+
15: 0.000584169,
297297
}
298298

299299
out = {

0 commit comments

Comments
 (0)