Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

introduced force_2d for a subset of geometries #180 #183

Merged
merged 5 commits into from
Nov 5, 2023
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
67 changes: 67 additions & 0 deletions pygeoif/factories.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
from typing import cast

from pygeoif.exceptions import WKTParserError
from pygeoif.functions import move_coordinates
from pygeoif.functions import signed_area
from pygeoif.geometry import Geometry
from pygeoif.geometry import GeometryCollection
Expand Down Expand Up @@ -60,6 +61,70 @@
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)

Check warning on line 80 in pygeoif/factories.py

View check run for this annotation

Codecov / codecov/patch

pygeoif/factories.py#L79-L80

Added lines #L79 - L80 were not covered by tests
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
raise TypeError(msg)
raise AttributeError(msg)

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens if I call

force_2d(object())

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)

Check warning on line 113 in pygeoif/factories.py

View check run for this annotation

Codecov / codecov/patch

pygeoif/factories.py#L112-L113

Added lines #L112 - L113 were not covered by tests
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]
Expand Down Expand Up @@ -332,6 +397,8 @@


__all__ = [
"force_2d",
"force_3d",
"box",
"from_wkt",
"mapping",
Expand Down
64 changes: 64 additions & 0 deletions pygeoif/functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,10 @@
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
Expand Down Expand Up @@ -188,11 +190,73 @@ def compare_geo_interface(
return False


def move_coordinate(
coordinate: Sequence[float],
move_by: Sequence[float],
Comment on lines +194 to +195
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
coordinate: Sequence[float],
move_by: Sequence[float],
coordinate: PointType,
move_by: PointType,

and add from pygeoif.types import PointType

z: float = 0,
) -> Tuple[float, ...]:
"""
Move the coordinate by the given vector.

This forcefully changes the dimensions of the coordinate to match the latter.
>>> move_coordinate((0, 0), (-1, 1))
(-1, 1)
>>> move_coordinate((0, 0, 0), (-1, 1))
(-1, 1)
>>> 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))


def move_coordinates(
coordinates: Sequence[Any],
move_by: Sequence[float],
Comment on lines +215 to +216
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
coordinates: Sequence[Any],
move_by: Sequence[float],
coordinates: Union[CoordinatesType, MultiCoordinatesType],
move_by: PointType,

z: float = 0,
) -> Sequence[Any]:
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
) -> Sequence[Any]:
) -> Union[CoordinatesType, MultiCoordinatesType]:

"""
Move the coordinates recursively by the given vector.

This forcefully changes the dimension of each of the coordinate to match
the dimensionality of the vector.
>>> move_coordinates(((0, 0), (-1, 1)), (-1, 1))
((-1, 1), (-2, 2))
>>> move_coordinates(((0, 0, 0), (-1, 1, 0)), (-1, 1))
((-1, 1), (-2, 2))
>>> 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)


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)


__all__ = [
"centroid",
"compare_coordinates",
"compare_geo_interface",
"convex_hull",
"dedupe",
"move_coordinate",
"move_coordinates",
"is_coordinate",
"signed_area",
]
161 changes: 161 additions & 0 deletions tests/test_factories.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,167 @@ def test_num_float() -> None:
assert isinstance(factories.num("1.1"), float)


def test_force_2d_point() -> None:
# 2d point to 2d point (no actual change)
p = geometry.Point(-1, 1)
p2d = factories.force_2d(p)
assert p2d.x == -1
assert p2d.y == 1
assert not p2d.has_z

# 3d point to 2d point
p = geometry.Point(-1, 1, 2)
p2d = factories.force_2d(p)
assert p2d.x == -1
assert p2d.y == 1
assert not p2d.has_z


def test_force_2d_multipoint() -> None:
# 2d to 2d (no actual change)
p = geometry.MultiPoint([(-1, 1), (2, 3)])
p2d = factories.force_2d(p)
assert list(p2d.geoms) == [geometry.Point(-1, 1), geometry.Point(2, 3)]
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
assert list(p2d.geoms) == [geometry.Point(-1, 1), geometry.Point(2, 3)]
assert p2d.geoms)== (geometry.Point(-1, 1), geometry.Point(2, 3))



def test_force_2d_linestring() -> None:
# 2d line string to 2d line string (no actual change)
ls = geometry.LineString([(1, 2), (3, 4)])
l2d = factories.force_2d(ls)
assert l2d.coords == ((1, 2), (3, 4))

# 3d line string to 2d line string
ls = geometry.LineString([(1, 2, 3), (4, 5, 6)])
l2d = factories.force_2d(ls)
assert l2d.coords == ((1, 2), (4, 5))


def test_force_2d_linearring() -> None:
# 2d linear ring to 2d linear ring (no actual change)
r = geometry.LinearRing([(1, 2), (3, 4)])
r2d = factories.force_2d(r)
assert r2d.coords == ((1, 2), (3, 4), (1, 2))

# 3d linear ring to 2d linear ring
r = geometry.LinearRing([(1, 2, 3), (4, 5, 6)])
r2d = factories.force_2d(r)
assert r2d.coords == ((1, 2), (4, 5), (1, 2))


