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>
231 lines
8.2 KiB
Python
231 lines
8.2 KiB
Python
"""
|
|
Terrain/DEM API Endpoints
|
|
Serves heightmap tiles from Copernicus DEM data
|
|
"""
|
|
from fastapi import APIRouter, HTTPException, Path, Query, Response
|
|
from fastapi.responses import JSONResponse
|
|
import structlog
|
|
|
|
from config import settings
|
|
from services.dem_service import DEMService
|
|
|
|
logger = structlog.get_logger(__name__)
|
|
router = APIRouter()
|
|
|
|
# Initialize DEM service
|
|
dem_service = DEMService()
|
|
|
|
|
|
@router.get("/{z}/{x}/{y}.png", response_class=Response)
|
|
async def get_heightmap_tile(
|
|
z: int = Path(..., ge=0, le=14, description="Zoom level"),
|
|
x: int = Path(..., ge=0, description="Tile X coordinate"),
|
|
y: int = Path(..., ge=0, description="Tile Y coordinate"),
|
|
):
|
|
"""
|
|
Get a heightmap tile as 16-bit PNG (Mapbox Terrain-RGB encoding).
|
|
|
|
Heightmaps are generated from Copernicus DEM GLO-30 (30m resolution).
|
|
The encoding allows for ~0.1m precision: height = -10000 + ((R * 256 * 256 + G * 256 + B) * 0.1)
|
|
"""
|
|
try:
|
|
tile_data = await dem_service.get_heightmap_tile(z, x, y)
|
|
|
|
if tile_data is None:
|
|
return Response(status_code=204) # No terrain data for this tile
|
|
|
|
return Response(
|
|
content=tile_data,
|
|
media_type="image/png",
|
|
headers={
|
|
"Cache-Control": "public, max-age=604800", # 7 days cache
|
|
"X-Tile-Source": "copernicus-dem",
|
|
},
|
|
)
|
|
|
|
except FileNotFoundError:
|
|
logger.warning("DEM data not found", z=z, x=x, y=y)
|
|
raise HTTPException(
|
|
status_code=503,
|
|
detail="DEM data not available. Please download Copernicus DEM first.",
|
|
)
|
|
except Exception as e:
|
|
logger.error("Error serving heightmap tile", z=z, x=x, y=y, error=str(e))
|
|
raise HTTPException(status_code=500, detail="Error serving heightmap tile")
|
|
|
|
|
|
@router.get("/hillshade/{z}/{x}/{y}.png", response_class=Response)
|
|
async def get_hillshade_tile(
|
|
z: int = Path(..., ge=0, le=14, description="Zoom level"),
|
|
x: int = Path(..., ge=0, description="Tile X coordinate"),
|
|
y: int = Path(..., ge=0, description="Tile Y coordinate"),
|
|
azimuth: float = Query(315, ge=0, le=360, description="Light azimuth in degrees"),
|
|
altitude: float = Query(45, ge=0, le=90, description="Light altitude in degrees"),
|
|
):
|
|
"""
|
|
Get a hillshade tile for terrain visualization.
|
|
|
|
Hillshade is rendered from DEM with configurable light direction.
|
|
Default light comes from northwest (azimuth=315) at 45° altitude.
|
|
"""
|
|
try:
|
|
tile_data = await dem_service.get_hillshade_tile(z, x, y, azimuth, altitude)
|
|
|
|
if tile_data is None:
|
|
return Response(status_code=204)
|
|
|
|
return Response(
|
|
content=tile_data,
|
|
media_type="image/png",
|
|
headers={
|
|
"Cache-Control": "public, max-age=604800",
|
|
"X-Hillshade-Azimuth": str(azimuth),
|
|
"X-Hillshade-Altitude": str(altitude),
|
|
},
|
|
)
|
|
|
|
except FileNotFoundError:
|
|
raise HTTPException(status_code=503, detail="DEM data not available")
|
|
except Exception as e:
|
|
logger.error("Error serving hillshade tile", z=z, x=x, y=y, error=str(e))
|
|
raise HTTPException(status_code=500, detail="Error serving hillshade tile")
|
|
|
|
|
|
@router.get("/contours/{z}/{x}/{y}.pbf", response_class=Response)
|
|
async def get_contour_tile(
|
|
z: int = Path(..., ge=0, le=14, description="Zoom level"),
|
|
x: int = Path(..., ge=0, description="Tile X coordinate"),
|
|
y: int = Path(..., ge=0, description="Tile Y coordinate"),
|
|
interval: int = Query(20, ge=5, le=100, description="Contour interval in meters"),
|
|
):
|
|
"""
|
|
Get contour lines as vector tile.
|
|
|
|
Contours are generated from DEM at the specified interval.
|
|
Useful for topographic map overlays.
|
|
"""
|
|
try:
|
|
tile_data = await dem_service.get_contour_tile(z, x, y, interval)
|
|
|
|
if tile_data is None:
|
|
return Response(status_code=204)
|
|
|
|
return Response(
|
|
content=tile_data,
|
|
media_type="application/x-protobuf",
|
|
headers={
|
|
"Content-Encoding": "gzip",
|
|
"Cache-Control": "public, max-age=604800",
|
|
"X-Contour-Interval": str(interval),
|
|
},
|
|
)
|
|
|
|
except FileNotFoundError:
|
|
raise HTTPException(status_code=503, detail="DEM data not available")
|
|
except Exception as e:
|
|
logger.error("Error serving contour tile", z=z, x=x, y=y, error=str(e))
|
|
raise HTTPException(status_code=500, detail="Error serving contour tile")
|
|
|
|
|
|
@router.get("/elevation")
|
|
async def get_elevation_at_point(
|
|
lat: float = Query(..., ge=47.27, le=55.06, description="Latitude"),
|
|
lon: float = Query(..., ge=5.87, le=15.04, description="Longitude"),
|
|
):
|
|
"""
|
|
Get elevation at a specific point.
|
|
|
|
Returns elevation in meters from Copernicus DEM.
|
|
Only works within Germany bounds.
|
|
"""
|
|
try:
|
|
elevation = await dem_service.get_elevation(lat, lon)
|
|
|
|
if elevation is None:
|
|
raise HTTPException(
|
|
status_code=404,
|
|
detail="No elevation data available for this location",
|
|
)
|
|
|
|
return {
|
|
"latitude": lat,
|
|
"longitude": lon,
|
|
"elevation_m": round(elevation, 1),
|
|
"source": "Copernicus DEM GLO-30",
|
|
"resolution_m": 30,
|
|
}
|
|
|
|
except FileNotFoundError:
|
|
raise HTTPException(status_code=503, detail="DEM data not available")
|
|
except Exception as e:
|
|
logger.error("Error getting elevation", lat=lat, lon=lon, error=str(e))
|
|
raise HTTPException(status_code=500, detail="Error getting elevation")
|
|
|
|
|
|
@router.post("/elevation/profile")
|
|
async def get_elevation_profile(
|
|
coordinates: list[list[float]],
|
|
samples: int = Query(100, ge=10, le=1000, description="Number of sample points"),
|
|
):
|
|
"""
|
|
Get elevation profile along a path.
|
|
|
|
Takes a list of [lon, lat] coordinates and returns elevations sampled along the path.
|
|
Useful for hiking/cycling route profiles.
|
|
"""
|
|
if len(coordinates) < 2:
|
|
raise HTTPException(status_code=400, detail="At least 2 coordinates required")
|
|
|
|
if len(coordinates) > 100:
|
|
raise HTTPException(status_code=400, detail="Maximum 100 coordinates allowed")
|
|
|
|
try:
|
|
profile = await dem_service.get_elevation_profile(coordinates, samples)
|
|
|
|
return {
|
|
"profile": profile,
|
|
"statistics": {
|
|
"min_elevation_m": min(p["elevation_m"] for p in profile if p["elevation_m"]),
|
|
"max_elevation_m": max(p["elevation_m"] for p in profile if p["elevation_m"]),
|
|
"total_ascent_m": sum(
|
|
max(0, profile[i + 1]["elevation_m"] - profile[i]["elevation_m"])
|
|
for i in range(len(profile) - 1)
|
|
if profile[i]["elevation_m"] and profile[i + 1]["elevation_m"]
|
|
),
|
|
"total_descent_m": sum(
|
|
max(0, profile[i]["elevation_m"] - profile[i + 1]["elevation_m"])
|
|
for i in range(len(profile) - 1)
|
|
if profile[i]["elevation_m"] and profile[i + 1]["elevation_m"]
|
|
),
|
|
},
|
|
"source": "Copernicus DEM GLO-30",
|
|
}
|
|
|
|
except FileNotFoundError:
|
|
raise HTTPException(status_code=503, detail="DEM data not available")
|
|
except Exception as e:
|
|
logger.error("Error getting elevation profile", error=str(e))
|
|
raise HTTPException(status_code=500, detail="Error getting elevation profile")
|
|
|
|
|
|
@router.get("/metadata")
|
|
async def get_dem_metadata():
|
|
"""
|
|
Get metadata about available DEM data.
|
|
"""
|
|
metadata = await dem_service.get_metadata()
|
|
|
|
return {
|
|
"name": "Copernicus DEM GLO-30",
|
|
"description": "Global 30m Digital Elevation Model",
|
|
"resolution_m": 30,
|
|
"coverage": "Germany (Deutschland)",
|
|
"bounds": [5.87, 47.27, 15.04, 55.06],
|
|
"vertical_datum": "EGM2008",
|
|
"horizontal_datum": "WGS84",
|
|
"license": "Copernicus Data (free, attribution required)",
|
|
"attribution": "© Copernicus Service Information 2024",
|
|
"data_available": metadata.get("data_available", False),
|
|
"tiles_generated": metadata.get("tiles_generated", 0),
|
|
}
|