refactor: 重构项目结构,将geo_tools重命名为app并更新相关引用

- 将主包名从geo_tools改为app
- 更新所有模块中的引用路径
- 迁移并更新测试用例
- 添加项目规则文档
- 保持原有功能不变,仅进行结构调整
This commit is contained in:
2026-04-12 19:49:56 +08:00
parent fcb8e1f255
commit db51d41aef
41 changed files with 4132 additions and 808 deletions

469
app/core/geometry.py Normal file
View File

@@ -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_validShapely >= 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')