This commit is contained in:
2025-01-26 19:24:23 -08:00
parent 32cd60e92b
commit d1dde0dbc6
4155 changed files with 29170 additions and 216373 deletions

View File

@@ -1,34 +1,30 @@
import random
import warnings
import numpy as np
import pandas as pd
from pyproj import CRS
import shapely
import shapely.affinity
import shapely.geometry
from shapely.geometry.base import CAP_STYLE, JOIN_STYLE, BaseGeometry
import shapely.wkb
import shapely.wkt
try:
from shapely import geos_version
except ImportError:
from shapely._buildcfg import geos_version
from shapely import geos_version
from shapely.geometry.base import CAP_STYLE, JOIN_STYLE
import geopandas
from geopandas._compat import HAS_PYPROJ
from geopandas.array import (
GeometryArray,
_check_crs,
_crs_mismatch_warn,
from_shapely,
from_wkb,
from_wkt,
points_from_xy,
to_wkb,
to_wkt,
_check_crs,
_crs_mismatch_warn,
)
import geopandas._compat as compat
import pytest
@@ -143,11 +139,8 @@ def test_from_wkb():
assert all(v.equals(t) for v, t in zip(res, points_no_missing))
# missing values
# TODO(pygeos) does not support empty strings, np.nan, or pd.NA
# TODO(shapely) does not support empty strings, np.nan, or pd.NA
missing_values = [None]
if not (compat.USE_SHAPELY_20 or compat.USE_PYGEOS):
missing_values.extend([b"", np.nan])
missing_values.append(pd.NA)
res = from_wkb(missing_values)
np.testing.assert_array_equal(res, np.full(len(missing_values), None))
@@ -170,6 +163,24 @@ def test_from_wkb_hex():
assert isinstance(res, GeometryArray)
def test_from_wkb_on_invalid():
# Single point LineString hex WKB: invalid
invalid_wkb_hex = "01020000000100000000000000000008400000000000000840"
message = "point array must contain 0 or >1 elements"
with pytest.raises(Exception, match=message):
from_wkb([invalid_wkb_hex], on_invalid="raise")
with pytest.warns(Warning, match=message):
res = from_wkb([invalid_wkb_hex], on_invalid="warn")
assert res == [None]
with warnings.catch_warnings():
warnings.simplefilter("error")
res = from_wkb([invalid_wkb_hex], on_invalid="ignore")
assert res == [None]
def test_to_wkb():
P = from_shapely(points_no_missing)
res = to_wkb(P)
@@ -211,13 +222,10 @@ def test_from_wkt(string_type):
assert all(v.equals_exact(t, tolerance=tol) for v, t in zip(res, points_no_missing))
# missing values
# TODO(pygeos) does not support empty strings, np.nan, or pd.NA
# TODO(shapely) does not support empty strings, np.nan, or pd.NA
missing_values = [None]
if not (compat.USE_SHAPELY_20 or compat.USE_PYGEOS):
missing_values.extend([f(""), np.nan])
missing_values.append(pd.NA)
res = from_wkb(missing_values)
res = from_wkt(missing_values)
np.testing.assert_array_equal(res, np.full(len(missing_values), None))
# single MultiPolygon
@@ -228,6 +236,24 @@ def test_from_wkt(string_type):
assert res[0] == multi_poly
def test_from_wkt_on_invalid():
# Single point LineString WKT: invalid
invalid_wkt = "LINESTRING(0 0)"
message = "point array must contain 0 or >1 elements"
with pytest.raises(Exception, match=message):
from_wkt([invalid_wkt], on_invalid="raise")
with pytest.warns(Warning, match=message):
res = from_wkt([invalid_wkt], on_invalid="warn")
assert res == [None]
with warnings.catch_warnings():
warnings.simplefilter("error")
res = from_wkt([invalid_wkt], on_invalid="ignore")
assert res == [None]
def test_to_wkt():
P = from_shapely(points_no_missing)
res = to_wkt(P, rounding_precision=-1)
@@ -241,22 +267,6 @@ def test_to_wkt():
assert res[0] is None
def test_data():
arr = from_shapely(points_no_missing)
with pytest.warns(DeprecationWarning):
np_arr = arr.data
assert isinstance(np_arr, np.ndarray)
if compat.USE_PYGEOS:
np_arr2 = arr.to_numpy()
assert isinstance(np_arr2[0], BaseGeometry)
np_arr3 = np.asarray(arr)
assert isinstance(np_arr3[0], BaseGeometry)
else:
assert arr.to_numpy() is np_arr
assert np.asarray(arr) is np_arr
def test_as_array():
arr = from_shapely(points_no_missing)
np_arr1 = np.asarray(arr)
@@ -281,6 +291,9 @@ def test_as_array():
("geom_almost_equals", (3,)),
],
)
# filters required for attr=geom_almost_equals only
@pytest.mark.filterwarnings(r"ignore:The \'geom_almost_equals\(\)\' method is deprecat")
@pytest.mark.filterwarnings(r"ignore:The \'almost_equals\(\)\' method is deprecated")
def test_predicates_vector_scalar(attr, args):
na_value = False
@@ -293,9 +306,11 @@ def test_predicates_vector_scalar(attr, args):
assert result.dtype == bool
expected = [
getattr(tri, attr if "geom" not in attr else attr[5:])(other, *args)
if tri is not None
else na_value
(
getattr(tri, attr if "geom" not in attr else attr[5:])(other, *args)
if tri is not None
else na_value
)
for tri in triangles
]
@@ -320,6 +335,9 @@ def test_predicates_vector_scalar(attr, args):
("geom_almost_equals", (3,)),
],
)
# filters required for attr=geom_almost_equals only
@pytest.mark.filterwarnings(r"ignore:The \'geom_almost_equals\(\)\' method is deprecat")
@pytest.mark.filterwarnings(r"ignore:The \'almost_equals\(\)\' method is deprecated")
def test_predicates_vector_vector(attr, args):
na_value = False
empty_value = True if attr == "disjoint" else False
@@ -449,17 +467,12 @@ def test_binary_geo_scalar(attr):
"is_simple",
"has_z",
# for is_ring we raise a warning about the value for Polygon changing
pytest.param(
"is_ring",
marks=[
pytest.mark.filterwarnings("ignore:is_ring:FutureWarning"),
],
),
"is_ring",
],
)
def test_unary_predicates(attr):
na_value = False
if attr == "is_simple" and geos_version < (3, 8) and not compat.USE_PYGEOS:
if attr == "is_simple" and geos_version < (3, 8):
# poly.is_simple raises an error for empty polygon for GEOS < 3.8
with pytest.raises(Exception): # noqa: B017
T.is_simple
@@ -471,40 +484,17 @@ def test_unary_predicates(attr):
result = getattr(V, attr)
if attr == "is_simple" and geos_version < (3, 8):
# poly.is_simple raises an error for empty polygon for GEOS < 3.8
# with shapely, pygeos always returns False for all GEOS versions
if attr == "is_ring":
expected = [
getattr(t, attr) if t is not None and not t.is_empty else na_value
getattr(t, attr) if t is not None and t.exterior is not None else na_value
for t in vals
]
elif attr == "is_ring":
expected = [
getattr(t.exterior, attr)
if t is not None and t.exterior is not None
else na_value
for t in vals
]
# empty Linearring.is_ring gives False with Shapely < 2.0
if compat.USE_PYGEOS and not compat.SHAPELY_GE_20:
expected[-2] = True
elif (
attr == "is_closed"
and compat.USE_PYGEOS
and compat.SHAPELY_GE_182
and not compat.SHAPELY_GE_20
):
# In shapely 1.8.2, is_closed was changed to return always True for
# Polygon/MultiPolygon, while PyGEOS returns always False
expected = [False] * len(vals)
else:
expected = [getattr(t, attr) if t is not None else na_value for t in vals]
assert result.tolist() == expected
# for is_ring we raise a warning about the value for Polygon changing
@pytest.mark.filterwarnings("ignore:is_ring:FutureWarning")
def test_is_ring():
g = [
shapely.geometry.LinearRing([(0, 0), (1, 1), (1, -1)]),
@@ -514,11 +504,7 @@ def test_is_ring():
shapely.wkt.loads("POLYGON EMPTY"),
None,
]
expected = [True, False, True, True, True, False]
if not compat.USE_PYGEOS and not compat.SHAPELY_GE_20:
# empty polygon is_ring gives False with Shapely < 2.0
expected[-2] = False
expected = [True, False, True, False, False, False]
result = from_shapely(g).is_ring
assert result.tolist() == expected
@@ -561,9 +547,11 @@ def test_binary_distance():
# vector - vector
result = P[: len(T)].distance(T[::-1])
expected = [
getattr(p, attr)(t)
if not ((t is None or t.is_empty) or (p is None or p.is_empty))
else na_value
(
getattr(p, attr)(t)
if not ((t is None or t.is_empty) or (p is None or p.is_empty))
else na_value
)
for t, p in zip(triangles[::-1], points)
]
np.testing.assert_allclose(result, expected)
@@ -620,9 +608,11 @@ def test_binary_project(normalized):
result = L.project(P, normalized=normalized)
expected = [
line.project(p, normalized=normalized)
if line is not None and p is not None
else na_value
(
line.project(p, normalized=normalized)
if line is not None and p is not None
else na_value
)
for p, line in zip(points, lines)
]
np.testing.assert_allclose(result, expected)
@@ -632,16 +622,15 @@ def test_binary_project(normalized):
@pytest.mark.parametrize("join_style", [JOIN_STYLE.round, JOIN_STYLE.bevel])
@pytest.mark.parametrize("resolution", [16, 25])
def test_buffer(resolution, cap_style, join_style):
if compat.USE_PYGEOS:
# TODO(pygeos) need to further investigate why this test fails
if cap_style == 1 and join_style == 3:
pytest.skip("failing TODO")
na_value = None
expected = [
p.buffer(0.1, resolution=resolution, cap_style=cap_style, join_style=join_style)
if p is not None
else na_value
(
p.buffer(
0.1, resolution=resolution, cap_style=cap_style, join_style=join_style
)
if p is not None
else na_value
)
for p in points
]
result = P.buffer(
@@ -676,10 +665,32 @@ def test_unary_union():
shapely.geometry.Polygon([(0, 0), (1, 0), (1, 1)]),
]
G = from_shapely(geoms)
u = G.unary_union()
with pytest.warns(
DeprecationWarning, match="The 'unary_union' attribute is deprecated"
):
u = G.unary_union()
expected = shapely.geometry.Polygon([(0, 0), (1, 0), (1, 1), (0, 1)])
assert u.equals(expected)
assert u.equals(G.union_all())
def test_union_all():
geoms = [
shapely.geometry.Polygon([(0, 0), (0, 1), (1, 1)]),
shapely.geometry.Polygon([(0, 0), (1, 0), (1, 1)]),
]
G = from_shapely(geoms)
u = G.union_all()
expected = shapely.geometry.Polygon([(0, 0), (1, 0), (1, 1), (0, 1)])
assert u.equals(expected)
u_cov = G.union_all(method="coverage")
assert u_cov.equals(expected)
with pytest.raises(ValueError, match="Method 'invalid' not recognized."):
G.union_all(method="invalid")
@pytest.mark.parametrize(
@@ -810,7 +821,7 @@ def test_setitem(item):
def test_equality_ops():
with pytest.raises(ValueError):
P[:5] == P[:7]
_ = P[:5] == P[:7]
a1 = from_shapely([points[1], points[2], points[3]])
a2 = from_shapely([points[1], points[0], points[3]])
@@ -833,7 +844,7 @@ def test_equality_ops():
def test_dir():
assert "contains" in dir(P)
assert "data" in dir(P)
assert "to_numpy" in dir(P)
def test_chaining():
@@ -894,6 +905,7 @@ def test_astype_multipolygon():
assert result[0] == multi_poly.wkt[:10]
@pytest.mark.skipif(not HAS_PYPROJ, reason="pyproj not installed")
def test_check_crs():
t1 = T.copy()
t1.crs = 4326
@@ -902,6 +914,7 @@ def test_check_crs():
assert _check_crs(t1, T, allow_none=True) is True
@pytest.mark.skipif(not HAS_PYPROJ, reason="pyproj not installed")
def test_crs_mismatch_warn():
t1 = T.copy()
t2 = T.copy()
@@ -921,6 +934,14 @@ def test_crs_mismatch_warn():
_crs_mismatch_warn(t1, T)
@pytest.mark.skipif(HAS_PYPROJ, reason="pyproj installed")
def test_missing_pyproj():
with pytest.warns(UserWarning, match="Cannot set the CRS, falling back to None"):
t = T.copy()
t.crs = 4326
assert t.crs is None
@pytest.mark.parametrize("NA", [None, np.nan])
def test_isna(NA):
t1 = T.copy()
@@ -948,6 +969,24 @@ def test_unique_has_crs():
assert t.unique().crs == t.crs
@pytest.mark.skipif(HAS_PYPROJ, reason="pyproj installed")
def test_to_crs_pyproj_error():
t = T.copy()
t.crs = 4326
with pytest.raises(
ImportError, match="The 'pyproj' package is required for to_crs"
):
t.to_crs(3857)
@pytest.mark.skipif(HAS_PYPROJ, reason="pyproj installed")
def test_estimate_utm_crs_pyproj_error():
with pytest.raises(
ImportError, match="The 'pyproj' package is required for estimate_utm_crs"
):
T.estimate_utm_crs()
class TestEstimateUtmCrs:
def setup_method(self):
self.esb = shapely.geometry.Point(-73.9847, 40.7484)
@@ -955,15 +994,21 @@ class TestEstimateUtmCrs:
self.landmarks = from_shapely([self.esb, self.sol], crs="epsg:4326")
def test_estimate_utm_crs__geographic(self):
assert self.landmarks.estimate_utm_crs() == CRS("EPSG:32618")
assert self.landmarks.estimate_utm_crs("NAD83") == CRS("EPSG:26918")
pyproj = pytest.importorskip("pyproj")
assert self.landmarks.estimate_utm_crs() == pyproj.CRS("EPSG:32618")
assert self.landmarks.estimate_utm_crs("NAD83") == pyproj.CRS("EPSG:26918")
def test_estimate_utm_crs__projected(self):
assert self.landmarks.to_crs("EPSG:3857").estimate_utm_crs() == CRS(
pyproj = pytest.importorskip("pyproj")
assert self.landmarks.to_crs("EPSG:3857").estimate_utm_crs() == pyproj.CRS(
"EPSG:32618"
)
def test_estimate_utm_crs__antimeridian(self):
pyproj = pytest.importorskip("pyproj")
antimeridian = from_shapely(
[
shapely.geometry.Point(1722483.900174921, 5228058.6143420935),
@@ -971,15 +1016,19 @@ class TestEstimateUtmCrs:
],
crs="EPSG:3851",
)
assert antimeridian.estimate_utm_crs() == CRS("EPSG:32760")
assert antimeridian.estimate_utm_crs() == pyproj.CRS("EPSG:32760")
def test_estimate_utm_crs__out_of_bounds(self):
pytest.importorskip("pyproj")
with pytest.raises(RuntimeError, match="Unable to determine UTM CRS"):
from_shapely(
[shapely.geometry.Polygon([(0, 90), (1, 90), (2, 90)])], crs="EPSG:4326"
).estimate_utm_crs()
def test_estimate_utm_crs__missing_crs(self):
pytest.importorskip("pyproj")
with pytest.raises(RuntimeError, match="crs must be set"):
from_shapely(
[shapely.geometry.Polygon([(0, 90), (1, 90), (2, 90)])]