#
# Copyright (C) 2012 -2022 Christian Ledermann
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
# You should have received a copy of the GNU Lesser General Public License
# along with this library; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
#
# file deepcode ignore inconsistent~equality: Python 3 only
"""Features."""
from collections.abc import Generator
from collections.abc import Iterator
from collections.abc import Sequence
from typing import Any
from typing import Optional
from typing import Union
from typing import cast
from pygeoif.functions import compare_coordinates
from pygeoif.geometry import Geometry
from pygeoif.types import Bounds
from pygeoif.types import GeoFeatureCollectionInterface
from pygeoif.types import GeoFeatureInterface
def feature_geo_interface_equals(
my_interface: GeoFeatureInterface,
other_interface: GeoFeatureInterface,
) -> bool:
"""Check if my interface is the same as the other interface."""
return all(
[
my_interface.get("id") == other_interface.get("id"),
my_interface["type"] == other_interface.get("type"),
my_interface["properties"] == other_interface.get("properties"),
my_interface["geometry"]["type"] == other_interface["geometry"].get("type"),
compare_coordinates(
coords=my_interface["geometry"]["coordinates"],
other=other_interface["geometry"].get(
"coordinates",
),
),
],
)
[docs]
class Feature:
"""
Aggregates a geometry instance with associated user-defined properties.
Attributes:
----------
geometry : object
A geometry instance
properties : dict
A dictionary linking field keys with values
associated with geometry instance
Example:
-------
>>> p = Point(1.0, -1.0)
>>> props = {'Name': 'Sample Point', 'Other': 'Other Data'}
>>> a = Feature(p, props)
>>> a.properties
{'Name': 'Sample Point', 'Other': 'Other Data'}
>>> a.properties['Name']
'Sample Point'
"""
def __init__(
self,
geometry: Geometry,
properties: Optional[dict[str, Any]] = None,
feature_id: Optional[Union[str, int]] = None,
) -> None:
"""Initialize the feature."""
self._geometry = geometry
self._properties = properties or {}
self._feature_id = feature_id
def __eq__(self, other: object) -> bool:
"""Check if the geointerfaces are equal."""
try:
if not other.__geo_interface__.get( # type: ignore [attr-defined]
"geometry",
):
return False
except AttributeError:
return False
return feature_geo_interface_equals(
my_interface=self.__geo_interface__,
other_interface=other.__geo_interface__, # type: ignore [attr-defined]
)
def __repr__(self) -> str:
"""Return the representation."""
return (
f"{self.__class__.__name__}({self._geometry!r},"
f" {self._properties}, {self._feature_id!r})"
)
@property
def id(self) -> Optional[Union[str, int]]:
"""Return the id of the feature."""
return self._feature_id
@property
def geometry(self) -> Geometry:
"""Return the geometry of the feature."""
return self._geometry
@property
def properties(self) -> dict[str, Any]:
"""Return a dictionary of properties."""
return self._properties
@property
def __geo_interface__(self) -> GeoFeatureInterface:
"""Return the GeoInterface of the geometry with properties."""
geo_interface: GeoFeatureInterface = {
"type": "Feature",
"bbox": cast("Bounds", self._geometry.bounds),
"geometry": self._geometry.__geo_interface__,
"properties": self._properties,
}
if self._feature_id is not None:
geo_interface["id"] = self._feature_id
return geo_interface
[docs]
class FeatureCollection:
"""
A heterogenous collection of Features.
Attributes:
----------
features : sequence
A sequence of feature instances
Example:
-------
>>> from pygeoif import geometry
>>> p = geometry.Point(1.0, -1.0)
>>> props = {'Name': 'Sample Point', 'Other': 'Other Data'}
>>> a = geometry.Feature(p, props)
>>> p2 = geometry.Point(1.0, -1.0)
>>> props2 = {'Name': 'Sample Point2', 'Other': 'Other Data2'}
>>> b = geometry.Feature(p2, props2)
>>> features = [a, b]
>>> c = geometry.FeatureCollection(features)
>>> c.__geo_interface__
{'type': 'FeatureCollection',
'features': [{'geometry': {'type': 'Point', 'coordinates': (1.0, -1.0)},
'type': 'Feature',
'properties': {'Other': 'Other Data', 'Name': 'Sample Point'}},
{'geometry': {'type': 'Point', 'coordinates': (1.0, -1.0)},
'type': 'Feature',
'properties': {'Other': 'Other Data2', 'Name': 'Sample Point2'}}]}
"""
def __init__(self, features: Sequence[Feature]) -> None:
"""Initialize the feature."""
self._features = tuple(features)
def __eq__(self, other: object) -> bool:
"""Check if the geointerfaces are equal."""
return self._check_interface(other) and all(
(
feature_geo_interface_equals(my_interface=mine, other_interface=other)
for mine, other in zip(
self.__geo_interface__["features"],
other.__geo_interface__["features"], # type: ignore [attr-defined]
)
),
)
def __len__(self) -> int:
"""Return the umber of features in this collection."""
return len(self._features)
def __iter__(self) -> Iterator[Feature]:
"""Iterate over the features of the collection."""
return iter(self._features)
def __repr__(self) -> str:
"""Return the representation."""
return f"{self.__class__.__name__}({self._features!r})"
@property
def features(self) -> Generator[Feature, None, None]:
"""Iterate over the features of the collection."""
yield from self._features
@property
def bounds(self) -> Bounds:
"""Return the X-Y bounding box."""
geom_bounds = list(
zip(*(feature.geometry.bounds for feature in self._features)),
)
return (
min(geom_bounds[0]),
min(geom_bounds[1]),
max(geom_bounds[2]),
max(geom_bounds[3]),
)
@property
def __geo_interface__(self) -> GeoFeatureCollectionInterface:
"""Return the GeoInterface of the feature."""
return {
"type": "FeatureCollection",
"bbox": self.bounds,
"features": tuple(feature.__geo_interface__ for feature in self._features),
}
def _check_interface(self, other: object) -> bool:
try:
return self.__geo_interface__["type"] == other.__geo_interface__.get( # type: ignore [attr-defined]
"type",
) and len(
self.__geo_interface__["features"],
) == len(
other.__geo_interface__.get( # type: ignore [attr-defined]
"features",
[],
),
)
except AttributeError:
return False
__all__ = [
"Feature",
"FeatureCollection",
]