Skip to content

Commit 8d1663b

Browse files
authored
feat: Update to H3 version 4 (#37)
1 parent d879c8a commit 8d1663b

File tree

7 files changed

+133
-115
lines changed

7 files changed

+133
-115
lines changed

environment.yml

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,8 @@ dependencies:
66
- shapely
77
- geopandas>=0.9.*
88
- pandas
9+
- h3-py>=4
910
# Notebooks
1011
- matplotlib
1112
# Pip
1213
- pip
13-
- pip:
14-
# Installing through pip to avoid segfault on Apple Silicon
15-
# https://github.com/uber/h3-py/issues/313
16-
- h3==3.7.6

h3pandas/h3pandas.py

Lines changed: 18 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,14 @@
88
import pandas as pd
99
import geopandas as gpd
1010

11-
from h3 import h3
11+
import h3
1212
from pandas.core.frame import DataFrame
1313
from geopandas.geodataframe import GeoDataFrame
1414

1515
from .const import COLUMN_H3_POLYFILL, COLUMN_H3_LINETRACE
1616
from .util.decorator import catch_invalid_h3_address, doc_standard
1717
from .util.functools import wrapped_partial
18-
from .util.shapely import polyfill, linetrace
18+
from .util.shapely import cell_to_boundary_lng_lat, polyfill, linetrace, _switch_lat_lng
1919

2020
AnyDataFrame = Union[DataFrame, GeoDataFrame]
2121

@@ -92,7 +92,7 @@ def geo_to_h3(
9292
lats = self._df[lat_col]
9393

9494
h3addresses = [
95-
h3.geo_to_h3(lat, lng, resolution) for lat, lng in zip(lats, lngs)
95+
h3.latlng_to_cell(lat, lng, resolution) for lat, lng in zip(lats, lngs)
9696
]
9797

9898
colname = self._format_resolution(resolution)
@@ -130,9 +130,9 @@ def h3_to_geo(self) -> GeoDataFrame:
130130
131131
"""
132132
return self._apply_index_assign(
133-
h3.h3_to_geo,
133+
h3.cell_to_latlng,
134134
"geometry",
135-
lambda x: shapely.geometry.Point(reversed(x)),
135+
lambda x: _switch_lat_lng(shapely.geometry.Point(x)),
136136
lambda x: gpd.GeoDataFrame(x, crs="epsg:4326"),
137137
)
138138

@@ -158,10 +158,9 @@ def h3_to_geo_boundary(self) -> GeoDataFrame:
158158
881e2659c3fffff 1 POLYGON ((14.99201 51.00565, 14.98973 51.00133...
159159
"""
160160
return self._apply_index_assign(
161-
wrapped_partial(h3.h3_to_geo_boundary, geo_json=True),
161+
wrapped_partial(cell_to_boundary_lng_lat),
162162
"geometry",
163-
lambda x: shapely.geometry.Polygon(x),
164-
lambda x: gpd.GeoDataFrame(x, crs="epsg:4326"),
163+
finalizer=lambda x: gpd.GeoDataFrame(x, crs="epsg:4326"),
165164
)
166165

167166
@doc_standard("h3_resolution", "containing the resolution of each H3 address")
@@ -176,7 +175,7 @@ def h3_get_resolution(self) -> AnyDataFrame:
176175
881e309739fffff 5 8
177176
881e2659c3fffff 1 8
178177
"""
179-
return self._apply_index_assign(h3.h3_get_resolution, "h3_resolution")
178+
return self._apply_index_assign(h3.get_resolution, "h3_resolution")
180179

181180
@doc_standard("h3_base_cell", "containing the base cell of each H3 address")
182181
def h3_get_base_cell(self):
@@ -190,7 +189,7 @@ def h3_get_base_cell(self):
190189
881e309739fffff 5 15
191190
881e2659c3fffff 1 15
192191
"""
193-
return self._apply_index_assign(h3.h3_get_base_cell, "h3_base_cell")
192+
return self._apply_index_assign(h3.get_base_cell_number, "h3_base_cell")
194193

195194
@doc_standard("h3_is_valid", "containing the validity of each H3 address")
196195
def h3_is_valid(self):
@@ -203,7 +202,7 @@ def h3_is_valid(self):
203202
881e309739fffff 5 True
204203
INVALID 1 False
205204
"""
206-
return self._apply_index_assign(h3.h3_is_valid, "h3_is_valid")
205+
return self._apply_index_assign(h3.is_valid_cell, "h3_is_valid")
207206

208207
@doc_standard(
209208
"h3_k_ring", "containing a list H3 addresses within a distance of `k`"
@@ -250,7 +249,7 @@ def k_ring(self, k: int = 1, explode: bool = False) -> AnyDataFrame:
250249
881e309739fffff 5 881e309739fffff
251250
881e309739fffff 5 881e309731fffff
252251
"""
253-
func = wrapped_partial(h3.k_ring, k=k)
252+
func = wrapped_partial(h3.grid_disk, k=k)
254253
column_name = "h3_k_ring"
255254
if explode:
256255
return self._apply_index_explode(func, column_name, list)
@@ -295,7 +294,7 @@ def hex_ring(self, k: int = 1, explode: bool = False) -> AnyDataFrame:
295294
881e309739fffff 5 881e309715fffff
296295
881e309739fffff 5 881e309731fffff
297296
"""
298-
func = wrapped_partial(h3.hex_ring, k=k)
297+
func = wrapped_partial(h3.grid_ring, k=k)
299298
column_name = "h3_hex_ring"
300299
if explode:
301300
return self._apply_index_explode(func, column_name, list)
@@ -330,7 +329,7 @@ def h3_to_parent(self, resolution: int = None) -> AnyDataFrame:
330329
else "h3_parent"
331330
)
332331
return self._apply_index_assign(
333-
wrapped_partial(h3.h3_to_parent, res=resolution), column
332+
wrapped_partial(h3.cell_to_parent, res=resolution), column
334333
)
335334

336335
@doc_standard("h3_center_child", "containing the center child of each H3 address")
@@ -352,7 +351,7 @@ def h3_to_center_child(self, resolution: int = None) -> AnyDataFrame:
352351
881e2659c3fffff 1 891e2659c23ffff
353352
"""
354353
return self._apply_index_assign(
355-
wrapped_partial(h3.h3_to_center_child, res=resolution), "h3_center_child"
354+
wrapped_partial(h3.cell_to_center_child, res=resolution), "h3_center_child"
356355
)
357356

358357
@doc_standard(
@@ -395,7 +394,7 @@ def polyfill(self, resolution: int, explode: bool = False) -> AnyDataFrame:
395394
"""
396395

397396
def func(row):
398-
return list(polyfill(row.geometry, resolution, True))
397+
return list(polyfill(row.geometry, resolution))
399398

400399
result = self._df.apply(func, axis=1)
401400

@@ -553,7 +552,7 @@ def h3_to_parent_aggregate(
553552
811e3ffffffffff 6
554553
"""
555554
parent_h3addresses = [
556-
catch_invalid_h3_address(h3.h3_to_parent)(h3address, resolution)
555+
catch_invalid_h3_address(h3.cell_to_parent)(h3address, resolution)
557556
for h3address in self._df.index
558557
]
559558
h3_parent_column = self._format_resolution(resolution)
@@ -758,9 +757,7 @@ def polyfill_resample(
758757

759758
return result.h3.h3_to_geo_boundary() if return_geometry else result
760759

761-
def linetrace(
762-
self, resolution : int, explode: bool = False
763-
) -> AnyDataFrame:
760+
def linetrace(self, resolution: int, explode: bool = False) -> AnyDataFrame:
764761
"""Experimental. An H3 cell representation of a (Multi)LineString,
765762
which permits repeated cells, but not if they are repeated in
766763
immediate sequence.
@@ -792,6 +789,7 @@ def linetrace(
792789
0 LINESTRING (0.00000 0.00000, 1.00000 0.00000, ... 837541fffffffff
793790
794791
"""
792+
795793
def func(row):
796794
return list(linetrace(row.geometry, resolution))
797795

h3pandas/util/decorator.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
from functools import wraps
22
from typing import Callable, Iterator
3-
from h3 import H3CellError
43

54

65
def catch_invalid_h3_address(f: Callable) -> Callable:
@@ -25,7 +24,7 @@ def catch_invalid_h3_address(f: Callable) -> Callable:
2524
def safe_f(*args, **kwargs):
2625
try:
2726
return f(*args, **kwargs)
28-
except (TypeError, ValueError, H3CellError) as e:
27+
except (TypeError, ValueError) as e:
2928
message = "H3 method raised an error. Is the H3 address correct?"
3029
message += f"\nCaller: {f.__name__}({_print_signature(*args, **kwargs)})"
3130
message += f"\nOriginal error: {repr(e)}"
@@ -47,13 +46,15 @@ def sequential_deduplication(func: Iterator[str]) -> Iterator[str]:
4746
-------
4847
Yields from f, but won't yield two items in a row that are the same.
4948
"""
49+
5050
def inner(*args):
5151
iterable = func(*args)
5252
last = None
5353
while (cell := next(iterable, None)) is not None:
5454
if cell != last:
5555
yield cell
5656
last = cell
57+
5758
return inner
5859

5960

h3pandas/util/shapely.py

Lines changed: 42 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,15 @@
1-
from typing import Union, Set, Tuple, List, Iterator
1+
from typing import Union, Set, Iterator
22
from shapely.geometry import Polygon, MultiPolygon, LineString, MultiLineString
3-
from h3 import h3
3+
from shapely.ops import transform
4+
import h3
45
from .decorator import sequential_deduplication
56

7+
68
MultiPolyOrPoly = Union[Polygon, MultiPolygon]
79
MultiLineOrLine = Union[LineString, MultiLineString]
810

911

10-
def _extract_coords(polygon: Polygon) -> Tuple[List, List[List]]:
11-
"""Extract the coordinates of outer and inner rings from a Polygon"""
12-
outer = list(polygon.exterior.coords)
13-
inners = [list(g.coords) for g in polygon.interiors]
14-
return outer, inners
15-
16-
17-
def polyfill(
18-
geometry: MultiPolyOrPoly, resolution: int, geo_json: bool = False
19-
) -> Set[str]:
12+
def polyfill(geometry: MultiPolyOrPoly, resolution: int) -> Set[str]:
2013
"""h3.polyfill accepting a shapely (Multi)Polygon
2114
2215
Parameters
@@ -25,8 +18,6 @@ def polyfill(
2518
Polygon to fill
2619
resolution : int
2720
H3 resolution of the filling cells
28-
geo_json : bool
29-
If True, coordinates are assumed to be lng/lat. Default: False (lat/lng)
3021
3122
Returns
3223
-------
@@ -36,24 +27,45 @@ def polyfill(
3627
------
3728
TypeError if geometry is not a Polygon or MultiPolygon
3829
"""
39-
if isinstance(geometry, Polygon):
40-
outer, inners = _extract_coords(geometry)
41-
return h3.polyfill_polygon(outer, resolution, inners, geo_json)
42-
43-
elif isinstance(geometry, MultiPolygon):
44-
h3_addresses = []
45-
for poly in geometry.geoms:
46-
h3_addresses.extend(polyfill(poly, resolution, geo_json))
47-
48-
return set(h3_addresses)
30+
if isinstance(geometry, (Polygon, MultiPolygon)):
31+
h3shape = h3.geo_to_h3shape(geometry)
32+
return set(h3.polygon_to_cells(h3shape, resolution))
4933
else:
5034
raise TypeError(f"Unknown type {type(geometry)}")
5135

5236

37+
def cell_to_boundary_lng_lat(h3_address: str) -> MultiLineString:
38+
"""h3.h3_to_geo_boundary equivalent for shapely
39+
40+
Parameters
41+
----------
42+
h3_address : str
43+
H3 address to convert to a boundary
44+
45+
Returns
46+
-------
47+
MultiLineString representing the H3 cell boundary
48+
"""
49+
return _switch_lat_lng(Polygon(h3.cell_to_boundary(h3_address)))
50+
51+
52+
def _switch_lat_lng(geometry: MultiPolyOrPoly) -> MultiPolyOrPoly:
53+
"""Switches the order of coordinates in a Polygon or MultiPolygon
54+
55+
Parameters
56+
----------
57+
geometry : Polygon or Multipolygon
58+
Polygon to switch coordinates
59+
60+
Returns
61+
-------
62+
Polygon or Multipolygon with switched coordinates
63+
"""
64+
return transform(lambda x, y: (y, x), geometry)
65+
66+
5367
@sequential_deduplication
54-
def linetrace(
55-
geometry: MultiLineOrLine, resolution: int
56-
) -> Iterator[str]:
68+
def linetrace(geometry: MultiLineOrLine, resolution: int) -> Iterator[str]:
5769
"""h3.polyfill equivalent for shapely (Multi)LineString
5870
Does not represent lines with duplicate sequential cells,
5971
but cells may repeat non-sequentially to represent
@@ -82,8 +94,8 @@ def linetrace(
8294
coords = zip(geometry.coords, geometry.coords[1:])
8395
while (vertex_pair := next(coords, None)) is not None:
8496
i, j = vertex_pair
85-
a = h3.geo_to_h3(*i[::-1], resolution)
86-
b = h3.geo_to_h3(*j[::-1], resolution)
87-
yield from h3.h3_line(a, b) # inclusive of a and b
97+
a = h3.latlng_to_cell(*i[::-1], resolution)
98+
b = h3.latlng_to_cell(*j[::-1], resolution)
99+
yield from h3.grid_path_cells(a, b) # inclusive of a and b
88100
else:
89101
raise TypeError(f"Unknown type {type(geometry)}")

0 commit comments

Comments
 (0)