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,17 +1,13 @@
import json
import os
import random
import re
import shutil
import tempfile
import warnings
import numpy as np
from numpy.testing import assert_array_equal
import pandas as pd
from pandas.testing import assert_index_equal
from pyproj import CRS
from shapely.geometry import (
GeometryCollection,
LineString,
@@ -23,14 +19,15 @@ from shapely.geometry import (
)
from shapely.geometry.base import BaseGeometry
from geopandas import GeoSeries, GeoDataFrame, read_file, datasets, clip
from geopandas._compat import ignore_shapely2_warnings
import geopandas._compat as compat
from geopandas import GeoDataFrame, GeoSeries, clip, read_file
from geopandas.array import GeometryArray, GeometryDtype
from geopandas.testing import assert_geoseries_equal, geom_almost_equals
from geopandas.tests.util import geom_equals
from pandas.testing import assert_series_equal
import pytest
from geopandas.testing import assert_geoseries_equal, geom_almost_equals
from geopandas.tests.util import geom_equals
from numpy.testing import assert_array_equal
from pandas.testing import assert_index_equal, assert_series_equal
class TestSeries:
@@ -41,8 +38,7 @@ class TestSeries:
self.sq = Polygon([(0, 0), (1, 0), (1, 1), (0, 1)])
self.g1 = GeoSeries([self.t1, self.sq])
self.g2 = GeoSeries([self.sq, self.t1])
self.g3 = GeoSeries([self.t1, self.t2])
self.g3.crs = "epsg:4326"
self.g3 = GeoSeries([self.t1, self.t2], crs="epsg:4326")
self.g4 = GeoSeries([self.t2, self.t1])
self.na = GeoSeries([self.t1, self.t2, Polygon()])
self.na_none = GeoSeries([self.t1, self.t2, None])
@@ -56,6 +52,9 @@ class TestSeries:
self.l1 = LineString([(0, 0), (0, 1), (1, 1)])
self.l2 = LineString([(0, 0), (1, 0), (1, 1), (0, 1)])
self.g5 = GeoSeries([self.l1, self.l2])
self.esb3857 = Point(-8235939.130493107, 4975301.253789809)
self.sol3857 = Point(-8242607.167991625, 4966620.938285081)
self.landmarks3857 = GeoSeries([self.esb3857, self.sol3857], crs="epsg:3857")
def teardown_method(self):
shutil.rmtree(self.tempdir)
@@ -82,18 +81,16 @@ class TestSeries:
assert a1["B"].equals(a2["B"])
assert a1["C"] is None
@pytest.mark.skipif(not compat.HAS_PYPROJ, reason="pyproj not available")
def test_align_crs(self):
a1 = self.a1
a1.crs = "epsg:4326"
a2 = self.a2
a2.crs = "epsg:31370"
a1 = self.a1.set_crs("epsg:4326")
a2 = self.a2.set_crs("epsg:31370")
res1, res2 = a1.align(a2)
assert res1.crs == "epsg:4326"
assert res2.crs == "epsg:31370"
a2.crs = None
res1, res2 = a1.align(a2)
res1, res2 = a1.align(a2.set_crs(None, allow_override=True))
assert res1.crs == "epsg:4326"
assert res2.crs is None
@@ -110,11 +107,11 @@ class TestSeries:
# Test that warning is issued when operating on non-aligned series
# _series_op
with pytest.warns(UserWarning, match="The indices .+ different"):
with pytest.warns(UserWarning, match="The indices .+ not equal"):
self.a1.contains(self.a2)
# _geo_op
with pytest.warns(UserWarning, match="The indices .+ different"):
with pytest.warns(UserWarning, match="The indices .+ not equal"):
self.a1.union(self.a2)
def test_no_warning_if_aligned(self):
@@ -136,8 +133,7 @@ class TestSeries:
assert_array_equal(self.g1.geom_equals(self.sq), [False, True])
def test_geom_equals_align(self):
with pytest.warns(UserWarning, match="The indices .+ different"):
a = self.a1.geom_equals(self.a2, align=True)
a = self.a1.geom_equals(self.a2, align=True)
exp = pd.Series([False, True, False], index=["A", "B", "C"])
assert_series_equal(a, exp)
@@ -145,27 +141,39 @@ class TestSeries:
exp = pd.Series([False, False], index=["A", "B"])
assert_series_equal(a, exp)
@pytest.mark.filterwarnings(r"ignore:The 'geom_almost_equals\(\)':FutureWarning")
def test_geom_almost_equals(self):
# TODO: test decimal parameter
with pytest.warns(FutureWarning, match=re.escape("The 'geom_almost_equals()'")):
assert np.all(self.g1.geom_almost_equals(self.g1))
assert_array_equal(self.g1.geom_almost_equals(self.sq), [False, True])
assert_array_equal(
self.a1.geom_almost_equals(self.a2, align=True), [False, True, False]
assert np.all(self.g1.geom_almost_equals(self.g1))
assert_array_equal(self.g1.geom_almost_equals(self.sq), [False, True])
with warnings.catch_warnings():
warnings.filterwarnings(
"ignore",
"The indices of the left and right GeoSeries' are not equal",
UserWarning,
)
assert_array_equal(
self.a1.geom_almost_equals(self.a2, align=False), [False, False]
self.a1.geom_almost_equals(self.a2, align=True),
[False, True, False],
)
assert_array_equal(
self.a1.geom_almost_equals(self.a2, align=False), [False, False]
)
def test_geom_equals_exact(self):
# TODO: test tolerance parameter
assert np.all(self.g1.geom_equals_exact(self.g1, 0.001))
assert_array_equal(self.g1.geom_equals_exact(self.sq, 0.001), [False, True])
assert_array_equal(
self.a1.geom_equals_exact(self.a2, 0.001, align=True), [False, True, False]
)
with warnings.catch_warnings():
warnings.filterwarnings(
"ignore",
"The indices of the left and right GeoSeries' are not equal",
UserWarning,
)
assert_array_equal(
self.a1.geom_equals_exact(self.a2, 0.001, align=True),
[False, True, False],
)
assert_array_equal(
self.a1.geom_equals_exact(self.a2, 0.001, align=False), [False, False]
)
@@ -190,15 +198,69 @@ class TestSeries:
Test whether GeoSeries.to_json works and returns an actual json file.
"""
json_str = self.g3.to_json()
json.loads(json_str)
data = json.loads(json_str)
assert "id" in data["features"][0].keys()
assert "bbox" in data["features"][0].keys()
# TODO : verify the output is a valid GeoJSON.
def test_to_json_drop_id(self):
"""
Test whether GeoSeries.to_json works when drop_id is True.
"""
json_str = self.g3.to_json(drop_id=True)
data = json.loads(json_str)
assert "id" not in data["features"][0].keys()
def test_to_json_no_bbox(self):
"""
Test whether GeoSeries.to_json works when show_bbox is False.
"""
json_str = self.g3.to_json(show_bbox=False)
data = json.loads(json_str)
assert "bbox" not in data["features"][0].keys()
def test_to_json_no_bbox_drop_id(self):
"""
Test whether GeoSeries.to_json works when show_bbox is False
and drop_id is True.
"""
json_str = self.g3.to_json(show_bbox=False, drop_id=True)
data = json.loads(json_str)
assert "id" not in data["features"][0].keys()
assert "bbox" not in data["features"][0].keys()
@pytest.mark.skipif(not compat.HAS_PYPROJ, reason="Requires pyproj")
def test_to_json_wgs84(self):
"""
Test whether the wgs84 conversion works as intended.
"""
text = self.landmarks3857.to_json(to_wgs84=True)
data = json.loads(text)
assert data["type"] == "FeatureCollection"
assert "id" in data["features"][0].keys()
coord1 = data["features"][0]["geometry"]["coordinates"]
coord2 = data["features"][1]["geometry"]["coordinates"]
np.testing.assert_allclose(coord1, self.esb.coords[0])
np.testing.assert_allclose(coord2, self.sol.coords[0])
def test_to_json_wgs84_false(self):
"""
Ensure no conversion to wgs84
"""
text = self.landmarks3857.to_json()
data = json.loads(text)
coord1 = data["features"][0]["geometry"]["coordinates"]
coord2 = data["features"][1]["geometry"]["coordinates"]
assert coord1 == [-8235939.130493107, 4975301.253789809]
assert coord2 == [-8242607.167991625, 4966620.938285081]
def test_representative_point(self):
assert np.all(self.g1.contains(self.g1.representative_point()))
assert np.all(self.g2.contains(self.g2.representative_point()))
assert np.all(self.g3.contains(self.g3.representative_point()))
assert np.all(self.g4.contains(self.g4.representative_point()))
@pytest.mark.skipif(not compat.HAS_PYPROJ, reason="pyproj not available")
def test_transform(self):
utm18n = self.landmarks.to_crs(epsg=26918)
lonlat = utm18n.to_crs(epsg=4326)
@@ -209,20 +271,24 @@ class TestSeries:
self.landmarks.to_crs(crs=None, epsg=None)
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"
)
@pytest.mark.skipif(not compat.HAS_PYPROJ, reason="pyproj not available")
def test_estimate_utm_crs__out_of_bounds(self):
with pytest.raises(RuntimeError, match="Unable to determine UTM CRS"):
GeoSeries(
[Polygon([(0, 90), (1, 90), (2, 90)])], crs="EPSG:4326"
).estimate_utm_crs()
@pytest.mark.skipif(not compat.HAS_PYPROJ, reason="pyproj not available")
def test_estimate_utm_crs__missing_crs(self):
with pytest.raises(RuntimeError, match="crs must be set"):
GeoSeries([Polygon([(0, 90), (1, 90), (2, 90)])]).estimate_utm_crs()
@@ -258,6 +324,7 @@ class TestSeries:
assert self.g1.__geo_interface__["type"] == "FeatureCollection"
assert len(self.g1.__geo_interface__["features"]) == self.g1.shape[0]
@pytest.mark.skipif(not compat.HAS_PYPROJ, reason="pyproj not available")
def test_proj4strings(self):
# As string
reprojected = self.g3.to_crs("+proj=utm +zone=30")
@@ -270,8 +337,7 @@ class TestSeries:
assert geom_almost_equals(self.g3, reprojected_back)
# Set to equivalent string, convert, compare to original
copy = self.g3.copy()
copy.crs = "epsg:4326"
copy = self.g3.copy().set_crs("epsg:4326", allow_override=True)
reprojected = copy.to_crs({"proj": "utm", "zone": "30"})
reprojected_back = reprojected.to_crs(epsg=4326)
assert geom_almost_equals(self.g3, reprojected_back)
@@ -284,6 +350,23 @@ class TestSeries:
def test_from_wkb(self):
assert_geoseries_equal(self.g1, GeoSeries.from_wkb([self.t1.wkb, self.sq.wkb]))
def test_from_wkb_on_invalid(self):
# 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):
GeoSeries.from_wkb([invalid_wkb_hex], on_invalid="raise")
with pytest.warns(Warning, match=message):
res = GeoSeries.from_wkb([invalid_wkb_hex], on_invalid="warn")
assert res[0] is None
with warnings.catch_warnings():
warnings.simplefilter("error")
res = GeoSeries.from_wkb([invalid_wkb_hex], on_invalid="ignore")
assert res[0] is None
def test_from_wkb_series(self):
s = pd.Series([self.t1.wkb, self.sq.wkb], index=[1, 2])
expected = self.g1.copy()
@@ -299,6 +382,23 @@ class TestSeries:
def test_from_wkt(self):
assert_geoseries_equal(self.g1, GeoSeries.from_wkt([self.t1.wkt, self.sq.wkt]))
def test_from_wkt_on_invalid(self):
# 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):
GeoSeries.from_wkt([invalid_wkt], on_invalid="raise")
with pytest.warns(Warning, match=message):
res = GeoSeries.from_wkt([invalid_wkt], on_invalid="warn")
assert res[0] is None
with warnings.catch_warnings():
warnings.simplefilter("error")
res = GeoSeries.from_wkt([invalid_wkt], on_invalid="ignore")
assert res[0] is None
def test_from_wkt_series(self):
s = pd.Series([self.t1.wkt, self.sq.wkt], index=[1, 2])
expected = self.g1.copy()
@@ -320,16 +420,38 @@ class TestSeries:
def test_to_wkt(self):
assert_series_equal(pd.Series([self.t1.wkt, self.sq.wkt]), self.g1.to_wkt())
@pytest.mark.skip_no_sindex
def test_clip(self):
left = read_file(datasets.get_path("naturalearth_cities"))
world = read_file(datasets.get_path("naturalearth_lowres"))
def test_clip(self, naturalearth_lowres, naturalearth_cities):
left = read_file(naturalearth_cities)
world = read_file(naturalearth_lowres)
south_america = world[world["continent"] == "South America"]
expected = clip(left.geometry, south_america)
result = left.geometry.clip(south_america)
assert_geoseries_equal(result, expected)
def test_clip_sorting(self, naturalearth_cities, naturalearth_lowres):
"""
Test sorting of geodseries when clipping.
"""
cities = read_file(naturalearth_cities)
world = read_file(naturalearth_lowres)
south_america = world[world["continent"] == "South America"]
unsorted_clipped_cities = clip(cities, south_america, sort=False)
sorted_clipped_cities = clip(cities, south_america, sort=True)
expected_sorted_index = pd.Index(
[55, 59, 62, 88, 101, 114, 122, 169, 181, 189, 210, 230, 236, 238, 239]
)
assert not (
sorted(unsorted_clipped_cities.index) == unsorted_clipped_cities.index
).all()
assert (
sorted(sorted_clipped_cities.index) == sorted_clipped_cities.index
).all()
assert_index_equal(expected_sorted_index, sorted_clipped_cities.index)
def test_from_xy_points(self):
x = self.landmarks.x.values
y = self.landmarks.y.values
@@ -375,6 +497,13 @@ class TestSeries:
expected = GeoSeries([Point(0, 2, -1), Point(3, 5, 4)])
assert_geoseries_equal(expected, GeoSeries.from_xy(x, y, z))
@pytest.mark.skipif(compat.HAS_PYPROJ, reason="pyproj installed")
def test_set_crs_pyproj_error(self):
with pytest.raises(
ImportError, match="The 'pyproj' package is required for set_crs"
):
self.g1.set_crs(3857)
@pytest.mark.filterwarnings("ignore::UserWarning")
def test_missing_values():
@@ -406,12 +535,22 @@ def test_isna_empty_geoseries():
assert_series_equal(result, pd.Series([], dtype="bool"))
@pytest.mark.skipif(not compat.HAS_PYPROJ, reason="pyproj not available")
def test_geoseries_crs():
gs = GeoSeries()
gs.crs = "IGNF:ETRS89UTM28"
gs = GeoSeries().set_crs("IGNF:ETRS89UTM28")
assert gs.crs.to_authority() == ("IGNF", "ETRS89UTM28")
@pytest.mark.skipif(not compat.HAS_PYPROJ, reason="Requires pyproj")
def test_geoseries_override_existing_crs_warning():
gs = GeoSeries(crs="epsg:4326")
with pytest.warns(
DeprecationWarning,
match="Overriding the CRS of a GeoSeries that already has CRS",
):
gs.crs = "epsg:2100"
# -----------------------------------------------------------------------------
# # Constructor tests
# -----------------------------------------------------------------------------
@@ -513,10 +652,8 @@ class TestConstructor:
Polygon([(random.random(), random.random()) for _ in range(3)])
for _ in range(10)
]
with ignore_shapely2_warnings():
# the warning here is not suppressed by GeoPandas, as this is a pure
# pandas construction call
s = pd.Series(shapes, index=list("abcdefghij"), name="foo")
s = pd.Series(shapes, index=list("abcdefghij"), name="foo")
g = GeoSeries(s)
check_geoseries(g)
@@ -524,6 +661,37 @@ class TestConstructor:
assert s.name == g.name
assert s.index is g.index
@pytest.mark.skipif(not compat.HAS_PYPROJ, reason="pyproj not available")
def test_from_series_no_set_crs_on_construction(self):
# https://github.com/geopandas/geopandas/issues/2492
# also when passing Series[geometry], ensure we don't change crs of
# original data
gs = GeoSeries([Point(1, 1), Point(2, 2), Point(3, 3)])
s = pd.Series(gs)
result = GeoSeries(s, crs=4326)
assert s.values.crs is None
assert gs.crs is None
assert result.crs == "EPSG:4326"
def test_copy(self):
# default is to copy with CoW / pandas 3+
arr = np.array([Point(x, x) for x in range(3)], dtype=object)
result = GeoSeries(arr)
# modifying result doesn't change original array
result.loc[0] = Point(10, 10)
if compat.PANDAS_GE_30 or getattr(pd.options.mode, "copy_on_write", False):
assert arr[0] == Point(0, 0)
else:
assert arr[0] == Point(10, 10)
# avoid copy with copy=False
arr = np.array([Point(x, x) for x in range(3)], dtype=object)
result = GeoSeries(arr, copy=False)
assert result.array._data.flags.writeable
# now modifying result also updates original array
result.loc[0] = Point(10, 10)
assert arr[0] == Point(10, 10)
# GH 1216
@pytest.mark.parametrize("name", [None, "geometry", "Points"])
@pytest.mark.parametrize("crs", [None, "epsg:4326"])