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 Dev 19855efacc
Some checks failed
Tests / Go Tests (push) Has been cancelled
Tests / Python Tests (push) Has been cancelled
Tests / Integration Tests (push) Has been cancelled
Tests / Go Lint (push) Has been cancelled
Tests / Python Lint (push) Has been cancelled
Tests / Security Scan (push) Has been cancelled
Tests / All Checks Passed (push) Has been cancelled
Security Scanning / Secret Scanning (push) Has been cancelled
Security Scanning / Dependency Vulnerability Scan (push) Has been cancelled
Security Scanning / Go Security Scan (push) Has been cancelled
Security Scanning / Python Security Scan (push) Has been cancelled
Security Scanning / Node.js Security Scan (push) Has been cancelled
Security Scanning / Docker Image Security (push) Has been cancelled
Security Scanning / Security Summary (push) Has been cancelled
CI/CD Pipeline / Go Tests (push) Has been cancelled
CI/CD Pipeline / Python Tests (push) Has been cancelled
CI/CD Pipeline / Website Tests (push) Has been cancelled
CI/CD Pipeline / Linting (push) Has been cancelled
CI/CD Pipeline / Security Scan (push) Has been cancelled
CI/CD Pipeline / Docker Build & Push (push) Has been cancelled
CI/CD Pipeline / Integration Tests (push) Has been cancelled
CI/CD Pipeline / Deploy to Staging (push) Has been cancelled
CI/CD Pipeline / Deploy to Production (push) Has been cancelled
CI/CD Pipeline / CI Summary (push) Has been cancelled
ci/woodpecker/manual/build-ci-image Pipeline was successful
ci/woodpecker/manual/main Pipeline failed
feat: BreakPilot PWA - Full codebase (clean push without large binaries)
All services: admin-v2, studio-v2, website, ai-compliance-sdk,
consent-service, klausur-service, voice-service, and infrastructure.
Large PDFs and compiled binaries excluded via .gitignore.
2026-02-11 13:25:58 +01:00

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),
}