进行路径计算

This commit is contained in:
wzy-warehouse
2026-06-29 11:37:04 +08:00
parent 1f01ceb062
commit e9169dfd13
6 changed files with 745 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
# hazard 配置模块
+93
View File
@@ -0,0 +1,93 @@
# 灾害影响范围计算参数配置
# 2026-06-29 初始版本
#
# 五种灾害类型各有独立参数,未匹配到灾害类型时使用 default
# 所有半径单位为米(m)
#
# 论文支撑:
# - 滑坡/崩塌: He et al. (2023) Landslides, Corominas (1996) CGJ
# - 泥石流: Baggio et al. (2021) NHESS, Cicoira et al. (2022) ESurf
# - 山洪: Costache et al. (2022) STOTEN, Zhao et al. (2023) J.Hydrology
# - 内涝: Wang et al. (2022) Water Res. Mgmt, Jamali et al. (2020) Water
# ============================================
# 滑坡 — 经验到达角法 + 地形修正
# ============================================
landslide:
height_drop_m: 50 # 假设崩滑高差(m)
reach_angle: # 到达角经验值(度) — He et al.(2023) 表2
小型: 31
中型: 28
大型: 25
特大型: 23
fan_angle: 45 # 侧向扇形展开角(度)
river_erosion_enhance: 1.3 # 河流距离<200m时的增强系数
river_distance_threshold: 200 # 河流侵蚀增强触发距离(m)
fault_enhance: 1.2 # 断裂带距离<500m时的增强系数
fault_distance_threshold: 500 # 断裂带增强触发距离(m)
min_radius_m: 100 # 最小影响半径
max_radius_m: 2000 # 最大影响半径
# ============================================
# 泥石流 — 河流关联缓冲区
# ============================================
debris_flow:
base_radius_m: 200 # 基础影响半径
slope_factor: 10 # 坡度影响系数 (半径 = base + slope * factor)
river_buffer_m: 80 # 河道缓冲区宽度
max_river_search_m: 2000 # 最近河流搜索范围
min_radius_m: 100
max_radius_m: 2000
# ============================================
# 山洪 — 多级河流缓冲区
# ============================================
flash_flood:
river_dist_thresholds: [100, 300, 500] # 距河流距离阈值(m)
buffer_by_level: # 对应各级缓冲区(m)
1: 500 # 干流
2: 300 # 一级支流
3: 150 # 二级支流
4: 80 # 三级以下
5: 80
max_river_search_m: 3000 # 最近河流搜索范围
# ============================================
# 内涝 — TWI简化 + 不透水率 + 管网修正
# ============================================
waterlogging:
base_radius_m: 100 # 基础积水半径
impervious_factor: 3 # 不透水率修正系数
pipe_density_factor: 400 # 管网密度修正系数
pipe_lower_bound: 0.3 # 管网修正下限
min_radius_m: 50
max_radius_m: 800
# ============================================
# 崩塌 — 锥体传播模型 (CONEFALL简化)
# ============================================
rockfall:
height_drop_m: 50 # 假设崩落高差(m)
reach_angle: # 到达角经验值(度) — Guerin et al.(2022)
小型: 33
中型: 30
大型: 27
特大型: 25
fault_angle_penalty: 5 # 断裂带<300m时到达角减少量(度)
fault_penalty_threshold: 300 # 断裂带惩罚触发距离(m)
fault_radius_enhance: 1.5 # 断裂带<200m时半径增强系数
fault_enhance_threshold: 200 # 半径增强触发距离(m)
min_radius_m: 50
max_radius_m: 1000
# ============================================
# 默认参数 — 未匹配灾害类型时的兜底
# ============================================
default:
radius_m: 200
+3
View File
@@ -0,0 +1,3 @@
from app.models.hazard.hazard_zone_calculator import HazardZoneCalculator, hazard_zone_calculator
__all__ = ['HazardZoneCalculator', 'hazard_zone_calculator']
+413
View File
@@ -0,0 +1,413 @@
"""
灾害影响范围计算器
根据灾害类型和静态因子计算隐患点的影响范围(zone)和影响路径(path)
论文支撑:
- 滑坡: He et al. (2023) Landslides — 经验到达角法
- 泥石流: Baggio et al. (2021) NHESS — 河流关联缓冲区
- 山洪: Costache et al. (2022) STOTEN — 多级缓冲区
- 内涝: Wang et al. (2022) Water Res. Mgmt — TWI简化法
- 崩塌: Guerin et al. (2022) Eng. Geology — 锥体传播模型
"""
import math
import os
from typing import Optional, Tuple, Dict, Any
import yaml
from app.config.paths import CONFIG_DIR
# 配置文件路径
_PARAMS_PATH = os.path.join(CONFIG_DIR, 'hazard', 'hazard_zone_params.yaml')
# 地球半径 (WGS84)
_EARTH_RADIUS_M = 6371000.0
def _load_params() -> Dict[str, Any]:
"""加载灾害影响范围计算参数"""
with open(_PARAMS_PATH, 'r', encoding='utf-8') as f:
return yaml.safe_load(f)
def _to_wkt_polygon(coords: list) -> str:
"""
将坐标列表转换为 WKT POLYGON 字符串
Args:
coords: [(lon1, lat1), (lon2, lat2), ...] 首尾需闭合
"""
points = ', '.join([f'{c[0]:.8f} {c[1]:.8f}' for c in coords])
return f'POLYGON(({points}))'
def _to_wkt_linestring(coords: list) -> str:
"""将坐标列表转换为 WKT LINESTRING 字符串"""
points = ', '.join([f'{c[0]:.8f} {c[1]:.8f}' for c in coords])
return f'LINESTRING({points})'
def _offset_to_lonlat(lon: float, lat: float, dx_m: float, dy_m: float) -> Tuple[float, float]:
"""
将米级偏移转换回经纬度
Args:
lon, lat: 中心点经纬度
dx_m: 东向偏移(m)
dy_m: 北向偏移(m)
Returns:
(new_lon, new_lat)
"""
lat_rad = math.radians(lat)
dlon = math.degrees(dx_m / (_EARTH_RADIUS_M * math.cos(lat_rad)))
dlat = math.degrees(dy_m / _EARTH_RADIUS_M)
return lon + dlon, lat + dlat
def _make_fan_wkt(lon: float, lat: float, direction_deg: float,
spread_deg: float, radius_m: float,
segments: int = 24) -> str:
"""
创建扇形/锥形影响范围 WKT
Args:
lon, lat: 隐患点坐标
direction_deg: 扇形主方向(坡向, 0=北, 90=东)
spread_deg: 扇形总开角(度)
radius_m: 半径(m)
segments: 弧线段数
Returns:
POLYGON WKT 字符串
"""
half_spread = spread_deg / 2.0
coords = [(lon, lat)] # 中心点
for i in range(segments + 1):
angle_deg = direction_deg - half_spread + (spread_deg * i / segments)
angle_rad = math.radians(angle_deg)
dx = radius_m * math.sin(angle_rad)
dy = radius_m * math.cos(angle_rad)
coords.append(_offset_to_lonlat(lon, lat, dx, dy))
coords.append((lon, lat)) # 闭合回中心
return _to_wkt_polygon(coords)
def _make_circle_wkt(lon: float, lat: float, radius_m: float,
segments: int = 48) -> str:
"""
创建圆形缓冲区 WKT(用正多边形近似圆)
Args:
lon, lat: 中心点
radius_m: 半径(m)
segments: 边数
"""
coords = []
for i in range(segments):
angle_rad = 2 * math.pi * i / segments
dx = radius_m * math.sin(angle_rad)
dy = radius_m * math.cos(angle_rad)
coords.append(_offset_to_lonlat(lon, lat, dx, dy))
coords.append(coords[0]) # 闭合
return _to_wkt_polygon(coords)
class HazardZoneCalculator:
"""
灾害影响范围计算器
根据隐患点的灾害类型和静态因子,计算影响范围(WKT多边形)和影响路径(WKT线)。
每种灾害类型使用不同的经验公式,参数从 hazard_zone_params.yaml 读取。
"""
def __init__(self):
self.params = _load_params()
# ==================== 公开接口 ====================
def calculate(self, source_id: int, disaster_type: str,
scale_grade: Optional[str],
lon: float, lat: float,
static_factors: Dict[str, Any]) -> Tuple[Optional[str], Optional[str]]:
"""
计算单个隐患点的影响范围
Args:
source_id: 隐患点ID
disaster_type: 灾害类型 (滑坡/泥石流/山洪/内涝/崩塌)
scale_grade: 规模等级 (小型/中型/大型)
lon: 经度
lat: 纬度
static_factors: 静态因子字典,从 xian_risk_factors.static_factors 获取
Returns:
(zone_wkt, path_wkt) — zone为POLYGON WKT, path为LINESTRING WKT或None
"""
# 选择对应灾害类型的计算方法
method_map = {
'滑坡': self._calc_landslide,
'泥石流': self._calc_debris_flow,
'山洪': self._calc_flash_flood,
'内涝': self._calc_waterlogging,
'崩塌': self._calc_rockfall,
}
method = method_map.get(disaster_type, self._calc_default)
return method(source_id, disaster_type, scale_grade, static_factors, lon, lat)
# ==================== 滑坡 ====================
def _calc_landslide(self, source_id: int, disaster_type: str,
scale_grade: Optional[str],
factors: Dict[str, Any],
lon: float, lat: float) -> Tuple[Optional[str], Optional[str]]:
"""滑坡: 经验到达角法 + 扇形展开"""
p = self.params['landslide']
dem = float(factors.get('dem_value', 0))
aspect = float(factors.get('aspect_value', 0))
river_dist = float(factors.get('river_distance', 9999))
fault_dist = float(factors.get('fault_distance', 9999))
# 到达角
angle_key = scale_grade if scale_grade in p['reach_angle'] else '中型'
reach_angle = p['reach_angle'][angle_key]
# 半径: H / tan(到达角)
H = p['height_drop_m']
radius = H / math.tan(math.radians(reach_angle))
# 修正
if river_dist < p['river_distance_threshold']:
radius *= p['river_erosion_enhance']
if fault_dist < p['fault_distance_threshold']:
radius *= p['fault_enhance']
radius = max(p['min_radius_m'], min(radius, p['max_radius_m']))
# 扇形 — 沿坡向展开
if aspect and aspect > 0:
zone_wkt = _make_fan_wkt(lon, lat, aspect, p['fan_angle'], radius)
# 滑坡路径: 从隐患点沿坡向延伸到堆积区边缘
end_lon, end_lat = _offset_to_lonlat(
lon, lat,
radius * math.sin(math.radians(aspect)),
radius * math.cos(math.radians(aspect))
)
path_wkt = _to_wkt_linestring([(lon, lat), (end_lon, end_lat)])
else:
zone_wkt = _make_circle_wkt(lon, lat, radius)
path_wkt = None
return zone_wkt, path_wkt
# ==================== 泥石流 ====================
def _calc_debris_flow(self, source_id: int, disaster_type: str,
scale_grade: Optional[str],
factors: Dict[str, Any],
lon: float, lat: float) -> Tuple[Optional[str], Optional[str]]:
"""
泥石流: 沿最近河流方向建立影响区
path_geom 为隐患点到最近河流的连线,代表潜在流动路径
"""
p = self.params['debris_flow']
slope = float(factors.get('slope_value', 0))
radius = p['base_radius_m'] + slope * p['slope_factor']
radius = max(p['min_radius_m'], min(radius, p['max_radius_m']))
zone_wkt = _make_circle_wkt(lon, lat, radius)
# 查询最近河流段 + 构建完整流动路径
river_geom_wkt = self._get_nearest_river_geom(lon, lat, p['max_river_search_m'])
if river_geom_wkt:
# 取河流上最近点
river_point = self._get_nearest_river_point(lon, lat, p['max_river_search_m'])
if river_point:
path_wkt = _to_wkt_linestring([
(lon, lat),
(river_point[0], river_point[1])
])
else:
path_wkt = river_geom_wkt # 降级:直接展示河流段
else:
path_wkt = None
return zone_wkt, path_wkt
# ==================== 山洪 ====================
def _calc_flash_flood(self, source_id: int, disaster_type: str,
scale_grade: Optional[str],
factors: Dict[str, Any],
lon: float, lat: float) -> Tuple[Optional[str], Optional[str]]:
"""
山洪: 根据距河流距离 + 河流等级确定缓冲区
path_geom 为关联的河流段
"""
p = self.params['flash_flood']
river_dist = float(factors.get('river_distance', 9999))
thresholds = p['river_dist_thresholds']
buffer_map = p['buffer_by_level']
# 按距离选缓冲半径
if river_dist < thresholds[0]:
radius = buffer_map[1]
elif river_dist < thresholds[1]:
radius = buffer_map[2]
elif river_dist < thresholds[2]:
radius = buffer_map[3]
else:
radius = buffer_map.get(4, 80)
zone_wkt = _make_circle_wkt(lon, lat, radius)
# 山洪路径: 关联的最近河流段(淹没路径沿河展开)
path_wkt = self._get_nearest_river_geom(lon, lat, p['max_river_search_m'])
return zone_wkt, path_wkt
# ==================== 内涝 ====================
def _calc_waterlogging(self, source_id: int, disaster_type: str,
scale_grade: Optional[str],
factors: Dict[str, Any],
lon: float, lat: float) -> Tuple[Optional[str], Optional[str]]:
"""内涝: TWI简化 + 不透水率 + 管网修正"""
p = self.params['waterlogging']
slope = float(factors.get('slope_value', 1))
impervious = float(factors.get('impervious_surface', 0))
pipe_density = float(factors.get('pipe_density', 0))
slope_rad = max(math.radians(slope), 0.001)
# TWI_proxy = ln(1 / tanβ)
twi = math.log(1.0 / math.tan(slope_rad))
radius = (p['base_radius_m'] * twi
* (1.0 + impervious * p['impervious_factor'])
* max(p['pipe_lower_bound'], 1.0 - pipe_density * p['pipe_density_factor']))
radius = max(p['min_radius_m'], min(radius, p['max_radius_m']))
zone_wkt = _make_circle_wkt(lon, lat, radius)
return zone_wkt, None
# ==================== 崩塌 ====================
def _calc_rockfall(self, source_id: int, disaster_type: str,
scale_grade: Optional[str],
factors: Dict[str, Any],
lon: float, lat: float) -> Tuple[Optional[str], Optional[str]]:
"""崩塌: 锥体传播模型 (CONEFALL简化)"""
p = self.params['rockfall']
aspect = float(factors.get('aspect_value', 0))
fault_dist = float(factors.get('fault_distance', 9999))
# 到达角
angle_key = scale_grade if scale_grade in p['reach_angle'] else '中型'
reach_angle = p['reach_angle'][angle_key]
# 断裂带惩罚:减少到达角 = 更平缓 = 影响更大
if fault_dist < p['fault_penalty_threshold']:
reach_angle -= p['fault_angle_penalty']
# 半径
H = p['height_drop_m']
radius = H / math.tan(math.radians(max(reach_angle, 10)))
# 断裂带增强
if fault_dist < p['fault_enhance_threshold']:
radius *= p['fault_radius_enhance']
radius = max(p['min_radius_m'], min(radius, p['max_radius_m']))
# 锥形 — 沿坡向,窄扇形
if aspect and aspect > 0:
zone_wkt = _make_fan_wkt(lon, lat, aspect, 45, radius)
else:
zone_wkt = _make_circle_wkt(lon, lat, radius)
return zone_wkt, None
# ==================== 默认 ====================
def _calc_default(self, source_id: int, disaster_type: str,
scale_grade: Optional[str],
factors: Dict[str, Any],
lon: float, lat: float) -> Tuple[Optional[str], Optional[str]]:
"""未匹配灾害类型的兜底:固定半径圆形"""
p = self.params['default']
zone_wkt = _make_circle_wkt(lon, lat, p['radius_m'])
return zone_wkt, None
# ==================== 数据库查询辅助 ====================
@staticmethod
def _get_nearest_river_point(lon: float, lat: float,
max_dist_m: float) -> Optional[Tuple[float, float]]:
"""
查询最近河流上距离隐患点最近的点的坐标
"""
from app.utils.db_helper import db_helper
sql = """
SELECT ST_X(ST_ClosestPoint(r.geom, ST_SetSRID(ST_MakePoint(%s, %s), 4326))) AS rx,
ST_Y(ST_ClosestPoint(r.geom, ST_SetSRID(ST_MakePoint(%s, %s), 4326))) AS ry
FROM xian_rivers r
WHERE r.is_delete = 0
AND ST_DWithin(r.geom::geography,
ST_SetSRID(ST_MakePoint(%s, %s), 4326)::geography,
%s)
ORDER BY ST_Distance(r.geom::geography,
ST_SetSRID(ST_MakePoint(%s, %s), 4326)::geography)
LIMIT 1
"""
row = db_helper.execute_query_one(
sql, (lon, lat, lon, lat, lon, lat, max_dist_m, lon, lat)
)
if row and row['rx'] and row['ry']:
return float(row['rx']), float(row['ry'])
return None
@staticmethod
def _get_nearest_river_geom(lon: float, lat: float,
max_dist_m: float) -> Optional[str]:
"""
查询最近河流段的 WKT
Returns:
LINESTRING WKT 字符串或 None
"""
from app.utils.db_helper import db_helper
sql = """
SELECT ST_AsText(r.geom) AS wkt
FROM xian_rivers r
WHERE r.is_delete = 0
AND ST_DWithin(r.geom::geography,
ST_SetSRID(ST_MakePoint(%s, %s), 4326)::geography,
%s)
ORDER BY ST_Distance(r.geom::geography,
ST_SetSRID(ST_MakePoint(%s, %s), 4326)::geography)
LIMIT 1
"""
row = db_helper.execute_query_one(sql, (lon, lat, max_dist_m, lon, lat))
if row and row['wkt']:
return row['wkt']
return None
# 全局实例
hazard_zone_calculator = HazardZoneCalculator()
+120
View File
@@ -0,0 +1,120 @@
"""
灾害影响范围数据仓库
负责 xian_hazard_zones 表的数据读取和写入
"""
from typing import List, Dict, Any, Optional
from app.utils.db_helper import db_helper
class HazardRepository:
"""灾害影响范围数据访问层"""
# ==================== 隐患点查询 ====================
@staticmethod
def get_all_hidden_danger_spots() -> List[Dict[str, Any]]:
"""
获取所有隐患点(含静态因子)
Returns:
隐患点列表,每个元素含: source_id, disaster_name, disaster_type,
scale_grade, county, lon, lat, static_factors
"""
sql = """
SELECT
h.id AS source_id,
h.disaster_name,
h.disaster_type,
h.scale_grade,
h.county,
rf.lon,
rf.lat,
rf.static_factors
FROM xian_hidden_danger_spots h
JOIN xian_risk_factors rf
ON rf.source_id = h.id AND rf.source_type = 1
WHERE h.is_delete = 0
AND rf.is_delete = 0
AND h.disaster_type IN ('滑坡', '泥石流', '山洪', '内涝', '崩塌')
AND rf.lon IS NOT NULL
AND rf.lat IS NOT NULL
ORDER BY h.id
"""
rows = db_helper.execute_query(sql)
return [{
'source_id': r['source_id'],
'disaster_name': r['disaster_name'],
'disaster_type': r['disaster_type'],
'scale_grade': r['scale_grade'],
'county': r['county'],
'lon': float(r['lon']),
'lat': float(r['lat']),
'static_factors': r.get('static_factors') or {}
} for r in rows]
# ==================== 影响范围写入 ====================
@staticmethod
def delete_existing_zones() -> int:
"""逻辑删除已有影响范围记录"""
sql = "UPDATE xian_hazard_zones SET is_delete = 1 WHERE is_delete = 0"
return db_helper.execute_update(sql)
@staticmethod
def insert_hazard_zone(source_id: int, disaster_type: str,
zone_wkt: str, path_wkt: Optional[str]) -> int:
"""
插入单条影响范围记录
Args:
source_id: 隐患点ID
disaster_type: 灾害类型
zone_wkt: 影响范围 POLYGON WKT
path_wkt: 影响路径 LINESTRING WKT (可为None)
Returns:
新插入记录的ID
"""
if path_wkt:
sql = """
INSERT INTO xian_hazard_zones (source_id, disaster_type, zone_geom, path_geom)
VALUES (%s, %s, ST_GeomFromText(%s, 4326), ST_GeomFromText(%s, 4326))
RETURNING id
"""
row = db_helper.execute_query_one(sql, (source_id, disaster_type, zone_wkt, path_wkt))
else:
sql = """
INSERT INTO xian_hazard_zones (source_id, disaster_type, zone_geom)
VALUES (%s, %s, ST_GeomFromText(%s, 4326))
RETURNING id
"""
row = db_helper.execute_query_one(sql, (source_id, disaster_type, zone_wkt))
return row['id'] if row else 0
@staticmethod
def batch_insert_zones(records: List[Dict[str, Any]]) -> int:
"""
批量插入影响范围记录
Args:
records: [{source_id, disaster_type, zone_wkt, path_wkt}, ...]
Returns:
成功插入的记录数
"""
count = 0
for rec in records:
hid = HazardRepository.insert_hazard_zone(
rec['source_id'],
rec['disaster_type'],
rec['zone_wkt'],
rec.get('path_wkt')
)
if hid > 0:
count += 1
return count
# 全局实例
hazard_repository = HazardRepository()
+115
View File
@@ -0,0 +1,115 @@
"""
计算所有隐患点的灾害影响范围并填充 xian_hazard_zones 表
仅计算隐患点 (xian_hidden_danger_spots),不计算风险点。
灾害类型包括: 滑坡、泥石流、山洪、内涝、崩塌
"""
import sys
import os
from datetime import datetime
# 确保项目根目录在 sys.path 中
_project_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
if _project_root not in sys.path:
sys.path.insert(0, _project_root)
from app.utils.logger import LoggerManager
from app.repositories.hazard_repository import hazard_repository
from app.models.hazard.hazard_zone_calculator import hazard_zone_calculator
_BATCH_SIZE = 200 # 每处理200条输出一次进度
logger = LoggerManager.get_logger("hazard_zones",
os.path.join(_project_root, "logs"))
def main():
logger.info("=" * 60)
logger.info("开始计算隐患点灾害影响范围")
logger.info(f"启动时间: {datetime.now().isoformat()}")
logger.info("=" * 60)
# 1. 获取所有隐患点
spots = hazard_repository.get_all_hidden_danger_spots()
total = len(spots)
logger.info(f"查询到 {total} 个隐患点")
if total == 0:
logger.warning("没有隐患点数据,退出")
return
# 按灾害类型统计
type_counts = {}
for s in spots:
dt = s['disaster_type']
type_counts[dt] = type_counts.get(dt, 0) + 1
logger.info(f"灾害类型分布: {type_counts}")
# 2. 逐点计算
records = []
success_count = 0
skip_count = 0
error_count = 0
for i, spot in enumerate(spots):
sid = spot['source_id']
dtype = spot['disaster_type']
dname = spot['disaster_name']
scale = spot.get('scale_grade')
try:
zone_wkt, path_wkt = hazard_zone_calculator.calculate(
source_id=sid,
disaster_type=dtype,
scale_grade=scale,
lon=spot['lon'],
lat=spot['lat'],
static_factors=spot['static_factors']
)
if zone_wkt:
records.append({
'source_id': sid,
'disaster_type': dtype,
'zone_wkt': zone_wkt,
'path_wkt': path_wkt
})
success_count += 1
else:
skip_count += 1
logger.debug(f"跳过 [{sid}] {dname}({dtype}): 无法计算影响范围")
except Exception as e:
error_count += 1
logger.error(f"计算失败 [{sid}] {dname}({dtype}): {e}")
# 进度输出
if (i + 1) % _BATCH_SIZE == 0 or (i + 1) == total:
logger.info(f"进度: {i + 1}/{total} "
f"(成功:{success_count} 跳过:{skip_count} 失败:{error_count})")
# 3. 写入数据库
logger.info(f"计算完成。开始写入数据库,共 {len(records)} 条记录")
# 先逻辑删除旧数据
deleted = hazard_repository.delete_existing_zones()
logger.info(f"已标记 {deleted} 条旧记录为删除")
# 批量写入新数据
inserted = hazard_repository.batch_insert_zones(records)
logger.info(f"成功写入 {inserted} 条新记录")
# 4. 汇总
logger.info("=" * 60)
logger.info("执行完成")
logger.info(f" 隐患点总数: {total}")
logger.info(f" 计算成功: {success_count}")
logger.info(f" 跳过: {skip_count}")
logger.info(f" 计算失败: {error_count}")
logger.info(f" 写入记录: {inserted}")
logger.info(f"完成时间: {datetime.now().isoformat()}")
logger.info("=" * 60)
if __name__ == '__main__':
main()