2026-01-14 11:37:35 +08:00
|
|
|
|
# earthwork_calculator.py
|
|
|
|
|
|
import numpy as np
|
|
|
|
|
|
from pyproj import Transformer, CRS
|
|
|
|
|
|
from scipy.spatial import Delaunay
|
2026-02-26 11:00:33 +08:00
|
|
|
|
from dataclasses import dataclass, field
|
|
|
|
|
|
from typing import List, Dict, Optional, Tuple, Union, Any
|
2026-01-14 11:37:35 +08:00
|
|
|
|
import logging
|
|
|
|
|
|
from enum import Enum
|
|
|
|
|
|
from abc import ABC, abstractmethod
|
|
|
|
|
|
import math
|
2026-01-29 11:51:20 +08:00
|
|
|
|
from pyproj import Geod
|
2026-02-26 11:00:33 +08:00
|
|
|
|
from scipy.interpolate import LinearNDInterpolator, CloughTocher2DInterpolator
|
|
|
|
|
|
from matplotlib.path import Path
|
2026-01-14 11:37:35 +08:00
|
|
|
|
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
|
|
|
class AlgorithmType(Enum):
|
|
|
|
|
|
"""计算算法类型"""
|
|
|
|
|
|
GRID = "grid"
|
|
|
|
|
|
TIN = "tin"
|
|
|
|
|
|
PRISM = "prism"
|
|
|
|
|
|
|
2026-02-26 11:00:33 +08:00
|
|
|
|
@dataclass
|
|
|
|
|
|
class GridCellData:
|
|
|
|
|
|
"""单个网格单元的数据"""
|
|
|
|
|
|
i: int # x方向索引
|
|
|
|
|
|
j: int # y方向索引
|
|
|
|
|
|
x_min: float # 网格最小x坐标
|
|
|
|
|
|
x_max: float # 网格最大x坐标
|
|
|
|
|
|
y_min: float # 网格最小y坐标
|
|
|
|
|
|
y_max: float # 网格最大y坐标
|
|
|
|
|
|
cut_volume: float # 挖方量
|
|
|
|
|
|
fill_volume: float # 填方量
|
|
|
|
|
|
net_volume: float # 净方量
|
|
|
|
|
|
avg_elevation: float # 平均自然高程
|
|
|
|
|
|
design_elevation: float # 设计高程
|
|
|
|
|
|
height_diff: float # 设计高程与自然高程差
|
|
|
|
|
|
area: float # 网格面积
|
|
|
|
|
|
is_valid: bool # 是否有效
|
|
|
|
|
|
corner_elevations: List[float] = field(default_factory=list) # 四个角点高程
|
|
|
|
|
|
|
|
|
|
|
|
def to_dict(self) -> Dict[str, Any]:
|
|
|
|
|
|
"""转换为字典格式"""
|
|
|
|
|
|
return {
|
|
|
|
|
|
'i': self.i,
|
|
|
|
|
|
'j': self.j,
|
|
|
|
|
|
'x_min': self.x_min,
|
|
|
|
|
|
'x_max': self.x_max,
|
|
|
|
|
|
'y_min': self.y_min,
|
|
|
|
|
|
'y_max': self.y_max,
|
|
|
|
|
|
'cut_volume': self.cut_volume,
|
|
|
|
|
|
'fill_volume': self.fill_volume,
|
|
|
|
|
|
'net_volume': self.net_volume,
|
|
|
|
|
|
'avg_elevation': self.avg_elevation,
|
|
|
|
|
|
'design_elevation': self.design_elevation,
|
|
|
|
|
|
'height_diff': self.height_diff,
|
|
|
|
|
|
'area': self.area,
|
|
|
|
|
|
'is_valid': self.is_valid,
|
|
|
|
|
|
'corner_elevations': self.corner_elevations
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass
|
|
|
|
|
|
class TriangleData:
|
|
|
|
|
|
"""单个三角形单元的数据"""
|
|
|
|
|
|
triangle_id: int # 三角形ID
|
|
|
|
|
|
vertices: List[Tuple[float, float, float]] # 三个顶点坐标(x,y,z)
|
|
|
|
|
|
vertex_indices: List[int] # 顶点索引
|
|
|
|
|
|
cut_volume: float # 挖方量
|
|
|
|
|
|
fill_volume: float # 填方量
|
|
|
|
|
|
net_volume: float # 净方量
|
|
|
|
|
|
avg_elevation: float # 平均自然高程
|
|
|
|
|
|
design_elevation: float # 设计高程
|
|
|
|
|
|
height_diff: float # 设计高程与自然高程差
|
|
|
|
|
|
area: float # 三角形面积
|
|
|
|
|
|
is_valid: bool # 是否有效
|
|
|
|
|
|
center_point: Tuple[float, float] # 三角形中心点
|
|
|
|
|
|
|
|
|
|
|
|
def to_dict(self) -> Dict[str, Any]:
|
|
|
|
|
|
"""转换为字典格式"""
|
|
|
|
|
|
return {
|
|
|
|
|
|
'triangle_id': self.triangle_id,
|
|
|
|
|
|
'vertices': self.vertices,
|
|
|
|
|
|
'vertex_indices': self.vertex_indices,
|
|
|
|
|
|
'cut_volume': self.cut_volume,
|
|
|
|
|
|
'fill_volume': self.fill_volume,
|
|
|
|
|
|
'net_volume': self.net_volume,
|
|
|
|
|
|
'avg_elevation': self.avg_elevation,
|
|
|
|
|
|
'design_elevation': self.design_elevation,
|
|
|
|
|
|
'height_diff': self.height_diff,
|
|
|
|
|
|
'area': self.area,
|
|
|
|
|
|
'is_valid': self.is_valid,
|
|
|
|
|
|
'center_point': self.center_point
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass
|
|
|
|
|
|
class PrismData:
|
|
|
|
|
|
"""单个三棱柱单元的数据"""
|
|
|
|
|
|
triangle_id: int # 三角形ID
|
|
|
|
|
|
vertices: List[Tuple[float, float, float]] # 三个顶点坐标(x,y,z)
|
|
|
|
|
|
vertex_indices: List[int] # 顶点索引
|
|
|
|
|
|
cut_volume: float # 挖方量
|
|
|
|
|
|
fill_volume: float # 填方量
|
|
|
|
|
|
net_volume: float # 净方量
|
|
|
|
|
|
avg_elevation: float # 平均自然高程
|
|
|
|
|
|
design_elevation: float # 设计高程
|
|
|
|
|
|
height_diffs: List[float] # 三个顶点的高程差
|
|
|
|
|
|
area: float # 三角形面积
|
|
|
|
|
|
is_valid: bool # 是否有效
|
|
|
|
|
|
center_point: Tuple[float, float] # 三角形中心点
|
|
|
|
|
|
edge_volumes: List[float] = field(default_factory=list) # 三个边的体积贡献
|
|
|
|
|
|
|
|
|
|
|
|
def to_dict(self) -> Dict[str, Any]:
|
|
|
|
|
|
"""转换为字典格式"""
|
|
|
|
|
|
return {
|
|
|
|
|
|
'triangle_id': self.triangle_id,
|
|
|
|
|
|
'vertices': self.vertices,
|
|
|
|
|
|
'vertex_indices': self.vertex_indices,
|
|
|
|
|
|
'cut_volume': self.cut_volume,
|
|
|
|
|
|
'fill_volume': self.fill_volume,
|
|
|
|
|
|
'net_volume': self.net_volume,
|
|
|
|
|
|
'avg_elevation': self.avg_elevation,
|
|
|
|
|
|
'design_elevation': self.design_elevation,
|
|
|
|
|
|
'height_diffs': self.height_diffs,
|
|
|
|
|
|
'area': self.area,
|
|
|
|
|
|
'is_valid': self.is_valid,
|
|
|
|
|
|
'center_point': self.center_point,
|
|
|
|
|
|
'edge_volumes': self.edge_volumes
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass
|
|
|
|
|
|
class GridCellDifference:
|
|
|
|
|
|
"""网格单元差异数据"""
|
|
|
|
|
|
i: int # x方向索引
|
|
|
|
|
|
j: int # y方向索引
|
|
|
|
|
|
x_min: float # 网格最小x坐标
|
|
|
|
|
|
x_max: float # 网格最大x坐标
|
|
|
|
|
|
y_min: float # 网格最小y坐标
|
|
|
|
|
|
y_max: float # 网格最大y坐标
|
|
|
|
|
|
cut_volume: float # 挖方量差值 (b-a)
|
|
|
|
|
|
fill_volume: float # 填方量差值 (b-a)
|
|
|
|
|
|
net_volume: float # 净方量差值 (b-a)
|
|
|
|
|
|
avg_elevation: float # 平均自然高程差值 (b-a)
|
|
|
|
|
|
height_diff: float # 高程差变化 (b-a)
|
|
|
|
|
|
is_valid: bool # 是否有效(两个网格都有效)
|
|
|
|
|
|
|
|
|
|
|
|
def to_dict(self) -> Dict[str, Any]:
|
|
|
|
|
|
"""转换为字典格式"""
|
|
|
|
|
|
return {
|
|
|
|
|
|
'i': self.i,
|
|
|
|
|
|
'j': self.j,
|
|
|
|
|
|
'x_min': self.x_min,
|
|
|
|
|
|
'x_max': self.x_max,
|
|
|
|
|
|
'y_min': self.y_min,
|
|
|
|
|
|
'y_max': self.y_max,
|
|
|
|
|
|
'cut_volume': self.cut_volume,
|
|
|
|
|
|
'fill_volume': self.fill_volume,
|
|
|
|
|
|
'net_volume': self.net_volume,
|
|
|
|
|
|
'avg_elevation': self.avg_elevation,
|
|
|
|
|
|
'height_diff': self.height_diff,
|
|
|
|
|
|
'is_valid': self.is_valid
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass
|
|
|
|
|
|
class EarthworkComparisonResult:
|
|
|
|
|
|
"""土方量比较结果"""
|
|
|
|
|
|
# 总体差异统计
|
|
|
|
|
|
total_cut_volume: float # 总挖方量差值
|
|
|
|
|
|
total_fill_volume: float # 总填方量差值
|
|
|
|
|
|
total_net_volume: float # 总净方量差值
|
|
|
|
|
|
|
|
|
|
|
|
# 网格单元差异数据
|
|
|
|
|
|
grid_differences: List[Dict[str, Any]]
|
|
|
|
|
|
|
|
|
|
|
|
def to_dict(self) -> Dict[str, Any]:
|
|
|
|
|
|
"""转换为字典"""
|
|
|
|
|
|
return {
|
|
|
|
|
|
"total_differences": {
|
|
|
|
|
|
"cut_volume": round(self.total_cut_volume, 8),
|
|
|
|
|
|
"fill_volume": round(self.total_fill_volume, 8),
|
|
|
|
|
|
"net_volume": round(self.total_net_volume, 8),
|
|
|
|
|
|
},
|
|
|
|
|
|
"grid_differences": self.grid_differences
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-14 11:37:35 +08:00
|
|
|
|
@dataclass
|
|
|
|
|
|
class EarthworkResult3dTiles:
|
|
|
|
|
|
"""土方量计算结果"""
|
|
|
|
|
|
cut_volume: float # 挖方量 (m³)
|
|
|
|
|
|
fill_volume: float # 填方量 (m³)
|
|
|
|
|
|
net_volume: float # 净方量 (m³)
|
|
|
|
|
|
area: float # 计算区域面积 (m²)
|
|
|
|
|
|
avg_elevation: float # 平均高程
|
|
|
|
|
|
min_elevation: float # 最低高程
|
|
|
|
|
|
max_elevation: float # 最高高程
|
|
|
|
|
|
points_count: int # 使用的点数
|
|
|
|
|
|
bounding_box: Dict[str, List[float]] # 边界框
|
|
|
|
|
|
volume_accuracy: float # 计算精度
|
|
|
|
|
|
algorithm: str # 使用的算法
|
|
|
|
|
|
resolution: float # 计算分辨率
|
|
|
|
|
|
|
2026-02-26 11:00:33 +08:00
|
|
|
|
# 新增属性 - 计算单元详细数据
|
|
|
|
|
|
calculation_details: List[Dict[str, Any]] = field(default_factory=list) # 所有计算单元的详细数据
|
|
|
|
|
|
details_type: str = "" # 计算单元类型: "grid_cells", "triangles", "prisms"
|
|
|
|
|
|
details_dimensions: Dict[str, Any] = field(default_factory=dict) # 计算单元维度信息
|
|
|
|
|
|
valid_unit_count: int = 0 # 有效计算单元数量
|
|
|
|
|
|
total_unit_count: int = 0 # 总计算单元数量
|
|
|
|
|
|
|
|
|
|
|
|
# 新增属性 - 统计信息
|
|
|
|
|
|
elevation_statistics: Dict[str, float] = field(default_factory=dict) # 高程统计信息
|
|
|
|
|
|
volume_distribution: Dict[str, Any] = field(default_factory=dict) # 土方量分布信息
|
|
|
|
|
|
|
2026-01-14 11:37:35 +08:00
|
|
|
|
def to_dict(self) -> Dict:
|
|
|
|
|
|
"""转换为字典"""
|
2026-02-26 11:00:33 +08:00
|
|
|
|
result = {
|
2026-01-14 11:37:35 +08:00
|
|
|
|
"volume": {
|
2026-01-29 11:51:20 +08:00
|
|
|
|
"cut": round(self.cut_volume, 8),
|
|
|
|
|
|
"fill": round(self.fill_volume, 8),
|
|
|
|
|
|
"net": round(self.net_volume, 8),
|
2026-01-14 11:37:35 +08:00
|
|
|
|
"unit": "m³"
|
|
|
|
|
|
},
|
|
|
|
|
|
"area": {
|
2026-01-29 11:51:20 +08:00
|
|
|
|
"value": round(self.area, 8),
|
2026-01-14 11:37:35 +08:00
|
|
|
|
"unit": "m²"
|
|
|
|
|
|
},
|
|
|
|
|
|
"elevation": {
|
|
|
|
|
|
"average": round(self.avg_elevation, 3),
|
|
|
|
|
|
"min": round(self.min_elevation, 3),
|
|
|
|
|
|
"max": round(self.max_elevation, 3),
|
|
|
|
|
|
"unit": "m"
|
|
|
|
|
|
},
|
|
|
|
|
|
"statistics": {
|
|
|
|
|
|
"points_count": self.points_count,
|
|
|
|
|
|
"accuracy": round(self.volume_accuracy, 3),
|
|
|
|
|
|
"algorithm": self.algorithm
|
|
|
|
|
|
},
|
|
|
|
|
|
"bounding_box": self.bounding_box,
|
|
|
|
|
|
"calculation_params": {
|
|
|
|
|
|
"resolution": self.resolution,
|
|
|
|
|
|
"accuracy": self.volume_accuracy
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-02-26 11:00:33 +08:00
|
|
|
|
|
|
|
|
|
|
# 添加新增的属性
|
|
|
|
|
|
if self.calculation_details:
|
|
|
|
|
|
result["calculation_details"] = {
|
|
|
|
|
|
"type": self.details_type,
|
|
|
|
|
|
"dimensions": self.details_dimensions,
|
|
|
|
|
|
"valid_unit_count": self.valid_unit_count,
|
|
|
|
|
|
"total_unit_count": self.total_unit_count,
|
|
|
|
|
|
"data": self.calculation_details
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if self.elevation_statistics:
|
|
|
|
|
|
result["elevation_statistics"] = self.elevation_statistics
|
|
|
|
|
|
|
|
|
|
|
|
if self.volume_distribution:
|
|
|
|
|
|
result["volume_distribution"] = self.volume_distribution
|
|
|
|
|
|
|
|
|
|
|
|
return result
|
|
|
|
|
|
|
2026-01-14 11:37:35 +08:00
|
|
|
|
class TerrainDataSource(ABC):
|
|
|
|
|
|
"""地形数据源抽象类"""
|
|
|
|
|
|
|
|
|
|
|
|
@abstractmethod
|
|
|
|
|
|
async def get_points_in_polygon(self,
|
|
|
|
|
|
polygon_coords: List[List[float]],
|
|
|
|
|
|
z_range: Optional[Tuple[float, float]] = None) -> np.ndarray:
|
|
|
|
|
|
"""
|
|
|
|
|
|
获取多边形区域内的点云数据
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
polygon_coords: 多边形坐标 [[x1,y1], [x2,y2], ...]
|
|
|
|
|
|
z_range: 高程范围 (min_z, max_z)
|
|
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
|
Nx3的numpy数组 [x, y, z]
|
|
|
|
|
|
"""
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
@abstractmethod
|
|
|
|
|
|
async def get_data_bounds(self) -> Dict[str, List[float]]:
|
|
|
|
|
|
"""获取数据范围"""
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
@abstractmethod
|
|
|
|
|
|
def get_crs(self) -> str:
|
|
|
|
|
|
"""获取数据坐标系"""
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
class GeometryUtils:
|
2026-01-29 11:51:20 +08:00
|
|
|
|
"""地理空间几何计算工具类(支持经纬度坐标)"""
|
2026-01-14 11:37:35 +08:00
|
|
|
|
|
2026-01-29 11:51:20 +08:00
|
|
|
|
def __init__(self, source_crs: str = "EPSG:4326", target_crs: str = "EPSG:3857"):
|
|
|
|
|
|
"""
|
|
|
|
|
|
初始化
|
|
|
|
|
|
Args:
|
|
|
|
|
|
source_crs: 源坐标系(通常是EPSG:4326)
|
|
|
|
|
|
target_crs: 目标投影坐标系(用于平面计算)
|
|
|
|
|
|
"""
|
|
|
|
|
|
self.source_crs = source_crs
|
|
|
|
|
|
self.target_crs = target_crs
|
|
|
|
|
|
self.geod = Geod(ellps="WGS84")
|
|
|
|
|
|
|
|
|
|
|
|
# 创建坐标转换器
|
|
|
|
|
|
self.transformer_to_proj = Transformer.from_crs(
|
|
|
|
|
|
source_crs, target_crs, always_xy=True
|
|
|
|
|
|
)
|
|
|
|
|
|
self.transformer_to_geo = Transformer.from_crs(
|
|
|
|
|
|
target_crs, source_crs, always_xy=True
|
|
|
|
|
|
)
|
2026-01-14 11:37:35 +08:00
|
|
|
|
|
2026-01-29 11:51:20 +08:00
|
|
|
|
def calculate_polygon_area(self, polygon_coords: List[List[float]]) -> float:
|
|
|
|
|
|
"""
|
|
|
|
|
|
计算多边形的地面实际面积(平方米)
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
polygon_coords: 经纬度坐标列表 [[lon1, lat1], ...]
|
|
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
|
面积(平方米)
|
|
|
|
|
|
"""
|
|
|
|
|
|
if len(polygon_coords) < 3:
|
|
|
|
|
|
return 0.0
|
|
|
|
|
|
|
|
|
|
|
|
# 确保多边形闭合
|
|
|
|
|
|
closed_coords = self._ensure_closed_polygon(polygon_coords)
|
|
|
|
|
|
|
|
|
|
|
|
# 提取经纬度
|
|
|
|
|
|
lons = [coord[0] for coord in closed_coords]
|
|
|
|
|
|
lats = [coord[1] for coord in closed_coords]
|
|
|
|
|
|
|
|
|
|
|
|
# 使用测地线计算面积
|
|
|
|
|
|
area, _ = self.geod.polygon_area_perimeter(lons, lats)
|
|
|
|
|
|
return abs(area)
|
2026-01-14 11:37:35 +08:00
|
|
|
|
|
2026-01-29 11:51:20 +08:00
|
|
|
|
def is_point_in_polygon(self, point: Tuple[float, float],
|
|
|
|
|
|
polygon_coords: List[List[float]],
|
|
|
|
|
|
use_spherical: bool = True) -> bool:
|
|
|
|
|
|
"""
|
|
|
|
|
|
判断点是否在多边形内(支持地球表面判断)
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
point: 点坐标 (lon, lat)
|
|
|
|
|
|
polygon_coords: 多边形顶点坐标
|
|
|
|
|
|
use_spherical: 是否使用球面算法
|
|
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
|
是否在多边形内
|
|
|
|
|
|
"""
|
|
|
|
|
|
if len(polygon_coords) < 3:
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
if use_spherical:
|
|
|
|
|
|
# 方法1:球面射线法(更准确)
|
|
|
|
|
|
return self._is_point_in_polygon_spherical(point, polygon_coords)
|
|
|
|
|
|
else:
|
|
|
|
|
|
# 方法2:投影到平面后判断(更快)
|
|
|
|
|
|
return self._is_point_in_polygon_planar(point, polygon_coords)
|
|
|
|
|
|
|
|
|
|
|
|
def calculate_triangle_area(self, points: np.ndarray) -> float:
|
|
|
|
|
|
"""
|
|
|
|
|
|
计算三角形的地面面积(平方米)
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
points: 3×2数组,每行是[lon, lat]
|
|
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
|
三角形地面面积(平方米)
|
|
|
|
|
|
"""
|
|
|
|
|
|
if points.shape != (3, 2):
|
|
|
|
|
|
raise ValueError("需要3个点的坐标")
|
|
|
|
|
|
|
|
|
|
|
|
# 转换为球面坐标计算
|
|
|
|
|
|
lons = points[:, 0]
|
|
|
|
|
|
lats = points[:, 1]
|
|
|
|
|
|
|
|
|
|
|
|
# 使用球面三角形面积公式
|
|
|
|
|
|
R = 6378137.0 # WGS84地球半径(米)
|
|
|
|
|
|
|
|
|
|
|
|
# 转换为弧度
|
|
|
|
|
|
lon_rad = np.radians(lons)
|
|
|
|
|
|
lat_rad = np.radians(lats)
|
|
|
|
|
|
|
|
|
|
|
|
# 计算球面三角形的面积
|
|
|
|
|
|
# 使用L'Huilier公式
|
|
|
|
|
|
a = self._spherical_distance(lon_rad[0], lat_rad[0], lon_rad[1], lat_rad[1])
|
|
|
|
|
|
b = self._spherical_distance(lon_rad[1], lat_rad[1], lon_rad[2], lat_rad[2])
|
|
|
|
|
|
c = self._spherical_distance(lon_rad[2], lat_rad[2], lon_rad[0], lat_rad[0])
|
|
|
|
|
|
|
2026-01-14 11:37:35 +08:00
|
|
|
|
s = (a + b + c) / 2
|
2026-01-29 11:51:20 +08:00
|
|
|
|
|
|
|
|
|
|
# 防止数值误差
|
|
|
|
|
|
tan_e2 = np.tan(s/2) * np.tan((s-a)/2) * np.tan((s-b)/2) * np.tan((s-c)/2)
|
|
|
|
|
|
tan_e2 = max(tan_e2, 0) # 避免负值
|
|
|
|
|
|
|
|
|
|
|
|
if tan_e2 > 0:
|
|
|
|
|
|
E = 4 * np.arctan(np.sqrt(tan_e2))
|
|
|
|
|
|
else:
|
|
|
|
|
|
E = 0
|
|
|
|
|
|
|
|
|
|
|
|
area = R * R * E
|
|
|
|
|
|
return area
|
2026-01-14 11:37:35 +08:00
|
|
|
|
|
2026-01-29 11:51:20 +08:00
|
|
|
|
def create_grid(self, polygon_coords: List[List[float]],
|
|
|
|
|
|
resolution_m: float,
|
|
|
|
|
|
use_projection: bool = True) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
|
|
|
|
|
|
"""
|
|
|
|
|
|
创建规则格网(地面距离为单位的网格)
|
2026-01-14 11:37:35 +08:00
|
|
|
|
|
2026-01-29 11:51:20 +08:00
|
|
|
|
Args:
|
|
|
|
|
|
polygon_coords: 多边形坐标
|
|
|
|
|
|
resolution_m: 网格分辨率(米)
|
|
|
|
|
|
use_projection: 是否使用投影坐标系
|
|
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
|
xx, yy: 网格坐标
|
|
|
|
|
|
grid_coords_geo: 网格点的地理坐标
|
|
|
|
|
|
"""
|
|
|
|
|
|
if use_projection:
|
|
|
|
|
|
# 方法1:投影到平面坐标系创建网格
|
|
|
|
|
|
return self._create_grid_projected(polygon_coords, resolution_m)
|
|
|
|
|
|
else:
|
|
|
|
|
|
# 方法2:直接在经纬度上创建近似网格(小区域可用)
|
|
|
|
|
|
return self._create_grid_geographic(polygon_coords, resolution_m)
|
2026-01-14 11:37:35 +08:00
|
|
|
|
|
2026-01-29 11:51:20 +08:00
|
|
|
|
def interpolate_grid(self, xx: np.ndarray, yy: np.ndarray,
|
|
|
|
|
|
points: np.ndarray,
|
|
|
|
|
|
method: str = 'linear',
|
|
|
|
|
|
return_geo: bool = False) -> np.ndarray:
|
|
|
|
|
|
"""
|
|
|
|
|
|
格网插值
|
2026-01-14 11:37:35 +08:00
|
|
|
|
|
2026-01-29 11:51:20 +08:00
|
|
|
|
Args:
|
|
|
|
|
|
xx, yy: 网格坐标(投影坐标系)
|
|
|
|
|
|
points: 已知点,每行是[lon, lat, elevation]或[x_proj, y_proj, elevation]
|
|
|
|
|
|
method: 插值方法 'linear' 或 'cubic'
|
|
|
|
|
|
return_geo: 是否返回地理坐标
|
|
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
|
插值后的高程网格
|
|
|
|
|
|
"""
|
|
|
|
|
|
# 确保points是投影坐标
|
|
|
|
|
|
if points.shape[1] != 3:
|
|
|
|
|
|
raise ValueError("points应为3列: x, y, z")
|
|
|
|
|
|
|
|
|
|
|
|
# 如果输入是地理坐标,转换为投影坐标
|
|
|
|
|
|
if np.max(np.abs(points[:, 0])) > 180: # 粗略判断
|
|
|
|
|
|
# 已经是投影坐标
|
|
|
|
|
|
points_proj = points
|
|
|
|
|
|
else:
|
|
|
|
|
|
# 转换为投影坐标
|
|
|
|
|
|
x_proj, y_proj = self.transformer_to_proj.transform(
|
|
|
|
|
|
points[:, 0], points[:, 1]
|
|
|
|
|
|
)
|
|
|
|
|
|
points_proj = np.column_stack([x_proj, y_proj, points[:, 2]])
|
2026-01-14 11:37:35 +08:00
|
|
|
|
|
2026-01-29 11:51:20 +08:00
|
|
|
|
# 创建插值器
|
2026-01-14 11:37:35 +08:00
|
|
|
|
if method == 'linear':
|
2026-01-29 11:51:20 +08:00
|
|
|
|
interpolator = LinearNDInterpolator(
|
|
|
|
|
|
points_proj[:, :2],
|
|
|
|
|
|
points_proj[:, 2],
|
|
|
|
|
|
fill_value=np.nan
|
|
|
|
|
|
)
|
2026-01-14 11:37:35 +08:00
|
|
|
|
elif method == 'cubic':
|
2026-01-29 11:51:20 +08:00
|
|
|
|
interpolator = CloughTocher2DInterpolator(
|
|
|
|
|
|
points_proj[:, :2],
|
|
|
|
|
|
points_proj[:, 2],
|
|
|
|
|
|
fill_value=np.nan
|
|
|
|
|
|
)
|
2026-01-14 11:37:35 +08:00
|
|
|
|
else:
|
|
|
|
|
|
raise ValueError(f"不支持的插值方法: {method}")
|
|
|
|
|
|
|
2026-01-29 11:51:20 +08:00
|
|
|
|
# 插值
|
|
|
|
|
|
grid_points = np.column_stack([xx.ravel(), yy.ravel()])
|
2026-01-14 11:37:35 +08:00
|
|
|
|
elevations = interpolator(grid_points)
|
2026-01-29 11:51:20 +08:00
|
|
|
|
result = elevations.reshape(xx.shape)
|
|
|
|
|
|
|
|
|
|
|
|
if return_geo:
|
|
|
|
|
|
# 如果需要,将网格点转回地理坐标
|
|
|
|
|
|
lon_grid, lat_grid = self.transformer_to_geo.transform(
|
|
|
|
|
|
xx.ravel(), yy.ravel()
|
|
|
|
|
|
)
|
|
|
|
|
|
lon_grid = lon_grid.reshape(xx.shape)
|
|
|
|
|
|
lat_grid = lat_grid.reshape(xx.shape)
|
|
|
|
|
|
return result, lon_grid, lat_grid
|
|
|
|
|
|
|
|
|
|
|
|
return result
|
|
|
|
|
|
|
|
|
|
|
|
# ============ 私有方法 ============
|
|
|
|
|
|
|
|
|
|
|
|
def _ensure_closed_polygon(self, coords: List[List[float]]) -> List[List[float]]:
|
|
|
|
|
|
"""确保多边形闭合"""
|
|
|
|
|
|
if len(coords) >= 3:
|
|
|
|
|
|
# 使用 numpy 比较
|
|
|
|
|
|
if not np.array_equal(coords[0], coords[-1]):
|
|
|
|
|
|
return coords + [coords[0]]
|
|
|
|
|
|
return coords
|
|
|
|
|
|
|
|
|
|
|
|
def _spherical_distance(self, lon1_rad: float, lat1_rad: float,
|
|
|
|
|
|
lon2_rad: float, lat2_rad: float) -> float:
|
|
|
|
|
|
"""计算球面两点间角距离"""
|
|
|
|
|
|
dlon = lon2_rad - lon1_rad
|
|
|
|
|
|
dlat = lat2_rad - lat1_rad
|
|
|
|
|
|
|
|
|
|
|
|
a = np.sin(dlat/2)**2 + np.cos(lat1_rad) * np.cos(lat2_rad) * np.sin(dlon/2)**2
|
|
|
|
|
|
return 2 * np.arcsin(np.sqrt(a))
|
|
|
|
|
|
|
|
|
|
|
|
def _is_point_in_polygon_spherical(self, point: Tuple[float, float],
|
|
|
|
|
|
polygon_coords: List[List[float]]) -> bool:
|
|
|
|
|
|
"""球面射线法判断点是否在多边形内"""
|
|
|
|
|
|
lon_p, lat_p = point
|
|
|
|
|
|
closed_polygon = self._ensure_closed_polygon(polygon_coords)
|
|
|
|
|
|
|
|
|
|
|
|
# 将多边形的边转换为球面大圆弧
|
|
|
|
|
|
crossings = 0
|
|
|
|
|
|
n = len(closed_polygon) - 1
|
|
|
|
|
|
|
|
|
|
|
|
for i in range(n):
|
|
|
|
|
|
lon1, lat1 = closed_polygon[i]
|
|
|
|
|
|
lon2, lat2 = closed_polygon[i + 1]
|
|
|
|
|
|
|
|
|
|
|
|
# 检查射线是否与边相交(近似算法)
|
|
|
|
|
|
# 简化:使用平面近似,对小区域足够准确
|
|
|
|
|
|
if ((lat1 > lat_p) != (lat2 > lat_p)) and \
|
|
|
|
|
|
(lon_p < (lon2 - lon1) * (lat_p - lat1) / (lat2 - lat1) + lon1):
|
|
|
|
|
|
crossings += 1
|
|
|
|
|
|
|
|
|
|
|
|
return crossings % 2 == 1
|
|
|
|
|
|
|
|
|
|
|
|
def _is_point_in_polygon_planar(self, point: Tuple[float, float],
|
|
|
|
|
|
polygon_coords: List[List[float]]) -> bool:
|
|
|
|
|
|
"""投影到平面后判断"""
|
|
|
|
|
|
# 转换为投影坐标
|
|
|
|
|
|
point_proj = np.array(self.transformer_to_proj.transform(point[0], point[1])).reshape(1, 2)
|
|
|
|
|
|
polygon_proj = np.array([
|
|
|
|
|
|
self.transformer_to_proj.transform(lon, lat)
|
|
|
|
|
|
for lon, lat in polygon_coords
|
|
|
|
|
|
])
|
|
|
|
|
|
|
|
|
|
|
|
# 使用平面方法判断
|
|
|
|
|
|
path = Path(polygon_proj)
|
|
|
|
|
|
return path.contains_point(point_proj[0])
|
|
|
|
|
|
|
|
|
|
|
|
def _create_grid_projected(self, polygon_coords: List[List[float]],
|
|
|
|
|
|
resolution_m: float) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
|
|
|
|
|
|
"""在投影坐标系中创建网格"""
|
|
|
|
|
|
# 将多边形转换为投影坐标
|
|
|
|
|
|
polygon_proj = []
|
|
|
|
|
|
for lon, lat in polygon_coords:
|
|
|
|
|
|
x, y = self.transformer_to_proj.transform(lon, lat)
|
|
|
|
|
|
polygon_proj.append([x, y])
|
|
|
|
|
|
|
|
|
|
|
|
polygon_proj = np.array(polygon_proj)
|
|
|
|
|
|
|
|
|
|
|
|
# 计算边界框
|
|
|
|
|
|
x_min, y_min = polygon_proj.min(axis=0)
|
|
|
|
|
|
x_max, y_max = polygon_proj.max(axis=0)
|
|
|
|
|
|
|
|
|
|
|
|
# 扩展半个网格
|
|
|
|
|
|
x_min -= resolution_m / 2
|
|
|
|
|
|
x_max += resolution_m / 2
|
|
|
|
|
|
y_min -= resolution_m / 2
|
|
|
|
|
|
y_max += resolution_m / 2
|
|
|
|
|
|
|
|
|
|
|
|
# 创建网格
|
|
|
|
|
|
x_grid = np.arange(x_min, x_max + resolution_m, resolution_m)
|
|
|
|
|
|
y_grid = np.arange(y_min, y_max + resolution_m, resolution_m)
|
|
|
|
|
|
xx, yy = np.meshgrid(x_grid, y_grid)
|
|
|
|
|
|
|
|
|
|
|
|
# 将网格点转回地理坐标
|
|
|
|
|
|
grid_coords_geo = []
|
|
|
|
|
|
for x, y in zip(xx.ravel(), yy.ravel()):
|
|
|
|
|
|
lon, lat = self.transformer_to_geo.transform(x, y)
|
|
|
|
|
|
grid_coords_geo.append([lon, lat])
|
|
|
|
|
|
grid_coords_geo = np.array(grid_coords_geo).reshape(xx.shape[0], xx.shape[1], 2)
|
|
|
|
|
|
|
|
|
|
|
|
return xx, yy, grid_coords_geo
|
|
|
|
|
|
|
|
|
|
|
|
def _create_grid_geographic(self, polygon_coords: List[List[float]],
|
|
|
|
|
|
resolution_m: float) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
|
|
|
|
|
|
"""在经纬度坐标系中创建近似网格"""
|
|
|
|
|
|
# 计算中心点
|
|
|
|
|
|
lons = [coord[0] for coord in polygon_coords]
|
|
|
|
|
|
lats = [coord[1] for coord in polygon_coords]
|
|
|
|
|
|
center_lon = np.mean(lons)
|
|
|
|
|
|
center_lat = np.mean(lats)
|
|
|
|
|
|
|
|
|
|
|
|
# 计算经纬度到米的换算系数
|
|
|
|
|
|
lat_rad = np.radians(center_lat)
|
|
|
|
|
|
meters_per_degree_lon = 111319.9 * np.cos(lat_rad)
|
|
|
|
|
|
meters_per_degree_lat = 111000.0
|
|
|
|
|
|
|
|
|
|
|
|
# 计算边界框(米)
|
|
|
|
|
|
x_min_m, x_max_m, y_min_m, y_max_m = 1e9, -1e9, 1e9, -1e9
|
|
|
|
|
|
for lon, lat in polygon_coords:
|
|
|
|
|
|
x_m = (lon - center_lon) * meters_per_degree_lon
|
|
|
|
|
|
y_m = (lat - center_lat) * meters_per_degree_lat
|
|
|
|
|
|
x_min_m = min(x_min_m, x_m)
|
|
|
|
|
|
x_max_m = max(x_max_m, x_m)
|
|
|
|
|
|
y_min_m = min(y_min_m, y_m)
|
|
|
|
|
|
y_max_m = max(y_max_m, y_m)
|
|
|
|
|
|
|
|
|
|
|
|
# 扩展半个网格
|
|
|
|
|
|
x_min_m -= resolution_m / 2
|
|
|
|
|
|
x_max_m += resolution_m / 2
|
|
|
|
|
|
y_min_m -= resolution_m / 2
|
|
|
|
|
|
y_max_m += resolution_m / 2
|
|
|
|
|
|
|
|
|
|
|
|
# 创建网格(米)
|
|
|
|
|
|
x_grid_m = np.arange(x_min_m, x_max_m + resolution_m, resolution_m)
|
|
|
|
|
|
y_grid_m = np.arange(y_min_m, y_max_m + resolution_m, resolution_m)
|
|
|
|
|
|
|
|
|
|
|
|
# 转换为经纬度
|
|
|
|
|
|
x_grid_lon = center_lon + x_grid_m / meters_per_degree_lon
|
|
|
|
|
|
y_grid_lat = center_lat + y_grid_m / meters_per_degree_lat
|
|
|
|
|
|
|
|
|
|
|
|
xx, yy = np.meshgrid(x_grid_lon, y_grid_lat)
|
|
|
|
|
|
|
|
|
|
|
|
# 网格坐标(经纬度)
|
|
|
|
|
|
grid_coords_geo = np.dstack([xx, yy])
|
|
|
|
|
|
|
|
|
|
|
|
return xx, yy, grid_coords_geo
|
|
|
|
|
|
|
|
|
|
|
|
def get_polygon_bounds(self, polygon_coords: List[List[float]],
|
|
|
|
|
|
in_meters: bool = False) -> dict:
|
|
|
|
|
|
"""
|
|
|
|
|
|
获取多边形边界信息
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
polygon_coords: 多边形坐标
|
|
|
|
|
|
in_meters: 是否返回米为单位
|
|
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
|
边界信息字典
|
|
|
|
|
|
"""
|
|
|
|
|
|
lons = [coord[0] for coord in polygon_coords]
|
|
|
|
|
|
lats = [coord[1] for coord in polygon_coords]
|
|
|
|
|
|
|
|
|
|
|
|
bounds = {
|
|
|
|
|
|
'min_lon': min(lons),
|
|
|
|
|
|
'max_lon': max(lons),
|
|
|
|
|
|
'min_lat': min(lats),
|
|
|
|
|
|
'max_lat': max(lats),
|
|
|
|
|
|
'center_lon': (min(lons) + max(lons)) / 2,
|
|
|
|
|
|
'center_lat': (min(lats) + max(lats)) / 2
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if in_meters:
|
|
|
|
|
|
# 计算实际尺寸(米)
|
|
|
|
|
|
center_lat = bounds['center_lat']
|
|
|
|
|
|
lat_rad = np.radians(center_lat)
|
|
|
|
|
|
meters_per_degree_lon = 111319.9 * np.cos(lat_rad)
|
|
|
|
|
|
meters_per_degree_lat = 111000.0
|
|
|
|
|
|
|
|
|
|
|
|
bounds['width_m'] = (bounds['max_lon'] - bounds['min_lon']) * meters_per_degree_lon
|
|
|
|
|
|
bounds['height_m'] = (bounds['max_lat'] - bounds['min_lat']) * meters_per_degree_lat
|
|
|
|
|
|
bounds['area_m2'] = self.calculate_polygon_area(polygon_coords)
|
|
|
|
|
|
|
|
|
|
|
|
return bounds
|
2026-01-14 11:37:35 +08:00
|
|
|
|
|
|
|
|
|
|
class EarthworkCalculator3dTiles:
|
|
|
|
|
|
"""土方量计算器"""
|
|
|
|
|
|
|
|
|
|
|
|
def __init__(self, data_source: TerrainDataSource):
|
|
|
|
|
|
"""
|
|
|
|
|
|
初始化计算器
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
data_source: 地形数据源
|
|
|
|
|
|
"""
|
|
|
|
|
|
self.data_source = data_source
|
2026-01-29 11:51:20 +08:00
|
|
|
|
self.geometryUtils = GeometryUtils()
|
2026-01-14 11:37:35 +08:00
|
|
|
|
self._transformer_cache = {}
|
2026-02-26 11:00:33 +08:00
|
|
|
|
|
|
|
|
|
|
def compare_grids_by_coordinate(self,
|
|
|
|
|
|
grids_a: List[Dict[str, Any]],
|
|
|
|
|
|
grids_b: List[Dict[str, Any]]
|
|
|
|
|
|
) -> EarthworkComparisonResult:
|
|
|
|
|
|
"""按坐标匹配比较网格数据"""
|
|
|
|
|
|
# 使用字典按坐标快速查找网格A
|
|
|
|
|
|
grids_a_by_coord = {}
|
|
|
|
|
|
for cell in grids_a:
|
|
|
|
|
|
key = (round(cell.get('x_min', 0), 6), round(cell.get('y_min', 0), 6))
|
|
|
|
|
|
grids_a_by_coord[key] = cell
|
|
|
|
|
|
|
|
|
|
|
|
grid_differences_list = []
|
|
|
|
|
|
total_cut_volume = 0.0
|
|
|
|
|
|
total_fill_volume = 0.0
|
|
|
|
|
|
total_net_volume = 0.0
|
|
|
|
|
|
|
|
|
|
|
|
# 收集有效网格的差异数据用于统计
|
|
|
|
|
|
valid_cut_diffs = []
|
|
|
|
|
|
valid_fill_diffs = []
|
|
|
|
|
|
valid_net_diffs = []
|
|
|
|
|
|
valid_elevation_diffs = []
|
|
|
|
|
|
|
|
|
|
|
|
matched_count = 0
|
|
|
|
|
|
unmatched_count = 0
|
|
|
|
|
|
|
|
|
|
|
|
# 遍历网格B,查找匹配的网格A
|
|
|
|
|
|
for cell_b in grids_b:
|
|
|
|
|
|
key = (round(cell_b.get('x_min', 0), 6), round(cell_b.get('y_min', 0), 6))
|
|
|
|
|
|
|
|
|
|
|
|
if key in grids_a_by_coord:
|
|
|
|
|
|
cell_a = grids_a_by_coord[key]
|
|
|
|
|
|
matched_count += 1
|
|
|
|
|
|
|
|
|
|
|
|
# 计算各项差值
|
|
|
|
|
|
cut_volume_diff = cell_b.get('cut_volume', 0) - cell_a.get('cut_volume', 0)
|
|
|
|
|
|
fill_volume_diff = cell_b.get('fill_volume', 0) - cell_a.get('fill_volume', 0)
|
|
|
|
|
|
net_volume_diff = cell_b.get('net_volume', 0) - cell_a.get('net_volume', 0)
|
|
|
|
|
|
|
|
|
|
|
|
# 计算平均高程差值
|
|
|
|
|
|
avg_elevation_a = cell_a.get('avg_elevation', np.nan)
|
|
|
|
|
|
avg_elevation_b = cell_b.get('avg_elevation', np.nan)
|
|
|
|
|
|
avg_elevation_diff = 0.0
|
|
|
|
|
|
if not np.isnan(avg_elevation_a) and not np.isnan(avg_elevation_b):
|
|
|
|
|
|
avg_elevation_diff = avg_elevation_b - avg_elevation_a
|
|
|
|
|
|
|
|
|
|
|
|
# 计算高程差变化
|
|
|
|
|
|
height_diff_a = cell_a.get('height_diff', np.nan)
|
|
|
|
|
|
height_diff_b = cell_b.get('height_diff', np.nan)
|
|
|
|
|
|
height_diff_change = 0.0
|
|
|
|
|
|
if not np.isnan(height_diff_a) and not np.isnan(height_diff_b):
|
|
|
|
|
|
height_diff_change = height_diff_b - height_diff_a
|
|
|
|
|
|
|
|
|
|
|
|
# 检查单元是否有效
|
|
|
|
|
|
is_valid_a = cell_a.get('is_valid', False)
|
|
|
|
|
|
is_valid_b = cell_b.get('is_valid', False)
|
|
|
|
|
|
is_valid = is_valid_a and is_valid_b
|
|
|
|
|
|
|
|
|
|
|
|
# 创建差异单元对象
|
|
|
|
|
|
diff_cell = GridCellDifference(
|
|
|
|
|
|
i=cell_b.get('i', 0),
|
|
|
|
|
|
j=cell_b.get('j', 0),
|
|
|
|
|
|
x_min=cell_b.get('x_min', 0),
|
|
|
|
|
|
x_max=cell_b.get('x_max', 0),
|
|
|
|
|
|
y_min=cell_b.get('y_min', 0),
|
|
|
|
|
|
y_max=cell_b.get('y_max', 0),
|
|
|
|
|
|
cut_volume=cut_volume_diff,
|
|
|
|
|
|
fill_volume=fill_volume_diff,
|
|
|
|
|
|
net_volume=net_volume_diff,
|
|
|
|
|
|
avg_elevation=avg_elevation_diff,
|
|
|
|
|
|
height_diff=height_diff_change,
|
|
|
|
|
|
is_valid=is_valid
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
grid_differences_list.append(diff_cell.to_dict())
|
|
|
|
|
|
|
|
|
|
|
|
# 累加有效网格的总体差异
|
|
|
|
|
|
if is_valid:
|
|
|
|
|
|
total_cut_volume += cut_volume_diff
|
|
|
|
|
|
total_fill_volume += fill_volume_diff
|
|
|
|
|
|
total_net_volume += net_volume_diff
|
|
|
|
|
|
|
|
|
|
|
|
# 收集统计信息
|
|
|
|
|
|
valid_cut_diffs.append(cut_volume_diff)
|
|
|
|
|
|
valid_fill_diffs.append(fill_volume_diff)
|
|
|
|
|
|
valid_net_diffs.append(net_volume_diff)
|
|
|
|
|
|
valid_elevation_diffs.append(avg_elevation_diff)
|
|
|
|
|
|
else:
|
|
|
|
|
|
unmatched_count += 1
|
|
|
|
|
|
# 对于未匹配的网格B,创建一个无效的差异单元
|
|
|
|
|
|
grid_differences_list.append({
|
|
|
|
|
|
'i': cell_b.get('i', 0),
|
|
|
|
|
|
'j': cell_b.get('j', 0),
|
|
|
|
|
|
'x_min': cell_b.get('x_min', 0),
|
|
|
|
|
|
'x_max': cell_b.get('x_max', 0),
|
|
|
|
|
|
'y_min': cell_b.get('y_min', 0),
|
|
|
|
|
|
'y_max': cell_b.get('y_max', 0),
|
|
|
|
|
|
'cut_volume': 0.0,
|
|
|
|
|
|
'fill_volume': 0.0,
|
|
|
|
|
|
'net_volume': 0.0,
|
|
|
|
|
|
'avg_elevation': 0.0,
|
|
|
|
|
|
'height_diff': 0.0,
|
|
|
|
|
|
'is_valid': False
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
return EarthworkComparisonResult(
|
|
|
|
|
|
total_cut_volume=total_cut_volume,
|
|
|
|
|
|
total_fill_volume=total_fill_volume,
|
|
|
|
|
|
total_net_volume=total_net_volume,
|
|
|
|
|
|
grid_differences=grid_differences_list
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
def compare_grids_by_index(self,
|
|
|
|
|
|
grids_a: List[Dict[str, Any]],
|
|
|
|
|
|
grids_b: List[Dict[str, Any]]
|
|
|
|
|
|
) -> EarthworkComparisonResult:
|
|
|
|
|
|
"""按索引顺序比较网格数据"""
|
|
|
|
|
|
grid_differences_list = []
|
|
|
|
|
|
total_cut_volume = 0.0
|
|
|
|
|
|
total_fill_volume = 0.0
|
|
|
|
|
|
total_net_volume = 0.0
|
|
|
|
|
|
|
|
|
|
|
|
# 收集有效网格的差异数据用于统计
|
|
|
|
|
|
valid_cut_diffs = []
|
|
|
|
|
|
valid_fill_diffs = []
|
|
|
|
|
|
valid_net_diffs = []
|
|
|
|
|
|
valid_elevation_diffs = []
|
|
|
|
|
|
|
|
|
|
|
|
for i in range(len(grids_a)):
|
|
|
|
|
|
cell_a = grids_a[i]
|
|
|
|
|
|
cell_b = grids_b[i]
|
|
|
|
|
|
|
|
|
|
|
|
# 检查是否是同一个网格单元
|
|
|
|
|
|
if cell_a.get('i') != cell_b.get('i') or cell_a.get('j') != cell_b.get('j'):
|
|
|
|
|
|
print(f"警告: 索引{i}的网格不匹配: A({cell_a.get('i')},{cell_a.get('j')}) vs B({cell_b.get('i')},{cell_b.get('j')})")
|
|
|
|
|
|
# 尝试通过坐标匹配
|
|
|
|
|
|
return self.compare_grids_by_coordinate(grids_a, grids_b)
|
|
|
|
|
|
|
|
|
|
|
|
# 计算各项差值
|
|
|
|
|
|
cut_volume_diff = cell_b.get('cut_volume', 0) - cell_a.get('cut_volume', 0)
|
|
|
|
|
|
fill_volume_diff = cell_b.get('fill_volume', 0) - cell_a.get('fill_volume', 0)
|
|
|
|
|
|
net_volume_diff = cell_b.get('net_volume', 0) - cell_a.get('net_volume', 0)
|
|
|
|
|
|
|
|
|
|
|
|
# 计算平均高程差值
|
|
|
|
|
|
avg_elevation_a = cell_a.get('avg_elevation', np.nan)
|
|
|
|
|
|
avg_elevation_b = cell_b.get('avg_elevation', np.nan)
|
|
|
|
|
|
avg_elevation_diff = 0.0
|
|
|
|
|
|
if not np.isnan(avg_elevation_a) and not np.isnan(avg_elevation_b):
|
|
|
|
|
|
avg_elevation_diff = avg_elevation_b - avg_elevation_a
|
|
|
|
|
|
|
|
|
|
|
|
# 计算高程差变化
|
|
|
|
|
|
height_diff_a = cell_a.get('height_diff', np.nan)
|
|
|
|
|
|
height_diff_b = cell_b.get('height_diff', np.nan)
|
|
|
|
|
|
height_diff_change = 0.0
|
|
|
|
|
|
if not np.isnan(height_diff_a) and not np.isnan(height_diff_b):
|
|
|
|
|
|
height_diff_change = height_diff_b - height_diff_a
|
|
|
|
|
|
|
|
|
|
|
|
# 检查单元是否有效
|
|
|
|
|
|
is_valid_a = cell_a.get('is_valid', False)
|
|
|
|
|
|
is_valid_b = cell_b.get('is_valid', False)
|
|
|
|
|
|
is_valid = is_valid_a and is_valid_b
|
|
|
|
|
|
|
|
|
|
|
|
# 创建差异单元对象
|
|
|
|
|
|
diff_cell = GridCellDifference(
|
|
|
|
|
|
i=cell_a.get('i', 0),
|
|
|
|
|
|
j=cell_a.get('j', 0),
|
|
|
|
|
|
x_min=cell_a.get('x_min', 0),
|
|
|
|
|
|
x_max=cell_a.get('x_max', 0),
|
|
|
|
|
|
y_min=cell_a.get('y_min', 0),
|
|
|
|
|
|
y_max=cell_a.get('y_max', 0),
|
|
|
|
|
|
cut_volume=cut_volume_diff,
|
|
|
|
|
|
fill_volume=fill_volume_diff,
|
|
|
|
|
|
net_volume=net_volume_diff,
|
|
|
|
|
|
avg_elevation=avg_elevation_diff,
|
|
|
|
|
|
height_diff=height_diff_change,
|
|
|
|
|
|
is_valid=is_valid
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
grid_differences_list.append(diff_cell.to_dict())
|
|
|
|
|
|
|
|
|
|
|
|
# 累加有效网格的总体差异
|
|
|
|
|
|
if is_valid:
|
|
|
|
|
|
total_cut_volume += cut_volume_diff
|
|
|
|
|
|
total_fill_volume += fill_volume_diff
|
|
|
|
|
|
total_net_volume += net_volume_diff
|
|
|
|
|
|
|
|
|
|
|
|
# 收集统计信息
|
|
|
|
|
|
valid_cut_diffs.append(cut_volume_diff)
|
|
|
|
|
|
valid_fill_diffs.append(fill_volume_diff)
|
|
|
|
|
|
valid_net_diffs.append(net_volume_diff)
|
|
|
|
|
|
valid_elevation_diffs.append(avg_elevation_diff)
|
|
|
|
|
|
|
|
|
|
|
|
return EarthworkComparisonResult(
|
|
|
|
|
|
total_cut_volume=total_cut_volume,
|
|
|
|
|
|
total_fill_volume=total_fill_volume,
|
|
|
|
|
|
total_net_volume=total_net_volume,
|
|
|
|
|
|
grid_differences=grid_differences_list
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
def compare_grid_cells( self,
|
|
|
|
|
|
grids_a: List[Dict[str, Any]],
|
|
|
|
|
|
grids_b: List[Dict[str, Any]]
|
|
|
|
|
|
) -> EarthworkComparisonResult:
|
|
|
|
|
|
"""
|
|
|
|
|
|
比较两个网格数据集合
|
2026-01-14 11:37:35 +08:00
|
|
|
|
|
2026-02-26 11:00:33 +08:00
|
|
|
|
Args:
|
|
|
|
|
|
grids_a: A结果的网格数据
|
|
|
|
|
|
grids_b: B结果的网格数据
|
|
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
|
比较结果对象
|
|
|
|
|
|
"""
|
|
|
|
|
|
if not grids_a or not grids_b:
|
|
|
|
|
|
raise ValueError("网格数据不能为空")
|
|
|
|
|
|
|
|
|
|
|
|
if len(grids_a) != len(grids_b):
|
|
|
|
|
|
# 如果网格数量不同,尝试按坐标匹配
|
|
|
|
|
|
print(f"警告: 网格数量不同 (A={len(grids_a)}, B={len(grids_b)}),尝试按坐标匹配")
|
|
|
|
|
|
return self.compare_grids_by_coordinate(grids_a, grids_b)
|
|
|
|
|
|
|
|
|
|
|
|
# 网格数量相同,按顺序比较
|
|
|
|
|
|
return self.compare_grids_by_index(grids_a, grids_b)
|
|
|
|
|
|
|
2026-01-14 11:37:35 +08:00
|
|
|
|
async def calculate(self,
|
|
|
|
|
|
polygon_coords: List[List[float]],
|
|
|
|
|
|
design_elevation: float,
|
|
|
|
|
|
algorithm: AlgorithmType = AlgorithmType.TIN,
|
|
|
|
|
|
resolution: float = 1.0,
|
|
|
|
|
|
target_crs: str = "EPSG:4326",
|
|
|
|
|
|
interpolation_method: str = 'linear') -> EarthworkResult3dTiles:
|
|
|
|
|
|
"""
|
|
|
|
|
|
计算土方量
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
polygon_coords: 多边形坐标
|
|
|
|
|
|
design_elevation: 设计高程
|
|
|
|
|
|
algorithm: 计算算法
|
2026-01-29 11:51:20 +08:00
|
|
|
|
resolution: 格网分辨率(米)
|
2026-01-14 11:37:35 +08:00
|
|
|
|
target_crs: 目标坐标系
|
|
|
|
|
|
interpolation_method: 插值方法
|
|
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
|
EarthworkResult: 计算结果
|
|
|
|
|
|
"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
# 1. 获取数据
|
|
|
|
|
|
points = await self.data_source.get_points_in_polygon(polygon_coords)
|
|
|
|
|
|
|
|
|
|
|
|
if points.size == 0:
|
2026-02-26 11:00:33 +08:00
|
|
|
|
# raise ValueError("区域内没有找到顶点数据")
|
|
|
|
|
|
raise ValueError("区域太小,请调整区域")
|
2026-01-14 11:37:35 +08:00
|
|
|
|
|
|
|
|
|
|
# 2. 坐标转换
|
|
|
|
|
|
points = await self._transform_coordinates(points, target_crs)
|
|
|
|
|
|
|
|
|
|
|
|
# 3. 执行计算
|
|
|
|
|
|
if algorithm == AlgorithmType.GRID:
|
|
|
|
|
|
result = await self._calculate_grid(points, polygon_coords,
|
|
|
|
|
|
design_elevation, resolution,
|
|
|
|
|
|
interpolation_method)
|
|
|
|
|
|
elif algorithm == AlgorithmType.TIN:
|
|
|
|
|
|
result = await self._calculate_tin(points, polygon_coords,
|
|
|
|
|
|
design_elevation)
|
|
|
|
|
|
elif algorithm == AlgorithmType.PRISM:
|
|
|
|
|
|
result = await self._calculate_prism(points, polygon_coords,
|
|
|
|
|
|
design_elevation, resolution)
|
|
|
|
|
|
else:
|
|
|
|
|
|
raise ValueError(f"不支持的算法: {algorithm}")
|
|
|
|
|
|
|
|
|
|
|
|
return result
|
|
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.error(f"土方量计算失败: {str(e)}")
|
|
|
|
|
|
raise
|
|
|
|
|
|
|
|
|
|
|
|
async def _transform_coordinates(self, points: np.ndarray,
|
|
|
|
|
|
target_crs: str) -> np.ndarray:
|
|
|
|
|
|
"""坐标转换"""
|
|
|
|
|
|
source_crs = self.data_source.get_crs()
|
|
|
|
|
|
|
|
|
|
|
|
if source_crs == target_crs:
|
|
|
|
|
|
return points
|
|
|
|
|
|
|
|
|
|
|
|
cache_key = f"{source_crs}->{target_crs}"
|
|
|
|
|
|
if cache_key not in self._transformer_cache:
|
|
|
|
|
|
self._transformer_cache[cache_key] = Transformer.from_crs(
|
|
|
|
|
|
CRS.from_string(source_crs),
|
|
|
|
|
|
CRS.from_string(target_crs),
|
|
|
|
|
|
always_xy=True
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
transformer = self._transformer_cache[cache_key]
|
|
|
|
|
|
points_2d = transformer.transform(points[:, 0], points[:, 1])
|
|
|
|
|
|
|
|
|
|
|
|
return np.column_stack([points_2d[0], points_2d[1], points[:, 2]])
|
|
|
|
|
|
|
|
|
|
|
|
async def _calculate_grid(self, points: np.ndarray,
|
2026-02-26 11:00:33 +08:00
|
|
|
|
polygon_coords: List[List[float]],
|
|
|
|
|
|
design_elevation: float,
|
|
|
|
|
|
resolution: float,
|
|
|
|
|
|
interpolation_method: str) -> EarthworkResult3dTiles:
|
|
|
|
|
|
"""格网法计算 - 包含网格单元数据"""
|
2026-01-14 11:37:35 +08:00
|
|
|
|
polygon_np = np.array(polygon_coords)
|
|
|
|
|
|
|
|
|
|
|
|
# 创建格网
|
2026-02-26 11:00:33 +08:00
|
|
|
|
xx, yy, _ = self.geometryUtils.create_grid(polygon_np, resolution)
|
|
|
|
|
|
x_grid = xx[0, :] # 从xx矩阵提取x网格线
|
|
|
|
|
|
y_grid = yy[:, 0] # 从yy矩阵提取y网格线
|
2026-01-14 11:37:35 +08:00
|
|
|
|
|
|
|
|
|
|
# 插值
|
2026-01-29 11:51:20 +08:00
|
|
|
|
natural_elevations = self.geometryUtils.interpolate_grid(xx, yy, points, interpolation_method)
|
2026-01-14 11:37:35 +08:00
|
|
|
|
|
2026-02-26 11:00:33 +08:00
|
|
|
|
# 初始化统计信息
|
|
|
|
|
|
cut_volume_total = 0.0
|
|
|
|
|
|
fill_volume_total = 0.0
|
2026-01-14 11:37:35 +08:00
|
|
|
|
total_area = 0.0
|
2026-02-26 11:00:33 +08:00
|
|
|
|
grid_cells: List[Dict[str, Any]] = []
|
|
|
|
|
|
valid_grid_count = 0
|
|
|
|
|
|
total_grid_count = (len(x_grid) - 1) * (len(y_grid) - 1)
|
|
|
|
|
|
|
|
|
|
|
|
# 收集高程和体积用于统计
|
|
|
|
|
|
valid_elevations = []
|
|
|
|
|
|
cut_volumes_list = []
|
|
|
|
|
|
fill_volumes_list = []
|
2026-01-14 11:37:35 +08:00
|
|
|
|
|
|
|
|
|
|
# 遍历每个格网单元
|
|
|
|
|
|
for i in range(len(x_grid) - 1):
|
|
|
|
|
|
for j in range(len(y_grid) - 1):
|
|
|
|
|
|
# 格网四个角点
|
2026-02-26 11:00:33 +08:00
|
|
|
|
x_min, x_max = x_grid[i], x_grid[i+1]
|
|
|
|
|
|
y_min, y_max = y_grid[j], y_grid[j+1]
|
|
|
|
|
|
|
2026-01-14 11:37:35 +08:00
|
|
|
|
cell_corners = np.array([
|
2026-02-26 11:00:33 +08:00
|
|
|
|
[x_min, y_min],
|
|
|
|
|
|
[x_max, y_min],
|
|
|
|
|
|
[x_max, y_max],
|
|
|
|
|
|
[x_min, y_max]
|
2026-01-14 11:37:35 +08:00
|
|
|
|
])
|
|
|
|
|
|
|
|
|
|
|
|
# 检查格网中心点是否在多边形内
|
|
|
|
|
|
cell_center = cell_corners.mean(axis=0)
|
2026-02-26 11:00:33 +08:00
|
|
|
|
is_in_polygon = self.geometryUtils.is_point_in_polygon(cell_center, polygon_np)
|
2026-01-14 11:37:35 +08:00
|
|
|
|
|
|
|
|
|
|
# 获取格网四个角点的高程
|
2026-02-26 11:00:33 +08:00
|
|
|
|
corner_elevations = [
|
|
|
|
|
|
float(natural_elevations[j, i]),
|
|
|
|
|
|
float(natural_elevations[j, i+1]),
|
|
|
|
|
|
float(natural_elevations[j+1, i+1]),
|
|
|
|
|
|
float(natural_elevations[j+1, i])
|
2026-01-14 11:37:35 +08:00
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
# 检查是否有无效数据
|
2026-02-26 11:00:33 +08:00
|
|
|
|
has_valid_data = not any(np.isnan(elev) for elev in corner_elevations)
|
2026-01-14 11:37:35 +08:00
|
|
|
|
|
2026-02-26 11:00:33 +08:00
|
|
|
|
if is_in_polygon and has_valid_data:
|
|
|
|
|
|
# 计算格网平均高程
|
|
|
|
|
|
avg_elevation = np.mean(corner_elevations)
|
|
|
|
|
|
cell_area = resolution * resolution
|
|
|
|
|
|
total_area += cell_area
|
|
|
|
|
|
|
|
|
|
|
|
# 计算挖填量
|
|
|
|
|
|
height_diff = design_elevation - avg_elevation
|
|
|
|
|
|
|
|
|
|
|
|
if height_diff > 0:
|
|
|
|
|
|
fill_volume = height_diff * cell_area
|
|
|
|
|
|
cut_volume = 0.0
|
|
|
|
|
|
fill_volume_total += fill_volume
|
|
|
|
|
|
fill_volumes_list.append(fill_volume)
|
|
|
|
|
|
else:
|
|
|
|
|
|
cut_volume = abs(height_diff) * cell_area
|
|
|
|
|
|
fill_volume = 0.0
|
|
|
|
|
|
cut_volume_total += cut_volume
|
|
|
|
|
|
cut_volumes_list.append(cut_volume)
|
|
|
|
|
|
|
|
|
|
|
|
net_volume = cut_volume - fill_volume
|
|
|
|
|
|
|
|
|
|
|
|
# 收集统计信息
|
|
|
|
|
|
valid_elevations.append(avg_elevation)
|
|
|
|
|
|
valid_grid_count += 1
|
|
|
|
|
|
|
|
|
|
|
|
# 创建网格单元数据对象
|
|
|
|
|
|
grid_cell = {
|
|
|
|
|
|
'i': i,
|
|
|
|
|
|
'j': j,
|
|
|
|
|
|
'x_min': x_min,
|
|
|
|
|
|
'x_max': x_max,
|
|
|
|
|
|
'y_min': y_min,
|
|
|
|
|
|
'y_max': y_max,
|
|
|
|
|
|
'cut_volume': float(cut_volume),
|
|
|
|
|
|
'fill_volume': float(fill_volume),
|
|
|
|
|
|
'net_volume': float(net_volume),
|
|
|
|
|
|
'avg_elevation': float(avg_elevation),
|
|
|
|
|
|
'design_elevation': design_elevation,
|
|
|
|
|
|
'height_diff': float(height_diff),
|
|
|
|
|
|
'area': cell_area,
|
|
|
|
|
|
'is_valid': True,
|
|
|
|
|
|
'corner_elevations': corner_elevations
|
|
|
|
|
|
}
|
2026-01-14 11:37:35 +08:00
|
|
|
|
else:
|
2026-02-26 11:00:33 +08:00
|
|
|
|
# 创建无效网格单元数据对象
|
|
|
|
|
|
grid_cell = {
|
|
|
|
|
|
'i': i,
|
|
|
|
|
|
'j': j,
|
|
|
|
|
|
'x_min': x_min,
|
|
|
|
|
|
'x_max': x_max,
|
|
|
|
|
|
'y_min': y_min,
|
|
|
|
|
|
'y_max': y_max,
|
|
|
|
|
|
'cut_volume': 0.0,
|
|
|
|
|
|
'fill_volume': 0.0,
|
|
|
|
|
|
'net_volume': 0.0,
|
|
|
|
|
|
'avg_elevation': np.nan,
|
|
|
|
|
|
'design_elevation': design_elevation,
|
|
|
|
|
|
'height_diff': np.nan,
|
|
|
|
|
|
'area': resolution * resolution,
|
|
|
|
|
|
'is_valid': False,
|
|
|
|
|
|
'corner_elevations': corner_elevations
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
grid_cells.append(grid_cell)
|
2026-01-14 11:37:35 +08:00
|
|
|
|
|
|
|
|
|
|
# 计算统计信息
|
2026-01-29 11:51:20 +08:00
|
|
|
|
area = self.geometryUtils.calculate_polygon_area(polygon_coords)
|
2026-01-14 11:37:35 +08:00
|
|
|
|
mask = ~np.isnan(natural_elevations)
|
2026-02-26 11:00:33 +08:00
|
|
|
|
valid_elevations_grid = natural_elevations[mask]
|
|
|
|
|
|
|
|
|
|
|
|
# 计算高程统计信息
|
|
|
|
|
|
if valid_elevations:
|
|
|
|
|
|
elevation_stats = {
|
|
|
|
|
|
"std": float(np.std(valid_elevations)),
|
|
|
|
|
|
"median": float(np.median(valid_elevations)),
|
|
|
|
|
|
"q1": float(np.percentile(valid_elevations, 25)),
|
|
|
|
|
|
"q3": float(np.percentile(valid_elevations, 75))
|
|
|
|
|
|
}
|
|
|
|
|
|
else:
|
|
|
|
|
|
elevation_stats = {}
|
|
|
|
|
|
|
|
|
|
|
|
# 计算体积分布信息
|
|
|
|
|
|
volume_distribution = {}
|
|
|
|
|
|
if cut_volumes_list:
|
|
|
|
|
|
volume_distribution["cut"] = {
|
|
|
|
|
|
"max": float(np.max(cut_volumes_list)),
|
|
|
|
|
|
"min": float(np.min(cut_volumes_list)),
|
|
|
|
|
|
"mean": float(np.mean(cut_volumes_list)),
|
|
|
|
|
|
"total_cells": len(cut_volumes_list)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if fill_volumes_list:
|
|
|
|
|
|
volume_distribution["fill"] = {
|
|
|
|
|
|
"max": float(np.max(fill_volumes_list)),
|
|
|
|
|
|
"min": float(np.min(fill_volumes_list)),
|
|
|
|
|
|
"mean": float(np.mean(fill_volumes_list)),
|
|
|
|
|
|
"total_cells": len(fill_volumes_list)
|
|
|
|
|
|
}
|
2026-01-14 11:37:35 +08:00
|
|
|
|
|
|
|
|
|
|
return EarthworkResult3dTiles(
|
2026-02-26 11:00:33 +08:00
|
|
|
|
cut_volume=cut_volume_total,
|
|
|
|
|
|
fill_volume=fill_volume_total,
|
|
|
|
|
|
net_volume=cut_volume_total - fill_volume_total,
|
2026-01-14 11:37:35 +08:00
|
|
|
|
area=area,
|
2026-02-26 11:00:33 +08:00
|
|
|
|
avg_elevation=np.mean(valid_elevations_grid) if valid_elevations_grid.size > 0 else 0,
|
|
|
|
|
|
min_elevation=np.min(valid_elevations_grid) if valid_elevations_grid.size > 0 else 0,
|
|
|
|
|
|
max_elevation=np.max(valid_elevations_grid) if valid_elevations_grid.size > 0 else 0,
|
2026-01-14 11:37:35 +08:00
|
|
|
|
points_count=points.shape[0],
|
|
|
|
|
|
bounding_box={
|
|
|
|
|
|
"min": [x_grid[0], y_grid[0]],
|
|
|
|
|
|
"max": [x_grid[-1], y_grid[-1]]
|
|
|
|
|
|
},
|
|
|
|
|
|
volume_accuracy=self._calculate_accuracy(points, resolution),
|
|
|
|
|
|
algorithm=AlgorithmType.GRID.value,
|
2026-02-26 11:00:33 +08:00
|
|
|
|
resolution=resolution,
|
|
|
|
|
|
calculation_details=grid_cells,
|
|
|
|
|
|
details_type="grid_cells",
|
|
|
|
|
|
details_dimensions={
|
|
|
|
|
|
"rows": len(y_grid) - 1,
|
|
|
|
|
|
"cols": len(x_grid) - 1,
|
|
|
|
|
|
"cell_size": resolution
|
|
|
|
|
|
},
|
|
|
|
|
|
valid_unit_count=valid_grid_count,
|
|
|
|
|
|
total_unit_count=total_grid_count,
|
|
|
|
|
|
elevation_statistics=elevation_stats,
|
|
|
|
|
|
volume_distribution=volume_distribution
|
|
|
|
|
|
)
|
2026-01-14 11:37:35 +08:00
|
|
|
|
|
|
|
|
|
|
async def _calculate_tin(self, points: np.ndarray,
|
2026-02-26 11:00:33 +08:00
|
|
|
|
polygon_coords: List[List[float]],
|
|
|
|
|
|
design_elevation: float) -> EarthworkResult3dTiles:
|
|
|
|
|
|
"""三角网法计算 - 包含三角形单元数据"""
|
2026-01-14 11:37:35 +08:00
|
|
|
|
polygon_np = np.array(polygon_coords)
|
|
|
|
|
|
|
|
|
|
|
|
# 创建Delaunay三角网
|
|
|
|
|
|
triangulation = Delaunay(points[:, :2])
|
|
|
|
|
|
|
2026-02-26 11:00:33 +08:00
|
|
|
|
# 初始化统计信息
|
|
|
|
|
|
cut_volume_total = 0.0
|
|
|
|
|
|
fill_volume_total = 0.0
|
2026-01-14 11:37:35 +08:00
|
|
|
|
total_area = 0.0
|
2026-02-26 11:00:33 +08:00
|
|
|
|
triangles: List[Dict[str, Any]] = []
|
|
|
|
|
|
triangle_id = 0
|
|
|
|
|
|
valid_triangle_count = 0
|
|
|
|
|
|
total_triangle_count = len(triangulation.simplices)
|
|
|
|
|
|
|
|
|
|
|
|
# 收集高程和体积用于统计
|
|
|
|
|
|
valid_elevations = []
|
|
|
|
|
|
cut_volumes_list = []
|
|
|
|
|
|
fill_volumes_list = []
|
|
|
|
|
|
triangle_areas = []
|
2026-01-14 11:37:35 +08:00
|
|
|
|
|
2026-02-26 11:00:33 +08:00
|
|
|
|
# 筛选多边形内的三角形
|
2026-01-14 11:37:35 +08:00
|
|
|
|
for simplex in triangulation.simplices:
|
|
|
|
|
|
triangle_points = points[simplex]
|
|
|
|
|
|
triangle_center = triangle_points.mean(axis=0)[:2]
|
|
|
|
|
|
|
|
|
|
|
|
# 检查三角形中心是否在多边形内
|
2026-02-26 11:00:33 +08:00
|
|
|
|
is_in_polygon = self.geometryUtils.is_point_in_polygon(triangle_center, polygon_np)
|
2026-01-14 11:37:35 +08:00
|
|
|
|
|
2026-02-26 11:00:33 +08:00
|
|
|
|
if is_in_polygon:
|
|
|
|
|
|
# 计算三角形面积
|
|
|
|
|
|
area = self.geometryUtils.calculate_triangle_area(triangle_points[:, :2])
|
|
|
|
|
|
if math.isnan(area):
|
|
|
|
|
|
# 跳过无效的三角形
|
|
|
|
|
|
triangles.append({
|
|
|
|
|
|
'triangle_id': triangle_id,
|
|
|
|
|
|
'vertices': [tuple(point) for point in triangle_points],
|
|
|
|
|
|
'vertex_indices': simplex.tolist(),
|
|
|
|
|
|
'cut_volume': 0.0,
|
|
|
|
|
|
'fill_volume': 0.0,
|
|
|
|
|
|
'net_volume': 0.0,
|
|
|
|
|
|
'avg_elevation': float(triangle_points[:, 2].mean()),
|
|
|
|
|
|
'design_elevation': design_elevation,
|
|
|
|
|
|
'height_diff': np.nan,
|
|
|
|
|
|
'area': 0.0,
|
|
|
|
|
|
'is_valid': False,
|
|
|
|
|
|
'center_point': tuple(triangle_center)
|
|
|
|
|
|
})
|
|
|
|
|
|
triangle_id += 1
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
|
|
total_area += area
|
|
|
|
|
|
triangle_areas.append(area)
|
|
|
|
|
|
|
|
|
|
|
|
# 计算平均高程(使用三个顶点的高程)
|
|
|
|
|
|
avg_elevation = triangle_points[:, 2].mean()
|
|
|
|
|
|
height_diff = design_elevation - avg_elevation
|
|
|
|
|
|
|
|
|
|
|
|
# 计算挖填量
|
|
|
|
|
|
if height_diff > 0:
|
|
|
|
|
|
fill_volume = height_diff * area
|
|
|
|
|
|
cut_volume = 0.0
|
|
|
|
|
|
fill_volume_total += fill_volume
|
|
|
|
|
|
fill_volumes_list.append(fill_volume)
|
|
|
|
|
|
else:
|
|
|
|
|
|
cut_volume = abs(height_diff) * area
|
|
|
|
|
|
fill_volume = 0.0
|
|
|
|
|
|
cut_volume_total += cut_volume
|
|
|
|
|
|
cut_volumes_list.append(cut_volume)
|
|
|
|
|
|
|
|
|
|
|
|
net_volume = cut_volume - fill_volume
|
|
|
|
|
|
|
|
|
|
|
|
# 收集统计信息
|
|
|
|
|
|
valid_elevations.extend(triangle_points[:, 2].tolist())
|
|
|
|
|
|
valid_triangle_count += 1
|
|
|
|
|
|
|
|
|
|
|
|
# 创建三角形数据对象
|
|
|
|
|
|
triangle = {
|
|
|
|
|
|
'triangle_id': triangle_id,
|
|
|
|
|
|
'vertices': [tuple(point) for point in triangle_points],
|
|
|
|
|
|
'vertex_indices': simplex.tolist(),
|
|
|
|
|
|
'cut_volume': float(cut_volume),
|
|
|
|
|
|
'fill_volume': float(fill_volume),
|
|
|
|
|
|
'net_volume': float(net_volume),
|
|
|
|
|
|
'avg_elevation': float(avg_elevation),
|
|
|
|
|
|
'design_elevation': design_elevation,
|
|
|
|
|
|
'height_diff': float(height_diff),
|
|
|
|
|
|
'area': float(area),
|
|
|
|
|
|
'is_valid': True,
|
|
|
|
|
|
'center_point': tuple(triangle_center)
|
|
|
|
|
|
}
|
2026-01-14 11:37:35 +08:00
|
|
|
|
else:
|
2026-02-26 11:00:33 +08:00
|
|
|
|
# 创建无效三角形数据对象
|
|
|
|
|
|
area = self.geometryUtils.calculate_triangle_area(triangle_points[:, :2])
|
|
|
|
|
|
if math.isnan(area):
|
|
|
|
|
|
area = 0.0
|
|
|
|
|
|
|
|
|
|
|
|
triangle = {
|
|
|
|
|
|
'triangle_id': triangle_id,
|
|
|
|
|
|
'vertices': [tuple(point) for point in triangle_points],
|
|
|
|
|
|
'vertex_indices': simplex.tolist(),
|
|
|
|
|
|
'cut_volume': 0.0,
|
|
|
|
|
|
'fill_volume': 0.0,
|
|
|
|
|
|
'net_volume': 0.0,
|
|
|
|
|
|
'avg_elevation': float(triangle_points[:, 2].mean()),
|
|
|
|
|
|
'design_elevation': design_elevation,
|
|
|
|
|
|
'height_diff': np.nan,
|
|
|
|
|
|
'area': float(area),
|
|
|
|
|
|
'is_valid': False,
|
|
|
|
|
|
'center_point': tuple(triangle_center)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
triangles.append(triangle)
|
|
|
|
|
|
triangle_id += 1
|
|
|
|
|
|
|
2026-01-14 11:37:35 +08:00
|
|
|
|
# 计算统计信息
|
2026-01-29 11:51:20 +08:00
|
|
|
|
area = self.geometryUtils.calculate_polygon_area(polygon_coords)
|
2026-01-14 11:37:35 +08:00
|
|
|
|
|
2026-02-26 11:00:33 +08:00
|
|
|
|
# 计算高程统计信息
|
|
|
|
|
|
if valid_elevations:
|
|
|
|
|
|
elevation_stats = {
|
|
|
|
|
|
"std": float(np.std(valid_elevations)),
|
|
|
|
|
|
"median": float(np.median(valid_elevations)),
|
|
|
|
|
|
"q1": float(np.percentile(valid_elevations, 25)),
|
|
|
|
|
|
"q3": float(np.percentile(valid_elevations, 75))
|
|
|
|
|
|
}
|
|
|
|
|
|
else:
|
|
|
|
|
|
elevation_stats = {}
|
|
|
|
|
|
|
|
|
|
|
|
# 计算体积分布信息
|
|
|
|
|
|
volume_distribution = {}
|
|
|
|
|
|
if cut_volumes_list:
|
|
|
|
|
|
volume_distribution["cut"] = {
|
|
|
|
|
|
"max": float(np.max(cut_volumes_list)),
|
|
|
|
|
|
"min": float(np.min(cut_volumes_list)),
|
|
|
|
|
|
"mean": float(np.mean(cut_volumes_list)),
|
|
|
|
|
|
"total_triangles": len(cut_volumes_list)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if fill_volumes_list:
|
|
|
|
|
|
volume_distribution["fill"] = {
|
|
|
|
|
|
"max": float(np.max(fill_volumes_list)),
|
|
|
|
|
|
"min": float(np.min(fill_volumes_list)),
|
|
|
|
|
|
"mean": float(np.mean(fill_volumes_list)),
|
|
|
|
|
|
"total_triangles": len(fill_volumes_list)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
# 计算三角形面积统计
|
|
|
|
|
|
if triangle_areas:
|
|
|
|
|
|
volume_distribution["area"] = {
|
|
|
|
|
|
"max": float(np.max(triangle_areas)),
|
|
|
|
|
|
"min": float(np.min(triangle_areas)),
|
|
|
|
|
|
"mean": float(np.mean(triangle_areas)),
|
|
|
|
|
|
"total_area": float(np.sum(triangle_areas))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-14 11:37:35 +08:00
|
|
|
|
return EarthworkResult3dTiles(
|
2026-02-26 11:00:33 +08:00
|
|
|
|
cut_volume=cut_volume_total,
|
|
|
|
|
|
fill_volume=fill_volume_total,
|
|
|
|
|
|
net_volume=cut_volume_total - fill_volume_total,
|
2026-01-14 11:37:35 +08:00
|
|
|
|
area=area,
|
|
|
|
|
|
avg_elevation=points[:, 2].mean(),
|
|
|
|
|
|
min_elevation=points[:, 2].min(),
|
|
|
|
|
|
max_elevation=points[:, 2].max(),
|
|
|
|
|
|
points_count=points.shape[0],
|
|
|
|
|
|
bounding_box={
|
|
|
|
|
|
"min": points.min(axis=0)[:2].tolist(),
|
|
|
|
|
|
"max": points.max(axis=0)[:2].tolist()
|
|
|
|
|
|
},
|
|
|
|
|
|
volume_accuracy=self._calculate_accuracy(points, 0),
|
|
|
|
|
|
algorithm=AlgorithmType.TIN.value,
|
2026-02-26 11:00:33 +08:00
|
|
|
|
resolution=0,
|
|
|
|
|
|
calculation_details=triangles,
|
|
|
|
|
|
details_type="triangles",
|
|
|
|
|
|
details_dimensions={
|
|
|
|
|
|
"triangle_count": total_triangle_count,
|
|
|
|
|
|
"valid_triangle_count": valid_triangle_count,
|
|
|
|
|
|
"vertex_count": points.shape[0]
|
|
|
|
|
|
},
|
|
|
|
|
|
valid_unit_count=valid_triangle_count,
|
|
|
|
|
|
total_unit_count=total_triangle_count,
|
|
|
|
|
|
elevation_statistics=elevation_stats,
|
|
|
|
|
|
volume_distribution=volume_distribution
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-01-14 11:37:35 +08:00
|
|
|
|
async def _calculate_prism(self, points: np.ndarray,
|
2026-02-26 11:00:33 +08:00
|
|
|
|
polygon_coords: List[List[float]],
|
|
|
|
|
|
design_elevation: float,
|
|
|
|
|
|
resolution: float) -> EarthworkResult3dTiles:
|
|
|
|
|
|
"""三棱柱法计算 - 包含三棱柱单元数据"""
|
2026-01-14 11:37:35 +08:00
|
|
|
|
# 先创建TIN
|
|
|
|
|
|
polygon_np = np.array(polygon_coords)
|
|
|
|
|
|
triangulation = Delaunay(points[:, :2])
|
|
|
|
|
|
|
2026-02-26 11:00:33 +08:00
|
|
|
|
cut_volume_total = 0.0
|
|
|
|
|
|
fill_volume_total = 0.0
|
2026-01-14 11:37:35 +08:00
|
|
|
|
total_area = 0.0
|
2026-02-26 11:00:33 +08:00
|
|
|
|
prisms: List[Dict[str, Any]] = []
|
|
|
|
|
|
triangle_id = 0
|
|
|
|
|
|
valid_prism_count = 0
|
|
|
|
|
|
total_prism_count = len(triangulation.simplices)
|
|
|
|
|
|
|
|
|
|
|
|
# 收集统计信息
|
|
|
|
|
|
valid_elevations = []
|
|
|
|
|
|
cut_volumes_list = []
|
|
|
|
|
|
fill_volumes_list = []
|
|
|
|
|
|
triangle_areas = []
|
|
|
|
|
|
height_diffs_list = []
|
2026-01-14 11:37:35 +08:00
|
|
|
|
|
|
|
|
|
|
for simplex in triangulation.simplices:
|
|
|
|
|
|
triangle_points = points[simplex]
|
|
|
|
|
|
triangle_center = triangle_points.mean(axis=0)[:2]
|
|
|
|
|
|
|
2026-02-26 11:00:33 +08:00
|
|
|
|
is_in_polygon = self.geometryUtils.is_point_in_polygon(triangle_center, polygon_np)
|
2026-01-14 11:37:35 +08:00
|
|
|
|
|
2026-02-26 11:00:33 +08:00
|
|
|
|
if is_in_polygon:
|
|
|
|
|
|
# 计算三角形面积
|
|
|
|
|
|
area = self.geometryUtils.calculate_triangle_area(triangle_points[:, :2])
|
|
|
|
|
|
if math.isnan(area):
|
|
|
|
|
|
# 跳过无效的三角形
|
|
|
|
|
|
prisms.append({
|
|
|
|
|
|
'triangle_id': triangle_id,
|
|
|
|
|
|
'vertices': [tuple(point) for point in triangle_points],
|
|
|
|
|
|
'vertex_indices': simplex.tolist(),
|
|
|
|
|
|
'cut_volume': 0.0,
|
|
|
|
|
|
'fill_volume': 0.0,
|
|
|
|
|
|
'net_volume': 0.0,
|
|
|
|
|
|
'avg_elevation': float(triangle_points[:, 2].mean()),
|
|
|
|
|
|
'design_elevation': design_elevation,
|
|
|
|
|
|
'height_diffs': [float(design_elevation - triangle_points[i, 2]) for i in range(3)],
|
|
|
|
|
|
'area': 0.0,
|
|
|
|
|
|
'is_valid': False,
|
|
|
|
|
|
'center_point': tuple(triangle_center),
|
|
|
|
|
|
'edge_volumes': [0.0, 0.0, 0.0]
|
|
|
|
|
|
})
|
|
|
|
|
|
triangle_id += 1
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
|
|
total_area += area
|
|
|
|
|
|
triangle_areas.append(area)
|
|
|
|
|
|
avg_elevation = triangle_points[:, 2].mean()
|
2026-01-14 11:37:35 +08:00
|
|
|
|
|
2026-02-26 11:00:33 +08:00
|
|
|
|
# 计算三棱柱体积
|
|
|
|
|
|
triangle_cut_volume = 0.0
|
|
|
|
|
|
triangle_fill_volume = 0.0
|
|
|
|
|
|
edge_volumes = []
|
|
|
|
|
|
height_diffs = []
|
2026-01-14 11:37:35 +08:00
|
|
|
|
|
2026-02-26 11:00:33 +08:00
|
|
|
|
for i in range(3):
|
|
|
|
|
|
j = (i + 1) % 3
|
|
|
|
|
|
# 计算边的长度
|
|
|
|
|
|
edge_length = np.linalg.norm(triangle_points[i, :2] - triangle_points[j, :2])
|
|
|
|
|
|
|
|
|
|
|
|
# 计算边两端点的挖填高度
|
|
|
|
|
|
height_i = design_elevation - triangle_points[i, 2]
|
|
|
|
|
|
height_j = design_elevation - triangle_points[j, 2]
|
|
|
|
|
|
height_diffs.extend([float(height_i), float(height_j)])
|
|
|
|
|
|
height_diffs_list.extend([float(height_i), float(height_j)])
|
|
|
|
|
|
|
|
|
|
|
|
# 计算边的平均挖填高度
|
|
|
|
|
|
avg_height = (abs(height_i) + abs(height_j)) / 2
|
|
|
|
|
|
|
|
|
|
|
|
# 计算边的面积(假设边宽度为resolution)
|
|
|
|
|
|
edge_area = edge_length * resolution
|
|
|
|
|
|
edge_volume = avg_height * edge_area
|
|
|
|
|
|
edge_volumes.append(float(edge_volume))
|
|
|
|
|
|
|
|
|
|
|
|
if height_i > 0 or height_j > 0:
|
|
|
|
|
|
triangle_fill_volume += edge_volume
|
|
|
|
|
|
fill_volume_total += edge_volume
|
|
|
|
|
|
fill_volumes_list.append(edge_volume)
|
|
|
|
|
|
else:
|
|
|
|
|
|
triangle_cut_volume += edge_volume
|
|
|
|
|
|
cut_volume_total += edge_volume
|
|
|
|
|
|
cut_volumes_list.append(edge_volume)
|
2026-01-14 11:37:35 +08:00
|
|
|
|
|
2026-02-26 11:00:33 +08:00
|
|
|
|
# 去重高度差
|
|
|
|
|
|
unique_height_diffs = list(set([height_diffs[0], height_diffs[2], height_diffs[4]]))
|
2026-01-14 11:37:35 +08:00
|
|
|
|
|
2026-02-26 11:00:33 +08:00
|
|
|
|
# 收集统计信息
|
|
|
|
|
|
valid_elevations.extend(triangle_points[:, 2].tolist())
|
|
|
|
|
|
valid_prism_count += 1
|
|
|
|
|
|
|
|
|
|
|
|
# 创建三棱柱数据对象
|
|
|
|
|
|
prism = {
|
|
|
|
|
|
'triangle_id': triangle_id,
|
|
|
|
|
|
'vertices': [tuple(point) for point in triangle_points],
|
|
|
|
|
|
'vertex_indices': simplex.tolist(),
|
|
|
|
|
|
'cut_volume': float(triangle_cut_volume),
|
|
|
|
|
|
'fill_volume': float(triangle_fill_volume),
|
|
|
|
|
|
'net_volume': float(triangle_cut_volume - triangle_fill_volume),
|
|
|
|
|
|
'avg_elevation': float(avg_elevation),
|
|
|
|
|
|
'design_elevation': design_elevation,
|
|
|
|
|
|
'height_diffs': unique_height_diffs,
|
|
|
|
|
|
'area': float(area),
|
|
|
|
|
|
'is_valid': True,
|
|
|
|
|
|
'center_point': tuple(triangle_center),
|
|
|
|
|
|
'edge_volumes': edge_volumes
|
|
|
|
|
|
}
|
|
|
|
|
|
else:
|
|
|
|
|
|
# 创建无效三棱柱数据对象
|
|
|
|
|
|
area = self.geometryUtils.calculate_triangle_area(triangle_points[:, :2])
|
|
|
|
|
|
if math.isnan(area):
|
|
|
|
|
|
area = 0.0
|
|
|
|
|
|
|
|
|
|
|
|
# 计算高度差
|
|
|
|
|
|
height_diffs = []
|
|
|
|
|
|
for i in range(3):
|
|
|
|
|
|
height_i = design_elevation - triangle_points[i, 2]
|
|
|
|
|
|
height_diffs.append(float(height_i))
|
|
|
|
|
|
|
|
|
|
|
|
prism = {
|
|
|
|
|
|
'triangle_id': triangle_id,
|
|
|
|
|
|
'vertices': [tuple(point) for point in triangle_points],
|
|
|
|
|
|
'vertex_indices': simplex.tolist(),
|
|
|
|
|
|
'cut_volume': 0.0,
|
|
|
|
|
|
'fill_volume': 0.0,
|
|
|
|
|
|
'net_volume': 0.0,
|
|
|
|
|
|
'avg_elevation': float(triangle_points[:, 2].mean()),
|
|
|
|
|
|
'design_elevation': design_elevation,
|
|
|
|
|
|
'height_diffs': height_diffs,
|
|
|
|
|
|
'area': float(area),
|
|
|
|
|
|
'is_valid': False,
|
|
|
|
|
|
'center_point': tuple(triangle_center),
|
|
|
|
|
|
'edge_volumes': [0.0, 0.0, 0.0]
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
prisms.append(prism)
|
|
|
|
|
|
triangle_id += 1
|
2026-01-14 11:37:35 +08:00
|
|
|
|
|
2026-01-29 11:51:20 +08:00
|
|
|
|
area = self.geometryUtils.calculate_polygon_area(polygon_coords)
|
2026-01-14 11:37:35 +08:00
|
|
|
|
|
2026-02-26 11:00:33 +08:00
|
|
|
|
# 计算高程统计信息
|
|
|
|
|
|
if valid_elevations:
|
|
|
|
|
|
elevation_stats = {
|
|
|
|
|
|
"std": float(np.std(valid_elevations)),
|
|
|
|
|
|
"median": float(np.median(valid_elevations)),
|
|
|
|
|
|
"q1": float(np.percentile(valid_elevations, 25)),
|
|
|
|
|
|
"q3": float(np.percentile(valid_elevations, 75))
|
|
|
|
|
|
}
|
|
|
|
|
|
else:
|
|
|
|
|
|
elevation_stats = {}
|
|
|
|
|
|
|
|
|
|
|
|
# 计算高度差统计信息
|
|
|
|
|
|
if height_diffs_list:
|
|
|
|
|
|
elevation_stats["height_diff"] = {
|
|
|
|
|
|
"max": float(np.max(height_diffs_list)),
|
|
|
|
|
|
"min": float(np.min(height_diffs_list)),
|
|
|
|
|
|
"mean": float(np.mean(height_diffs_list)),
|
|
|
|
|
|
"std": float(np.std(height_diffs_list))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
# 计算体积分布信息
|
|
|
|
|
|
volume_distribution = {}
|
|
|
|
|
|
if cut_volumes_list:
|
|
|
|
|
|
volume_distribution["cut"] = {
|
|
|
|
|
|
"max": float(np.max(cut_volumes_list)),
|
|
|
|
|
|
"min": float(np.min(cut_volumes_list)),
|
|
|
|
|
|
"mean": float(np.mean(cut_volumes_list)),
|
|
|
|
|
|
"total_edges": len(cut_volumes_list)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if fill_volumes_list:
|
|
|
|
|
|
volume_distribution["fill"] = {
|
|
|
|
|
|
"max": float(np.max(fill_volumes_list)),
|
|
|
|
|
|
"min": float(np.min(fill_volumes_list)),
|
|
|
|
|
|
"mean": float(np.mean(fill_volumes_list)),
|
|
|
|
|
|
"total_edges": len(fill_volumes_list)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
# 计算三角形面积统计
|
|
|
|
|
|
if triangle_areas:
|
|
|
|
|
|
volume_distribution["area"] = {
|
|
|
|
|
|
"max": float(np.max(triangle_areas)),
|
|
|
|
|
|
"min": float(np.min(triangle_areas)),
|
|
|
|
|
|
"mean": float(np.mean(triangle_areas)),
|
|
|
|
|
|
"total_area": float(np.sum(triangle_areas))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-14 11:37:35 +08:00
|
|
|
|
return EarthworkResult3dTiles(
|
2026-02-26 11:00:33 +08:00
|
|
|
|
cut_volume=cut_volume_total,
|
|
|
|
|
|
fill_volume=fill_volume_total,
|
|
|
|
|
|
net_volume=cut_volume_total - fill_volume_total,
|
2026-01-14 11:37:35 +08:00
|
|
|
|
area=area,
|
|
|
|
|
|
avg_elevation=points[:, 2].mean(),
|
|
|
|
|
|
min_elevation=points[:, 2].min(),
|
|
|
|
|
|
max_elevation=points[:, 2].max(),
|
|
|
|
|
|
points_count=points.shape[0],
|
|
|
|
|
|
bounding_box={
|
|
|
|
|
|
"min": points.min(axis=0)[:2].tolist(),
|
|
|
|
|
|
"max": points.max(axis=0)[:2].tolist()
|
|
|
|
|
|
},
|
|
|
|
|
|
volume_accuracy=self._calculate_accuracy(points, resolution),
|
|
|
|
|
|
algorithm=AlgorithmType.PRISM.value,
|
2026-02-26 11:00:33 +08:00
|
|
|
|
resolution=resolution,
|
|
|
|
|
|
calculation_details=prisms,
|
|
|
|
|
|
details_type="prisms",
|
|
|
|
|
|
details_dimensions={
|
|
|
|
|
|
"prism_count": total_prism_count,
|
|
|
|
|
|
"valid_prism_count": valid_prism_count,
|
|
|
|
|
|
"vertex_count": points.shape[0],
|
|
|
|
|
|
"edge_resolution": resolution
|
|
|
|
|
|
},
|
|
|
|
|
|
valid_unit_count=valid_prism_count,
|
|
|
|
|
|
total_unit_count=total_prism_count,
|
|
|
|
|
|
elevation_statistics=elevation_stats,
|
|
|
|
|
|
volume_distribution=volume_distribution
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-01-14 11:37:35 +08:00
|
|
|
|
def _calculate_accuracy(self, points: np.ndarray, resolution: float) -> float:
|
|
|
|
|
|
"""计算精度评估"""
|
|
|
|
|
|
if points.shape[0] < 10:
|
|
|
|
|
|
return 0.5
|
|
|
|
|
|
|
|
|
|
|
|
# 基于点密度和分辨率估算精度
|
|
|
|
|
|
if points.shape[0] > 0:
|
|
|
|
|
|
# 计算点密度
|
|
|
|
|
|
bounds = points[:, :2]
|
|
|
|
|
|
area = (bounds[:, 0].max() - bounds[:, 0].min()) * \
|
|
|
|
|
|
(bounds[:, 1].max() - bounds[:, 1].min())
|
|
|
|
|
|
|
|
|
|
|
|
if area > 0:
|
|
|
|
|
|
point_density = points.shape[0] / area
|
|
|
|
|
|
|
|
|
|
|
|
if point_density > 100 and resolution <= 0.5: # 高密度,高分辨率
|
|
|
|
|
|
return 0.05
|
|
|
|
|
|
elif point_density > 10 and resolution <= 1.0:
|
|
|
|
|
|
return 0.1
|
|
|
|
|
|
elif point_density > 1:
|
|
|
|
|
|
return 0.2
|
|
|
|
|
|
|
|
|
|
|
|
return 0.3
|
|
|
|
|
|
|
|
|
|
|
|
async def validate(self, polygon_coords: List[List[float]]) -> Dict:
|
|
|
|
|
|
"""验证计算参数"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
points = await self.data_source.get_points_in_polygon(polygon_coords)
|
|
|
|
|
|
|
|
|
|
|
|
validation_result = {
|
|
|
|
|
|
"polygon_valid": len(polygon_coords) >= 3,
|
2026-01-29 11:51:20 +08:00
|
|
|
|
"area": self.geometryUtils.calculate_polygon_area(polygon_coords),
|
2026-01-14 11:37:35 +08:00
|
|
|
|
"points_available": points.size > 0,
|
|
|
|
|
|
"points_count": points.shape[0] if points.size > 0 else 0,
|
|
|
|
|
|
"data_quality": "good" if points.shape[0] > 100 else "poor",
|
|
|
|
|
|
"suggested_algorithm": self._suggest_algorithm(points),
|
|
|
|
|
|
"estimated_accuracy": self._calculate_accuracy(points, 1.0) if points.size > 0 else None
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if points.size > 0:
|
|
|
|
|
|
validation_result.update({
|
|
|
|
|
|
"elevation_range": {
|
|
|
|
|
|
"min": float(points[:, 2].min()),
|
|
|
|
|
|
"max": float(points[:, 2].max()),
|
|
|
|
|
|
"avg": float(points[:, 2].mean())
|
|
|
|
|
|
},
|
|
|
|
|
|
"bounding_box": {
|
|
|
|
|
|
"min": points.min(axis=0).tolist(),
|
|
|
|
|
|
"max": points.max(axis=0).tolist()
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
return validation_result
|
|
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.error(f"验证失败: {str(e)}")
|
|
|
|
|
|
return {
|
|
|
|
|
|
"polygon_valid": False,
|
|
|
|
|
|
"error": str(e)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
def _suggest_algorithm(self, points: np.ndarray) -> str:
|
|
|
|
|
|
"""建议计算算法"""
|
|
|
|
|
|
if points.shape[0] < 100:
|
|
|
|
|
|
return AlgorithmType.TIN.value
|
|
|
|
|
|
|
|
|
|
|
|
# 计算点的规则性
|
|
|
|
|
|
bounds = points[:, :2]
|
|
|
|
|
|
x_range = bounds[:, 0].max() - bounds[:, 0].min()
|
|
|
|
|
|
y_range = bounds[:, 1].max() - bounds[:, 1].min()
|
|
|
|
|
|
|
|
|
|
|
|
# 如果点分布相对规则,建议使用格网法
|
|
|
|
|
|
if x_range > 0 and y_range > 0:
|
|
|
|
|
|
aspect_ratio = max(x_range, y_range) / min(x_range, y_range)
|
|
|
|
|
|
if aspect_ratio < 2: # 相对规则
|
|
|
|
|
|
return AlgorithmType.GRID.value
|
|
|
|
|
|
|
|
|
|
|
|
return AlgorithmType.TIN.value
|