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>
222 lines
7.4 KiB
Python
222 lines
7.4 KiB
Python
"""
|
|
Tile Server API Endpoints
|
|
Serves Vector Tiles from PMTiles or generates on-demand from PostGIS
|
|
"""
|
|
from fastapi import APIRouter, HTTPException, Path, Query, Response
|
|
from fastapi.responses import JSONResponse
|
|
import structlog
|
|
|
|
from config import settings
|
|
from services.tile_server import TileServerService
|
|
|
|
logger = structlog.get_logger(__name__)
|
|
router = APIRouter()
|
|
|
|
# Initialize tile server service
|
|
tile_service = TileServerService()
|
|
|
|
|
|
@router.get("/{z}/{x}/{y}.pbf", response_class=Response)
|
|
async def get_vector_tile(
|
|
z: int = Path(..., ge=0, le=22, description="Zoom level"),
|
|
x: int = Path(..., ge=0, description="Tile X coordinate"),
|
|
y: int = Path(..., ge=0, description="Tile Y coordinate"),
|
|
):
|
|
"""
|
|
Get a vector tile in Protocol Buffers format.
|
|
|
|
Returns OSM data as vector tiles suitable for MapLibre GL JS.
|
|
Tiles are served from pre-generated PMTiles or cached on-demand.
|
|
"""
|
|
try:
|
|
tile_data = await tile_service.get_tile(z, x, y)
|
|
|
|
if tile_data is None:
|
|
# Return empty tile (204 No Content is standard for empty tiles)
|
|
return Response(status_code=204)
|
|
|
|
return Response(
|
|
content=tile_data,
|
|
media_type="application/x-protobuf",
|
|
headers={
|
|
"Content-Encoding": "gzip",
|
|
"Cache-Control": "public, max-age=86400", # 24h cache
|
|
"X-Tile-Source": "pmtiles",
|
|
},
|
|
)
|
|
|
|
except FileNotFoundError:
|
|
logger.warning("PMTiles file not found", z=z, x=x, y=y)
|
|
raise HTTPException(
|
|
status_code=503,
|
|
detail="Tile data not available. Please run the data download script first.",
|
|
)
|
|
except Exception as e:
|
|
logger.error("Error serving tile", z=z, x=x, y=y, error=str(e))
|
|
raise HTTPException(status_code=500, detail="Error serving tile")
|
|
|
|
|
|
@router.get("/style.json")
|
|
async def get_maplibre_style(
|
|
base_url: str = Query(None, description="Base URL for tile server"),
|
|
):
|
|
"""
|
|
Get MapLibre GL JS style specification.
|
|
|
|
Returns a style document configured for the self-hosted tile server.
|
|
"""
|
|
# Use provided base_url or construct from settings
|
|
if base_url is None:
|
|
base_url = f"http://localhost:{settings.port}"
|
|
|
|
style = {
|
|
"version": 8,
|
|
"name": "GeoEdu Germany",
|
|
"metadata": {
|
|
"description": "Self-hosted OSM tiles for DSGVO-compliant education",
|
|
"attribution": "© OpenStreetMap contributors",
|
|
},
|
|
"sources": {
|
|
"osm": {
|
|
"type": "vector",
|
|
"tiles": [f"{base_url}/api/v1/tiles/{{z}}/{{x}}/{{y}}.pbf"],
|
|
"minzoom": 0,
|
|
"maxzoom": 14,
|
|
"attribution": "© OpenStreetMap contributors (ODbL)",
|
|
},
|
|
"terrain": {
|
|
"type": "raster-dem",
|
|
"tiles": [f"{base_url}/api/v1/terrain/{{z}}/{{x}}/{{y}}.png"],
|
|
"tileSize": 256,
|
|
"maxzoom": 14,
|
|
"attribution": "© Copernicus DEM GLO-30",
|
|
},
|
|
},
|
|
"sprite": "",
|
|
"glyphs": "https://fonts.openmaptiles.org/{fontstack}/{range}.pbf",
|
|
"layers": [
|
|
# Background
|
|
{
|
|
"id": "background",
|
|
"type": "background",
|
|
"paint": {"background-color": "#f8f4f0"},
|
|
},
|
|
# Water
|
|
{
|
|
"id": "water",
|
|
"type": "fill",
|
|
"source": "osm",
|
|
"source-layer": "water",
|
|
"paint": {"fill-color": "#a0c8f0"},
|
|
},
|
|
# Landuse - Parks
|
|
{
|
|
"id": "landuse-park",
|
|
"type": "fill",
|
|
"source": "osm",
|
|
"source-layer": "landuse",
|
|
"filter": ["==", "class", "park"],
|
|
"paint": {"fill-color": "#c8e6c8", "fill-opacity": 0.5},
|
|
},
|
|
# Landuse - Forest
|
|
{
|
|
"id": "landuse-forest",
|
|
"type": "fill",
|
|
"source": "osm",
|
|
"source-layer": "landuse",
|
|
"filter": ["==", "class", "wood"],
|
|
"paint": {"fill-color": "#94d294", "fill-opacity": 0.5},
|
|
},
|
|
# Buildings
|
|
{
|
|
"id": "building",
|
|
"type": "fill",
|
|
"source": "osm",
|
|
"source-layer": "building",
|
|
"minzoom": 13,
|
|
"paint": {"fill-color": "#d9d0c9", "fill-opacity": 0.8},
|
|
},
|
|
# Roads - Minor
|
|
{
|
|
"id": "road-minor",
|
|
"type": "line",
|
|
"source": "osm",
|
|
"source-layer": "transportation",
|
|
"filter": ["all", ["==", "$type", "LineString"], ["in", "class", "minor", "service"]],
|
|
"paint": {"line-color": "#ffffff", "line-width": 1},
|
|
},
|
|
# Roads - Major
|
|
{
|
|
"id": "road-major",
|
|
"type": "line",
|
|
"source": "osm",
|
|
"source-layer": "transportation",
|
|
"filter": ["all", ["==", "$type", "LineString"], ["in", "class", "primary", "secondary", "tertiary"]],
|
|
"paint": {"line-color": "#ffc107", "line-width": 2},
|
|
},
|
|
# Roads - Highway
|
|
{
|
|
"id": "road-highway",
|
|
"type": "line",
|
|
"source": "osm",
|
|
"source-layer": "transportation",
|
|
"filter": ["all", ["==", "$type", "LineString"], ["==", "class", "motorway"]],
|
|
"paint": {"line-color": "#ff6f00", "line-width": 3},
|
|
},
|
|
# Place labels
|
|
{
|
|
"id": "place-label",
|
|
"type": "symbol",
|
|
"source": "osm",
|
|
"source-layer": "place",
|
|
"layout": {
|
|
"text-field": "{name}",
|
|
"text-font": ["Open Sans Regular"],
|
|
"text-size": 12,
|
|
},
|
|
"paint": {"text-color": "#333333", "text-halo-color": "#ffffff", "text-halo-width": 1},
|
|
},
|
|
],
|
|
"terrain": {"source": "terrain", "exaggeration": 1.5},
|
|
}
|
|
|
|
return JSONResponse(content=style)
|
|
|
|
|
|
@router.get("/metadata")
|
|
async def get_tile_metadata():
|
|
"""
|
|
Get metadata about available tiles.
|
|
|
|
Returns information about data coverage, zoom levels, and update status.
|
|
"""
|
|
metadata = await tile_service.get_metadata()
|
|
|
|
return {
|
|
"name": "GeoEdu Germany Tiles",
|
|
"description": "Self-hosted OSM vector tiles for Germany",
|
|
"format": "pbf",
|
|
"scheme": "xyz",
|
|
"minzoom": metadata.get("minzoom", 0),
|
|
"maxzoom": metadata.get("maxzoom", 14),
|
|
"bounds": metadata.get("bounds", [5.87, 47.27, 15.04, 55.06]), # Germany bbox
|
|
"center": metadata.get("center", [10.45, 51.16, 6]), # Center of Germany
|
|
"attribution": "© OpenStreetMap contributors (ODbL)",
|
|
"data_available": metadata.get("data_available", False),
|
|
"last_updated": metadata.get("last_updated"),
|
|
}
|
|
|
|
|
|
@router.get("/bounds")
|
|
async def get_tile_bounds():
|
|
"""
|
|
Get the geographic bounds of available tile data.
|
|
|
|
Returns bounding box for Germany in [west, south, east, north] format.
|
|
"""
|
|
return {
|
|
"bounds": [5.87, 47.27, 15.04, 55.06], # Germany bounding box
|
|
"center": [10.45, 51.16],
|
|
"description": "Germany (Deutschland)",
|
|
}
|