初次提交

This commit is contained in:
2026-03-04 17:07:07 +08:00
commit af988ea7b9
37 changed files with 3674 additions and 0 deletions

1
tests/__init__.py Normal file
View File

@@ -0,0 +1 @@
# tests package

80
tests/conftest.py Normal file
View File

@@ -0,0 +1,80 @@
"""
tests/conftest.py
~~~~~~~~~~~~~~~~~
共享测试夹具Fixture—— 提供测试数据,供所有测试文件复用。
"""
from __future__ import annotations
import json
import tempfile
from pathlib import Path
import geopandas as gpd
import pytest
from shapely.geometry import Point, Polygon
# ── 示例 GeoDataFrame ──────────────────────────────────────────────────────────
@pytest.fixture
def sample_points_gdf() -> gpd.GeoDataFrame:
"""3 个点的 GeoDataFrameWGS84"""
return gpd.GeoDataFrame(
{
"id": [1, 2, 3],
"name": ["点A", "点B", "点C"],
"value": [10.5, 20.0, 15.3],
},
geometry=[Point(116.4, 39.9), Point(121.5, 31.2), Point(113.3, 23.1)],
crs="EPSG:4326",
)
@pytest.fixture
def sample_polygon_gdf() -> gpd.GeoDataFrame:
"""1 个矩形多边形的 GeoDataFrameWGS84"""
poly = Polygon([(115.0, 38.0), (122.0, 38.0), (122.0, 41.0), (115.0, 41.0)])
return gpd.GeoDataFrame(
{"region": ["华北区"], "area_km2": [450000.0]},
geometry=[poly],
crs="EPSG:4326",
)
@pytest.fixture
def sample_multi_polygon_gdf() -> gpd.GeoDataFrame:
"""包含两个多边形的 GeoDataFrame用于融合/叠置测试WGS84"""
poly1 = Polygon([(100, 20), (110, 20), (110, 30), (100, 30)])
poly2 = Polygon([(105, 20), (115, 20), (115, 30), (105, 30)])
return gpd.GeoDataFrame(
{"zone": ["A", "B"], "value": [100, 200]},
geometry=[poly1, poly2],
crs="EPSG:4326",
)
# ── 临时文件路径 ───────────────────────────────────────────────────────────────
@pytest.fixture
def tmp_geojson_path(tmp_path: Path, sample_points_gdf: gpd.GeoDataFrame) -> Path:
"""将 sample_points_gdf 写出为临时 GeoJSON 并返回路径。"""
path = tmp_path / "sample.geojson"
sample_points_gdf.to_file(str(path), driver="GeoJSON")
return path
@pytest.fixture
def tmp_gpkg_path(tmp_path: Path, sample_points_gdf: gpd.GeoDataFrame) -> Path:
"""将 sample_points_gdf 写出为临时 GPKG 并返回路径。"""
path = tmp_path / "sample.gpkg"
sample_points_gdf.to_file(str(path), driver="GPKG", layer="points")
return path
@pytest.fixture
def tmp_output_dir(tmp_path: Path) -> Path:
"""空的临时输出目录。"""
out = tmp_path / "output"
out.mkdir()
return out

20
tests/test1.py Normal file
View File

@@ -0,0 +1,20 @@
import sys
import os
os.environ["OGR_ORGANIZE_POLYGONS"] = "SKIP"
from pathlib import Path
# 添加项目根目录到路径
sys.path.insert(0, str(Path(__file__).parent.parent))
import geo_tools
gdb_path = r"E:\@三普\@临时文件夹\临时数据库.gdb"
# 列出图层
# layers = geo_tools.list_gdb_layers(gdb_path)
# print(layers)
# 读取图层
gdf = geo_tools.read_gdb(gdb_path, layer="马关综合后图斑")
print(gdf.crs)

80
tests/test_analysis.py Normal file
View File

