diff --git a/src/h3/_h3shape.py b/src/h3/_h3shape.py index 5c362951..7aa7f628 100644 --- a/src/h3/_h3shape.py +++ b/src/h3/_h3shape.py @@ -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, ... """ @@ -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']}'." + ) diff --git a/tests/test_geojson_containers.py b/tests/test_geojson_containers.py new file mode 100644 index 00000000..35629d55 --- /dev/null +++ b/tests/test_geojson_containers.py @@ -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([ + [(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([ + [(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