Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 51 additions & 9 deletions src/h3/_h3shape.py
Original file line number Diff line number Diff line change
Expand Up @@ -184,13 +184,6 @@ def __geo_interface__(self):
LL1: list of LL0s
LL2: list of LL1s (i.e., a polygon with holes)
LL3: list of LL2s (i.e., several polygons with holes)


## TODO

- Allow user to specify "container" in `cells_to_geojson`.
- That is, they may want a MultiPolygon even if the output fits in a Polygon
- 'auto', Polygon, MultiPolygon, FeatureCollection, GeometryCollection, ...
"""


Expand Down Expand Up @@ -325,14 +318,63 @@ def geo_to_h3shape(geo):
return shape


def h3shape_to_geo(h3shape):
def h3shape_to_geo(h3shape, container='auto'):
"""
Translate from an ``H3Shape`` to a ``__geo_interface__`` dict.

``h3shape`` should be either ``LatLngPoly`` or ``LatLngMultiPoly``

Parameters
----------
container : str, optional
Specify the desired GeoJSON output container.
Options: 'auto', 'Polygon', 'MultiPolygon', 'Feature',
'FeatureCollection', or 'GeometryCollection'.
Default is 'auto' (returns the simplest valid geometry).

Returns
-------
dict
"""
return h3shape.__geo_interface__
base_geo = h3shape.__geo_interface__

# Return immediately if no wrapping is needed
if container == 'auto' or container == base_geo['type']:
return base_geo

# Upgrade a Polygon to a MultiPolygon if explicitly requested
if container == 'MultiPolygon' and base_geo['type'] == 'Polygon':
return {
'type': 'MultiPolygon',
'coordinates': (base_geo['coordinates'],)
}

if container == 'Feature':
return {
'type': 'Feature',
'geometry': base_geo,
'properties': {}
}

if container == 'FeatureCollection':
return {
'type': 'FeatureCollection',
'features': [{
'type': 'Feature',
'geometry': base_geo,
'properties': {}
}]
}

if container == 'GeometryCollection':
return {
'type': 'GeometryCollection',
'geometries': [base_geo]
}

# If we reach here, the requested container is either unknown or
# insufficient for the data (e.g., requesting a Polygon for MultiPolygon data).
raise ValueError(
f"Requested container '{container}' is invalid or insufficient "
f"for data of type '{base_geo['type']}'."
)
62 changes: 62 additions & 0 deletions tests/test_lib/test_geojson_containers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import pytest
from h3._h3shape import LatLngPoly, h3shape_to_geo, LatLngMultiPoly


def test_h3shape_to_geo_containers():
# Create a basic dummy polygon for testing
poly = LatLngPoly(
[(37.68, -122.54), (37.68, -122.34), (37.82, -122.34), (37.82, -122.54)]
)

# Test 'auto' (Default behavior should return a Polygon)
auto_geo = h3shape_to_geo(poly, container='auto')
assert auto_geo['type'] == 'Polygon'

# Test MultiPolygon wrapper
mp_geo = h3shape_to_geo(poly, container='MultiPolygon')
assert mp_geo['type'] == 'MultiPolygon'
assert len(mp_geo['coordinates']) == 1

# Test Feature wrapper
feat_geo = h3shape_to_geo(poly, container='Feature')
assert feat_geo['type'] == 'Feature'
assert feat_geo['geometry']['type'] == 'Polygon'

# Test FeatureCollection wrapper
fc_geo = h3shape_to_geo(poly, container='FeatureCollection')
assert fc_geo['type'] == 'FeatureCollection'
assert len(fc_geo['features']) == 1
assert fc_geo['features'][0]['type'] == 'Feature'
assert fc_geo['features'][0]['geometry']['type'] == 'Polygon'

# Test GeometryCollection wrapper
gc_geo = h3shape_to_geo(poly, container='GeometryCollection')
assert gc_geo['type'] == 'GeometryCollection'
assert len(gc_geo['geometries']) == 1
assert gc_geo['geometries'][0]['type'] == 'Polygon'


def test_h3shape_to_geo_invalid_containers():
# 1. Test an unknown container string
poly = LatLngPoly([(37.68, -122.54), (37.68, -122.34), (37.82, -122.34)])
with pytest.raises(ValueError, match='invalid or insufficient'):
h3shape_to_geo(poly, container='InvalidString')

# 2. Test an insufficient container (MultiPolygon data into a Polygon container)
mpoly = LatLngMultiPoly(
LatLngPoly([(37.68, -122.54), (37.68, -122.34), (37.82, -122.34)])
)
with pytest.raises(ValueError, match='invalid or insufficient'):
h3shape_to_geo(mpoly, container='Polygon')


def test_h3shape_to_geo_exact_match():
# Test that requesting a MultiPolygon for MultiPolygon data succeeds
mpoly = LatLngMultiPoly(
LatLngPoly([(37.68, -122.54), (37.68, -122.34), (37.82, -122.34)])
)

mp_geo = h3shape_to_geo(mpoly, container='MultiPolygon')

assert mp_geo['type'] == 'MultiPolygon'
assert len(mp_geo['coordinates']) == 1
Loading