Initial GeoIntel V1 foundation
This commit is contained in:
@@ -0,0 +1,288 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from uuid import UUID, uuid4
|
||||
|
||||
from geoalchemy2.shape import from_shape
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.models import Area, Dataset, Metric, Project, QualityCheck
|
||||
from app.schemas.demo import DemoWorkflowResponse
|
||||
from app.services.geojson_service import parse_geojson_payload
|
||||
from app.services.qa_service import QaService
|
||||
from app.services.quality_service import QualityService
|
||||
from app.services.storage_service import StorageService
|
||||
from app.services.vector_feature_service import VectorFeatureService
|
||||
from app.utils.geometry import area_m2, geometry_bbox_polygon, normalize_to_multipolygon
|
||||
|
||||
|
||||
class DemoWorkflowService:
|
||||
PROJECT_NAME = "GeoIntel Demo - Building QA"
|
||||
AREA_NAME = "Demo AOI - Geel buildings"
|
||||
REFERENCE_FILENAME = "demo_reference_buildings.geojson"
|
||||
CANDIDATE_FILENAME = "demo_predicted_buildings.geojson"
|
||||
|
||||
@staticmethod
|
||||
def _repo_root() -> Path:
|
||||
return Path(__file__).resolve().parents[3]
|
||||
|
||||
@staticmethod
|
||||
def _fixture_path(filename: str) -> Path:
|
||||
return DemoWorkflowService._repo_root() / "fixtures" / "golden" / filename
|
||||
|
||||
@staticmethod
|
||||
def _load_fixture(filename: str) -> tuple[dict, bytes]:
|
||||
path = DemoWorkflowService._fixture_path(filename)
|
||||
raw = path.read_bytes()
|
||||
return json.loads(raw.decode("utf-8")), raw
|
||||
|
||||
@staticmethod
|
||||
def _find_existing_project(db: Session) -> Project | None:
|
||||
return (
|
||||
db.query(Project)
|
||||
.filter(Project.name == DemoWorkflowService.PROJECT_NAME)
|
||||
.filter(Project.status != "deleted")
|
||||
.first()
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _create_area(db: Session, project_id: UUID) -> Area:
|
||||
geometry = {
|
||||
"type": "MultiPolygon",
|
||||
"coordinates": [
|
||||
[
|
||||
[
|
||||
[4.30, 51.18],
|
||||
[4.45, 51.18],
|
||||
[4.45, 51.33],
|
||||
[4.30, 51.33],
|
||||
[4.30, 51.18],
|
||||
]
|
||||
]
|
||||
],
|
||||
}
|
||||
multipolygon = normalize_to_multipolygon(geometry)
|
||||
area = Area(
|
||||
id=uuid4(),
|
||||
project_id=project_id,
|
||||
name=DemoWorkflowService.AREA_NAME,
|
||||
geometry=from_shape(multipolygon, srid=4326),
|
||||
original_crs="EPSG:4326",
|
||||
area_m2=area_m2(multipolygon),
|
||||
bbox=from_shape(geometry_bbox_polygon(multipolygon), srid=4326),
|
||||
)
|
||||
db.add(area)
|
||||
db.commit()
|
||||
db.refresh(area)
|
||||
return area
|
||||
|
||||
@staticmethod
|
||||
def _create_dataset(
|
||||
db: Session,
|
||||
*,
|
||||
project_id: UUID,
|
||||
area_id: UUID,
|
||||
filename: str,
|
||||
payload: dict,
|
||||
raw: bytes,
|
||||
role: str,
|
||||
source_name: str,
|
||||
reference_layer_name: str | None,
|
||||
) -> Dataset:
|
||||
dataset_id = uuid4()
|
||||
storage_info = StorageService.persist_dataset_file(
|
||||
project_id=str(project_id),
|
||||
dataset_id=str(dataset_id),
|
||||
dataset_type="vector",
|
||||
original_filename=filename,
|
||||
content=raw,
|
||||
content_type="application/geo+json",
|
||||
)
|
||||
metadata = parse_geojson_payload(payload)
|
||||
dataset = Dataset(
|
||||
id=dataset_id,
|
||||
project_id=project_id,
|
||||
area_id=area_id,
|
||||
name=filename,
|
||||
dataset_type="vector",
|
||||
source="fixture",
|
||||
dataset_role=role,
|
||||
source_name=source_name,
|
||||
reference_layer_name=reference_layer_name,
|
||||
source_metadata={
|
||||
"fixture": True,
|
||||
"fixture_name": filename,
|
||||
"usage": "offline demo workflow only",
|
||||
},
|
||||
provenance_metadata={
|
||||
"created_by": "demo_workflow",
|
||||
"source_path": str(DemoWorkflowService._fixture_path(filename)),
|
||||
},
|
||||
imported_at=datetime.now(timezone.utc),
|
||||
storage_path=storage_info["storage_path"],
|
||||
original_filename=storage_info["original_filename"],
|
||||
stored_filename=storage_info["stored_filename"],
|
||||
content_type=storage_info["content_type"],
|
||||
size_bytes=storage_info["size_bytes"],
|
||||
checksum_sha256=storage_info["checksum_sha256"],
|
||||
crs=metadata.get("crs"),
|
||||
bounds_json=metadata.get("bounds_json"),
|
||||
metadata_json=metadata,
|
||||
status="ready",
|
||||
)
|
||||
db.add(dataset)
|
||||
db.commit()
|
||||
db.refresh(dataset)
|
||||
VectorFeatureService.persist_geojson_features(
|
||||
db=db,
|
||||
dataset_id=dataset.id,
|
||||
payload=payload,
|
||||
feature_class=reference_layer_name or "building",
|
||||
)
|
||||
return dataset
|
||||
|
||||
@staticmethod
|
||||
def _persist_qa(
|
||||
db: Session,
|
||||
*,
|
||||
project_id: UUID,
|
||||
candidate_dataset_id: UUID,
|
||||
reference_dataset_id: UUID,
|
||||
area_id: UUID,
|
||||
) -> QualityCheck:
|
||||
result = QaService.compare_candidate_with_reference(
|
||||
db=db,
|
||||
project_id=project_id,
|
||||
candidate_dataset_id=candidate_dataset_id,
|
||||
reference_dataset_id=reference_dataset_id,
|
||||
iou_threshold=0.5,
|
||||
area_id=area_id,
|
||||
)
|
||||
return QualityService.persist_quality_check(
|
||||
db=db,
|
||||
project_id=project_id,
|
||||
candidate_dataset_id=candidate_dataset_id,
|
||||
reference_dataset_id=reference_dataset_id,
|
||||
check_type="demo_candidate_vs_reference",
|
||||
status=result.status,
|
||||
score=result.f1_score,
|
||||
parameters={
|
||||
"iou_threshold": result.iou_threshold,
|
||||
"area_id": str(area_id),
|
||||
"fixture_workflow": True,
|
||||
},
|
||||
findings={
|
||||
"matches": result.matches,
|
||||
"false_positives": result.false_positives,
|
||||
"false_negatives": result.false_negatives,
|
||||
"warnings": result.warnings,
|
||||
"unsupported_geometry": result.unsupported_geometry,
|
||||
"unsupported_geometries": result.unsupported_geometries,
|
||||
},
|
||||
metrics={
|
||||
"precision": result.precision,
|
||||
"recall": result.recall,
|
||||
"f1": result.f1_score,
|
||||
"mean_iou": result.mean_iou,
|
||||
"false_positive_count": result.false_positives,
|
||||
"false_negative_count": result.false_negatives,
|
||||
},
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def seed(db: Session) -> DemoWorkflowResponse:
|
||||
existing = DemoWorkflowService._find_existing_project(db)
|
||||
if existing:
|
||||
area = db.query(Area).filter(Area.project_id == existing.id).order_by(Area.created_at.asc()).first()
|
||||
reference = (
|
||||
db.query(Dataset)
|
||||
.filter(Dataset.project_id == existing.id)
|
||||
.filter(Dataset.dataset_role == "reference")
|
||||
.filter(Dataset.source_name == "fixture")
|
||||
.first()
|
||||
)
|
||||
candidate = (
|
||||
db.query(Dataset)
|
||||
.filter(Dataset.project_id == existing.id)
|
||||
.filter(Dataset.dataset_role == "source")
|
||||
.filter(Dataset.source_name == "fixture")
|
||||
.first()
|
||||
)
|
||||
quality_check = (
|
||||
db.query(QualityCheck)
|
||||
.filter(QualityCheck.project_id == existing.id)
|
||||
.filter(QualityCheck.check_type == "demo_candidate_vs_reference")
|
||||
.order_by(QualityCheck.created_at.desc())
|
||||
.first()
|
||||
)
|
||||
if area and reference and candidate and quality_check:
|
||||
return DemoWorkflowResponse(
|
||||
project_id=existing.id,
|
||||
area_id=area.id,
|
||||
reference_dataset_id=reference.id,
|
||||
candidate_dataset_id=candidate.id,
|
||||
quality_check_id=quality_check.id,
|
||||
metric_count=db.query(Metric).filter(Metric.quality_check_id == quality_check.id).count(),
|
||||
status="ready",
|
||||
message="Demo workflow already exists.",
|
||||
created=False,
|
||||
)
|
||||
|
||||
project = Project(
|
||||
id=uuid4(),
|
||||
name=DemoWorkflowService.PROJECT_NAME,
|
||||
description="Offline fixture workflow: reference buildings, predicted buildings and persisted QA metrics.",
|
||||
region="Kempen",
|
||||
status="active",
|
||||
)
|
||||
db.add(project)
|
||||
db.commit()
|
||||
db.refresh(project)
|
||||
|
||||
area = DemoWorkflowService._create_area(db, project.id)
|
||||
reference_payload, reference_raw = DemoWorkflowService._load_fixture("reference_buildings.geojson")
|
||||
candidate_payload, candidate_raw = DemoWorkflowService._load_fixture("predicted_buildings.geojson")
|
||||
|
||||
reference = DemoWorkflowService._create_dataset(
|
||||
db=db,
|
||||
project_id=project.id,
|
||||
area_id=area.id,
|
||||
filename=DemoWorkflowService.REFERENCE_FILENAME,
|
||||
payload=reference_payload,
|
||||
raw=reference_raw,
|
||||
role="reference",
|
||||
source_name="fixture",
|
||||
reference_layer_name="buildings",
|
||||
)
|
||||
candidate = DemoWorkflowService._create_dataset(
|
||||
db=db,
|
||||
project_id=project.id,
|
||||
area_id=area.id,
|
||||
filename=DemoWorkflowService.CANDIDATE_FILENAME,
|
||||
payload=candidate_payload,
|
||||
raw=candidate_raw,
|
||||
role="source",
|
||||
source_name="fixture",
|
||||
reference_layer_name=None,
|
||||
)
|
||||
quality_check = DemoWorkflowService._persist_qa(
|
||||
db=db,
|
||||
project_id=project.id,
|
||||
candidate_dataset_id=candidate.id,
|
||||
reference_dataset_id=reference.id,
|
||||
area_id=area.id,
|
||||
)
|
||||
|
||||
return DemoWorkflowResponse(
|
||||
project_id=project.id,
|
||||
area_id=area.id,
|
||||
reference_dataset_id=reference.id,
|
||||
candidate_dataset_id=candidate.id,
|
||||
quality_check_id=quality_check.id,
|
||||
metric_count=6,
|
||||
status="ready",
|
||||
message="Demo workflow seeded from explicit local fixtures.",
|
||||
created=True,
|
||||
)
|
||||
Reference in New Issue
Block a user