ai_project_v1/b3dm/earthwork_calculator_3d_tiles.py

1647 lines
64 KiB
Python
Raw Normal View History

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
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
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"
@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 # 计算分辨率
# 新增属性 - 计算单元详细数据
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:
"""转换为字典"""
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": ""
},
"area": {
2026-01-29 11:51:20 +08:00
"value": round(self.area, 8),
2026-01-14 11:37:35 +08:00
"unit": ""
},
"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
}
}
# 添加新增的属性
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 = {}
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
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:
# 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,
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)
# 创建格网
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
# 初始化统计信息
cut_volume_total = 0.0
fill_volume_total = 0.0
2026-01-14 11:37:35 +08:00
total_area = 0.0
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):
# 格网四个角点
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([
[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)
is_in_polygon = self.geometryUtils.is_point_in_polygon(cell_center, polygon_np)
2026-01-14 11:37:35 +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
]
# 检查是否有无效数据
has_valid_data = not any(np.isnan(elev) for elev in corner_elevations)
2026-01-14 11:37:35 +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:
# 创建无效网格单元数据对象
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)
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(
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=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,
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,
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])
# 初始化统计信息
cut_volume_total = 0.0
fill_volume_total = 0.0
2026-01-14 11:37:35 +08:00
total_area = 0.0
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-01-14 11:37:35 +08:00
for simplex in triangulation.simplices:
triangle_points = points[simplex]
triangle_center = triangle_points.mean(axis=0)[:2]
# 检查三角形中心是否在多边形内
is_in_polygon = self.geometryUtils.is_point_in_polygon(triangle_center, polygon_np)
2026-01-14 11:37:35 +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:
# 创建无效三角形数据对象
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
# 计算高程统计信息
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(
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,
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,
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])
cut_volume_total = 0.0
fill_volume_total = 0.0
2026-01-14 11:37:35 +08:00
total_area = 0.0
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]
is_in_polygon = self.geometryUtils.is_point_in_polygon(triangle_center, polygon_np)
2026-01-14 11:37:35 +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
# 计算三棱柱体积
triangle_cut_volume = 0.0
triangle_fill_volume = 0.0
edge_volumes = []
height_diffs = []
2026-01-14 11:37:35 +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
# 去重高度差
unique_height_diffs = list(set([height_diffs[0], height_diffs[2], height_diffs[4]]))
2026-01-14 11:37:35 +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
# 计算高程统计信息
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(
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,
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