Files
geo_tools/app/utils/validators.py
missum db51d41aef refactor: 重构项目结构,将geo_tools重命名为app并更新相关引用
- 将主包名从geo_tools改为app
- 更新所有模块中的引用路径
- 迁移并更新测试用例
- 添加项目规则文档
- 保持原有功能不变,仅进行结构调整
2026-04-12 19:49:56 +08:00

184 lines
5.2 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
geo_tools.utils.validators
~~~~~~~~~~~~~~~~~~~~~~~~~~
数据验证工具CRS 合法性、几何有效性、文件格式等。
"""
from __future__ import annotations
from pathlib import Path
from typing import TYPE_CHECKING
if TYPE_CHECKING:
import geopandas as gpd
from shapely.geometry.base import BaseGeometry
# ── CRS 校验 ──────────────────────────────────────────────────────────────────
def is_valid_crs(crs_input: str | int) -> bool:
"""检查 CRS 是否可以被 pyproj 正常解析。
Parameters
----------
crs_input:
EPSG 代码(整数或 ``"EPSG:4326"`` 字符串)或 proj 字符串。
Returns
-------
bool
"""
try:
from pyproj import CRS
CRS.from_user_input(crs_input)
return True
except Exception:
return False
def validate_crs(crs_input: str | int) -> str:
"""校验并标准化 CRS返回 EPSG 代码字符串。
Raises
------
ValueError
如果 CRS 无法被 pyproj 解析。
"""
from pyproj import CRS
try:
crs_obj = CRS.from_user_input(crs_input)
# 尝试返回简洁的 EPSG 字符串
epsg = crs_obj.to_epsg()
if epsg:
return f"EPSG:{epsg}"
return crs_obj.to_string()
except Exception as exc:
raise ValueError(f"无效的 CRS{crs_input!r}。原因:{exc}") from exc
# ── 几何校验 ──────────────────────────────────────────────────────────────────
def validate_geometry(gdf: "gpd.GeoDataFrame", *, raise_on_invalid: bool = False) -> dict[str, int]:
"""检查 GeoDataFrame 中几何对象的有效性。
Parameters
----------
gdf:
待检查的 GeoDataFrame。
raise_on_invalid:
若为 True当存在无效几何时抛出 ``ValueError``。
Returns
-------
dict
包含 ``total``、``valid``、``invalid``、``null`` 计数。
"""
null_count = gdf.geometry.isna().sum()
non_null = gdf.geometry.dropna()
invalid_mask = ~non_null.is_valid # type: ignore
invalid_count = int(invalid_mask.sum())
valid_count = len(non_null) - invalid_count
result = {
"total": len(gdf),
"valid": valid_count,
"invalid": invalid_count,
"null": int(null_count),
}
if raise_on_invalid and (invalid_count > 0 or null_count > 0):
raise ValueError(
f"GeoDataFrame 存在 {invalid_count} 个无效几何、{null_count} 个空几何。"
)
return result
# ── 文件格式校验 ───────────────────────────────────────────────────────────────
#: 支持读取的矢量文件扩展名fiona 驱动映射)
SUPPORTED_VECTOR_EXTENSIONS: dict[str, str] = {
".shp": "ESRI Shapefile",
".geojson": "GeoJSON",
".json": "GeoJSON",
".gpkg": "GPKG",
".gdb": "OpenFileGDB",
".kml": "KML",
".kmz": "KML",
".csv": "CSV",
".gml": "GML",
".dxf": "DXF",
".fgb": "FlatGeobuf",
}
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
if path.is_dir() and suffix == ".gdb":
return True
return suffix in SUPPORTED_VECTOR_EXTENSIONS
def validate_vector_path(path: str | Path) -> Path:
"""校验矢量数据路径,返回 Path 对象。
Raises
------
FileNotFoundError
文件或目录不存在。
ValueError
文件格式不受支持。
"""
path = Path(path)
# GDB 是目录
if not path.exists():
raise FileNotFoundError(f"路径不存在:{path}")
if not is_supported_vector_format(path):
raise ValueError(
f"不支持的矢量格式:{path.suffix!r}"
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