@@ -0,0 +1,80 @@
"""tests/test_analysis.py —— 空间分析单元测试。"""
import pytest
import geopandas as gpd
from shapely.geometry import Point, Polygon
from geo_tools.analysis.spatial_ops import overlay, select_by_location
from geo_tools.analysis.stats import area_weighted_mean, count_by_polygon, summarize_attributes
class TestOverlay:
def test_intersection(self, sample_multi_polygon_gdf):
poly_a = sample_multi_polygon_gdf.iloc[[0]].copy()
poly_b = sample_multi_polygon_gdf.iloc[[1]].copy()
result = overlay(poly_a, poly_b, how="intersection")
assert len(result) >= 1
assert result.geometry.is_valid.all()
def test_union(self, sample_multi_polygon_gdf):
poly_a = sample_multi_polygon_gdf.iloc[[0]].copy()
poly_b = sample_multi_polygon_gdf.iloc[[1]].copy()
result = overlay(poly_a, poly_b, how="union", keep_geom_type=False)
assert result.geometry.is_valid.all()
class TestSelectByLocation:
def test_select_points_in_polygon(self, sample_points_gdf, sample_polygon_gdf):
# polygon 覆盖华北区,应选中 北京 点
result = select_by_location(sample_points_gdf, sample_polygon_gdf, predicate="intersects")
assert len(result) >= 1
def test_select_within(self, sample_points_gdf, sample_polygon_gdf):
result = select_by_location(sample_points_gdf, sample_polygon_gdf, predicate="within")
assert len(result) >= 0 # 可能有点在边界上
class TestAreaWeightedMean:
def test_global_weighted_mean(self, sample_multi_polygon_gdf):
result = area_weighted_mean(sample_multi_polygon_gdf, value_col="value")
assert "area_weighted_mean" in result.index
assert result["area_weighted_mean"] > 0
def test_grouped_weighted_mean(self, sample_multi_polygon_gdf):
gdf = sample_multi_polygon_gdf.copy()
gdf["group"] = ["A", "B"]
result = area_weighted_mean(gdf, value_col="value", group_col="group")
assert "area_weighted_mean" in result.columns
assert len(result) == 2
class TestSummarizeAttributes:
def test_basic_summary(self, sample_points_gdf):
result = summarize_attributes(sample_points_gdf, columns=["value"])
assert "column" in result.columns
assert "mean" in result.columns
def test_grouped_summary(self, sample_points_gdf):
gdf = sample_points_gdf.copy()
gdf["group"] = ["北方", "东部", "南方"]
result = summarize_attributes(gdf, columns=["value"], group_col="group")
# 每组一行
assert len(result) == 3
class TestCountByPolygon:
def test_count_points_in_polygons(self, sample_points_gdf, sample_polygon_gdf):
result = count_by_polygon(sample_points_gdf, sample_polygon_gdf)
assert "point_count" in result.columns
assert result["point_count"].dtype.kind == "i" # 整数
def test_polygon_with_no_points(self):
# 南海中的 polygon不含任何点
poly = Polygon([(115, 10), (120, 10), (120, 15), (115, 15)])
polygons = gpd.GeoDataFrame({"id": [1]}, geometry=[poly], crs="EPSG:4326")
points = gpd.GeoDataFrame(
geometry=[Point(116.4, 39.9)], # 北京,不在 polygon 内
crs="EPSG:4326",
)
result = count_by_polygon(points, polygons)
assert result["point_count"].iloc[0] == 0

110
tests/test_geometry.py Normal file
View File

@@ -0,0 +1,110 @@
"""tests/test_geometry.py —— 几何运算单元测试。"""
import pytest
from shapely.geometry import LineString, Point, Polygon
import geo_tools
from geo_tools.core.geometry import (
buffer_geometry,
bounding_box,
centroid,
contains,
convex_hull,
difference,
distance_between,
fix_geometry,
intersect,
intersects,
is_valid_geometry,
unary_union,
union,
within,
)
class TestIsValidGeometry:
def test_valid_polygon(self):
poly = Polygon([(0, 0), (1, 0), (1, 1), (0, 1)])
assert is_valid_geometry(poly) is True
def test_none_returns_false(self):
assert is_valid_geometry(None) is False
def test_invalid_self_intersecting(self):
# 蝴蝶形(自相交)
bowtie = Polygon([(0, 0), (1, 1), (1, 0), (0, 1)])
assert is_valid_geometry(bowtie) is False
class TestFixGeometry:
def test_fix_bowtie(self):
bowtie = Polygon([(0, 0), (1, 1), (1, 0), (0, 1)])
assert not bowtie.is_valid
fixed = fix_geometry(bowtie)
assert fixed is not None
assert fixed.is_valid
def test_valid_geometry_unchanged(self):
poly = Polygon([(0, 0), (1, 0), (1, 1), (0, 1)])
fixed = fix_geometry(poly)
assert fixed.is_valid
assert fixed.area == pytest.approx(poly.area)
def test_none_returns_none(self):
assert fix_geometry(None) is None
class TestBufferGeometry:
def test_point_buffer(self):
pt = Point(0, 0)
buf = buffer_geometry(pt, 1.0)
assert buf.area > 3.0 # π * r² ≈ 3.14
def test_zero_distance_returns_point_like(self):
pt = Point(0, 0)
buf = buffer_geometry(pt, 0.0)
# buffer(0) on point may return empty or point
assert buf is not None
class TestSetOperations:
@pytest.fixture
def poly_a(self):
return Polygon([(0, 0), (2, 0), (2, 2), (0, 2)])
@pytest.fixture
def poly_b(self):
return Polygon([(1, 0), (3, 0), (3, 2), (1, 2)])
def test_intersection(self, poly_a, poly_b):
result = intersect(poly_a, poly_b)
assert result.area == pytest.approx(2.0)
def test_union(self, poly_a, poly_b):
result = union(poly_a, poly_b)
assert result.area == pytest.approx(6.0)
def test_difference(self, poly_a, poly_b):
result = difference(poly_a, poly_b)
assert result.area == pytest.approx(2.0)
def test_unary_union(self, poly_a, poly_b):
result = unary_union([poly_a, poly_b])
assert result.area == pytest.approx(6.0)
class TestSpatialRelations:
def test_contains_true(self):
big = Polygon([(0, 0), (10, 0), (10, 10), (0, 10)])
small = Polygon([(1, 1), (2, 1), (2, 2), (1, 2)])
assert contains(big, small) is True
def test_within(self):
big = Polygon([(0, 0), (10, 0), (10, 10), (0, 10)])
small = Polygon([(1, 1), (2, 1), (2, 2), (1, 2)])
assert within(small, big) is True
def test_distance(self):
p1 = Point(0, 0)
p2 = Point(3, 4)
assert distance_between(p1, p2) == pytest.approx(5.0)

