坡度坡向tif生成
This commit is contained in:
parent
c5eeb87488
commit
9a09c1e1cf
@ -145,11 +145,7 @@ class MinIO3DTilesManager:
|
|||||||
visited = set()
|
visited = set()
|
||||||
|
|
||||||
# 下载入口文件
|
# 下载入口文件
|
||||||
entry_local_path = self.get_local_path(
|
entry_local_path = self.get_local_path(entry_bucket, entry_path, save_dir)
|
||||||
entry_bucket, entry_path,
|
|
||||||
entry_bucket, entry_dir,
|
|
||||||
save_dir
|
|
||||||
)
|
|
||||||
|
|
||||||
success, result = self.download_file(entry_bucket, entry_path, entry_local_path)
|
success, result = self.download_file(entry_bucket, entry_path, entry_local_path)
|
||||||
if not success:
|
if not success:
|
||||||
@ -352,39 +348,23 @@ class MinIO3DTilesManager:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def get_local_path(self, bucket_name, object_name, base_bucket, base_object, save_dir):
|
def get_local_path(self, bucket_name, object_name, save_dir):
|
||||||
"""生成保持目录结构的本地路径"""
|
"""生成保持目录结构的本地路径"""
|
||||||
clean_bucket = self.clean_filename(bucket_name)
|
clean_bucket = self.clean_filename(bucket_name)
|
||||||
bucket_dir = clean_bucket
|
|
||||||
|
|
||||||
if bucket_name == base_bucket and base_object:
|
path_parts = object_name.split('/')
|
||||||
base_dir = os.path.dirname(base_object)
|
cleaned_parts = []
|
||||||
|
for part in path_parts:
|
||||||
if base_dir:
|
cleaned_part = self.clean_filename(part)
|
||||||
if object_name.startswith(base_dir):
|
if cleaned_part:
|
||||||
relative_path = object_name[len(base_dir):].lstrip('/\\')
|
cleaned_parts.append(cleaned_part)
|
||||||
else:
|
|
||||||
relative_path = object_name
|
|
||||||
else:
|
|
||||||
relative_path = object_name
|
|
||||||
else:
|
|
||||||
relative_path = object_name
|
|
||||||
|
|
||||||
if relative_path:
|
if cleaned_parts:
|
||||||
path_parts = relative_path.split('/')
|
cleaned_relative = '/'.join(cleaned_parts)
|
||||||
cleaned_parts = []
|
local_path = os.path.join(save_dir, clean_bucket, cleaned_relative)
|
||||||
for part in path_parts:
|
|
||||||
cleaned_part = self.clean_filename(part)
|
|
||||||
if cleaned_part:
|
|
||||||
cleaned_parts.append(cleaned_part)
|
|
||||||
|
|
||||||
if cleaned_parts:
|
|
||||||
cleaned_relative = '/'.join(cleaned_parts)
|
|
||||||
local_path = os.path.join(save_dir, bucket_dir, cleaned_relative)
|
|
||||||
else:
|
|
||||||
local_path = os.path.join(save_dir, bucket_dir)
|
|
||||||
else:
|
else:
|
||||||
local_path = os.path.join(save_dir, bucket_dir)
|
local_path = os.path.join(save_dir, clean_bucket)
|
||||||
|
|
||||||
|
|
||||||
return os.path.normpath(local_path)
|
return os.path.normpath(local_path)
|
||||||
|
|
||||||
@ -437,11 +417,7 @@ class MinIO3DTilesManager:
|
|||||||
print(f"下载文件:{file_id}")
|
print(f"下载文件:{file_id}")
|
||||||
visited.add(file_id)
|
visited.add(file_id)
|
||||||
|
|
||||||
local_path = self.get_local_path(
|
local_path = self.get_local_path(file_bucket, file_path, save_dir)
|
||||||
file_bucket, file_path,
|
|
||||||
base_bucket, base_dir,
|
|
||||||
save_dir
|
|
||||||
)
|
|
||||||
|
|
||||||
self.download_file(file_bucket, file_path, local_path)
|
self.download_file(file_bucket, file_path, local_path)
|
||||||
|
|
||||||
|
|||||||
@ -778,7 +778,319 @@ def parse_tileset(tileset_path, region_coords=None, enable_enhancement=True, deb
|
|||||||
return []
|
return []
|
||||||
|
|
||||||
# ========== DEM生成函数 ==========
|
# ========== DEM生成函数 ==========
|
||||||
def points_to_dem(points, output_dem_path, pixel_size=None, quality='medium'):
|
def points_to_dem(points, output_dem_path, pixel_size=None, quality='high', min_resolution=10):
|
||||||
|
"""
|
||||||
|
将点云转换为DEM,确保足够的网格分辨率用于坡度坡向计算
|
||||||
|
:param points: 点云数据 [(lon, lat, height), ...]
|
||||||
|
:param output_dem_path: 输出DEM文件路径
|
||||||
|
:param pixel_size: 像素大小(度),如果为None则自动计算
|
||||||
|
:param quality: 质量等级 'low'|'medium'|'high'|'terrain'(地形分析专用)
|
||||||
|
:param min_resolution: 最小网格分辨率(像素数),用于保证坡度计算
|
||||||
|
"""
|
||||||
|
import time
|
||||||
|
import os
|
||||||
|
import numpy as np
|
||||||
|
from osgeo import gdal, osr
|
||||||
|
from scipy.interpolate import griddata
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
if len(points) == 0:
|
||||||
|
raise ValueError("无点云数据,无法生成DEM")
|
||||||
|
|
||||||
|
start_time = time.time()
|
||||||
|
|
||||||
|
# 转换为numpy数组
|
||||||
|
points_array = np.array(points)
|
||||||
|
lons = points_array[:, 0]
|
||||||
|
lats = points_array[:, 1]
|
||||||
|
heights = points_array[:, 2]
|
||||||
|
|
||||||
|
# 计算范围
|
||||||
|
min_lon, max_lon = lons.min(), lons.max()
|
||||||
|
min_lat, max_lat = lats.min(), lats.max()
|
||||||
|
lon_range = max_lon - min_lon
|
||||||
|
lat_range = max_lat - min_lat
|
||||||
|
|
||||||
|
print(f"[DEM生成] 点云范围:")
|
||||||
|
print(f" 经度: {min_lon:.6f}° ~ {max_lon:.6f}° (范围: {lon_range:.6f}°)")
|
||||||
|
print(f" 纬度: {min_lat:.6f}° ~ {max_lat:.6f}° (范围: {lat_range:.6f}°)")
|
||||||
|
print(f" 高程: {heights.min():.2f}m ~ {heights.max():.2f}m")
|
||||||
|
print(f" 点数: {len(points):,}")
|
||||||
|
|
||||||
|
# 自动确定像素大小(优化版本)
|
||||||
|
if pixel_size is None:
|
||||||
|
# 根据数据量自动确定
|
||||||
|
if quality == 'terrain': # 地形分析专用
|
||||||
|
# 确保足够的网格分辨率用于坡度计算
|
||||||
|
target_pixels = max(min_resolution, int(np.sqrt(len(points)) / 2))
|
||||||
|
|
||||||
|
# 根据数据范围计算像素大小
|
||||||
|
lon_pixel = lon_range / target_pixels
|
||||||
|
lat_pixel = lat_range / target_pixels
|
||||||
|
|
||||||
|
# 取较小的像素大小以保证分辨率
|
||||||
|
pixel_size = min(lon_pixel, lat_pixel)
|
||||||
|
|
||||||
|
# 设置像素大小范围限制
|
||||||
|
pixel_size = max(pixel_size, 0.000001) # 最小约0.1米
|
||||||
|
pixel_size = min(pixel_size, 0.0005) # 最大约55米
|
||||||
|
|
||||||
|
elif quality == 'high':
|
||||||
|
pixel_size = max(0.00001, lon_range / 100) # 至少1.1米,最多100像素
|
||||||
|
elif quality == 'medium':
|
||||||
|
pixel_size = max(0.00002, lon_range / 50) # 至少2.2米,最多50像素
|
||||||
|
else: # low
|
||||||
|
pixel_size = max(0.00005, lon_range / 20) # 至少5.6米,最多20像素
|
||||||
|
|
||||||
|
# 计算网格尺寸
|
||||||
|
width = max(10, int(lon_range / pixel_size) + 1)
|
||||||
|
height = max(10, int(lat_range / pixel_size) + 1)
|
||||||
|
|
||||||
|
# 确保网格大小符合最小分辨率要求
|
||||||
|
if width * height < min_resolution * min_resolution:
|
||||||
|
print(f"[DEM生成] 警告:网格分辨率不足 ({width}x{height}),自动调整...")
|
||||||
|
# 重新计算像素大小以满足最小分辨率
|
||||||
|
target_cells = min_resolution * min_resolution
|
||||||
|
target_pixel_size = np.sqrt(lon_range * lat_range / target_cells)
|
||||||
|
pixel_size = max(0.000001, min(pixel_size, target_pixel_size))
|
||||||
|
|
||||||
|
width = max(min_resolution, int(lon_range / pixel_size) + 1)
|
||||||
|
height = max(min_resolution, int(lat_range / pixel_size) + 1)
|
||||||
|
|
||||||
|
# 扩展数据范围以增加边缘像素(有助于坡度计算)
|
||||||
|
expand_factor = 1.1 # 扩展10%
|
||||||
|
min_lon_exp = min_lon - lon_range * (expand_factor - 1) / 2
|
||||||
|
max_lon_exp = max_lon + lon_range * (expand_factor - 1) / 2
|
||||||
|
min_lat_exp = min_lat - lat_range * (expand_factor - 1) / 2
|
||||||
|
max_lat_exp = max_lat + lat_range * (expand_factor - 1) / 2
|
||||||
|
|
||||||
|
print(f"[DEM生成] 网格设置:")
|
||||||
|
print(f" 像素大小: {pixel_size:.6f}° (~{pixel_size*111320:.1f}米)")
|
||||||
|
print(f" 网格尺寸: {width} × {height}")
|
||||||
|
print(f" 总像素数: {width * height:,}")
|
||||||
|
print(f" 点云密度: {len(points)/(width*height):.2f} 点/像素")
|
||||||
|
|
||||||
|
# 创建网格
|
||||||
|
x_grid = np.linspace(min_lon_exp, max_lon_exp, width)
|
||||||
|
y_grid = np.linspace(max_lat_exp, min_lat_exp, height) # 纬度从上到下
|
||||||
|
xi, yi = np.meshgrid(x_grid, y_grid)
|
||||||
|
|
||||||
|
# 插值 - 优化版本
|
||||||
|
print("[DEM生成] 开始插值计算...")
|
||||||
|
|
||||||
|
# 检查数据分布
|
||||||
|
grid_points = np.column_stack([xi.flatten(), yi.flatten()])
|
||||||
|
data_points = np.column_stack([lons, lats])
|
||||||
|
|
||||||
|
# 使用KD树加速最近邻查询
|
||||||
|
try:
|
||||||
|
from scipy.spatial import cKDTree
|
||||||
|
tree = cKDTree(data_points)
|
||||||
|
dists, _ = tree.query(grid_points, k=1)
|
||||||
|
max_dist = np.max(dists)
|
||||||
|
print(f"[DEM生成] 最大最近邻距离: {max_dist*111320:.1f}米")
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# 尝试IDW插值(适用于地形)
|
||||||
|
try:
|
||||||
|
if len(points) > 100:
|
||||||
|
print("[DEM生成] 使用IDW插值...")
|
||||||
|
zi = idw_interpolation(lons, lats, heights, xi, yi, power=2)
|
||||||
|
else:
|
||||||
|
# 数据太少,使用线性+最近邻
|
||||||
|
zi = griddata((lons, lats), heights, (xi, yi), method='linear', fill_value=np.nan)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[DEM生成] IDW插值失败: {e},使用线性插值")
|
||||||
|
zi = griddata((lons, lats), heights, (xi, yi), method='linear', fill_value=np.nan)
|
||||||
|
|
||||||
|
# 处理空白区域
|
||||||
|
nan_mask = np.isnan(zi)
|
||||||
|
if np.any(nan_mask):
|
||||||
|
nan_count = np.sum(nan_mask)
|
||||||
|
nan_percent = nan_count / (width * height) * 100
|
||||||
|
print(f"[DEM生成] 插值空白: {nan_count:,} 像素 ({nan_percent:.1f}%)")
|
||||||
|
|
||||||
|
if nan_percent < 50: # 空白区域少于50%
|
||||||
|
# 使用最近邻填充空白
|
||||||
|
zi_nn = griddata((lons, lats), heights, (xi, yi), method='nearest')
|
||||||
|
zi[nan_mask] = zi_nn[nan_mask]
|
||||||
|
else:
|
||||||
|
print("[DEM生成] 警告:空白区域过多,使用最近邻插值")
|
||||||
|
zi = griddata((lons, lats), heights, (xi, yi), method='nearest')
|
||||||
|
|
||||||
|
# 平滑处理(可选,有助于坡度计算)
|
||||||
|
if quality in ['terrain', 'high']:
|
||||||
|
try:
|
||||||
|
from scipy.ndimage import gaussian_filter
|
||||||
|
sigma = 0.5 # 高斯滤波参数
|
||||||
|
zi = gaussian_filter(zi, sigma=sigma, mode='nearest')
|
||||||
|
print(f"[DEM生成] 应用高斯平滑 (sigma={sigma})")
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# 创建GeoTIFF
|
||||||
|
print("[DEM生成] 创建GeoTIFF文件...")
|
||||||
|
driver = gdal.GetDriverByName("GTiff")
|
||||||
|
|
||||||
|
# 压缩选项
|
||||||
|
if quality in ['terrain', 'high']:
|
||||||
|
options = ["COMPRESS=DEFLATE", "PREDICTOR=3", "ZLEVEL=6", "TILED=YES", "BLOCKXSIZE=256", "BLOCKYSIZE=256"]
|
||||||
|
else:
|
||||||
|
options = ["COMPRESS=LZW", "TILED=YES"]
|
||||||
|
|
||||||
|
dem_ds = driver.Create(output_dem_path, width, height, 1, gdal.GDT_Float32, options)
|
||||||
|
|
||||||
|
if dem_ds is None:
|
||||||
|
raise RuntimeError(f"无法创建DEM文件: {output_dem_path}")
|
||||||
|
|
||||||
|
# 设置投影和地理变换
|
||||||
|
srs = osr.SpatialReference()
|
||||||
|
srs.ImportFromEPSG(4326) # WGS84
|
||||||
|
dem_ds.SetProjection(srs.ExportToWkt())
|
||||||
|
|
||||||
|
# 注意:使用扩展后的范围
|
||||||
|
geotransform = [
|
||||||
|
min_lon_exp, pixel_size, 0,
|
||||||
|
max_lat_exp, 0, -pixel_size
|
||||||
|
]
|
||||||
|
dem_ds.SetGeoTransform(geotransform)
|
||||||
|
|
||||||
|
# 写入数据
|
||||||
|
band = dem_ds.GetRasterBand(1)
|
||||||
|
band.WriteArray(zi)
|
||||||
|
band.SetNoDataValue(-9999.0)
|
||||||
|
band.SetDescription("Elevation (meters)")
|
||||||
|
band.SetUnitType("meters")
|
||||||
|
|
||||||
|
# 计算统计信息(处理可能的统计问题)
|
||||||
|
print("[DEM生成] 计算统计信息...")
|
||||||
|
band.FlushCache()
|
||||||
|
|
||||||
|
try:
|
||||||
|
band.ComputeStatistics(False)
|
||||||
|
print("[DEM生成] 统计信息计算完成")
|
||||||
|
except RuntimeError as e:
|
||||||
|
print(f"[DEM生成] 警告:统计信息计算失败: {e}")
|
||||||
|
# 手动计算统计信息
|
||||||
|
valid_mask = zi != -9999.0
|
||||||
|
if np.any(valid_mask):
|
||||||
|
valid_data = zi[valid_mask]
|
||||||
|
stats = [
|
||||||
|
float(np.min(valid_data)),
|
||||||
|
float(np.max(valid_data)),
|
||||||
|
float(np.mean(valid_data)),
|
||||||
|
float(np.std(valid_data))
|
||||||
|
]
|
||||||
|
band.SetStatistics(*stats)
|
||||||
|
print(f"[DEM生成] 手动设置统计信息:")
|
||||||
|
print(f" 最小值: {stats[0]:.2f}m")
|
||||||
|
print(f" 最大值: {stats[1]:.2f}m")
|
||||||
|
print(f" 平均值: {stats[2]:.2f}m")
|
||||||
|
print(f" 标准差: {stats[3]:.2f}m")
|
||||||
|
|
||||||
|
# 构建金字塔
|
||||||
|
if quality in ['terrain', 'high']:
|
||||||
|
print("[DEM生成] 构建金字塔...")
|
||||||
|
gdal.SetConfigOption('COMPRESS_OVERVIEW', 'DEFLATE')
|
||||||
|
dem_ds.BuildOverviews("AVERAGE", [2, 4, 8, 16])
|
||||||
|
|
||||||
|
dem_ds = None # 关闭文件
|
||||||
|
|
||||||
|
# 验证结果
|
||||||
|
elapsed_time = time.time() - start_time
|
||||||
|
file_size_mb = os.path.getsize(output_dem_path) / (1024 * 1024) if os.path.exists(output_dem_path) else 0
|
||||||
|
|
||||||
|
# 重新打开验证
|
||||||
|
try:
|
||||||
|
ds = gdal.Open(output_dem_path, gdal.GA_ReadOnly)
|
||||||
|
if ds:
|
||||||
|
band = ds.GetRasterBand(1)
|
||||||
|
actual_width = ds.RasterXSize
|
||||||
|
actual_height = ds.RasterYSize
|
||||||
|
|
||||||
|
print(f"[DEM生成] 验证结果:")
|
||||||
|
print(f" 实际尺寸: {actual_width} × {actual_height}")
|
||||||
|
print(f" 文件大小: {file_size_mb:.2f} MB")
|
||||||
|
print(f" 处理时间: {elapsed_time:.1f}秒")
|
||||||
|
|
||||||
|
# 检查是否适合坡度计算
|
||||||
|
if actual_width >= 10 and actual_height >= 10:
|
||||||
|
print(f"[DEM生成] ✓ DEM分辨率适合坡度坡向计算")
|
||||||
|
else:
|
||||||
|
print(f"[DEM生成] ⚠ DEM分辨率较低,坡度计算可能不准确")
|
||||||
|
|
||||||
|
ds = None
|
||||||
|
except:
|
||||||
|
print(f"[DEM生成] 完成! 文件: {output_dem_path}")
|
||||||
|
|
||||||
|
return output_dem_path
|
||||||
|
|
||||||
|
|
||||||
|
def idw_interpolation(x, y, z, xi, yi, power=2, radius=None):
|
||||||
|
"""
|
||||||
|
反距离权重插值
|
||||||
|
"""
|
||||||
|
import numpy as np
|
||||||
|
from scipy.spatial import cKDTree
|
||||||
|
|
||||||
|
x = np.asarray(x)
|
||||||
|
y = np.asarray(y)
|
||||||
|
z = np.asarray(z)
|
||||||
|
xi = np.asarray(xi)
|
||||||
|
yi = np.asarray(yi)
|
||||||
|
|
||||||
|
# 展平网格
|
||||||
|
xi_flat = xi.flatten()
|
||||||
|
yi_flat = yi.flatten()
|
||||||
|
|
||||||
|
# 创建KD树
|
||||||
|
tree = cKDTree(np.column_stack([x, y]))
|
||||||
|
|
||||||
|
# 设置搜索半径
|
||||||
|
if radius is None:
|
||||||
|
# 自动确定半径:平均点间距的3倍
|
||||||
|
from scipy.spatial.distance import pdist
|
||||||
|
if len(x) > 1:
|
||||||
|
distances = pdist(np.column_stack([x, y]))
|
||||||
|
radius = np.mean(distances) * 3
|
||||||
|
else:
|
||||||
|
radius = 0.01 # 默认值
|
||||||
|
|
||||||
|
# 查询最近邻点
|
||||||
|
if len(z) > 50: # 数据较多时,限制搜索点数
|
||||||
|
k = min(12, len(z))
|
||||||
|
dists, idxs = tree.query(np.column_stack([xi_flat, yi_flat]), k=k, distance_upper_bound=radius)
|
||||||
|
else:
|
||||||
|
dists, idxs = tree.query(np.column_stack([xi_flat, yi_flat]), k=len(z))
|
||||||
|
|
||||||
|
# IDW插值
|
||||||
|
zi_flat = np.zeros(len(xi_flat))
|
||||||
|
|
||||||
|
for i in range(len(xi_flat)):
|
||||||
|
valid_mask = idxs[i] < len(z)
|
||||||
|
if np.any(valid_mask):
|
||||||
|
valid_dists = dists[i][valid_mask]
|
||||||
|
valid_idx = idxs[i][valid_mask]
|
||||||
|
|
||||||
|
# 避免除零
|
||||||
|
valid_dists = np.maximum(valid_dists, 1e-10)
|
||||||
|
|
||||||
|
# 计算权重
|
||||||
|
weights = 1.0 / (valid_dists ** power)
|
||||||
|
weights = weights / np.sum(weights)
|
||||||
|
|
||||||
|
# 加权平均
|
||||||
|
zi_flat[i] = np.sum(z[valid_idx] * weights)
|
||||||
|
else:
|
||||||
|
zi_flat[i] = np.nan
|
||||||
|
|
||||||
|
# 恢复形状
|
||||||
|
zi = zi_flat.reshape(xi.shape)
|
||||||
|
|
||||||
|
return zi
|
||||||
|
|
||||||
|
def points_to_dem1(points, output_dem_path, pixel_size=None, quality='medium'):
|
||||||
"""
|
"""
|
||||||
将点云转换为DEM
|
将点云转换为DEM
|
||||||
:param quality: 质量等级 'low'|'medium'|'high'
|
:param quality: 质量等级 'low'|'medium'|'high'
|
||||||
|
|||||||
@ -14,33 +14,34 @@ earthwork_bp = Blueprint("earthwork", url_prefix="")
|
|||||||
logging.basicConfig(level=logging.INFO)
|
logging.basicConfig(level=logging.INFO)
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
# 全局变量
|
|
||||||
_calculator_point_cloud = None
|
|
||||||
_calculator_3d_tiles = None
|
|
||||||
_data_source_3d_tiles = None
|
|
||||||
|
|
||||||
# 初始化函数
|
# 初始化函数
|
||||||
async def init_app():
|
def init_app(url, type = "3dtiles"):
|
||||||
"""初始化应用"""
|
"""初始化应用"""
|
||||||
global _data_source_3d_tiles, _calculator_3d_tiles, _calculator_point_cloud
|
data_source = None
|
||||||
|
calculator_3d_tiles = None
|
||||||
|
calculator_point_cloud = None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# 配置数据源
|
|
||||||
tileset_path = "./data/3dtiles/tileset.json"
|
|
||||||
|
|
||||||
# 初始化数据源
|
# 初始化数据源
|
||||||
_data_source_3d_tiles = TilesetDataSource(tileset_path)
|
data_source = TilesetDataSource(url)
|
||||||
await _data_source_3d_tiles.initialize()
|
data_source.dowload_map_data(url)
|
||||||
|
|
||||||
# 初始化计算器-3dTiles
|
if type == "3dtiles" :
|
||||||
_calculator_3d_tiles = EarthworkCalculator3dTiles(_data_source_3d_tiles)
|
# 初始化计算器-3dTiles
|
||||||
|
calculator_3d_tiles = EarthworkCalculator3dTiles(data_source)
|
||||||
# 初始化计算器-点云
|
elif type == "pointcloud" :
|
||||||
point_cloud_path = "./data/pointCloud/simulated_points.laz"
|
# 初始化计算器-点云
|
||||||
_calculator_point_cloud = EarthworkCalculatorPointCloud(point_cloud_path)
|
calculator_point_cloud = EarthworkCalculatorPointCloud(data_source.tileset_path)
|
||||||
|
else :
|
||||||
|
logger.info(f"不支持的3d地图数据格式:{type}")
|
||||||
|
raise
|
||||||
|
|
||||||
logger.info("土方量计算器初始化完成")
|
logger.info("土方量计算器初始化完成")
|
||||||
|
return {
|
||||||
|
"data_source":data_source,
|
||||||
|
"calculator_3d_tiles":calculator_3d_tiles,
|
||||||
|
"calculator_point_cloud":calculator_point_cloud
|
||||||
|
}
|
||||||
except ImportError as e:
|
except ImportError as e:
|
||||||
logger.error(f"依赖库缺失: {str(e)}")
|
logger.error(f"依赖库缺失: {str(e)}")
|
||||||
raise
|
raise
|
||||||
@ -56,12 +57,26 @@ async def calc_earthwork(request: Request):
|
|||||||
|
|
||||||
请求参数示例:
|
请求参数示例:
|
||||||
{
|
{
|
||||||
"polygonCoords": [[120.1, 30.1], [120.2, 30.1], [120.2, 30.2], [120.1, 30.2]],
|
"polygonCoords": [
|
||||||
"designElevation": 50.0,
|
[
|
||||||
|
115.70440468338526,
|
||||||
|
30.77363140345639
|
||||||
|
],
|
||||||
|
[
|
||||||
|
115.70443054007985,
|
||||||
|
30.773510462589584
|
||||||
|
],
|
||||||
|
[
|
||||||
|
115.70459702429197,
|
||||||
|
30.77360789911405
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"designElevation": 100,
|
||||||
"algorithm": "tin",
|
"algorithm": "tin",
|
||||||
"resolution": 1.0,
|
"resolution": 1,
|
||||||
"crs": "EPSG:4326",
|
"crs": "EPSG:4326",
|
||||||
"interpolationMethod": "linear"
|
"interpolationMethod": "linear",
|
||||||
|
"url": "http://222.212.85.86:9000/300bdf2b-a150-406e-be63-d28bd29b409f/model/hbgldk/yzk/20260113/3D/terra_b3dms/tileset.json"
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
@ -73,11 +88,14 @@ async def calc_earthwork(request: Request):
|
|||||||
# 2. 提取参数
|
# 2. 提取参数
|
||||||
polygon_coords = data.get("polygonCoords")
|
polygon_coords = data.get("polygonCoords")
|
||||||
design_elevation = data.get("designElevation")
|
design_elevation = data.get("designElevation")
|
||||||
|
url = data.get("url")
|
||||||
|
|
||||||
if not polygon_coords:
|
if not polygon_coords:
|
||||||
return _error_response("多边形坐标不能为空", 400)
|
return _error_response("多边形坐标不能为空", 400)
|
||||||
if design_elevation is None:
|
if design_elevation is None:
|
||||||
return _error_response("设计高程不能为空", 400)
|
return _error_response("设计高程不能为空", 400)
|
||||||
|
if url is None:
|
||||||
|
return _error_response("地图不能为空", 400)
|
||||||
|
|
||||||
# 3. 可选参数
|
# 3. 可选参数
|
||||||
algorithm = data.get("algorithm", "tin")
|
algorithm = data.get("algorithm", "tin")
|
||||||
@ -102,13 +120,13 @@ async def calc_earthwork(request: Request):
|
|||||||
return _error_response("分辨率必须在0-100米之间", 400)
|
return _error_response("分辨率必须在0-100米之间", 400)
|
||||||
|
|
||||||
# 5. 确保计算器已初始化
|
# 5. 确保计算器已初始化
|
||||||
if _calculator_3d_tiles is None:
|
app_info = init_app(url)
|
||||||
await init_app()
|
calculator_3d_tiles = app_info.get("calculator_3d_tiles")
|
||||||
|
|
||||||
# 6. 执行计算
|
# 6. 执行计算
|
||||||
algorithm_type = AlgorithmType(algorithm)
|
algorithm_type = AlgorithmType(algorithm)
|
||||||
|
|
||||||
result = await _calculator_3d_tiles.calculate(
|
result = await calculator_3d_tiles.calculate(
|
||||||
polygon_coords=polygon_coords,
|
polygon_coords=polygon_coords,
|
||||||
design_elevation=design_elevation,
|
design_elevation=design_elevation,
|
||||||
algorithm=algorithm_type,
|
algorithm=algorithm_type,
|
||||||
@ -144,6 +162,10 @@ async def validate_earthwork(request: Request):
|
|||||||
if not polygon_coords:
|
if not polygon_coords:
|
||||||
return _error_response("多边形坐标不能为空", 400)
|
return _error_response("多边形坐标不能为空", 400)
|
||||||
|
|
||||||
|
url = data.get("url")
|
||||||
|
if url is None:
|
||||||
|
return _error_response("地图不能为空", 400)
|
||||||
|
|
||||||
# 3. 参数验证
|
# 3. 参数验证
|
||||||
if len(polygon_coords) < 3:
|
if len(polygon_coords) < 3:
|
||||||
return _error_response("多边形至少需要3个点", 400)
|
return _error_response("多边形至少需要3个点", 400)
|
||||||
@ -153,11 +175,11 @@ async def validate_earthwork(request: Request):
|
|||||||
polygon_coords.append(polygon_coords[0])
|
polygon_coords.append(polygon_coords[0])
|
||||||
|
|
||||||
# 4. 确保计算器已初始化
|
# 4. 确保计算器已初始化
|
||||||
if _calculator_3d_tiles is None:
|
app_info = init_app(url)
|
||||||
await init_app()
|
calculator_3d_tiles = app_info.get("calculator_3d_tiles")
|
||||||
|
|
||||||
# 5. 执行验证
|
# 5. 执行验证
|
||||||
validation_result = await _calculator_3d_tiles.validate(polygon_coords)
|
validation_result = await calculator_3d_tiles.validate(polygon_coords)
|
||||||
|
|
||||||
# 6. 返回结果
|
# 6. 返回结果
|
||||||
return _success_response(validation_result)
|
return _success_response(validation_result)
|
||||||
@ -242,9 +264,7 @@ async def batch_calc_earthwork(request: Request):
|
|||||||
if len(calculations) > 100:
|
if len(calculations) > 100:
|
||||||
return _error_response("批量计算数量超过限制(最多100个)", 400)
|
return _error_response("批量计算数量超过限制(最多100个)", 400)
|
||||||
|
|
||||||
# 2. 确保计算器已初始化
|
|
||||||
if _calculator_3d_tiles is None:
|
|
||||||
await init_app()
|
|
||||||
|
|
||||||
# 3. 执行批量计算
|
# 3. 执行批量计算
|
||||||
results = []
|
results = []
|
||||||
@ -255,8 +275,9 @@ async def batch_calc_earthwork(request: Request):
|
|||||||
# 提取参数
|
# 提取参数
|
||||||
polygon_coords = calc_data.get("polygonCoords")
|
polygon_coords = calc_data.get("polygonCoords")
|
||||||
design_elevation = calc_data.get("designElevation")
|
design_elevation = calc_data.get("designElevation")
|
||||||
|
url = calc_data.get("url")
|
||||||
|
|
||||||
if not polygon_coords or design_elevation is None:
|
if not polygon_coords or design_elevation is None or url is None:
|
||||||
errors.append({
|
errors.append({
|
||||||
"index": i,
|
"index": i,
|
||||||
"error": "缺少必要参数"
|
"error": "缺少必要参数"
|
||||||
@ -280,11 +301,15 @@ async def batch_calc_earthwork(request: Request):
|
|||||||
resolution = calc_data.get("resolution", 1.0)
|
resolution = calc_data.get("resolution", 1.0)
|
||||||
crs = calc_data.get("crs", "EPSG:4326")
|
crs = calc_data.get("crs", "EPSG:4326")
|
||||||
interpolation_method = calc_data.get("interpolationMethod", "linear")
|
interpolation_method = calc_data.get("interpolationMethod", "linear")
|
||||||
|
|
||||||
|
# 2. 确保计算器已初始化
|
||||||
|
app_info = init_app(url)
|
||||||
|
calculator_3d_tiles = app_info.get("calculator_3d_tiles")
|
||||||
|
|
||||||
# 执行计算
|
# 执行计算
|
||||||
algorithm_type = AlgorithmType(algorithm)
|
algorithm_type = AlgorithmType(algorithm)
|
||||||
|
|
||||||
result = await _calculator_3d_tiles.calculate(
|
result = await calculator_3d_tiles.calculate(
|
||||||
polygon_coords=polygon_coords,
|
polygon_coords=polygon_coords,
|
||||||
design_elevation=design_elevation,
|
design_elevation=design_elevation,
|
||||||
algorithm=algorithm_type,
|
algorithm=algorithm_type,
|
||||||
@ -338,19 +363,22 @@ async def calc_earthwork_point_cloud(request: Request):
|
|||||||
polygon_coords = data.get("polygonCoords") # 计算区域多边形坐标
|
polygon_coords = data.get("polygonCoords") # 计算区域多边形坐标
|
||||||
design_elev = data.get("designElevation") # 设计高程
|
design_elev = data.get("designElevation") # 设计高程
|
||||||
crs = data.get("crs", "EPSG:4326") # 坐标系,默认WGS84
|
crs = data.get("crs", "EPSG:4326") # 坐标系,默认WGS84
|
||||||
|
url = data.get("url")
|
||||||
|
if url is None:
|
||||||
|
return _error_response("地图不能为空", 400)
|
||||||
|
|
||||||
# 2. 确保计算器已初始化
|
# 2. 确保计算器已初始化
|
||||||
if _calculator_point_cloud is None:
|
app_info = init_app(url)
|
||||||
await init_app()
|
calculator_point_cloud = app_info.get("calculator_point_cloud")
|
||||||
|
|
||||||
result = _calculator_point_cloud.calculate_earthwork(polygon_coords=polygon_coords, design_elev=design_elev, crs=crs)
|
result = calculator_point_cloud.calculate_earthwork(polygon_coords=polygon_coords, design_elev=design_elev, crs=crs)
|
||||||
|
|
||||||
# 3. 处理结果
|
# 3. 处理结果
|
||||||
if not result["success"]:
|
if not result["success"]:
|
||||||
return _error_response(result["error"], 400)
|
return _error_response(result["error"], 400)
|
||||||
|
|
||||||
# 4. 格式化结果
|
# 4. 格式化结果
|
||||||
formatted_result = _calculator_point_cloud.format_result(result)
|
formatted_result = calculator_point_cloud.format_result(result)
|
||||||
|
|
||||||
# 5. 返回成功响应
|
# 5. 返回成功响应
|
||||||
return _success_response(formatted_result)
|
return _success_response(formatted_result)
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import logging
|
|||||||
from enum import Enum
|
from enum import Enum
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
import math
|
import math
|
||||||
|
from pyproj import Geod
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -37,13 +38,13 @@ class EarthworkResult3dTiles:
|
|||||||
"""转换为字典"""
|
"""转换为字典"""
|
||||||
return {
|
return {
|
||||||
"volume": {
|
"volume": {
|
||||||
"cut": round(self.cut_volume, 3),
|
"cut": round(self.cut_volume, 8),
|
||||||
"fill": round(self.fill_volume, 3),
|
"fill": round(self.fill_volume, 8),
|
||||||
"net": round(self.net_volume, 3),
|
"net": round(self.net_volume, 8),
|
||||||
"unit": "m³"
|
"unit": "m³"
|
||||||
},
|
},
|
||||||
"area": {
|
"area": {
|
||||||
"value": round(self.area, 3),
|
"value": round(self.area, 8),
|
||||||
"unit": "m²"
|
"unit": "m²"
|
||||||
},
|
},
|
||||||
"elevation": {
|
"elevation": {
|
||||||
@ -94,67 +95,375 @@ class TerrainDataSource(ABC):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
class GeometryUtils:
|
class GeometryUtils:
|
||||||
"""几何计算工具类"""
|
"""地理空间几何计算工具类(支持经纬度坐标)"""
|
||||||
|
|
||||||
@staticmethod
|
def __init__(self, source_crs: str = "EPSG:4326", target_crs: str = "EPSG:3857"):
|
||||||
def calculate_polygon_area(polygon_coords: List[List[float]]) -> float:
|
"""
|
||||||
"""计算多边形面积(平面面积)"""
|
初始化
|
||||||
polygon_np = np.array(polygon_coords)
|
Args:
|
||||||
x = polygon_np[:, 0]
|
source_crs: 源坐标系(通常是EPSG:4326)
|
||||||
y = polygon_np[:, 1]
|
target_crs: 目标投影坐标系(用于平面计算)
|
||||||
return 0.5 * np.abs(np.dot(x, np.roll(y, 1)) - np.dot(y, np.roll(x, 1)))
|
"""
|
||||||
|
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
|
||||||
|
)
|
||||||
|
|
||||||
@staticmethod
|
def calculate_polygon_area(self, polygon_coords: List[List[float]]) -> float:
|
||||||
def is_point_in_polygon(point: np.ndarray, polygon: np.ndarray) -> bool:
|
"""
|
||||||
"""判断点是否在多边形内"""
|
计算多边形的地面实际面积(平方米)
|
||||||
from matplotlib.path import Path
|
|
||||||
path = Path(polygon)
|
Args:
|
||||||
return path.contains_point(point)
|
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)
|
||||||
|
|
||||||
@staticmethod
|
def is_point_in_polygon(self, point: Tuple[float, float],
|
||||||
def calculate_triangle_area(points: np.ndarray) -> float:
|
polygon_coords: List[List[float]],
|
||||||
"""计算三角形面积"""
|
use_spherical: bool = True) -> bool:
|
||||||
a = np.linalg.norm(points[0] - points[1])
|
"""
|
||||||
b = np.linalg.norm(points[1] - points[2])
|
判断点是否在多边形内(支持地球表面判断)
|
||||||
c = np.linalg.norm(points[2] - points[0])
|
|
||||||
|
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])
|
||||||
|
|
||||||
s = (a + b + c) / 2
|
s = (a + b + c) / 2
|
||||||
return np.sqrt(s * (s - a) * (s - b) * (s - c))
|
|
||||||
|
# 防止数值误差
|
||||||
|
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
|
||||||
|
|
||||||
@staticmethod
|
def create_grid(self, polygon_coords: List[List[float]],
|
||||||
def create_grid(polygon: np.ndarray, resolution: float) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
|
resolution_m: float,
|
||||||
"""创建规则格网"""
|
use_projection: bool = True) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
|
||||||
x_min, y_min = polygon.min(axis=0)
|
"""
|
||||||
x_max, y_max = polygon.max(axis=0)
|
创建规则格网(地面距离为单位的网格)
|
||||||
|
|
||||||
# 扩展一个格网单元
|
Args:
|
||||||
x_min -= resolution
|
polygon_coords: 多边形坐标
|
||||||
x_max += resolution
|
resolution_m: 网格分辨率(米)
|
||||||
y_min -= resolution
|
use_projection: 是否使用投影坐标系
|
||||||
y_max += resolution
|
|
||||||
|
Returns:
|
||||||
x_grid = np.arange(x_min, x_max + resolution, resolution)
|
xx, yy: 网格坐标
|
||||||
y_grid = np.arange(y_min, y_max + resolution, resolution)
|
grid_coords_geo: 网格点的地理坐标
|
||||||
xx, yy = np.meshgrid(x_grid, y_grid)
|
"""
|
||||||
|
if use_projection:
|
||||||
return xx, yy, x_grid, y_grid
|
# 方法1:投影到平面坐标系创建网格
|
||||||
|
return self._create_grid_projected(polygon_coords, resolution_m)
|
||||||
|
else:
|
||||||
|
# 方法2:直接在经纬度上创建近似网格(小区域可用)
|
||||||
|
return self._create_grid_geographic(polygon_coords, resolution_m)
|
||||||
|
|
||||||
@staticmethod
|
def interpolate_grid(self, xx: np.ndarray, yy: np.ndarray,
|
||||||
def interpolate_grid(xx: np.ndarray, yy: np.ndarray,
|
points: np.ndarray,
|
||||||
points: np.ndarray, method: str = 'linear') -> np.ndarray:
|
method: str = 'linear',
|
||||||
"""格网插值"""
|
return_geo: bool = False) -> np.ndarray:
|
||||||
from scipy.interpolate import LinearNDInterpolator, CloughTocher2DInterpolator
|
"""
|
||||||
|
格网插值
|
||||||
|
|
||||||
grid_points = np.column_stack([xx.ravel(), yy.ravel()])
|
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]])
|
||||||
|
|
||||||
|
# 创建插值器
|
||||||
if method == 'linear':
|
if method == 'linear':
|
||||||
interpolator = LinearNDInterpolator(points[:, :2], points[:, 2])
|
interpolator = LinearNDInterpolator(
|
||||||
|
points_proj[:, :2],
|
||||||
|
points_proj[:, 2],
|
||||||
|
fill_value=np.nan
|
||||||
|
)
|
||||||
elif method == 'cubic':
|
elif method == 'cubic':
|
||||||
interpolator = CloughTocher2DInterpolator(points[:, :2], points[:, 2])
|
interpolator = CloughTocher2DInterpolator(
|
||||||
|
points_proj[:, :2],
|
||||||
|
points_proj[:, 2],
|
||||||
|
fill_value=np.nan
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
raise ValueError(f"不支持的插值方法: {method}")
|
raise ValueError(f"不支持的插值方法: {method}")
|
||||||
|
|
||||||
|
# 插值
|
||||||
|
grid_points = np.column_stack([xx.ravel(), yy.ravel()])
|
||||||
elevations = interpolator(grid_points)
|
elevations = interpolator(grid_points)
|
||||||
return elevations.reshape(xx.shape)
|
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
|
||||||
|
|
||||||
class EarthworkCalculator3dTiles:
|
class EarthworkCalculator3dTiles:
|
||||||
"""土方量计算器"""
|
"""土方量计算器"""
|
||||||
@ -167,6 +476,7 @@ class EarthworkCalculator3dTiles:
|
|||||||
data_source: 地形数据源
|
data_source: 地形数据源
|
||||||
"""
|
"""
|
||||||
self.data_source = data_source
|
self.data_source = data_source
|
||||||
|
self.geometryUtils = GeometryUtils()
|
||||||
self._transformer_cache = {}
|
self._transformer_cache = {}
|
||||||
|
|
||||||
async def calculate(self,
|
async def calculate(self,
|
||||||
@ -183,7 +493,7 @@ class EarthworkCalculator3dTiles:
|
|||||||
polygon_coords: 多边形坐标
|
polygon_coords: 多边形坐标
|
||||||
design_elevation: 设计高程
|
design_elevation: 设计高程
|
||||||
algorithm: 计算算法
|
algorithm: 计算算法
|
||||||
resolution: 格网分辨率(米)
|
resolution: 格网分辨率(米)
|
||||||
target_crs: 目标坐标系
|
target_crs: 目标坐标系
|
||||||
interpolation_method: 插值方法
|
interpolation_method: 插值方法
|
||||||
|
|
||||||
@ -195,7 +505,7 @@ class EarthworkCalculator3dTiles:
|
|||||||
points = await self.data_source.get_points_in_polygon(polygon_coords)
|
points = await self.data_source.get_points_in_polygon(polygon_coords)
|
||||||
|
|
||||||
if points.size == 0:
|
if points.size == 0:
|
||||||
raise ValueError("区域内没有找到高程数据")
|
raise ValueError("区域内没有找到顶点数据")
|
||||||
|
|
||||||
# 2. 坐标转换
|
# 2. 坐标转换
|
||||||
points = await self._transform_coordinates(points, target_crs)
|
points = await self._transform_coordinates(points, target_crs)
|
||||||
@ -250,10 +560,10 @@ class EarthworkCalculator3dTiles:
|
|||||||
polygon_np = np.array(polygon_coords)
|
polygon_np = np.array(polygon_coords)
|
||||||
|
|
||||||
# 创建格网
|
# 创建格网
|
||||||
xx, yy, x_grid, y_grid = GeometryUtils.create_grid(polygon_np, resolution)
|
xx, yy, x_grid, y_grid = self.geometryUtils.create_grid(polygon_np, resolution)
|
||||||
|
|
||||||
# 插值
|
# 插值
|
||||||
natural_elevations = GeometryUtils.interpolate_grid(xx, yy, points, interpolation_method)
|
natural_elevations = self.geometryUtils.interpolate_grid(xx, yy, points, interpolation_method)
|
||||||
|
|
||||||
# 初始化挖填量
|
# 初始化挖填量
|
||||||
cut_volume = 0.0
|
cut_volume = 0.0
|
||||||
@ -273,7 +583,7 @@ class EarthworkCalculator3dTiles:
|
|||||||
|
|
||||||
# 检查格网中心点是否在多边形内
|
# 检查格网中心点是否在多边形内
|
||||||
cell_center = cell_corners.mean(axis=0)
|
cell_center = cell_corners.mean(axis=0)
|
||||||
if not GeometryUtils.is_point_in_polygon(cell_center, polygon_np):
|
if not self.geometryUtils.is_point_in_polygon(cell_center, polygon_np):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# 获取格网四个角点的高程
|
# 获取格网四个角点的高程
|
||||||
@ -301,7 +611,7 @@ class EarthworkCalculator3dTiles:
|
|||||||
cut_volume += abs(height_diff) * cell_area
|
cut_volume += abs(height_diff) * cell_area
|
||||||
|
|
||||||
# 计算统计信息
|
# 计算统计信息
|
||||||
area = GeometryUtils.calculate_polygon_area(polygon_coords)
|
area = self.geometryUtils.calculate_polygon_area(polygon_coords)
|
||||||
mask = ~np.isnan(natural_elevations)
|
mask = ~np.isnan(natural_elevations)
|
||||||
valid_elevations = natural_elevations[mask]
|
valid_elevations = natural_elevations[mask]
|
||||||
|
|
||||||
@ -342,14 +652,16 @@ class EarthworkCalculator3dTiles:
|
|||||||
triangle_center = triangle_points.mean(axis=0)[:2]
|
triangle_center = triangle_points.mean(axis=0)[:2]
|
||||||
|
|
||||||
# 检查三角形中心是否在多边形内
|
# 检查三角形中心是否在多边形内
|
||||||
if not GeometryUtils.is_point_in_polygon(triangle_center, polygon_np):
|
if not self.geometryUtils.is_point_in_polygon(triangle_center, polygon_np):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# 计算三角形面积
|
# 计算三角形面积
|
||||||
area = GeometryUtils.calculate_triangle_area(triangle_points[:, :2])
|
area = self.geometryUtils.calculate_triangle_area(triangle_points[:, :2])
|
||||||
|
if math.isnan(area) :
|
||||||
|
continue
|
||||||
total_area += area
|
total_area += area
|
||||||
|
|
||||||
# 计算平均高程(使用三个顶点的高程)
|
# 计算平均高程(使用三个顶点的高程)
|
||||||
avg_elevation = triangle_points[:, 2].mean()
|
avg_elevation = triangle_points[:, 2].mean()
|
||||||
|
|
||||||
# 计算挖填量
|
# 计算挖填量
|
||||||
@ -358,9 +670,13 @@ class EarthworkCalculator3dTiles:
|
|||||||
fill_volume += height_diff * area
|
fill_volume += height_diff * area
|
||||||
else:
|
else:
|
||||||
cut_volume += abs(height_diff) * area
|
cut_volume += abs(height_diff) * area
|
||||||
|
# if math.isnan(cut_volume) :
|
||||||
|
# print("cut_volume变为nan")
|
||||||
|
# if math.isnan(fill_volume) :
|
||||||
|
# print("fill_volume变为nan")
|
||||||
|
|
||||||
# 计算统计信息
|
# 计算统计信息
|
||||||
area = GeometryUtils.calculate_polygon_area(polygon_coords)
|
area = self.geometryUtils.calculate_polygon_area(polygon_coords)
|
||||||
|
|
||||||
return EarthworkResult3dTiles(
|
return EarthworkResult3dTiles(
|
||||||
cut_volume=cut_volume,
|
cut_volume=cut_volume,
|
||||||
@ -397,11 +713,11 @@ class EarthworkCalculator3dTiles:
|
|||||||
triangle_points = points[simplex]
|
triangle_points = points[simplex]
|
||||||
triangle_center = triangle_points.mean(axis=0)[:2]
|
triangle_center = triangle_points.mean(axis=0)[:2]
|
||||||
|
|
||||||
if not GeometryUtils.is_point_in_polygon(triangle_center, polygon_np):
|
if not self.geometryUtils.is_point_in_polygon(triangle_center, polygon_np):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# 计算三角形面积
|
# 计算三角形面积
|
||||||
area = GeometryUtils.calculate_triangle_area(triangle_points[:, :2])
|
area = self.geometryUtils.calculate_triangle_area(triangle_points[:, :2])
|
||||||
total_area += area
|
total_area += area
|
||||||
|
|
||||||
# 对于每个三角形,计算三棱柱体积
|
# 对于每个三角形,计算三棱柱体积
|
||||||
@ -418,7 +734,7 @@ class EarthworkCalculator3dTiles:
|
|||||||
# 计算边的平均挖填高度
|
# 计算边的平均挖填高度
|
||||||
avg_height = (abs(height_i) + abs(height_j)) / 2
|
avg_height = (abs(height_i) + abs(height_j)) / 2
|
||||||
|
|
||||||
# 计算边的面积(假设边宽度为resolution)
|
# 计算边的面积(假设边宽度为resolution)
|
||||||
edge_area = edge_length * resolution
|
edge_area = edge_length * resolution
|
||||||
|
|
||||||
if height_i > 0 or height_j > 0:
|
if height_i > 0 or height_j > 0:
|
||||||
@ -426,7 +742,7 @@ class EarthworkCalculator3dTiles:
|
|||||||
else:
|
else:
|
||||||
cut_volume += avg_height * edge_area
|
cut_volume += avg_height * edge_area
|
||||||
|
|
||||||
area = GeometryUtils.calculate_polygon_area(polygon_coords)
|
area = self.geometryUtils.calculate_polygon_area(polygon_coords)
|
||||||
|
|
||||||
return EarthworkResult3dTiles(
|
return EarthworkResult3dTiles(
|
||||||
cut_volume=cut_volume,
|
cut_volume=cut_volume,
|
||||||
@ -477,7 +793,7 @@ class EarthworkCalculator3dTiles:
|
|||||||
|
|
||||||
validation_result = {
|
validation_result = {
|
||||||
"polygon_valid": len(polygon_coords) >= 3,
|
"polygon_valid": len(polygon_coords) >= 3,
|
||||||
"area": GeometryUtils.calculate_polygon_area(polygon_coords),
|
"area": self.geometryUtils.calculate_polygon_area(polygon_coords),
|
||||||
"points_available": points.size > 0,
|
"points_available": points.size > 0,
|
||||||
"points_count": points.shape[0] if points.size > 0 else 0,
|
"points_count": points.shape[0] if points.size > 0 else 0,
|
||||||
"data_quality": "good" if points.shape[0] > 100 else "poor",
|
"data_quality": "good" if points.shape[0] > 100 else "poor",
|
||||||
|
|||||||
1062
b3dm/slope_aspect_tif.py
Normal file
1062
b3dm/slope_aspect_tif.py
Normal file
File diff suppressed because it is too large
Load Diff
@ -174,7 +174,7 @@ async def preload_3dtiles(request: Request):
|
|||||||
|
|
||||||
# 创建并启动线程
|
# 创建并启动线程
|
||||||
script_dir = os.path.dirname(os.path.abspath(__file__))
|
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
thread1 = threading.Thread(target=TerrainCalculator.preload_3dtiles, args=(vector.url))
|
thread1 = threading.Thread(target=TerrainCalculator.preload_3dtiles, args=(vector.url,))
|
||||||
# 启动线程
|
# 启动线程
|
||||||
thread1.start()
|
thread1.start()
|
||||||
url_prefix = extract_and_rebuild_url(vector.url)
|
url_prefix = extract_and_rebuild_url(vector.url)
|
||||||
|
|||||||
@ -3,58 +3,39 @@ from typing import List, Tuple, Dict, Any
|
|||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import uuid
|
import uuid
|
||||||
from b3dm.data_3dtiles_manager import MinIO3DTilesManager
|
|
||||||
import b3dm.data_3dtiles_to_dem as data_3dtiles_to_dem
|
import b3dm.data_3dtiles_to_dem as data_3dtiles_to_dem
|
||||||
import b3dm.slope_aspect_img as slope_aspect_img
|
import b3dm.slope_aspect_img as slope_aspect_img
|
||||||
import b3dm.slope_aspect_tif as slope_aspect_tif
|
import b3dm.slope_aspect_tif as slope_aspect_tif
|
||||||
|
from b3dm.tileset_data_source import TilesetDataSource
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
ENDPOINT_URL = "222.212.85.86:9000"
|
_data_source = None
|
||||||
ACCESS_KEY = "WuRenJi"
|
|
||||||
SECRET_KEY = "WRJ@2024"
|
|
||||||
|
|
||||||
|
|
||||||
class TerrainCalculator:
|
class TerrainCalculator:
|
||||||
"""地形坡度和坡向计算器"""
|
"""地形坡度和坡向计算器"""
|
||||||
|
|
||||||
def preload_3dtiles(url) :
|
def preload_3dtiles(url: str) :
|
||||||
# 下载3dtiles地图数据
|
# 下载3dtiles地图数据
|
||||||
manager = MinIO3DTilesManager(
|
_data_source = TilesetDataSource(url)
|
||||||
endpoint_url=ENDPOINT_URL,
|
_data_source.dowload_map_data(url)
|
||||||
access_key=ACCESS_KEY,
|
|
||||||
secret_key=SECRET_KEY,
|
if not _data_source.tileset_path :
|
||||||
secure=False
|
|
||||||
)
|
|
||||||
script_dir = os.path.dirname(os.path.abspath(__file__))
|
|
||||||
success, entry_local_path = manager.download_full_tileset(
|
|
||||||
tileset_url=url,
|
|
||||||
save_dir=f"data_3dtiles",
|
|
||||||
region_filter=None
|
|
||||||
)
|
|
||||||
if not success :
|
|
||||||
logger.info(f"下载地图数据失败: {url}")
|
logger.info(f"下载地图数据失败: {url}")
|
||||||
return "下载地图数据失败", None
|
return "下载地图数据失败", None
|
||||||
|
|
||||||
def generate_slopeAspect_3d_overlook(region_coords, url, overall_3d_png_name, minio_sub_path) :
|
def generate_slopeAspect_3d_overlook(region_coords, url, overall_3d_png_name, minio_sub_path) :
|
||||||
# 下载3dtiles地图数据
|
# 下载3dtiles地图数据
|
||||||
manager = MinIO3DTilesManager(
|
_data_source = TilesetDataSource(url)
|
||||||
endpoint_url=ENDPOINT_URL,
|
_data_source.dowload_map_data(url)
|
||||||
access_key=ACCESS_KEY,
|
|
||||||
secret_key=SECRET_KEY,
|
if not _data_source.tileset_path :
|
||||||
secure=False
|
|
||||||
)
|
|
||||||
script_dir = os.path.dirname(os.path.abspath(__file__))
|
|
||||||
success, entry_local_path = manager.download_full_tileset(
|
|
||||||
tileset_url=url,
|
|
||||||
save_dir=f"data_3dtiles",
|
|
||||||
region_filter=None
|
|
||||||
)
|
|
||||||
if not success :
|
|
||||||
logger.info(f"下载地图数据失败: {url},{region_coords}")
|
logger.info(f"下载地图数据失败: {url},{region_coords}")
|
||||||
return "下载地图数据失败", None
|
return "下载地图数据失败", None
|
||||||
|
|
||||||
tileset_path = entry_local_path
|
tileset_path = _data_source.tileset_path
|
||||||
|
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
dem_path = os.path.join(script_dir, f"o_dem_{uuid.uuid4().hex[:8]}.tif")
|
dem_path = os.path.join(script_dir, f"o_dem_{uuid.uuid4().hex[:8]}.tif")
|
||||||
data_3dtiles_to_dem.generate_dem(tileset_path, dem_path, region_coords)
|
data_3dtiles_to_dem.generate_dem(tileset_path, dem_path, region_coords)
|
||||||
|
|
||||||
@ -66,8 +47,8 @@ class TerrainCalculator:
|
|||||||
slope_aspect_img.read_slope_aspect_by_dem(dem_path, overall_3d_png_path)
|
slope_aspect_img.read_slope_aspect_by_dem(dem_path, overall_3d_png_path)
|
||||||
logger.info(f"生成成功: {url},{region_coords},{overall_3d_png_path}")
|
logger.info(f"生成成功: {url},{region_coords},{overall_3d_png_path}")
|
||||||
|
|
||||||
entry_bucket, _ = manager.parse_minio_url(url);
|
entry_bucket, _ = _data_source.parse_minio_url(url);
|
||||||
success, minio_path = manager.upload_file(entry_bucket, f"{minio_sub_path}/{overall_3d_png_name}", overall_3d_png_path)
|
success, minio_path = _data_source.upload_file(entry_bucket, f"{minio_sub_path}/{overall_3d_png_name}", overall_3d_png_path)
|
||||||
if success :
|
if success :
|
||||||
return "生成成功", minio_path
|
return "生成成功", minio_path
|
||||||
else :
|
else :
|
||||||
@ -75,36 +56,28 @@ class TerrainCalculator:
|
|||||||
|
|
||||||
def generate_slopeAspect_tif(region_coords, url, slope_aspect_tif_name, minio_sub_path) :
|
def generate_slopeAspect_tif(region_coords, url, slope_aspect_tif_name, minio_sub_path) :
|
||||||
# 下载3dtiles地图数据
|
# 下载3dtiles地图数据
|
||||||
manager = MinIO3DTilesManager(
|
_data_source = TilesetDataSource(url)
|
||||||
endpoint_url=ENDPOINT_URL,
|
_data_source.dowload_map_data(url)
|
||||||
access_key=ACCESS_KEY,
|
|
||||||
secret_key=SECRET_KEY,
|
if not _data_source.tileset_path :
|
||||||
secure=False
|
|
||||||
)
|
|
||||||
script_dir = os.path.dirname(os.path.abspath(__file__))
|
|
||||||
success, entry_local_path = manager.download_full_tileset(
|
|
||||||
tileset_url=url,
|
|
||||||
save_dir=f"data_3dtiles",
|
|
||||||
region_filter=None
|
|
||||||
)
|
|
||||||
if not success :
|
|
||||||
logger.info(f"下载地图数据失败: {url},{region_coords}")
|
logger.info(f"下载地图数据失败: {url},{region_coords}")
|
||||||
return "下载地图数据失败", None
|
return "下载地图数据失败", None
|
||||||
|
|
||||||
tileset_path = entry_local_path
|
tileset_path = _data_source.tileset_path
|
||||||
|
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
dem_path = os.path.join(script_dir, f"o_dem_{uuid.uuid4().hex[:8]}.tif")
|
dem_path = os.path.join(script_dir, f"o_dem_{uuid.uuid4().hex[:8]}.tif")
|
||||||
data_3dtiles_to_dem.generate_dem(tileset_path, dem_path, region_coords)
|
data_3dtiles_to_dem.generate_dem(tileset_path, dem_path, region_coords)
|
||||||
|
|
||||||
if not os.path.exists(dem_path) :
|
if not os.path.exists(dem_path) :
|
||||||
logger.info(f"生成坡度坡向俯视图失败: {url},{region_coords}")
|
logger.info(f"生成坡度坡向tif失败: {url},{region_coords}")
|
||||||
return "生成坡度坡向俯视图失败", None
|
return "生成坡度坡向tif失败", None
|
||||||
|
|
||||||
slope_aspect_tif_path = os.path.join(script_dir, slope_aspect_tif_name)
|
slope_aspect_tif_path = os.path.join(script_dir, slope_aspect_tif_name)
|
||||||
slope_aspect_tif.create_slope_aspect(dem_path, 'combined', slope_aspect_tif_path)
|
slope_aspect_tif.create_slope_aspect(dem_path, 'combined', slope_aspect_tif_path)
|
||||||
logger.info(f"生成成功: {url},{region_coords},{slope_aspect_tif_path}")
|
logger.info(f"生成成功: {url},{region_coords},{slope_aspect_tif_path}")
|
||||||
|
|
||||||
entry_bucket, _ = manager.parse_minio_url(url);
|
entry_bucket, _ = _data_source.parse_minio_url(url);
|
||||||
success, minio_path = manager.upload_file(entry_bucket, f"{minio_sub_path}/{slope_aspect_tif_name}", slope_aspect_tif_path)
|
success, minio_path = _data_source.upload_file(entry_bucket, f"{minio_sub_path}/{slope_aspect_tif_name}", slope_aspect_tif_path)
|
||||||
if success :
|
if success :
|
||||||
return "生成成功", minio_path
|
return "生成成功", minio_path
|
||||||
else :
|
else :
|
||||||
|
|||||||
@ -5,148 +5,71 @@ import asyncio
|
|||||||
from concurrent.futures import ThreadPoolExecutor
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
from b3dm.data_3dtiles_manager import MinIO3DTilesManager
|
||||||
|
import b3dm.data_3dtiles_to_dem as data_3dtiles_to_dem
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
ENDPOINT_URL = "222.212.85.86:9000"
|
||||||
|
ACCESS_KEY = "WuRenJi"
|
||||||
|
SECRET_KEY = "WRJ@2024"
|
||||||
|
|
||||||
class TilesetDataSource:
|
class TilesetDataSource:
|
||||||
"""使用py3dtiles库的数据源"""
|
"""使用py3dtiles库的数据源"""
|
||||||
|
|
||||||
def __init__(self, tileset_path: str, cache_size: int = 1000):
|
def __init__(self, url: str, cache_size: int = 1000):
|
||||||
self.tileset_path = os.path.abspath(tileset_path)
|
self.url = url
|
||||||
self.tileset_dir = os.path.dirname(self.tileset_path)
|
self.tileset_path = None
|
||||||
self.cache_size = cache_size
|
self.tileset_dir = None
|
||||||
self._tileset = None
|
|
||||||
self._point_cache = {}
|
|
||||||
self._executor = ThreadPoolExecutor(max_workers=4)
|
|
||||||
self._crs = "EPSG:4979"
|
self._crs = "EPSG:4979"
|
||||||
|
|
||||||
async def initialize(self):
|
def parse_minio_url(self, url):
|
||||||
"""初始化"""
|
manager = MinIO3DTilesManager(
|
||||||
try:
|
endpoint_url=ENDPOINT_URL,
|
||||||
# 尝试导入py3dtiles
|
access_key=ACCESS_KEY,
|
||||||
try:
|
secret_key=SECRET_KEY,
|
||||||
import py3dtiles
|
secure=False
|
||||||
from py3dtiles.tileset import TileSet
|
)
|
||||||
except ImportError:
|
return manager.parse_minio_url(url)
|
||||||
logger.warning("py3dtiles未安装,将使用简化数据源")
|
|
||||||
raise ImportError("请安装py3dtiles: pip install py3dtiles")
|
|
||||||
|
|
||||||
loop = asyncio.get_event_loop()
|
|
||||||
self._tileset = await loop.run_in_executor(
|
|
||||||
self._executor,
|
|
||||||
TileSet.from_file,
|
|
||||||
self.tileset_path
|
|
||||||
)
|
|
||||||
|
|
||||||
logger.info(f"py3dtiles数据源初始化完成: {self.tileset_path}")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"py3dtiles初始化失败: {str(e)}")
|
|
||||||
# 回退到简化数据源
|
|
||||||
self._tileset = None
|
|
||||||
|
|
||||||
async def get_points_in_polygon(self,
|
def upload_file(self, bucket_name, object_name, file_path):
|
||||||
polygon_coords: List[List[float]],
|
manager = MinIO3DTilesManager(
|
||||||
z_range: Optional[Tuple[float, float]] = None) -> np.ndarray:
|
endpoint_url=ENDPOINT_URL,
|
||||||
"""获取点数据"""
|
access_key=ACCESS_KEY,
|
||||||
if self._tileset is None:
|
secret_key=SECRET_KEY,
|
||||||
# 使用简化数据源
|
secure=False
|
||||||
return await self._get_simulated_points(polygon_coords, z_range)
|
)
|
||||||
|
flag, path = manager.upload_file(bucket_name, object_name, file_path)
|
||||||
|
if flag :
|
||||||
|
os.remove(file_path)
|
||||||
|
return flag, path
|
||||||
|
|
||||||
try:
|
|
||||||
# 使用py3dtiles API获取数据
|
def dowload_map_data(self, url: str) :
|
||||||
points = []
|
# 下载3dtiles地图数据
|
||||||
polygon_np = np.array(polygon_coords)
|
manager = MinIO3DTilesManager(
|
||||||
|
endpoint_url=ENDPOINT_URL,
|
||||||
# 遍历tileset中的所有tile
|
access_key=ACCESS_KEY,
|
||||||
for tile in self._tileset.root_tile.traverse():
|
secret_key=SECRET_KEY,
|
||||||
tile_points = self._extract_tile_points(tile)
|
secure=False
|
||||||
if tile_points.size > 0:
|
)
|
||||||
# 筛选多边形内的点
|
success, tileset_path = manager.download_full_tileset(
|
||||||
points_in_polygon = self._filter_points_by_polygon(tile_points, polygon_np)
|
tileset_url=url,
|
||||||
if points_in_polygon.size > 0:
|
save_dir=f"data_3dtiles",
|
||||||
points.append(points_in_polygon)
|
region_filter=None
|
||||||
|
)
|
||||||
if points:
|
if success :
|
||||||
all_points = np.vstack(points)
|
self.tileset_path = os.path.abspath(tileset_path)
|
||||||
if z_range:
|
self.tileset_dir = os.path.dirname(tileset_path)
|
||||||
mask = (all_points[:, 2] >= z_range[0]) & (all_points[:, 2] <= z_range[1])
|
|
||||||
all_points = all_points[mask]
|
|
||||||
return all_points
|
|
||||||
|
|
||||||
return np.array([])
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"py3dtiles获取数据失败: {str(e)}")
|
|
||||||
return await self._get_simulated_points(polygon_coords, z_range)
|
|
||||||
|
|
||||||
def _extract_tile_points(self, tile) -> np.ndarray:
|
async def get_points_in_polygon(self, polygon_coords, z_range=None):
|
||||||
"""从tile提取点数据"""
|
"""获取多边形内的点数据"""
|
||||||
try:
|
points = data_3dtiles_to_dem.parse_tileset(self.tileset_path, polygon_coords)
|
||||||
if hasattr(tile, 'content') and tile.content:
|
return np.array(points)
|
||||||
# 尝试获取点数据
|
|
||||||
if hasattr(tile.content, 'points'):
|
|
||||||
return tile.content.points.positions
|
|
||||||
elif hasattr(tile.content, 'body'):
|
|
||||||
# 处理其他格式
|
|
||||||
return np.array([])
|
|
||||||
return np.array([])
|
|
||||||
except:
|
|
||||||
return np.array([])
|
|
||||||
|
|
||||||
def _filter_points_by_polygon(self, points: np.ndarray, polygon: np.ndarray) -> np.ndarray:
|
|
||||||
"""筛选多边形内的点"""
|
|
||||||
from matplotlib.path import Path
|
|
||||||
if points.size == 0:
|
|
||||||
return points
|
|
||||||
|
|
||||||
path = Path(polygon[:, :2])
|
|
||||||
mask = path.contains_points(points[:, :2])
|
|
||||||
return points[mask]
|
|
||||||
|
|
||||||
async def _get_simulated_points(self,
|
|
||||||
polygon_coords: List[List[float]],
|
|
||||||
z_range: Optional[Tuple[float, float]] = None) -> np.ndarray:
|
|
||||||
"""获取模拟点数据"""
|
|
||||||
# 同上一个类的生成模拟数据方法
|
|
||||||
polygon_np = np.array(polygon_coords)
|
|
||||||
|
|
||||||
if polygon_np.shape[0] < 3:
|
|
||||||
return np.array([])
|
|
||||||
|
|
||||||
x_min, y_min = polygon_np.min(axis=0)
|
|
||||||
x_max, y_max = polygon_np.max(axis=0)
|
|
||||||
|
|
||||||
grid_size = 100
|
|
||||||
x = np.linspace(x_min, x_max, grid_size)
|
|
||||||
y = np.linspace(y_min, y_max, grid_size)
|
|
||||||
xx, yy = np.meshgrid(x, y)
|
|
||||||
|
|
||||||
points = np.column_stack([xx.ravel(), yy.ravel()])
|
|
||||||
|
|
||||||
np.random.seed(42)
|
|
||||||
base_elevation = 50.0
|
|
||||||
terrain_variation = 10.0
|
|
||||||
z = base_elevation + terrain_variation * np.sin(0.1 * xx).ravel() * np.cos(0.1 * yy).ravel()
|
|
||||||
|
|
||||||
points = np.column_stack([points[:, 0], points[:, 1], z])
|
|
||||||
|
|
||||||
from matplotlib.path import Path
|
|
||||||
path = Path(polygon_np[:, :2])
|
|
||||||
mask = path.contains_points(points[:, :2])
|
|
||||||
points = points[mask]
|
|
||||||
|
|
||||||
if z_range:
|
|
||||||
mask = (points[:, 2] >= z_range[0]) & (points[:, 2] <= z_range[1])
|
|
||||||
points = points[mask]
|
|
||||||
|
|
||||||
logger.info(f"生成 {points.shape[0]} 个模拟点")
|
|
||||||
return points
|
|
||||||
|
|
||||||
async def get_data_bounds(self) -> Dict[str, List[float]]:
|
async def get_data_bounds(self) -> Dict[str, List[float]]:
|
||||||
"""获取数据边界"""
|
"""获取数据边界"""
|
||||||
if self._tileset is None:
|
|
||||||
await self.initialize()
|
|
||||||
|
|
||||||
bounds = {
|
bounds = {
|
||||||
"min": [float('inf'), float('inf'), float('inf')],
|
"min": [float('inf'), float('inf'), float('inf')],
|
||||||
@ -164,4 +87,19 @@ class TilesetDataSource:
|
|||||||
return bounds
|
return bounds
|
||||||
|
|
||||||
def get_crs(self) -> str:
|
def get_crs(self) -> str:
|
||||||
return self._crs
|
return self._crs
|
||||||
|
|
||||||
|
async def main() :
|
||||||
|
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
SCRIPT_PAR_DIR = os.path.dirname(SCRIPT_DIR)
|
||||||
|
tileset_path = os.path.join(SCRIPT_PAR_DIR, "data/3dtiles/tileset.json")
|
||||||
|
data_source_3d_tiles = TilesetDataSource(tileset_path)
|
||||||
|
# tileSet = TileSet()
|
||||||
|
# path = Path(tileset_path)
|
||||||
|
# tileset_data = tileSet.from_file(path)
|
||||||
|
print("====================================================")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
loop = asyncio.get_event_loop()
|
||||||
|
loop.run_until_complete(main())
|
||||||
Loading…
x
Reference in New Issue
Block a user