diff --git a/.trae/rules/geo_rules.md b/.trae/rules/geo_rules.md
new file mode 100644
index 0000000..0a24594
--- /dev/null
+++ b/.trae/rules/geo_rules.md
@@ -0,0 +1,10 @@
+# Geo-Tools 项目特定编码规范
+
+## 地理数据处理黄金法则
+1. **投影先行**:任何涉及距离、面积的计算前,必须先检查 CRS,若是地理坐标系(如 EPSG:4326),必须提示用户或自动调用 `suggest_projected_crs` 转换。
+2. **几何清洗**:任何接收用户输入的 GeoDataFrame 的公开方法,内部必须包含 `gdf = gdf[gdf.is_valid]` 或 `make_valid()` 的处理逻辑,防止脏数据导致底层 C 库崩溃。
+3. **内存控制**:读取大文件函数需要提供 `mask` (裁剪范围) 或 `rows` (读取行数限制) 参数。
+
+## 代码风格
+- 空间操作函数返回新的 GeoDataFrame,尽量不修改原始输入数据(Immutable)。
+- 使用 `pathlib.Path` 处理所有文件路径,而不是字符串拼接。
\ No newline at end of file
diff --git a/.trae/rules/project_rules.md b/.trae/rules/project_rules.md
new file mode 100644
index 0000000..07639a8
--- /dev/null
+++ b/.trae/rules/project_rules.md
@@ -0,0 +1,8 @@
+# 给 AI 助手看的规则 - 小白专用版
+
+## 关于内存(最重要)
+1. 凡是读文件的函数,只要没加 `rows` 限制或 `chunk_size` 分块,都要在注释里用【警告】两个字提醒用户注意内存风险。
+2. 不要用 `for` 循环一行行修改 GeoDataFrame,要用 `apply` 或者向量化操作。
+
+## 关于报错
+3. 禁止直接把底层库(fiona, shapely)的英文报错抛给用户看。必须包一层 try-except,翻译成中文提示,例如:“文件打不开,检查一下路径是不是有中文?”或者“数据里有烂掉的图形,已自动修复/跳过”。
\ No newline at end of file
diff --git a/README.md b/README.md
deleted file mode 100644
index 542a57e..0000000
--- a/README.md
+++ /dev/null
@@ -1,128 +0,0 @@
-# geo_tools
-
-> 专业地理信息数据处理工具库 —— 基于 geopandas / shapely / fiona
-
-[](https://www.python.org)
-[](LICENSE)
-
----
-
-## 功能特性
-
-- **统一 IO 接口**:一行代码读写 Shapefile、GeoJSON、GeoPackage、**File Geodatabase (GDB)**、KML、CSV 等格式
-- **核心几何运算**:基于 Shapely 2.x 的缓冲区、集合运算、有效性检查与自动修复
-- **坐标系处理**:重投影、CRS 信息查询、批量坐标转换,内置中国常用 CRS 常量
-- **空间分析**:叠置分析、最近邻、按位置选择、面积加权均值、属性统计汇总
-- **配置驱动**:通过 `.env` 或环境变量控制输出路径、日志级别、默认 CRS 等
-- **栅格预留接口**:为 rasterio 集成预留扩展点
-
-## 项目结构
-
-```
-geo_tools/
-├── geo_tools/ # 主包
-│ ├── config/ # Pydantic BaseSettings 全局配置
-│ ├── core/ # 核心处理(vector / geometry / projection / raster)
-│ ├── io/ # 数据读写(readers / writers,含 GDB)
-│ ├── analysis/ # 空间分析(spatial_ops / stats)
-│ └── utils/ # 通用工具(logger / validators / config)
-├── scripts/ # 独立处理脚本
-├── tests/ # pytest 测试套件
-├── data/sample/ # 示例数据(GeoJSON)
-├── output/ # 处理结果输出目录
-├── logs/ # 日志文件目录
-├── docs/ # 文档
-└── pyproject.toml # 项目配置与依赖
-```
-
-## 快速开始
-
-### 安装依赖
-
-```bash
-# 推荐使用 conda 安装地理库(避免 GDAL 编译问题)
-conda install -c conda-forge geopandas shapely fiona pyproj
-
-# 然后安装本项目(开发模式)
-pip install -e ".[dev]"
-```
-
-### 基本使用
-
-```python
-import geo_tools
-
-# 读取矢量数据(自动识别格式)
-gdf = geo_tools.read_vector("data/sample/sample_points.geojson")
-
-# 读写 File Geodatabase
-layers = geo_tools.list_gdb_layers("path/to/data.gdb")
-gdf = geo_tools.read_gdb("path/to/data.gdb", layer="my_layer")
-geo_tools.write_gdb(gdf, "output/result.gdb", layer="result")
-
-# 坐标系转换
-gdf_proj = geo_tools.reproject(gdf, "EPSG:3857")
-
-# 缓冲区分析
-from geo_tools.core.geometry import buffer_geometry
-buffered_geom = buffer_geometry(gdf.geometry[0], distance=1000)
-
-# 空间叠置
-from geo_tools.analysis.spatial_ops import overlay
-result = geo_tools.overlay(layer_a, layer_b, how="intersection")
-
-# 面积加权均值
-from geo_tools.analysis.stats import area_weighted_mean
-result = area_weighted_mean(polygon_gdf, value_col="soil_ph", group_col="region")
-```
-
-### 配置
-
-复制 `.env.example` 为 `.env` 并按需修改:
-
-```bash
-GEO_TOOLS_OUTPUT_DIR=D:/output
-GEO_TOOLS_DEFAULT_CRS=EPSG:4490
-GEO_TOOLS_LOG_LEVEL=DEBUG
-```
-
-## 运行测试
-
-```bash
-# 运行全部测试
-pytest tests/ -v
-
-# 运行带覆盖率报告
-pytest tests/ -v --cov=geo_tools --cov-report=html
-```
-
-## 运行示例脚本
-
-```bash
-python scripts/example_workflow.py
-```
-
-## GDB 支持说明
-
-本项目通过 `fiona>=1.9` 的 `OpenFileGDB` 驱动读写 Esri File Geodatabase(`.gdb`)。
-
-| 操作 | 驱动 | 要求 |
-|------|------|------|
-| 读取 GDB | `OpenFileGDB` | fiona >= 1.9(内置) |
-| 写出 GDB | `OpenFileGDB` | fiona >= 1.9(内置) |
-| 编辑 GDB(高级) | `FileGDB` | 需要 ESRI FileGDB API |
-
-```python
-# 列出所有图层
-layers = geo_tools.list_gdb_layers("data.gdb")
-
-# 读取指定图层
-gdf = geo_tools.read_gdb("data.gdb", layer="土地利用", crs="EPSG:4490")
-
-# 写出到 GDB(新建或追加图层)
-geo_tools.write_gdb(result_gdf, "output.gdb", layer="分析结果", mode="w")
-```
-
-## 许可证
-
-MIT License
diff --git a/TUTORIAL.md b/TUTORIAL.md
new file mode 100644
index 0000000..3dce080
--- /dev/null
+++ b/TUTORIAL.md
@@ -0,0 +1,468 @@
+# Geo-Tools 使用教程
+
+## 1. 安装与环境准备
+
+### 1.1 前提条件
+
+在安装 Geo-Tools 之前,您需要确保已经安装了以下依赖:
+
+- Python 3.8 或更高版本
+- pip(Python 包管理工具)
+
+### 1.2 安装方法
+
+1. **克隆项目仓库**
+
+ ```bash
+ git clone <项目地址>
+ cd geo_tools
+ ```
+
+2. **安装依赖**
+
+ ```bash
+ pip install -r requirements.txt
+ ```
+
+3. **安装 Geo-Tools**
+
+ ```bash
+ pip install -e .
+ ```
+
+### 1.3 验证安装
+
+运行以下命令验证 Geo-Tools 是否安装成功:
+
+```python
+from app.io.readers import read_vector
+print("Geo-Tools 安装成功!")
+```
+
+## 2. 5分钟体验
+
+### 2.1 读取 Shapefile
+
+```python
+from app.io.readers import read_vector
+from pathlib import Path
+
+# 读取 Shapefile 文件
+# 这里使用示例数据,您可以替换为自己的文件路径
+data_path = Path("data/sample/sample_points.geojson")
+gdf = read_vector(data_path)
+
+# 查看数据基本信息
+print(f"数据形状:{gdf.shape}")
+print(f"数据列名:{list(gdf.columns)}")
+print(f"坐标系:{gdf.crs}")
+```
+
+### 2.2 预览数据
+
+```python
+# 预览前5行数据
+print("\n数据预览:")
+print(gdf.head())
+
+# 或者使用 rows 参数直接只读取前5行
+print("\n使用 rows 参数预览:")
+gdf_preview = read_vector(data_path, rows=5)
+print(gdf_preview)
+```
+
+### 2.3 制作缓冲区
+
+```python
+from app.core.geometry import buffer_geometry
+
+# 为每个点创建缓冲区
+gdf["buffer"] = gdf.geometry.apply(lambda geom: buffer_geometry(geom, distance=0.1))
+
+# 查看缓冲区结果
+print("\n缓冲区创建完成!")
+print(f"原始几何类型:{gdf.geometry.geom_type.unique()}")
+print(f"缓冲区几何类型:{gdf['buffer'].geom_type.unique()}")
+```
+
+### 2.4 保存结果
+
+```python
+from app.io.writers import write_vector
+
+# 保存为 GeoJSON 文件
+output_path = Path("output/buffered_points.geojson")
+write_vector(gdf, output_path)
+print(f"\n结果已保存至:{output_path}")
+```
+
+## 3. 进阶功能详解
+
+### 3.1 大文件处理
+
+#### 3.1.1 使用 rows 参数预览数据
+
+当处理大文件时,您可以使用 `rows` 参数只读取前几行数据,快速了解数据结构,而不需要加载整个文件:
+
+```python
+from app.io.readers import read_vector
+from pathlib import Path
+
+# 只读取前10行数据进行预览
+large_file_path = Path("path/to/large_file.shp")
+gdf_preview = read_vector(large_file_path, rows=10)
+
+print(f"预览数据包含 {len(gdf_preview)} 条记录")
+print(gdf_preview.head())
+print(gdf_preview.columns)
+```
+
+#### 3.1.2 使用 chunk_size 分块读取
+
+对于非常大的文件,您可以使用 `chunk_size` 参数进行分块读取,逐块处理数据,避免内存溢出:
+
+```python
+from app.io.readers import read_vector
+from pathlib import Path
+
+# 分块读取,每块10000条数据
+large_file_path = Path("path/to/large_file.shp")
+
+# 使用 for 循环处理每个数据块
+for i, chunk in enumerate(read_vector(large_file_path, chunk_size=10000)):
+ print(f"处理第 {i+1} 块数据,包含 {len(chunk)} 条记录")
+
+ # 在这里进行您的处理操作
+ # 例如:计算缓冲区
+ chunk["buffer"] = chunk.geometry.apply(lambda geom: buffer_geometry(geom, distance=0.1))
+
+ # 保存当前块的结果
+ output_path = Path(f"output/chunk_{i+1}.geojson")
+ write_vector(chunk, output_path)
+ print(f"第 {i+1} 块结果已保存")
+```
+
+### 3.2 坐标系转换
+
+#### 3.2.1 什么时候需要转换坐标系?
+
+- 当您需要进行距离、面积计算时,地理坐标系(如 EPSG:4326)的单位是度,不适合直接计算
+- 当您需要将数据与其他不同坐标系的数据集叠加时
+- 当您需要使用特定坐标系的工具或服务时
+
+#### 3.2.2 如何转换坐标系
+
+```python
+from app.io.readers import read_vector
+from app.core.projection import reproject
+from pathlib import Path
+
+# 读取数据
+data_path = Path("data/sample/sample_points.geojson")
+gdf = read_vector(data_path)
+print(f"原始坐标系:{gdf.crs}")
+
+# 转换到 Web Mercator 坐标系(EPSG:3857)
+gdf_3857 = reproject(gdf, "EPSG:3857")
+print(f"转换后坐标系:{gdf_3857.crs}")
+
+# 或者在读取时直接指定目标坐标系
+gdf_direct = read_vector(data_path, crs="EPSG:3857")
+print(f"直接指定坐标系:{gdf_direct.crs}")
+```
+
+### 3.3 空间分析
+
+#### 3.3.1 缓冲区分析
+
+```python
+from app.io.readers import read_vector
+from app.core.geometry import buffer_geometry
+from pathlib import Path
+
+# 读取数据
+data_path = Path("data/sample/sample_points.geojson")
+gdf = read_vector(data_path)
+
+# 创建不同距离的缓冲区
+gdf["buffer_05"] = gdf.geometry.apply(lambda geom: buffer_geometry(geom, distance=0.05))
+gdf["buffer_10"] = gdf.geometry.apply(lambda geom: buffer_geometry(geom, distance=0.1))
+
+# 保存结果
+output_path = Path("output/buffers.geojson")
+write_vector(gdf, output_path)
+print(f"缓冲区分析结果已保存至:{output_path}")
+```
+
+#### 3.3.2 空间叠加分析
+
+```python
+from app.io.readers import read_vector
+from app.analysis.spatial_ops import overlay
+from pathlib import Path
+
+# 读取两个数据集
+points_path = Path("data/sample/sample_points.geojson")
+regions_path = Path("data/sample/sample_regions.geojson")
+
+points = read_vector(points_path)
+regions = read_vector(regions_path)
+
+# 执行空间叠加(交集)
+overlay_result = overlay(points, regions, how="intersection")
+print(f"叠加结果包含 {len(overlay_result)} 条记录")
+
+# 保存结果
+output_path = Path("output/overlay_result.geojson")
+write_vector(overlay_result, output_path)
+print(f"叠加分析结果已保存至:{output_path}")
+```
+
+#### 3.3.3 最近邻查找
+
+```python
+from app.core.geometry import distance_between
+from app.io.readers import read_vector
+from pathlib import Path
+
+# 读取点数据集
+points_path = Path("data/sample/sample_points.geojson")
+gdf = read_vector(points_path)
+
+# 定义目标点
+target_point = gdf.geometry.iloc[0]
+print(f"目标点:{target_point}")
+
+# 计算每个点到目标点的距离
+gdf["distance"] = gdf.geometry.apply(lambda geom: distance_between(geom, target_point))
+
+# 找到最近的点
+nearest_point = gdf.loc[gdf["distance"].idxmin()]
+print(f"最近的点:{nearest_point.geometry}")
+print(f"距离:{nearest_point['distance']}")
+```
+
+## 4. 常见问题排查
+
+### 4.1 报错“文件打不开”怎么办?
+
+**可能原因:**
+- 文件路径不存在
+- 文件路径包含中文或特殊字符
+- 文件格式不支持
+- 文件损坏
+
+**解决方案:**
+1. 检查文件路径是否正确,使用绝对路径
+2. 确保文件路径不包含中文或特殊字符
+3. 确认文件格式是否被支持(GeoJSON、Shapefile、GPKG、GDB等)
+4. 尝试使用其他软件打开文件,确认文件是否损坏
+
+**示例代码:**
+
+```python
+from app.io.readers import read_vector
+from pathlib import Path
+
+# 使用绝对路径
+try:
+ file_path = Path("c:/data/my_shapefile.shp")
+ gdf = read_vector(file_path)
+ print("文件读取成功!")
+except Exception as e:
+ print(f"文件读取失败:{e}")
+ # 检查路径是否存在
+ if not file_path.exists():
+ print("错误:文件路径不存在")
+ # 检查文件扩展名
+ if file_path.suffix not in [".shp", ".geojson", ".gpkg"]:
+ print("错误:文件格式可能不支持")
+```
+
+### 4.2 报错“几何无效”怎么办?
+
+**可能原因:**
+- 几何数据损坏
+- 几何自相交
+- 几何为空
+
+**解决方案:**
+1. 使用 `fix_geometry` 函数尝试修复无效几何
+2. 使用 `is_valid_geometry` 函数检查几何有效性
+3. 过滤掉无效几何
+
+**示例代码:**
+
+```python
+from app.core.geometry import fix_geometry, is_valid_geometry
+from app.io.readers import read_vector
+from pathlib import Path
+
+# 读取数据
+data_path = Path("data/sample/sample_points.geojson")
+gdf = read_vector(data_path)
+
+# 检查并修复几何
+print("检查几何有效性...")
+valid_count = 0
+fixed_count = 0
+
+for i, geom in enumerate(gdf.geometry):
+ if is_valid_geometry(geom):
+ valid_count += 1
+ else:
+ # 尝试修复
+ fixed_geom = fix_geometry(geom)
+ if fixed_geom is not None:
+ gdf.geometry.iloc[i] = fixed_geom
+ fixed_count += 1
+
+print(f"有效几何:{valid_count}")
+print(f"修复几何:{fixed_count}")
+print(f"无效几何:{len(gdf) - valid_count - fixed_count}")
+
+# 过滤掉仍然无效的几何
+gdf_valid = gdf[gdf.geometry.apply(is_valid_geometry)]
+print(f"过滤后剩余几何:{len(gdf_valid)}")
+```
+
+### 4.3 内存爆了怎么办?
+
+**可能原因:**
+- 文件太大,一次性加载到内存
+- 处理过程中创建了过多临时对象
+
+**解决方案:**
+1. 使用 `rows` 参数预览数据
+2. 使用 `chunk_size` 参数分块读取
+3. 处理完数据后及时释放内存
+4. 考虑使用更高效的数据结构和算法
+
+**示例代码:**
+
+```python
+from app.io.readers import read_vector
+from app.io.writers import write_vector
+from pathlib import Path
+
+# 分块处理大文件
+large_file = Path("path/to/large_file.shp")
+output_file = Path("output/processed_file.geojson")
+
+# 分块读取并处理
+chunks = []
+for i, chunk in enumerate(read_vector(large_file, chunk_size=10000)):
+ print(f"处理第 {i+1} 块...")
+
+ # 在这里进行处理
+ # 例如:添加面积列
+ chunk["area"] = chunk.geometry.area
+
+ chunks.append(chunk)
+
+# 合并所有块
+import geopandas as gpd
+result = gpd.GeoDataFrame(pd.concat(chunks, ignore_index=True))
+
+# 保存结果
+write_vector(result, output_file)
+print(f"处理完成,结果已保存至:{output_file}")
+
+# 释放内存
+import gc
+gc.collect()
+```
+
+## 5. 完整案例
+
+### 5.1 土地利用数据分析
+
+**场景:** 读取土地利用数据,筛选出耕地,计算耕地总面积,然后导出为 GeoJSON 文件。
+
+**步骤:**
+
+1. **读取数据**
+
+ ```python
+ from app.io.readers import read_vector
+ from pathlib import Path
+
+ # 读取土地利用数据
+ landuse_path = Path("data/landuse.shp")
+ # 先预览数据结构
+ landuse_preview = read_vector(landuse_path, rows=10)
+ print("数据列名:", list(landuse_preview.columns))
+ print("土地利用类型:", landuse_preview["type"].unique())
+
+ # 读取完整数据
+ landuse = read_vector(landuse_path)
+ print(f"总数据量:{len(landuse)}")
+ ```
+
+2. **筛选耕地**
+
+ ```python
+ # 假设耕地的类型代码是 "1" 或 "耕地"
+ # 根据实际数据结构调整条件
+ farmland = landuse[landuse["type"] == "耕地"]
+ print(f"耕地数量:{len(farmland)}")
+ ```
+
+3. **计算耕地总面积**
+
+ ```python
+ from app.core.projection import reproject
+
+ # 检查坐标系
+ print(f"原始坐标系:{farmland.crs}")
+
+ # 如果是地理坐标系,转换到投影坐标系以获得准确的面积
+ if farmland.crs and farmland.crs.to_epsg() == 4326:
+ # 转换到 UTM 坐标系(根据数据所在区域选择合适的 EPSG 代码)
+ farmland_proj = reproject(farmland, "EPSG:32649") # 示例:UTM 49N
+ print(f"转换后坐标系:{farmland_proj.crs}")
+ else:
+ farmland_proj = farmland
+
+ # 计算面积(单位:平方米)
+ farmland_proj["area"] = farmland_proj.geometry.area
+ total_area = farmland_proj["area"].sum()
+ print(f"耕地总面积:{total_area:.2f} 平方米")
+ print(f"耕地总面积:{total_area/10000:.2f} 公顷")
+ ```
+
+4. **导出结果**
+
+ ```python
+ from app.io.writers import write_vector
+
+ # 导出为 GeoJSON
+ output_path = Path("output/farmland.geojson")
+ write_vector(farmland_proj, output_path)
+ print(f"耕地数据已导出至:{output_path}")
+
+ # 导出面积统计
+ import pandas as pd
+ stats = pd.DataFrame({
+ "总耕地数量": [len(farmland)],
+ "总面积(平方米)": [total_area],
+ "总面积(公顷)": [total_area/10000]
+ })
+ stats_path = Path("output/farmland_stats.csv")
+ stats.to_csv(stats_path, index=False, encoding="utf-8-sig")
+ print(f"统计数据已导出至:{stats_path}")
+ ```
+
+## 6. 总结
+
+Geo-Tools 是一个功能强大的地理数据处理库,提供了丰富的空间分析工具和便捷的文件 I/O 功能。通过本教程,您应该已经掌握了:
+
+- 基本的文件读取和写入操作
+- 大文件的分块处理技巧
+- 坐标系转换的方法
+- 常见的空间分析操作
+- 常见问题的排查方法
+
+如果您在使用过程中遇到任何问题,可以参考本教程的常见问题排查部分,或者查看项目的详细文档。
+
+祝您使用愉快!
\ No newline at end of file
diff --git a/app/__init__.py b/app/__init__.py
new file mode 100644
index 0000000..161fbe6
--- /dev/null
+++ b/app/__init__.py
@@ -0,0 +1,63 @@
+"""
+geo_tools
+~~~~~~~~~
+专业地理信息数据处理工具库。
+
+核心依赖:geopandas、shapely、fiona、pyproj。
+
+快速开始
+--------
+>>> from geo_tools.io import readers
+>>> from geo_tools.core import vector
+>>> gdf = readers.read_vector("data/sample/sample_points.geojson")
+>>> gdf_proj = vector.reproject(gdf, "EPSG:3857")
+>>> print(gdf_proj.crs)
+
+GDB 读写
+--------
+>>> from geo_tools.io import readers, writers
+>>> layers = readers.list_gdb_layers("path/to/data.gdb")
+>>> gdf = readers.read_gdb("path/to/data.gdb", layer="my_layer")
+>>> writers.write_gdb(gdf, "output/result.gdb", layer="result_layer")
+
+要素类投影
+----------
+>>> from geo_tools.core import projection
+>>> gdf_proj = projection.reproject_gdf(gdf, "EPSG:4490")
+>>> gdf_utm = projection.reproject_gdf(gdf, auto_utm=True)
+"""
+
+from importlib.metadata import PackageNotFoundError, version
+
+# ── 版本 ──────────────────────────────────────────────────────────────────────
+try:
+ __version__ = version("geo-tools")
+except PackageNotFoundError:
+ __version__ = "0.1.0-dev"
+
+# ── 配置 & 日志 ───────────────────────────────────────────────────────────────
+from .io import readers, writers
+from .config.settings import settings
+from .utils.logger import get_logger, set_global_level
+from .utils.validators import (
+ SUPPORTED_VECTOR_EXTENSIONS,
+ is_supported_vector_format,
+ is_valid_crs,
+ validate_crs,
+ validate_geometry,
+ validate_vector_path,
+)
+
+__all__ = [
+ "__version__",
+ "settings",
+ # utils
+ "get_logger",
+ "set_global_level",
+ "is_valid_crs",
+ "validate_crs",
+ "validate_geometry",
+ "is_supported_vector_format",
+ "validate_vector_path",
+ "SUPPORTED_VECTOR_EXTENSIONS",
+]
diff --git a/app/analysis/__init__.py b/app/analysis/__init__.py
new file mode 100644
index 0000000..9ff13ca
--- /dev/null
+++ b/app/analysis/__init__.py
@@ -0,0 +1 @@
+"""geo_tools.analysis 包 —— 空间分析层。"""
diff --git a/geo_tools/analysis/spatial_ops.py b/app/analysis/spatial_ops.py
similarity index 93%
rename from geo_tools/analysis/spatial_ops.py
rename to app/analysis/spatial_ops.py
index cb57d51..b542291 100644
--- a/geo_tools/analysis/spatial_ops.py
+++ b/app/analysis/spatial_ops.py
@@ -11,7 +11,7 @@ from typing import Any
import geopandas as gpd
import pandas as pd
-from geo_tools.utils.logger import get_logger
+from app.utils.logger import get_logger
logger = get_logger(__name__)
@@ -56,7 +56,7 @@ def buffer_and_overlay(
result = gpd.overlay(buffered, target, how=how, keep_geom_type=False)
if projected_crs:
- result = result.to_crs(original_crs)
+ result = result.to_crs(original_crs) # type: ignore
logger.info("叠置分析完成:%d 条结果", len(result))
return result
@@ -77,7 +77,7 @@ def overlay(
``"symmetric_difference"``、``"identity"``。
"""
if df1.crs != df2.crs:
- df2 = df2.to_crs(df1.crs)
+ df2 = df2.to_crs(df1.crs) # type: ignore
result = gpd.overlay(df1, df2, how=how, keep_geom_type=keep_geom_type)
logger.debug("overlay(%s):%d 条结果", how, len(result))
return result
@@ -108,7 +108,7 @@ def nearest_features(
连接了最近 target 属性的 source GDF(可能包含重复行,每行对应一个近邻)。
"""
if source.crs != target.crs:
- target = target.to_crs(source.crs)
+ target = target.to_crs(source.crs) # type: ignore
result = gpd.sjoin_nearest(
source,
@@ -141,7 +141,7 @@ def select_by_location(
满足条件的 source 子集。
"""
if source.crs != selector.crs:
- selector = selector.to_crs(source.crs)
+ selector = selector.to_crs(source.crs) # type: ignore
joined = gpd.sjoin(source, selector, how="inner", predicate=predicate)
result = source.loc[source.index.isin(joined.index)].copy()
diff --git a/geo_tools/analysis/stats.py b/app/analysis/stats.py
similarity index 94%
rename from geo_tools/analysis/stats.py
rename to app/analysis/stats.py
index b9540c4..27a9f55 100644
--- a/geo_tools/analysis/stats.py
+++ b/app/analysis/stats.py
@@ -10,7 +10,7 @@ import geopandas as gpd
import numpy as np
import pandas as pd
-from geo_tools.utils.logger import get_logger
+from app.utils.logger import get_logger
logger = get_logger(__name__)
@@ -55,7 +55,7 @@ def area_weighted_mean(
def _weighted(group: pd.DataFrame) -> float:
return float((group[value_col] * group["_area"]).sum() / group["_area"].sum())
- result = gdf.groupby(group_col).apply(_weighted, include_groups=False).rename("area_weighted_mean")
+ result = gdf.groupby(group_col).apply(_weighted, include_groups=False).rename("area_weighted_mean") # type: ignore[no-untyped-call]
area_sum = gdf.groupby(group_col)["_area"].sum().rename("total_area")
return pd.concat([result, area_sum], axis=1).reset_index()
@@ -97,7 +97,7 @@ def summarize_attributes(
subset = df[columns]
if group_col is None:
- return subset.agg(agg_funcs).T.rename_axis("column").reset_index()
+ return subset.agg(agg_funcs).T.rename_axis("column").reset_index() # type: ignore[no-untyped-call]
df_with_group = df[[group_col] + columns]
return df_with_group.groupby(group_col)[columns].agg(agg_funcs).reset_index()
@@ -125,7 +125,7 @@ def count_by_polygon(
含 ``count_col`` 列的 polygons 副本。
"""
if points.crs != polygons.crs:
- points = points.to_crs(polygons.crs)
+ points = points.to_crs(polygons.crs) # type: ignore
joined = gpd.sjoin(points, polygons, how="inner", predicate="within")
point_counts = joined.groupby("index_right").size().rename(count_col)
diff --git a/geo_tools/config/__init__.py b/app/config/__init__.py
similarity index 60%
rename from geo_tools/config/__init__.py
rename to app/config/__init__.py
index cf3de1c..45ee430 100644
--- a/geo_tools/config/__init__.py
+++ b/app/config/__init__.py
@@ -1,5 +1,5 @@
"""geo_tools.config 包 —— 全局配置层。"""
-from geo_tools.config.settings import GeoToolsSettings, settings
+from app.config.settings import GeoToolsSettings, settings
__all__ = ["GeoToolsSettings", "settings"]
diff --git a/geo_tools/config/project_enum.py b/app/config/project_enum.py
similarity index 100%
rename from geo_tools/config/project_enum.py
rename to app/config/project_enum.py
diff --git a/geo_tools/config/settings.py b/app/config/settings.py
similarity index 100%
rename from geo_tools/config/settings.py
rename to app/config/settings.py
diff --git a/app/core/__init__.py b/app/core/__init__.py
new file mode 100644
index 0000000..42eaf5f
--- /dev/null
+++ b/app/core/__init__.py
@@ -0,0 +1 @@
+"""geo_tools.core 包 —— 核心地理处理层。"""
diff --git a/app/core/geometry.py b/app/core/geometry.py
new file mode 100644
index 0000000..54c7cf7
--- /dev/null
+++ b/app/core/geometry.py
@@ -0,0 +1,469 @@
+"""
+geo_tools.core.geometry
+~~~~~~~~~~~~~~~~~~~~~~~
+基于 Shapely 2.x 的几何运算工具函数。
+"""
+
+from __future__ import annotations
+
+from typing import Literal, Sequence
+
+import shapely
+from shapely.geometry import (
+ LinearRing,
+ LineString,
+ MultiLineString,
+ MultiPoint,
+ MultiPolygon,
+ Point,
+ Polygon,
+)
+from shapely.geometry.base import BaseGeometry
+
+from app.utils.logger import get_logger
+from app.utils.validators import ensure_valid_geometry
+
+logger = get_logger(__name__)
+
+
+# ── 几何有效性 ────────────────────────────────────────────────────────────────
+
+def is_valid_geometry(geom: BaseGeometry | None) -> bool:
+ """判断几何对象是否有效(非空且通过 Shapely 合法性检查)。
+
+ Parameters
+ ----------
+ geom:
+ 输入几何对象,可为 None。
+
+ Returns
+ -------
+ bool
+ 如果几何对象有效且非空,返回 True;否则返回 False。
+ """
+ if geom is None:
+ return False
+ return bool(geom.is_valid and not geom.is_empty)
+
+
+def fix_geometry(geom: BaseGeometry | None) -> BaseGeometry | None:
+ """尝试修复无效几何。
+
+ 依次尝试:
+ 1. ``buffer(0)`` — 适合大多数自相交多边形
+ 2. ``make_valid``(Shapely 2.x)— 覆盖更多情形
+
+ Parameters
+ ----------
+ geom:
+ 输入几何对象,可为 None。
+
+ Returns
+ -------
+ BaseGeometry | None
+ 修复后的几何;无法修复时返回 ``None``。
+
+ Notes
+ -----
+ 对于复杂的无效几何,可能无法完全修复,此时会返回 None。
+ """
+ if geom is None:
+ return None
+ if geom.is_valid:
+ return geom
+
+ # 方法一:buffer(0)
+ try:
+ fixed = geom.buffer(0)
+ if fixed.is_valid and not fixed.is_empty:
+ return fixed
+ except Exception:
+ pass
+
+ # 方法二:shapely.make_valid(Shapely >= 1.8)
+ try:
+ fixed = shapely.make_valid(geom)
+ if fixed.is_valid and not fixed.is_empty:
+ return fixed
+ except Exception:
+ pass
+
+ logger.warning("无法修复几何:%r", geom.geom_type)
+ return None
+
+
+def explain_validity(geom: BaseGeometry) -> str:
+ """返回 Shapely 对该几何的有效性说明(英文)。
+
+ Parameters
+ ----------
+ geom:
+ 输入几何对象。
+
+ Returns
+ -------
+ str
+ Shapely 生成的有效性说明字符串。
+ """
+ from shapely.validation import explain_validity as _explain
+ return _explain(geom)
+
+
+# ── 基础几何运算 ───────────────────────────────────────────────────────────────
+
+def buffer_geometry(
+ geom: BaseGeometry,
+ distance: float,
+ cap_style: Literal["round", "square", "flat"] = "round",
+ join_style: Literal["round", "mitre", "bevel"] = "round",
+ resolution: int = 16,
+ verbose: bool = False,
+) -> BaseGeometry | None:
+ """对几何对象执行缓冲区运算。
+
+ Parameters
+ ----------
+ geom:
+ 输入几何。
+ distance:
+ 缓冲距离(单位与 CRS 一致;地理坐标系单位为度)。
+ cap_style:
+ 端头样式:"round"(圆角)、"square"(方角)、"flat"(平角)(仅线要素有效)。
+ join_style:
+ 转角样式:"round"(圆角)、"mitre"(斜角)、"bevel"(尖角)。
+ resolution:
+ 圆弧逼近精度(段数),默认 16。
+ verbose:
+ 是否输出修复几何的警告信息,默认 False。
+
+ Returns
+ -------
+ BaseGeometry | None
+ 缓冲区运算后的几何对象;失败时返回 None。
+ """
+ try:
+ geom = ensure_valid_geometry(geom, verbose)
+ return geom.buffer(distance, cap_style=cap_style, join_style=join_style, resolution=resolution)
+ except Exception as e:
+ logger.warning(f"缓冲区计算失败,已跳过,原因:{str(e)}")
+ return None
+
+
+def centroid(geom: BaseGeometry, verbose: bool = False) -> Point | None:
+ """返回几何的质心点。
+
+ Parameters
+ ----------
+ geom:
+ 输入几何。
+ verbose:
+ 是否输出修复几何的警告信息,默认 False。
+
+ Returns
+ -------
+ Point | None
+ 几何的质心点;失败时返回 None。
+ """
+ try:
+ geom = ensure_valid_geometry(geom, verbose)
+ return geom.centroid
+ except Exception as e:
+ logger.warning(f"质心计算失败,已跳过,原因:{str(e)}")
+ return None
+
+
+def bounding_box(geom: BaseGeometry, verbose: bool = False) -> Polygon | None:
+ """返回几何的最小外接矩形(BBOX)为多边形。
+
+ Parameters
+ ----------
+ geom:
+ 输入几何。
+ verbose:
+ 是否输出修复几何的警告信息,默认 False。
+
+ Returns
+ -------
+ Polygon | None
+ 最小外接矩形多边形;失败时返回 None。
+ """
+ try:
+ geom = ensure_valid_geometry(geom, verbose)
+ from shapely.geometry import box
+ return box(*geom.bounds)
+ except Exception as e:
+ logger.warning(f"最小外接矩形计算失败,已跳过,原因:{str(e)}")
+ return None
+
+
+def convex_hull(geom: BaseGeometry, verbose: bool = False) -> BaseGeometry | None:
+ """返回几何的凸包。
+
+ Parameters
+ ----------
+ geom:
+ 输入几何。
+ verbose:
+ 是否输出修复几何的警告信息,默认 False。
+
+ Returns
+ -------
+ BaseGeometry | None
+ 几何的凸包;失败时返回 None。
+ """
+ try:
+ geom = ensure_valid_geometry(geom, verbose)
+ return geom.convex_hull
+ except Exception as e:
+ logger.warning(f"凸包计算失败,已跳过,原因:{str(e)}")
+ return None
+
+
+# ── 集合运算 ──────────────────────────────────────────────────────────────────
+
+def intersect(geom_a: BaseGeometry, geom_b: BaseGeometry, verbose: bool = False) -> BaseGeometry | None:
+ """返回两几何的交集。
+
+ Parameters
+ ----------
+ geom_a:
+ 第一个几何。
+ geom_b:
+ 第二个几何。
+ verbose:
+ 是否输出修复几何的警告信息,默认 False。
+
+ Returns
+ -------
+ BaseGeometry | None
+ 两几何的交集;失败时返回 None。
+ """
+ try:
+ geom_a = ensure_valid_geometry(geom_a, verbose)
+ geom_b = ensure_valid_geometry(geom_b, verbose)
+ return geom_a.intersection(geom_b)
+ except Exception as e:
+ logger.warning(f"交集计算失败,已跳过,原因:{str(e)}")
+ return None
+
+
+def union(geom_a: BaseGeometry, geom_b: BaseGeometry, verbose: bool = False) -> BaseGeometry | None:
+ """返回两几何的并集。
+
+ Parameters
+ ----------
+ geom_a:
+ 第一个几何。
+ geom_b:
+ 第二个几何。
+ verbose:
+ 是否输出修复几何的警告信息,默认 False。
+
+ Returns
+ -------
+ BaseGeometry | None
+ 两几何的并集;失败时返回 None。
+ """
+ try:
+ geom_a = ensure_valid_geometry(geom_a, verbose)
+ geom_b = ensure_valid_geometry(geom_b, verbose)
+ return geom_a.union(geom_b)
+ except Exception as e:
+ logger.warning(f"并集计算失败,已跳过,原因:{str(e)}")
+ return None
+
+
+def difference(geom_a: BaseGeometry, geom_b: BaseGeometry, verbose: bool = False) -> BaseGeometry | None:
+ """返回 ``geom_a`` 减去 ``geom_b`` 的差集。
+
+ Parameters
+ ----------
+ geom_a:
+ 第一个几何。
+ geom_b:
+ 第二个几何。
+ verbose:
+ 是否输出修复几何的警告信息,默认 False。
+
+ Returns
+ -------
+ BaseGeometry | None
+ geom_a 减去 geom_b 的差集;失败时返回 None。
+ """
+ try:
+ geom_a = ensure_valid_geometry(geom_a, verbose)
+ geom_b = ensure_valid_geometry(geom_b, verbose)
+ return geom_a.difference(geom_b)
+ except Exception as e:
+ logger.warning(f"差集计算失败,已跳过,原因:{str(e)}")
+ return None
+
+
+def symmetric_difference(geom_a: BaseGeometry, geom_b: BaseGeometry, verbose: bool = False) -> BaseGeometry | None:
+ """返回两几何的对称差集(异或)。
+
+ Parameters
+ ----------
+ geom_a:
+ 第一个几何。
+ geom_b:
+ 第二个几何。
+ verbose:
+ 是否输出修复几何的警告信息,默认 False。
+
+ Returns
+ -------
+ BaseGeometry | None
+ 两几何的对称差集;失败时返回 None。
+ """
+ try:
+ geom_a = ensure_valid_geometry(geom_a, verbose)
+ geom_b = ensure_valid_geometry(geom_b, verbose)
+ return geom_a.symmetric_difference(geom_b)
+ except Exception as e:
+ logger.warning(f"对称差集计算失败,已跳过,原因:{str(e)}")
+ return None
+
+
+def unary_union(geoms: Sequence[BaseGeometry], verbose: bool = False) -> BaseGeometry | None:
+ """将多个几何合并为一个(等同于逐一 union)。
+
+ Parameters
+ ----------
+ geoms:
+ 几何对象序列。
+ verbose:
+ 是否输出修复几何的警告信息,默认 False。
+
+ Returns
+ -------
+ BaseGeometry | None
+ 合并后的几何对象;失败时返回 None。
+ """
+ try:
+ # 检查几何有效性并尝试修复
+ fixed_geoms = []
+ for i, geom in enumerate(geoms):
+ try:
+ fixed = ensure_valid_geometry(geom, verbose)
+ if fixed is not None:
+ fixed_geoms.append(fixed)
+ else:
+ # 无法修复的几何跳过
+ if verbose:
+ logger.warning(f"几何对象 {i} 无效且无法修复,已跳过")
+ except Exception as e:
+ logger.warning(f"几何对象 {i} 处理失败,已跳过,原因:{str(e)}")
+ if not fixed_geoms:
+ logger.warning("没有有效的几何对象可合并")
+ return None
+ return shapely.unary_union(fixed_geoms)
+ except Exception as e:
+ logger.warning(f"几何合并失败,已跳过,原因:{str(e)}")
+ return None
+
+
+# ── 空间关系判断 ───────────────────────────────────────────────────────────────
+
+def contains(geom_a: BaseGeometry, geom_b: BaseGeometry, verbose: bool = False) -> bool:
+ """判断 ``geom_a`` 是否完全包含 ``geom_b``。
+
+ Parameters
+ ----------
+ geom_a:
+ 第一个几何。
+ geom_b:
+ 第二个几何。
+ verbose:
+ 是否输出修复几何的警告信息,默认 False。
+
+ Returns
+ -------
+ bool
+ 如果 geom_a 完全包含 geom_b,返回 True;否则返回 False;失败时返回 False。
+ """
+ try:
+ geom_a = ensure_valid_geometry(geom_a, verbose)
+ geom_b = ensure_valid_geometry(geom_b, verbose)
+ return bool(geom_a.contains(geom_b))
+ except Exception as e:
+ logger.warning(f"包含关系判断失败,已返回 False,原因:{str(e)}")
+ return False
+
+
+def within(geom_a: BaseGeometry, geom_b: BaseGeometry, verbose: bool = False) -> bool:
+ """判断 ``geom_a`` 是否完全在 ``geom_b`` 内。
+
+ Parameters
+ ----------
+ geom_a:
+ 第一个几何。
+ geom_b:
+ 第二个几何。
+ verbose:
+ 是否输出修复几何的警告信息,默认 False。
+
+ Returns
+ -------
+ bool
+ 如果 geom_a 完全在 geom_b 内,返回 True;否则返回 False;失败时返回 False。
+ """
+ try:
+ geom_a = ensure_valid_geometry(geom_a, verbose)
+ geom_b = ensure_valid_geometry(geom_b, verbose)
+ return bool(geom_a.within(geom_b))
+ except Exception as e:
+ logger.warning(f"包含于关系判断失败,已返回 False,原因:{str(e)}")
+ return False
+
+
+def intersects(geom_a: BaseGeometry, geom_b: BaseGeometry, verbose: bool = False) -> bool:
+ """判断两几何是否相交(含边界接触)。
+
+ Parameters
+ ----------
+ geom_a:
+ 第一个几何。
+ geom_b:
+ 第二个几何。
+ verbose:
+ 是否输出修复几何的警告信息,默认 False。
+
+ Returns
+ -------
+ bool
+ 如果两几何相交,返回 True;否则返回 False;失败时返回 False。
+ """
+ try:
+ geom_a = ensure_valid_geometry(geom_a, verbose)
+ geom_b = ensure_valid_geometry(geom_b, verbose)
+ return bool(geom_a.intersects(geom_b))
+ except Exception as e:
+ logger.warning(f"相交关系判断失败,已返回 False,原因:{str(e)}")
+ return False
+
+
+def distance_between(geom_a: BaseGeometry, geom_b: BaseGeometry, verbose: bool = False) -> float:
+ """计算两几何间的最小距离(单位与 CRS 一致)。
+
+ Parameters
+ ----------
+ geom_a:
+ 第一个几何。
+ geom_b:
+ 第二个几何。
+ verbose:
+ 是否输出修复几何的警告信息,默认 False。
+
+ Returns
+ -------
+ float
+ 两几何间的最小距离;失败时返回无穷大。
+ """
+ try:
+ geom_a = ensure_valid_geometry(geom_a, verbose)
+ geom_b = ensure_valid_geometry(geom_b, verbose)
+ return geom_a.distance(geom_b)
+ except Exception as e:
+ logger.warning(f"距离计算失败,已返回无穷大,原因:{str(e)}")
+ return float('inf')
diff --git a/geo_tools/core/projection.py b/app/core/projection.py
similarity index 91%
rename from geo_tools/core/projection.py
rename to app/core/projection.py
index cbd424e..58c12c3 100644
--- a/geo_tools/core/projection.py
+++ b/app/core/projection.py
@@ -9,17 +9,22 @@ from typing import Sequence
from pyproj import CRS, Transformer
import geopandas as gpd
-from geo_tools.utils.logger import get_logger
+from app.utils.logger import get_logger
logger = get_logger(__name__)
def get_crs_info(crs_input: str | int) -> dict[str, str | int | None]:
"""获取 CRS 的基本信息。
+ Parameters
+ ----------
+ crs_input:
+ EPSG 代码(整数或 ``"EPSG:4326"`` 字符串)或 proj 字符串。
+
Returns
-------
dict
- 包含 ``name``、``epsg``、``unit``、``is_geographic``、``is_projected``。
+ 包含 ``name``、``epsg``、``unit``、``is_geographic``、``is_projected``、``datum``。
"""
crs = CRS.from_user_input(crs_input)
return {
@@ -152,7 +157,7 @@ def reproject_gdf(
与 ``auto_crs=True`` 二选一。
auto_crs:
为 ``True`` 时忽略 ``target_crs``,根据数据中心点自动推荐
- CGCS2000 高斯-克吕格 带号(适合面积/距离计算场景)。
+ CGCS2000 高斯-克吕格 带号(默认使用 3度分带)。
verbose:
为 ``True`` 时在日志中打印投影前后的 CRS 信息。
@@ -171,11 +176,11 @@ def reproject_gdf(
>>> # 指定目标 CRS
>>> gdf_proj = reproject_gdf(gdf, "EPSG:4490")
- >>> # 自动推荐 CGCS2000 高斯-克吕格 带号(用于面积计算)
+ >>> # 自动推荐 CGCS2000 高斯-克吕格 带号 默认使用 3度分带
>>> gdf_utm = reproject_gdf(gdf, auto_crs=True)
>>> # 配合 GDB 读取
- >>> gdf = read_gdb("data.gdb", layer="图斑")
+ >>> gdf = geo_tools.readers.read_vector("data.gdb/图斑")
>>> gdf_proj = reproject_gdf(gdf, "EPSG:4326")
"""
@@ -186,9 +191,9 @@ def reproject_gdf(
if auto_crs:
# 先统一到地理坐标系,再取中心点推荐 CGCS2000 高斯-克吕格 带号
if gdf.crs.is_projected:
- center = gdf.to_crs("EPSG:4490").geometry.unary_union.centroid
+ center = gdf.to_crs("EPSG:4490").geometry.union_all().centroid
else:
- center = gdf.geometry.unary_union.centroid
+ center = gdf.geometry.union_all().centroid
target_crs = suggest_projected_crs(center.x, center.y)
logger.info("auto_crs:自动推荐投影 CRS = %s", target_crs)
diff --git a/geo_tools/core/raster.py b/app/core/raster.py
similarity index 100%
rename from geo_tools/core/raster.py
rename to app/core/raster.py
diff --git a/geo_tools/core/vector.py b/app/core/vector.py
similarity index 92%
rename from geo_tools/core/vector.py
rename to app/core/vector.py
index f215c27..d9c14d8 100644
--- a/geo_tools/core/vector.py
+++ b/app/core/vector.py
@@ -6,13 +6,13 @@ geo_tools.core.vector
from __future__ import annotations
-from typing import Any
+from typing import Any, Literal
import geopandas as gpd
import pandas as pd
-from geo_tools.utils.logger import get_logger
-from geo_tools.utils.validators import validate_geometry
+from app.utils.logger import get_logger
+from app.utils.validators import validate_geometry
logger = get_logger(__name__)
@@ -82,7 +82,7 @@ def clip_to_extent(
result = gdf.clip(mask)
else:
if bbox.crs != gdf.crs:
- bbox = bbox.to_crs(gdf.crs)
+ bbox = bbox.to_crs(gdf.crs) # type: ignore
result = gdf.clip(bbox)
logger.debug("裁切完成:%d → %d 条", len(gdf), len(result))
@@ -119,6 +119,11 @@ def dissolve_by(
def explode_multipart(gdf: gpd.GeoDataFrame) -> gpd.GeoDataFrame:
"""将多部分几何(MultiPolygon 等)拆分为单部分要素。
+ Parameters
+ ----------
+ gdf:
+ 输入 GeoDataFrame。
+
Returns
-------
gpd.GeoDataFrame
@@ -144,10 +149,10 @@ def drop_invalid_geometries(gdf: gpd.GeoDataFrame, *, fix: bool = False) -> gpd.
return gdf
if fix:
- from geo_tools.core.geometry import fix_geometry
+ from app.core.geometry import fix_geometry
gdf = gdf.copy()
mask = ~gdf.geometry.is_valid | gdf.geometry.isna()
- gdf.loc[mask, "geometry"] = gdf.loc[mask, "geometry"].apply(fix_geometry)
+ gdf.loc[mask, "geometry"] = gdf.loc[mask, "geometry"].apply(fix_geometry) # type: ignore
logger.info("已修复 %d 个无效几何", stats["invalid"])
else:
before = len(gdf)
@@ -159,7 +164,7 @@ def drop_invalid_geometries(gdf: gpd.GeoDataFrame, *, fix: bool = False) -> gpd.
def spatial_join(
left: gpd.GeoDataFrame,
right: gpd.GeoDataFrame,
- how: str = "left",
+ how: Literal["left", "right", "inner"] = "left",
predicate: str = "intersects",
**kwargs: Any,
) -> gpd.GeoDataFrame:
@@ -177,7 +182,7 @@ def spatial_join(
空间谓词:``"intersects"``、``"contains"``、``"within"``、``"touches"``。
"""
if left.crs != right.crs:
- right = right.to_crs(left.crs)
+ right = right.to_crs(left.crs) # type: ignore
result = gpd.sjoin(left, right, how=how, predicate=predicate, **kwargs)
logger.debug("空间连接完成:%d 条结果", len(result))
return result
diff --git a/app/io/__init__.py b/app/io/__init__.py
new file mode 100644
index 0000000..c738e2c
--- /dev/null
+++ b/app/io/__init__.py
@@ -0,0 +1 @@
+"""geo_tools.io 包 —— 数据读写层。"""
diff --git a/geo_tools/io/readers.py b/app/io/readers.py
similarity index 57%
rename from geo_tools/io/readers.py
rename to app/io/readers.py
index 4f3e0b3..9201804 100644
--- a/geo_tools/io/readers.py
+++ b/app/io/readers.py
@@ -15,14 +15,15 @@ geo_tools.io.readers
from __future__ import annotations
+import os
from pathlib import Path
-from typing import Any
+from typing import Any, Generator
import fiona
import geopandas as gpd
-from geo_tools.utils.logger import get_logger
-from geo_tools.utils.validators import validate_vector_path
+from app.utils.logger import get_logger
+from app.utils.validators import validate_vector_path
logger = get_logger(__name__)
@@ -34,8 +35,10 @@ def read_vector(
layer: str | int | None = None,
crs: str | int | None = None,
encoding: str = "utf-8",
+ chunk_size: int | None = None,
+ rows: int | None = None,
**kwargs: Any,
-) -> gpd.GeoDataFrame:
+):
"""统一的矢量数据读取入口,自动识别文件格式。
Parameters
@@ -48,19 +51,46 @@ def read_vector(
读取后强制重投影到目标 CRS(不传则保留原始 CRS)。
encoding:
属性表编码,Shapefile 中文路径常需指定 ``"gbk"``。
+ chunk_size:
+ 分块大小,默认 None(一次性读取全部数据)。
+ 【警告】:若不设置 chunk_size,大文件可能会占用大量内存。
+ rows:
+ 限制读取的行数,默认 None(读取全部数据)。
+ 用于快速预览数据,避免读取大文件的全部内容。
**kwargs:
透传给 ``geopandas.read_file`` 的额外参数。
Returns
-------
- gpd.GeoDataFrame
+ gpd.GeoDataFrame 或生成器
+ 如果 chunk_size 为 None,返回完整的 GeoDataFrame;
+ 如果设置了 chunk_size,返回一个生成器,每次 yield 一个 GeoDataFrame 块。
+
+ 示例
+ -----
+ # 全量读取(老方法)
+ gdf = read_vector("data.shp")
+
+ # 分块读取(新方法)
+ for chunk in read_vector("large_data.shp", chunk_size=10000):
+ # 处理每个数据块
+ print(f"处理了 {len(chunk)} 条数据")
+ # 在这里做你的操作,比如计算、过滤等
+
+ # 只读取前 5 行数据(预览模式)
+ gdf_preview = read_vector("large_data.shp", rows=5)
+ print(gdf_preview.head())
"""
+ path, layer = _split_gdb_layer(path)
path = validate_vector_path(path)
suffix = path.suffix.lower()
logger.info("读取矢量数据:%s(格式:%s,图层:%s)", path, suffix or "目录", layer)
if suffix == ".csv":
+ # CSV 文件暂时不支持分块读取
+ if chunk_size is not None:
+ logger.warning("CSV 文件暂不支持分块读取,将一次性读取全部数据")
return _read_csv_vector(path, crs=crs, **kwargs)
# fiona / geopandas 通用读取
@@ -68,14 +98,60 @@ def read_vector(
if layer is not None:
read_kwargs["layer"] = layer
- gdf = gpd.read_file(str(path), **read_kwargs)
+ # 分块读取模式
+ if chunk_size is not None:
+ def _chunk_generator():
+ logger.info("启用分块读取模式,每块 %d 条数据", chunk_size)
+ try:
+ # 使用 fiona 打开文件
+ with fiona.open(str(path), **read_kwargs) as src:
+ # 获取坐标系信息
+ crs_info = src.crs
+ # 分块读取
+ features = []
+ for i, feature in enumerate(src):
+ # 检查是否达到行数限制
+ if rows is not None and i >= rows:
+ break
+
+ features.append(feature)
+ if (i + 1) % chunk_size == 0:
+ # 创建 GeoDataFrame 并设置 CRS
+ gdf = gpd.GeoDataFrame.from_features(features, crs=crs_info)
+ # 重投影
+ if crs is not None:
+ gdf = gdf.to_crs(crs) # type: ignore
+ logger.debug("读取并处理第 %d 块数据,共 %d 条", (i + 1) // chunk_size, len(gdf))
+ yield gdf
+ features = []
+ # 处理最后一块
+ if features:
+ gdf = gpd.GeoDataFrame.from_features(features, crs=crs_info)
+ if crs is not None:
+ gdf = gdf.to_crs(crs) # type: ignore
+ logger.debug("读取并处理最后一块数据,共 %d 条", len(gdf))
+ yield gdf
+ except Exception as exc:
+ raise RuntimeError(f"无法分块读取矢量数据:{exc}") from None
+ return _chunk_generator()
+ else:
+ # 一次性读取模式
+ try:
+ # 添加 rows 参数到读取参数中
+ if rows is not None:
+ read_kwargs["rows"] = rows
+ logger.info("限制读取行数:%d", rows)
+
+ gdf = gpd.read_file(str(path), **read_kwargs)
+ except Exception as exc:
+ raise RuntimeError(f"无法读取矢量数据:{exc}") from None
- if crs is not None:
- logger.debug("重投影到 %s", crs)
- gdf = gdf.to_crs(crs)
+ if crs is not None:
+ logger.debug("重投影到 %s", crs)
+ gdf = gdf.to_crs(crs) # type: ignore
- logger.info("读取完成:共 %d 条要素,CRS=%s", len(gdf), gdf.crs)
- return gdf
+ logger.info("读取完成:共 %d 条要素,CRS=%s", len(gdf), gdf.crs)
+ return gdf # type: ignore
# ── GDB 专用 ───────────────────────────────────────────────────────────────────
@@ -125,10 +201,10 @@ def read_gdb(
gdf = gpd.read_file(str(gdb_path), layer=layer, encoding=encoding, **kwargs)
if crs is not None:
- gdf = gdf.to_crs(crs)
+ gdf = gdf.to_crs(crs) # type: ignore
logger.info("GDB 读取完成:%d 条要素,CRS=%s", len(gdf), gdf.crs)
- return gdf
+ return gdf # type: ignore
def list_gdb_layers(gdb_path: str | Path) -> list[str]:
@@ -154,6 +230,42 @@ def list_gdb_layers(gdb_path: str | Path) -> list[str]:
f"原始错误:{exc}"
) from exc
+def _split_gdb_layer(path: str | Path) -> tuple[Path, str | None]:
+ """从完整路径中分离 GDB 数据库路径和图层名。
+
+ Parameters
+ ----------
+ path:
+ 完整路径,可以是字符串或 Path 对象。
+
+ Returns
+ -------
+ tuple[Path, str | None]
+ (gdb_path, layer_name),其中 gdb_path 是 GDB 目录路径,layer_name 是图层名,若没有图层名则为 None。
+ """
+ path_obj = Path(path)
+ str_path = str(path_obj)
+
+ # 查找 .gdb 的位置
+ gdb_pos = str_path.find('.gdb')
+
+ if gdb_pos == -1:
+ # 如果没有 .gdb,整个路径作为 GDB 路径,没有图层
+ return path_obj, None
+
+ # 提取 GDB 路径(包含 .gdb)
+ gdb_path = str_path[:gdb_pos + 4]
+
+ # 提取图层名(.gdb 之后的部分)
+ layer_part = str_path[gdb_pos + 4:]
+ # 去除开头的路径分隔符
+ layer_name = layer_part.lstrip(os.sep).lstrip('/').lstrip('\\')
+
+ # 如果没有图层名,返回 None
+ if not layer_name:
+ layer_name = None
+
+ return Path(gdb_path), layer_name
# ── GPKG 专用 ──────────────────────────────────────────────────────────────────
@@ -188,12 +300,23 @@ def read_gpkg(
gdf = gpd.read_file(str(gpkg_path), layer=layer, **kwargs)
if crs is not None:
- gdf = gdf.to_crs(crs)
- return gdf
+ gdf = gdf.to_crs(crs) # type: ignore
+ return gdf # type: ignore
def list_gpkg_layers(gpkg_path: str | Path) -> list[str]:
- """列出 GeoPackage 中所有图层名称。"""
+ """列出 GeoPackage 中所有图层名称。
+
+ Parameters
+ ----------
+ gpkg_path:
+ GeoPackage 文件路径,可以是字符串或 Path 对象。
+
+ Returns
+ -------
+ list[str]
+ 图层名称列表。
+ """
return fiona.listlayers(str(gpkg_path))
diff --git a/geo_tools/io/writers.py b/app/io/writers.py
similarity index 99%
rename from geo_tools/io/writers.py
rename to app/io/writers.py
index 63d4619..8e56e20 100644
--- a/geo_tools/io/writers.py
+++ b/app/io/writers.py
@@ -17,7 +17,7 @@ from typing import Any, Literal
import geopandas as gpd
-from geo_tools.utils.logger import get_logger
+from app.utils.logger import get_logger
logger = get_logger(__name__)
diff --git a/geo_tools/utils/__init__.py b/app/utils/__init__.py
similarity index 73%
rename from geo_tools/utils/__init__.py
rename to app/utils/__init__.py
index b8a88e0..b96baa4 100644
--- a/geo_tools/utils/__init__.py
+++ b/app/utils/__init__.py
@@ -1,8 +1,8 @@
"""geo_tools.utils 包 —— 通用工具函数。"""
-from geo_tools.utils.config import load_config, load_json_config, load_toml_config, load_yaml_config
-from geo_tools.utils.logger import get_logger, set_global_level
-from geo_tools.utils.validators import (
+from app.utils.config import load_config, load_json_config, load_toml_config, load_yaml_config
+from app.utils.logger import get_logger, set_global_level
+from app.utils.validators import (
SUPPORTED_VECTOR_EXTENSIONS,
is_supported_vector_format,
is_valid_crs,
diff --git a/geo_tools/utils/config.py b/app/utils/config.py
similarity index 100%
rename from geo_tools/utils/config.py
rename to app/utils/config.py
diff --git a/geo_tools/utils/logger.py b/app/utils/logger.py
similarity index 98%
rename from geo_tools/utils/logger.py
rename to app/utils/logger.py
index c1cd4ea..f2621f8 100644
--- a/geo_tools/utils/logger.py
+++ b/app/utils/logger.py
@@ -50,7 +50,7 @@ def get_logger(
logging.Logger
"""
# 延迟导入,避免循环依赖
- from geo_tools.config.settings import settings as _settings
+ from app.config.settings import settings as _settings
if level is None:
level = _settings.log_level
diff --git a/geo_tools/utils/validators.py b/app/utils/validators.py
similarity index 79%
rename from geo_tools/utils/validators.py
rename to app/utils/validators.py
index 32c0bad..f75a5d6 100644
--- a/geo_tools/utils/validators.py
+++ b/app/utils/validators.py
@@ -73,11 +73,9 @@ def validate_geometry(gdf: "gpd.GeoDataFrame", *, raise_on_invalid: bool = False
dict
包含 ``total``、``valid``、``invalid``、``null`` 计数。
"""
- import geopandas as gpd # noqa: F811
-
null_count = gdf.geometry.isna().sum()
non_null = gdf.geometry.dropna()
- invalid_mask = ~non_null.is_valid
+ invalid_mask = ~non_null.is_valid # type: ignore
invalid_count = int(invalid_mask.sum())
valid_count = len(non_null) - invalid_count
@@ -114,7 +112,18 @@ SUPPORTED_VECTOR_EXTENSIONS: dict[str, str] = {
def is_supported_vector_format(path: str | Path) -> bool:
- """判断路径是否为已知的矢量格式。"""
+ """判断路径是否为已知的矢量格式。
+
+ Parameters
+ ----------
+ path:
+ 输入路径,可以是字符串或 Path 对象。
+
+ Returns
+ -------
+ bool
+ 如果路径是支持的矢量格式,返回 True;否则返回 False。
+ """
path = Path(path)
suffix = path.suffix.lower()
# .gdb 可能是目录(FileGDB)
@@ -143,3 +152,32 @@ def validate_vector_path(path: str | Path) -> Path:
f"支持的格式:{list(SUPPORTED_VECTOR_EXTENSIONS.keys())}"
)
return path
+
+
+def ensure_valid_geometry(geom: 'BaseGeometry', verbose: bool = False) -> 'BaseGeometry':
+ """确保几何对象有效,无效时尝试修复。
+
+ Parameters
+ ----------
+ geom:
+ 输入几何。
+ verbose:
+ 是否输出修复几何的警告信息。
+
+ Returns
+ -------
+ BaseGeometry
+ 有效的几何对象(可能是修复后的)。
+ """
+ from app.core.geometry import fix_geometry
+ from app.utils.logger import get_logger
+
+ logger = get_logger(__name__)
+
+ if not geom.is_valid:
+ fixed_geom = fix_geometry(geom)
+ if fixed_geom is not None:
+ if verbose:
+ logger.warning("几何对象无效,已自动修复")
+ return fixed_geom
+ return geom
diff --git a/docs/projection_guide.md b/docs/projection_guide.md
deleted file mode 100644
index bd11818..0000000
--- a/docs/projection_guide.md
+++ /dev/null
@@ -1,248 +0,0 @@
-# geo_tools.core.projection 使用说明
-
-> 坐标系查询、坐标转换、投影推荐工具,基于 [pyproj](https://pyproj4.github.io/pyproj/)。
-
----
-
-## 导入方式
-
-```python
-# 推荐:从顶层包导入
-from geo_tools.core.projection import (
- get_crs_info,
- crs_to_epsg,
- transform_coordinates,
- transform_point,
- suggest_projected_crs,
- WGS84, CGCS2000, WEB_MERCATOR, CGCS2000_UTM_50N,
-)
-
-# 或直接通过 geo_tools 导入
-import geo_tools
-```
-
----
-
-## CRS 常量
-
-模块内置了中国地理信息处理中最常用的 CRS 快捷常量,可直接作为参数传入所有函数:
-
-| 常量名 | EPSG | 说明 |
-|--------|------|------|
-| `WGS84` | `EPSG:4326` | WGS84 地理坐标系(经纬度,最通用) |
-| `CGCS2000` | `EPSG:4490` | 中国国家大地坐标系 2000(经纬度) |
-| `WEB_MERCATOR` | `EPSG:3857` | Web Mercator 投影(网络地图常用,单位:米) |
-| `CGCS2000_UTM_50N` | `EPSG:4508` | CGCS2000 / 3° 高斯-克吕格 50 带(单位:米) |
-
-```python
-from geo_tools.core.projection import WGS84, CGCS2000
-
-# 直接用常量替代字符串
-gdf = gdf.to_crs(CGCS2000)
-```
-
----
-
-## 函数说明
-
-### `get_crs_info(crs_input)` — 查询 CRS 信息
-
-返回坐标系的详细描述字典,方便快速了解一个未知 EPSG 的含义。
-
-**参数**
-- `crs_input`:EPSG 代码字符串(如 `"EPSG:4523"`)、整数(如 `4523`)或 proj 字符串。
-
-**返回值**(`dict`)
-
-| 键 | 含义 |
-|----|------|
-| `name` | 坐标系名称 |
-| `epsg` | EPSG 整数编号(无法识别时为 `None`) |
-| `unit` | 坐标单位(`degree` / `metre`) |
-| `is_geographic` | 是否为地理坐标系(经纬度) |
-| `is_projected` | 是否为投影坐标系(平面直角) |
-| `datum` | 基准面名称 |
-
-```python
-from geo_tools.core.projection import get_crs_info
-
-# 查询读取到的 GDB 数据的 CRS 含义
-info = get_crs_info("EPSG:4523")
-print(info)
-# {
-# 'name': 'CGCS2000 / 3-degree Gauss-Kruger zone 45',
-# 'epsg': 4523,
-# 'unit': 'metre',
-# 'is_geographic': False,
-# 'is_projected': True,
-# 'datum': 'China Geodetic Coordinate System 2000'
-# }
-
-# 直接传整数
-info = get_crs_info(32650)
-print(info["name"]) # WGS 84 / UTM zone 50N
-```
-
----
-
-### `crs_to_epsg(crs_input)` — 获取 EPSG 编号
-
-将任意 CRS 描述转为整数 EPSG 编号,无法识别时返回 `None`(不抛异常)。
-
-```python
-from geo_tools.core.projection import crs_to_epsg
-
-epsg = crs_to_epsg("EPSG:4490")
-print(epsg) # 4490
-
-epsg = crs_to_epsg("WGS 84")
-print(epsg) # 4326
-
-epsg = crs_to_epsg("invalid_crs")
-print(epsg) # None
-```
-
----
-
-### `transform_coordinates(xs, ys, source_crs, target_crs)` — 批量坐标转换
-
-将一组坐标点从源坐标系批量转换到目标坐标系,返回转换后的 `(xs, ys)` 列表。
-
-**参数**
-- `xs`:X 坐标序列(地理 CRS 时为**经度**)
-- `ys`:Y 坐标序列(地理 CRS 时为**纬度**)
-- `source_crs`:源坐标系
-- `target_crs`:目标坐标系
-- `always_xy`(关键字参数):强制按 (经度/X, 纬度/Y) 顺序处理,默认 `True`,**建议不修改**
-
-```python
-from geo_tools.core.projection import transform_coordinates, WGS84, WEB_MERCATOR
-
-# 将北京、上海、广州的 WGS84 经纬度转为 Web Mercator 米制坐标
-lons = [116.4074, 121.4737, 113.2644]
-lats = [39.9042, 31.2304, 23.1291]
-
-xs, ys = transform_coordinates(lons, lats, WGS84, WEB_MERCATOR)
-print(xs) # [12959618.8, 13521606.3, 12608870.0](单位:米)
-print(ys) # [4859767.2, 3649094.2, 2641877.0]
-
-# 国家坐标系转换:CGCS2000 经纬度 → CGCS2000 3° 高斯带(50带)
-from geo_tools.core.projection import CGCS2000, CGCS2000_UTM_50N
-xs_proj, ys_proj = transform_coordinates(lons, lats, CGCS2000, CGCS2000_UTM_50N)
-```
-
----
-
-### `transform_point(x, y, source_crs, target_crs)` — 单点坐标转换
-
-`transform_coordinates` 的单点版本,直接返回 `(x, y)` 元组。
-
-```python
-from geo_tools.core.projection import transform_point, WGS84, CGCS2000
-
-# 单点:WGS84 → CGCS2000(两者数值非常接近,差异在毫米级)
-x, y = transform_point(116.4074, 39.9042, WGS84, CGCS2000)
-print(f"CGCS2000 坐标:经度={x:.6f}, 纬度={y:.6f}")
-
-# 单点:经纬度 → 投影坐标(米)
-from geo_tools.core.projection import WEB_MERCATOR
-mx, my = transform_point(116.4074, 39.9042, WGS84, WEB_MERCATOR)
-print(f"墨卡托坐标:X={mx:.2f}m, Y={my:.2f}m")
-```
-
----
-
-### `suggest_projected_crs(lon, lat)` — 自动推荐投影 CRS
-
-根据数据中心坐标(WGS84 经纬度)自动推荐适合**面积/距离计算**的 UTM 投影带,避免在地理坐标系下计算面积出错。
-
-**参数**
-- `lon`:中心经度(WGS84)
-- `lat`:中心纬度(WGS84,北半球为正)
-
-**返回值**:EPSG 代码字符串,如 `"EPSG:32650"`
-
-```python
-from geo_tools.core.projection import suggest_projected_crs
-
-# 云南马关县(约 104.4°E, 23.0°N)
-proj_crs = suggest_projected_crs(lon=104.4, lat=23.0)
-print(proj_crs) # EPSG:32648 (WGS84 UTM zone 48N)
-
-# 北京(116.4°E, 39.9°N)
-proj_crs = suggest_projected_crs(lon=116.4, lat=39.9)
-print(proj_crs) # EPSG:32650 (WGS84 UTM zone 50N)
-
-# 实际场景:读取 GDB 后用推荐的投影计算面积
-import geo_tools
-
-gdf = geo_tools.read_gdb("data.gdb", layer="图斑")
-cx, cy = gdf.geometry.unary_union.centroid.x, gdf.geometry.unary_union.centroid.y
-
-# 如果数据是投影坐标系(单位:米),先转到地理坐标系再推荐
-if gdf.crs.is_projected:
- cx, cy = geo_tools.transform_point(cx, cy, gdf.crs, "EPSG:4326")
-
-proj_crs = suggest_projected_crs(cx, cy)
-gdf_proj = geo_tools.reproject(gdf, proj_crs) # 重投影
-gdf_proj = geo_tools.add_area_column(gdf_proj) # 计算面积(单位:m²)
-```
-
----
-
-## 常见场景示例
-
-### 场景一:不认识数据的 CRS,先查一下
-
-```python
-import geo_tools
-
-gdf = geo_tools.read_gdb("临时数据库.gdb", layer="马关综合后图斑")
-# 读取完成:CRS=EPSG:4523
-
-info = geo_tools.get_crs_info(gdf.crs)
-print(info["name"]) # CGCS2000 / 3-degree Gauss-Kruger zone 45
-print(info["unit"]) # metre(投影坐标系,单位是米)
-print(info["is_projected"]) # True
-```
-
-### 场景二:统一坐标系后叠置分析
-
-```python
-import geo_tools
-from geo_tools.core.projection import CGCS2000
-
-layer_a = geo_tools.read_gdb("a.gdb", layer="林地") # EPSG:4523
-layer_b = geo_tools.read_vector("b.geojson") # EPSG:4326
-
-# 统一到 CGCS2000 地理坐标系后再做叠置
-layer_a = geo_tools.reproject(layer_a, CGCS2000)
-layer_b = geo_tools.reproject(layer_b, CGCS2000)
-
-result = geo_tools.overlay(layer_a, layer_b, how="intersection")
-```
-
-### 场景三:在地理坐标系数据上正确计算面积
-
-```python
-import geo_tools
-from geo_tools.core.projection import suggest_projected_crs
-
-gdf = geo_tools.read_vector("data.geojson") # EPSG:4326,单位是度
-
-# 自动推荐合适的投影
-proj = suggest_projected_crs(lon=105.0, lat=25.0) # 云贵地区
-
-gdf = geo_tools.add_area_column(gdf, projected_crs=proj)
-print(gdf["area_m2"].describe())
-```
-
----
-
-## 注意事项
-
-> [!WARNING]
-> 在**地理坐标系**(EPSG:4326 / 4490)下直接调用 `geometry.area` 得到的是"平方度",**不是平方米**,面积计算会严重失真。始终用 `add_area_column()` 或先 `reproject()` 到投影坐标系后再计算。
-
-> [!NOTE]
-> `WGS84`(EPSG:4326)与 `CGCS2000`(EPSG:4490)的坐标数值差异极小(通常 < 1 米),在普通精度的分析中可视为等价,但正式国家项目中必须使用 CGCS2000。
diff --git a/geo_tools/__init__.py b/geo_tools/__init__.py
deleted file mode 100644
index d0739a4..0000000
--- a/geo_tools/__init__.py
+++ /dev/null
@@ -1,40 +0,0 @@
-"""
-geo_tools
-~~~~~~~~~
-专业地理信息数据处理工具库。
-
-核心依赖:geopandas、shapely、fiona、pyproj。
-
-快速开始
---------
->>> import geo_tools
->>> gdf = geo_tools.read_vector("data/sample/sample_points.geojson")
->>> gdf_proj = geo_tools.reproject(gdf, "EPSG:3857")
->>> print(gdf_proj.crs)
-
-GDB 读写
---------
->>> layers = geo_tools.list_gdb_layers("path/to/data.gdb")
->>> gdf = geo_tools.read_gdb("path/to/data.gdb", layer="my_layer")
->>> geo_tools.write_gdb(gdf, "output/result.gdb", layer="result_layer")
-"""
-
-from importlib.metadata import PackageNotFoundError, version
-
-# ── 版本 ──────────────────────────────────────────────────────────────────────
-try:
- __version__ = version("geo-tools")
-except PackageNotFoundError:
- __version__ = "0.1.0-dev"
-
-# ── 配置 & 日志 ───────────────────────────────────────────────────────────────
-from geo_tools.config.settings import settings
-from geo_tools.utils.logger import get_logger, set_global_level
-
-__all__ = [
- "__version__",
- "settings",
- # utils
- "get_logger",
- "set_global_level"
-]
diff --git a/geo_tools/analysis/__init__.py b/geo_tools/analysis/__init__.py
deleted file mode 100644
index ae7beb7..0000000
--- a/geo_tools/analysis/__init__.py
+++ /dev/null
@@ -1,25 +0,0 @@
-"""geo_tools.analysis 包 —— 空间分析层。"""
-
-from geo_tools.analysis.spatial_ops import (
- buffer_and_overlay,
- nearest_features,
- overlay,
- select_by_location,
-)
-from geo_tools.analysis.stats import (
- area_weighted_mean,
- count_by_polygon,
- summarize_attributes,
-)
-
-__all__ = [
- # spatial_ops
- "buffer_and_overlay",
- "overlay",
- "nearest_features",
- "select_by_location",
- # stats
- "area_weighted_mean",
- "summarize_attributes",
- "count_by_polygon",
-]
diff --git a/geo_tools/core/__init__.py b/geo_tools/core/__init__.py
deleted file mode 100644
index 6562bde..0000000
--- a/geo_tools/core/__init__.py
+++ /dev/null
@@ -1,80 +0,0 @@
-"""geo_tools.core 包 —— 核心地理处理层。"""
-
-from geo_tools.core.geometry import (
- buffer_geometry,
- bounding_box,
- centroid,
- contains,
- convex_hull,
- difference,
- distance_between,
- explain_validity,
- fix_geometry,
- intersect,
- intersects,
- is_valid_geometry,
- symmetric_difference,
- unary_union,
- union,
- within,
-)
-from geo_tools.core.projection import (
- CGCS2000,
- CGCS2000_UTM_50N,
- WEB_MERCATOR,
- WGS84,
- crs_to_epsg,
- get_crs_info,
- suggest_projected_crs,
- transform_coordinates,
- transform_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,
-)
-
-__all__ = [
- # geometry
- "is_valid_geometry",
- "fix_geometry",
- "explain_validity",
- "buffer_geometry",
- "centroid",
- "bounding_box",
- "convex_hull",
- "intersect",
- "union",
- "difference",
- "symmetric_difference",
- "unary_union",
- "contains",
- "within",
- "intersects",
- "distance_between",
- # projection
- "WGS84",
- "CGCS2000",
- "WEB_MERCATOR",
- "CGCS2000_UTM_50N",
- "get_crs_info",
- "crs_to_epsg",
- "transform_coordinates",
- "transform_point",
- "suggest_projected_crs",
- # vector
- "reproject",
- "set_crs",
- "clip_to_extent",
- "dissolve_by",
- "explode_multipart",
- "drop_invalid_geometries",
- "spatial_join",
- "add_area_column",
-]
diff --git a/geo_tools/core/geometry.py b/geo_tools/core/geometry.py
deleted file mode 100644
index 5fcf449..0000000
--- a/geo_tools/core/geometry.py
+++ /dev/null
@@ -1,169 +0,0 @@
-"""
-geo_tools.core.geometry
-~~~~~~~~~~~~~~~~~~~~~~~
-基于 Shapely 2.x 的几何运算工具函数。
-"""
-
-from __future__ import annotations
-
-from typing import Sequence
-
-import shapely
-from shapely.geometry import (
- LinearRing,
- LineString,
- MultiLineString,
- MultiPoint,
- MultiPolygon,
- Point,
- Polygon,
-)
-from shapely.geometry.base import BaseGeometry
-
-from geo_tools.utils.logger import get_logger
-
-logger = get_logger(__name__)
-
-
-# ── 几何有效性 ────────────────────────────────────────────────────────────────
-
-def is_valid_geometry(geom: BaseGeometry | None) -> bool:
- """判断几何对象是否有效(非空且通过 Shapely 合法性检查)。"""
- if geom is None:
- return False
- return bool(geom.is_valid and not geom.is_empty)
-
-
-def fix_geometry(geom: BaseGeometry | None) -> BaseGeometry | None:
- """尝试修复无效几何。
-
- 依次尝试:
- 1. ``buffer(0)`` — 适合大多数自相交多边形
- 2. ``make_valid``(Shapely 2.x)— 覆盖更多情形
-
- Returns
- -------
- BaseGeometry | None
- 修复后的几何;无法修复时返回 ``None``。
- """
- if geom is None:
- return None
- if geom.is_valid:
- return geom
-
- # 方法一:buffer(0)
- try:
- fixed = geom.buffer(0)
- if fixed.is_valid and not fixed.is_empty:
- return fixed
- except Exception:
- pass
-
- # 方法二:shapely.make_valid(Shapely >= 1.8)
- try:
- fixed = shapely.make_valid(geom)
- if fixed.is_valid and not fixed.is_empty:
- return fixed
- except Exception:
- pass
-
- logger.warning("无法修复几何:%r", geom.geom_type)
- return None
-
-
-def explain_validity(geom: BaseGeometry) -> str:
- """返回 Shapely 对该几何的有效性说明(英文)。"""
- from shapely.validation import explain_validity as _explain
- return _explain(geom)
-
-
-# ── 基础几何运算 ───────────────────────────────────────────────────────────────
-
-def buffer_geometry(
- geom: BaseGeometry,
- distance: float,
- cap_style: int = 1,
- join_style: int = 1,
- resolution: int = 16,
-) -> BaseGeometry:
- """对几何对象执行缓冲区运算。
-
- Parameters
- ----------
- geom:
- 输入几何。
- distance:
- 缓冲距离(单位与 CRS 一致;地理坐标系单位为度)。
- cap_style:
- 端头样式:1=圆形,2=平头,3=方头(仅线要素有效)。
- join_style:
- 转角样式:1=圆角,2=斜角,3=尖角。
- resolution:
- 圆弧逼近精度(段数),默认 16。
- """
- return geom.buffer(distance, cap_style=cap_style, join_style=join_style, resolution=resolution)
-
-
-def centroid(geom: BaseGeometry) -> Point:
- """返回几何的质心点。"""
- return geom.centroid
-
-
-def bounding_box(geom: BaseGeometry) -> Polygon:
- """返回几何的最小外接矩形(BBOX)为多边形。"""
- from shapely.geometry import box
- return box(*geom.bounds)
-
-
-def convex_hull(geom: BaseGeometry) -> BaseGeometry:
- """返回几何的凸包。"""
- return geom.convex_hull
-
-
-# ── 集合运算 ──────────────────────────────────────────────────────────────────
-
-def intersect(geom_a: BaseGeometry, geom_b: BaseGeometry) -> BaseGeometry:
- """返回两几何的交集。"""
- return geom_a.intersection(geom_b)
-
-
-def union(geom_a: BaseGeometry, geom_b: BaseGeometry) -> BaseGeometry:
- """返回两几何的并集。"""
- return geom_a.union(geom_b)
-
-
-def difference(geom_a: BaseGeometry, geom_b: BaseGeometry) -> BaseGeometry:
- """返回 ``geom_a`` 减去 ``geom_b`` 的差集。"""
- return geom_a.difference(geom_b)
-
-
-def symmetric_difference(geom_a: BaseGeometry, geom_b: BaseGeometry) -> BaseGeometry:
- """返回两几何的对称差集(异或)。"""
- return geom_a.symmetric_difference(geom_b)
-
-
-def unary_union(geoms: Sequence[BaseGeometry]) -> BaseGeometry:
- """将多个几何合并为一个(等同于逐一 union)。"""
- return shapely.unary_union(list(geoms))
-
-
-# ── 空间关系判断 ───────────────────────────────────────────────────────────────
-
-def contains(geom_a: BaseGeometry, geom_b: BaseGeometry) -> bool:
- """判断 ``geom_a`` 是否完全包含 ``geom_b``。"""
- return bool(geom_a.contains(geom_b))
-
-
-def within(geom_a: BaseGeometry, geom_b: BaseGeometry) -> bool:
- """判断 ``geom_a`` 是否完全在 ``geom_b`` 内。"""
- return bool(geom_a.within(geom_b))
-
-
-def intersects(geom_a: BaseGeometry, geom_b: BaseGeometry) -> bool:
- """判断两几何是否相交(含边界接触)。"""
- return bool(geom_a.intersects(geom_b))
-
-
-def distance_between(geom_a: BaseGeometry, geom_b: BaseGeometry) -> float:
- """计算两几何间的最小距离(单位与 CRS 一致)。"""
- return float(geom_a.distance(geom_b))
diff --git a/geo_tools/io/__init__.py b/geo_tools/io/__init__.py
deleted file mode 100644
index ff9c62c..0000000
--- a/geo_tools/io/__init__.py
+++ /dev/null
@@ -1,31 +0,0 @@
-"""geo_tools.io 包 —— 数据读写层。"""
-
-from geo_tools.io.readers import (
- list_gdb_layers,
- list_gpkg_layers,
- read_csv_points,
- read_gdb,
- read_gpkg,
- read_vector,
-)
-from geo_tools.io.writers import (
- write_csv,
- write_gdb,
- write_gpkg,
- write_vector,
-)
-
-__all__ = [
- # readers
- "read_vector",
- "read_gdb",
- "list_gdb_layers",
- "read_gpkg",
- "list_gpkg_layers",
- "read_csv_points",
- # writers
- "write_vector",
- "write_gdb",
- "write_gpkg",
- "write_csv",
-]
diff --git a/scripts/example_workflow.py b/scripts/example_workflow.py
index 8d7533b..f30a4eb 100644
--- a/scripts/example_workflow.py
+++ b/scripts/example_workflow.py
@@ -17,8 +17,8 @@ from pathlib import Path
import sys
sys.path.insert(0, str(Path(__file__).parent.parent))
-import geo_tools
-from geo_tools.utils.logger import get_logger
+import app
+from app.utils.logger import get_logger
logger = get_logger("example_workflow")
@@ -29,37 +29,37 @@ OUTPUT_DIR.mkdir(exist_ok=True)
def main() -> None:
logger.info("=" * 60)
- logger.info("geo_tools 端到端工作流示例 v%s", geo_tools.__version__)
+ logger.info("geo_tools 端到端工作流示例 v%s", app.__version__)
logger.info("=" * 60)
# ── 1. 读取示例点数据 ──────────────────────────────────────────
logger.info("\n[步骤 1] 读取示例点数据(GeoJSON)")
- points = geo_tools.read_vector(DATA_DIR / "sample_points.geojson")
+ points = app.read_vector(DATA_DIR / "sample_points.geojson")
logger.info(" 读取完成:%d 条要素,CRS=%s", len(points), points.crs)
logger.info(" 字段:%s", list(points.columns))
# ── 2. 读取示例面数据 ──────────────────────────────────────────
logger.info("\n[步骤 2] 读取示例区域多边形(GeoJSON)")
- regions = geo_tools.read_vector(DATA_DIR / "sample_regions.geojson")
+ regions = app.read_vector(DATA_DIR / "sample_regions.geojson")
logger.info(" 区域列表:%s", regions["name"].tolist())
# ── 3. 数据校验 ───────────────────────────────────────────────
logger.info("\n[步骤 3] 几何有效性校验")
- stats = geo_tools.validate_geometry(points)
+ stats = app.validate_geometry(points)
logger.info(" 点数据校验结果:%s", stats)
- stats = geo_tools.validate_geometry(regions)
+ stats = app.validate_geometry(regions)
logger.info(" 面数据校验结果:%s", stats)
# ── 4. 坐标系信息 ─────────────────────────────────────────────
logger.info("\n[步骤 4] 查询 CRS 信息")
- crs_info = geo_tools.get_crs_info("EPSG:4326")
+ crs_info = app.get_crs_info("EPSG:4326")
logger.info(" WGS84 信息:%s", crs_info)
- proj_crs = geo_tools.suggest_projected_crs(116.4, 39.9)
+ proj_crs = app.suggest_projected_crs(116.4, 39.9)
logger.info(" 北京适合的投影 CRS:%s", proj_crs)
# ── 5. 重投影 ─────────────────────────────────────────────────
logger.info("\n[步骤 5] 重投影到 Web Mercator(用于可视化)")
- points_3857 = geo_tools.reproject(points, "EPSG:3857")
+ points_3857 = app.reproject(points, "EPSG:3857")
logger.info(" 重投影完成:CRS=%s", points_3857.crs)
# ── 6. 面积加权均值 ───────────────────────────────────────────
@@ -68,31 +68,31 @@ def main() -> None:
points_buffered = points.to_crs("EPSG:3857").copy()
points_buffered["geometry"] = points_buffered.geometry.buffer(100_000) # 100km缓冲
points_buffered = points_buffered.to_crs("EPSG:4326")
- from geo_tools.analysis.stats import area_weighted_mean
+ from app.analysis.stats import area_weighted_mean
aw_result = area_weighted_mean(points_buffered, value_col="value")
logger.info(" 全局面积加权均值:%.4f", aw_result["area_weighted_mean"])
# ── 7. 按位置选择 ─────────────────────────────────────────────
logger.info("\n[步骤 7] 按位置选择:筛选华南区域内的城市")
hua_nan = regions[regions["name"] == "华南"]
- points_in_huanan = geo_tools.select_by_location(points, hua_nan, predicate="intersects")
+ points_in_huanan = app.select_by_location(points, hua_nan, predicate="intersects")
logger.info(" 华南区域内的城市:%s", points_in_huanan["name"].tolist())
# ── 8. 统计汇总 ───────────────────────────────────────────────
logger.info("\n[步骤 8] 属性统计汇总")
- from geo_tools.analysis.stats import summarize_attributes
+ from app.analysis.stats import summarize_attributes
summary = summarize_attributes(points, columns=["value"], group_col="category")
logger.info(" 按分类汇总:\n%s", summary.to_string(index=False))
# ── 9. 写出结果 ───────────────────────────────────────────────
logger.info("\n[步骤 9] 写出处理结果")
out_geojson = OUTPUT_DIR / "result_points_3857.geojson"
- geo_tools.write_vector(points_3857, out_geojson)
+ app.write_vector(points_3857, out_geojson)
logger.info(" GeoJSON 写出:%s", out_geojson)
out_gpkg = OUTPUT_DIR / "results.gpkg"
- geo_tools.write_gpkg(points, out_gpkg, layer="original_points")
- geo_tools.write_gpkg(regions, out_gpkg, layer="regions", mode="a")
+ app.write_gpkg(points, out_gpkg, layer="original_points")
+ app.write_gpkg(regions, out_gpkg, layer="regions", mode="a")
logger.info(" GPKG 写出(2 图层):%s", out_gpkg)
logger.info("\n" + "=" * 60)
diff --git a/scripts/其他工具/A耕作层厚度栅格制作_新1.py b/scripts/其他工具/A耕作层厚度栅格制作_新1.py
new file mode 100644
index 0000000..5506c35
--- /dev/null
+++ b/scripts/其他工具/A耕作层厚度栅格制作_新1.py
@@ -0,0 +1,188 @@
+import os
+from pathlib import Path
+import time
+
+import geopandas as gpd
+from geopandas.io import file
+import pandas as pd
+import numpy as np
+
+def assign_gzchd_flexible_v2(soil_prop, point_path, polygon_path, output_path):
+ print("正在读取数据...")
+ points = gpd.read_file(point_path)
+ polygons = gpd.read_file(polygon_path)
+
+ # 1. 坐标系转换
+ if points.crs != polygons.crs:
+ print(f"坐标系不一致,正在转换点数据...")
+ points = points.to_crs(polygons.crs)
+
+ # 2. 预处理, 判断样点是否存在TZ字段,如果不存在,则用TDLYLX字段代替,并将其转为字符串类型,如果两个字段都不存在,则报错
+ if 'TZ' not in points.columns:
+ if 'TDLYLX' in points.columns:
+ points['TZ'] = points['TDLYLX'].astype(str).str.strip()
+ else:
+ raise ValueError("点要素类中不存在TZ或TDLYLX字段,无法进行匹配!")
+ else:
+ points['TZ'] = points['TZ'].astype(str).str.strip()
+
+ polygons['TZ'] = polygons['TZ'].astype(str).str.strip()
+
+ # 确保 GZCHD 是数值类型,避免合并时类型冲突
+ points[soil_prop] = pd.to_numeric(points[soil_prop], errors='coerce').fillna(0)
+
+ if soil_prop in polygons.columns:
+ polygons = polygons.drop(columns=[soil_prop])
+
+ # 辅助函数:按指定字段分组进行最近点匹配
+ def match_by_attribute(poly_gdf, pt_gdf, attr_name, suffix):
+ if attr_name not in poly_gdf.columns or attr_name not in pt_gdf.columns:
+ return None, []
+
+ poly_sub = poly_gdf[poly_gdf[attr_name].notna()].copy()
+ point_sub = pt_gdf[pt_gdf[attr_name].notna()].copy()
+
+ if poly_sub.empty or point_sub.empty:
+ return None, []
+
+ poly_sub[attr_name] = poly_sub[attr_name].astype(str).str.strip()
+ point_sub[attr_name] = point_sub[attr_name].astype(str).str.strip()
+
+ common_values = set(poly_sub[attr_name].unique()) & set(point_sub[attr_name].unique())
+ if not common_values:
+ return None, []
+
+ matched_parts = []
+ matched_ids = []
+ for value in common_values:
+ poly_part = poly_sub[poly_sub[attr_name] == value].copy()
+ point_part = point_sub[point_sub[attr_name] == value][[soil_prop, 'geometry']].copy()
+ if poly_part.empty or point_part.empty:
+ continue
+
+ matched_part = gpd.sjoin_nearest(poly_part, point_part, how='left', rsuffix=suffix)
+ matched_part = matched_part[~matched_part.index.duplicated(keep='first')]
+ if not matched_part.empty:
+ matched_parts.append(matched_part)
+ matched_ids.extend(matched_part.index.tolist())
+
+ if matched_parts:
+ return pd.concat(matched_parts), matched_ids
+ return None, []
+
+ matched_results = []
+ matched_indices = []
+
+ # --- 第一步:按相同 TZ 匹配 ---
+ print("步骤 1: 正在匹配相同 TZ 的最近点...")
+ first_matched, first_ids = match_by_attribute(polygons, points, 'TZ', '_p1')
+ if first_matched is not None and not first_matched.empty:
+ matched_results.append(first_matched)
+ matched_indices.extend(first_ids)
+
+ # --- 第二步:按 TS 匹配未匹配面 ---
+ unmatched_mask = ~polygons.index.isin(matched_indices)
+ remaining_polygons = polygons[unmatched_mask].copy()
+ print(f"步骤 2: 正在为 {len(remaining_polygons)} 个要素匹配 TS 最近点...")
+
+ if not remaining_polygons.empty:
+ ts_matched, ts_ids = match_by_attribute(remaining_polygons, points, 'TS', '_p_ts')
+ if ts_matched is not None and not ts_matched.empty:
+ matched_results.append(ts_matched)
+ matched_indices.extend(ts_ids)
+ remaining_polygons = polygons[~polygons.index.isin(matched_indices)].copy()
+ print(f"已匹配 TS: {len(ts_ids)} 个要素,剩余 {len(remaining_polygons)} 个。")
+ else:
+ print("未匹配到 TS 类型要素,继续下一步。")
+
+ # --- 第三步:按 YL 匹配未匹配面 ---
+ if not remaining_polygons.empty:
+ print(f"步骤 3: 正在为 {len(remaining_polygons)} 个要素匹配 YL 最近点...")
+ yl_matched, yl_ids = match_by_attribute(remaining_polygons, points, 'YL', '_p_yl')
+ if yl_matched is not None and not yl_matched.empty:
+ matched_results.append(yl_matched)
+ matched_indices.extend(yl_ids)
+ remaining_polygons = polygons[~polygons.index.isin(matched_indices)].copy()
+ print(f"已匹配 YL: {len(yl_ids)} 个要素,剩余 {len(remaining_polygons)} 个。")
+ else:
+ print("未匹配到 YL 类型要素,继续下一步。")
+
+ # --- 第四步:按 TL 匹配未匹配面 ---
+ if not remaining_polygons.empty:
+ print(f"步骤 4: 正在为 {len(remaining_polygons)} 个要素匹配 TL 最近点...")
+ tl_matched, tl_ids = match_by_attribute(remaining_polygons, points, 'TL', '_p_tl')
+ if tl_matched is not None and not tl_matched.empty:
+ matched_results.append(tl_matched)
+ matched_indices.extend(tl_ids)
+ remaining_polygons = polygons[~polygons.index.isin(matched_indices)].copy()
+ print(f"已匹配 TL: {len(tl_ids)} 个要素,剩余 {len(remaining_polygons)} 个。")
+ else:
+ print("未匹配到 TL 类型要素,继续全局最近点。")
+
+ else:
+ print("没有未匹配的面要素,跳过 TS/YL/TL 匹配。")
+
+ # --- 最后:全局最近点匹配剩余面 ---
+ unmatched_mask = ~polygons.index.isin(matched_indices)
+ remaining_polygons = polygons[unmatched_mask].copy()
+ print(f"最后一步: 正在为 {len(remaining_polygons)} 个要素匹配全局最近点...")
+
+ if not remaining_polygons.empty:
+ point_pool = points[[soil_prop, 'geometry']]
+ step_final = gpd.sjoin_nearest(remaining_polygons, point_pool, how='left', rsuffix='_p2')
+ step_final = step_final[~step_final.index.duplicated(keep='first')]
+ matched_results.append(step_final)
+
+ # --- 第三步:稳健合并 ---
+ print("正在合并数据...")
+ # 过滤掉列表中可能的 None 或空 DataFrame,防止 FutureWarning
+ to_concat = [res for res in matched_results if res is not None and not res.empty]
+
+ if to_concat:
+ final_gdf = pd.concat(to_concat)
+ else:
+ # 如果没有任何匹配结果,返回带空 GZCHD 的原面要素
+ final_gdf = polygons.copy()
+ final_gdf[soil_prop] = 0
+
+ # --- 4. 清理与保存 ---
+ # 删除临时列
+ cols_to_drop = [
+ c for c in final_gdf.columns
+ if 'index_' in c
+ or '_p1' in c
+ or '_p2' in c
+ or '_p_ts' in c
+ or '_p_yl' in c
+ or '_p_tl' in c
+ ]
+ final_gdf = final_gdf.drop(columns=cols_to_drop)
+
+ # 强制去重复列名
+ final_gdf = final_gdf.loc[:, ~final_gdf.columns.duplicated()]
+
+ # 填充空值并确保类型一致
+ if soil_prop in final_gdf.columns:
+ final_gdf[soil_prop] = final_gdf[soil_prop].fillna(0)
+ else:
+ final_gdf[soil_prop] = 0
+
+ print(f"正在保存结果至: {output_path}")
+ final_gdf.to_file(output_path, encoding='utf-8')
+ print("处理完成!")
+
+ return final_gdf
+
+if __name__ == "__main__":
+ # 遍历文件夹中所有样点shp文件,并进行处理
+ shp_file = r"E:\@三普属性图出图\测试\YXTCHD.shp" # 样点数据文件夹
+ dltb_file = r"E:\@三普属性图出图\广西天峨县\@基础数据\土壤类型图\土壤类型图.shp" # 耕地图斑
+ output_folder = r"E:\@三普属性图出图\广西天峨县" # 输出文件夹
+
+ assign_gzchd_flexible_v2(
+ soil_prop= "YXTCHD", # 耕地层厚度字段名
+ point_path=shp_file, # 样点数据
+ polygon_path= dltb_file,
+ output_path=fr'{output_folder}\YXTCHD.shp' # 输出文件
+ )
+ time.sleep(2) # 防止文件读写冲突
\ No newline at end of file
diff --git a/scripts/其他工具/样点剔除统计表格.py b/scripts/其他工具/样点剔除统计表格.py
new file mode 100644
index 0000000..c6960a6
--- /dev/null
+++ b/scripts/其他工具/样点剔除统计表格.py
@@ -0,0 +1,404 @@
+import pandas as pd
+import numpy as np
+import os
+import geopandas as gpd
+from openpyxl import Workbook
+from openpyxl.styles import Alignment, Font, Border, Side
+from openpyxl.utils import get_column_letter
+
+# 定义指标代码与单位的对应关系
+INDICATOR_UNITS = {
+ # 基本指标
+ 'PH': ('pH', '-'),
+ 'ECA': ('交换性钙', 'cmol(½Ca²⁺)/kg'),
+ 'EMG': ('交换性镁', 'cmol(½Mg²⁺)/kg'),
+ 'TN': ('全氮', 'g/kg'),
+ 'TP': ('全磷', 'g/kg'),
+ 'TK': ('全钾', 'g/kg'),
+ 'AS1': ('有效硫', 'mg/kg'),
+ 'AB': ('有效硼', 'mg/kg'),
+ 'AP': ('有效磷', 'mg/kg'),
+ 'AFE': ('有效铁', 'mg/kg'),
+ 'ACU': ('有效铜', 'mg/kg'),
+ 'AZN': ('有效锌', 'mg/kg'),
+ 'AMN': ('有效锰', 'mg/kg'),
+ 'OM': ('有机质', 'g/kg'),
+ 'GZCHD': ('耕层厚度', 'cm'),
+ 'AK': ('速效钾', 'mg/kg'),
+ 'CEC': ('阳离子交换量', 'cmol/kg'),
+ # 特殊指标 - 根据文件名对应字段
+ 'FL': ('粉粒', '%'),
+ 'NL': ('黏粒', '%'),
+ 'SL': ('砂粒', '%'),
+ 'TRRZPJZ': ('土壤容重', 'g/cm³'),
+ 'TRZD': ('土壤质地', '分类'),
+ # 其他可能指标
+ 'AMO': ('有效钼', 'mg/kg'),
+ 'TSE': ('全硒', 'mg/kg'),
+ 'YXTCHD': ('有效土层厚度', 'cm')
+}
+
+# 文件名到字段的映射
+FILENAME_TO_FIELD = {
+ '粉粒': 'FL',
+ '黏粒': 'NL',
+ '砂粒': 'SL',
+ '表层容重': 'TRRZPJZ',
+ '土壤质地十二级分类': 'TRZD',
+ '双江县YXTCHD': 'YXTCHD'
+}
+
+# 扩展字段别名映射,支持更多pH字段名
+FIELD_ALIASES = {
+ 'PH': ['pH', 'PH', 'ph'], # 支持pH的各种大小写形式
+ 'ECA': ['交换性钙', 'ECA'],
+ 'EMG': ['交换性镁', 'EMG'],
+ 'TN': ['全氮', 'TN'],
+ 'TP': ['全磷', 'TP'],
+ 'TK': ['全钾', 'TK'],
+ 'AS1': ['有效硫', 'AS1'],
+ 'AB': ['有效硼', 'AB'],
+ 'AP': ['有效磷', 'AP'],
+ 'AFE': ['有效铁', 'AFE'],
+ 'ACU': ['有效铜', 'ACU'],
+ 'AZN': ['有效锌', 'AZN'],
+ 'AMN': ['有效锰', 'AMN'],
+ 'OM': ['有机质', 'OM'],
+ 'GZCHD': ['耕层厚度', 'GZCHD'],
+ 'AK': ['速效钾', 'AK'],
+ 'CEC': ['阳离子交换量', 'CEC'],
+ 'FL': ['粉粒', 'FL'],
+ 'NL': ['黏粒', 'NL'],
+ 'SL': ['砂粒', 'SL'],
+ 'TRRZPJZ': ['土壤容重', 'TRRZPJZ'],
+ 'TRZD': ['土壤质地', 'TRZD'],
+ 'AMO': ['有效钼', 'AMO'],
+ 'TSE': ['全硒', 'TSE'],
+ 'YXTCHD': ['有效土层厚度', 'YXTCHD']
+}
+
+
+def find_shapefiles(folder_path):
+ """在文件夹中递归查找所有的Shapefile文件"""
+ shapefiles = []
+
+ for root, dirs, files in os.walk(folder_path):
+ for file in files:
+ if file.lower().endswith('.shp'):
+ shapefiles.append(os.path.join(root, file))
+
+ return shapefiles
+
+
+def read_shapefile_data(shapefile_path):
+ """读取Shapefile数据并返回属性表"""
+ try:
+ print(f" 读取Shapefile: {os.path.basename(shapefile_path)}")
+ gdf = gpd.read_file(shapefile_path, encoding='utf-8')
+
+ print(f" 要素数量: {len(gdf)}")
+ print(f" 属性字段: {list(gdf.columns)}")
+
+ return gdf
+ except Exception as e:
+ print(f" 读取Shapefile失败: {e}")
+ try:
+ gdf = gpd.read_file(shapefile_path, encoding='gbk')
+ print(f" 使用GBK编码成功读取")
+ return gdf
+ except:
+ return None
+
+
+def get_indicator_data(gdf, filename):
+ """从GeoDataFrame中获取指标数据,使用统一字段匹配逻辑"""
+ indicator_data = {}
+
+ basename = os.path.basename(filename).replace('.shp', '')
+
+ # 1. 首先尝试文件名映射
+ target_field = None
+ if basename in FILENAME_TO_FIELD:
+ target_field = FILENAME_TO_FIELD[basename]
+ if target_field in gdf.columns:
+ indicator_data[target_field] = gdf[target_field]
+ print(f" 通过文件名映射找到字段: {target_field}")
+ else:
+ # 尝试通过别名查找
+ for indicator_code in INDICATOR_UNITS.keys():
+ if target_field == indicator_code:
+ for alias in FIELD_ALIASES.get(indicator_code, []):
+ if alias in gdf.columns:
+ indicator_data[indicator_code] = gdf[alias]
+ print(f" 通过文件名映射+别名找到字段: {alias} -> {indicator_code}")
+ break
+
+ # 2. 如果没有通过文件名找到,尝试直接匹配所有指标和别名
+ if not indicator_data:
+ for indicator_code in INDICATOR_UNITS.keys():
+ # 先尝试直接匹配指标代码
+ if indicator_code in gdf.columns:
+ indicator_data[indicator_code] = gdf[indicator_code]
+ print(f" 直接匹配字段: {indicator_code}")
+ continue
+
+ # 再尝试匹配别名
+ aliases = FIELD_ALIASES.get(indicator_code, [])
+ for alias in aliases:
+ if alias in gdf.columns:
+ indicator_data[indicator_code] = gdf[alias]
+ print(f" 通过别名匹配: {alias} -> {indicator_code}")
+ break
+
+ # 3. 额外检查:如果文件名包含特定关键词,尝试匹配
+ if not indicator_data:
+ filename_lower = basename.lower()
+ for indicator_code, (chinese_name, unit) in INDICATOR_UNITS.items():
+ if indicator_code.lower() in filename_lower or chinese_name in filename_lower:
+ # 尝试匹配指标代码或中文名
+ if indicator_code in gdf.columns:
+ indicator_data[indicator_code] = gdf[indicator_code]
+ print(f" 通过文件名关键词匹配: {indicator_code}")
+ break
+ elif chinese_name in gdf.columns:
+ indicator_data[indicator_code] = gdf[chinese_name]
+ print(f" 通过文件名关键词匹配中文名: {chinese_name} -> {indicator_code}")
+ break
+
+ return indicator_data
+
+
+def get_combined_stats_from_folder(folder_path, folder_name="数据"):
+ """从文件夹中所有shapefile合并统计指定指标"""
+ shapefiles = find_shapefiles(folder_path)
+
+ if not shapefiles:
+ print(f" 未找到Shapefile文件")
+ return pd.DataFrame()
+
+ print(f" 找到 {len(shapefiles)} 个Shapefile文件")
+
+ all_data = {code: [] for code in INDICATOR_UNITS.keys()}
+
+ for i, shp_file in enumerate(shapefiles, 1):
+ print(f"\n [{i}] 处理文件: {os.path.basename(shp_file)}")
+ gdf = read_shapefile_data(shp_file)
+
+ if gdf is not None:
+ indicator_data = get_indicator_data(gdf, shp_file)
+
+ for indicator_code, data_series in indicator_data.items():
+ if indicator_code in all_data:
+ # 转换为数值类型,处理可能的非数值数据
+ try:
+ data_series = pd.to_numeric(data_series, errors='coerce')
+ valid_data = data_series.dropna()
+ if len(valid_data) > 0:
+ all_data[indicator_code].extend(valid_data.tolist())
+ print(f" 提取 {indicator_code}: {len(valid_data)} 个值")
+ except Exception as e:
+ print(f" 处理 {indicator_code} 数据时出错: {e}")
+
+ # 计算每个指标的合并统计
+ stats_list = []
+
+ for indicator_code, (chinese_name, unit) in INDICATOR_UNITS.items():
+ data_list = all_data.get(indicator_code, [])
+ if not data_list:
+ continue
+
+ data_series = pd.Series(data_list)
+ # 过滤极端值(可选,根据实际需求调整)
+ data_series = data_series[(data_series >= 0) | pd.isna(data_series)]
+
+ if len(data_series) == 0:
+ continue
+
+ # 关键修复1:计算总体标准差(ddof=0),而不是默认的样本标准差(ddof=1)
+ std_dev = data_series.std(ddof=0)
+ mean_val = data_series.mean()
+
+ # 关键修复2:优化变异系数计算
+ if abs(mean_val) < 1e-8: # 均值接近0时
+ cv_value = 0.0
+ else:
+ # CV = (标准差 / 均值) * 100,保留2位小数
+ cv_value = round((std_dev / mean_val) * 100, 2)
+
+ stats = {
+ '指标代码': indicator_code,
+ '指标': chinese_name,
+ '单位': unit,
+ '样点数': int(len(data_series)),
+ 'Min': round(float(data_series.min()), 2),
+ 'Max': round(float(data_series.max()), 2),
+ 'Mean': round(float(mean_val), 2),
+ 'Std': round(float(std_dev), 2), # 使用总体标准差
+ 'CV': cv_value
+ }
+ stats_list.append(stats)
+ print(f" 统计 {chinese_name}({indicator_code}): {len(data_series)} 个样点")
+
+ if stats_list:
+ stats_df = pd.DataFrame(stats_list)
+ stats_df = stats_df.sort_values('指标')
+ print(f"\n 总共统计到 {len(stats_df)} 个指标")
+ return stats_df
+
+ print(" 未找到任何指标数据")
+ return pd.DataFrame()
+
+
+def create_statistics_excel(before_folder, after_folder, output_path):
+ """创建融合的统计表格,在剔除后表格前加一列剔除前样点数和剔除样点数"""
+ workbook = Workbook()
+
+ # 移除默认sheet
+ if 'Sheet' in workbook.sheetnames:
+ default_sheet = workbook['Sheet']
+ workbook.remove(default_sheet)
+
+ # 定义样式
+ thin_border = Border(
+ left=Side(style='thin'),
+ right=Side(style='thin'),
+ top=Side(style='thin'),
+ bottom=Side(style='thin')
+ )
+
+ print("=" * 60)
+ print("开始分析样点数据")
+ print("=" * 60)
+
+ # 分析剔除前数据
+ before_stats = None
+ if os.path.exists(before_folder):
+ print(f"\n[1] 分析剔除前数据:")
+ print(f"文件夹路径: {before_folder}")
+ before_stats = get_combined_stats_from_folder(before_folder, "剔除前")
+ if not before_stats.empty:
+ print(f"✓ 剔除前统计完成: {len(before_stats)} 个指标")
+ else:
+ print("✗ 剔除前未找到指定指标数据")
+ before_stats = None
+ else:
+ print(f"✗ 剔除前文件夹不存在: {before_folder}")
+ before_stats = None
+
+ # 分析剔除后数据
+ if os.path.exists(after_folder):
+ print(f"\n[2] 分析剔除后数据:")
+ print(f"文件夹路径: {after_folder}")
+ after_stats = get_combined_stats_from_folder(after_folder, "剔除后")
+
+ if not after_stats.empty:
+ # 创建融合的统计工作表
+ sheet_combined = workbook.create_sheet(title="样点统计")
+
+ # 新的表头:指标, 单位, 剔除前样点数, 剔除样点数, 剔除后样点数, Min, Max, Mean, Std, CV
+ combined_headers = ['指标', '单位', '剔除前样点数', '剔除样点数', '剔除后样点数', 'Min', 'Max', 'Mean',
+ 'Std', 'CV']
+ for col, header in enumerate(combined_headers, 1):
+ cell = sheet_combined.cell(row=1, column=col, value=header)
+ cell.alignment = Alignment(horizontal='center', vertical='center')
+ cell.font = Font(bold=True)
+ cell.border = thin_border
+
+ # 写入数据
+ for row_idx, (index, after_row) in enumerate(after_stats.iterrows(), start=2):
+ # 查找对应的剔除前数据
+ before_sample_count = 0
+ before_row = None
+ if before_stats is not None:
+ # 首先尝试通过指标代码匹配
+ matching_rows = before_stats[before_stats['指标代码'] == after_row['指标代码']]
+ if not matching_rows.empty:
+ before_row = matching_rows.iloc[0]
+ else:
+ # 如果指标代码匹配失败,尝试通过指标名称匹配
+ matching_rows = before_stats[before_stats['指标'] == after_row['指标']]
+ if not matching_rows.empty:
+ before_row = matching_rows.iloc[0]
+
+ if before_row is not None:
+ before_sample_count = int(before_row['样点数'])
+
+ # 计算剔除样点数
+ after_sample_count = int(after_row['样点数'])
+ abnormal_count = max(0, before_sample_count - after_sample_count)
+
+ # 写入数据
+ sheet_combined.cell(row=row_idx, column=1, value=after_row['指标']) # 指标
+ sheet_combined.cell(row=row_idx, column=2, value=after_row['单位']) # 单位
+ sheet_combined.cell(row=row_idx, column=3, value=before_sample_count) # 剔除前样点数
+ sheet_combined.cell(row=row_idx, column=4, value=abnormal_count) # 剔除样点数
+ sheet_combined.cell(row=row_idx, column=5, value=after_sample_count) # 剔除后样点数
+ sheet_combined.cell(row=row_idx, column=6, value=after_row['Min']) # Min
+ sheet_combined.cell(row=row_idx, column=7, value=after_row['Max']) # Max
+ sheet_combined.cell(row=row_idx, column=8, value=after_row['Mean']) # Mean
+ sheet_combined.cell(row=row_idx, column=9, value=after_row['Std']) # Std
+ sheet_combined.cell(row=row_idx, column=10, value=after_row['CV']) # CV
+
+ # 设置所有单元格的样式
+ for col_idx in range(1, 11):
+ cell = sheet_combined.cell(row=row_idx, column=col_idx)
+ cell.alignment = Alignment(horizontal='center', vertical='center')
+ cell.border = thin_border
+
+ # 如果剔除了样点,高亮显示剔除样点数列
+ if abnormal_count > 0:
+ cell = sheet_combined.cell(row=row_idx, column=4) # 剔除样点数列
+ cell.font = Font(bold=True, color="FF0000") # 红色加粗
+
+ # 调整列宽
+ combined_column_widths = {
+ '指标': 15,
+ '单位': 12,
+ '剔除前样点数': 12,
+ '剔除样点数': 12,
+ '剔除后样点数': 12,
+ 'Min': 10,
+ 'Max': 10,
+ 'Mean': 10,
+ 'Std': 10,
+ 'CV': 10
+ }
+
+ for col_idx, col_name in enumerate(combined_headers, 1):
+ column_letter = get_column_letter(col_idx)
+ if col_name in combined_column_widths:
+ sheet_combined.column_dimensions[column_letter].width = combined_column_widths[col_name]
+
+ print(f"\n✓ 融合统计完成: {len(after_stats)} 个指标")
+
+ # 输出匹配信息
+ if before_stats is not None:
+ print(f" 剔除前找到 {len(before_stats)} 个指标")
+ print(f" 剔除后找到 {len(after_stats)} 个指标")
+ print(
+ f" 成功匹配 {len([i for i in range(2, len(after_stats) + 2) if sheet_combined.cell(row=i, column=3).value > 0])} 个指标的剔除前数据")
+ else:
+ print("✗ 剔除后未找到指定指标数据")
+ sheet_combined = workbook.create_sheet(title="样点统计")
+ sheet_combined.cell(row=1, column=1, value="未找到指定指标数据")
+ else:
+ print(f"✗ 剔除后文件夹不存在: {after_folder}")
+ sheet_combined = workbook.create_sheet(title="样点统计")
+ sheet_combined.cell(row=1, column=1, value="剔除后文件夹不存在")
+
+ # 保存文件
+ workbook.save(output_path)
+ print(f"\n" + "=" * 60)
+ print(f"文件保存成功: {output_path}")
+ print("=" * 60)
+
+
+# ================ 使用示例 ================
+if __name__ == "__main__":
+ # 方式1: 处理单个样点数据对
+ before_folder = r"D:\a陆平\1.5实验数据\12月新版实验室数据\云南省实验室数据成果1218\永仁县20260127"
+ after_folder = r"D:\a陆平\1.5实验数据\12月新版实验室数据\云南省实验室数据成果1218\永仁县20260127剔除后"
+ output_path = r"D:\a陆平\1.5实验数据\12月新版实验室数据\云南省实验室数据成果1218\永仁县样点统计结果.xlsx"
+
+ # 执行
+ create_statistics_excel(before_folder, after_folder, output_path)
\ No newline at end of file
diff --git a/scripts/其他工具/面积加权均值_华南.py b/scripts/其他工具/面积加权均值_华南.py
new file mode 100644
index 0000000..23177db
--- /dev/null
+++ b/scripts/其他工具/面积加权均值_华南.py
@@ -0,0 +1,1050 @@
+# 生成完整的exactextract面积加权计算Python脚本
+# -*- coding: utf-8 -*-
+"""
+土壤属性栅格数据面积加权统计脚本
+基于exactextract库实现多属性栅格的面积加权平均值计算
+最终输出格式与土壤属性图斑数据表一致
+"""
+
+import re
+import sys
+
+import exactextract as ee
+import geopandas as gpd
+import rasterio
+import pandas as pd
+import numpy as np
+from pathlib import Path
+import warnings
+from pyproj import CRS
+warnings.filterwarnings('ignore')
+
+project_root = Path(__file__).parent.parent.parent
+sys.path.insert(0, str(project_root))
+
+import app
+
+
+def init_logger():
+ """初始化日志输出,便于跟踪处理过程"""
+ import logging
+ logging.basicConfig(
+ level=logging.INFO,
+ format='%(asctime)s - %(levelname)s - %(message)s',
+ datefmt='%Y-%m-%d %H:%M:%S'
+ )
+ return logging.getLogger(__name__)
+
+def validate_data(vector_path, raster_files):
+ """
+ 验证输入数据的有效性
+ :param vector_path: 矢量图斑文件路径
+ :param raster_files: 栅格文件字典
+ :return: 验证结果(布尔值)
+ """
+ logger = init_logger()
+
+ # 验证矢量文件是否存在
+ # if not Path(vector_path).exists():
+ # logger.error(f"矢量文件不存在:{vector_path}")
+ # return False
+
+ # 验证栅格文件是否存在
+ for attr_name, raster_path in raster_files.items():
+ if not Path(raster_path).exists():
+ logger.error(f"栅格文件不存在:{attr_name} -> {raster_path}")
+ return False
+
+ # 验证矢量文件格式
+ try:
+ # gdf = gpd.read_file(vector_path)
+ gdf = app.readers.read_vector(vector_path)
+ if gdf.geometry.type.unique()[0] != 'Polygon' and gdf.geometry.type.unique()[0] != 'MultiPolygon':
+ logger.error("矢量文件必须是Polygon类型(面要素)")
+ return False
+ except Exception as e:
+ logger.error(f"矢量文件读取失败:{str(e)}")
+ return False
+
+ # 验证栅格文件格式
+ try:
+ test_raster = next(iter(raster_files.values()))
+ with rasterio.open(test_raster) as src:
+ if src.count != 1:
+ logger.error("每个栅格文件必须是单波段(每个属性单独一个栅格)")
+ return False
+ except Exception as e:
+ logger.error(f"栅格文件读取失败:{str(e)}")
+ return False
+
+ logger.info("所有输入数据验证通过")
+ return True
+
+def extract_epsg(crs_obj):
+ """
+ 终极EPSG提取器:从正常方法到暴力正则
+ """
+ if crs_obj is None:
+ return None
+
+ # 1. 尝试常规方法
+ epsg = crs_obj.to_epsg()
+ if epsg:
+ return str(epsg)
+
+ # 2. 降低匹配严格度尝试 (pyproj 内置的容错方法)
+ epsg_loose = crs_obj.to_epsg(min_confidence=25)
+ if epsg_loose:
+ return str(epsg_loose)
+
+ # 3. 终极必杀:正则匹配 WKT 文本
+ try:
+ wkt_str = crs_obj.to_wkt()
+ # 匹配文本中的 AUTHORITY["EPSG","4523"] 或 ID["EPSG", 4523]
+ matches = re.findall(r'(?:AUTHORITY|ID)\["EPSG",\s*"?(\d+)"?\]', wkt_str)
+ if matches:
+ # WKT中会有多个EPSG(比如单位、椭球体),最后一个通常才是整个坐标系的EPSG
+ return matches[-1]
+ except Exception as e:
+ pass
+
+ return None
+
+def standardize_crs(gdf, raster_path):
+ """
+ 标准化矢量与栅格的坐标参考系统(CRS)
+ """
+ logger = init_logger() # 请确保你有 init_logger
+
+ with rasterio.open(raster_path) as src:
+ # 兼容旧版本 rasterio 的读取方式
+ raster_crs = CRS.from_user_input(src.crs)
+
+ vector_crs = CRS.from_user_input(gdf.crs)
+
+ # 使用终极提取器获取 EPSG
+ v_epsg = extract_epsg(vector_crs)
+ r_epsg = extract_epsg(raster_crs)
+
+ print(f"当前矢量CRS EPSG:{v_epsg}")
+ print(f"目标栅格CRS EPSG:{r_epsg}")
+
+ # 判断逻辑:先看数学是否相等,再看强制提取的EPSG编号是否相等
+ is_same_crs = False
+ if vector_crs.equals(raster_crs):
+ is_same_crs = True
+ elif v_epsg and r_epsg and v_epsg == r_epsg:
+ is_same_crs = True
+
+ # 执行转换(若真的不同才转)
+ if not is_same_crs:
+ logger.warning("矢量与栅格CRS不一致,正在进行转换...")
+ gdf = gdf.to_crs(raster_crs)
+ new_epsg = extract_epsg(CRS.from_user_input(gdf.crs))
+ logger.info(f"CRS转换完成,新矢量CRS EPSG:{new_epsg}")
+ else:
+ logger.info(f"矢量与栅格CRS一致 (均判定为 EPSG:{v_epsg}),无需转换。")
+
+ return gdf
+
+def calculate_area_weighted_stats(vector_path, raster_files, output_path):
+ """
+ 核心函数:计算面积加权统计值
+ :param vector_path: 矢量图斑文件路径
+ :param raster_files: 栅格文件字典(键:属性名,值:栅格路径)
+ :param output_path: 结果输出路径(Excel文件)
+ :return: 统计结果DataFrame
+ """
+ logger = init_logger()
+ logger.info("开始面积加权统计计算")
+
+ # 1. 加载矢量数据
+ logger.info(f"加载矢量数据:{vector_path}")
+ gdf = app.readers.read_vector(vector_path)
+
+ # 2. 标准化CRS
+ test_raster = next(iter(raster_files.values()))
+ gdf = standardize_crs(gdf, test_raster)
+
+ # 中断程序
+ # sys.exit(0)
+
+ # 3. 初始化结果DataFrame(保留矢量中的关键属性)
+ logger.info("初始化结果数据结构")
+ # 基础字段列表(与土壤属性图斑表格式对齐)
+ # TODO
+ base_fields = ["BSM","YSDM","TBBH","DLBM", "DLMC","TL", "YL", "TS", "TZ","ZLDWDM","ZLDWMC","TBMJ","geometry"]
+
+ # 检查矢量中是否包含必要字段,若不包含则创建空字段
+ result_gdf = gpd.GeoDataFrame()
+ for field in base_fields:
+ if field in gdf.columns:
+ result_gdf[field] = gdf[field]
+ else:
+ result_gdf[field] = np.nan
+ logger.warning(f"矢量中缺少'{field}'字段,将生成空值")
+
+ # 设置几何列以保持GeoDataFrame
+ result_gdf = gpd.GeoDataFrame(result_gdf, geometry=gdf.geometry)
+
+ # 4. 计算图斑面积(转换为亩)
+ # logger.info("计算图斑面积(单位:亩)")
+ # # 计算平方米面积(根据CRS单位自动适应)
+ # gdf["area_sqm"] = gdf.geometry.area
+ # # 转换为亩(1亩 ≈ 666.6667平方米)
+ # result_gdf["面积亩"] = gdf["area_sqm"] * 0.0015
+ # # 保留6位小数,与示例数据格式一致
+ # result_gdf["面积亩"] = result_gdf["面积亩"].round(6)
+
+ # 5. 对每个土壤属性进行面积加权平均计算
+ logger.info("开始处理土壤属性栅格(面积加权平均)")
+ for attr_idx, (attr_name, raster_path) in enumerate(raster_files.items(), 1):
+ total_attrs = len(raster_files)
+ logger.info(f"处理进度:{attr_idx}/{total_attrs} - 属性:{attr_name}")
+
+ try:
+ # 使用exactextract计算面积加权平均
+ # weights="area":按矢量与栅格的交集面积进行加权,如果是TRZD等分类属性,则不使用面积加权,直接计算众数
+ if attr_name in ["TRZD"]:
+ stats = ee.exact_extract(
+ raster_path,
+ gdf,
+ ["majority"], # 计算众数
+ output="pandas" # 输出为DataFrame格式
+ )
+ else:
+ stats = ee.exact_extract(
+ raster_path,
+ gdf,
+ ["mean"], # 计算平均值
+ output="pandas" # 输出为DataFrame格式
+ )
+
+ # 将统计结果添加到结果DataFrame
+ if stats is None:
+ logger.warning(f"{attr_name}计算结果为空,可能无交集区域")
+ result_gdf[attr_name] = np.nan
+ continue
+ else:
+ # 确保 stats 为 pandas.DataFrame,以便使用字符串索引
+ if not isinstance(stats, pd.DataFrame):
+ stats = pd.DataFrame(stats)
+
+ # 保留4位小数,确保数据精度
+ if attr_name in ["TRZD"]:
+ result_gdf[attr_name] = stats["majority"]
+ else:
+ result_gdf[attr_name] = stats["mean"].round(2)
+
+ # 处理可能的空值(无交集区域)
+ if result_gdf[attr_name].isnull().sum() > 0:
+ null_count = result_gdf[attr_name].isnull().sum()
+ logger.warning(f"{attr_name}存在{null_count}个空值(图斑与栅格无交集)")
+ # 用0填充空值(可根据业务需求调整)
+ result_gdf[attr_name] = result_gdf[attr_name].fillna(0)
+
+ except Exception as e:
+ logger.error(f"{attr_name}处理失败:{str(e)}")
+ # 失败时填充空值,避免整个程序崩溃
+ result_gdf[attr_name] = np.nan
+
+ # 6. 添加属性分级字段(根据业务规则实现)
+ logger.info("添加土壤属性分级字段")
+ result_gdf = add_attribute_classification(result_gdf)
+
+ # 7. 整理最终字段顺序(与示例表格完全对齐)
+ logger.info("整理输出字段顺序")
+ # TODO
+ final_columns = [
+ "BSM","YSDM","TBBH","DLBM", "DLMC","TL", "YL", "TS", "TZ","ZLDWDM","ZLDWMC","TBMJ",
+ "SL", "FL", "NL", "TRZD", "GZCHD", "TRRZ", "SWXDT", "DBLSFD",
+ "PH", "CEC", "OM", "TN", "TP", "TK", "AP", "AK", "ECA", "EMG",
+ "AS1", "AFE", "AMN", "ACU","AZN","AB","AMO",
+ "SL_DJ", "FL_DJ", "NL_DJ", "TRZD_DJ", "GZCHD_DJ", "TRRZ_DJ", "SWXDT_DJ", "DBLSFD_DJ",
+ "PH_DJ", "CEC_DJ", "OM_DJ", "TN_DJ", "TP_DJ", "TK_DJ", "AP_DJ", "AK_DJ", "ECA_DJ", "EMG_DJ",
+ "AS1_DJ", "AFE_DJ", "AMN_DJ", "ACU_DJ","AZN_DJ","AB_DJ","AMO_DJ",
+ "SL_DJZY", "FL_DJZY", "NL_DJZY", "TRZD_DJZY", "GZCHD_DJZY", "TRRZ_DJZY", "SWXDT_DJZY", "DBLSFD_DJZY",
+ "PH_DJZY", "CEC_DJZY", "OM_DJZY", "TN_DJZY", "TP_DJZY", "TK_DJZY", "AP_DJZY", "AK_DJZY", "ECA_DJZY", "EMG_DJZY",
+ "AS1_DJZY", "AFE_DJZY", "AMN_DJZY", "ACU_DJZY","AZN_DJZY","AB_DJZY","AMO_DJZY",
+ "geometry" # 保留几何列以保持GeoDataFrame
+ ]
+
+ # 补充缺失的字段(如乡代码等)
+ for col in final_columns:
+ if col not in result_gdf.columns:
+ result_gdf[col] = np.nan
+
+ # 按最终顺序排列字段
+ result_gdf = result_gdf[final_columns]
+
+ # 8. 导出结果到Excel
+ logger.info(f"导出结果到:{output_path}")
+ # 使用openpyxl引擎支持.xlsx格式
+ # result_gdf.to_excel(output_path, index=False, engine="openpyxl")
+ result_gdf.to_file(output_path, index=False, driver="ESRI Shapefile") # 导出为GeoPackage格式,保留空间信息
+ logger.info(f"结果导出完成,共生成{len(result_gdf)}条记录")
+
+ return result_gdf
+
+def add_attribute_classification(df):
+ """
+ 添加土壤属性分级字段(根据常见土壤分类标准实现)
+ 可根据实际业务需求调整分级阈值
+ :param df: 包含原始属性的DataFrame
+ :return: 包含分级字段和分级值域的DataFrame
+ """
+ # todo
+ # 1. 酸碱度分级(pH值) - 按PH标准
+ def classify_ph(ph):
+ """
+ 土壤pH值分级(第三次全国土壤普查标准)
+ 标准等级:
+ 等级一: 6.0~7.0
+ 等级二: 7.0~7.5, 5.5~6.0
+ 等级三: 7.5~8.0, 5.0~5.5
+ 等级四: 8.0~8.5, 4.5~5.0
+ 等级五: >8.5, ≤4.5
+ """
+ if ph > 8.5 or ph <= 4.5:
+ return 5 # 等级五: >8.5, ≤4.5
+ elif (8.0 < ph <= 8.5) or (4.5 < ph <= 5.0):
+ return 4 # 等级四: 8.0~8.5, 4.5~5.0
+ elif (7.5 < ph <= 8.0) or (5.0 < ph <= 5.5):
+ return 3 # 等级三: 7.5~8.0, 5.0~5.5
+ elif (7.0 < ph <= 7.5) or (5.5 < ph <= 6.0):
+ return 2 # 等级二: 7.0~7.5, 5.5~6.0
+ elif 6.0 < ph <= 7.0:
+ return 1 # 等级一: 6.0~7.0
+ else:
+ return None # 异常值
+
+ # 2. 有机质分级(单位:g/kg) - 按OM标准
+ def classify_organic(organic):
+ """
+ 土壤有机质分级(第三次全国土壤普查标准)
+ 标准等级:
+ 等级一: >35.0
+ 等级二: 25.0~35.0
+ 等级三: 15.0~25.0
+ 等级四: 10.0~15.0
+ 等级五: ≤10.0
+ """
+ if organic > 35.0:
+ return 1 # 等级一: >35.0
+ elif 30.0 < organic <= 35.0:
+ return 2 # 等级二: 25.0~35.0
+ elif 20.0 < organic <= 30.0:
+ return 3 # 等级三: 15.0~25.0
+ elif 10.0 < organic <= 20.0:
+ return 4 # 等级四: 10.0~15.0
+ elif organic <= 10.0:
+ return 5 # 等级五: ≤10.0
+ else:
+ return None # 异常值
+
+ # 3. 阳离子交换量分级(单位:cmol/kg) - 按CEC标准
+ def classify_cation(cation):
+ """
+ 土壤阳离子交换量分级(第三次全国土壤普查标准)
+ 标准等级:
+ 等级一: >20.0
+ 等级二: 15.0~20.0
+ 等级三: 10.0~15.0
+ 等级四: 5.0~10.0
+ 等级五: ≤5.0
+ """
+ if cation > 20.0:
+ return 1 # 等级一: >20.0
+ elif 15.0 < cation <= 20.0:
+ return 2 # 等级二: 15.0~20.0
+ elif 10.0 < cation <= 15.0:
+ return 3 # 等级三: 10.0~15.0
+ elif 5.0 < cation <= 10.0:
+ return 4 # 等级四: 5.0~10.0
+ elif cation <= 5.0:
+ return 5 # 等级五: ≤5.0
+ else:
+ return None # 异常值
+
+ # 4. 有效磷分级(单位:mg/kg) - 按AP标准
+ def classify_available_p(p):
+ """
+ 土壤有效磷分级(第三次全国土壤普查标准)
+ 标准等级:
+ 等级一: >40.0
+ 等级二: 20.0~40.0
+ 等级三: 10.0~20.0
+ 等级四: 5.0~10.0
+ 等级五: ≤5.0
+ """
+ if p > 40.0:
+ return 1 # 等级一: >40.0
+ elif 20.0 < p <= 40.0:
+ return 2 # 等级二: 25.0~40.0
+ elif 10.0 < p <= 20.0:
+ return 3 # 等级三: 15.0~25.0
+ elif 5.0 < p <= 10.0:
+ return 4 # 等级四: 5.0~15.0
+ elif p <= 5.0:
+ return 5 # 等级五: ≤5.0
+ else:
+ return None # 异常值
+
+ # 5. 速效钾分级(单位:mg/kg) - 按AK标准
+ def classify_available_k(k):
+ """
+ 土壤速效钾分级(第三次全国土壤普查标准)
+ 标准等级:
+ 等级一: >150
+ 等级二: 100~150
+ 等级三: 75~100
+ 等级四: 50~75
+ 等级五: ≤50
+ """
+ if k > 150:
+ return 1 # 等级一: >150
+ elif 100 < k <= 150:
+ return 2 # 等级二: 100~150
+ elif 75 < k <= 100:
+ return 3 # 等级三: 75~100
+ elif 50 < k <= 75:
+ return 4 # 等级四: 50~75
+ elif k <= 50:
+ return 5 # 等级五: ≤50
+ else:
+ return None # 异常值
+
+ # 6. 耕层厚度分级(单位:cm) - 按GZCHD标准
+ def classify_soil_depth(depth):
+ """
+ 土壤耕作层厚度分级(第三次全国土壤普查标准)
+ 标准等级:
+ 等级一: >25.0
+ 等级二: 20.0~25.0
+ 等级三: 15.0~20.0
+ 等级四: 10.0~15.0
+ 等级五: ≤10.0
+ """
+ if depth > 25.0:
+ return 1 # 等级一: >25.0
+ elif 20.0 < depth <= 25.0:
+ return 2 # 等级二: 20.0~25.0
+ elif 15.0 < depth <= 20.0:
+ return 3 # 等级三: 15.0~20.0
+ elif 10.0 < depth <= 15.0:
+ return 4 # 等级四: 10.0~15.0
+ elif depth <= 10.0:
+ return 5 # 等级五: ≤10.0
+ else:
+ return None # 异常值
+
+ # 7. 土壤容重分级(单位:g/cm³) - 按TRRZ标准
+ def classify_bulk_density(density):
+ """
+ 土壤容重分级(第三次全国土壤普查标准)
+ 标准等级:
+ 等级一: 1.00~1.20
+ 等级二: 1.20~1.30
+ 等级三: 1.30~1.40, 0.90~1.00
+ 等级四: 1.40~1.50
+ 等级五: >1.50, ≤0.90
+ """
+ if 1.00 < density <= 1.20:
+ return 1 # 等级一: 1.00~1.20
+ elif 1.20 < density <= 1.30:
+ return 2 # 等级二: 1.20~1.30
+ elif (1.30 < density <= 1.40) or (0.90 < density <= 1.00):
+ return 3 # 等级三: 1.30~1.40, 0.90~1.00
+ elif 1.40 < density <= 1.50:
+ return 4 # 等级四: 1.40~1.50
+ elif density > 1.50 or density <= 0.90:
+ return 5 # 等级五: >1.50, ≤0.90
+ else:
+ return None # 异常值
+
+ # 8. 全氮分级(单位:g/kg) - 按TN标准
+ def classify_total_n(n):
+ """
+ 土壤全氮分级(第三次全国土壤普查标准)
+ 标准等级:
+ 等级一: >2.00
+ 等级二: 1.50~2.00
+ 等级三: 1.00~1.50
+ 等级四: 0.50~1.00
+ 等级五: ≤0.50
+ """
+ if n > 2.00:
+ return 1 # 等级一: >2.00
+ elif 1.50 < n <= 2.00:
+ return 2 # 等级二: 1.50~2.00
+ elif 1.00 < n <= 1.50:
+ return 3 # 等级三: 1.00~1.50
+ elif 0.50 < n <= 1.00:
+ return 4 # 等级四: 0.50~1.00
+ elif n <= 0.50:
+ return 5 # 等级五: ≤0.50
+ else:
+ return None # 异常值
+
+ # 9. 全磷分级(单位:g/kg) - 按TP标准
+ def classify_total_p(p):
+ """
+ 土壤全磷分级(第三次全国土壤普查标准)
+ 标准等级:
+ 等级一: >1.50
+ 等级二: 1.00~1.50
+ 等级三: 0.60~1.00
+ 等级四: 0.40~0.60
+ 等级五: ≤0.40
+ """
+ if p > 1.50:
+ return 1 # 等级一: >1.50
+ elif 1.00 < p <= 1.50:
+ return 2 # 等级二: 1.00~1.50
+ elif 0.60 < p <= 1.00:
+ return 3 # 等级三: 0.60~1.00
+ elif 0.40 <= p <= 0.60:
+ return 4 # 等级四: 0.40~0.60
+ elif p <= 0.40:
+ return 5 # 等级五: ≤0.40
+ else:
+ return None # 异常值
+
+ # 10. 全钾分级(单位:g/kg) - 按TK标准
+ def classify_total_k(k):
+ """
+ 土壤全钾分级(第三次全国土壤普查标准)
+ 标准等级:
+ 等级一: >20.0
+ 等级二: 15.0~20.0
+ 等级三: 10.0~15.0
+ 等级四: 5.0~10.0
+ 等级五: ≤5.0
+ """
+ if k > 20.0:
+ return 1 # 等级一: >20.0
+ elif 15.0 < k <= 20.0:
+ return 2 # 等级二: 15.0~20.0
+ elif 10.0 < k <= 15.0:
+ return 3 # 等级三: 10.0~15.0
+ elif 5.0 < k <= 10.0:
+ return 4 # 等级四: 5.0~10.0
+ elif k <= 5.0:
+ return 5 # 等级五: ≤5.0
+ else:
+ return None # 异常值
+
+ # 11. 有效铁分级(单位:mg/kg)
+ def classify_available_fe(fe):
+ """
+ 土壤有效铁分级(第三次全国土壤普查标准)
+
+ 参数:
+ fe: 有效铁含量 (mg/kg)
+
+ 返回:
+ 分级等级 (1-5)
+ """
+ if fe <= 3.0:
+ return 5 # 等级五: ≤3.0
+ elif 3.0 < fe <= 8.0:
+ return 4 # 等级四: 3.0~8.0
+ elif 8.0 < fe <= 10.0:
+ return 3 # 等级三: 8.0~10.0
+ elif 10.0 < fe <= 20.0:
+ return 2 # 等级二: 10.0~20.0
+ else: # fe > 20.0
+ return 1 # 等级一: >20.0
+
+ # 12. 有效锌分级(单位:mg/kg)
+ def classify_available_zn(zn):
+ """
+ 土壤有效锌分级(第三次全国土壤普查标准)
+
+ 参数:
+ zn: 有效锌含量 (mg/kg)
+
+ 返回:
+ 分级等级 (1-5)
+ """
+ if zn <= 0.50:
+ return 5 # 等级五: ≤0.50
+ elif 0.50 < zn <= 1.00:
+ return 4 # 等级四: 0.50~1.00
+ elif 1.00 < zn <= 2.00:
+ return 3 # 等级三: 1.00~2.00
+ elif 2.00 < zn <= 3.00:
+ return 2 # 等级二: 2.00~3.00
+ else: # zn > 3.00
+ return 1 # 等级一: >3.00
+
+ # 13. 有效锰分级(单位:mg/kg)
+ def classify_available_mn(mn):
+ """
+ 土壤有效锰分级(第三次全国土壤普查标准)
+
+ 参数:
+ mn: 有效锰含量 (mg/kg)
+
+ 返回:
+ 分级等级 (1-5)
+ """
+ if mn <= 5.0:
+ return 5 # 等级五: ≤5.0
+ elif 5.0 < mn <= 10.0:
+ return 4 # 等级四: 5.0~10.0
+ elif 10.0 < mn <= 20.0:
+ return 3 # 等级三: 10.0~20.0
+ elif 20.0 < mn <= 30.0:
+ return 2 # 等级二: 20.0~30.0
+ else: # mn > 30.0
+ return 1 # 等级一: >30.0
+
+ # 14. 有效铜分级(单位:mg/kg)
+ def classify_available_cu(cu):
+ """
+ 土壤有效铜分级(第三次全国土壤普查标准)
+
+ 参数:
+ cu: 有效铜含量 (mg/kg)
+
+ 返回:
+ 分级等级 (1-5)
+ """
+ if cu <= 0.20:
+ return 5 # 等级五: ≤0.20
+ elif 0.20 < cu <= 0.50:
+ return 4 # 等级四: 0.20~0.50
+ elif 0.50 < cu <= 1.00:
+ return 3 # 等级三: 0.50~1.00
+ elif 1.00 < cu <= 1.80:
+ return 2 # 等级二: 1.00~1.80
+ else: # cu > 1.80
+ return 1 # 等级一: >1.80
+
+ # 15. 有效硼分级(单位:mg/kg)
+ def classify_available_b(b):
+ """
+ 土壤有效硼分级(第三次全国土壤普查标准)
+
+ 参数:
+ b: 有效硼含量 (mg/kg)
+
+ 返回:
+ 分级等级 (1-5)
+ """
+ if b <= 0.20:
+ return 5 # 等级五: ≤0.20
+ elif 0.20 < b <= 0.50:
+ return 4 # 等级四: 0.20~0.50
+ elif 0.50 < b <= 1.00:
+ return 3 # 等级三: 0.50~1.00
+ elif 1.00 < b <= 2.00:
+ return 2 # 等级二: 1.00~2.00
+ else: # b > 2.00
+ return 1 # 等级一: >2.00
+
+ # 16. 有效钼分级(单位:mg/kg)
+ def classify_available_mo(mo):
+ """
+ 土壤有效钼分级(第三次全国土壤普查标准)
+
+ 参数:
+ mo: 有效钼含量 (mg/kg)
+
+ 返回:
+ 分级等级 (1-5)
+ """
+ if mo <= 0.05:
+ return 5 # 等级五: ≤0.05
+ elif 0.05 < mo <= 0.10:
+ return 4 # 等级四: 0.05~0.10
+ elif 0.10 < mo <= 0.15:
+ return 3 # 等级三: 0.10~0.15
+ elif 0.15 < mo <= 0.20:
+ return 2 # 等级二: 0.15~0.20
+ else: # mo > 0.20
+ return 1 # 等级一: >0.20
+
+ # 17. 有效硫分级(单位:mg/kg)
+ def classify_available_s(s):
+ """
+ 土壤有效硫分级(第三次全国土壤普查标准)
+
+ 参数:
+ s: 有效硫含量 (mg/kg)
+
+ 返回:
+ 分级等级 (1-5)
+ """
+ if s <= 10.0:
+ return 5 # 等级五: ≤10.0
+ elif 10.0 < s <= 20.0:
+ return 4 # 等级四: 10.0~20.0
+ elif 20.0 < s <= 30.0:
+ return 3 # 等级三: 20.0~30.0
+ elif 30.0 < s <= 40.0:
+ return 2 # 等级二: 30.0~40.0
+ else: # s > 40.0
+ return 1 # 等级一: >40.0
+
+ # 18. 交换性钙分级(单位:cmol(½Ca²⁺)/kg)
+ def classify_exchangeable_ca(ca):
+ """
+ 土壤交换性钙分级(第三次全国土壤普查标准)
+
+ 参数:
+ ca: 交换性钙含量 (cmol(½Ca²⁺)/kg)
+
+ 返回:
+ 分级等级 (1-5)
+ """
+ if ca <= 0.25:
+ return 5 # 等级五: ≤0.25
+ elif 0.25 < ca <= 1.00:
+ return 4 # 等级四: 0.25~1.00
+ elif 1.00 < ca <= 2.50:
+ return 3 # 等级三: 1.00~2.50
+ elif 2.50 < ca <= 4.99:
+ return 2 # 等级二: 2.50~4.99
+ else: # ca > 4.99
+ return 1 # 等级一: >4.99
+
+ # 19. 交换性镁分级(单位:cmol(½Mg²⁺)/kg)
+ def classify_exchangeable_mg(mg):
+ """
+ 土壤交换性镁分级(第三次全国土壤普查标准)
+
+ 参数:
+ mg: 交换性镁含量 (cmol(½Mg²⁺)/kg)
+
+ 返回:
+ 分级等级 (1-5)
+ """
+ if mg <= 0.21:
+ return 5 # 等级五: ≤0.21
+ elif 0.21 < mg <= 0.41:
+ return 4 # 等级四: 0.21~0.41
+ elif 0.41 < mg <= 0.82:
+ return 3 # 等级三: 0.41~0.82
+ elif 0.82 < mg <= 1.64:
+ return 2 # 等级二: 0.82~1.64
+ else: # mg > 1.64
+ return 1 # 等级一: >1.64
+
+ # 20. 全硒分级(单位:mg/kg)
+ def classify_total_se(se):
+ """
+ 土壤全硒分级(第三次全国土壤普查标准)
+
+ 参数:
+ se: 全硒含量 (mg/kg)
+
+ 返回:
+ 分级等级 (1-4)
+ """
+ if se <= 0.17:
+ return 4 # 等级四: ≤0.17
+ elif 0.17 < se <= 0.40:
+ return 3 # 等级三: 0.17~0.40
+ elif 0.40 < se <= 3.00:
+ return 2 # 等级二: 0.40~3.00
+ else: # se > 3.00
+ return 1 # 等级一: >3.00
+
+ # 21. 粉粒含量分级(单位:%)
+ def classify_silt(silt):
+ """
+ 土壤粉粒含量分级(第三次全国土壤普查标准)
+
+ 参数:
+ silt: 粉粒含量 (%)
+
+ 返回:
+ 分级等级 (1-5)
+ """
+ if silt > 75:
+ return 5 # 等级五: >75
+ elif 45 < silt <= 75:
+ return 4 # 等级四: 45~75
+ elif 30 < silt <= 45:
+ return 3 # 等级三: 30~45
+ elif 15 < silt <= 30:
+ return 2 # 等级二: 15~30
+ else: # silt <= 15
+ return 1 # 等级一: ≤15
+
+ # 22. 黏粒含量分级(单位:%)
+ def classify_clay(clay):
+ """
+ 土壤黏粒含量分级(第三次全国土壤普查标准)
+
+ 参数:
+ clay: 黏粒含量 (%)
+
+ 返回:
+ 分级等级 (1-5)
+ """
+ if clay > 65:
+ return 5 # 等级五: >65
+ elif 45 < clay <= 65:
+ return 4 # 等级四: 45~65
+ elif 25 < clay <= 45:
+ return 3 # 等级三: 25~45
+ elif 15 < clay <= 25:
+ return 2 # 等级二: 15~25
+ else: # clay <= 15
+ return 1 # 等级一: ≤15
+
+ # 23. 砂粒含量分级(单位:%)
+ def classify_sand(sand):
+ """
+ 土壤砂粒含量分级(第三次全国土壤普查标准)
+
+ 参数:
+ sand: 砂粒含量 (%)
+
+ 返回:
+ 分级等级 (1-5)
+ """
+ if sand > 85:
+ return 5 # 等级五: >85
+ elif 55 < sand <= 85:
+ return 4 # 等级四: 55~85
+ elif 40 < sand <= 55:
+ return 3 # 等级三: 40~55
+ elif 30 < sand <= 40:
+ return 2 # 等级二: 30~40
+ else: # sand <= 30
+ return 1 # 等级一: ≤30
+
+ # 24. 有效土层厚度分级(单位:cm)
+ def classify_yxtchd(depth):
+ """
+ 土壤有效土层厚度分级(第三次全国土壤普查标准)
+
+ 参数:
+ depth: 有效土层厚度 (cm)
+
+ 返回:
+ 分级等级 (1-5)
+ """
+ if depth <= 40:
+ return 5 # 等级五: ≤40
+ elif 40 < depth <= 60:
+ return 4 # 等级四: 40~60
+ elif 60 < depth <= 80:
+ return 3 # 等级三: 60~80
+ elif 80 < depth <= 100:
+ return 2 # 等级二: 80~100
+ else: # depth > 100
+ return 1 # 等级一: >100
+
+ # 25. 土壤质地
+ def classify_trzd(trzd):
+ """
+ 土壤质地
+
+ 参数:
+ trzd: 土壤质地分类(1-5)
+
+ 返回:
+ 分级等级 (1-5)
+ """
+ trzd = round(trzd, 0)
+ return trzd
+ # if trzd == 5:
+ # return 5 # 等级五: ≤40
+ # elif trzd == 4:
+ # return 4 # 等级四: 40~60
+ # elif trzd == 3:
+ # return 3 # 等级三: 60~80
+ # elif trzd == 2:
+ # return 2 # 等级二: 80~100
+ # else: # depth > 100
+ # return 1 # 等级一: >100
+
+ # 26. 地表砾石丰度分级(单位:%)
+ def classify_lsfd(gravel):
+ """
+ 土壤地表砾石丰度分级(第三次全国土壤普查标准)
+
+ 参数:
+ gravel: 地表砾石丰度 (%)
+
+ 返回:
+ 分级等级 (1-5)
+ """
+ if gravel <= 0:
+ return 1 # 等级一: ≤0
+ elif 0 < gravel <= 5:
+ return 2 # 等级二: 0~5
+ elif 5 < gravel <= 15:
+ return 3 # 等级三: 5~15
+ elif 15 < gravel <= 50:
+ return 4 # 等级三: 15~50
+ else: # gravel > 50
+ return 5 # 等级五: >50
+
+ # 应用分级函数(只处理非空值)
+ if "PH" in df.columns:
+ df["PH_DJ"] = df["PH"].apply(lambda x: classify_ph(x) if pd.notna(x) else np.nan)
+ df["PH_DJZY"] = "等级一: 6.0~7.0,等级二: 7.0~7.5, 5.5~6.0,等级三: 7.5~8.0, 5.0~5.5,等级四: 8.0~8.5, 4.5~5.0,等级五: >8.5, ≤4.5"
+ if "OM" in df.columns:
+ df["OM_DJ"] = df["OM"].apply(lambda x: classify_organic(x) if pd.notna(x) else np.nan)
+ df["OM_DJZY"] = "等级一: >35,等级二: 30~35,等级三: 20~30,等级四: 10~20,等级五: ≤10"
+ if "CEC" in df.columns:
+ df["CEC_DJ"] = df["CEC"].apply(lambda x: classify_cation(x) if pd.notna(x) else np.nan)
+ df["CEC_DJZY"] = "等级一: >20,等级二: 15~20,等级三: 10~15,等级四: 5~10,等级五: ≤5"
+ if "AP" in df.columns:
+ df["AP_DJ"] = df["AP"].apply(lambda x: classify_available_p(x) if pd.notna(x) else np.nan)
+ df["AP_DJZY"] = "等级一: >40,等级二: 20~40,等级三: 10~20,等级四: 5~10,等级五: ≤5"
+ if "AK" in df.columns:
+ df["AK_DJ"] = df["AK"].apply(lambda x: classify_available_k(x) if pd.notna(x) else np.nan)
+ df["AK_DJZY"] = "等级一: >150,等级二: 100~150,等级三: 75~100,等级四: 50~75,等级五: ≤50"
+ if "GZCHD" in df.columns:
+ df["GZCHD_DJ"] = df["GZCHD"].apply(lambda x: classify_soil_depth(x) if pd.notna(x) else np.nan)
+ df["GZCHD_DJZY"] = "等级一: >25,等级二: 20~25,等级三: 15~20,等级四: 10~15,等级五: ≤10"
+ if "TRRZ" in df.columns:
+ df["TRRZ_DJ"] = df["TRRZ"].apply(lambda x: classify_bulk_density(x) if pd.notna(x) else np.nan)
+ df["TRRZ_DJZY"] = "等级一: >1.00~1.20,等级二: 1.20~1.30,等级三: 1.30~1.40,0.90~1.0,等级四: 1.40~1.50,等级五: >1.50,≤0.90"
+ if "TN" in df.columns:
+ df["TN_DJ"] = df["TN"].apply(lambda x: classify_total_n(x) if pd.notna(x) else np.nan)
+ df["TN_DJZY"] = "等级一: >2.0,等级二: 1.5~2.0,等级三: 1.0~1.5,等级四: 0.5~1.0,等级五: ≤0.5"
+ if "TP" in df.columns:
+ df["TP_DJ"] = df["TP"].apply(lambda x: classify_total_p(x) if pd.notna(x) else np.nan)
+ df["TP_DJZY"] = "等级一: >1.5,等级二: 1.0~1.5,等级三: 0.6~1.0,等级四: 0.4~0.6,等级五: ≤0.4"
+ if "TK" in df.columns:
+ df["TK_DJ"] = df["TK"].apply(lambda x: classify_total_k(x) if pd.notna(x) else np.nan)
+ df["TK_DJZY"] = "等级一: >20.0,等级二: 15.0~20.0,等级三: 10.0~15.0,等级四: 5.0~10.0,等级五: ≤5.0"
+ if "AFE" in df.columns:
+ df["AFE_DJ"] = df["AFE"].apply(lambda x: classify_available_fe(x) if pd.notna(x) else np.nan)
+ df["AFE_DJZY"] = "等级一: >20,等级二: 10~20,等级三: 8~10,等级四: 3~8,等级五: ≤3"
+ if "AZN" in df.columns:
+ df["AZN_DJ"] = df["AZN"].apply(lambda x: classify_available_zn(x) if pd.notna(x) else np.nan)
+ df["AZN_DJZY"] = "等级一: >3.0,等级二: 2.0~3.0,等级三: 1.0~2.0,等级四: 0.5~1.0,等级五: ≤0.5"
+ if "AMN" in df.columns:
+ df["AMN_DJ"] = df["AMN"].apply(lambda x: classify_available_mn(x) if pd.notna(x) else np.nan)
+ df["AMN_DJZY"] = "等级一: >30.0,等级二: 20.0~30.0,等级三: 10.0~20.0,等级四: 5.0~10.0,等级五: ≤5.0"
+ if "ACU" in df.columns:
+ df["ACU_DJ"] = df["ACU"].apply(lambda x: classify_available_cu(x) if pd.notna(x) else np.nan)
+ df["ACU_DJZY"] = "等级一: >1.80,等级二: 1.00~1.80,等级三: 0.50~1.00,等级四: 0.2~0.5,等级五: ≤0.2"
+ if "AB" in df.columns:
+ df["AB_DJ"] = df["AB"].apply(lambda x: classify_available_b(x) if pd.notna(x) else np.nan)
+ df["AB_DJZY"] = "等级一: >2.0,等级二: 1.0~2.0,等级三: 0.5~1.0,等级四: 0.2~0.5,等级五: ≤0.2"
+ if "AMO" in df.columns:
+ df["AMO_DJ"] = df["AMO"].apply(lambda x: classify_available_mo(x) if pd.notna(x) else np.nan)
+ df["AMO_DJZY"] = "等级一: >0.20,等级二: 0.15~0.20,等级三: 0.10~0.15,等级四: 0.05~0.10,等级五: ≤0.05"
+ if "AS1" in df.columns:
+ df["AS1_DJ"] = df["AS1"].apply(lambda x: classify_available_s(x) if pd.notna(x) else np.nan)
+ df["AS1_DJZY"] = "等级一: >40.0,等级二: 30.0~40.0,等级三: 20.0~30.0,等级四: 10.0~20.0,等级五: ≤10.0"
+ if "ECA" in df.columns:
+ df["ECA_DJ"] = df["ECA"].apply(lambda x: classify_exchangeable_ca(x) if pd.notna(x) else np.nan)
+ df["ECA_DJZY"] = "等级一: >20,等级二: 15~30.0,等级三: 10.0~15.0,等级四: 5~10.0,等级五: ≤5.0"
+ if "EMG" in df.columns:
+ df["EMG_DJ"] = df["EMG"].apply(lambda x: classify_exchangeable_mg(x) if pd.notna(x) else np.nan)
+ df["EMG_DJZY"] = "等级一: >1.64,等级二: 0.82~1.64,等级三: 0.41~0.82,等级四: 0.21~0.41,等级五: ≤0.21"
+ if "TSE" in df.columns:
+ df["TSE_DJ"] = df["TSE"].apply(lambda x: classify_total_se(x) if pd.notna(x) else np.nan)
+ df["TSE_DJZY"] = "等级一: >3.0,等级二: 0.4~3.0,等级三: 0.17~0.4,等级四:≤0.17"
+ if "FL" in df.columns:
+ df["FL_DJ"] = df["FL"].apply(lambda x: classify_silt(x) if pd.notna(x) else np.nan)
+ df["FL_DJZY"] = "等级一: ≤15,等级二: 15~30,等级三: 30~45,等级四: 45~75,等级五: >75"
+ if "NL" in df.columns:
+ df["NL_DJ"] = df["NL"].apply(lambda x: classify_clay(x) if pd.notna(x) else np.nan)
+ df["NL_DJZY"] = "等级一: ≤15,等级二: 15~25,等级三: 25~45,等级四: 45~65,等级五: >65"
+ if "SL" in df.columns:
+ df["SL_DJ"] = df["SL"].apply(lambda x: classify_sand(x) if pd.notna(x) else np.nan)
+ df["SL_DJZY"] = "等级一: ≤30,等级二: 30~40,等级三: 40~55,等级四: 55~85,等级五: >85"
+ if "YXTCHD" in df.columns:
+ df["YXTCHD_DJ"] = df["YXTCHD"].apply(lambda x: classify_yxtchd(x) if pd.notna(x) else np.nan)
+ df["YXTCHD_DJZY"] = "等级一: >100,等级二: 80~100,等级三: 60~80,等级四: 40~60,等级五: ≤40"
+ if "TRZD" in df.columns:
+ df["TRZD_DJ"] = df["TRZD"].apply(lambda x: classify_trzd(x) if pd.notna(x) else np.nan)
+ df["TRZD_DJZY"] = "粉(砂)质黏壤土: 1,粉(砂)质黏土: 2,粉(砂)质壤土: 3,黏壤土: 4,黏土: 5,壤土: 6,壤质黏土: 7,砂土及壤质砂土: 8,砂质黏壤土: 9,砂质黏土: 10,砂质壤土: 11,重黏土: 12"
+ if "DBLSFD" in df.columns:
+ df["DBLSFD_DJ"] = df["DBLSFD"].apply(lambda x: classify_lsfd(x) if pd.notna(x) else np.nan)
+ df["DBLSFD_DJZY"] = "等级一: =0,等级二: 0~5,等级三: 5.0~15.0,等级四: 15~50,等级五: ≥50"
+
+ return df
+
+def main():
+ """
+ 主函数:程序入口
+ 用户需根据实际情况修改以下参数
+ """
+ logger = init_logger()
+ logger.info("="*50)
+ logger.info("土壤属性栅格面积加权统计程序启动")
+ logger.info("="*50)
+
+ # --------------------------
+ # 用户配置区域(必须修改!)
+ # --------------------------
+ # 1. 矢量图斑文件路径(支持Shapefile、GeoPackage等格式)
+ # TODO
+ VECTOR_PATH = r"F:\@user\missum\Documents\ArcGIS\Projects\MyProject\MyProject.gdb\西畴县耕园林草其他_SpatialJoin" # 示例:"D:/data/土壤图斑.shp"
+
+ # 2. 土壤属性栅格文件配置(键:属性名称,值:栅格文件路径)
+ # 注意:属性名称必须与最终表格列名一致
+ # TODO
+ RASTER_FILES = {
+ # "GZCHD": r"E:\@三普\云南丘北县\基础数据\三普栅格\GZCHD.tif",
+ # "TRRZ": r"E:\@三普\云南丘北县\基础数据\三普栅格\TRRZ.tif",
+ # "SL": r"E:\@三普\云南丘北县\基础数据\三普栅格\SL.tif",
+ # "FL": r"E:\@三普\云南丘北县\基础数据\三普栅格\FL.tif",
+ # "NL": r"E:\@三普\云南丘北县\基础数据\三普栅格\NL.tif",
+ # "PH": r"E:\@三普\云南丘北县\基础数据\三普栅格\PH.tif",
+ # "CEC": r"E:\@三普\云南丘北县\基础数据\三普栅格\CEC.tif",
+ # "OM": r"E:\@三普\云南丘北县\基础数据\三普栅格\OM.tif",
+ # "TN": r"E:\@三普\云南丘北县\基础数据\三普栅格\TN.tif",
+ # "TP": r"E:\@三普\云南丘北县\基础数据\三普栅格\TP.tif",
+ # "TK": r"E:\@三普\云南丘北县\基础数据\三普栅格\TK.tif",
+ # "AP": r"E:\@三普\云南丘北县\基础数据\三普栅格\AP.tif",
+ # "AK": r"E:\@三普\云南丘北县\基础数据\三普栅格\AK.tif",
+ # "AFE": r"E:\@三普\云南丘北县\基础数据\三普栅格\AFE.tif",
+ # "AZN": r"E:\@三普\云南丘北县\基础数据\三普栅格\AZN.tif",
+ # "AMN": r"E:\@三普\云南丘北县\基础数据\三普栅格\AMN.tif",
+ # "ACU": r"E:\@三普\云南丘北县\基础数据\三普栅格\ACU.tif",
+ # "AB": r"E:\@三普\云南丘北县\基础数据\三普栅格\AB.tif",
+ # "AMO": r"E:\@三普\云南丘北县\基础数据\三普栅格\AMO.tif",
+ # "AS1": r"E:\@三普\云南丘北县\基础数据\三普栅格\AS1.tif",
+ # "ECA": r"E:\@三普\云南丘北县\基础数据\三普栅格\ECA.tif",
+ # "EMG": r"E:\@三普\云南丘北县\基础数据\三普栅格\EMG.tif",
+ # "YXTCHD": r"E:\@三普\云南丘北县\基础数据\三普栅格\YXTCHD.tif",
+ "TRZD": r"E:\@三普\云南西畴县\基础数据\三普栅格\投影后\TRZD.tif",
+ # "DBLSFD": r"E:\@三普\云南丘北县\基础数据\三普栅格\LSFD.tif",
+ # "TSE": r"E:\@三普\云南丘北县\基础数据\三普栅格\TSE.tif"
+ }
+
+ # 3. 结果输出路径(Excel文件)
+ OUTPUT_PATH = r"E:\@三普\@临时文件夹\西畴县TRSX11.shp" # 示例:"D:/result/结果.shp"
+
+ # --------------------------
+ # 程序执行流程(无需修改)
+ # --------------------------
+ try:
+ # 1. 数据验证
+ if not validate_data(VECTOR_PATH, RASTER_FILES):
+ logger.error("数据验证失败,程序终止")
+ return
+
+ # 2. 执行面积加权统计
+ result_gdf = calculate_area_weighted_stats(VECTOR_PATH, RASTER_FILES, OUTPUT_PATH)
+
+ # 3. 显示结果预览
+ # logger.info("\\n结果预览(前3行):")
+ # print(result_gdf.head(3).to_string(index=False))
+
+ logger.info("\\n" + "="*50)
+ logger.info("程序执行完成!")
+ logger.info(f"结果文件:{OUTPUT_PATH}")
+ logger.info("="*50)
+
+ except Exception as e:
+ logger.error(f"程序执行出错:{str(e)}", exc_info=True)
+ logger.error("程序异常终止")
+
+if __name__ == "__main__":
+ # 启动主程序
+ main()
diff --git a/scripts/其他工具/面积加权均值_西南.py b/scripts/其他工具/面积加权均值_西南.py
new file mode 100644
index 0000000..76c2207
--- /dev/null
+++ b/scripts/其他工具/面积加权均值_西南.py
@@ -0,0 +1,932 @@
+# 生成完整的exactextract面积加权计算Python脚本
+# -*- coding: utf-8 -*-
+"""
+土壤属性栅格数据面积加权统计脚本
+基于exactextract库实现多属性栅格的面积加权平均值计算
+最终输出格式与土壤属性图斑数据表一致
+"""
+
+import exactextract as ee
+import geopandas as gpd
+import rasterio
+import pandas as pd
+import numpy as np
+from pathlib import Path
+import warnings
+warnings.filterwarnings('ignore')
+
+def init_logger():
+ """初始化日志输出,便于跟踪处理过程"""
+ import logging
+ logging.basicConfig(
+ level=logging.INFO,
+ format='%(asctime)s - %(levelname)s - %(message)s',
+ datefmt='%Y-%m-%d %H:%M:%S'
+ )
+ return logging.getLogger(__name__)
+
+def validate_data(vector_path, raster_files):
+ """
+ 验证输入数据的有效性
+ :param vector_path: 矢量图斑文件路径
+ :param raster_files: 栅格文件字典
+ :return: 验证结果(布尔值)
+ """
+ logger = init_logger()
+
+ # 验证矢量文件是否存在
+ if not Path(vector_path).exists():
+ logger.error(f"矢量文件不存在:{vector_path}")
+ return False
+
+ # 验证栅格文件是否存在
+ for attr_name, raster_path in raster_files.items():
+ if not Path(raster_path).exists():
+ logger.error(f"栅格文件不存在:{attr_name} -> {raster_path}")
+ return False
+
+ # 验证矢量文件格式
+ try:
+ gdf = gpd.read_file(vector_path)
+ if gdf.geometry.type.unique()[0] != 'Polygon':
+ logger.error("矢量文件必须是Polygon类型(面要素)")
+ return False
+ except Exception as e:
+ logger.error(f"矢量文件读取失败:{str(e)}")
+ return False
+
+ # 验证栅格文件格式
+ try:
+ test_raster = next(iter(raster_files.values()))
+ with rasterio.open(test_raster) as src:
+ if src.count != 1:
+ logger.error("每个栅格文件必须是单波段(每个属性单独一个栅格)")
+ return False
+ except Exception as e:
+ logger.error(f"栅格文件读取失败:{str(e)}")
+ return False
+
+ logger.info("所有输入数据验证通过")
+ return True
+
+def standardize_crs(gdf, raster_path):
+ """
+ 标准化矢量与栅格的坐标参考系统(CRS)
+ :param gdf: 矢量GeoDataFrame
+ :param raster_path: 任意一个栅格文件路径(用于获取目标CRS)
+ :return: 标准化后的GeoDataFrame
+ """
+ logger = init_logger()
+
+ # 获取栅格CRS
+ with rasterio.open(raster_path) as src:
+ raster_crs = src.crs
+ raster_crs_str = src.crs.to_string()
+
+ # 获取矢量CRS
+ vector_crs = gdf.crs
+ vector_crs_str = gdf.crs.to_string()
+
+ logger.info(f"当前矢量CRS:{vector_crs_str}")
+ logger.info(f"目标栅格CRS:{raster_crs_str}")
+
+ # 若CRS不一致,进行转换
+ if vector_crs != raster_crs:
+ logger.warning("矢量与栅格CRS不一致,正在进行转换...")
+ gdf = gdf.to_crs(raster_crs)
+ logger.info(f"CRS转换完成,新矢量CRS:{gdf.crs.to_string()}")
+
+ return gdf
+
+def calculate_area_weighted_stats(vector_path, raster_files, output_path):
+ """
+ 核心函数:计算面积加权统计值
+ :param vector_path: 矢量图斑文件路径
+ :param raster_files: 栅格文件字典(键:属性名,值:栅格路径)
+ :param output_path: 结果输出路径(Excel文件)
+ :return: 统计结果DataFrame
+ """
+ logger = init_logger()
+ logger.info("开始面积加权统计计算")
+
+ # 1. 加载矢量数据
+ logger.info(f"加载矢量数据:{vector_path}")
+ gdf = gpd.read_file(vector_path)
+
+ # 2. 标准化CRS
+ test_raster = next(iter(raster_files.values()))
+ gdf = standardize_crs(gdf, test_raster)
+
+ # 3. 初始化结果DataFrame(保留矢量中的关键属性)
+ logger.info("初始化结果数据结构")
+ # 基础字段列表(与土壤属性图斑表格式对齐)
+ # TODO
+ base_fields = ["FID","DM","XZM","QSDWDM","QSDWMC","TL", "YL", "TS", "TZ", "DLBM", "DLMC"]
+
+ # 检查矢量中是否包含必要字段,若不包含则创建空字段
+ result_df = pd.DataFrame()
+ for field in base_fields:
+ if field in gdf.columns:
+ result_df[field] = gdf[field]
+ else:
+ result_df[field] = np.nan
+ logger.warning(f"矢量中缺少'{field}'字段,将生成空值")
+
+ # 4. 计算图斑面积(转换为亩)
+ logger.info("计算图斑面积(单位:亩)")
+ # 计算平方米面积(根据CRS单位自动适应)
+ gdf["area_sqm"] = gdf.geometry.area
+ # 转换为亩(1亩 ≈ 666.6667平方米)
+ result_df["面积亩"] = gdf["area_sqm"] * 0.0015
+ # 保留6位小数,与示例数据格式一致
+ result_df["面积亩"] = result_df["面积亩"].round(6)
+
+ # 5. 对每个土壤属性进行面积加权平均计算
+ logger.info("开始处理土壤属性栅格(面积加权平均)")
+ for attr_idx, (attr_name, raster_path) in enumerate(raster_files.items(), 1):
+ total_attrs = len(raster_files)
+ logger.info(f"处理进度:{attr_idx}/{total_attrs} - 属性:{attr_name}")
+
+ try:
+ # 使用exactextract计算面积加权平均
+ # weights="area":按矢量与栅格的交集面积进行加权
+ stats = ee.exact_extract(
+ raster_path,
+ gdf,
+ ["mean"], # 计算平均值
+ output="pandas" # 输出为DataFrame格式
+ )
+
+ # 将统计结果添加到结果DataFrame
+ if stats is None:
+ logger.warning(f"{attr_name}计算结果为空,可能无交集区域")
+ result_df[attr_name] = np.nan
+ continue
+ else:
+ # 确保 stats 为 pandas.DataFrame,以便使用字符串索引
+ if not isinstance(stats, pd.DataFrame):
+ stats = pd.DataFrame(stats)
+
+ # 保留4位小数,确保数据精度
+ result_df[attr_name] = stats["mean"].round(4)
+
+ # 处理可能的空值(无交集区域)
+ if result_df[attr_name].isnull().sum() > 0:
+ null_count = result_df[attr_name].isnull().sum()
+ logger.warning(f"{attr_name}存在{null_count}个空值(图斑与栅格无交集)")
+ # 用0填充空值(可根据业务需求调整)
+ result_df[attr_name] = result_df[attr_name].fillna(0)
+
+ except Exception as e:
+ logger.error(f"{attr_name}处理失败:{str(e)}")
+ # 失败时填充空值,避免整个程序崩溃
+ result_df[attr_name] = np.nan
+
+ # 6. 添加属性分级字段(根据业务规则实现)
+ logger.info("添加土壤属性分级字段")
+ result_df = add_attribute_classification(result_df)
+
+ # 7. 整理最终字段顺序(与示例表格完全对齐)
+ logger.info("整理输出字段顺序")
+ # TODO
+ final_columns = [
+ "FID","DM", "XZM", "QSDWDM", "QSDWMC",
+ "TL", "YL", "TS", "TZ", "DLBM", "DLMC",
+ "耕层厚度", "土壤容重", "砂粒", "粉粒", "黏粒", "酸碱度", "阳离子",
+ "有机质", "全氮", "全磷", "全钾", "有效磷", "速效钾", "有效铁", "有效锰",
+ "有效铜", "有效锌", "有效硼", "有效钼", "有效硫", "交换性钙", "交换性镁", "全硒",
+ "有效土层厚度", "土壤质地",
+ "耕层厚度分级", "土壤容重分级", "砂粒分级", "粉粒分级", "黏粒分级", "酸碱度分级", "阳离子分级",
+ "有机质分级", "全氮分级", "全磷分级", "全钾分级", "有效铁分级", "速效钾分级", "有效铁分级", "有效锰分级",
+ "有效铜分级", "有效锌分级", "有效硼分级", "有效钼分级", "有效硫分级", "交换性钙分级", "交换性镁分级", "全硒分级",
+ "有效土层厚度分级", "土壤质地分级",
+ "面积亩"
+ ]
+
+ # 补充缺失的字段(如乡代码等)
+ for col in final_columns:
+ if col not in result_df.columns:
+ result_df[col] = np.nan
+
+ # 按最终顺序排列字段
+ result_df = result_df[final_columns]
+
+ # 8. 导出结果到Excel
+ logger.info(f"导出结果到:{output_path}")
+ # 使用openpyxl引擎支持.xlsx格式
+ result_df.to_excel(output_path, index=False, engine="openpyxl")
+ logger.info(f"结果导出完成,共生成{len(result_df)}条记录")
+
+ return result_df
+
+def add_attribute_classification(df):
+ """
+ 添加土壤属性分级字段(根据常见土壤分类标准实现)
+ 可根据实际业务需求调整分级阈值
+ :param df: 包含原始属性的DataFrame
+ :return: 包含分级字段的DataFrame
+ """
+ # todo
+ # 1. 酸碱度分级(pH值) - 按PH标准
+ def classify_ph(ph):
+ """
+ 土壤pH值分级(第三次全国土壤普查标准)
+ 标准等级:
+ 等级一: 6.0~7.0
+ 等级二: 7.0~7.5, 5.5~6.0
+ 等级三: 7.5~8.0, 5.0~5.5
+ 等级四: 8.0~8.5, 4.5~5.0
+ 等级五: >8.5, ≤4.5
+ """
+ if ph > 8.5 or ph <= 4.5:
+ return 5 # 等级五: >8.5, ≤4.5
+ elif (8.0 < ph <= 8.5) or (4.5 < ph <= 5.0):
+ return 4 # 等级四: 8.0~8.5, 4.5~5.0
+ elif (7.5 < ph <= 8.0) or (5.0 < ph <= 5.5):
+ return 3 # 等级三: 7.5~8.0, 5.0~5.5
+ elif (7.0 < ph <= 7.5) or (5.5 < ph <= 6.0):
+ return 2 # 等级二: 7.0~7.5, 5.5~6.0
+ elif 6.0 < ph <= 7.0:
+ return 1 # 等级一: 6.0~7.0
+ else:
+ return None # 异常值
+
+ # 2. 有机质分级(单位:g/kg) - 按OM标准
+ def classify_organic(organic):
+ """
+ 土壤有机质分级(第三次全国土壤普查标准)
+ 标准等级:
+ 等级一: >35.0
+ 等级二: 25.0~35.0
+ 等级三: 15.0~25.0
+ 等级四: 10.0~15.0
+ 等级五: ≤10.0
+ """
+ if organic > 35.0:
+ return 1 # 等级一: >35.0
+ elif 25.0 < organic <= 35.0:
+ return 2 # 等级二: 25.0~35.0
+ elif 15.0 < organic <= 25.0:
+ return 3 # 等级三: 15.0~25.0
+ elif 10.0 < organic <= 15.0:
+ return 4 # 等级四: 10.0~15.0
+ elif organic <= 10.0:
+ return 5 # 等级五: ≤10.0
+ else:
+ return None # 异常值
+
+ # 3. 阳离子交换量分级(单位:cmol/kg) - 按CEC标准
+ def classify_cation(cation):
+ """
+ 土壤阳离子交换量分级(第三次全国土壤普查标准)
+ 标准等级:
+ 等级一: >30.0
+ 等级二: 20.0~30.0
+ 等级三: 15.0~20.0
+ 等级四: 10.0~15.0
+ 等级五: ≤10.0
+ """
+ if cation > 30.0:
+ return 1 # 等级一: >30.0
+ elif 20.0 < cation <= 30.0:
+ return 2 # 等级二: 20.0~30.0
+ elif 15.0 < cation <= 20.0:
+ return 3 # 等级三: 15.0~20.0
+ elif 10.0 < cation <= 15.0:
+ return 4 # 等级四: 10.0~15.0
+ elif cation <= 10.0:
+ return 5 # 等级五: ≤10.0
+ else:
+ return None # 异常值
+
+ # 4. 有效磷分级(单位:mg/kg) - 按AP标准
+ def classify_available_p(p):
+ """
+ 土壤有效磷分级(第三次全国土壤普查标准)
+ 标准等级:
+ 等级一: >40.0
+ 等级二: 25.0~40.0
+ 等级三: 15.0~25.0
+ 等级四: 5.0~15.0
+ 等级五: ≤5.0
+ """
+ if p > 40.0:
+ return 1 # 等级一: >40.0
+ elif 25.0 < p <= 40.0:
+ return 2 # 等级二: 25.0~40.0
+ elif 15.0 < p <= 25.0:
+ return 3 # 等级三: 15.0~25.0
+ elif 5.0 < p <= 15.0:
+ return 4 # 等级四: 5.0~15.0
+ elif p <= 5.0:
+ return 5 # 等级五: ≤5.0
+ else:
+ return None # 异常值
+
+ # 5. 速效钾分级(单位:mg/kg) - 按AK标准
+ def classify_available_k(k):
+ """
+ 土壤速效钾分级(第三次全国土壤普查标准)
+ 标准等级:
+ 等级一: >150
+ 等级二: 100~150
+ 等级三: 75~100
+ 等级四: 50~75
+ 等级五: ≤50
+ """
+ if k > 150:
+ return 1 # 等级一: >150
+ elif 100 < k <= 150:
+ return 2 # 等级二: 100~150
+ elif 75 < k <= 100:
+ return 3 # 等级三: 75~100
+ elif 50 < k <= 75:
+ return 4 # 等级四: 50~75
+ elif k <= 50:
+ return 5 # 等级五: ≤50
+ else:
+ return None # 异常值
+
+ # 6. 耕层厚度分级(单位:cm) - 按GZCHD标准
+ def classify_soil_depth(depth):
+ """
+ 土壤耕作层厚度分级(第三次全国土壤普查标准)
+ 标准等级:
+ 等级一: >25.0
+ 等级二: 20.0~25.0
+ 等级三: 15.0~20.0
+ 等级四: 10.0~15.0
+ 等级五: ≤10.0
+ """
+ if depth > 25.0:
+ return 1 # 等级一: >25.0
+ elif 20.0 < depth <= 25.0:
+ return 2 # 等级二: 20.0~25.0
+ elif 15.0 < depth <= 20.0:
+ return 3 # 等级三: 15.0~20.0
+ elif 10.0 < depth <= 15.0:
+ return 4 # 等级四: 10.0~15.0
+ elif depth <= 10.0:
+ return 5 # 等级五: ≤10.0
+ else:
+ return None # 异常值
+
+ # 7. 土壤容重分级(单位:g/cm³) - 按TRRZ标准
+ def classify_bulk_density(density):
+ """
+ 土壤容重分级(第三次全国土壤普查标准)
+ 标准等级:
+ 等级一: 1.10~1.25
+ 等级二: 1.25~1.35, 1.00~1.10
+ 等级三: 1.35~1.45
+ 等级四: 1.45~1.55, 0.90~1.00
+ 等级五: >1.55, ≤0.90
+ """
+ if 1.10 < density <= 1.25:
+ return 1 # 等级一: 1.10~1.25
+ elif (1.25 < density <= 1.35) or (1.00 < density <= 1.10):
+ return 2 # 等级二: 1.25~1.35, 1.00~1.10
+ elif 1.35 < density <= 1.45:
+ return 3 # 等级三: 1.35~1.45
+ elif (1.45 < density <= 1.55) or (0.90 < density <= 1.00):
+ return 4 # 等级四: 1.45~1.55, 0.90~1.00
+ elif density > 1.55 or density <= 0.90:
+ return 5 # 等级五: >1.55, ≤0.90
+ else:
+ return None # 异常值
+
+ # 8. 全氮分级(单位:g/kg) - 按TN标准
+ def classify_total_n(n):
+ """
+ 土壤全氮分级(第三次全国土壤普查标准)
+ 标准等级:
+ 等级一: >2.00
+ 等级二: 1.50~2.00
+ 等级三: 1.00~1.50
+ 等级四: 0.50~1.00
+ 等级五: ≤0.50
+ """
+ if n > 2.00:
+ return 1 # 等级一: >2.00
+ elif 1.50 < n <= 2.00:
+ return 2 # 等级二: 1.50~2.00
+ elif 1.00 < n <= 1.50:
+ return 3 # 等级三: 1.00~1.50
+ elif 0.50 < n <= 1.00:
+ return 4 # 等级四: 0.50~1.00
+ elif n <= 0.50:
+ return 5 # 等级五: ≤0.50
+ else:
+ return None # 异常值
+
+ # 9. 全磷分级(单位:g/kg) - 按TP标准
+ def classify_total_p(p):
+ """
+ 土壤全磷分级(第三次全国土壤普查标准)
+ 标准等级:
+ 等级一: >1.00
+ 等级二: 0.80~1.00
+ 等级三: 0.60~0.80
+ 等级四: 0.40~0.60
+ 等级五: ≤0.40
+ """
+ if p > 1.00:
+ return 1 # 等级一: >1.00
+ elif 0.80 < p <= 1.00:
+ return 2 # 等级二: 0.80~1.00
+ elif 0.60 < p <= 0.80:
+ return 3 # 等级三: 0.60~0.80
+ elif 0.40 < p <= 0.60:
+ return 4 # 等级四: 0.40~0.60
+ elif p <= 0.40:
+ return 5 # 等级五: ≤0.40
+ else:
+ return None # 异常值
+
+ # 10. 全钾分级(单位:g/kg) - 按TK标准
+ def classify_total_k(k):
+ """
+ 土壤全钾分级(第三次全国土壤普查标准)
+ 标准等级:
+ 等级一: >20.0
+ 等级二: 15.0~20.0
+ 等级三: 10.0~15.0
+ 等级四: 5.0~10.0
+ 等级五: ≤5.0
+ """
+ if k > 20.0:
+ return 1 # 等级一: >20.0
+ elif 15.0 < k <= 20.0:
+ return 2 # 等级二: 15.0~20.0
+ elif 10.0 < k <= 15.0:
+ return 3 # 等级三: 10.0~15.0
+ elif 5.0 < k <= 10.0:
+ return 4 # 等级四: 5.0~10.0
+ elif k <= 5.0:
+ return 5 # 等级五: ≤5.0
+ else:
+ return None # 异常值
+
+ # 11. 有效铁分级(单位:mg/kg)
+ def classify_available_fe(fe):
+ """
+ 土壤有效铁分级(第三次全国土壤普查标准)
+
+ 参数:
+ fe: 有效铁含量 (mg/kg)
+
+ 返回:
+ 分级等级 (1-5)
+ """
+ if fe <= 3.0:
+ return 5 # 等级五: ≤3.0
+ elif 3.0 < fe <= 5.0:
+ return 4 # 等级四: 3.0~5.0
+ elif 5.0 < fe <= 10.0:
+ return 3 # 等级三: 5.0~10.0
+ elif 10.0 < fe <= 20.0:
+ return 2 # 等级二: 10.0~20.0
+ else: # fe > 20.0
+ return 1 # 等级一: >20.0
+
+ # 12. 有效锌分级(单位:mg/kg)
+ def classify_available_zn(zn):
+ """
+ 土壤有效锌分级(第三次全国土壤普查标准)
+
+ 参数:
+ zn: 有效锌含量 (mg/kg)
+
+ 返回:
+ 分级等级 (1-5)
+ """
+ if zn <= 0.20:
+ return 5 # 等级五: ≤0.20
+ elif 0.20 < zn <= 0.50:
+ return 4 # 等级四: 0.20~0.50
+ elif 0.50 < zn <= 1.00:
+ return 3 # 等级三: 0.50~1.00
+ elif 1.00 < zn <= 3.00:
+ return 2 # 等级二: 1.00~3.00
+ else: # zn > 3.00
+ return 1 # 等级一: >3.00
+
+ # 13. 有效锰分级(单位:mg/kg)
+ def classify_available_mn(mn):
+ """
+ 土壤有效锰分级(第三次全国土壤普查标准)
+
+ 参数:
+ mn: 有效锰含量 (mg/kg)
+
+ 返回:
+ 分级等级 (1-5)
+ """
+ if mn <= 1.0:
+ return 5 # 等级五: ≤1.0
+ elif 1.0 < mn <= 5.0:
+ return 4 # 等级四: 1.0~5.0
+ elif 5.0 < mn <= 15.0:
+ return 3 # 等级三: 5.0~15.0
+ elif 15.0 < mn <= 30.0:
+ return 2 # 等级二: 15.0~30.0
+ else: # mn > 30.0
+ return 1 # 等级一: >30.0
+
+ # 14. 有效铜分级(单位:mg/kg)
+ def classify_available_cu(cu):
+ """
+ 土壤有效铜分级(第三次全国土壤普查标准)
+
+ 参数:
+ cu: 有效铜含量 (mg/kg)
+
+ 返回:
+ 分级等级 (1-5)
+ """
+ if cu <= 0.20:
+ return 5 # 等级五: ≤0.20
+ elif 0.20 < cu <= 0.50:
+ return 4 # 等级四: 0.20~0.50
+ elif 0.50 < cu <= 1.00:
+ return 3 # 等级三: 0.50~1.00
+ elif 1.00 < cu <= 2.00:
+ return 2 # 等级二: 1.00~2.00
+ else: # cu > 2.00
+ return 1 # 等级一: >2.00
+
+ # 15. 有效硼分级(单位:mg/kg)
+ def classify_available_b(b):
+ """
+ 土壤有效硼分级(第三次全国土壤普查标准)
+
+ 参数:
+ b: 有效硼含量 (mg/kg)
+
+ 返回:
+ 分级等级 (1-5)
+ """
+ if b <= 0.20:
+ return 5 # 等级五: ≤0.20
+ elif 0.20 < b <= 0.50:
+ return 4 # 等级四: 0.20~0.50
+ elif 0.50 < b <= 0.80:
+ return 3 # 等级三: 0.50~0.80
+ elif 0.80 < b <= 1.00:
+ return 2 # 等级二: 0.80~1.00
+ else: # b > 1.00
+ return 1 # 等级一: >1.00
+
+ # 16. 有效钼分级(单位:mg/kg)
+ def classify_available_mo(mo):
+ """
+ 土壤有效钼分级(第三次全国土壤普查标准)
+
+ 参数:
+ mo: 有效钼含量 (mg/kg)
+
+ 返回:
+ 分级等级 (1-5)
+ """
+ if mo <= 0.05:
+ return 5 # 等级五: ≤0.05
+ elif 0.05 < mo <= 0.10:
+ return 4 # 等级四: 0.05~0.10
+ elif 0.10 < mo <= 0.15:
+ return 3 # 等级三: 0.10~0.15
+ elif 0.15 < mo <= 0.20:
+ return 2 # 等级二: 0.15~0.20
+ else: # mo > 0.20
+ return 1 # 等级一: >0.20
+
+ # 17. 有效硫分级(单位:mg/kg)
+ def classify_available_s(s):
+ """
+ 土壤有效硫分级(第三次全国土壤普查标准)
+
+ 参数:
+ s: 有效硫含量 (mg/kg)
+
+ 返回:
+ 分级等级 (1-5)
+ """
+ if s <= 10.0:
+ return 5 # 等级五: ≤10.0
+ elif 10.0 < s <= 20.0:
+ return 4 # 等级四: 10.0~20.0
+ elif 20.0 < s <= 30.0:
+ return 3 # 等级三: 20.0~30.0
+ elif 30.0 < s <= 40.0:
+ return 2 # 等级二: 30.0~40.0
+ else: # s > 40.0
+ return 1 # 等级一: >40.0
+
+ # 18. 交换性钙分级(单位:cmol(½Ca²⁺)/kg)
+ def classify_exchangeable_ca(ca):
+ """
+ 土壤交换性钙分级(第三次全国土壤普查标准)
+
+ 参数:
+ ca: 交换性钙含量 (cmol(½Ca²⁺)/kg)
+
+ 返回:
+ 分级等级 (1-5)
+ """
+ if ca <= 1.00:
+ return 5 # 等级五: ≤1.00
+ elif 1.00 < ca <= 2.50:
+ return 4 # 等级四: 1.00~2.50
+ elif 2.50 < ca <= 4.99:
+ return 3 # 等级三: 2.50~4.99
+ elif 4.99 < ca <= 7.49:
+ return 2 # 等级二: 4.99~7.49
+ else: # ca > 7.49
+ return 1 # 等级一: >7.49
+
+ # 19. 交换性镁分级(单位:cmol(½Mg²⁺)/kg)
+ def classify_exchangeable_mg(mg):
+ """
+ 土壤交换性镁分级(第三次全国土壤普查标准)
+
+ 参数:
+ mg: 交换性镁含量 (cmol(½Mg²⁺)/kg)
+
+ 返回:
+ 分级等级 (1-5)
+ """
+ if mg <= 0.41:
+ return 5 # 等级五: ≤0.41
+ elif 0.41 < mg <= 0.82:
+ return 4 # 等级四: 0.41~0.82
+ elif 0.82 < mg <= 1.23:
+ return 3 # 等级三: 0.82~1.23
+ elif 1.23 < mg <= 1.64:
+ return 2 # 等级二: 1.23~1.64
+ else: # mg > 1.64
+ return 1 # 等级一: >1.64
+
+ # 20. 全硒分级(单位:mg/kg)
+ def classify_total_se(se):
+ """
+ 土壤全硒分级(第三次全国土壤普查标准)
+
+ 参数:
+ se: 全硒含量 (mg/kg)
+
+ 返回:
+ 分级等级 (1-4)
+ """
+ if se <= 0.17:
+ return 4 # 等级四: ≤0.17
+ elif 0.17 < se <= 0.40:
+ return 3 # 等级三: 0.17~0.40
+ elif 0.40 < se <= 3.00:
+ return 2 # 等级二: 0.40~3.00
+ else: # se > 3.00
+ return 1 # 等级一: >3.00
+
+ # 21. 粉粒含量分级(单位:%)
+ def classify_silt(silt):
+ """
+ 土壤粉粒含量分级(第三次全国土壤普查标准)
+
+ 参数:
+ silt: 粉粒含量 (%)
+
+ 返回:
+ 分级等级 (1-5)
+ """
+ if silt > 75:
+ return 5 # 等级五: >75
+ elif 45 < silt <= 75:
+ return 4 # 等级四: 45~75
+ elif 30 < silt <= 45:
+ return 3 # 等级三: 30~45
+ elif 15 < silt <= 30:
+ return 2 # 等级二: 15~30
+ else: # silt <= 15
+ return 1 # 等级一: ≤15
+
+ # 22. 黏粒含量分级(单位:%)
+ def classify_clay(clay):
+ """
+ 土壤黏粒含量分级(第三次全国土壤普查标准)
+
+ 参数:
+ clay: 黏粒含量 (%)
+
+ 返回:
+ 分级等级 (1-5)
+ """
+ if clay > 65:
+ return 5 # 等级五: >65
+ elif 45 < clay <= 65:
+ return 4 # 等级四: 45~65
+ elif 25 < clay <= 45:
+ return 3 # 等级三: 25~45
+ elif 15 < clay <= 25:
+ return 2 # 等级二: 15~25
+ else: # clay <= 15
+ return 1 # 等级一: ≤15
+
+ # 23. 砂粒含量分级(单位:%)
+ def classify_sand(sand):
+ """
+ 土壤砂粒含量分级(第三次全国土壤普查标准)
+
+ 参数:
+ sand: 砂粒含量 (%)
+
+ 返回:
+ 分级等级 (1-5)
+ """
+ if sand > 85:
+ return 5 # 等级五: >85
+ elif 55 < sand <= 85:
+ return 4 # 等级四: 55~85
+ elif 40 < sand <= 55:
+ return 3 # 等级三: 40~55
+ elif 30 < sand <= 40:
+ return 2 # 等级二: 30~40
+ else: # sand <= 30
+ return 1 # 等级一: ≤30
+
+ # 24. 有效土层厚度分级(单位:cm)
+ def classify_yxtchd(depth):
+ """
+ 土壤有效土层厚度分级(第三次全国土壤普查标准)
+
+ 参数:
+ depth: 有效土层厚度 (cm)
+
+ 返回:
+ 分级等级 (1-5)
+ """
+ if depth <= 40:
+ return 5 # 等级五: ≤40
+ elif 40 < depth <= 60:
+ return 4 # 等级四: 40~60
+ elif 60 < depth <= 80:
+ return 3 # 等级三: 60~80
+ elif 80 < depth <= 100:
+ return 2 # 等级二: 80~100
+ else: # depth > 100
+ return 1 # 等级一: >100
+
+ # 25. 土壤质地
+ def classify_trzd(trzd):
+ """
+ 土壤质地
+
+ 参数:
+ trzd: 土壤质地分类(1-5)
+
+ 返回:
+ 分级等级 (1-5)
+ """
+ trzd = round(trzd, 0)
+ if trzd == 5:
+ return 5 # 等级五: ≤40
+ elif trzd == 4:
+ return 4 # 等级四: 40~60
+ elif trzd == 3:
+ return 3 # 等级三: 60~80
+ elif trzd == 2:
+ return 2 # 等级二: 80~100
+ else: # depth > 100
+ return 1 # 等级一: >100
+
+
+ # 应用分级函数(只处理非空值)
+ if "酸碱度" in df.columns:
+ df["酸碱度分级"] = df["酸碱度"].apply(lambda x: classify_ph(x) if pd.notna(x) else np.nan)
+ if "有机质" in df.columns:
+ df["有机质分级"] = df["有机质"].apply(lambda x: classify_organic(x) if pd.notna(x) else np.nan)
+ if "阳离子" in df.columns:
+ df["阳离子分级"] = df["阳离子"].apply(lambda x: classify_cation(x) if pd.notna(x) else np.nan)
+ if "有效磷" in df.columns:
+ df["有效磷分级"] = df["有效磷"].apply(lambda x: classify_available_p(x) if pd.notna(x) else np.nan)
+ if "速效钾" in df.columns:
+ df["速效钾分级"] = df["速效钾"].apply(lambda x: classify_available_k(x) if pd.notna(x) else np.nan)
+ if "耕层厚度" in df.columns:
+ df["耕层厚度分级"] = df["耕层厚度"].apply(lambda x: classify_soil_depth(x) if pd.notna(x) else np.nan)
+ if "土壤容重" in df.columns:
+ df["土壤容重分级"] = df["土壤容重"].apply(lambda x: classify_bulk_density(x) if pd.notna(x) else np.nan)
+ if "全氮" in df.columns:
+ df["全氮分级"] = df["全氮"].apply(lambda x: classify_total_n(x) if pd.notna(x) else np.nan)
+ if "全磷" in df.columns:
+ df["全磷分级"] = df["全磷"].apply(lambda x: classify_total_p(x) if pd.notna(x) else np.nan)
+ if "全钾" in df.columns:
+ df["全钾分级"] = df["全钾"].apply(lambda x: classify_total_k(x) if pd.notna(x) else np.nan)
+ if "有效铁" in df.columns:
+ df["有效铁分级"] = df["有效铁"].apply(lambda x: classify_available_fe(x) if pd.notna(x) else np.nan)
+ if "有效锌" in df.columns:
+ df["有效锌分级"] = df["有效锌"].apply(lambda x: classify_available_zn(x) if pd.notna(x) else np.nan)
+ if "有效锰" in df.columns:
+ df["有效锰分级"] = df["有效锰"].apply(lambda x: classify_available_mn(x) if pd.notna(x) else np.nan)
+ if "有效铜" in df.columns:
+ df["有效铜分级"] = df["有效铜"].apply(lambda x: classify_available_cu(x) if pd.notna(x) else np.nan)
+ if "有效硼" in df.columns:
+ df["有效硼分级"] = df["有效硼"].apply(lambda x: classify_available_b(x) if pd.notna(x) else np.nan)
+ if "有效钼" in df.columns:
+ df["有效钼分级"] = df["有效钼"].apply(lambda x: classify_available_mo(x) if pd.notna(x) else np.nan)
+ if "有效硫" in df.columns:
+ df["有效硫分级"] = df["有效硫"].apply(lambda x: classify_available_s(x) if pd.notna(x) else np.nan)
+ if "交换性钙" in df.columns:
+ df["交换性钙分级"] = df["交换性钙"].apply(lambda x: classify_exchangeable_ca(x) if pd.notna(x) else np.nan)
+ if "交换性镁" in df.columns:
+ df["交换性镁分级"] = df["交换性镁"].apply(lambda x: classify_exchangeable_mg(x) if pd.notna(x) else np.nan)
+ if "全硒" in df.columns:
+ df["全硒分级"] = df["全硒"].apply(lambda x: classify_total_se(x) if pd.notna(x) else np.nan)
+ if "粉粒" in df.columns:
+ df["粉粒分级"] = df["粉粒"].apply(lambda x: classify_silt(x) if pd.notna(x) else np.nan)
+ if "黏粒" in df.columns:
+ df["黏粒分级"] = df["黏粒"].apply(lambda x: classify_clay(x) if pd.notna(x) else np.nan)
+ if "砂粒" in df.columns:
+ df["砂粒分级"] = df["砂粒"].apply(lambda x: classify_sand(x) if pd.notna(x) else np.nan)
+ if "有效土层厚度" in df.columns:
+ df["有效土层厚度分级"] = df["有效土层厚度"].apply(lambda x: classify_yxtchd(x) if pd.notna(x) else np.nan)
+ if "土壤质地" in df.columns:
+ df["土壤质地分级"] = df["土壤质地"].apply(lambda x: classify_trzd(x) if pd.notna(x) else np.nan)
+
+ return df
+
+def main():
+ """
+ 主函数:程序入口
+ 用户需根据实际情况修改以下参数
+ """
+ logger = init_logger()
+ logger.info("="*50)
+ logger.info("土壤属性栅格面积加权统计程序启动")
+ logger.info("="*50)
+
+ # --------------------------
+ # 用户配置区域(必须修改!)
+ # --------------------------
+ # 1. 矢量图斑文件路径(支持Shapefile、GeoPackage等格式)
+ # TODO
+ VECTOR_PATH = r"D:\工作\三普成果编制\出图数据\北海\三普栅格\DL_ALL.shp" # 示例:"D:/data/土壤图斑.shp"
+
+ # 2. 土壤属性栅格文件配置(键:属性名称,值:栅格文件路径)
+ # 注意:属性名称必须与最终表格列名一致
+ # TODO
+ RASTER_FILES = {
+ "耕层厚度": r"D:\工作\三普成果编制\出图数据\北海\三普栅格\GZCHD.tif", # 示例:"D:/data/耕层厚度.tif"
+ "土壤容重": r"D:\工作\三普成果编制\出图数据\北海\三普栅格\TRRZ.tif", # 示例:"D:/data/土壤容重.tif"
+ "砂粒": r"D:\工作\三普成果编制\出图数据\北海\三普栅格\SL.tif", # 示例:"D:/data/砂粒含量.tif"
+ "粉粒": r"D:\工作\三普成果编制\出图数据\北海\三普栅格\FL.tif", # 示例:"D:/data/粉粒含量.tif"
+ "黏粒": r"D:\工作\三普成果编制\出图数据\北海\三普栅格\NL.tif", # 示例:"D:/data/黏粒含量.tif"
+ "酸碱度": r"D:\工作\三普成果编制\出图数据\北海\三普栅格\PH.tif", # 示例:"D:/data/pH值.tif"
+ "阳离子": r"D:\工作\三普成果编制\出图数据\北海\三普栅格\CEC.tif", # 示例:"D:/data/阳离子交换量.tif"
+ "有机质": r"D:\工作\三普成果编制\出图数据\北海\三普栅格\OM.tif", # 示例:"D:/data/有机质含量.tif"
+ "全氮": r"D:\工作\三普成果编制\出图数据\北海\三普栅格\TN.tif", # 示例:"D:/data/全氮含量.tif"
+ "全磷": r"D:\工作\三普成果编制\出图数据\北海\三普栅格\TP.tif", # 示例:"D:/data/全磷含量.tif"
+ "全钾": r"D:\工作\三普成果编制\出图数据\北海\三普栅格\TK.tif", # 示例:"D:/data/全钾含量.tif"
+ "有效磷": r"D:\工作\三普成果编制\出图数据\北海\三普栅格\AP.tif", # 示例:"D:/data/有效磷含量.tif"
+ "速效钾": r"D:\工作\三普成果编制\出图数据\北海\三普栅格\AK.tif", # 示例:"D:/data/速效钾含量.tif"
+ # "有效铁": r"D:\工作\三普成果编制\出图数据\北海\三普栅格\AFE.tif", # 示例:"D:/data/有效铁含量.tif"
+ "有效锌": r"D:\工作\三普成果编制\出图数据\北海\三普栅格\AZN.tif", # 示例:"D:/data/有效锌含量.tif"
+ "有效锰": r"D:\工作\三普成果编制\出图数据\北海\三普栅格\AMN.tif", # 示例:"D:/data/有效锰含量.tif"
+ "有效铜": r"D:\工作\三普成果编制\出图数据\北海\三普栅格\ACU.tif", # 示例:"D:/data/有效铜含量.tif"
+ "有效硼": r"D:\工作\三普成果编制\出图数据\北海\三普栅格\AB.tif", # 示例:"D:/data/有效硼含量.tif"
+ "有效钼": r"D:\工作\三普成果编制\出图数据\北海\三普栅格\AMO.tif", # 示例:"D:/data/有效钼含量.tif"
+ "有效硫": r"D:\工作\三普成果编制\出图数据\北海\三普栅格\AS1.tif", # 示例:"D:/data/有效硫含量.tif"
+ "交换性钙": r"D:\工作\三普成果编制\出图数据\北海\三普栅格\ECA.tif", # 示例:"D:/data/交换性钙含量.tif"
+ "交换性镁": r"D:\工作\三普成果编制\出图数据\北海\三普栅格\EMG.tif", # 示例:"D:/data/交换性镁含量.tif"
+ "全硒": r"D:\工作\三普成果编制\出图数据\北海\三普栅格\TSE.tif", # 示例:"D:/data/全硒含量.tif"
+ "有效土层厚度": r"D:\工作\三普成果编制\出图数据\北海\三普栅格\YXTCHD.tif", # 示例:"D:/data/有效土层厚度.tif"
+ "土壤质地": r"D:\工作\三普成果编制\出图数据\北海\三普栅格\TRZD.tif", # 示例:"D:/data/土壤质地.tif"
+ }
+
+ # 3. 结果输出路径(Excel文件)
+ OUTPUT_PATH = "土壤属性图斑数据_面积加权结果.xlsx" # 示例:"D:/result/结果.xlsx"
+
+ # --------------------------
+ # 程序执行流程(无需修改)
+ # --------------------------
+ try:
+ # 1. 数据验证
+ if not validate_data(VECTOR_PATH, RASTER_FILES):
+ logger.error("数据验证失败,程序终止")
+ return
+
+ # 2. 执行面积加权统计
+ result_df = calculate_area_weighted_stats(VECTOR_PATH, RASTER_FILES, OUTPUT_PATH)
+
+ # 3. 显示结果预览
+ logger.info("\\n结果预览(前3行):")
+ print(result_df.head(3).to_string(index=False))
+
+ logger.info("\\n" + "="*50)
+ logger.info("程序执行完成!")
+ logger.info(f"结果文件:{OUTPUT_PATH}")
+ logger.info("="*50)
+
+ except Exception as e:
+ logger.error(f"程序执行出错:{str(e)}", exc_info=True)
+ logger.error("程序异常终止")
+
+if __name__ == "__main__":
+ # 启动主程序
+ main()
diff --git a/tests/test1.py b/tests/test1.py
index 2b2ece4..397b4e1 100644
--- a/tests/test1.py
+++ b/tests/test1.py
@@ -7,14 +7,21 @@ from pathlib import Path
# 添加项目根目录到路径
sys.path.insert(0, str(Path(__file__).parent.parent))
-import geo_tools
+import app
-gdb_path = r"E:\@三普\@临时文件夹\临时数据库.gdb"
+gdb_path = r"E:\@三普\@临时文件夹\临时数据库.gdb\马关综合后图斑"
+shp_path = r"E:\@三普\@临时文件夹\新建文件夹\靖西二普样点\AK.shp"
+shp = r"E:\@三普\@临时文件夹\新建文件夹\靖西二普样点"
+
+gdb = r"E:\@三普\@临时文件夹\新建文件地理数据库.gdb"
# 列出图层
-# layers = geo_tools.list_gdb_layers(gdb_path)
+# layers = geo_tools.readers.list_gdb_layers(gdb_path)
# print(layers)
# 读取图层
-gdf = geo_tools.read_gdb(gdb_path, layer="马关综合后图斑")
-print(gdf.crs)
\ No newline at end of file
+gdf = app.readers.read_vector(gdb_path)
+print(gdf.crs)
+# 获取几何类型
+print(gdf.head())
+
diff --git a/tests/test_analysis.py b/tests/test_analysis.py
index a86a0b5..269d8cf 100644
--- a/tests/test_analysis.py
+++ b/tests/test_analysis.py
@@ -4,8 +4,8 @@ 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
+from app.analysis.spatial_ops import overlay, select_by_location
+from app.analysis.stats import area_weighted_mean, count_by_polygon, summarize_attributes
class TestOverlay:
diff --git a/tests/test_geometry.py b/tests/test_geometry.py
index a1cd50d..a242d8a 100644
--- a/tests/test_geometry.py
+++ b/tests/test_geometry.py
@@ -3,8 +3,8 @@
import pytest
from shapely.geometry import LineString, Point, Polygon
-import geo_tools
-from geo_tools.core.geometry import (
+import app
+from app.core.geometry import (
buffer_geometry,
bounding_box,
centroid,
diff --git a/tests/test_io.py b/tests/test_io.py
index a628874..859b98b 100644
--- a/tests/test_io.py
+++ b/tests/test_io.py
@@ -4,8 +4,8 @@ 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
+from app.io.readers import read_vector, read_gpkg, list_gpkg_layers, read_csv_points
+from app.io.writers import write_vector, write_gpkg, write_csv
class TestReadVector:
@@ -61,7 +61,7 @@ class TestWriteReadRoundtrip:
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 app.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)
diff --git a/tests/test_proj.py b/tests/test_proj.py
index 5d95a8d..0108e62 100644
--- a/tests/test_proj.py
+++ b/tests/test_proj.py
@@ -3,19 +3,31 @@ import os
os.environ["OGR_ORGANIZE_POLYGONS"] = "SKIP"
from pathlib import Path
+import geopandas as gpd
# 添加项目根目录到路径
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
+import app
+from app.core import projection
+from app.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))
+input_folder = r"E:\@三普\@二普和测土配方样点处理\云南省"
+output_folder = r"E:\@三普\@二普和测土配方样点处理\云南省_投影转换"
-# 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")
\ No newline at end of file
+for root, dirs, files in os.walk(input_folder):
+ for file in files:
+ if file.endswith(".shp"):
+ input_path = os.path.join(root, file)
+ output_path = os.path.join(output_folder, os.path.relpath(input_path, input_folder))
+ os.makedirs(os.path.dirname(output_path), exist_ok=True)
+
+ gdf = gpd.read_file(input_path)
+ if gdf.crs is not None:
+ reproject_gdf = projection.reproject_gdf(gdf, auto_crs=True)
+ reproject_gdf.to_file(output_path)
+ print("Reprojected CRS:", reproject_gdf.crs)
+ else:
+ print("No CRS information found.")
\ No newline at end of file
diff --git a/tests/test_vector.py b/tests/test_vector.py
index 7ec21d5..f55d39d 100644
--- a/tests/test_vector.py
+++ b/tests/test_vector.py
@@ -4,7 +4,7 @@ import pytest
import geopandas as gpd
from shapely.geometry import Point
-from geo_tools.core.vector import (
+from app.core.vector import (
add_area_column,
clip_to_extent,
dissolve_by,
diff --git a/项目分析报告.md b/项目分析报告.md
new file mode 100644
index 0000000..bb2a13f
--- /dev/null
+++ b/项目分析报告.md
@@ -0,0 +1,260 @@
+# Geo-Tools 项目分析报告
+
+## 1. 项目概览
+
+Geo-Tools 是一个专业的地理信息数据处理工具库,基于 geopandas、shapely、fiona 等开源地理空间库,提供了统一、简洁的 API 接口,简化地理空间数据处理流程。
+
+- **核心价值**:为地理空间数据处理提供标准化、高效的工具集
+- **应用场景**:科学研究、GIS 分析、土地利用规划、农业分析等
+- **技术特点**:统一数据读写接口、丰富的空间分析功能、坐标系转换支持
+
+## 2. 目录结构
+
+项目采用模块化设计,清晰分离核心功能、IO 操作、分析工具和辅助功能。主要目录结构如下:
+
+```
+geo_tools/
+├── app/ # 主应用目录
+│ ├── analysis/ # 空间分析模块
+│ │ ├── spatial_ops.py # 空间叠加与邻域分析
+│ │ └── stats.py # 空间统计工具
+│ ├── config/ # 配置模块
+│ ├── core/ # 核心地理处理层
+│ │ ├── geometry.py # 几何运算工具
+│ │ ├── projection.py # 坐标系转换
+│ │ ├── raster.py # 栅格数据处理
+│ │ └── vector.py # 矢量数据处理
+│ ├── io/ # 数据读写模块
+│ │ ├── readers.py # 数据读取
+│ │ └── writers.py # 数据写入
+│ └── utils/ # 工具模块
+│ ├── config.py # 配置工具
+│ ├── logger.py # 日志工具
+│ └── validators.py # 数据验证
+├── data/ # 数据目录
+│ └── sample/ # 示例数据
+├── scripts/ # 脚本目录
+│ └── 其他工具/ # 特定领域工具脚本
+├── tests/ # 测试目录
+└── pyproject.toml # 项目配置文件
+```
+
+## 3. 核心功能模块
+
+### 3.1 数据读写(IO)
+
+**功能描述**:提供统一的矢量数据读取接口,支持多种格式的数据读写。
+
+**支持格式**:
+- Shapefile (.shp)
+- GeoJSON (.geojson / .json)
+- GeoPackage (.gpkg)
+- File Geodatabase (.gdb)
+- KML / KMZ
+- FlatGeobuf (.fgb)
+- CSV(含 WKT 或 经纬度列)
+
+**核心方法**:
+- `read_vector()`:统一的矢量数据读取入口
+- `read_gdb()`:读取 Esri File Geodatabase
+- `read_gpkg()`:读取 GeoPackage 文件
+- `read_csv_points()`:从 CSV 读取点数据
+- `list_gdb_layers()`:列出 GDB 中所有图层
+
+### 3.2 几何处理(Core)
+
+**功能描述**:基于 Shapely 提供几何对象的创建、操作和分析功能。
+
+**核心功能**:
+- **几何有效性**:验证和修复几何对象
+- **基础几何运算**:缓冲区、质心、边界框、凸包
+- **集合运算**:交集、并集、差集、对称差集
+- **空间关系判断**:包含、在内部、相交、距离计算
+
+**核心方法**:
+- `is_valid_geometry()`:判断几何对象是否有效
+- `fix_geometry()`:尝试修复无效几何
+- `buffer_geometry()`:对几何对象执行缓冲区运算
+- `intersect()/union()/difference()`:几何集合运算
+- `contains()/within()/intersects()`:空间关系判断
+
+### 3.3 坐标系转换(Core)
+
+**功能描述**:基于 pyproj 提供坐标系信息查询和投影转换功能。
+
+**核心功能**:
+- CRS 信息查询
+- 坐标点批量转换
+- 矢量数据重投影
+- 自动推荐适合的投影 CRS
+
+**核心方法**:
+- `get_crs_info()`:获取 CRS 的基本信息
+- `transform_coordinates()`:批量转换坐标点
+- `reproject_gdf()`:将 GeoDataFrame 重投影到目标坐标系
+- `suggest_projected_crs()`:根据经纬度自动推荐投影 CRS
+
+### 3.4 空间分析(Analysis)
+
+**功能描述**:提供空间叠加、邻域分析和空间统计功能。
+
+**核心功能**:
+- **空间叠加**:缓冲区叠加、图层叠加
+- **邻域分析**:最近邻分析
+- **位置选择**:按空间关系选择要素
+- **空间统计**:面积加权均值、属性汇总、点计数
+
+**核心方法**:
+- `buffer_and_overlay()`:缓冲区后执行叠置分析
+- `nearest_features()`:找到最近的要素
+- `select_by_location()`:按位置关系选择要素
+- `area_weighted_mean()`:计算面积加权均值
+- `summarize_attributes()`:属性统计汇总
+- `count_by_polygon()`:统计面要素内的点数量
+
+## 4. 技术架构与依赖
+
+### 4.1 核心依赖
+
+| 依赖库 | 版本要求 | 用途 |
+|-------|---------|------|
+| geopandas | >=0.14 | 地理空间数据处理核心库 |
+| shapely | >=2.0 | 几何对象操作 |
+| fiona | >=1.9 | 矢量数据读写 |
+| pyproj | >=3.6 | 坐标系转换 |
+| pandas | >=2.0 | 数据处理 |
+| numpy | >=1.24 | 数值计算 |
+| pydantic | >=2.0 | 数据验证 |
+| pydantic-settings | >=2.0 | 配置管理 |
+| python-dotenv | >=1.0 | 环境变量管理 |
+
+### 4.2 可选依赖
+
+| 依赖组 | 包含库 | 用途 |
+|-------|-------|------|
+| dev | pytest, black, isort, flake8, mypy | 测试和代码质量工具 |
+| notebook | jupyter, matplotlib, contextily, folium | 交互式分析和可视化 |
+| raster | rasterio, xarray, rio-cogeo | 栅格数据处理 |
+
+### 4.3 技术架构
+
+项目采用分层架构设计:
+
+1. **底层依赖层**:基于 geopandas、shapely、fiona 等核心库
+2. **核心处理层**:提供几何运算、坐标系转换等基础功能
+3. **IO 层**:统一数据读写接口,支持多种格式
+4. **分析层**:提供空间分析和统计功能
+5. **工具层**:提供配置、日志、验证等辅助功能
+6. **应用层**:通过脚本和 API 接口供用户使用
+
+## 5. 业务定位与应用场景
+
+### 5.1 业务定位
+
+Geo-Tools 定位为专业的地理信息数据处理工具库,主要服务于以下场景:
+
+- **科学研究**:为地理空间相关研究提供数据处理和分析工具
+- **GIS 专业人员**:简化日常工作流程,提高处理效率
+- **土地利用规划**:支持空间分析和统计功能
+- **农业分析**:如耕作层厚度栅格制作、面积加权均值计算等
+
+### 5.2 应用场景
+
+从项目中的脚本和功能来看,主要应用场景包括:
+
+1. **数据格式转换**:在不同地理数据格式之间转换
+2. **空间分析**:缓冲区分析、叠加分析、邻域分析
+3. **空间统计**:面积加权均值、属性汇总、点计数
+4. **坐标系管理**:不同坐标系之间的转换和管理
+5. **农业分析**:如耕作层厚度分析、区域面积统计
+
+### 5.3 典型工作流
+
+1. **数据读取**:从各种格式读取地理空间数据
+2. **数据校验**:验证几何有效性和坐标系信息
+3. **数据处理**:几何运算、坐标系转换
+4. **空间分析**:执行叠加分析、邻域分析等
+5. **统计计算**:计算面积加权均值、属性汇总等
+6. **结果输出**:将处理结果写入各种格式
+
+## 6. 核心 API/类/函数
+
+### 6.1 数据读写 API
+
+| 函数名 | 功能描述 | 参数说明 | 返回值 |
+|-------|---------|---------|-------|
+| `read_vector()` | 统一的矢量数据读取入口 | path: 数据路径
layer: 图层名
crs: 目标坐标系 | GeoDataFrame |
+| `read_gdb()` | 读取 Esri File Geodatabase | gdb_path: GDB 路径
layer: 图层名 | GeoDataFrame |
+| `list_gdb_layers()` | 列出 GDB 中所有图层 | gdb_path: GDB 路径 | list[str] |
+| `write_vector()` | 统一的矢量数据写入 | gdf: GeoDataFrame
path: 输出路径 | None |
+
+### 6.2 几何处理 API
+
+| 函数名 | 功能描述 | 参数说明 | 返回值 |
+|-------|---------|---------|-------|
+| `is_valid_geometry()` | 判断几何对象是否有效 | geom: 几何对象 | bool |
+| `fix_geometry()` | 尝试修复无效几何 | geom: 几何对象 | BaseGeometry |
+| `buffer_geometry()` | 对几何对象执行缓冲区运算 | geom: 几何对象
distance: 缓冲距离 | BaseGeometry |
+| `intersect()/union()/difference()` | 几何集合运算 | geom_a, geom_b: 几何对象 | BaseGeometry |
+
+### 6.3 坐标系转换 API
+
+| 函数名 | 功能描述 | 参数说明 | 返回值 |
+|-------|---------|---------|-------|
+| `get_crs_info()` | 获取 CRS 的基本信息 | crs_input: CRS 输入 | dict |
+| `transform_coordinates()` | 批量转换坐标点 | xs, ys: 坐标序列
source_crs: 源 CRS
target_crs: 目标 CRS | (list[float], list[float]) |
+| `reproject_gdf()` | 将 GeoDataFrame 重投影 | gdf: GeoDataFrame
target_crs: 目标 CRS | GeoDataFrame |
+| `suggest_projected_crs()` | 自动推荐投影 CRS | lon, lat: 经纬度 | str |
+
+### 6.4 空间分析 API
+
+| 函数名 | 功能描述 | 参数说明 | 返回值 |
+|-------|---------|---------|-------|
+| `buffer_and_overlay()` | 缓冲区后执行叠置分析 | source: 源图层
distance: 缓冲距离
target: 目标图层 | GeoDataFrame |
+| `nearest_features()` | 找到最近的要素 | source: 查询图层
target: 被查询图层
k: 最近邻数量 | GeoDataFrame |
+| `select_by_location()` | 按位置关系选择要素 | source: 源图层
selector: 选择图层
predicate: 空间谓词 | GeoDataFrame |
+| `area_weighted_mean()` | 计算面积加权均值 | gdf: GeoDataFrame
value_col: 值列名
group_col: 分组列名 | pd.Series 或 pd.DataFrame |
+
+## 7. 技术亮点与优势
+
+1. **统一的数据读写接口**:支持多种格式,简化数据输入输出流程
+2. **丰富的几何运算功能**:基于 Shapely 提供全面的几何操作
+3. **智能的坐标系管理**:自动推荐适合的投影 CRS,简化坐标系转换
+4. **强大的空间分析能力**:支持缓冲区分析、叠加分析、邻域分析等
+5. **灵活的空间统计功能**:面积加权均值、属性汇总、点计数等
+6. **良好的错误处理**:提供友好的错误信息,便于调试
+7. **模块化设计**:清晰的代码结构,便于维护和扩展
+8. **完善的文档**:详细的函数文档和示例代码
+
+## 8. 潜在应用领域
+
+1. **土地利用规划**:分析土地利用类型、面积统计
+2. **农业分析**:耕作层厚度分析、农田面积计算
+3. **环境监测**:生态区域分析、污染源影响范围评估
+4. **城市规划**:基础设施布局、服务范围分析
+5. **交通规划**:路线分析、站点服务范围
+6. **灾害评估**:灾害影响范围分析、风险评估
+7. **市场分析**:商业网点覆盖范围、人口分布分析
+
+## 9. 项目发展建议
+
+1. **增强栅格数据处理能力**:目前栅格处理是可选依赖,可考虑增强其功能
+2. **添加更多空间分析算法**:如空间插值、网络分析等
+3. **开发可视化工具**:集成更多可视化功能,便于结果展示
+4. **提供更多领域特定工具**:针对不同领域开发专用工具
+5. **完善测试覆盖**:增加更多测试用例,提高代码质量
+6. **优化性能**:对大型数据集的处理性能进行优化
+7. **提供更多示例**:增加不同领域的应用示例
+
+## 10. 总结
+
+Geo-Tools 是一个功能强大、设计合理的地理信息数据处理工具库,基于成熟的开源地理空间库,提供了统一、简洁的 API 接口,简化了地理空间数据的处理流程。
+
+该项目的核心价值在于:
+- 提供了标准化的地理空间数据处理工具
+- 支持多种数据格式的读写
+- 提供丰富的空间分析和统计功能
+- 简化了坐标系管理和转换
+- 为不同领域的地理空间分析提供了基础工具
+
+Geo-Tools 不仅是一个实用的工具库,也是学习地理空间数据处理的良好资源,通过模块化的设计和清晰的文档,为用户提供了一个易于使用和扩展的地理信息处理框架。
\ No newline at end of file