Change detection was the one analysis that ignored the selection entirely. It compared two datasets in full, loaded every feature of both into Python with no spatial predicate, and — with include_unchanged defaulting to true — returned a FeatureCollection holding both datasets. For a regional building layer that is the wrong answer to "what changed here" and a response no browser should be asked to hold. It now accepts bbox and area_id, resolved the way every other analysis resolves them, and loads through an indexed ST_Intersects predicate. Features are deliberately not clipped to the selection. A change class describes a whole object: comparing a clipped earlier footprint against an unclipped later one would report the selection edge itself as a change. Objects the edge crosses are compared in full and counted in a warning. The returned geometry is capped by preview_limit, spending that budget on modified, added and removed before unchanged, while every count still describes the whole selection. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
47 lines
1.9 KiB
Python
47 lines
1.9 KiB
Python
from __future__ import annotations
|
|
|
|
from fastapi import APIRouter, Depends
|
|
from sqlalchemy.orm import Session
|
|
|
|
from app.core.errors import AppError
|
|
from app.db.session import get_db
|
|
from app.models import Dataset
|
|
from app.schemas import Envelope, JobRead
|
|
from app.schemas.analysis import ChangeDetectionRequest
|
|
from app.services.change_detection_service import ChangeDetectionService
|
|
from app.services.job_service import JobService
|
|
from app.utils.response import envelope
|
|
|
|
router = APIRouter(prefix="/analysis", tags=["analysis"])
|
|
|
|
|
|
@router.post("/change-detection", response_model=Envelope[JobRead])
|
|
def run_change_detection(
|
|
payload: ChangeDetectionRequest,
|
|
db: Session = Depends(get_db),
|
|
) -> dict:
|
|
source_dataset = db.get(Dataset, payload.source_dataset_id)
|
|
if not source_dataset:
|
|
raise AppError(code="DATASET_NOT_FOUND", message="Source dataset not found", status_code=404)
|
|
ChangeDetectionService._get_project_vector_dataset(db, payload.source_dataset_id, source_dataset.project_id, "Source")
|
|
job = JobService.run_sync_job(
|
|
db=db,
|
|
project_id=source_dataset.project_id,
|
|
job_type="analysis.change-detection",
|
|
parameters=payload.model_dump(mode="json"),
|
|
input_dataset_id=payload.source_dataset_id,
|
|
operation=lambda: ChangeDetectionService.compare_vector_datasets(
|
|
db=db,
|
|
project_id=source_dataset.project_id,
|
|
source_dataset_id=payload.source_dataset_id,
|
|
target_dataset_id=payload.target_dataset_id,
|
|
iou_threshold=payload.iou_threshold,
|
|
modified_threshold=payload.modified_threshold,
|
|
include_unchanged=payload.include_unchanged,
|
|
bbox=payload.bbox.model_dump() if payload.bbox is not None else None,
|
|
area_id=payload.area_id,
|
|
preview_limit=payload.preview_limit,
|
|
).model_dump(mode="json"),
|
|
)
|
|
return envelope(job)
|