diff --git a/pygeoif/factories.py b/pygeoif/factories.py index fc584213..b0f61450 100644 --- a/pygeoif/factories.py +++ b/pygeoif/factories.py @@ -24,7 +24,7 @@ from typing import cast from pygeoif.exceptions import WKTParserError -from pygeoif.functions import move_coordinates +from pygeoif.functions import move_geo_interface from pygeoif.functions import signed_area from pygeoif.geometry import Geometry from pygeoif.geometry import GeometryCollection @@ -61,70 +61,6 @@ mpre: Pattern[str] = re.compile(r"\(\((.+?)\)\)") -def force_2d( - context: Union[GeoType, GeoCollectionType], -) -> Union[Geometry, GeometryCollection]: - """ - Force the dimensionality of a geometry to 2D. - - >>> force_2d(Point(0, 0, 1)) - Point(0, 0) - >>> force_2d(Point(0, 0)) - Point(0, 0) - >>> force_2d(LineString([(0, 0, 0), (0, 1, 1), (1, 1, 2)])) - LineString(((0, 0), (0, 1), (1, 1))) - """ - geometry = context if isinstance(context, dict) else mapping(context) - if not geometry: - msg = "Object does not implement __geo_interface__" - raise TypeError(msg) - if geometry["type"] == "GeometryCollection": - return GeometryCollection( - force_2d(g) # type: ignore [arg-type] - for g in geometry["geometries"] # type: ignore [typeddict-item] - ) - - geometry["coordinates"] = move_coordinates( # type: ignore [typeddict-unknown-key] - geometry["coordinates"], # type: ignore [typeddict-item] - (0, 0), - ) - return shape(geometry) - - -def force_3d( - context: Union[GeoType, GeoCollectionType], - z: float = 0, -) -> Union[Geometry, GeometryCollection]: - """ - Force the dimensionality of a geometry to 3D. - - >>> force_3d(Point(0, 0)) - Point(0, 0, 0) - >>> force_3d(Point(0, 0), 1) - Point(0, 0, 1) - >>> force_3d(Point(0, 0, 0)) - Point(0, 0, 0) - >>> force_3d(LineString([(0, 0), (0, 1), (1, 1)])) - LineString(((0, 0, 0), (0, 1, 0), (1, 1, 0))) - """ - geometry = context if isinstance(context, dict) else mapping(context) - if not geometry: - msg = "Object does not implement __geo_interface__" - raise TypeError(msg) - if geometry["type"] == "GeometryCollection": - return GeometryCollection( - force_3d(g, z) # type: ignore [arg-type] - for g in geometry["geometries"] # type: ignore [typeddict-item] - ) - - geometry["coordinates"] = move_coordinates( # type: ignore [typeddict-unknown-key] - geometry["coordinates"], # type: ignore [typeddict-item] - (0, 0, 0), - z, - ) - return shape(geometry) - - def get_oriented_ring(ring: LineType, ccw: bool) -> LineType: # noqa: FBT001 s = 1.0 if ccw else -1.0 return ring if signed_area(ring) / s >= 0 else ring[::-1] @@ -396,6 +332,43 @@ def mapping( return ob.__geo_interface__ +def force_2d( + context: Union[GeoType, GeoCollectionType], +) -> Union[Geometry, GeometryCollection]: + """ + Force the dimensionality of a geometry to 2D. + + >>> force_2d(Point(0, 0, 1)) + Point(0, 0) + >>> force_2d(Point(0, 0)) + Point(0, 0) + >>> force_2d(LineString([(0, 0, 0), (0, 1, 1), (1, 1, 2)])) + LineString(((0, 0), (0, 1), (1, 1))) + """ + geometry = mapping(context) + return shape(move_geo_interface(geometry, (0, 0))) + + +def force_3d( + context: Union[GeoType, GeoCollectionType], + z: float = 0, +) -> Union[Geometry, GeometryCollection]: + """ + Force the dimensionality of a geometry to 3D. + + >>> force_3d(Point(0, 0)) + Point(0, 0, 0) + >>> force_3d(Point(0, 0), 1) + Point(0, 0, 1) + >>> force_3d(Point(0, 0, 0)) + Point(0, 0, 0) + >>> force_3d(LineString([(0, 0), (0, 1), (1, 1)])) + LineString(((0, 0, 0), (0, 1, 0), (1, 1, 0))) + """ + geometry = mapping(context) + return shape(move_geo_interface(geometry, (0, 0, z))) + + __all__ = [ "force_2d", "force_3d", diff --git a/pygeoif/feature.py b/pygeoif/feature.py index 23a45173..633d51d6 100644 --- a/pygeoif/feature.py +++ b/pygeoif/feature.py @@ -46,9 +46,9 @@ def feature_geo_interface_equals( my_interface["geometry"]["type"] == other_interface["geometry"].get("type"), compare_coordinates( coords=my_interface["geometry"]["coordinates"], - other=other_interface["geometry"].get( + other=other_interface["geometry"].get( # type: ignore [arg-type] "coordinates", - ), # type: ignore[arg-type] + ), ), ], ) @@ -128,7 +128,7 @@ def properties(self) -> Dict[str, Any]: def __geo_interface__(self) -> GeoFeatureInterface: """Return the GeoInterface of the geometry with properties.""" geo_interface: GeoFeatureInterface = { - "type": self.__class__.__name__, + "type": "Feature", "bbox": cast(Bounds, self._geometry.bounds), "geometry": self._geometry.__geo_interface__, "properties": self._properties, @@ -220,7 +220,7 @@ def bounds(self) -> Bounds: def __geo_interface__(self) -> GeoFeatureCollectionInterface: """Return the GeoInterface of the feature.""" return { - "type": self.__class__.__name__, + "type": "FeatureCollection", "bbox": self.bounds, "features": tuple(feature.__geo_interface__ for feature in self._features), } diff --git a/pygeoif/functions.py b/pygeoif/functions.py index 034f669e..d9f5190d 100644 --- a/pygeoif/functions.py +++ b/pygeoif/functions.py @@ -19,10 +19,8 @@ import math from itertools import groupby from itertools import zip_longest -from typing import Any from typing import Iterable from typing import List -from typing import Sequence from typing import Tuple from typing import Union from typing import cast @@ -33,6 +31,7 @@ from pygeoif.types import LineType from pygeoif.types import MultiCoordinatesType from pygeoif.types import Point2D +from pygeoif.types import PointType def signed_area(coords: LineType) -> float: @@ -191,10 +190,9 @@ def compare_geo_interface( def move_coordinate( - coordinate: Sequence[float], - move_by: Sequence[float], - z: float = 0, -) -> Tuple[float, ...]: + coordinate: PointType, + move_by: PointType, +) -> PointType: """ Move the coordinate by the given vector. @@ -206,16 +204,19 @@ def move_coordinate( >>> move_coordinate((0, 0), (-1, 1, 0)) (-1, 1, 0) """ - if len(coordinate) > len(move_by): - return tuple(c + m for c, m in zip(coordinate, move_by)) - return tuple(c + m for c, m in zip_longest(coordinate, move_by, fillvalue=z)) + if len(coordinate) < len(move_by): + return cast( + PointType, + tuple(c + m for c, m in zip_longest(coordinate, move_by, fillvalue=0)), + ) + + return cast(PointType, tuple(c + m for c, m in zip(coordinate, move_by))) def move_coordinates( - coordinates: Sequence[Any], - move_by: Sequence[float], - z: float = 0, -) -> Sequence[Any]: + coordinates: CoordinatesType, + move_by: PointType, +) -> CoordinatesType: """ Move the coordinates recursively by the given vector. @@ -228,25 +229,34 @@ def move_coordinates( >>> move_coordinates(((0, 0), (-1, 1)), (-1, 1, 0)) ((-1, 1, 0), (-2, 2, 0)) """ - if is_coordinate(coordinates): - # a single coordinate - return move_coordinate(coordinates, move_by, z) - # a list of coordinates - return tuple(move_coordinates(c, move_by, z) for c in coordinates) + if isinstance(coordinates[0], (int, float)): + return move_coordinate(cast(PointType, coordinates), move_by) + return cast( + CoordinatesType, + tuple(move_coordinates(cast(CoordinatesType, c), move_by) for c in coordinates), + ) -def is_coordinate(val: Any) -> bool: # noqa: ANN401 - """ - Check if given value is a coordinate i.e. vector of generic dimensionality. - - >>> is_coordinate((1, 0)) - True - >>> is_coordinate(1) - False - >>> is_coordinate([(1, 2), (3, 4)]) - False - """ - return isinstance(val, tuple) and all(isinstance(x, (int, float)) for x in val) +def move_geo_interface( + interface: Union[GeoInterface, GeoCollectionInterface], + move_by: PointType, +) -> Union[GeoInterface, GeoCollectionInterface]: + """Move the coordinates of the geo interface by the given vector.""" + if interface["type"] == "GeometryCollection": + return { + "type": "GeometryCollection", + "geometries": tuple( + move_geo_interface(g, move_by) + for g in interface["geometries"] # type: ignore [typeddict-item] + ), + } + return { + "type": interface["type"], + "coordinates": move_coordinates( + interface["coordinates"], # type: ignore [typeddict-item, arg-type] + move_by, + ), + } __all__ = [ @@ -257,6 +267,5 @@ def is_coordinate(val: Any) -> bool: # noqa: ANN401 "dedupe", "move_coordinate", "move_coordinates", - "is_coordinate", "signed_area", ] diff --git a/pygeoif/types.py b/pygeoif/types.py index 861781e8..e9de16f6 100644 --- a/pygeoif/types.py +++ b/pygeoif/types.py @@ -23,8 +23,9 @@ from typing import Union from typing_extensions import Literal +from typing_extensions import NotRequired from typing_extensions import Protocol -from typing_extensions import TypedDict +from typing_extensions import TypedDict # for Python <3.11 with (Not)Required Point2D = Tuple[float, float] Point3D = Tuple[float, float, float] @@ -48,17 +49,12 @@ MultiCoordinatesType = Sequence[CoordinatesType] -class GeoInterfaceBase(TypedDict): +class GeoInterface(TypedDict): """Required keys for the GeoInterface.""" type: str coordinates: Union[CoordinatesType, MultiCoordinatesType] - - -class GeoInterface(GeoInterfaceBase, total=False): - """GeoInterface provides an optional bbox.""" - - bbox: Bounds + bbox: NotRequired[Bounds] class GeoCollectionInterface(TypedDict): @@ -66,35 +62,26 @@ class GeoCollectionInterface(TypedDict): type: Literal["GeometryCollection"] geometries: Sequence[Union[GeoInterface, "GeoCollectionInterface"]] + bbox: NotRequired[Bounds] -class GeoFeatureInterfaceBase(TypedDict): - """Required keys for the GeoInterface for Features.""" - - type: str - geometry: GeoInterface - - -class GeoFeatureInterface(GeoFeatureInterfaceBase, total=False): +class GeoFeatureInterface(TypedDict): """The GeoFeatureInterface has optional keys.""" - bbox: Bounds - properties: Dict[str, Any] - id: Union[str, int] # noqa: A003 - - -class GeoFeatureCollectionInterfaceBase(TypedDict): - """Required Keys for the GeoInterface of a FeatureCollection.""" - - type: str - features: Sequence[GeoFeatureInterface] + type: Literal["Feature"] + bbox: NotRequired[Bounds] + properties: NotRequired[Dict[str, Any]] + id: NotRequired[Union[str, int]] + geometry: GeoInterface -class GeoFeatureCollectionInterface(GeoFeatureCollectionInterfaceBase, total=False): +class GeoFeatureCollectionInterface(TypedDict): """Bbox and id are optional keys for the GeoFeatureCollectionInterface.""" - bbox: Bounds - id: Union[str, int] # noqa: A003 + type: Literal["FeatureCollection"] + features: Sequence[GeoFeatureInterface] + bbox: NotRequired[Bounds] + id: NotRequired[Union[str, int]] class GeoType(Protocol): diff --git a/pyproject.toml b/pyproject.toml index 0101bf95..1facfba7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -54,6 +54,7 @@ dev = [ "pygeoif[complexity]", "pygeoif[linting]", "pygeoif[tests]", + "pygeoif[typing]", ] linting = [ "black", @@ -210,7 +211,7 @@ select = [ "W", "YTT", ] -target-version = "py37" +target-version = "py38" [tool.ruff.isort] force-single-line = true diff --git a/tests/test_functions.py b/tests/test_functions.py index 3267ea66..e1441a22 100644 --- a/tests/test_functions.py +++ b/tests/test_functions.py @@ -11,7 +11,6 @@ from pygeoif.functions import compare_geo_interface from pygeoif.functions import convex_hull from pygeoif.functions import dedupe -from pygeoif.functions import is_coordinate from pygeoif.functions import signed_area @@ -453,18 +452,3 @@ def test_compare_neq_empty_geo_interface() -> None: } assert compare_geo_interface(geo_if, {}) is False - - -def test_is_coordinate() -> None: - assert is_coordinate((1, 2)) is True - assert is_coordinate((1,)) is True - - -def test_is_coordinate_not_composite_coordinates() -> None: - assert is_coordinate([(1, 2)]) is False - assert is_coordinate(((1, 2),)) is False - assert is_coordinate((((1, 2),),)) is False - - -def test_is_coordinate_not_primitive() -> None: - assert is_coordinate(1) is False