81
tests/test_io.py Normal file
View File

@@ -0,0 +1,81 @@
"""tests/test_io.py —— IO 读写单元测试。"""
import pytest
import geopandas as gpd
from pathlib import Path
from geo_tools.io.readers import read_vector, read_gpkg, list_gpkg_layers, read_csv_points
from geo_tools.io.writers import write_vector, write_gpkg, write_csv
class TestReadVector:
def test_read_geojson(self, tmp_geojson_path):
gdf = read_vector(tmp_geojson_path)
assert isinstance(gdf, gpd.GeoDataFrame)
assert len(gdf) == 3
assert gdf.crs is not None
def test_read_with_crs_reprojection(self, tmp_geojson_path):
gdf = read_vector(tmp_geojson_path, crs="EPSG:3857")
assert gdf.crs.to_epsg() == 3857
def test_read_nonexistent_raises(self, tmp_path):
with pytest.raises(FileNotFoundError):
read_vector(tmp_path / "nonexistent.geojson")
def test_read_unsupported_format_raises(self, tmp_path):
bad_file = tmp_path / "data.xyz"
bad_file.write_text("dummy")
with pytest.raises(ValueError, match="不支持"):
read_vector(bad_file)
class TestWriteReadRoundtrip:
def test_geojson_roundtrip(self, sample_points_gdf, tmp_output_dir):
out = tmp_output_dir / "out.geojson"
write_vector(sample_points_gdf, out)
loaded = read_vector(out)
assert len(loaded) == len(sample_points_gdf)
assert list(loaded.columns) == list(sample_points_gdf.columns)
def test_gpkg_roundtrip(self, sample_points_gdf, tmp_output_dir):
out = tmp_output_dir / "out.gpkg"
write_gpkg(sample_points_gdf, out, layer="points")
loaded = read_gpkg(out, layer="points")
assert len(loaded) == len(sample_points_gdf)
def test_gpkg_multiple_layers(self, sample_points_gdf, sample_polygon_gdf, tmp_output_dir):
out = tmp_output_dir / "multi.gpkg"
write_gpkg(sample_points_gdf, out, layer="points")
write_gpkg(sample_polygon_gdf, out, layer="polygons", mode="a")
layers = list_gpkg_layers(out)
assert "points" in layers
assert "polygons" in layers
def test_csv_roundtrip(self, sample_points_gdf, tmp_output_dir):
out = tmp_output_dir / "out.csv"
write_csv(sample_points_gdf, out)
# CSV 写出的是 WKT geometry 列,用 pandas 读回验证
import pandas as pd
df = pd.read_csv(out)
assert "geometry" in df.columns # 存在 WKT 几何列
assert len(df) == len(sample_points_gdf) # 行数一致
# 再用 read_csv_points 以 WKT 模式读回
from geo_tools.io.readers import _read_csv_vector
from pathlib import Path
gdf_back = _read_csv_vector(Path(out), wkt_col="geometry")
assert len(gdf_back) == len(sample_points_gdf)
class TestReadCsvPoints:
def test_read_csv_with_latlon(self, tmp_path):
import pandas as pd
csv_path = tmp_path / "points.csv"
pd.DataFrame({
"longitude": [116.4, 121.5],
"latitude": [39.9, 31.2],
"name": ["北京", "上海"],
}).to_csv(csv_path, index=False)
gdf = read_csv_points(csv_path)
assert len(gdf) == 2
assert gdf.crs.to_epsg() == 4326

21
tests/test_proj.py Normal file
View File

