This repository has been archived on 2026-02-15. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
breakpilot-pwa/geo-service/api/aoi.py
Benjamin Admin 21a844cb8a fix: Restore all files lost during destructive rebase
A previous `git pull --rebase origin main` dropped 177 local commits,
losing 3400+ files across admin-v2, backend, studio-v2, website,
klausur-service, and many other services. The partial restore attempt
(660295e2) only recovered some files.

This commit restores all missing files from pre-rebase ref 98933f5e
while preserving post-rebase additions (night-scheduler, night-mode UI,
NightModeWidget dashboard integration).

Restored features include:
- AI Module Sidebar (FAB), OCR Labeling, OCR Compare
- GPU Dashboard, RAG Pipeline, Magic Help
- Klausur-Korrektur (8 files), Abitur-Archiv (5+ files)
- Companion, Zeugnisse-Crawler, Screen Flow
- Full backend, studio-v2, website, klausur-service
- All compliance SDKs, agent-core, voice-service
- CI/CD configs, documentation, scripts

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-09 09:51:32 +01:00

305 lines
8.6 KiB
Python

"""
AOI (Area of Interest) API Endpoints
Handles polygon selection, validation, and Unity bundle generation
"""
from fastapi import APIRouter, HTTPException, Path, Query, BackgroundTasks
from fastapi.responses import JSONResponse, FileResponse
from pydantic import BaseModel, Field
from typing import Optional
from datetime import datetime
import uuid
import structlog
from config import settings
from models.aoi import AOIRequest, AOIResponse, AOIStatus, AOIManifest
from services.aoi_packager import AOIPackagerService
from services.osm_extractor import OSMExtractorService
logger = structlog.get_logger(__name__)
router = APIRouter()
# Initialize services
aoi_packager = AOIPackagerService()
osm_extractor = OSMExtractorService()
@router.post("", response_model=AOIResponse)
async def create_aoi(
request: AOIRequest,
background_tasks: BackgroundTasks,
):
"""
Create a new AOI (Area of Interest) for Unity 3D export.
Validates the polygon, checks size limits (max 4 km²), and queues
bundle generation. Returns immediately with a status URL.
The bundle will contain:
- Terrain heightmap
- OSM features (buildings, roads, water, etc.)
- Learning node positions
- Attribution information
"""
# Validate polygon
try:
area_km2 = aoi_packager.calculate_area_km2(request.polygon)
except ValueError as e:
raise HTTPException(status_code=400, detail=f"Invalid polygon: {str(e)}")
# Check size limit
if area_km2 > settings.max_aoi_size_km2:
raise HTTPException(
status_code=400,
detail=f"AOI too large: {area_km2:.2f} km² exceeds maximum of {settings.max_aoi_size_km2} km²",
)
# Check if polygon is within Germany bounds
if not aoi_packager.is_within_germany(request.polygon):
raise HTTPException(
status_code=400,
detail="AOI must be within Germany. Bounds: [5.87°E, 47.27°N] to [15.04°E, 55.06°N]",
)
# Generate AOI ID
aoi_id = str(uuid.uuid4())
# Create AOI record
aoi_data = {
"id": aoi_id,
"polygon": request.polygon,
"theme": request.theme,
"quality": request.quality,
"area_km2": area_km2,
"status": AOIStatus.QUEUED,
"created_at": datetime.utcnow().isoformat(),
}
# Start background processing
background_tasks.add_task(
aoi_packager.process_aoi,
aoi_id=aoi_id,
polygon=request.polygon,
theme=request.theme,
quality=request.quality,
)
logger.info(
"AOI created",
aoi_id=aoi_id,
area_km2=area_km2,
theme=request.theme,
)
return AOIResponse(
aoi_id=aoi_id,
status=AOIStatus.QUEUED,
area_km2=area_km2,
estimated_size_mb=aoi_packager.estimate_bundle_size_mb(area_km2, request.quality),
message="AOI queued for processing",
)
@router.get("/{aoi_id}", response_model=AOIResponse)
async def get_aoi_status(
aoi_id: str = Path(..., description="AOI UUID"),
):
"""
Get the status of an AOI processing job.
Returns current status (queued, processing, completed, failed)
and download URLs when ready.
"""
aoi_data = await aoi_packager.get_aoi_status(aoi_id)
if aoi_data is None:
raise HTTPException(status_code=404, detail="AOI not found")
response = AOIResponse(
aoi_id=aoi_id,
status=aoi_data["status"],
area_km2=aoi_data.get("area_km2", 0),
estimated_size_mb=aoi_data.get("estimated_size_mb", 0),
message=aoi_data.get("message", ""),
)
# Add download URLs if completed
if aoi_data["status"] == AOIStatus.COMPLETED:
response.download_url = f"/api/v1/aoi/{aoi_id}/bundle.zip"
response.manifest_url = f"/api/v1/aoi/{aoi_id}/manifest.json"
response.completed_at = aoi_data.get("completed_at")
return response
@router.get("/{aoi_id}/manifest.json")
async def get_aoi_manifest(
aoi_id: str = Path(..., description="AOI UUID"),
):
"""
Get the Unity bundle manifest for an AOI.
The manifest contains:
- Terrain configuration
- Asset list and paths
- Learning node positions
- Attribution requirements
"""
manifest = await aoi_packager.get_manifest(aoi_id)
if manifest is None:
raise HTTPException(status_code=404, detail="Manifest not found or AOI not ready")
return JSONResponse(content=manifest)
@router.get("/{aoi_id}/bundle.zip")
async def download_aoi_bundle(
aoi_id: str = Path(..., description="AOI UUID"),
):
"""
Download the complete AOI bundle as a ZIP file.
Contains all assets needed for Unity 3D rendering:
- terrain.heightmap (16-bit PNG)
- osm_features.json (buildings, roads, etc.)
- learning_nodes.json (educational content positions)
- attribution.json (required license notices)
"""
bundle_path = await aoi_packager.get_bundle_path(aoi_id)
if bundle_path is None:
raise HTTPException(status_code=404, detail="Bundle not found or AOI not ready")
return FileResponse(
path=bundle_path,
filename=f"geo-lernwelt-{aoi_id[:8]}.zip",
media_type="application/zip",
)
@router.delete("/{aoi_id}")
async def delete_aoi(
aoi_id: str = Path(..., description="AOI UUID"),
):
"""
Delete an AOI and its bundle.
Implements DSGVO data minimization - users can delete their data.
"""
success = await aoi_packager.delete_aoi(aoi_id)
if not success:
raise HTTPException(status_code=404, detail="AOI not found")
logger.info("AOI deleted", aoi_id=aoi_id)
return {"message": "AOI deleted successfully", "aoi_id": aoi_id}
@router.get("/{aoi_id}/preview")
async def get_aoi_preview(
aoi_id: str = Path(..., description="AOI UUID"),
width: int = Query(512, ge=64, le=2048, description="Preview width"),
height: int = Query(512, ge=64, le=2048, description="Preview height"),
):
"""
Get a preview image of the AOI.
Returns a rendered preview showing terrain, OSM features,
and learning node positions.
"""
preview_data = await aoi_packager.generate_preview(aoi_id, width, height)
if preview_data is None:
raise HTTPException(status_code=404, detail="Preview not available")
from fastapi.responses import Response
return Response(
content=preview_data,
media_type="image/png",
)
@router.post("/validate")
async def validate_aoi_polygon(
polygon: dict,
):
"""
Validate an AOI polygon without creating it.
Checks:
- Valid GeoJSON format
- Within Germany bounds
- Within size limits
- Not self-intersecting
"""
try:
# Validate geometry
is_valid, message = aoi_packager.validate_polygon(polygon)
if not is_valid:
return {
"valid": False,
"error": message,
}
# Calculate area
area_km2 = aoi_packager.calculate_area_km2(polygon)
# Check bounds
within_germany = aoi_packager.is_within_germany(polygon)
# Check size
within_size_limit = area_km2 <= settings.max_aoi_size_km2
return {
"valid": is_valid and within_germany and within_size_limit,
"area_km2": round(area_km2, 3),
"within_germany": within_germany,
"within_size_limit": within_size_limit,
"max_size_km2": settings.max_aoi_size_km2,
"estimated_bundle_size_mb": aoi_packager.estimate_bundle_size_mb(area_km2, "medium"),
}
except Exception as e:
return {
"valid": False,
"error": str(e),
}
@router.get("/templates/mainau")
async def get_mainau_template():
"""
Get pre-configured AOI template for Mainau island (demo location).
Mainau is a small island in Lake Constance (Bodensee) -
perfect for educational geography lessons.
"""
return {
"name": "Insel Mainau",
"description": "Blumeninsel im Bodensee - ideal fuer Erdkunde-Unterricht",
"polygon": {
"type": "Polygon",
"coordinates": [
[
[9.1875, 47.7055],
[9.1975, 47.7055],
[9.1975, 47.7115],
[9.1875, 47.7115],
[9.1875, 47.7055],
]
],
},
"center": [9.1925, 47.7085],
"area_km2": 0.45,
"suggested_themes": ["topographie", "vegetation", "landnutzung"],
"features": [
"Schloss und Schlosskirche",
"Botanischer Garten",
"Bodensee-Ufer",
"Waldgebiete",
],
}