def test_force_2d_multilinestring() -> None:
# 2d multi line string to 2d multi line string (no actual change)
mls = geometry.MultiLineString([[(1, 2), (3, 4)], [(5, 6), (7, 8)]])
mls2d = factories.force_2d(mls)
assert list(mls2d.geoms) == list(mls.geoms)

# 3d multi line string to 2d multi line string
mls = geometry.MultiLineString([[(1, 2, 3), (4, 5, 6)], [(7, 8, 9), (10, 11, 12)]])
mls2d = factories.force_2d(mls)
assert list(mls2d.geoms) == [
geometry.LineString([(1, 2), (4, 5)]),
geometry.LineString([(7, 8), (10, 11)]),
]


def test_force_2d_polygon() -> None:
# 2d to 2d (no actual change)
external = [(0, 0), (0, 2), (2, 2), (2, 0), (0, 0)]
internal = [(0.5, 0.5), (0.5, 1.5), (1.5, 1.5), (1.5, 0.5), (0.5, 0.5)]
p = geometry.Polygon(external, [internal])
p2d = factories.force_2d(p)
assert p2d.coords[0] == (((0, 0), (0, 2), (2, 2), (2, 0), (0, 0)))
assert p2d.coords[1] == (
((0.5, 0.5), (0.5, 1.5), (1.5, 1.5), (1.5, 0.5), (0.5, 0.5)),
)
assert not p2d.has_z
assert p.maybe_valid == p2d.maybe_valid

# 3d to 2d
external = [(0, 0, 1), (0, 2, 1), (2, 2, 1), (2, 0, 1), (0, 0, 1)]
internal = [
(0.5, 0.5, 1),
(0.5, 1.5, 1),
(1.5, 1.5, 1),
(1.5, 0.5, 1),
(0.5, 0.5, 1),
]

p = geometry.Polygon(external, [internal])
p2d = factories.force_2d(p)
assert p2d.coords[0] == (((0, 0), (0, 2), (2, 2), (2, 0), (0, 0)))
assert p2d.coords[1] == (
((0.5, 0.5), (0.5, 1.5), (1.5, 1.5), (1.5, 0.5), (0.5, 0.5)),
)
assert not p2d.has_z


def test_force_2d_multipolygon() -> None:
# 2d to 2d (no actual change)
external = [(0, 0), (0, 2), (2, 2), (2, 0), (0, 0)]
internal = [(0.5, 0.5), (0.5, 1.5), (1.5, 1.5), (1.5, 0.5), (0.5, 0.5)]
mp = geometry.MultiPolygon([(external, [internal]), (external, [internal])])
mp2d = factories.force_2d(mp)

assert list(mp2d.geoms) == list(mp.geoms)


def test_force2d_collection() -> None:
# 2d to 2d (no actual change)
gc = geometry.GeometryCollection([geometry.Point(-1, 1), geometry.Point(-2, 2)])
gc2d = factories.force_2d(gc)
assert list(gc2d.geoms) == list(gc.geoms)

# 3d to 2d
gc = geometry.GeometryCollection(
[geometry.Point(-1, 1, 0), geometry.Point(-2, 2, 0)],
)
gc2d = factories.force_2d(gc)
assert list(gc2d.geoms) == [geometry.Point(-1, 1), geometry.Point(-2, 2)]
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
assert list(gc2d.geoms) == [geometry.Point(-1, 1), geometry.Point(-2, 2)]
assert gc2d.geoms == (geometry.Point(-1, 1), geometry.Point(-2, 2))



def test_force_2d_nongeo() -> None:
pytest.raises(AttributeError, factories.force_2d, (1, 2, 3))


def test_force_3d_point() -> None:
p = geometry.Point(0, 0)
p3d = factories.force_3d(p)
assert p3d.x == 0
assert p3d.y == 0
assert p3d.z == 0
assert p3d.has_z


def test_force_3d_collection() -> None:
gc = geometry.GeometryCollection(
[geometry.Point(-1, 1), geometry.Point(-2, 2)],
)
gc3d = factories.force_3d(gc)
assert list(gc3d.geoms) == [geometry.Point(-1, 1, 0), geometry.Point(-2, 2, 0)]


def test_force_3d_point_with_z() -> None:
p = geometry.Point(0, 0, 1)
p3d = factories.force_3d(p)
assert p3d.x == 0
assert p3d.y == 0
assert p3d.z == 1
assert p3d.has_z


def test_force_3d_point_noop() -> None:
p = geometry.Point(1, 2, 3)
p3d = factories.force_3d(p)
assert p3d.x == 1
assert p3d.y == 2
assert p3d.z == 3
assert p3d.has_z


def test_force_3d_nongeo() -> None:
pytest.raises(AttributeError, factories.force_3d, (1, 2))


def test_orient_true() -> None:
ext = [(0, 0), (0, 2), (2, 2), (2, 0), (0, 0)]
int_1 = [(0.5, 0.25), (1.5, 0.25), (1.5, 1.25), (0.5, 1.25), (0.5, 0.25)]
Expand Down
16 changes: 16 additions & 0 deletions tests/test_functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
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


Expand Down Expand Up @@ -452,3 +453,18 @@ 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
Loading