@@ -13,6 +13,7 @@ from ...schemas.elevation import (
|
||||
ElevationDatasetAnalyzeResponse,
|
||||
ElevationDatasetCreateRequest,
|
||||
ElevationDatasetListResponse,
|
||||
ElevationDatasetPreviewResponse,
|
||||
ElevationDatasetSummary,
|
||||
ElevationDatasetUpdateRequest,
|
||||
)
|
||||
@@ -23,6 +24,7 @@ from ...services.elevation_service import (
|
||||
get_job_by_id,
|
||||
list_datasets,
|
||||
list_jobs,
|
||||
preview_dataset,
|
||||
serialize_job,
|
||||
update_dataset,
|
||||
)
|
||||
@@ -78,6 +80,20 @@ def analyze_elevation_dataset(
|
||||
return analyze_dataset(db, dataset_id=dataset_id, actor=current_user.user)
|
||||
|
||||
|
||||
@router.get("/datasets/{dataset_id}/preview", response_model=ElevationDatasetPreviewResponse)
|
||||
def preview_elevation_dataset(
|
||||
dataset_id: str,
|
||||
max_points: int = Query(default=1500, ge=1, le=5000),
|
||||
_: CurrentUser = Depends(require_any_permission("elevation.read", "elevation.manage")),
|
||||
db: Session = Depends(get_db),
|
||||
) -> ElevationDatasetPreviewResponse:
|
||||
return preview_dataset(
|
||||
db,
|
||||
dataset_id=dataset_id,
|
||||
max_points=max_points,
|
||||
)
|
||||
|
||||
|
||||
@router.get("/jobs", response_model=ElevationApplyJobListResponse)
|
||||
def get_elevation_jobs(
|
||||
line_id: str | None = Query(default=None),
|
||||
|
||||
@@ -61,6 +61,20 @@ class ElevationDatasetAnalyzeResponse(BaseModel):
|
||||
warnings: list[str] = Field(default_factory=list)
|
||||
|
||||
|
||||
class ElevationDatasetPreviewPoint(BaseModel):
|
||||
longitude: float
|
||||
latitude: float
|
||||
altitude_m: float
|
||||
|
||||
|
||||
class ElevationDatasetPreviewResponse(BaseModel):
|
||||
dataset: ElevationDatasetSummary
|
||||
total_points: int
|
||||
sampled_points: int
|
||||
points: list[ElevationDatasetPreviewPoint] = Field(default_factory=list)
|
||||
warnings: list[str] = Field(default_factory=list)
|
||||
|
||||
|
||||
class ElevationApplyJobSummary(BaseModel):
|
||||
id: str
|
||||
line_id: str
|
||||
|
||||
@@ -26,6 +26,8 @@ from ..schemas.elevation import (
|
||||
ElevationDatasetAnalyzeResponse,
|
||||
ElevationDatasetCreateRequest,
|
||||
ElevationDatasetListResponse,
|
||||
ElevationDatasetPreviewPoint,
|
||||
ElevationDatasetPreviewResponse,
|
||||
ElevationDatasetSummary,
|
||||
ElevationDatasetUpdateRequest,
|
||||
)
|
||||
@@ -295,6 +297,40 @@ def analyze_dataset(
|
||||
)
|
||||
|
||||
|
||||
def preview_dataset(
|
||||
db: Session,
|
||||
*,
|
||||
dataset_id: str,
|
||||
max_points: int,
|
||||
) -> ElevationDatasetPreviewResponse:
|
||||
item = get_dataset_by_id(db, dataset_id)
|
||||
if not item:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="高程数据集不存在")
|
||||
if item.status != "active":
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="高程数据集未启用")
|
||||
|
||||
preview_limit = max(1, min(max_points, 5000))
|
||||
file_format = _resolve_dataset_file_format(item)
|
||||
if file_format == "csv":
|
||||
points, warnings = _load_dataset_points(db, item)
|
||||
sampled = _sample_preview_points_from_csv(points=points, limit=preview_limit)
|
||||
return ElevationDatasetPreviewResponse(
|
||||
dataset=serialize_dataset(item),
|
||||
total_points=len(points),
|
||||
sampled_points=len(sampled),
|
||||
points=[ElevationDatasetPreviewPoint(longitude=point.lon, latitude=point.lat, altitude_m=point.altitude_m) for point in sampled],
|
||||
warnings=warnings,
|
||||
)
|
||||
|
||||
if file_format in RASTER_FILE_FORMATS:
|
||||
return _build_raster_preview(db, dataset=item, limit=preview_limit)
|
||||
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"不支持的高程文件格式: {file_format}",
|
||||
)
|
||||
|
||||
|
||||
def list_jobs(
|
||||
db: Session,
|
||||
*,
|
||||
@@ -561,6 +597,22 @@ def _compute_dataset_stats(points: list[ElevationSamplePoint]) -> dict[str, floa
|
||||
}
|
||||
|
||||
|
||||
def _sample_preview_points_from_csv(
|
||||
*,
|
||||
points: list[ElevationSamplePoint],
|
||||
limit: int,
|
||||
) -> list[ElevationSamplePoint]:
|
||||
if len(points) <= limit:
|
||||
return points
|
||||
if limit <= 1:
|
||||
return [points[0]]
|
||||
step = max(1, len(points) // limit)
|
||||
sampled = points[::step]
|
||||
if len(sampled) > limit:
|
||||
sampled = sampled[:limit]
|
||||
return sampled
|
||||
|
||||
|
||||
def _analyze_dataset_content(
|
||||
db: Session,
|
||||
dataset: ElevationDataset,
|
||||
@@ -858,6 +910,95 @@ def _compute_raster_stats(
|
||||
)
|
||||
|
||||
|
||||
def _build_raster_preview(
|
||||
db: Session,
|
||||
*,
|
||||
dataset: ElevationDataset,
|
||||
limit: int,
|
||||
) -> ElevationDatasetPreviewResponse:
|
||||
warnings: list[str] = []
|
||||
with _open_raster_dataset(db, dataset) as opened:
|
||||
rasterio = opened.rasterio
|
||||
src = opened.dataset
|
||||
warning_text = _append_non_wgs84_bounds_warning(rasterio=rasterio, src=src)
|
||||
if warning_text:
|
||||
warnings.append(warning_text)
|
||||
|
||||
width = int(src.width or 0)
|
||||
height = int(src.height or 0)
|
||||
if width <= 0 or height <= 0:
|
||||
return ElevationDatasetPreviewResponse(
|
||||
dataset=serialize_dataset(dataset),
|
||||
total_points=0,
|
||||
sampled_points=0,
|
||||
points=[],
|
||||
warnings=warnings,
|
||||
)
|
||||
|
||||
band_nodata = src.nodatavals[0] if src.nodatavals else None
|
||||
total_points = width * height
|
||||
sampled_points: list[ElevationDatasetPreviewPoint] = []
|
||||
|
||||
target_count = max(1, limit)
|
||||
step = max(1, int((width * height / target_count) ** 0.5))
|
||||
y = 0
|
||||
while y < height and len(sampled_points) < target_count:
|
||||
x = 0
|
||||
while x < width and len(sampled_points) < target_count:
|
||||
try:
|
||||
value = src.read(1, window=((y, y + 1), (x, x + 1)), masked=True)[0][0]
|
||||
except Exception:
|
||||
x += step
|
||||
continue
|
||||
|
||||
if _is_masked_value(value):
|
||||
x += step
|
||||
continue
|
||||
altitude = float(value)
|
||||
if band_nodata is not None and _almost_equal(altitude, float(band_nodata)):
|
||||
x += step
|
||||
continue
|
||||
if not _is_finite_number(altitude):
|
||||
x += step
|
||||
continue
|
||||
|
||||
world_x, world_y = src.xy(y, x)
|
||||
lon = float(world_x)
|
||||
lat = float(world_y)
|
||||
if src.crs and str(src.crs) not in {"EPSG:4326", "OGC:CRS84"}:
|
||||
try:
|
||||
xs, ys = rasterio.warp.transform(src.crs, "EPSG:4326", [lon], [lat])
|
||||
lon = float(xs[0])
|
||||
lat = float(ys[0])
|
||||
except Exception:
|
||||
x += step
|
||||
continue
|
||||
if lon < -180 or lon > 180 or lat < -90 or lat > 90:
|
||||
x += step
|
||||
continue
|
||||
|
||||
sampled_points.append(
|
||||
ElevationDatasetPreviewPoint(
|
||||
longitude=round(lon, 6),
|
||||
latitude=round(lat, 6),
|
||||
altitude_m=round(altitude, 3),
|
||||
)
|
||||
)
|
||||
x += step
|
||||
y += step
|
||||
|
||||
if not sampled_points:
|
||||
warnings.append("未提取到有效预览点(可能为 nodata 或投影不匹配)")
|
||||
|
||||
return ElevationDatasetPreviewResponse(
|
||||
dataset=serialize_dataset(dataset),
|
||||
total_points=total_points,
|
||||
sampled_points=len(sampled_points),
|
||||
points=sampled_points,
|
||||
warnings=warnings,
|
||||
)
|
||||
|
||||
|
||||
def _apply_raster_to_line_towers(
|
||||
db: Session,
|
||||
*,
|
||||
|
||||
Reference in New Issue
Block a user