Skip to content

Commit

Permalink
Merge pull request #183 from whisk/develop
Browse files Browse the repository at this point in the history
introduced force_2d for a subset of geometries #180
  • Loading branch information
cleder authored Nov 5, 2023
2 parents 5b461c7 + a213905 commit 216525b
Show file tree
Hide file tree
Showing 4 changed files with 308 additions and 0 deletions.
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)
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]
Expand Down Expand Up @@ -332,6 +397,8 @@ def mapping(


__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],
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],
z: float = 0,
) -> Sequence[Any]:
"""
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)]


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)]


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

0 comments on commit 216525b

Please sign in to comment.