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/tiles.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

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