mirror of
https://github.com/BigBodyCobain/Shadowbroker.git
synced 2026-07-05 03:47:54 +02:00
b64b9e0962
Port DrishX truck-motion detection as an opt-in slow layer: on-demand map-center analysis, preset corridors, layer panel toggle, and Docker road-corridor extras. Co-authored-by: Cursor <cursoragent@cursor.com>
106 lines
3.8 KiB
Python
106 lines
3.8 KiB
Python
"""Road corridor Sentinel-2 freight trend endpoints (opt-in slow layer)."""
|
|
|
|
from fastapi import APIRouter, HTTPException, Query, Request
|
|
from pydantic import BaseModel, Field
|
|
|
|
from limiter import limiter
|
|
from services.road_corridor_sat.config import optional_deps_available, road_corridor_sat_enabled
|
|
from services.road_corridor_sat.credentials import sentinel_credentials_configured
|
|
from services.road_corridor_sat.jobs import enqueue_analyze, get_job, get_latest_job, job_to_dict
|
|
from services.road_corridor_sat.presets import CORRIDOR_PRESETS, get_preset
|
|
from services.road_corridor_sat.storage import build_trends_payload, preset_metadata
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
def _status_payload() -> dict:
|
|
latest = get_latest_job()
|
|
return {
|
|
"enabled": road_corridor_sat_enabled(),
|
|
"deps_installed": optional_deps_available(),
|
|
"credentials_configured": sentinel_credentials_configured(),
|
|
"preset_count": len(CORRIDOR_PRESETS),
|
|
"attribution": "backend/third_party/drishx/NOTICE.md",
|
|
"active_job": job_to_dict(latest) if latest and latest.status in {"queued", "running"} else None,
|
|
}
|
|
|
|
|
|
def _require_analyze_ready() -> None:
|
|
if not optional_deps_available():
|
|
raise HTTPException(
|
|
status_code=503,
|
|
detail="Install optional road-corridor dependencies (uv sync --extra road-corridor)",
|
|
)
|
|
if not sentinel_credentials_configured():
|
|
raise HTTPException(
|
|
status_code=503,
|
|
detail="Set SENTINEL_CLIENT_ID and SENTINEL_CLIENT_SECRET in Imagery settings",
|
|
)
|
|
|
|
|
|
class AnalyzeRequest(BaseModel):
|
|
lat: float = Field(ge=-90, le=90)
|
|
lon: float = Field(ge=-180, le=180)
|
|
label: str | None = Field(default=None, max_length=120)
|
|
|
|
|
|
@router.get("/api/road-corridors/status")
|
|
@limiter.limit("60/minute")
|
|
async def road_corridors_status(request: Request) -> dict:
|
|
return {"ok": True, **_status_payload()}
|
|
|
|
|
|
@router.get("/api/road-corridors")
|
|
@limiter.limit("60/minute")
|
|
async def list_road_corridors(request: Request) -> dict:
|
|
return {
|
|
"ok": True,
|
|
"status": _status_payload(),
|
|
"presets": CORRIDOR_PRESETS,
|
|
"trends": build_trends_payload(),
|
|
}
|
|
|
|
|
|
@router.post("/api/road-corridors/analyze")
|
|
@limiter.limit("6/minute")
|
|
async def analyze_road_corridor_here(request: Request, payload: AnalyzeRequest) -> dict:
|
|
"""Start an on-demand Sentinel-2 corridor analysis at map center."""
|
|
_require_analyze_ready()
|
|
try:
|
|
job = enqueue_analyze(payload.lat, payload.lon, payload.label)
|
|
except RuntimeError as exc:
|
|
if str(exc) == "analysis_already_running":
|
|
active = get_latest_job()
|
|
raise HTTPException(
|
|
status_code=409,
|
|
detail="Analysis already in progress",
|
|
headers={"X-Job-Id": active.job_id if active else ""},
|
|
) from exc
|
|
raise
|
|
return {"ok": True, **job_to_dict(job)}
|
|
|
|
|
|
@router.get("/api/road-corridors/analyze/status")
|
|
@limiter.limit("120/minute")
|
|
async def analyze_road_corridor_status(
|
|
request: Request,
|
|
job_id: str | None = Query(default=None),
|
|
) -> dict:
|
|
job = get_job(job_id) if job_id else get_latest_job()
|
|
if job is None:
|
|
return {"ok": True, "job": None}
|
|
return {"ok": True, "job": job_to_dict(job)}
|
|
|
|
|
|
@router.get("/api/road-corridors/{preset_id}")
|
|
@limiter.limit("60/minute")
|
|
async def get_road_corridor(preset_id: str, request: Request) -> dict:
|
|
meta = preset_metadata(preset_id)
|
|
if meta is None:
|
|
raise HTTPException(status_code=404, detail="Unknown corridor preset")
|
|
preset = get_preset(preset_id)
|
|
if preset is None:
|
|
# Ad-hoc viewport runs are stored on disk but not in CORRIDOR_PRESETS.
|
|
return {"ok": True, "preset": None, "result": meta, "status": _status_payload()}
|
|
return {"ok": True, "preset": preset, "result": meta, "status": _status_payload()}
|