@@ -0,0 +1,21 @@
import sys
import os
os.environ["OGR_ORGANIZE_POLYGONS"] = "SKIP"
from pathlib import Path
# 添加项目根目录到路径
project_root = Path(__file__).parent.parent
sys.path.insert(0, str(project_root))
import geo_tools
from geo_tools.core import projection
from geo_tools.config.project_enum import CRS
info = projection.get_crs_info(CRS.CGCS2000_6_DEGREE_ZONE_18.value)
print(info)
print(type(CRS.CGCS2000_3_DEGREE_ZONE_27))
# aa = geo_tools.read_vector(r"E:\@三普\@临时文件夹\样点异常值剔除\容县\异常样点数据\AB_outliers.shp")
# projection.reproject_gdf(aa,CRS.CGCS2000_3_DEGREE_ZONE_37).to_file(r"E:\@三普\@临时文件夹\样点异常值剔除\容县\AB_ou.shp")

100
tests/test_vector.py Normal file
View File

@@ -0,0 +1,100 @@
"""tests/test_vector.py —— 矢量操作单元测试。"""
import pytest
import geopandas as gpd
from shapely.geometry import Point
from geo_tools.core.vector import (
add_area_column,
clip_to_extent,
dissolve_by,
drop_invalid_geometries,
explode_multipart,
reproject,
set_crs,
spatial_join,
)
class TestReproject:
def test_basic_reproject(self, sample_points_gdf):
result = reproject(sample_points_gdf, "EPSG:3857")
assert result.crs.to_epsg() == 3857
assert len(result) == len(sample_points_gdf)
def test_reproject_preserves_count(self, sample_points_gdf):
result = reproject(sample_points_gdf, "EPSG:4490")
assert len(result) == 3
def test_reproject_no_crs_raises(self):
gdf = gpd.GeoDataFrame(geometry=[Point(0, 0)]) # 没有 CRS
with pytest.raises(ValueError, match="CRS"):
reproject(gdf, "EPSG:4326")
class TestSetCrs:
def test_set_crs_on_new_gdf(self):
gdf = gpd.GeoDataFrame(geometry=[Point(116.4, 39.9)])
result = set_crs(gdf, "EPSG:4326")
assert result.crs.to_epsg() == 4326
def test_overwrite_blocked_by_default(self):
gdf = gpd.GeoDataFrame(geometry=[Point(0, 0)], crs="EPSG:4326")
with pytest.raises(ValueError, match="overwrite"):
set_crs(gdf, "EPSG:3857")
def test_overwrite_allowed(self):
gdf = gpd.GeoDataFrame(geometry=[Point(0, 0)], crs="EPSG:4326")
result = set_crs(gdf, "EPSG:3857", overwrite=True)
assert result.crs.to_epsg() == 3857
class TestClipToExtent:
def test_clip_by_bbox(self, sample_points_gdf):
# 只包含北京116.4, 39.9)的 bbox
result = clip_to_extent(sample_points_gdf, (115.0, 38.0, 118.0, 41.0))
assert len(result) == 1
def test_clip_by_geodataframe(self, sample_points_gdf, sample_polygon_gdf):
# polygon 覆盖 115-122E38-41N应该包含北京
result = clip_to_extent(sample_points_gdf, sample_polygon_gdf)
assert len(result) >= 1
class TestDissolveBy:
def test_dissolve_by_field(self, sample_multi_polygon_gdf):
gdf = sample_multi_polygon_gdf.copy()
gdf["group"] = ["X", "X"] # 两条都归入同一组
result = dissolve_by(gdf, by="group")
assert len(result) == 1
def test_dissolve_preserves_crs(self, sample_multi_polygon_gdf):
gdf = sample_multi_polygon_gdf.copy()
gdf["group"] = ["A", "B"]
result = dissolve_by(gdf, by="group")
assert result.crs == gdf.crs
class TestAddAreaColumn:
def test_area_column_added(self, sample_polygon_gdf):
result = add_area_column(sample_polygon_gdf, col_name="area_m2")
assert "area_m2" in result.columns
assert result["area_m2"].iloc[0] > 0
class TestDropInvalidGeometries:
def test_drop_invalid(self):
from shapely.geometry import Polygon
valid = Polygon([(0, 0), (1, 0), (1, 1), (0, 1)])
invalid = Polygon([(0, 0), (1, 1), (1, 0), (0, 1)]) # 蝴蝶形
gdf = gpd.GeoDataFrame(geometry=[valid, invalid], crs="EPSG:4326")
result = drop_invalid_geometries(gdf)
assert len(result) == 1
def test_fix_invalid(self):
from shapely.geometry import Polygon
invalid = Polygon([(0, 0), (1, 1), (1, 0), (0, 1)])
gdf = gpd.GeoDataFrame(geometry=[invalid], crs="EPSG:4326")
result = drop_invalid_geometries(gdf, fix=True)
assert len(result) == 1
assert result.geometry.is_valid.all()