Skip to content

Commit c98fbe0

Browse files
committed
Add support for nested GeometryCollection with warnings.
Closes #110.
1 parent 9864544 commit c98fbe0

File tree

3 files changed

+61
-26
lines changed

3 files changed

+61
-26
lines changed

geojson_pydantic/geometries.py

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
"""pydantic models for GeoJSON Geometry objects."""
2+
from __future__ import annotations
3+
24
import abc
5+
import warnings
36
from typing import Any, Iterator, List, Literal, Optional, Protocol, Union
47

58
from pydantic import BaseModel, Field, ValidationError, validator
@@ -241,18 +244,18 @@ class GeometryCollection(BaseModel, GeoInterfaceMixin):
241244
"""GeometryCollection Model"""
242245

243246
type: Literal["GeometryCollection"]
244-
geometries: List[Geometry]
247+
geometries: List[Union[Geometry, GeometryCollection]]
245248
bbox: Optional[BBox] = None
246249

247-
def __iter__(self) -> Iterator[Geometry]: # type: ignore [override]
250+
def __iter__(self) -> Iterator[Union[Geometry, GeometryCollection]]: # type: ignore [override]
248251
"""iterate over geometries"""
249252
return iter(self.geometries)
250253

251254
def __len__(self) -> int:
252255
"""return geometries length"""
253256
return len(self.geometries)
254257

255-
def __getitem__(self, index: int) -> Geometry:
258+
def __getitem__(self, index: int) -> Union[Geometry, GeometryCollection]:
256259
"""get geometry at a given index"""
257260
return self.geometries[index]
258261

@@ -274,6 +277,23 @@ def wkt(self) -> str:
274277
z = " Z " if "Z" in geometries else " "
275278
return f"{self.type.upper()}{z}{geometries}"
276279

280+
@validator("geometries")
281+
def check_geometries(cls, geometries: List) -> List:
282+
"""Add warnings for conditions the spec does not explicitly forbid."""
283+
if len(geometries) == 1:
284+
warnings.warn(
285+
"GeometryCollection should not be used for single geometries."
286+
)
287+
if any(geom.type == "GeometryCollection" for geom in geometries):
288+
warnings.warn(
289+
"GeometryCollection should not be used for nested GeometryCollections."
290+
)
291+
if len(set(geom.type for geom in geometries)) == 1:
292+
warnings.warn(
293+
"GeometryCollection should not be used for homogeneous collections."
294+
)
295+
return geometries
296+
277297

278298
def parse_geometry_obj(obj: Any) -> Geometry:
279299
"""

tests/test_features.py

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -26,22 +26,29 @@ class GenericProperties(BaseModel):
2626
"size": randint(0, 1000),
2727
}
2828

29+
coordinates = [
30+
[
31+
[13.38272, 52.46385],
32+
[13.42786, 52.46385],
33+
[13.42786, 52.48445],
34+
[13.38272, 52.48445],
35+
[13.38272, 52.46385],
36+
]
37+
]
38+
2939
polygon: Dict[str, Any] = {
3040
"type": "Polygon",
31-
"coordinates": [
32-
[
33-
[13.38272, 52.46385],
34-
[13.42786, 52.46385],
35-
[13.42786, 52.48445],
36-
[13.38272, 52.48445],
37-
[13.38272, 52.46385],
38-
]
39-
],
41+
"coordinates": coordinates,
42+
}
43+
44+
multipolygon: Dict[str, Any] = {
45+
"type": "MultiPolygon",
46+
"coordinates": [coordinates],
4047
}
4148

4249
geom_collection: Dict[str, Any] = {
4350
"type": "GeometryCollection",
44-
"geometries": [polygon, polygon],
51+
"geometries": [polygon, multipolygon],
4552
}
4653

4754
test_feature: Dict[str, Any] = {
@@ -131,7 +138,6 @@ def test_generic_geometry_collection():
131138
assert feature.properties.id == test_feature_geometry_collection["properties"]["id"]
132139
assert type(feature.geometry) == GeometryCollection
133140
assert feature.geometry.wkt.startswith("GEOMETRYCOLLECTION (POLYGON ")
134-
assert feature.geometry.geometries[0].wkt == feature.geometry.geometries[1].wkt
135141
assert type(feature.properties) == GenericProperties
136142
assert hasattr(feature.properties, "id")
137143

tests/test_geometries.py

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -452,33 +452,42 @@ def test_parse_geometry_obj_invalid_point():
452452
def test_geometry_collection_iteration(coordinates):
453453
"""test if geometry collection is iterable"""
454454
polygon = Polygon(type="Polygon", coordinates=coordinates)
455-
gc = GeometryCollection(type="GeometryCollection", geometries=[polygon, polygon])
455+
multipolygon = MultiPolygon(type="MultiPolygon", coordinates=[coordinates])
456+
gc = GeometryCollection(
457+
type="GeometryCollection", geometries=[polygon, multipolygon]
458+
)
456459
assert hasattr(gc, "__geo_interface__")
457460
assert_wkt_equivalence(gc)
458461
iter(gc)
459462

460463

461464
@pytest.mark.parametrize(
462-
"polygon", [[[(1.0, 2.0), (3.0, 4.0), (5.0, 6.0), (1.0, 2.0)]]]
465+
"coordinates", [[[(1.0, 2.0), (3.0, 4.0), (5.0, 6.0), (1.0, 2.0)]]]
463466
)
464-
def test_len_geometry_collection(polygon):
467+
def test_len_geometry_collection(coordinates):
465468
"""test if GeometryCollection return self leng"""
466-
polygon = Polygon(type="Polygon", coordinates=polygon)
467-
gc = GeometryCollection(type="GeometryCollection", geometries=[polygon, polygon])
469+
polygon = Polygon(type="Polygon", coordinates=coordinates)
470+
multipolygon = MultiPolygon(type="MultiPolygon", coordinates=[coordinates])
471+
gc = GeometryCollection(
472+
type="GeometryCollection", geometries=[polygon, multipolygon]
473+
)
468474
assert_wkt_equivalence(gc)
469475
assert len(gc) == 2
470476

471477

472478
@pytest.mark.parametrize(
473-
"polygon", [[[(1.0, 2.0), (3.0, 4.0), (5.0, 6.0), (1.0, 2.0)]]]
479+
"coordinates", [[[(1.0, 2.0), (3.0, 4.0), (5.0, 6.0), (1.0, 2.0)]]]
474480
)
475-
def test_getitem_geometry_collection(polygon):
476-
"""test if GeometryCollection return self leng"""
477-
polygon = Polygon(type="Polygon", coordinates=polygon)
478-
gc = GeometryCollection(type="GeometryCollection", geometries=[polygon, polygon])
481+
def test_getitem_geometry_collection(coordinates):
482+
"""test if GeometryCollection is subscriptable"""
483+
polygon = Polygon(type="Polygon", coordinates=coordinates)
484+
multipolygon = MultiPolygon(type="MultiPolygon", coordinates=[coordinates])
485+
gc = GeometryCollection(
486+
type="GeometryCollection", geometries=[polygon, multipolygon]
487+
)
479488
assert_wkt_equivalence(gc)
480-
item = gc[0]
481-
assert item == gc[0]
489+
assert polygon == gc[0]
490+
assert multipolygon == gc[1]
482491

483492

484493
def test_wkt_mixed_geometry_collection():

0 commit comments

Comments
 (0)