673 lines
27 KiB
Python
673 lines
27 KiB
Python
from __future__ import annotations
|
|
|
|
import os
|
|
import re
|
|
from collections.abc import Callable
|
|
from pathlib import Path
|
|
from typing import Annotated, Any, Final
|
|
|
|
from fastapi import APIRouter, HTTPException, Query, Response
|
|
|
|
from k1link.compute import (
|
|
LidarFieldReviewV1,
|
|
LidarGroundBenchmarkV1,
|
|
LidarGroundError,
|
|
LidarReplayError,
|
|
LidarReplayPackV2,
|
|
lidar_field_review_catalog_item,
|
|
lidar_ground_benchmark_catalog_item,
|
|
lidar_ground_frame_detail,
|
|
lidar_pack_catalog_item,
|
|
lidar_pack_detail,
|
|
)
|
|
from k1link.datasets import (
|
|
RELLIS_SOURCE_ID,
|
|
DatasetAdmissionError,
|
|
configured_dataset_admission_manifest,
|
|
configured_dataset_ground_preview,
|
|
configured_dataset_preview,
|
|
dataset_gateway_catalog,
|
|
read_dataset_ground_preview,
|
|
read_dataset_native_scan_preview,
|
|
)
|
|
from k1link.web.lidar_local_surface_service import (
|
|
K1LocalSurfaceReadService,
|
|
LocalSurfaceSourceUnavailable,
|
|
)
|
|
|
|
LIDAR_CATALOG_SCHEMA: Final = "missioncore.lidar-replay-pack-catalog/v1"
|
|
LIDAR_GROUND_CATALOG_SCHEMA: Final = "missioncore.lidar-ground-benchmark-catalog/v1"
|
|
LIDAR_FIELD_REVIEW_CATALOG_SCHEMA: Final = "missioncore.lidar-field-review-catalog/v1"
|
|
K1_LOCAL_SURFACE_CATALOG_SCHEMA: Final = "missioncore.k1-local-surface-catalog/v1"
|
|
_PACK_ID = re.compile(r"^lidar-replay-pack-[a-f0-9]{64}$")
|
|
_BENCHMARK_ID = re.compile(r"^ground-benchmark-[a-f0-9]{64}$")
|
|
_FIELD_REVIEW_ID = re.compile(r"^lidar-field-review-[a-f0-9]{64}$")
|
|
_LOCAL_SURFACE_ID = re.compile(r"^k1-local-surface-[a-f0-9]{64}$")
|
|
RootProvider = Callable[[], Path | None]
|
|
DatasetArtifactProvider = Callable[[], Path | None]
|
|
|
|
|
|
def configured_lidar_replay_root() -> Path | None:
|
|
value = os.environ.get("MISSIONCORE_LIDAR_REPLAY_ROOT", "").strip()
|
|
return Path(value).expanduser().absolute() if value else None
|
|
|
|
|
|
def configured_lidar_ground_root() -> Path | None:
|
|
value = os.environ.get("MISSIONCORE_LIDAR_GROUND_ROOT", "").strip()
|
|
return Path(value).expanduser().absolute() if value else None
|
|
|
|
|
|
def configured_lidar_field_review_root() -> Path | None:
|
|
value = os.environ.get("MISSIONCORE_LIDAR_FIELD_REVIEW_ROOT", "").strip()
|
|
return Path(value).expanduser().absolute() if value else None
|
|
|
|
|
|
def configured_k1_local_surface_root() -> Path | None:
|
|
value = os.environ.get("MISSIONCORE_K1_LOCAL_SURFACE_ROOT", "").strip()
|
|
return Path(value).expanduser().absolute() if value else None
|
|
|
|
|
|
def configured_e10_lidar_source_root() -> Path | None:
|
|
value = os.environ.get("MISSIONCORE_E10_LIDAR_SOURCE_ROOT", "").strip()
|
|
return Path(value).expanduser().absolute() if value else None
|
|
|
|
|
|
def build_lidar_router(
|
|
*,
|
|
root_provider: RootProvider = configured_lidar_replay_root,
|
|
ground_root_provider: RootProvider = configured_lidar_ground_root,
|
|
field_review_root_provider: RootProvider = configured_lidar_field_review_root,
|
|
local_surface_root_provider: RootProvider = configured_k1_local_surface_root,
|
|
e10_source_root_provider: RootProvider = configured_e10_lidar_source_root,
|
|
dataset_admission_provider: DatasetArtifactProvider = configured_dataset_admission_manifest,
|
|
dataset_preview_provider: DatasetArtifactProvider = configured_dataset_preview,
|
|
dataset_rellis_preview_provider: DatasetArtifactProvider = lambda: None,
|
|
dataset_rellis_admission_provider: DatasetArtifactProvider = lambda: None,
|
|
dataset_ground_preview_provider: DatasetArtifactProvider = configured_dataset_ground_preview,
|
|
local_surface_read_service: K1LocalSurfaceReadService | None = None,
|
|
) -> APIRouter:
|
|
router = APIRouter(prefix="/api/v1/lidar", tags=["lidar"])
|
|
surface_reader = local_surface_read_service or K1LocalSurfaceReadService()
|
|
|
|
@router.get("/dataset-gateway")
|
|
def get_dataset_gateway() -> dict[str, object]:
|
|
return dataset_gateway_catalog(
|
|
admission_manifest_path=dataset_admission_provider(),
|
|
rellis_preview_path=dataset_rellis_preview_provider(),
|
|
rellis_admission_path=dataset_rellis_admission_provider(),
|
|
)
|
|
|
|
@router.get("/dataset-gateway/preview")
|
|
def get_dataset_gateway_preview(
|
|
source_id: Annotated[
|
|
str,
|
|
Query(min_length=1, max_length=160),
|
|
] = "goose-3d/v2025-08-22",
|
|
) -> dict[str, Any]:
|
|
if source_id == "goose-3d/v2025-08-22":
|
|
path = dataset_preview_provider()
|
|
elif source_id == RELLIS_SOURCE_ID:
|
|
path = dataset_rellis_preview_provider()
|
|
else:
|
|
raise HTTPException(status_code=404, detail="Dataset source не найден.")
|
|
if path is None or not path.is_file():
|
|
raise HTTPException(
|
|
status_code=404,
|
|
detail="Размеченный native scan источника ещё не импортирован.",
|
|
)
|
|
try:
|
|
preview = read_dataset_native_scan_preview(path)
|
|
if preview["source_id"] != source_id:
|
|
raise DatasetAdmissionError("dataset preview source mismatch")
|
|
return preview
|
|
except DatasetAdmissionError as exc:
|
|
raise HTTPException(
|
|
status_code=500,
|
|
detail="Preview датасета не прошло проверку целостности.",
|
|
) from exc
|
|
|
|
@router.get("/dataset-gateway/ground-comparison")
|
|
def get_dataset_gateway_ground_comparison() -> dict[str, Any]:
|
|
path = dataset_ground_preview_provider()
|
|
if path is None or not path.is_file():
|
|
raise HTTPException(
|
|
status_code=404,
|
|
detail="Ground baseline ещё не сравнивался с GOOSE-разметкой.",
|
|
)
|
|
try:
|
|
return read_dataset_ground_preview(path)
|
|
except DatasetAdmissionError as exc:
|
|
raise HTTPException(
|
|
status_code=500,
|
|
detail="Ground comparison не прошло проверку целостности.",
|
|
) from exc
|
|
|
|
@router.get("/replay-packs")
|
|
def list_lidar_replay_packs(
|
|
limit: int = Query(default=20, ge=1, le=100),
|
|
) -> dict[str, Any]:
|
|
root = root_provider()
|
|
if root is None or not root.is_dir():
|
|
return {
|
|
"schema_version": LIDAR_CATALOG_SCHEMA,
|
|
"configured": root is not None,
|
|
"items": [],
|
|
"valid_total": 0,
|
|
"invalid_total": 0,
|
|
"duplicate_total": 0,
|
|
"access": "read-only",
|
|
}
|
|
items: list[dict[str, object]] = []
|
|
invalid_total = 0
|
|
duplicate_total = 0
|
|
seen_logical_content: set[str] = set()
|
|
candidates = sorted(
|
|
(
|
|
candidate
|
|
for candidate in root.iterdir()
|
|
if candidate.is_dir() and _PACK_ID.fullmatch(candidate.name) is not None
|
|
),
|
|
key=lambda candidate: candidate.stat().st_mtime_ns,
|
|
reverse=True,
|
|
)
|
|
for candidate in candidates:
|
|
try:
|
|
pack = LidarReplayPackV2(candidate)
|
|
try:
|
|
item = lidar_pack_catalog_item(pack)
|
|
logical_content = str(item["logical_content_sha256"])
|
|
if logical_content in seen_logical_content:
|
|
duplicate_total += 1
|
|
continue
|
|
seen_logical_content.add(logical_content)
|
|
item["created_at_utc"] = pack.manifest.get("created_at_utc")
|
|
items.append(item)
|
|
finally:
|
|
pack.close()
|
|
except (LidarReplayError, OSError):
|
|
invalid_total += 1
|
|
return {
|
|
"schema_version": LIDAR_CATALOG_SCHEMA,
|
|
"configured": True,
|
|
"items": items[:limit],
|
|
"valid_total": len(items),
|
|
"invalid_total": invalid_total,
|
|
"duplicate_total": duplicate_total,
|
|
"access": "read-only",
|
|
}
|
|
|
|
@router.get("/replay-packs/{pack_id}")
|
|
def get_lidar_replay_pack(pack_id: str) -> dict[str, object]:
|
|
if _PACK_ID.fullmatch(pack_id) is None:
|
|
raise HTTPException(status_code=404, detail="LiDAR replay pack не найден")
|
|
root = root_provider()
|
|
if root is None or not root.is_dir():
|
|
raise HTTPException(status_code=503, detail="LiDAR replay storage не настроен")
|
|
candidate = root / pack_id
|
|
if not candidate.is_dir():
|
|
raise HTTPException(status_code=404, detail="LiDAR replay pack не найден")
|
|
try:
|
|
pack = LidarReplayPackV2(candidate)
|
|
try:
|
|
return lidar_pack_detail(pack)
|
|
finally:
|
|
pack.close()
|
|
except (LidarReplayError, OSError) as exc:
|
|
raise HTTPException(
|
|
status_code=409,
|
|
detail="LiDAR replay pack не прошёл проверку целостности",
|
|
) from exc
|
|
|
|
@router.get("/ground-benchmarks")
|
|
def list_lidar_ground_benchmarks(
|
|
pack_id: str | None = Query(default=None),
|
|
limit: int = Query(default=20, ge=1, le=100),
|
|
) -> dict[str, Any]:
|
|
if pack_id is not None and _PACK_ID.fullmatch(pack_id) is None:
|
|
raise HTTPException(status_code=404, detail="LiDAR replay pack не найден")
|
|
root = ground_root_provider()
|
|
if root is None or not root.is_dir():
|
|
return {
|
|
"schema_version": LIDAR_GROUND_CATALOG_SCHEMA,
|
|
"configured": root is not None,
|
|
"items": [],
|
|
"valid_total": 0,
|
|
"invalid_total": 0,
|
|
"access": "read-only",
|
|
}
|
|
items: list[dict[str, object]] = []
|
|
invalid_total = 0
|
|
candidates = sorted(
|
|
(
|
|
candidate
|
|
for candidate in root.iterdir()
|
|
if candidate.is_dir() and _BENCHMARK_ID.fullmatch(candidate.name) is not None
|
|
),
|
|
key=lambda candidate: candidate.stat().st_mtime_ns,
|
|
reverse=True,
|
|
)
|
|
for candidate in candidates:
|
|
try:
|
|
benchmark = LidarGroundBenchmarkV1(candidate)
|
|
try:
|
|
if pack_id is None or benchmark.identity.get("replay_pack_id") == pack_id:
|
|
items.append(lidar_ground_benchmark_catalog_item(benchmark))
|
|
finally:
|
|
benchmark.close()
|
|
except (LidarGroundError, OSError):
|
|
invalid_total += 1
|
|
return {
|
|
"schema_version": LIDAR_GROUND_CATALOG_SCHEMA,
|
|
"configured": True,
|
|
"items": items[:limit],
|
|
"valid_total": len(items),
|
|
"invalid_total": invalid_total,
|
|
"access": "read-only",
|
|
}
|
|
|
|
@router.get("/ground-benchmarks/{benchmark_id}")
|
|
def get_lidar_ground_benchmark(benchmark_id: str) -> dict[str, object]:
|
|
if _BENCHMARK_ID.fullmatch(benchmark_id) is None:
|
|
raise HTTPException(
|
|
status_code=404,
|
|
detail="LiDAR ground benchmark не найден",
|
|
)
|
|
root = ground_root_provider()
|
|
if root is None or not root.is_dir():
|
|
raise HTTPException(
|
|
status_code=503,
|
|
detail="LiDAR ground storage не настроен",
|
|
)
|
|
candidate = root / benchmark_id
|
|
if not candidate.is_dir():
|
|
raise HTTPException(
|
|
status_code=404,
|
|
detail="LiDAR ground benchmark не найден",
|
|
)
|
|
try:
|
|
benchmark = LidarGroundBenchmarkV1(candidate)
|
|
try:
|
|
return {
|
|
"schema_version": ("missioncore.lidar-ground-benchmark-detail/v1"),
|
|
"benchmark": lidar_ground_benchmark_catalog_item(benchmark),
|
|
"report": benchmark.report,
|
|
"access": "read-only",
|
|
}
|
|
finally:
|
|
benchmark.close()
|
|
except (LidarGroundError, OSError) as exc:
|
|
raise HTTPException(
|
|
status_code=409,
|
|
detail="LiDAR ground benchmark не прошёл проверку целостности",
|
|
) from exc
|
|
|
|
@router.get("/ground-benchmarks/{benchmark_id}/frames/{frame_index}")
|
|
def get_lidar_ground_frame(
|
|
benchmark_id: str,
|
|
frame_index: int,
|
|
) -> dict[str, object]:
|
|
if _BENCHMARK_ID.fullmatch(benchmark_id) is None or frame_index < 0:
|
|
raise HTTPException(
|
|
status_code=404,
|
|
detail="LiDAR ground frame не найден",
|
|
)
|
|
ground_root = ground_root_provider()
|
|
replay_root = root_provider()
|
|
if ground_root is None or not ground_root.is_dir():
|
|
raise HTTPException(
|
|
status_code=503,
|
|
detail="LiDAR ground storage не настроен",
|
|
)
|
|
if replay_root is None or not replay_root.is_dir():
|
|
raise HTTPException(
|
|
status_code=503,
|
|
detail="LiDAR replay storage не настроен",
|
|
)
|
|
benchmark_path = ground_root / benchmark_id
|
|
if not benchmark_path.is_dir():
|
|
raise HTTPException(
|
|
status_code=404,
|
|
detail="LiDAR ground benchmark не найден",
|
|
)
|
|
try:
|
|
benchmark = LidarGroundBenchmarkV1(benchmark_path)
|
|
try:
|
|
replay_pack_id = benchmark.identity.get("replay_pack_id")
|
|
if (
|
|
not isinstance(replay_pack_id, str)
|
|
or _PACK_ID.fullmatch(replay_pack_id) is None
|
|
):
|
|
raise LidarGroundError("Ground benchmark replay identity is invalid")
|
|
replay_path = replay_root / replay_pack_id
|
|
if not replay_path.is_dir():
|
|
raise HTTPException(
|
|
status_code=404,
|
|
detail="Связанный LiDAR replay pack не найден",
|
|
)
|
|
replay = LidarReplayPackV2(replay_path)
|
|
try:
|
|
return lidar_ground_frame_detail(
|
|
benchmark,
|
|
replay,
|
|
frame_index,
|
|
)
|
|
finally:
|
|
replay.close()
|
|
finally:
|
|
benchmark.close()
|
|
except IndexError as exc:
|
|
raise HTTPException(
|
|
status_code=404,
|
|
detail="LiDAR ground frame не найден",
|
|
) from exc
|
|
except HTTPException:
|
|
raise
|
|
except (LidarGroundError, LidarReplayError, OSError) as exc:
|
|
raise HTTPException(
|
|
status_code=409,
|
|
detail="LiDAR ground frame не прошёл проверку целостности",
|
|
) from exc
|
|
|
|
@router.get("/field-reviews")
|
|
def list_lidar_field_reviews(
|
|
limit: int = Query(default=10, ge=1, le=50),
|
|
) -> dict[str, Any]:
|
|
root = field_review_root_provider()
|
|
if root is None or not root.is_dir():
|
|
return {
|
|
"schema_version": LIDAR_FIELD_REVIEW_CATALOG_SCHEMA,
|
|
"configured": root is not None,
|
|
"items": [],
|
|
"valid_total": 0,
|
|
"invalid_total": 0,
|
|
"access": "read-only",
|
|
}
|
|
items: list[dict[str, object]] = []
|
|
invalid_total = 0
|
|
candidates = sorted(
|
|
(
|
|
candidate
|
|
for candidate in root.iterdir()
|
|
if candidate.is_dir() and _FIELD_REVIEW_ID.fullmatch(candidate.name) is not None
|
|
),
|
|
key=lambda candidate: candidate.stat().st_mtime_ns,
|
|
reverse=True,
|
|
)
|
|
for candidate in candidates:
|
|
try:
|
|
review = LidarFieldReviewV1(candidate)
|
|
try:
|
|
items.append(lidar_field_review_catalog_item(review))
|
|
finally:
|
|
review.close()
|
|
except (LidarGroundError, OSError):
|
|
invalid_total += 1
|
|
return {
|
|
"schema_version": LIDAR_FIELD_REVIEW_CATALOG_SCHEMA,
|
|
"configured": True,
|
|
"items": items[:limit],
|
|
"valid_total": len(items),
|
|
"invalid_total": invalid_total,
|
|
"access": "read-only",
|
|
}
|
|
|
|
@router.get("/field-reviews/{review_id}/windows/{window_index}")
|
|
def get_lidar_field_review_window(
|
|
review_id: str,
|
|
window_index: int,
|
|
) -> dict[str, object]:
|
|
if _FIELD_REVIEW_ID.fullmatch(review_id) is None or window_index < 0:
|
|
raise HTTPException(
|
|
status_code=404,
|
|
detail="LiDAR field-review window не найден",
|
|
)
|
|
root = field_review_root_provider()
|
|
if root is None or not root.is_dir():
|
|
raise HTTPException(
|
|
status_code=503,
|
|
detail="LiDAR field-review storage не настроен",
|
|
)
|
|
candidate = root / review_id
|
|
if not candidate.is_dir():
|
|
raise HTTPException(
|
|
status_code=404,
|
|
detail="LiDAR field review не найден",
|
|
)
|
|
try:
|
|
review = LidarFieldReviewV1(candidate)
|
|
try:
|
|
return review.window_detail(window_index)
|
|
finally:
|
|
review.close()
|
|
except IndexError as exc:
|
|
raise HTTPException(
|
|
status_code=404,
|
|
detail="LiDAR field-review window не найден",
|
|
) from exc
|
|
except (LidarGroundError, OSError) as exc:
|
|
raise HTTPException(
|
|
status_code=409,
|
|
detail="LiDAR field review не прошёл проверку целостности",
|
|
) from exc
|
|
|
|
@router.get("/field-reviews/{review_id}/windows/{window_index}/preview")
|
|
def get_lidar_field_review_preview(
|
|
review_id: str,
|
|
window_index: int,
|
|
) -> Response:
|
|
if _FIELD_REVIEW_ID.fullmatch(review_id) is None or window_index < 0:
|
|
raise HTTPException(
|
|
status_code=404,
|
|
detail="LiDAR field-review preview не найден",
|
|
)
|
|
root = field_review_root_provider()
|
|
if root is None or not root.is_dir():
|
|
raise HTTPException(
|
|
status_code=503,
|
|
detail="LiDAR field-review storage не настроен",
|
|
)
|
|
candidate = root / review_id
|
|
if not candidate.is_dir():
|
|
raise HTTPException(
|
|
status_code=404,
|
|
detail="LiDAR field review не найден",
|
|
)
|
|
try:
|
|
review = LidarFieldReviewV1(candidate)
|
|
try:
|
|
windows = review.report.get("windows")
|
|
if not isinstance(windows, list) or not 0 <= window_index < len(windows):
|
|
raise IndexError(window_index)
|
|
window = windows[window_index]
|
|
if not isinstance(window, dict) or not isinstance(window.get("key"), str):
|
|
raise LidarGroundError("LiDAR field-review preview key is invalid")
|
|
preview = review.preview_paths.get(window["key"])
|
|
if preview is None:
|
|
raise LidarGroundError("LiDAR field-review preview is missing")
|
|
content = preview.read_bytes()
|
|
finally:
|
|
review.close()
|
|
except IndexError as exc:
|
|
raise HTTPException(
|
|
status_code=404,
|
|
detail="LiDAR field-review preview не найден",
|
|
) from exc
|
|
except (LidarGroundError, OSError) as exc:
|
|
raise HTTPException(
|
|
status_code=409,
|
|
detail="LiDAR field-review preview не прошёл проверку",
|
|
) from exc
|
|
return Response(
|
|
content=content,
|
|
media_type="image/jpeg",
|
|
headers={"Cache-Control": "private, max-age=31536000, immutable"},
|
|
)
|
|
|
|
@router.get("/local-surfaces")
|
|
def list_k1_local_surfaces(
|
|
limit: int = Query(default=10, ge=1, le=50),
|
|
) -> dict[str, Any]:
|
|
root = local_surface_root_provider()
|
|
if root is None or not root.is_dir():
|
|
return {
|
|
"schema_version": K1_LOCAL_SURFACE_CATALOG_SCHEMA,
|
|
"configured": root is not None,
|
|
"items": [],
|
|
"valid_total": 0,
|
|
"invalid_total": 0,
|
|
"access": "read-only",
|
|
}
|
|
items: list[dict[str, object]] = []
|
|
invalid_total = 0
|
|
candidates = sorted(
|
|
(
|
|
candidate
|
|
for candidate in root.iterdir()
|
|
if candidate.is_dir()
|
|
and _LOCAL_SURFACE_ID.fullmatch(candidate.name) is not None
|
|
),
|
|
key=lambda candidate: candidate.stat().st_mtime_ns,
|
|
reverse=True,
|
|
)
|
|
for candidate in candidates:
|
|
if len(items) >= limit:
|
|
break
|
|
try:
|
|
items.append(surface_reader.catalog_item(candidate))
|
|
except (LidarGroundError, OSError):
|
|
invalid_total += 1
|
|
return {
|
|
"schema_version": K1_LOCAL_SURFACE_CATALOG_SCHEMA,
|
|
"configured": True,
|
|
"items": items,
|
|
"valid_total": len(items),
|
|
"invalid_total": invalid_total,
|
|
"candidate_total": len(candidates),
|
|
"scan_complete": len(items) + invalid_total == len(candidates),
|
|
"access": "read-only",
|
|
}
|
|
|
|
@router.get("/local-surfaces/{model_id}/timeline")
|
|
def get_k1_local_surface_timeline(model_id: str) -> dict[str, object]:
|
|
if _LOCAL_SURFACE_ID.fullmatch(model_id) is None:
|
|
raise HTTPException(
|
|
status_code=404,
|
|
detail="K1 local-surface timeline не найден",
|
|
)
|
|
model_root = local_surface_root_provider()
|
|
source_root = e10_source_root_provider()
|
|
if model_root is None or not model_root.is_dir():
|
|
raise HTTPException(
|
|
status_code=503,
|
|
detail="K1 local-surface storage не настроен",
|
|
)
|
|
if source_root is None or not source_root.is_dir():
|
|
raise HTTPException(
|
|
status_code=503,
|
|
detail="E10 LiDAR source storage не настроен",
|
|
)
|
|
model_path = model_root / model_id
|
|
if not model_path.is_dir():
|
|
raise HTTPException(
|
|
status_code=404,
|
|
detail="K1 local-surface model не найден",
|
|
)
|
|
try:
|
|
return surface_reader.timeline_detail(model_path, source_root)
|
|
except LocalSurfaceSourceUnavailable as exc:
|
|
raise HTTPException(
|
|
status_code=404,
|
|
detail="Связанный E10 LiDAR source не найден",
|
|
) from exc
|
|
except (LidarGroundError, OSError) as exc:
|
|
raise HTTPException(
|
|
status_code=409,
|
|
detail="K1 local-surface timeline не прошёл проверку целостности",
|
|
) from exc
|
|
|
|
@router.get("/local-surfaces/{model_id}/review")
|
|
def get_k1_local_surface_review(model_id: str) -> dict[str, object]:
|
|
if _LOCAL_SURFACE_ID.fullmatch(model_id) is None:
|
|
raise HTTPException(
|
|
status_code=404,
|
|
detail="K1 local-surface review не найден",
|
|
)
|
|
model_root = local_surface_root_provider()
|
|
source_root = e10_source_root_provider()
|
|
if model_root is None or not model_root.is_dir():
|
|
raise HTTPException(
|
|
status_code=503,
|
|
detail="K1 local-surface storage не настроен",
|
|
)
|
|
if source_root is None or not source_root.is_dir():
|
|
raise HTTPException(
|
|
status_code=503,
|
|
detail="E10 LiDAR source storage не настроен",
|
|
)
|
|
model_path = model_root / model_id
|
|
if not model_path.is_dir():
|
|
raise HTTPException(
|
|
status_code=404,
|
|
detail="K1 local-surface model не найден",
|
|
)
|
|
try:
|
|
return surface_reader.review_detail(model_path, source_root)
|
|
except LocalSurfaceSourceUnavailable as exc:
|
|
raise HTTPException(
|
|
status_code=404,
|
|
detail="Связанный E10 LiDAR source не найден",
|
|
) from exc
|
|
except (LidarGroundError, OSError) as exc:
|
|
raise HTTPException(
|
|
status_code=409,
|
|
detail="K1 local-surface review не прошёл проверку целостности",
|
|
) from exc
|
|
|
|
@router.get("/local-surfaces/{model_id}/frames/{frame_index}")
|
|
def get_k1_local_surface_frame(
|
|
model_id: str,
|
|
frame_index: int,
|
|
) -> dict[str, object]:
|
|
if _LOCAL_SURFACE_ID.fullmatch(model_id) is None or frame_index < 0:
|
|
raise HTTPException(
|
|
status_code=404,
|
|
detail="K1 local-surface frame не найден",
|
|
)
|
|
model_root = local_surface_root_provider()
|
|
source_root = e10_source_root_provider()
|
|
if model_root is None or not model_root.is_dir():
|
|
raise HTTPException(
|
|
status_code=503,
|
|
detail="K1 local-surface storage не настроен",
|
|
)
|
|
if source_root is None or not source_root.is_dir():
|
|
raise HTTPException(
|
|
status_code=503,
|
|
detail="E10 LiDAR source storage не настроен",
|
|
)
|
|
model_path = model_root / model_id
|
|
if not model_path.is_dir():
|
|
raise HTTPException(
|
|
status_code=404,
|
|
detail="K1 local-surface model не найден",
|
|
)
|
|
try:
|
|
return surface_reader.frame_detail(model_path, source_root, frame_index)
|
|
except IndexError as exc:
|
|
raise HTTPException(
|
|
status_code=404,
|
|
detail="K1 local-surface frame не найден",
|
|
) from exc
|
|
except LocalSurfaceSourceUnavailable as exc:
|
|
raise HTTPException(
|
|
status_code=404,
|
|
detail="Связанный E10 LiDAR source не найден",
|
|
) from exc
|
|
except (LidarGroundError, OSError) as exc:
|
|
raise HTTPException(
|
|
status_code=409,
|
|
detail="K1 local-surface evidence не прошло проверку целостности",
|
|
) from exc
|
|
|
|
return router
|