# Lodapi — Vollständige API-Dokumentation Diese Datei ist die maschinell konkatenierte Doku aller API-Endpoints und Guides. Generiert aus `04_engineering/api/` im Repository — die einzige Quelle. Drop diese Datei in dein LLM-Context-Window, um vollen API-Kontext in einem Roundtrip zu erhalten. - Basis-URL: https://api.lodapi.de - Docs: https://lodapi.de/docs - OpenAPI: https://api.lodapi.de/openapi.json --- ## Endpoints ### GET /v1/admin/usage/anonymous - **operation_id**: `admin_anonymous_usage` - **stability**: `beta` - **since_version**: `0.1.0` - **auth**: `server_to_server_bearer` - **data_product**: `platform` - **snapshot_aware**: `False` - **attribution_block**: `False` - **rate_limit_tier**: `admin` # `GET /v1/admin/usage/anonymous` — Nutzungsreport für keylose Calls Aggregiert allen Traffic, der **ohne** `X-API-Key`-Header ankam (`api_key_id IS NULL`, ADR-0014 anonymer/Free-Tier-Zugang). Gleiche Buckets wie der per-Key-Report, ohne Quota-Felder (anonymer Traffic hat keine Quota). Schließt die Lücke, dass der per-Key-Endpoint eine `api_key_id` braucht und NULL-Keys daher nur per direkter SQL-Abfrage sichtbar waren. > **Auth-Pflicht.** Bearer-Token via `Authorization`-Header, muss `LODAPI_ADMIN_TOKEN` matchen. Ohne Token-Setup auf dem Server: **503**. ## Wann verwenden - **NICHT als Client-Endpoint.** Exklusiv für Lodapi-Operations. - Free-Tier-/Demo-Nutzung beobachten (Volumen, beliebteste Routen, BL-Schwerpunkt). - Conversion-Signal: hohe anonyme Last auf einer Route = Kandidat für Outreach/Paywall. ## Query-Parameter | Param | Type | Default | Beschreibung | |---|---|---|---| | `months` | int | `6` | Anzahl rückblickender Monate (1–13) | ## Example ```bash curl "https://api.lodapi.de/v1/admin/usage/anonymous?months=3" \ -H "Authorization: Bearer $LODAPI_ADMIN_TOKEN" ``` ## Response `200 OK · application/json`: - `window_months`, `total_calls`, `current_month_calls`, `unique_ips` - `unique_ips` = `COUNT(DISTINCT remote_ip)` — grober Caller-Proxy, NAT-gefaltet (kein exakter Nutzerzähler). - `periods[]` — pro Kalendermonat: `period_start`, `call_count`, `bytes_sent`, `error_count` (`calls_over_quota` immer `0`) - `top_endpoints[]` — meistgenutzte Routen über das Fenster: `endpoint_pattern`, `call_count`, `error_count` - `bl_distribution[]` — Calls pro Bundesland (aus dem gmlid-Prefix abgeleitet): `bundesland_code`, `call_count` > **MVP-Vorbehalt.** `bl_distribution` zählt nur `/v1/buildings/DE…`-Pfade; bbox-/koordinatenbasierte Calls werden nicht räumlich aufgelöst. Das Usage-Logging ist fail-open — bei DB-Fehler wird der Insert geschluckt, der Report ist daher *nahezu*, nicht garantiert vollständig. ## Fehler | Status | Bedingung | |---|---| | `400` | `months` außerhalb [1, 13] | | `401` | Kein/falscher Bearer-Token | | `503` | `LODAPI_ADMIN_TOKEN` nicht gesetzt (Endpoint deaktiviert) | ## Verwandte Endpoints - [`GET /v1/admin/api-keys/{api_key_id}/usage`](./admin-api-key-usage.md) — derselbe Report, aber pro ausgestelltem Key. --- ### GET /v1/admin/api-keys/{api_key_id}/usage - **operation_id**: `admin_api_key_usage` - **stability**: `beta` - **since_version**: `0.1.0` - **auth**: `server_to_server_bearer` - **data_product**: `platform` - **snapshot_aware**: `False` - **attribution_block**: `False` - **rate_limit_tier**: `admin` # `GET /v1/admin/api-keys/{api_key_id}/usage` — Nutzungsreport pro Key Liefert pro Kalendermonat Call-Count, Error-Count und Byte-Volumen für einen einzelnen Key — die Datengrundlage fürs Concierge-Billing (ADR-0014). `current_month_calls` ist der laufende Zähler des angefangenen Monats (zum Abgleich gegen `monthly_call_quota`). > **Auth-Pflicht.** Bearer-Token via `Authorization`-Header, muss `LODAPI_ADMIN_TOKEN` matchen. Ohne Token-Setup auf dem Server: **503**. ## Wann verwenden - **NICHT als Client-Endpoint.** Exklusiv für Lodapi-Operations. - Monatliche manuelle Rechnungsstellung (Concierge-Billing). - Quota-Überschreitungen erkennen (`calls_over_quota` pro Periode). ## Path- & Query-Parameter | Param | Type | Default | Beschreibung | |---|---|---|---| | `api_key_id` | string (UUID) | — | Key aus Create-/List-Response | | `months` | int | `6` | Anzahl rückblickender Monate (1–13) | ## Example ```bash curl "https://api.lodapi.de/v1/admin/api-keys/3f9a...-uuid/usage?months=3" \ -H "Authorization: Bearer $LODAPI_ADMIN_TOKEN" ``` ## Response `200 OK · application/json`: - `api_key_id`, `customer_name`, `tier`, `monthly_call_quota`, `current_month_calls` - `periods[]` — pro Kalendermonat: `period_start`, `call_count`, `bytes_sent`, `error_count`, `calls_over_quota` - `top_endpoints[]` — meistgenutzte Routen über das Fenster: `endpoint_pattern`, `call_count`, `error_count` - `bl_distribution[]` — Calls pro Bundesland (aus dem gmlid-Prefix abgeleitet): `bundesland_code`, `call_count` ## Fehler | Status | Bedingung | |---|---| | `400` | `months` außerhalb [1, 13] | | `401` | Kein/falscher Bearer-Token | | `404` | Key nicht gefunden | | `503` | `LODAPI_ADMIN_TOKEN` nicht gesetzt (Endpoint deaktiviert) | ## Verwandte Endpoints - [`GET /v1/admin/api-keys`](./admin-list-api-keys.md) — Keys auflisten. - [`POST /v1/admin/api-keys`](./admin-create-api-key.md) — neuen Key ausstellen. --- ### POST /v1/admin/api-keys - **operation_id**: `admin_create_api_key` - **stability**: `beta` - **since_version**: `0.1.0` - **auth**: `server_to_server_bearer` - **data_product**: `platform` - **snapshot_aware**: `False` - **attribution_block**: `False` - **rate_limit_tier**: `admin` # `POST /v1/admin/api-keys` — Kunden-API-Key ausstellen (Concierge-Billing) Generiert einen frischen `lod_*`-Key, speichert dessen **SHA-256-Hash + Prefix** in `lodapi_meta` und gibt den Klartext **genau einmal** zurück. Der Klartext ist danach nicht mehr rekonstruierbar — sofort an den Kunden übergeben oder Key widerrufen + neu ausstellen. Teil des Concierge-Billing-MVP (ADR-0014): Keys werden manuell via curl/Postman gemintet, nicht über Self-Serve-Signup. > **Auth-Pflicht.** Bearer-Token via `Authorization`-Header, muss `LODAPI_ADMIN_TOKEN` matchen (constant-time-compare). Ohne Token-Setup auf dem Server: **503**. ## Wann verwenden - **NICHT als Client-Endpoint.** Exklusiv für Lodapi-Operations (Konsti). - Beim Onboarding eines neuen Pilot-/Pro-Kunden. - Der zurückgegebene `plaintext_key` geht **einmalig** an den Kunden (z.B. per Welcome-Mail). ## Example ```bash curl -X POST https://api.lodapi.de/v1/admin/api-keys \ -H "Authorization: Bearer $LODAPI_ADMIN_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "customer_name": "Enpal GmbH", "tier": "pro", "contact_email": "tech@enpal.de", "monthly_call_quota": 100000, "stripe_customer_id": "cus_...", "notes": "Solar-Pilot, FOUNDER30" }' ``` ## Request-Body | Feld | Type | Required | Beschreibung | |---|---|---|---| | `customer_name` | string | yes | Anzeigename des Kunden | | `tier` | string | yes | gültiger Tier (z.B. `free`, `pro`, `scale`) | | `contact_email` | string | no | Kontaktadresse | | `monthly_call_quota` | int | no | Monats-Kontingent (≥ 0; `null` = unlimitiert) | | `stripe_customer_id` | string | no | Verknüpfung zum Stripe-Kunden | | `notes` | string | no | freie Notiz | ## Response `201 Created · application/json` — enthält `plaintext_key` (**show-once**), `api_key_id`, `key_prefix`, `customer_name`, `tier`, `monthly_call_quota`, `created_at`. ## Fehler | Status | Bedingung | |---|---| | `401` | Kein/falscher Bearer-Token | | `422` | Validation-Error (ungültiger `tier`, negative Quota) | | `503` | `LODAPI_ADMIN_TOKEN` nicht gesetzt (Endpoint deaktiviert) | ## Verwandte Endpoints - [`GET /v1/admin/api-keys`](./admin-list-api-keys.md) — ausgestellte Keys auflisten. - [`DELETE /v1/admin/api-keys/{api_key_id}`](./admin-revoke-api-key.md) — Key widerrufen. - [`GET /v1/admin/api-keys/{api_key_id}/usage`](./admin-api-key-usage.md) — Nutzungsreport. --- ### GET /v1/admin/data-freshness - **operation_id**: `admin_data_freshness` - **stability**: `beta` - **since_version**: `0.1.0` - **auth**: `server_to_server_bearer` - **data_product**: `platform` - **snapshot_aware**: `False` - **attribution_block**: `False` - **rate_limit_tier**: `admin` # `GET /v1/admin/data-freshness` — Daten-Aktualität pro Bundesland Liefert eine Liste aller registrierten Bundesländer mit ihrem aktuellen Snapshot-Datum, `last_sync`-Zeitstempel und der Differenz in Tagen seit dem letzten Connector-Run. Dient als Grundlage für Monitoring-Alerts wenn ein BL über die erwartete Update-Frequenz hinaus stagniert. --- ### GET /v1/admin/health - **operation_id**: `admin_health` - **stability**: `beta` - **since_version**: `0.1.0` - **auth**: `server_to_server_bearer` - **data_product**: `platform` - **snapshot_aware**: `False` - **attribution_block**: `False` - **rate_limit_tier**: `admin` # `GET /v1/admin/health` — Admin-Health-Check Liefert einen erweiterten Health-Snapshot der API für Operator-Diagnose: DB-Connection-Status, asyncpg-Pool-Auslastung, Connector-Run-Historie und Datenbank-Latenz. Im Gegensatz zu `/healthz` (Liveness-Probe) hat dieser Endpoint Authentifizierungs-Pflicht und liefert detailliertere Metriken. --- ### GET /v1/admin/api-keys - **operation_id**: `admin_list_api_keys` - **stability**: `beta` - **since_version**: `0.1.0` - **auth**: `server_to_server_bearer` - **data_product**: `platform` - **snapshot_aware**: `False` - **attribution_block**: `False` - **rate_limit_tier**: `admin` # `GET /v1/admin/api-keys` — Kunden-API-Keys auflisten Listet alle Kunden-API-Keys. Widerrufene Keys sind standardmäßig ausgeblendet — `include_revoked=true` nimmt sie mit auf. Der Klartext wird **nie** zurückgegeben, nur `key_prefix` zur menschlichen Zuordnung. > **Auth-Pflicht.** Bearer-Token via `Authorization`-Header, muss `LODAPI_ADMIN_TOKEN` matchen. Ohne Token-Setup auf dem Server: **503**. ## Wann verwenden - **NICHT als Client-Endpoint.** Exklusiv für Lodapi-Operations. - Übersicht über aktive Kunden + ihre Tiers/Quotas. - `key_prefix` identifiziert einen Key in Logs ohne den Klartext zu kennen. ## Query-Parameter | Param | Type | Default | Beschreibung | |---|---|---|---| | `include_revoked` | bool | `false` | widerrufene Keys mit auflisten | ## Example ```bash curl https://api.lodapi.de/v1/admin/api-keys?include_revoked=true \ -H "Authorization: Bearer $LODAPI_ADMIN_TOKEN" ``` ## Response `200 OK · application/json` — `{ "api_keys": [...], "count": N }`. Jedes Item: `api_key_id`, `key_prefix`, `customer_name`, `contact_email`, `tier`, `monthly_call_quota`, `stripe_customer_id`, `created_at`, `updated_at`, `last_used_at`, `revoked_at`. ## Fehler | Status | Bedingung | |---|---| | `401` | Kein/falscher Bearer-Token | | `503` | `LODAPI_ADMIN_TOKEN` nicht gesetzt (Endpoint deaktiviert) | ## Verwandte Endpoints - [`POST /v1/admin/api-keys`](./admin-create-api-key.md) — neuen Key ausstellen. - [`DELETE /v1/admin/api-keys/{api_key_id}`](./admin-revoke-api-key.md) — Key widerrufen. - [`GET /v1/admin/api-keys/{api_key_id}/usage`](./admin-api-key-usage.md) — Nutzungsreport. --- ### GET /v1/admin/pipeline-runs - **operation_id**: `admin_pipeline_runs` - **stability**: `beta` - **since_version**: `0.1.0` - **auth**: `server_to_server_bearer` - **data_product**: `platform` - **snapshot_aware**: `False` - **attribution_block**: `False` - **rate_limit_tier**: `admin` # `GET /v1/admin/pipeline-runs` — Pipeline-Run-Audit Liefert eine paginierte Liste der letzten Pipeline-Runs (Voll-Imports, Tilings, Finalizations) pro Bundesland mit Start-/End-Zeitstempel, Stage-Sequenz, Error-Count und Resultat. Dient dem Operator zur Nachverfolgung von Pipeline-Erfolg/-Failure. --- ### POST /v1/admin/datasets - **operation_id**: `admin_register_dataset` - **stability**: `stable` - **since_version**: `0.1.0` - **auth**: `server_to_server_bearer` - **data_product**: `platform` - **snapshot_aware**: `True` - **attribution_block**: `False` - **rate_limit_tier**: `admin` # `POST /v1/admin/datasets` — Dataset registrieren (Server-to-Server) UPSERT eines Dataset-Eintrags + N Tileset-Einträgen in `lodapi_meta`. Wird von ETL-Hosts (M1-Macs, künftig Airflow-Worker) aufgerufen, **nachdem** die Tiles auf die Prod-Disk gerollt wurden — sodass `GET /v1/datasets` aufhört, eine leere Liste zurückzugeben. Idempotent: zweimal mit derselben `(bundesland_code, snapshot_date)` posten = UPDATE statt INSERT. > **Auth-Pflicht.** Bearer-Token via `Authorization`-Header. Token muss `LODAPI_ADMIN_TOKEN`-Env-Var matchen (constant-time-compare). Ohne Token-Setup auf dem Server: **503**. ## Wann verwenden - **NICHT als Client-Endpoint**. Dieser ist exklusiv für die ETL-Pipeline. - Nur Lodapi-Operations-Hosts dürfen das Token haben. - Curl-Calls aus dem Public-Internet schlagen mit 401 fehl. ## Example (ETL-internal) ```bash curl -X POST https://api.lodapi.de/v1/admin/datasets \ -H "Authorization: Bearer $LODAPI_ADMIN_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "bundesland_code": "he", "snapshot_date": "2026-04-15", "source_url": "https://gds.hessen.de/...", "source_hash": "sha256:...", "schema_name": "bl_he", "building_count": 4934236, "validation_pass_pct": 99.7, "import_started_at": "2026-05-12T18:00:00Z", "import_finished_at": "2026-05-13T03:42:18Z", "tilesets": [ { "region_code": "he", "s3_key": "s3://lodapi-tiles/he/2026-04-15/tileset.json", "bbox_wgs84": [8.0, 49.4, 10.2, 51.65], "tile_count": 21785, "total_bytes": 731000000 } ] }' ``` ## Request-Body | Feld | Type | Required | Beschreibung | |---|---|---|---| | `bundesland_code` | string (2) | yes | 2-Buchstaben-BL-Code | | `snapshot_date` | date | yes | ISO 8601 (`YYYY-MM-DD`) | | `source_url` | string | yes | Quelle (Behörden-URL oder lokaler Snapshot) | | `source_hash` | string | no | SHA-256 des Quellpakets | | `schema_name` | string | yes | Postgres-Schema (`bl_`) | | `building_count` | int | no | | | `validation_pass_pct` | float | no | val3dity-Pass-Rate (0..100) | | `import_started_at` | datetime | no | | | `import_finished_at` | datetime | no | erst gesetzt wenn Import durch | | `tilesets` | array | yes | Liste von Tileset-Einträgen (s.u.) | **Tileset-Element**: | Feld | Type | Required | |---|---|---| | `region_code` | string | yes | | `s3_key` | string | yes | | `bbox_wgs84` | float[4] | yes — `[minLon,minLat,maxLon,maxLat]` | | `tile_count` | int | no | | `total_bytes` | int | no | ## Response `200 OK · application/json` — Echo des persistierten Datasets mit Server-IDs (UUIDs). ## Fehler | Status | Bedingung | |---|---| | `401` | Kein/falscher Bearer-Token | | `503` | `LODAPI_ADMIN_TOKEN` nicht gesetzt (Endpoint deaktiviert) | | `422` | Validation-Error im Body (RFC 7807) | ## Verwandte Endpoints - [`GET /v1/datasets`](./list-datasets.md) — sichtbare Konsequenz des erfolgreichen Aufrufs. - [`GET /v1/tilesets`](./list-tilesets-bbox.md) — Tileset-Discovery. --- ### DELETE /v1/admin/api-keys/{api_key_id} - **operation_id**: `admin_revoke_api_key` - **stability**: `beta` - **since_version**: `0.1.0` - **auth**: `server_to_server_bearer` - **data_product**: `platform` - **snapshot_aware**: `False` - **attribution_block**: `False` - **rate_limit_tier**: `admin` # `DELETE /v1/admin/api-keys/{api_key_id}` — Kunden-API-Key widerrufen Setzt `revoked_at = now()`. Der Key authentifiziert **sofort** nicht mehr (der Active-Only-Index schließt widerrufene Zeilen aus). Idempotent im Sinne von: ein bereits widerrufener (oder unbekannter) Key liefert **404**. > **Auth-Pflicht.** Bearer-Token via `Authorization`-Header, muss `LODAPI_ADMIN_TOKEN` matchen. Ohne Token-Setup auf dem Server: **503**. ## Wann verwenden - **NICHT als Client-Endpoint.** Exklusiv für Lodapi-Operations. - Kompromittierter Key, Vertragsende, oder Key-Rotation (revoke + neu ausstellen). - Die Zeile bleibt zur Audit-/Usage-Historie erhalten (Soft-Delete via `revoked_at`). ## Path-Parameter | Param | Type | Beschreibung | |---|---|---| | `api_key_id` | string (UUID) | ID aus dem Create-/List-Response | ## Example ```bash curl -X DELETE https://api.lodapi.de/v1/admin/api-keys/3f9a...-uuid \ -H "Authorization: Bearer $LODAPI_ADMIN_TOKEN" ``` ## Response `200 OK · application/json` — `api_key_id`, `key_prefix`, `customer_name`, `tier`, `revoked_at`. ## Fehler | Status | Bedingung | |---|---| | `401` | Kein/falscher Bearer-Token | | `404` | Key nicht gefunden oder bereits widerrufen | | `503` | `LODAPI_ADMIN_TOKEN` nicht gesetzt (Endpoint deaktiviert) | ## Verwandte Endpoints - [`POST /v1/admin/api-keys`](./admin-create-api-key.md) — neuen Key ausstellen. - [`GET /v1/admin/api-keys`](./admin-list-api-keys.md) — Keys auflisten. --- ### GET /v1/buildings/{gmlid} - **operation_id**: `get_building_detail` - **stability**: `stable` - **since_version**: `0.1.0` - **auth**: `none` - **data_product**: `buildings` - **snapshot_aware**: `True` - **attribution_block**: `False` - **rate_limit_tier**: `public` # `GET /v1/buildings/{gmlid}` — Building-Detail Liefert ein einzelnes Gebäude mit voller LoD2-Geometrie (GeoJSON-encoded MultiPolygonZ, **aggregiert über alle Surfaces des `building_id`**). Föderiert: das Bundesland wird aus dem GML-ID-Präfix abgeleitet; ist das Präfix mehrdeutig, probiert die API alle aktiven BL durch. ## Examples ### curl ```bash curl -s https://api.lodapi.de/v1/buildings/DEHE_LOD2_45292_GMLID_1 | jq ``` ### Python — Felder lesen ```python import httpx r = httpx.get("https://api.lodapi.de/v1/buildings/DEHE_LOD2_45292_GMLID_1") r.raise_for_status() b = r.json() print(f"BL: {b['bundesland_code']}") print(f"building_id: {b['building_id']}, LoD: {b['lod']}") print(f"GML-ID: {b['gmlid']}") ``` ## Parameters | Parameter | In | Type | Required | Beschreibung | |---|---|---|---|---| | `gmlid` | path | string | yes | GML-ID des Gebäudes (BL-Präfix oder pure UUID) | ## Response `200 OK · application/json` — Schema `#/components/schemas/BuildingDetail`. Felder: | Feld | Bedeutung | |---|---| | `gmlid` | Echo der Anfrage | | `bundesland_code` | Quell-BL (2 Buchstaben) | | `building_id` | Interner Building-Key, über den die Surfaces aggregiert werden | | `lod` | LoD-Level (`"LoD2"` in Phase 1) | | `geometry` | GeoJSON-Geometrie (MultiPolygonZ, WGS84), aggregiert über alle Surfaces des `building_id` | ## Fehler | Status | Bedingung | |---|---| | `404` | `gmlid` in keinem BL gefunden | ## Stolperdrähte - **GML-IDs sind nicht stabil über Snapshots** — wenn die Behörde reparst, ändert sich die ID. Persistenz braucht ein eigenes Mapping. - **Nur Geometrie + Klassifikation**: Der Slim-Datenbestand enthält keine per-Building-Sachattribute. Die Response trägt `{gmlid, bundesland_code, building_id, geometry, lod}` — es gibt kein `properties`-Mapping. - **Geometrie ist building-aggregiert**: die `geometry` fasst alle Surfaces (Wall/Ground/Roof) des `building_id` zusammen. Für Surface-Level-Einzelflächen mit `surface_class` ist `/v1/buildings` (bbox) der Pfad. ## Verwandte Endpoints - [`GET /v1/buildings`](./list-buildings-bbox.md) — bbox-Discovery. - [`GET /v1/buildings/3d.glb`](./get-buildings-glb.md) — Multi-Building-GLB-Export. --- ### GET /v1/buildings/{gmlid}/roof - **operation_id**: `get_building_roof_surfaces` - **stability**: `beta` - **since_version**: `0.1.0` - **auth**: `none` - **data_product**: `buildings` - **snapshot_aware**: `True` - **attribution_block**: `True` - **rate_limit_tier**: `public` # `GET /v1/buildings/{gmlid}/roof` — Dachflächen mit Neigung & Ausrichtung (Solar-Keil) Liefert eine GeoJSON-`FeatureCollection` aller `con:RoofSurface`-Polygone (objectclass_id 712 in 3DCityDB v5) eines Gebäudes, identifiziert über seine GML-id. Jedes Feature trägt aus der LoD2-Geometrie abgeleitete Solar-Attribute. ## Feature-Attribute | Property | Beschreibung | |---|---| | `tilt_deg` | Neigung gegen die Horizontale (0–90°) | | `azimuth_deg` | Kompass-Ausrichtung der Außennormalen (0=N, 90=O, 180=S, 270=W); `null` bei nahezu flachen Flächen (tilt < 5°) | | `area_m2` | projizierte Dachfläche in m² (UTM, **nicht** die geneigte Fläche) | Eine leere FeatureCollection (`count: 0`) ist valide — für Gebäude, die im Datensatz existieren, aber keine RoofSurface-Features haben (z.B. SL-INSPIRE-Import, der nur Wand- + Bodenflächen liefert). ## Path-Parameter | Param | Type | Beschreibung | |---|---|---| | `gmlid` | string | GML-id des Gebäudes (z.B. `DEBBAL0100000001`) | ## Example ```bash curl https://api.lodapi.de/v1/buildings/DEBBAL0100000001/roof ``` ## Response `200 OK · application/json` — GeoJSON-FeatureCollection: ```json { "type": "FeatureCollection", "count": 2, "features": [ { "type": "Feature", "geometry": { "type": "Polygon", "coordinates": [ ... ] }, "properties": { "tilt_deg": 38.2, "azimuth_deg": 178.4, "area_m2": 54.1 } } ], "lodapi": { "gmlid": "DEBBAL0100000001", "bl": "bb" } } ``` Der `lodapi`-Block nennt die aufgelöste `gmlid` + das Quell-Bundesland (`bl`). ## Fehler | Status | Bedingung | |---|---| | `404` | `gmlid` in keinem aktiven Bundesland gefunden | ## Hinweise - Die Geometrie stammt aus dem LoD2-Bestand — Dachflächen sind ebene Facetten, nicht die reale gekrümmte Oberfläche. - `azimuth_deg` ist für Solar-Lead-Qualifizierung gedacht (Süd-Ausrichtung = hoher Ertrag); für die finale Ertragsrechnung gehört noch Verschattung dazu (volle Solar-API, Phase Q4 2026). ## Verwandte Endpoints - [`GET /v1/buildings`](./list-buildings-bbox.md) — Gebäude per Bbox finden (liefert `gmlid`). - [`GET /v1/buildings/{gmlid}`](./get-building-detail.md) — Gebäude-Detailgeometrie. --- ### GET /v1/buildings/3d.glb - **operation_id**: `get_buildings_glb` - **stability**: `stable` - **since_version**: `0.1.0` - **auth**: `none` - **data_product**: `buildings` - **snapshot_aware**: `True` - **attribution_block**: `False` - **rate_limit_tier**: `public` # `GET /v1/buildings/3d.glb` — Bbox als einzelnes GLB Bündelt **alle Gebäude einer WGS84-Bbox in eine einzige binäre glTF-Datei** (`model/gltf-binary`). Ready für Three.js, Blender, QGIS-3D-Map-Plugins. Koordinaten sind im lokalen ENU-Frame (East-North-Up, Origin im bbox-Center). Z ist per default per-Building-normalisiert — jedes Gebäude steht „auf Z=0". Optional: Draco-Kompression (18-bit Position-Quantization), Material-pro-Surface-Klasse (Wall/Ground/Roof), Merging-Modus. ## Wann verwenden - Architektur-Visualisierung / 3D-Konfiguratoren (z.B. PV-Tool, Bebauungsplan-Browser). - Blender-Import für Renderings. - Three.js-Scene ohne 3D-Tiles-Streaming-Komplexität. Für **streaming/large-area-Rendering** ist `/v1/tilesets` der bessere Pfad. `3d.glb` ist für **statisch-bbox-Use-Cases**. ## Examples ### curl ```bash curl -s "https://api.lodapi.de/v1/buildings/3d.glb?bbox=8.66,50.108,8.665,50.111&compression=draco&colorize_roofs=true" \ > frankfurt-block.glb ``` ### Three.js — Drop-in-Loader ```ts import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js"; import { DRACOLoader } from "three/addons/loaders/DRACOLoader.js"; const draco = new DRACOLoader(); draco.setDecoderPath("https://www.gstatic.com/draco/v1/decoders/"); const loader = new GLTFLoader().setDRACOLoader(draco); const url = "https://api.lodapi.de/v1/buildings/3d.glb?bbox=8.66,50.108,8.665,50.111&compression=draco"; const gltf = await loader.loadAsync(url); scene.add(gltf.scene); ``` ### Python — Offline-Variante ```bash # Pure offline-Skript ohne API-Roundtrip, gleicher Output: uv run python bin/scenerii_bbox_export.py \ --bbox 8.66,50.108,8.665,50.111 \ --out frankfurt-block.glb \ --draco --draco-position-bits 18 \ --colorize-roofs ``` ## Parameters | Parameter | In | Type | Required | Default | Beschreibung | |---|---|---|---|---|---| | `bbox` | query | string | yes | — | WGS84 `minLon,minLat,maxLon,maxLat` | | `z_base` | query | enum | no | `per_building` | `per_building` / `bbox_min` / `absolute` | | `compression` | query | enum | no | `none` | `none` / `draco` | | `colorize_roofs` | query | bool | no | `false` | Wall/Ground/Roof als separate Materials | | `merge_buildings` | query | bool | no | `true` | Alle Gebäude in 1 Mesh + Primitive-pro-Material | | `target_frame` | query | enum | no | `utm` | `utm` (UTM-Meter, Blender/DCC) / `mercator` (MapLibre-Scene-Units am Anchor, scenerii-App-kompatibel) | | `origin` | query | enum | no | `center` | `center` (bbox-Mitte) / `corner` (bbox-(minLon,minLat); überstehende Gebäude bleiben relativ korrekt) | | `rotate_x` | query | float | no | `0` | Rotation um X-Achse in Grad, intrinsisch vor Z | | `rotate_z` | query | float | no | `0` | Rotation um Z-Achse in Grad, intrinsisch nach X. Blender-Y-up: `rotate_x=-90&rotate_z=-90` | | `include_ground` | query | bool | no | `true` | `false` = GroundSurface (class 710) weglassen, vermeidet Z-Fighting mit Boden-Layer | | `weld_tolerance_m` | query | float | no | `0.0` | Vertex-Merge-Toleranz (Quantize). 0 = aus; 0.01 schrumpft Stadt-Meshes um ~40 % | ### Admin-Header | Header | Beschreibung | |---|---| | `X-Lodapi-Admin-Token` | Wenn gesetzt + Match gegen Server-`LODAPI_ADMIN_TOKEN`: erlaubt bis 250 km² / 1 000 000 Buildings (statt 1 km² / 20 000). Server-zu-Server only, kein Public-Sharing. | ## Z-Normalization - **`per_building`** (Default): jedes Gebäude steht auf Z=0. Konsistente Höhe-über-Boden für Visualizer, ideal für scenerii-style-Apps. - **`bbox_min`**: alle Gebäude verschoben um `min(z)` der bbox. Gelände bleibt erkennbar, Bbox-Boden auf Z=0. - **`absolute`**: original DHHN2016-NHN-Werte. Für GIS-Werkzeuge mit Terrain-Kontext. ## Response `200 OK · model/gltf-binary` — eine GLB-Datei. Antwort-Header tragen Metadata: | Header | Bedeutung | |---|---| | `ETag` | für Conditional-GET | | `Cache-Control` | `public, max-age=300` | | `X-Lodapi-Buildings` | Anzahl Gebäude im GLB | | `X-Lodapi-Anchor-Srid` | UTM-Anker-EPSG (z.B. 25832) | | `X-Lodapi-Anchor-LonLat` | `lon,lat` des Anchor-Punkts (WGS84) — bei `origin=corner` = `minLon,minLat` | | `X-Lodapi-Origin-EN` | `E,N` des Origin-Punkts im Anker-CRS | | `X-Lodapi-Target-Frame` | `utm` oder `mercator` | | `X-Lodapi-Z-Base` | gewähltes Z-Modell | | `X-Lodapi-Compression` | tatsächlich angewandte Compression (`none` / `draco`) | | `X-Lodapi-Compression-Requested` | angefragte Compression — Vergleich zu `X-Lodapi-Compression` signalisiert Fallback | | `X-Lodapi-Compression-Error` | nur gesetzt bei Fallback: kurze Fehler-Beschreibung | | `X-Lodapi-Raw-Size` | unkomprimierte Bytes (zum Vergleich) | | `X-Lodapi-Max-Buildings` | Server-Limit (20000 oder 1000000 bei Admin-Token) | | `X-Lodapi-Admin-Limits` | `1` wenn Admin-Bypass aktiv, sonst `0` | ## Limits | Limit | Public-Default | Mit `X-Lodapi-Admin-Token` | |---|---|---| | `bbox`-Fläche | 1 km² | 250 km² | | Gebäude-Count | 20 000 | 1 000 000 | Bei Überschreitung: `413 Payload Too Large`. Public-Limit-Fehlermeldung enthält Hint `or supply X-Lodapi-Admin-Token`. ## Stolperdrähte - **Draco-Default-Quantization von gltf-pipeline ist 11 bit** (~5 m Auflösung bei 10 km bbox) — Lodapi nutzt **18 bit** (3,8 cm @ 10 km), sonst zerlegt Draco die Geometrie sichtbar. Wenn du eigene Draco-Codecs schaltest, halt mind. 18 bit ein. - **Frame-Wahl ist Konsumenten-spezifisch**: `target_frame=utm` (Default) für Blender/DCC/Cesium-ENU; `target_frame=mercator` für MapLibre-/Three.js-Setups, die `MercatorCoordinate.fromLngLat()` als Scene-Basis nutzen (z. B. scenerii-App). UTM-Vertices in einem Mercator-Scene-Frame driften linear mit Entfernung vom Anchor wegen `sec(lat)`-Stretch. - **`origin=corner` für Tile-Style-Mounts**: überstehende Gebäude erhalten dann leicht negative Vertex-Koordinaten — das ist absichtlich, damit ihre Position relativ zur BBox-Ecke korrekt bleibt. - **`merge_buildings: false`** erzeugt 1 Mesh pro `gmlid` — drastisch mehr Draw-Calls in Three.js. Default `true` (1 Mesh + Materials) ist fast immer richtig. - **Compression-Fallback verifizieren**: wenn `X-Lodapi-Compression: none` zurückkommt obwohl `?compression=draco` angefragt war, sind die Bytes roh. `X-Lodapi-Compression-Error` enthält die Ursache (typisch: `gltf-pipeline` nicht im Container-Image). - **`include_ground=false`** vermeidet Z-Fighting mit Boden-Layer in 3D-Viewern; GroundSurface ist eh selten sichtbar. ## Verwandte Endpoints - [`GET /v1/buildings`](./list-buildings-bbox.md) — FeatureCollection statt GLB. - [`GET /v1/tilesets`](./list-tilesets-bbox.md) — 3D-Tiles-Streaming für große Flächen. --- ### GET /v1/terrain/elevation - **operation_id**: `get_terrain_elevation` - **stability**: `stable` - **since_version**: `0.1.0` - **auth**: `none` - **data_product**: `terrain` - **snapshot_aware**: `True` - **attribution_block**: `False` - **rate_limit_tier**: `public` # `GET /v1/terrain/elevation` — Punktabfrage Geländehöhe Liefert die Geländehöhe (DHHN2016, m über NHN) an einem WGS84-Punkt. Die API routet die Anfrage über einen PostGIS-Spatial-Index auf den zuständigen DGM1-Tile und sampelt das Pixel via GDAL/rasterio mit HTTP-Range-Request auf dem Cloud-Optimized GeoTIFF. **Kein Bundesland ist hartcodiert** — Punkte überall in Deutschland funktionieren, sofern Lodapi den DGM1-Layer für die zuständige Region ingested hat. ## Wann verwenden - Höhenkote für einen Adresspunkt holen (z.B. für PV-Konfigurator, Statik-Vorabprüfung). - Karten-UI mit Hover-Tooltip „Höhe an Cursor-Position". - Geo-Pipeline: LoD2-Gebäude auf Geländehöhe normieren (Z-Differenz Gebäude-First-Floor – Terrain). Wenn du **viele Punkte entlang einer Linie** sampeln willst → `/v1/terrain/profile`. Wenn du **eine Fläche** als Mesh brauchst → `/v1/terrain-mesh/datasets` (3D-Tiles-Mesh-Tilesets). ## Examples ### curl — Brandenburger Tor ```bash curl -s 'https://api.lodapi.de/v1/terrain/elevation?lat=52.5163&lon=13.3777' | jq ``` ```json { "elevation_m": 34.95, "datum": "DHHN2016", "source_bl": "bb", "snapshot": "2025-12-18", "license": "dl-de-zero-2.0", "attribution": "© SenStadt Berlin (DL-DE/Zero 2.0)", "tile_id": "bb_33_388_5818" } ``` ### Python — Adresse → Höhe ```python import httpx def elevation_at(lat: float, lon: float) -> float | None: r = httpx.get( "https://api.lodapi.de/v1/terrain/elevation", params={"lat": lat, "lon": lon}, ) if r.status_code == 404: return None # außerhalb der Coverage oder NoData (Wasserfläche) r.raise_for_status() return r.json()["elevation_m"] print(elevation_at(52.5163, 13.3777)) # → 34.95 ``` ### TypeScript — Cesium-Integration ```ts async function clampToGround(lon: number, lat: number) { const url = new URL("https://api.lodapi.de/v1/terrain/elevation"); url.searchParams.set("lat", String(lat)); url.searchParams.set("lon", String(lon)); const r = await fetch(url); if (r.status === 404) return null; const { elevation_m } = await r.json(); return Cesium.Cartesian3.fromDegrees(lon, lat, elevation_m); } ``` ## Parameters | Parameter | In | Type | Required | Default | Range | Beschreibung | |---|---|---|---|---|---|---| | `lat` | query | float | yes | — | −90..90 | WGS84-Breite (Grad) | | `lon` | query | float | yes | — | −180..180 | WGS84-Länge (Grad) | | `srs` | query | string | no | `EPSG:4326` | — | Räumlicher Bezug. Aktuell nur EPSG:4326. | ## Response `200 OK · application/json` — Schema-Quelle [`openapi.json`](../openapi/openapi.json) (`#/components/schemas/ElevationResponse`). Felder: | Feld | Beschreibung | |---|---| | `elevation_m` | Höhe in Metern, DHHN2016, gerundet auf 3 Nachkommastellen | | `datum` | Vertikales Datum (immer `DHHN2016` in Phase 1) | | `source_bl` | 2-Buchstaben-BL-Code des zuständigen Tile | | `snapshot` | Snapshot-Datum des DGM1-Quelldatensatzes (ISO 8601) | | `license` | Lizenz-ID (`dl-de-zero-2.0`, `cc-by-4.0`, `dl-de-by-2.0`) | | `attribution` | Pflicht-Attribution-String für UI-Anzeige | | `tile_id` | Eindeutige Tile-Kennung (Cache-Key oder Audit-Pfad) | ## Fehler | Status | Bedeutung | Bedingung | |---|---|---| | `422` | Unprocessable Entity | `lat`/`lon` außerhalb Range, `srs` nicht EPSG:4326 (FastAPI/Pydantic-Validation) | | `404` | Not Found, kein Tile | Punkt außerhalb aller ingested Terrain-Coverages | | `404` | Not Found, NoData | Tile gefunden, aber Pixel ist NoData (Wasser, Tile-Rand) | | `502` | Bad Gateway | COG-Quelle nicht erreichbar (Pass-Through-Modus auf Behörden-Server) | | `503` | Service Unavailable | rasterio nicht installiert (deploy issue) | Alle Fehler sind im **RFC-7807-Format** (`application/problem+json`). ## Stolperdrähte - **Wasserflächen** liefern `404` mit Hinweis-Text, nicht `0.0`. Mancher PV-Konfigurator interpretiert `0` als „Meereshöhe" — der bewusste 404 verhindert das. - **Tile-Rand-Pixel** werden 0.6 m einwärts gesampelt (siehe ADR-0009 §Edge-Cases) — sonst gibt es zwischen zwei benachbarten Tiles minimale Sprünge. - **CRS-Wechsel** für östliche BL (BB/SN/MV/BE) erfolgt automatisch (UTM33N statt UTM32N). Aus Konsumentensicht: WGS84 rein, NHN raus. - **`source_bl` ist die BL-Zuordnung des Datensatzes**, nicht zwingend des Standorts. An Bundesländer-Grenzen kann ein Punkt im BB-Tile liegen, obwohl er administrativ noch zu BE gehört. ## Live-Coverage Stand 2026-06: alle 16 Bundesländer (16/16) via `/v1/terrain/elevation`. Aktuelles Inventar via `/v1/terrain/datasets`. ## Verwandte Endpoints - [`GET /v1/terrain/datasets`](./list-terrain-datasets.md) — Coverage + Snapshot-Stand. - [`GET /v1/terrain/profile`](./get-terrain-profile.md) — Linien-Sampling für mehrere Punkte. --- ### GET /v1/terrain/profile - **operation_id**: `get_terrain_profile` - **stability**: `stable` - **since_version**: `0.1.0` - **auth**: `none` - **data_product**: `terrain` - **snapshot_aware**: `True` - **attribution_block**: `False` - **rate_limit_tier**: `public` # `GET /v1/terrain/profile` — Höhenprofil entlang einer Linie Sampelt die Geländehöhe (DHHN2016) an N gleichverteilten Punkten entlang einer WGS84-Polyline. Liefert GeoJSON-FeatureCollection mit kumulierten Distanzen + Pro-Tile-Quellen-Tracking. ## Wann verwenden - Wanderwege / Mountainbike-Strecken-Tools (Höhenprofil-Chart). - Lineare Infrastruktur-Planung (Glasfaser, Leitungen, Bahn). - Sichtachsen-Analyse über Gelände. Für **eine einzelne Punktabfrage** ist `/v1/terrain/elevation` schneller — `/profile` öffnet pro betroffenem Tile eine separate COG-Sample-Operation (Batch-optimiert). ## Examples ### curl — 500 Punkte über 10 km ```bash curl -s 'https://api.lodapi.de/v1/terrain/profile?coords=13.37,52.51;13.45,52.55&samples=500' \ | jq '.properties, .features[0]' ``` ### Python — Chart-Datenpunkte ```python import httpx, json r = httpx.get( "https://api.lodapi.de/v1/terrain/profile", params={"coords": "13.37,52.51;13.40,52.53;13.45,52.55", "samples": 200}, ) data = r.json() chart = [ (f["properties"]["distance_m"], f["properties"]["elevation_m"]) for f in data["features"] if f["properties"]["elevation_m"] is not None ] print(f"Total length: {data['properties']['total_length_m']:.0f} m, {len(chart)} valid samples") ``` ## Parameters | Parameter | In | Type | Required | Default | Range | Beschreibung | |---|---|---|---|---|---|---| | `coords` | query | string | yes | — | ≥ 2 Punkte | `lon1,lat1;lon2,lat2[;...]` WGS84 Grad | | `samples` | query | int | no | 100 | 2..2000 | Anzahl Sample-Punkte | ## Response `200 OK · application/json` — GeoJSON-FeatureCollection (RFC 7946) mit Lodapi-Erweiterungen in `properties`. ```json { "type": "FeatureCollection", "properties": { "datum": "DHHN2016", "coords_count": 3, "samples": 200, "total_length_m": 9543.7, "partial": false, "failed_tile_ids": [] }, "features": [ { "type": "Feature", "geometry": { "type": "Point", "coordinates": [13.37, 52.51] }, "properties": { "elevation_m": 38.4, "distance_m": 0.0, "source_bl": "be", "tile_id": "be_33_387_5821" } } ] } ``` ## Partial-Modus `properties.partial: true` zeigt an, dass **mindestens ein Sample** außerhalb der Coverage liegt oder bei der COG-Sample-Operation gescheitert ist: - Diese Features haben `elevation_m: null`, `tile_id: null`. - `properties.failed_tile_ids[]` listet Tile-IDs, deren Read failed ist (Behörden-Server kurz down, NoData-Cluster). - Die anderen Features sind weiter valide — die Antwort ist **200**, nicht 502. ## Fehler | Status | Bedingung | |---|---| | `400` | `coords` malformed oder < 2 Punkte | | `404` | **Alle** Samples außerhalb Coverage | | `422` | `samples` out of range | | `502` | DB unerreichbar | ## Stolperdrähte - **Linien-Länge wird in UTM32N gemessen** (auch wenn die Polyline durch UTM33N-Gebiet geht) — minimaler Längenfehler an Region-Grenzen, unter 0,1 %. - **`samples` skaliert linear mit Antwortgröße** — 2000 Samples × ~150 Bytes/Feature ≈ 300 KB. - **Pro-Tile-Batching** macht große Profile sehr schnell, weil ein COG einmal geöffnet wird und alle seine Sample-Punkte zusammen abruft. ## Verwandte Endpoints - [`GET /v1/terrain/elevation`](./get-terrain-elevation.md) — Punktabfrage. - [`GET /v1/terrain/datasets`](./list-terrain-datasets.md) — Coverage. --- ### GET /v1/tilesets/{tileset_id} - **operation_id**: `get_tileset` - **stability**: `stable` - **since_version**: `0.1.0` - **auth**: `none` - **data_product**: `buildings` - **snapshot_aware**: `True` - **attribution_block**: `False` - **rate_limit_tier**: `public` # `GET /v1/tilesets/{tileset_id}` — Tileset-Detail Holt das Metadaten-Objekt eines einzelnen Tilesets über seine stabile UUID. Verwende den Endpoint, wenn du eine `tileset_id` aus einer vorherigen `/v1/tilesets`-Antwort persistiert hast (z.B. in einem Frontend-State oder einer Bookmark-URL) und das Tileset jetzt erneut auflösen willst. ## Examples ### curl ```bash curl -s https://api.lodapi.de/v1/tilesets/f4b1c0d2-3e5a-4d8b-9f01-2a3b4c5d6e7f | jq ``` ### Python ```python import httpx ts = httpx.get(f"https://api.lodapi.de/v1/tilesets/{tileset_id}").json() print(ts["tileset_url"]) # → https://tiles.lodapi.de/he/2026-04-15/tileset.json ``` ## Parameters | Parameter | In | Type | Required | Beschreibung | |---|---|---|---|---| | `tileset_id` | path | string (UUID) | yes | Stabile Tileset-ID aus `/v1/tilesets` | ## Response `200 OK · application/json` — Schema `#/components/schemas/Tileset` in [`openapi.json`](../openapi/openapi.json). Felder identisch zu einem Eintrag der `/v1/tilesets`-Liste (siehe dort). ## Fehler | Status | Bedingung | |---|---| | `404` | `tileset_id` unbekannt | ## Stolperdrähte - **UUIDs sind stabil über Snapshots** — der gleiche BL bekommt mit neuem Snapshot eine **neue** Tileset-ID. Für „immer aktuell" brauchst du `/v1/tilesets?bbox=…` mit neuem Lookup. - **Kein Attribution-Block** in dieser Response. Wenn du das brauchst: über `/v1/datasets` oder `/v1/tilesets?bbox=…` holen. ## Verwandte Endpoints - [`GET /v1/tilesets`](./list-tilesets-bbox.md) — Discovery per Bbox. - [`GET /v1/tilesets/{tileset_id}/tileset.json`](./redirect-tileset-json.md) — direkt zum tileset.json (Redirect). --- ### GET /healthz - **operation_id**: `health_check` - **stability**: `stable` - **since_version**: `0.1.0` - **auth**: `none` - **data_product**: `platform` - **snapshot_aware**: `False` - **attribution_block**: `False` - **rate_limit_tier**: `public` # `GET /healthz` — Liveness-Check Antwortet `200 OK` mit `{"status": "ok", "service": "lodapi-api"}` solange der API-Container läuft. Wird vom Caddy-Reverse-Proxy + Docker-Compose-Healthcheck konsumiert. ## Wann verwenden - Monitoring-Pings (Prometheus-Blackbox, Uptime-Robot, …). - Caddy / Load-Balancer Health-Probes. - Sanity-Test nach jedem Deploy. ## Example ```bash curl -s https://api.lodapi.de/healthz | jq # {"status": "ok", "service": "lodapi-api"} ``` ## Parameters Keine. ## Response `200 OK · application/json`: ```json { "status": "ok", "service": "lodapi-api" } ``` ## Stolperdrähte - **Kein DB-Check** in dieser Antwort. Postgres kann down sein und `/healthz` antwortet trotzdem 200. Für DB-Liveness eigene Probe gegen `/v1/datasets` schicken. - **Phase 2** wird einen tieferen `/readyz` ergänzen, der DB-Connection-Pool + Auth-Provider prüft. ## Verwandte Endpoints Keine — Health ist orthogonal zum Datenmodell. --- ### GET /v1/buildings - **operation_id**: `list_buildings_bbox` - **stability**: `stable` - **since_version**: `0.1.0` - **auth**: `none` - **data_product**: `buildings` - **snapshot_aware**: `True` - **attribution_block**: `True` - **rate_limit_tier**: `public` # `GET /v1/buildings` — Gebäude per Bounding-Box Föderierte GeoJSON-FeatureCollection aller LoD2-**Surfaces** innerhalb einer WGS84-Bounding-Box. „Föderiert" bedeutet: die API durchsucht **alle** Bundesländer mit `import_finished_at IS NOT NULL` parallel und mischt die Ergebnisse. Aus Konsumentensicht: eine API für 16 Bundesländer. **Wichtig — Surface-Level**: Jedes Feature ist eine einzelne Surface (Wall/Ground/Roof), **nicht** ein ganzes Gebäude. Mehrere Surfaces gehören über `building_id` zum selben Gebäude — zum Rekonstruieren eines Gebäudes nach `building_id` gruppieren. ## Wann verwenden - Karten-Frontends, die Gebäude-Footprints + 3D-Geometrie auf einer Region brauchen. - Analyse-Scripts (Building-Count pro Stadtteil, Attribut-Filter). - 3D-Renderer mit eigener Triangulation (statt fertiges 3D-Tiles aus `/v1/tilesets`). Wenn du eine **bbox > ~1 km²** brauchst → eher `/v1/tilesets` verwenden (3D-Tiles streamen). `/v1/buildings` ist für detail-orientierte, kleinflächige Queries. ## Examples ### curl — kleine bbox Frankfurt-Mitte ```bash curl -s "https://api.lodapi.de/v1/buildings?bbox=8.66,50.108,8.665,50.111&limit=50" | jq '.count, .features[0].properties' ``` ### Python — bbox + Pagination ```python import httpx def fetch_all_buildings(bbox: str, limit: int = 1000): cursor = None while True: params = {"bbox": bbox, "limit": limit} if cursor: params["cursor"] = cursor r = httpx.get("https://api.lodapi.de/v1/buildings", params=params) r.raise_for_status() data = r.json() yield from data["features"] cursor = data["lodapi"]["next"] if not cursor: break for feature in fetch_all_buildings("8.66,50.108,8.69,50.13"): print(feature["id"], feature["properties"]["surface_class"]) ``` ### TypeScript — Cesium-Anbindung ```ts import * as Cesium from "cesium"; const r = await fetch( "https://api.lodapi.de/v1/buildings?bbox=8.66,50.108,8.665,50.111&limit=200" ); const fc = await r.json(); const ds = await Cesium.GeoJsonDataSource.load(fc, { clampToGround: false }); viewer.dataSources.add(ds); ``` ## Parameters | Parameter | In | Type | Required | Default | Beschreibung | |---|---|---|---|---|---| | `bbox` | query | string | yes | — | WGS84 `minLon,minLat,maxLon,maxLat` (Komma-separiert) | | `limit` | query | int | no | 100 | 1 ≤ limit ≤ 1000 | | `cursor` | query | string | no | — | Opaque cursor aus `lodapi.next` des vorherigen Calls | ## Response `200 OK · application/json` — GeoJSON-FeatureCollection. Schema-Quelle [`openapi.json`](../openapi/openapi.json). ```json { "type": "FeatureCollection", "bbox": [8.66, 50.108, 8.665, 50.111], "count": 47, "limit": 100, "features": [ { "type": "Feature", "id": "DEHE_LOD2_45292_GMLID_1", "geometry": { "type": "MultiPolygon", "coordinates": [[[ [8.661, 50.108, 105.2], ... ]]] }, "properties": { "surface_id": 12345678, "building_id": 987654, "gmlid": "DEHE_LOD2_45292_GMLID_1", "surface_class": 712, "bundesland": "he", "lod": "LoD2" } } ], "lodapi": { "attribution": [ { "source": "HLBG", "license": "DL-DE/Zero 2.0", "url": "https://www.govdata.de/dl-de/zero-2-0", "tiles_count": 1 } ], "next": "aGU6MTIzNDU2Nzg=" } } ``` ## Attribution Jede Antwort enthält `lodapi.attribution[]` mit einem Eintrag pro Bundesland, das in den `features[]` vorkommt. Die Felder: | Feld | Bedeutung | |---|---| | `source` | Behörden-/Geobasis-Stelle (z.B. „HLBG", „BezReg Köln") | | `license` | DL-DE/Zero 2.0, CC BY 4.0 oder DL-DE BY 2.0 | | `url` | Lizenz-URL für Verweis im UI | | `tiles_count` | nicht verwendet bei /v1/buildings (immer 1) | **Frontend-Pflicht**: Bei `license != DL-DE/Zero 2.0` muss der `source`-String sichtbar in der Map-Footer-Zeile stehen (siehe [Attribution-Guide](../guides/attribution.md)). ## Cursor-Pagination `/v1/buildings` verwendet **opaque cursor pagination** statt OFFSET, weil OFFSET auf großen PostGIS-bbox-Resultaten pathologisch langsam ist. Der Cursor ist ein base64-codiertes `:`-Tupel — clients sollten ihn als opak behandeln. **Achtung**: Der Cursor ist **federation-aware** (breaking change ggü. Phase-1-Clients, siehe [README](../../api/README.md#versions--stabilitäts-modell)). Cursor aus altem Format führen zu 400. ## Stolperdrähte - **`limit > 1000` → 422**. Validation Error, RFC-7807-Format. - **`bbox` ohne korrekte 4 Komma-Werte → 400** mit Hinweis auf `minLon,minLat,maxLon,maxLat`. - **bbox-Area-Limit gibt es nicht** auf `/v1/buildings` — aber: bei sehr großen bboxen werden viele Pages gezogen. Für Großflächen-Use-Cases ist `/v1/tilesets` der richtige Pfad (3D-Tiles streamen statt FeatureCollection). - **`features[0].properties.bundesland`** ist der unprefixte 2-Buchstaben-Code (`"he"`, `"nrw"`, …), keinerlei BL-Prefix in den IDs in dieser Property. - **Surface-Level, nicht Building-Level**: ein Feature = eine Surface. `properties.surface_class` ist die CityGML-ObjectClass-ID: `709` = WallSurface, `710` = GroundSurface, `712` = RoofSurface. Zum Rekonstruieren eines kompletten Gebäudes die Features nach `building_id` gruppieren (oder direkt `/v1/buildings/{gmlid}` für die aggregierte Geometrie nutzen). - **Nur Geometrie + Klassifikation**: Die Properties sind `{surface_id, building_id, gmlid, surface_class, bundesland, lod}`. Per-Building-Sachattribute sind im Slim-Datenbestand nicht enthalten; ältere Feldnamen wie `roof_type`/`tile_x`/`tile_y`/`feature_id` existieren nicht. ## Verwandte Endpoints - [`GET /v1/buildings/{gmlid}`](./get-building-detail.md) — Single-Building-Detail. - [`GET /v1/buildings/3d.glb`](./get-buildings-glb.md) — bbox → einzelnes GLB-Mesh. - [`GET /v1/tilesets`](./list-tilesets-bbox.md) — 3D-Tiles-Streaming für große Flächen. --- ### GET /v1/datasets - **operation_id**: `list_datasets` - **stability**: `stable` - **since_version**: `0.1.0` - **auth**: `none` - **data_product**: `buildings` - **snapshot_aware**: `True` - **attribution_block**: `False` - **rate_limit_tier**: `public` # `GET /v1/datasets` — Verfügbare LoD2-Datensätze Listet alle Bundesländer, für die Lodapi LoD2-Gebäude bereitstellt. Eine Zeile pro Bundesland, je mit dem aktuellsten Snapshot-Datum, Building-Count, Lizenz und Quell-URL. Dies ist der **Discovery-Endpoint** — typischerweise der erste Call jeder Lodapi-Integration. ## Wann verwenden - Frontend zeigt eine BL-Auswahl an und braucht „welche sind live"-Information. - Pipeline-Code prüft, ob ein Snapshot-Update verfügbar ist (Compare `snapshot_date`). - Compliance-Sicht: welche Lizenzen liegen pro BL an (relevant für eigene Attribution-Strings). ## Examples ### curl ```bash curl -s https://api.lodapi.de/v1/datasets | jq ``` ### Python ```python import httpx r = httpx.get("https://api.lodapi.de/v1/datasets") r.raise_for_status() for ds in r.json()["datasets"]: print(f'{ds["bundesland_code"]:3s} {ds["snapshot_date"]} {ds["building_count"]:>10,} {ds["license_id"]}') ``` ### TypeScript (fetch) ```ts const r = await fetch("https://api.lodapi.de/v1/datasets"); const { datasets } = await r.json() as { datasets: Dataset[] }; const live = datasets.filter(d => d.snapshot_date); console.log(`${live.length} Bundesländer live`); ``` ### Caching `/v1/datasets` setzt **keinen `ETag`** — Conditional GET via `If-None-Match`/`304` greift hier nicht. Caching läuft ausschließlich über `Cache-Control: public, max-age=300` (5 min Browser-/CDN-Cache). ## Parameters Keine. Der Endpoint nimmt keine Query- oder Path-Parameter entgegen. ## Response `200 OK · application/json` — Schema-Quelle [`openapi.json`](../openapi/openapi.json) (`#/components/schemas/DatasetListResponse`). ```json { "datasets": [ { "id": "he", "bundesland_code": "he", "name": "© HLBG (DL-DE/Zero 2.0)", "license_id": "dl-de-zero-2.0", "source_url": "https://gds.hessen.de/INTERMET/Geodaten/3D-Gebaeudemodelle/", "snapshot_date": "2026-04-15", "building_count": 4934236, "validation_pass_pct": 99.7, "last_sync": "2026-05-13T09:42:18Z" } ], "count": 3 } ``` ### Antwort-Header | Header | Wert | Verwendung | |---|---|---| | `Cache-Control` | `public, max-age=300` | 5 min Browser-/CDN-Cache | ## Stolperdrähte - **`validation_pass_pct: null`** ist legitim — manche BL haben noch keinen `val3dity`-Run gemacht. Frontend muss null-tolerant sein. - **`snapshot_date`** ist das Datum des **Quell-Downloads**, nicht das Veröffentlichungsdatum der BL-Behörde. Manchmal liegen 6+ Monate dazwischen (z.B. ST liefert halbjährlich). - **Welche BL fehlt**: ein BL erscheint nur, wenn `import_finished_at IS NOT NULL` in `lodapi_meta.dataset`. Bei laufendem Voll-Run fehlt der BL bis zum Abschluss — keine Teil-Daten in der API. ## Verwandte Endpoints - [`GET /v1/tilesets`](./list-tilesets-bbox.md) — räumliche Tileset-Suche. - [`GET /v1/buildings`](./list-buildings-bbox.md) — föderierte Building-Query. - [`GET /v1/terrain/datasets`](./list-terrain-datasets.md) — Pendant für DGM1-Terrain. --- ### GET /v1/osm-cover/datasets - **operation_id**: `list_osm_cover_datasets` - **stability**: `beta` - **since_version**: `0.1.0` - **auth**: `none` - **data_product**: `osm-cover` - **snapshot_aware**: `True` - **attribution_block**: `False` - **rate_limit_tier**: `public` # `GET /v1/osm-cover/datasets` — OSM-Cover-Tilesets Discovery-Endpoint für die **OSM-Bodenbedeckung als 3D-Tiles** (ADR-0017): OSM-Flächen (Wiese, Wasser, Wald, Straßen, Schienen …) auf das DGM1-Gelände drapiert und als b3dm + Draco-Compressed glTF gerendert. Konsumiert direkt von Cesium, three.js (`3d-tiles-renderer`), Blender (3D-Tiles-Addon). Pro BL der jüngste Snapshot. Der entscheidende Unterschied zu `/v1/terrain-mesh/datasets`: jedes BL liefert **mehrere thematische Cluster-Tilesets** statt genau einem. Die Themes (`surface`, `transport`, `canopy`, bei Küsten-BL zusätzlich `sea`) sind eigene Tilesets, die der Client als parallele Layer lädt — unabhängig rebuild- und cutover-bar. Die Tilesets selbst werden statisch von Caddy unter `tiles.lodapi.de/osm-cover////tileset.json` ausgeliefert (Reverse-Proxy auf Hetzner-Storage-Box). > **Status: beta.** Cover-Geometrie ist Buffer-basiert (Straßen), Veredelung via osm2streets ist Phase 2. Lizenz: ODbL (Share-Alike) — siehe Attribution. ## Wann verwenden - 3D-Visualizer, die LoD2-Gebäude **+** Gelände **+** Bodenbedeckung zusammen zeigen. - Selektives Layern: nur `surface` ohne `transport`/`canopy`, oder Canopy als zuschaltbarer Baum-Layer. - Three.js/Cesium-Szenen, die OSM-Kontext ohne eigenen OSM-Render-Stack brauchen. ## Examples ### curl ```bash curl -s https://api.lodapi.de/v1/osm-cover/datasets \ | jq '.datasets[] | {bl: .bundesland_code, themes: [.themes[] | {name, tiles: .tile_count, url: .tileset_url}]}' ``` ### Three.js — alle Themes als parallele Tilesets ```ts import { TilesRenderer } from "3d-tiles-renderer"; const r = await fetch("https://api.lodapi.de/v1/osm-cover/datasets"); const { datasets } = await r.json(); const be = datasets.find(d => d.bundesland_code === "be"); for (const theme of be.themes) { const tiles = new TilesRenderer(theme.tileset_url); tiles.setCamera(camera); tiles.setResolutionFromRenderer(camera, renderer); scene.add(tiles.group); } ``` ### Cesium — Themes einzeln zuschaltbar ```ts const r = await fetch("https://api.lodapi.de/v1/osm-cover/datasets"); const { datasets } = await r.json(); const be = datasets.find(d => d.bundesland_code === "be"); const canopy = be.themes.find(t => t.name === "canopy"); const ts = await Cesium.Cesium3DTileset.fromUrl(canopy.tileset_url); viewer.scene.primitives.add(ts); // Baum-Layer separat ein-/ausblendbar ``` ## Parameters Keine. ## Response `200 OK · application/json` — Schema `#/components/schemas/OsmCoverDatasetListResponse`. ```json { "datasets": [ { "bundesland_code": "be", "snapshot_date": "2026-06-15", "themes": [ { "name": "surface", "tileset_url": "https://tiles.lodapi.de/osm-cover/be/2026-06-15/surface/tileset.json", "tile_count": 1234 }, { "name": "transport", "tileset_url": "https://tiles.lodapi.de/osm-cover/be/2026-06-15/transport/tileset.json", "tile_count": 567 }, { "name": "canopy", "tileset_url": "https://tiles.lodapi.de/osm-cover/be/2026-06-15/canopy/tileset.json", "tile_count": 89 } ], "bbox": [13.0883, 52.3382, 13.7612, 52.6755], "license": "odbl-1.0", "attribution": "© OpenStreetMap-Mitwirkende (ODbL)", "built_at": "2026-06-15T22:14:00Z" } ] } ``` ### Antwort-Header | Header | Wert | |---|---| | `Cache-Control` | `public, max-age=300` | ## Themes | Theme | Inhalt | |---|---| | `surface` | Flächen-Bodenbedeckung (Grün, Wasser, Urban, Plätze, Parken, Sport, Friedhof, Hecke) | | `transport` | Straßen, Wege, Schienen (Buffer-Geometrie) | | `canopy` | Baumkronen-Mesh (aus DLR-Höhenraster) | | `sea` | Topf-Meer (flach Z=0) — nur Küsten-BL | Die `themes`-Liste ist variabel lang: Binnenland-BL liefern 3 Themes (ohne `sea`), Küsten-BL 4. ## Stolperdrähte - **ODbL-Share-Alike**: osm-cover-Daten unterliegen der Open Database License. Vor öffentlichem Einsatz Attribution + Share-Alike-Pflichten prüfen (Themis-Freigabe). - **Canopy ist best-effort**: Wenn das DLR-Höhenraster für ein BL fehlt/truncated ist, kann das `canopy`-Theme fehlen — der Endpoint listet dann nur die vorhandenen Themes. - **Blender-3D-Tiles-Addon braucht Draco-Decoder** (`KHR_draco_mesh_compression`). - **Cover liegt knapp über dem Terrain** (`cover_base_lift_m`), um Z-Fighting mit dem terrain-mesh zu vermeiden — beim gemeinsamen Rendern beide Produkte laden. ## Verwandte Endpoints - [`GET /v1/terrain-mesh/datasets`](./list-terrain-mesh-datasets.md) — Gelände-Mesh (ein Tileset pro BL). - [`GET /v1/tilesets`](./list-tilesets-bbox.md) — LoD2-Buildings-3D-Tiles. --- ### GET /v1/terrain/datasets - **operation_id**: `list_terrain_datasets` - **stability**: `stable` - **since_version**: `0.1.0` - **auth**: `none` - **data_product**: `terrain` - **snapshot_aware**: `True` - **attribution_block**: `False` - **rate_limit_tier**: `public` # `GET /v1/terrain/datasets` — Terrain-Coverage-Discovery Liefert eine Zeile pro Bundesland mit dem jüngsten DGM1-Snapshot. Pendant zu `/v1/datasets` für LoD2 — beantworte „welche BL sind live", bevor du Punkt- oder Linienabfragen issuest. ## Examples ### curl ```bash curl -s https://api.lodapi.de/v1/terrain/datasets | jq '.datasets[] | {bl: .bl, snap: .snapshot, count: .tile_count, lic: .license}' ``` ### Python — Coverage-Check ```python import httpx covered = {d["bl"] for d in httpx.get("https://api.lodapi.de/v1/terrain/datasets").json()["datasets"]} print(f"Terrain live in: {sorted(covered)}") ``` ## Parameters Keine. ## Response `200 OK · application/json` — Schema `#/components/schemas/TerrainDatasetListResponse`. ```json { "datasets": [ { "bl": "be", "license": "dl-de-zero-2.0", "snapshot": "2025-12-18", "format": "GeoTIFF (COG)", "source": "convert+storage-box", "tile_count": 297, "crs": "EPSG:25833", "vertical_datum": "DHHN2016", "attribution": "© SenStadt Berlin (DL-DE/Zero 2.0)", "coverage_pct": null } ], "count": 5 } ``` ### Antwort-Header | Header | Wert | |---|---| | `Cache-Control` | `public, max-age=300` | ## Stolperdrähte - **`coverage_pct` ist immer null in Phase-2a** — wir liefern ein ehrliches null statt ausgedachte 100.0. Phase-3 (ADR-0009) wird `coverage_polygon` für jedes Dataset berechnen. - **`source`** unterscheidet zwischen `convert+storage-box` (Lodapi-Konvertierung auf Hetzner-Storage-Box), `r2-mirror` (Mirror auf Cloudflare R2), `pass-through` (live gegen Behörden-Server). - **CRS pro BL**: westliche BL EPSG:25832 (UTM32N), östliche BL EPSG:25833 (UTM33N). `/v1/terrain/elevation` macht die Transformation automatisch. ## Verwandte Endpoints - [`GET /v1/terrain/elevation`](./get-terrain-elevation.md) — Punktabfrage. - [`GET /v1/terrain/profile`](./get-terrain-profile.md) — Linienprofil. - [`GET /v1/terrain-mesh/datasets`](./list-terrain-mesh-datasets.md) — 3D-Tiles-Mesh-Tilesets (Phase-2-Mesh). --- ### GET /v1/terrain-mesh/datasets - **operation_id**: `list_terrain_mesh_datasets` - **stability**: `beta` - **since_version**: `0.1.0` - **auth**: `none` - **data_product**: `terrain-mesh` - **snapshot_aware**: `True` - **attribution_block**: `False` - **rate_limit_tier**: `public` # `GET /v1/terrain-mesh/datasets` — Terrain-Mesh-Tilesets Discovery-Endpoint für 3D-Tiles-1.1-Mesh-Tilesets, die das DGM1-Gelände als **gerendertes Mesh** ausliefern (statt als Raster-COG wie `/v1/terrain/*`). Format: b3dm + Draco-Compressed glTF. Konsumiert direkt von Cesium, three.js (`3d-tiles-renderer`), Blender (3D-Tiles-Addon). Pro BL der jüngste Snapshot. Die Tilesets selbst werden statisch von Caddy unter `tiles.lodapi.de/terrain-mesh///tileset.json` ausgeliefert (Reverse-Proxy auf Hetzner-Storage-Box). > **Status: beta.** Phase-2a-Implementierung ist Single-Level — für große BL (NRW ~35k Tiles) lädt der Renderer alle Leaves auf einmal. Multi-Level-LoD-Pyramide ist Phase 2b. ## Wann verwenden - 3D-Visualizer, die LoD2-Gebäude **+** Geländerelief gleichzeitig zeigen sollen. - Blender-Workflows für Architektur-Renderings mit Topographie-Kontext. - Three.js-Apps ohne Cesium-Terrain-Provider-Lock-in. Für **API-Punkt-Höhe** ist `/v1/terrain/elevation` der richtige Pfad. Mesh ist Visual, Raster ist Query. ## Examples ### curl ```bash curl -s https://api.lodapi.de/v1/terrain-mesh/datasets | jq '.datasets[] | {bl: .bundesland_code, url: .tileset_url, tiles: .tile_count}' ``` ### Three.js — Drop-in mit 3d-tiles-renderer ```ts import { TilesRenderer } from "3d-tiles-renderer"; const r = await fetch("https://api.lodapi.de/v1/terrain-mesh/datasets"); const { datasets } = await r.json(); const be = datasets.find(d => d.bundesland_code === "be"); const tiles = new TilesRenderer(be.tileset_url); tiles.setCamera(camera); tiles.setResolutionFromRenderer(camera, renderer); scene.add(tiles.group); ``` ### Cesium — als generisches Tileset ```ts const r = await fetch("https://api.lodapi.de/v1/terrain-mesh/datasets"); const { datasets } = await r.json(); for (const t of datasets) { const ts = await Cesium.Cesium3DTileset.fromUrl(t.tileset_url); viewer.scene.primitives.add(ts); } ``` > **Hinweis**: Cesium konsumiert Mesh-Tilesets als generische 3D-Tiles, **nicht** als Terrain-Provider. Building-Clamping muss self-managed sein — bei LoD2 ist das aber egal, weil Z absolut aus dem CityGML kommt. ## Parameters Keine. ## Response `200 OK · application/json` — Schema `#/components/schemas/TerrainMeshDatasetListResponse`. ```json { "datasets": [ { "bundesland_code": "be", "snapshot_date": "2025-12-18", "tileset_url": "https://tiles.lodapi.de/terrain-mesh/be/2025-12-18/tileset.json", "tile_count": 297, "rtin_tolerance_m": 0.5, "tiling_scheme": "single-level-quadtree", "license": "dl-de-zero-2.0", "attribution": "© SenStadt Berlin (DL-DE/Zero 2.0)", "built_at": "2026-05-13T22:14:00Z" } ] } ``` ### Antwort-Header | Header | Wert | |---|---| | `Cache-Control` | `public, max-age=300` | ## Build-Parameter | Feld | Bedeutung | |---|---| | `rtin_tolerance_m` | Maximal-Fehler der RTIN-Approximation (typ. 0,5 m für LoD2-Default) | | `tiling_scheme` | aktuell `single-level-quadtree` (Phase 2a); Phase 2b: `multi-level-quadtree` | ## Stolperdrähte - **Blender-3D-Tiles-Addon braucht Draco-Decoder** (`KHR_draco_mesh_compression`). Verifikation steht noch aus (Phase-2a-Gate). - **Single-Level-Tilesets** sind teuer bei großen BL — Renderer-Frustum-Culling lädt potentiell alle Leaves. Workaround: bbox-Filter auf der Client-Seite, bis Phase 2b live ist. - **CRS in den Tiles**: ECEF-Koordinaten (geozentrisches Erd-System) per `B3dm.from_numpy_arrays`-Transform. Standard für 3D-Tiles 1.1. ## Verwandte Endpoints - [`GET /v1/terrain/datasets`](./list-terrain-datasets.md) — Raster-COG-Variante. - [`GET /v1/tilesets`](./list-tilesets-bbox.md) — LoD2-Buildings-3D-Tiles. --- ### GET /v1/tilesets - **operation_id**: `list_tilesets_bbox` - **stability**: `stable` - **since_version**: `0.1.0` - **auth**: `none` - **data_product**: `buildings` - **snapshot_aware**: `True` - **attribution_block**: `True` - **rate_limit_tier**: `public` # `GET /v1/tilesets` — 3D-Tiles per Bbox Liefert alle 3D-Tiles-1.1-Tilesets, deren Bounding-Volume die angegebene WGS84-Bbox schneidet. Ein Tileset = ein Bundesland-Snapshot. Die zurückgelieferte `tileset_url` ist direkt für `Cesium3DTileset.fromUrl(...)` verwendbar — kein weiterer API-Roundtrip nötig. ## Wann verwenden - Beim Initialisieren eines 3D-Viewers: welche Tilesets musst du laden? - Beim Wechsel der Karten-Region: nachladen, wenn neue BL ins Sichtfeld wandern. - Discovery-Endpoint für Cross-BL-Apps (mehrere Tilesets gleichzeitig laden). ## Examples ### curl ```bash curl -s 'https://api.lodapi.de/v1/tilesets?bbox=8.5,50.0,8.8,50.2' | jq '.tilesets[] | {bl: .bundesland_code, url: .tileset_url, count: .building_count}' ``` ### Cesium (TypeScript) ```ts const r = await fetch("https://api.lodapi.de/v1/tilesets?bbox=8.5,50.0,8.8,50.2"); const { tilesets } = await r.json(); for (const t of tilesets) { const ts = await Cesium.Cesium3DTileset.fromUrl(t.tileset_url); viewer.scene.primitives.add(ts); } ``` ### MapLibre + Deck.gl (TypeScript) ```ts import { Tiles3DLayer } from "@deck.gl/geo-layers"; const r = await fetch("https://api.lodapi.de/v1/tilesets?bbox=..."); const { tilesets } = await r.json(); const layers = tilesets.map(t => new Tiles3DLayer({ id: t.tileset_id, data: t.tileset_url })); ``` ## Parameters | Parameter | In | Type | Required | Beschreibung | |---|---|---|---|---| | `bbox` | query | string | yes | WGS84 `minLon,minLat,maxLon,maxLat` (Komma-separiert) | ## Response `200 OK · application/json` — Schema in [`openapi.json`](../openapi/openapi.json) (`#/components/schemas/TilesetListResponse`). ```json { "tilesets": [ { "tileset_id": "f4b...", "region_code": "he", "bundesland_code": "he", "snapshot_date": "2026-04-15", "s3_key": "s3://lodapi-tiles/he/2026-04-15/tileset.json", "tileset_url": "https://tiles.lodapi.de/he/2026-04-15/tileset.json", "tile_count": 21785, "total_bytes": 731000000, "building_count": 4934236, "generated_at": "2026-05-13T17:42:00Z", "bounding_volume": { "type": "Polygon", "coordinates": [...] } } ], "count": 1, "bbox": [8.5, 50.0, 8.8, 50.2], "lodapi": { "attribution": [{ "source": "HLBG", "license": "DL-DE/Zero 2.0", ... }] } } ``` ### Antwort-Header | Header | Wert | |---|---| | `Cache-Control` | `public, max-age=300` | ## Attribution `lodapi.attribution[]` enthält einen Eintrag pro zurückgegebenes BL. Frontends müssen den `source`-String anzeigen, wenn `license != DL-DE/Zero 2.0`. Siehe [Attribution-Guide](../guides/attribution.md). ## Stolperdrähte - **`bounding_volume`** ist GeoJSON-Polygon (kein 3D-Tiles-Bounding-Volume-Format). Nützlich für Map-Vorschauen, nicht für 3D-Frustum-Culling. - **Mehrere Tilesets pro BL**: möglich bei Mehrsnapshot-Coverage. Aktuell liefert die API nur den jüngsten Snapshot pro BL. - **bbox-Schneiden vs. -Enthalten**: ST_Intersects, nicht ST_Contains. Ein Tileset, das die bbox nur am Rand kratzt, ist enthalten. ## Verwandte Endpoints - [`GET /v1/tilesets/{tileset_id}`](./get-tileset.md) — Detail per ID. - [`GET /v1/tilesets/{tileset_id}/tileset.json`](./redirect-tileset-json.md) — direkt zum tileset.json. - [`GET /v1/buildings`](./list-buildings-bbox.md) — föderierte Feature-Query als Alternative. --- ### GET /v1/tilesets/{tileset_id}/tileset.json - **operation_id**: `redirect_tileset_json` - **stability**: `stable` - **since_version**: `0.1.0` - **auth**: `none` - **data_product**: `buildings` - **snapshot_aware**: `True` - **attribution_block**: `False` - **rate_limit_tier**: `public` # `GET /v1/tilesets/{tileset_id}/tileset.json` — 302-Redirect zur CDN Antwortet mit `302 Found` und `Location: https://tiles.lodapi.de///tileset.json`. Damit funktioniert die Lodapi-API als **stabiler Einstiegspunkt** für 3D-Tiles-Renderer, auch wenn das CDN-Storage-Layout sich später ändert. Cesium, three.js (`3d-tiles-renderer`) und Deck.gl folgen dem Redirect transparent — kein Code-Anpassen nötig. ## Wann verwenden - Bookmark-stabile URL für ein Tileset (öffentliche Demo-Links, Embedded-Maps). - Cross-CDN-Migration ohne Client-Update. Bei direktem Performance-Bedarf den Endpoint **nicht** verwenden — der Redirect ist ein extra HTTP-Roundtrip. Stattdessen die `tileset_url` aus `/v1/tilesets` direkt nutzen. ## Examples ### curl ```bash curl -sIL https://api.lodapi.de/v1/tilesets/f4b1c0d2.../tileset.json | grep -i '^location:' # Location: https://tiles.lodapi.de/he/2026-04-15/tileset.json ``` ### Cesium ```ts // Funktioniert — Cesium folgt 302 transparent. const ts = await Cesium.Cesium3DTileset.fromUrl( "https://api.lodapi.de/v1/tilesets/f4b1c0d2-3e5a-4d8b-9f01-2a3b4c5d6e7f/tileset.json" ); viewer.scene.primitives.add(ts); ``` ## Parameters | Parameter | In | Type | Required | Beschreibung | |---|---|---|---|---| | `tileset_id` | path | string (UUID) | yes | Stabile Tileset-ID | ## Response `302 Found` mit `Location`-Header auf die `tileset.json`-URL. ## Fehler | Status | Bedingung | |---|---| | `404` | `tileset_id` unbekannt | ## Verwandte Endpoints - [`GET /v1/tilesets/{tileset_id}`](./get-tileset.md) — Metadaten-Variante. - [`GET /v1/tilesets`](./list-tilesets-bbox.md) — Discovery. --- ## Guides # Attribution & Lizenz-Compliance Open Data heißt nicht „ohne Pflichten". Die Daten hinter Lodapi sind frei nutzbar — aber je nach Lizenz musst du die Quelle sichtbar nennen. Lodapi nimmt dir die 16 Behörden-Lizenzdialekte ab und normalisiert sie auf drei Familien. Die **Anzeigepflicht im Frontend** bleibt aber bei dir, dem Konsumenten. ## Warum überhaupt In Deutschland kommen die LoD2- und DGM1-Daten unter drei Lizenzfamilien: | Lizenz | Attributionspflicht | |---|---| | **DL-DE/Zero 2.0** | **Keine** — nutzbar ohne Quellenangabe (Nennung trotzdem empfohlen). | | **DL-DE BY 2.0** | **Ja** — Quellenangabe Pflicht. | | **CC BY 4.0** | **Ja** — Quellenangabe Pflicht. | Welche Familie für ein Bundesland gilt, entscheidet die jeweilige Geobasis-Stelle, nicht Lodapi. Du musst das nicht pro Behörde nachschlagen — die API sagt es dir pro Antwort. ## Der `lodapi.attribution[]`-Block Jede attributionsrelevante Response trägt top-level ein `lodapi`-Objekt mit `attribution[]`. Betroffen sind u.a. `/v1/buildings`, `/v1/tilesets` und `/v1/buildings/{gmlid}/roof`. Jeder Array-Eintrag steht für ein Bundesland, das in der Antwort vorkommt: ```json { "lodapi": { "attribution": [ { "source": "HLBG", "license": "DL-DE/Zero 2.0", "url": "https://www.govdata.de/dl-de/zero-2-0", "tiles_count": 1 } ] } } ``` Felder: | Feld | Bedeutung | |---|---| | `source` | Behörden-/Geobasis-Stelle (z.B. `"HLBG"`, `"BezReg Köln"`). Das ist der String, den du anzeigst. | | `license` | Klartext der Lizenz: `DL-DE/Zero 2.0`, `DL-DE BY 2.0` oder `CC BY 4.0`. | | `url` | Lizenz-URL zum Verlinken im UI. | | `tiles_count` | Anzahl beteiligter Tiles (bei `/v1/tilesets` aussagekräftig; bei `/v1/buildings` immer `1`). | ## Die Pflicht-Regel > Ist `license != "DL-DE/Zero 2.0"`, **MUSS** der `source`-String sichtbar im Frontend stehen — typischerweise im Map-Footer oder als Credit-Zeile. Bei `DL-DE/Zero 2.0` ist die Nennung **optional**, aber empfohlen. Die einfachste sichere Strategie: immer alle `source`-Strings anzeigen, dann liegst du bei jeder Lizenz richtig. ## Code-Snippets ### 1. Generisch (Vanilla JS) — Footer rendern ```js const res = await fetch("https://api.lodapi.de/v1/buildings?bbox=8.66,50.108,8.665,50.111"); const data = await res.json(); const attrs = data.lodapi?.attribution ?? []; // Pflicht: alles außer DL-DE/Zero anzeigen. Hier zeigen wir der Einfachheit halber alles. const credits = attrs.map(a => `© ${a.source} (${a.license})` ); document.getElementById("map-credit").innerHTML = "Daten: " + credits.join(" · "); ``` ### 2. CesiumJS — Credits in der Toolbar ```ts const data = await (await fetch( `https://api.lodapi.de/v1/tilesets?bbox=${bbox}` )).json(); for (const a of data.lodapi.attribution) { viewer.scene.creditDisplay.addStaticCredit( new Cesium.Credit(`${a.source} (${a.license})`, true) ); } ``` ### 3. Statischer Footer-String-Builder ```js function buildAttributionLine(attribution) { // Dedupe nach source (mehrere Tiles derselben Behörde). const seen = new Set(); const parts = []; for (const a of attribution) { if (seen.has(a.source)) continue; seen.add(a.source); parts.push(`© ${a.source} (${a.license})`); } return "Daten: " + parts.join(" · "); } // → "Daten: © HLBG (DL-DE/Zero 2.0) · © BezReg Köln (DL-DE BY 2.0)" ``` ## Terrain: Attribution als String Die Terrain-Endpoints `/v1/terrain/elevation` und `/v1/terrain/profile` liefern ein **einzelnes** Feld `attribution` als **String** (nicht als Array), weil eine Höhenkote bzw. ein Profil aus genau einer Quelle stammt: ```json { "attribution": "© SenStadt Berlin (DL-DE/Zero 2.0)" } ``` Den String kannst du direkt anzeigen — er enthält Quelle und Lizenz bereits formatiert. ## Bezug - [Quickstart](./quickstart.md) — Erste Calls, inklusive Lizenz-Hinweis. - [Lodapi mit CesiumJS](./using-with-cesium.md) — Credit-Display in der Praxis. - Endpoint-Details: `/v1/buildings`, `/v1/tilesets` und `/v1/buildings/{gmlid}/roof` in der [API-Referenz](/docs/endpoints). --- # Quickstart Lodapi ist auth-frei in Phase 1 (öffentliche Demo). Du brauchst **nichts** außer `curl` (oder einen Browser) und optional einen 3D-Renderer für Schritt 5. ## 1. Welche Bundesländer sind live? ```bash curl -s https://api.lodapi.de/v1/datasets | jq '.datasets[] | {bl: .bundesland_code, snap: .snapshot_date, count: .building_count}' ``` Antwort: eine Zeile pro live-BL mit Snapshot-Datum + Building-Count + Lizenz. ## 2. Gebäude in einer bbox Frankfurt-Innenstadt (kleine bbox, ~351 Gebäude; limit begrenzt nur die zurückgelieferten Features): ```bash curl -s "https://api.lodapi.de/v1/buildings?bbox=8.66,50.108,8.665,50.111&limit=20" \ | jq '.count, .features[0].properties' ``` GeoJSON-FeatureCollection. Jede `feature.properties.bundesland` zeigt das Quell-Bundesland. ## 3. Höhenkote an einem Punkt ```bash curl -s 'https://api.lodapi.de/v1/terrain/elevation?lat=50.110&lon=8.662' | jq ``` Liefert Geländehöhe (DHHN2016, m über NHN) aus dem zuständigen DGM1-Tile. Beispiel: ~110 m für Frankfurt. ## 4. 3D-Tiles-Tilesets für eine Region ```bash curl -s "https://api.lodapi.de/v1/tilesets?bbox=8.5,50.0,8.8,50.2" \ | jq '.tilesets[] | {url: .tileset_url, count: .building_count}' ``` Die `tileset_url`-Werte zeigen auf 3D-Tiles-1.1-tileset.json-Files auf `tiles.lodapi.de`. Direkt mit Cesium konsumierbar: ```ts import * as Cesium from "cesium"; const viewer = new Cesium.Viewer("cesiumContainer"); const r = await fetch("https://api.lodapi.de/v1/tilesets?bbox=8.5,50.0,8.8,50.2"); const { tilesets } = await r.json(); for (const t of tilesets) { const tileset = await Cesium.Cesium3DTileset.fromUrl(t.tileset_url); viewer.scene.primitives.add(tileset); } ``` ## 5. Eine bbox als einzelnes GLB Brauchst du das ganze Frankfurt-Sub-Quadrat als **eine** GLB-Datei für Blender oder Three.js? ```bash curl -s "https://api.lodapi.de/v1/buildings/3d.glb?bbox=8.66,50.108,8.665,50.111&compression=draco" \ > frankfurt.glb ``` ~100 KB nach Draco-Kompression bei ~351 Gebäuden. Limits: bbox ≤ 1 km², max 20000 Gebäude. ## Lizenzpflicht beachten Jede Antwort enthält den `lodapi.attribution[]`-Block. Bei **DL-DE BY 2.0** oder **CC BY 4.0** (das ist die Mehrheit) musst du den Quell-String in deinem Frontend sichtbar machen. Details: [Attribution-Guide](./attribution.md). ## Nächste Schritte - [Vollständige API-Referenz](/docs/endpoints) — alle 13 Endpoints. - [Für KI-Agenten](./using-with-ai-agents.md) — wenn du Lodapi via Cursor / Claude Code anbindest. - [Cesium-Integration](./using-with-cesium.md) (in Bearbeitung) — komplette Demo-App. --- # Lodapi für KI-Coding-Agenten Wenn du Lodapi mit einem KI-Coding-Agent (Cursor, Claude Code, Aider, ChatGPT mit Tool-Use, OpenAI Codex) anbinden willst, sind drei Roh-Artefakte deine Eingangstür — kein HTML-Crawling nötig. ## 1. `llms.txt` — Index für Discovery ``` https://lodapi.de/llms.txt ``` Sektionierter Index nach [llmstxt.org](https://llmstxt.org). Enthält alle Endpoints gruppiert nach Tag, plus Links zu Guides und Reference-Material. Klein genug, um permanent im LLM-System-Prompt zu liegen. **Verwendung in Claude Code**: ```bash # Lade Lodapi-Kontext in eine neue Session curl -s https://lodapi.de/llms.txt | claude --append-system ``` ## 2. `llms-full.txt` — Volle Doku im Context-Window ``` https://lodapi.de/llms-full.txt ``` Maschinell konkatenierte Doku aller Endpoints + Guides. Drop diese Datei in den LLM-Context — der Agent hat vollen API-Kontext für einen Roundtrip. ```python # Beispiel: ChatGPT/Claude mit vollständigem Lodapi-Kontext import httpx, anthropic docs = httpx.get("https://lodapi.de/llms-full.txt").text client = anthropic.Anthropic() msg = client.messages.create( model="claude-opus-4-7", max_tokens=2048, system=f"Du kennst die Lodapi-API. Doku:\n\n{docs}", messages=[{"role": "user", "content": "Wie hole ich Gebäude für Frankfurt-Innenstadt?"}], ) ``` ## 3. `.well-known/lodapi.json` — Discovery-Manifest ``` https://lodapi.de/.well-known/lodapi.json ``` Maschinenlesbares JSON mit: - **`api.openapi_url`** — Live-OpenAPI-3.1 - **`api.openapi_snapshot_url`** — committed Snapshot - **`docs.url`** — Doku-Site - **`llms.index`** + **`llms.full`** — die zwei Files oben - **`data_products[]`** — alle 6 Datenprodukte mit Status (live / beta / planned) - **`auth`** — Modell + Headers + Status Das ist das Format, das AI-Tool-Use-Discovery erwartet (`name`, `description`, `homepage` Felder sind Standard-kompatibel). ## 4. Markdown-Roh-Konsum pro Endpoint Jede Endpoint-Page hat einen `.md`-Suffix für Roh-Markdown: | HTML-View | Markdown-View (für LLM) | |---|---| | `https://lodapi.de/docs/endpoints/list-datasets` | `https://lodapi.de/docs/endpoints/list-datasets.md` | | `https://lodapi.de/docs/endpoints/get-terrain-elevation` | `https://lodapi.de/docs/endpoints/get-terrain-elevation.md` | `Content-Type: text/markdown; charset=utf-8`. Frontmatter + Body verbatim. ## 5. OpenAPI 3.1 für SDK-Generierung ``` https://lodapi.de/openapi.json (committed Snapshot) https://api.lodapi.de/openapi.json (live) https://api.lodapi.de/docs (Swagger-UI) https://api.lodapi.de/redoc (ReDoc) ``` Standard-OpenAPI-3.1 — funktioniert mit jedem Generator (`openapi-typescript`, `openapi-python-client`, `swagger-codegen`). ## 6. MCP-Server (geplant Phase 2) Ein Model-Context-Protocol-Server unter `mcp.lodapi.de` ist für Q3 2026 in Planung. Dann werden alle Lodapi-Endpoints direkt als MCP-Tools nutzbar — Claude Desktop, Cursor und MCP-fähige IDEs binden Lodapi ohne Custom-Integration an. Status-Update: `data_products[].mcp_server` in `.well-known/lodapi.json` wird gesetzt, sobald live. ## Stolperdrähte für KI-Agenten - **Cursor-Pagination ist opak** — der `lodapi.next`-String darf nicht aufgebrochen werden. Der Agent sollte ihn als Token behandeln. - **`features[].properties.bundesland`** ist der unprefixte 2-Buchstaben-Code (`"he"`, `"nrw"`). Manche LLMs raten "Hessen" — kommunizieren Sie den Code-Stil explizit. - **bbox-Reihenfolge ist `minLon,minLat,maxLon,maxLat`** (West, Süd, Ost, Nord), nicht GeoJSON-Bbox-Konvention. - **`/v1/terrain/elevation`** liefert **404** für Wasserflächen, nicht `0.0`. Das ist Absicht — siehe [Endpoint-Doku](/docs/endpoints/get-terrain-elevation). - **Lizenz-Attribution** ist kein optional Step — bei DL-DE BY 2.0 oder CC BY 4.0 muss der Quell-String sichtbar bleiben. Der `lodapi.attribution[]`-Block der Response gibt den exakten Text. --- # Lodapi mit CesiumJS [CesiumJS](https://cesium.com/platform/cesiumjs/) ist der Mainstream-3D-Globe-Renderer für Web. Lodapi exponiert alle Buildings als 3D-Tiles-1.1-Tilesets, die Cesium nativ konsumieren kann. Das DGM1-Terrain ist ebenfalls als 3D-Tiles-Mesh verfügbar (Phase 2 beta). ## Minimal-Setup ```html
``` Speichere als `index.html`, öffne in einem Browser. Du hast LoD2-Frankfurt in einem laufenden Cesium-Viewer. ## Terrain dazu ```ts // Terrain-Mesh-Tilesets sind separate 3D-Tilesets, KEIN Cesium-Terrain-Provider. // Building-Z ist absolut aus dem CityGML — beide Layer liegen ohne Provider-Magic // koordinaten-korrekt übereinander. const r2 = await fetch("https://api.lodapi.de/v1/terrain-mesh/datasets"); const { datasets } = await r2.json(); for (const t of datasets) { const ts = await Cesium.Cesium3DTileset.fromUrl(t.tileset_url); viewer.scene.primitives.add(ts); } ``` ## Attribution einbinden Lodapi liefert pro Antwort einen `lodapi.attribution[]`-Block. Cesium zeigt Credits in seiner unteren Toolbar — füge die Lodapi-Quellen hinzu: ```ts const attrs = tilesets.length > 0 ? (await fetch( `https://api.lodapi.de/v1/tilesets?bbox=${bbox}` )).then(r => r.json()).then(d => d.lodapi.attribution) : []; for (const a of attrs) { viewer.scene.creditDisplay.addStaticCredit( new Cesium.Credit(`${a.source} (${a.license})`, true) ); } ``` ## Building anklicken → Detail-API Cesium-3D-Tilesets unterstützen Per-Building-Picking. Die `feature.gmlid` ist als Batch-Table-Property eingebettet und matched die Lodapi-API: ```ts viewer.screenSpaceEventHandler.setInputAction(async (movement) => { const f = viewer.scene.pick(movement.position); if (f instanceof Cesium.Cesium3DTileFeature) { const gmlid = f.getProperty("gmlid"); if (!gmlid) return; const detail = await fetch(`https://api.lodapi.de/v1/buildings/${gmlid}`).then(r => r.json()); console.log("Building:", detail.building_id, "BL:", detail.bundesland_code, "LoD:", detail.lod); } }, Cesium.ScreenSpaceEventType.LEFT_CLICK); ``` ## Performance-Tipps - **`maximumScreenSpaceError`** auf 8–16 setzen (Default 16). Niedriger = mehr Detail, mehr Memory. - **`cullWithChildrenBounds: true`** standardmäßig, lass das. - **`preferLeaves: false`** für initial-snap-in, danach `true` für hohes Detail. - **Mehrere BL parallel laden**: jede `tileset_url` als eigenes `Cesium3DTileset.fromUrl(...)` — keine Sammelladung nötig. ## Voller Demo-Code Lodapi-Repo hat einen fertigen Cesium-Viewer unter [`code/demo/api-tilesets/index.html`](../../../code/demo/api-tilesets/index.html) — der gleiche Code, aber mit BL-Switcher und Inspector-Panel. ## Stolperdrähte - **Ion-Key nicht zwingend**, aber ohne Imagery-Provider sieht der Globus leer aus. Setze `Cesium.Ion.defaultAccessToken = "..."` für hochauflösende Sat-Karten. - **CesiumJS ≥ 1.118** für 3D-Tiles-1.1-Support (Implicit Tiling). Lodapi-Tilesets sind 1.1. - **CORS**: `https://api.lodapi.de` erlaubt `*.lodapi.de` und `localhost:*`. Andere Origins brauchen einen Custom-Eintrag — Issue eröffnen. --- # Lodapi mit QGIS [QGIS](https://qgis.org/) ist die Standard-OSS-GIS-Suite. Lodapi liefert Geodaten in GIS-kompatiblen Formaten — GeoJSON für Features, GeoTIFF (COG) für Terrain. In QGIS gibt es **drei Wege**, Lodapi anzubinden. > Voraussetzung: QGIS **3.34** oder neuer (LTR). Für 3D-Tiles direkt brauchst du QGIS 3.34+ (3D-Map-Plugin). ## Pfad 1 — Buildings als GeoJSON-Layer (URL-Layer) Schnellster Pfad. Layer → **Add Vector Layer** → Protocol: HTTP(S) → URL: ``` https://api.lodapi.de/v1/buildings?bbox=8.66,50.108,8.665,50.111&limit=1000 ``` QGIS lädt die FeatureCollection und stellt die Gebäude als 2D-Polygone dar (Footprint-Projektion). ### Empfohlene Einstellungen - **Layer-CRS**: `EPSG:4326` (WGS84) — Lodapi liefert WGS84-Koordinaten direkt. - **Symbology**: Categorized by `surface_class` (709 Wall / 710 Ground / 712 Roof) für eine erste Sichtprüfung. - **Joinfähig**: `properties.gmlid` identifiziert eine Surface; `properties.building_id` gruppiert zusammengehörige Surfaces (pro `bundesland` eindeutig). ### Refresh-Strategie GeoJSON-Layer in QGIS ist nicht live — beim nächsten Map-Refresh **wird nicht** neu vom Server geladen. Workaround: - Layer löschen + neu hinzufügen für Updates, oder - Skript-basierter Bulk-Download (s.u.) mit `cursor`-Pagination. ## Pfad 2 — Bulk-Download per Skript Für ganze Städte oder mehrere bboxen einen Python-Snippet im **Python-Console** (QGIS → Plugins → Python Console): ```python import urllib.request, json def lodapi_buildings(bbox: str, max_features: int = 50000) -> list[dict]: features = [] cursor = None while True: url = f"https://api.lodapi.de/v1/buildings?bbox={bbox}&limit=1000" if cursor: url += f"&cursor={cursor}" with urllib.request.urlopen(url) as r: data = json.loads(r.read()) features.extend(data["features"]) cursor = data["lodapi"]["next"] if not cursor or len(features) >= max_features: break return features fc = {"type": "FeatureCollection", "features": lodapi_buildings("8.5,50.0,8.8,50.2")} # Als temporären Layer einhängen. from qgis.core import QgsVectorLayer, QgsProject import tempfile tmp = tempfile.NamedTemporaryFile(suffix=".geojson", delete=False) json.dump(fc, open(tmp.name, "w")) layer = QgsVectorLayer(tmp.name, "Lodapi Buildings", "ogr") QgsProject.instance().addMapLayer(layer) ``` ## Pfad 3 — Terrain als Raster-Layer (COG via /vsicurl/) Lodapi-COGs sind direkt von GDAL/QGIS via HTTP Range-Request konsumierbar — kein lokaler Download nötig. Layer → **Add Raster Layer** → Protocol → URL: ``` /vsicurl/https://tiles.lodapi.de/terrain/be/2025-12-18/33_388_5818.tif ``` QGIS streamt nur die sichtbaren Pixel — funktioniert auch über mobile Verbindungen. ### Geländehöhe an einem Punkt Mit dem **Identify Tool** kannst du einzelne Pixel abfragen. Für **viele Punkte gleichzeitig** über die API: ```python import urllib.request, json from qgis.core import QgsField, QgsPointXY # Ein Punkt-Layer mit Geometrie sei `layer`. for f in layer.getFeatures(): p: QgsPointXY = f.geometry().asPoint() r = urllib.request.urlopen( f"https://api.lodapi.de/v1/terrain/elevation?lat={p.y()}&lon={p.x()}" ) if r.status == 200: data = json.loads(r.read()) # ... `data["elevation_m"]` ins Attribut-Feld schreiben. ``` ## Pfad 4 — 3D-Map-View QGIS 3.34+ kann 3D-Tiles-1.1-Tilesets nativ anzeigen — allerdings noch experimentell. 1. Hauptmenü → **View → 3D Map View**. 2. Im 3D-Map-Layer-Panel: **Add Tiled Mesh Layer**. 3. URL aus `/v1/tilesets?bbox=…` als `tileset_url` einsetzen. > Alternativ: GLB-Export per `/v1/buildings/3d.glb` → in QGIS via `Add Mesh Layer` (3.36+ unterstützt GLB-Import experimentell). ## OGC-API-Plugin (Phase 2) Lodapi wird in Phase 2 [`ldproxy`](https://github.com/interactive-instruments/ldproxy) als zweite Layer aktivieren und unter `/ogcapi/*` einen **OGC API Features**-konformen Endpoint anbieten. QGIS hat dafür ein eingebautes Provider-Plugin (`OGC API - Features`) — sobald aktiv, kann Lodapi direkt als OGC-Source angebunden werden. Stand 2026-05-14: noch nicht live. Tracking-Issue in der Roadmap. ## Lizenzpflicht in QGIS-Projekten Wenn du Lodapi-Daten in einer Karte verwendest, muss die Lizenz-Attribution sichtbar sein. Hole sie aus dem `lodapi.attribution[]`-Block der API-Antwort und füge sie in der **Layout-Composer**-Map-Legende als Text-Element ein: ``` © HLBG (DL-DE/Zero 2.0) via Lodapi ``` Voller Lizenz-Text: siehe [Attribution-Guide](./attribution.md). ## Stolperdrähte - **GeoJSON-Layer-Memory**: QGIS lädt die volle FeatureCollection in den Speicher. Für ganze Städte (>100k Gebäude) ist der Bulk-Download-Pfad mit `.gpkg`-Konvertierung der saubere Weg — siehe Snippet oben. - **Z-Werte**: Lodapi-Gebäude haben absolute DHHN2016-Z-Koordinaten. QGIS-2D-Map verwirft Z. Im 3D-Map-View bleiben sie — Terrain als Basis aktivieren (Pfad 3). - **CRS-Detection**: QGIS rät manchmal falsch. Bei expliziten BL-Daten setze das Layer-CRS manuell — Lodapi liefert WGS84 (`EPSG:4326`) für GeoJSON, das jeweilige UTM-CRS für COG. --- # Lodapi mit Three.js [Three.js](https://threejs.org/) ist die meist-verwendete Web-3D-Library. Für Lodapi-Konsum gibt es zwei Pfade: 1. **Bbox als einzelnes GLB** (`/v1/buildings/3d.glb`) — drop-in mit `GLTFLoader`. Für statische bbox-Use-Cases (Konfiguratoren, Architektur-Visualizer). 2. **3D-Tiles-Streaming** über [`3d-tiles-renderer`](https://github.com/NASA-AMMOS/3DTilesRendererJS) — für große Flächen ohne Memory-Druck. ## Pfad 1 — Bbox-GLB ### Minimal-Setup ```html ``` Du hast einen rotierbaren Three.js-Viewer mit Frankfurt-Buildings (Wall grau, Roof ziegelrot dank `colorize_roofs=true`). ### Z-Modus wählen | Modus | Wann | |---|---| | `per_building` (Default) | Alle Gebäude stehen auf `y=0` — saubere Konfigurator-Sicht | | `bbox_min` | Bbox-Boden auf `y=0`, relative Höhen erhalten | | `absolute` | DHHN2016-NHN-Werte — für GIS mit Terrain-Kontext | ```ts const url = ".../3d.glb?bbox=...&z_base=bbox_min&compression=draco"; ``` ### Building anklicken ```ts import { Raycaster, Vector2 } from "three"; const ray = new Raycaster(); canvas.addEventListener("click", (e) => { const mouse = new Vector2( (e.clientX / window.innerWidth) * 2 - 1, -(e.clientY / window.innerHeight) * 2 + 1 ); ray.setFromCamera(mouse, cam); const hits = ray.intersectObjects(gltf.scene.children, true); if (hits.length > 0) console.log("Hit:", hits[0].object); }); ``` > ⚠️ Mit `merge_buildings=true` (Default) sind alle Gebäude in **einem** Mesh — du kannst per Raycast nicht zwischen Gebäuden unterscheiden. Wenn das wichtig ist: `merge_buildings=false` setzen. Konsequenz: drastisch mehr Draw-Calls. ## Pfad 2 — 3D-Tiles-Streaming Für große Flächen (mehrere km², ganze BL) ist GLB ungeeignet. Nutze NASA's [`3d-tiles-renderer`](https://github.com/NASA-AMMOS/3DTilesRendererJS): ```bash npm install 3d-tiles-renderer three ``` ```ts import { TilesRenderer } from "3d-tiles-renderer"; import * as THREE from "three"; const r = await fetch("https://api.lodapi.de/v1/tilesets?bbox=8.5,50.0,8.8,50.2"); const { tilesets } = await r.json(); for (const t of tilesets) { const tiles = new TilesRenderer(t.tileset_url); tiles.setCamera(cam); tiles.setResolutionFromRenderer(cam, renderer); scene.add(tiles.group); renderer.setAnimationLoop(() => { cam.updateMatrixWorld(); tiles.update(); renderer.render(scene, cam); }); } ``` ## Terrain-Mesh dazu ```ts const r = await fetch("https://api.lodapi.de/v1/terrain-mesh/datasets"); const { datasets } = await r.json(); const be = datasets.find(d => d.bundesland_code === "be"); const terrain = new TilesRenderer(be.tileset_url); terrain.setCamera(cam); terrain.setResolutionFromRenderer(cam, renderer); scene.add(terrain.group); ``` ## Stolperdrähte - **ECEF-Koordinaten** in 3D-Tiles-Tilesets — der `3d-tiles-renderer` macht den Cartesian-→-Lokal-Transform automatisch über die `tiles.group.matrix`. Wenn du die scene um die Tile-Group rotierst, geht das schief — rotiere stattdessen `tiles.group`. - **DRACO-Decoder-Pfad** muss erreichbar sein. CDN oben funktioniert offline-frei; für eigenes Hosting den Pfad anpassen. - **OrbitControls + große ECEF-Werte**: bei `z_base=absolute` sind Koordinaten in Millionen Metern — Near/Far-Plane entsprechend setzen (`cam.far = 1e7`), sonst Z-Fighting. ## Voller Demo-Code Lodapi-Repo: [`code/demo/three3dtiles/index.html`](../../../code/demo/three3dtiles/index.html) — komplette Three.js-Demo mit 3D-Tiles-Renderer. --- ## Reference # Bundesländer-Coverage Lodapi aggregiert 16 deutsche Bundesländer mit **drei Lizenz-Dialekten**. Die Quelle ist immer **offen** (DL-DE/Zero, CC BY 4.0 oder DL-DE BY 2.0) — keine kommerziellen Lizenzen, keine NDAs. Aktueller Coverage-Stand: dynamisch via [`/v1/datasets`](../endpoints/list-datasets.md). ## Lizenz-Dialekte | Lizenz | Attribution-Pflicht | Beispiel-BL | |---|---|---| | **DL-DE/Zero 2.0** | **Nein** (Empfehlung trotzdem) | NRW, BE, HE, BY, NI | | **CC BY 4.0** | **Ja** (Quellnennung + Lizenz-Link) | TH, BW | | **DL-DE BY 2.0** | **Ja** (Quellnennung) | BB, SN, MV, ST | Vollständige Lizenz-Strings stehen pro API-Antwort im `lodapi.attribution[]`-Block — siehe [Attribution-Guide](../guides/attribution.md). ## BL-Übersicht > **Diese Tabelle wird zur Build-Zeit aus den Live-APIs gepullt.** Authoritative Quelle: [`/v1/datasets`](../endpoints/list-datasets.md) + [`/v1/terrain/datasets`](../endpoints/list-terrain-datasets.md). Bei API-Outage zur Build-Zeit fällt der Build auf einen committed Snapshot zurück. ## Snapshot-Frequenz Pro BL unterschiedlich. Lodapi pflegt Snapshots im Rhythmus der Behörden-Veröffentlichung: | BL-Cluster | Update-Rhythmus | |---|---| | BY | wöchentlich (volle 16 BL automatisch parsbar) | | NRW, BE, HE | quartalsweise | | Mehrheit (NI/RP/HB/HH/...) | halbjährlich | | ST | halbjährlich (länger) | Die Snapshot-Pipeline ist semi-automatisch — Connector-Code in `code/pipelines//`, Refresh-Logik in `bin/e2e_smoke.py`. ## Bezug - [Attribution-Guide](../guides/attribution.md) — wie Lizenz-Strings im Frontend einzubinden sind. - [Compliance-Operation](../../../05_operations/compliance.md) — Default-Attribution-Policy + pro-BL-Overrides. - [Pipeline-Dokumentation](../../pipelines/) — Connector-Details pro BL. --- # API-Changelog Versions-Anzeige in `info.version` der [OpenAPI](../openapi/openapi.json). Phase-1-Pre-1.0-Konvention: minor-Bumps können bruchhaft sein, bis das `/v1/`-Vertrag mit 1.0.0 stabil wird. Ab 1.0.0: **strict semver**. Breaking changes nur in `/v2/`. ## 0.1.0 — 2026-05-14 **Erstes dokumentiertes Schema.** Stand der API am Tag der Doku-Architektur-Entscheidung (ADR-0013). ### Endpoints (13) - `GET /healthz` - `GET /v1/datasets` - `GET /v1/tilesets` - `GET /v1/tilesets/{tileset_id}` - `GET /v1/tilesets/{tileset_id}/tileset.json` - `GET /v1/buildings` - `GET /v1/buildings/{gmlid}` - `GET /v1/buildings/3d.glb` - `GET /v1/terrain/elevation` - `GET /v1/terrain/datasets` - `GET /v1/terrain/profile` - `GET /v1/terrain-mesh/datasets` - `POST /v1/admin/datasets` ### Stabilität - 12 Endpoints sind `stable`. - 1 Endpoint `beta` (`/v1/terrain-mesh/datasets` — Phase 2a Single-Level-Tilesets, Multi-Level-LoD-Pyramide in Phase 2b). ### Operation-IDs Alle Endpoints haben explizite `operation_id` (statt FastAPI-Default-Hashes) — matched 1:1 mit den Doku-MD-Filenamen. ## In Pipeline (nicht released) - **Auth-Layer (Phase 2, Q3 2026)** — `X-Lodapi-Key` Header + Zitadel-OIDC. Wird im `/openapi.json` als `securitySchemes` ergänzt; Phase-1-Public-Endpoints bleiben unauth. - **OGC-API-Features-Layer (Phase 2)** — `/ogcapi/*` über ldproxy-Sidecar. Eigene OpenAPI, eigener Spec-Output. - **Buildings + Terrain Coverage** — kontinuierliche BL-Erweiterung, kein API-Schema-Change. ## Versions-Strategie | Pre-1.0 (Phase 1) | Post-1.0 (Phase 2+) | |---|---| | Minor-Bumps können bruchhaft sein | Breaking changes nur im `/v2/`-Pfad | | Doku-Drift-Check verhindert versehentliche Breaking | Doku-Drift-Check + Schema-Test pro PR | | Pfad-Versionierung `/v1/` fest | `/v2/` parallel ab Phase-2-Start | ## Vor-`0.1.0`-Vergangenheit Die API existierte vor diesem Changelog informal — siehe Tagesprotokolle in `08_logs/daily/2026-04-*` und `2026-05-*`. Das Schema-Snapshot war bis 2026-05-14 nicht eingecheckt; rückwirkende Versionsangaben wären spekulativ. --- # CRS-Konventionen Lodapi nutzt drei CRS-Klassen, je nach Schicht: ## 1. API-Ausgabe — WGS84 (EPSG:4326) Alle Geo-Antworten der API sind in **WGS84-Längengrad/Breitengrad** (`EPSG:4326`, GeoJSON-Default). Reihenfolge: `[lon, lat]` (RFC 7946). | Endpoint | Output-CRS | |---|---| | `/v1/buildings*` | WGS84 (GeoJSON) | | `/v1/tilesets` `bounding_volume` | WGS84 (GeoJSON-Polygon) | | `/v1/terrain/elevation` | Input WGS84, Output ohne Geometrie | | `/v1/terrain/profile` | WGS84 (GeoJSON FeatureCollection) | ## 2. 3D-Tiles + Mesh-Tilesets — ECEF 3D-Tiles 1.1 verwendet **Earth-Centered Earth-Fixed (ECEF)**-Koordinaten (geozentrisches kartesisches System). Cesium, three.js (`3d-tiles-renderer`) und Blender-Addon transformieren das automatisch zur Anzeige. | Asset | CRS | |---|---| | `tileset.json` (Buildings) | ECEF | | Terrain-Mesh `tileset.json` | ECEF | | b3dm-Files (in beiden) | ECEF | ## 3. GLB-Export — ENU-Lokal `/v1/buildings/3d.glb` liefert **East-North-Up (ENU)**-Koordinaten relativ zum Bbox-Center. Origin-Metadaten kommen in den Response-Headern (`X-Lodapi-Anchor-Srid`, `X-Lodapi-Origin-EN`). ``` X-Lodapi-Anchor-Srid: 25832 X-Lodapi-Origin-EN: 478234.567,5556789.123 ``` ## 4. DB-intern — UTM32N / UTM33N Pro BL natives UTM (siehe [bundeslaender.md](./bundeslaender.md)). Die API transformiert dynamisch zu WGS84. | CRS | Verwendung | |---|---| | `EPSG:25832` (UTM32N + ETRS89) | Westliche BL — BW, BY, HB, HH, HE, NI, NW, RP, SL, SH | | `EPSG:25833` (UTM33N + ETRS89) | Östliche BL — BE, BB, MV, SN, ST, TH | ## 5. Vertikales Datum Terrain-Höhen sind **DHHN2016** (Höhen über NHN). Lodapi gibt das explizit in jeder Höhen-Antwort als `datum`-Feld zurück. Building-Z-Werte (LoD2) sind ebenfalls DHHN2016, kommen aus dem CityGML-Original ohne Transformation. ## Pitfalls ### Bbox-Reihenfolge Lodapi-API erwartet `bbox=minLon,minLat,maxLon,maxLat` (West-Süd-Ost-Nord) — die **OGC-Standard-Reihenfolge** (BBOX in WGS84 Lon/Lat-Order). Das ist die gleiche Reihenfolge wie GeoJSON-Bbox-Feld. Verwechsle das nicht mit: - WMS GetMap mit `SRS=EPSG:4326` (Lat,Lon-Order). - ISO-19107 Bbox (Lat,Lon). ### Mixed-CRS-Federation an BL-Grenzen Föderierte bbox-Queries (`/v1/buildings`) sammeln Geometrie aus **mehreren UTM-Zonen** auf einmal und transformieren alles zu WGS84. An den BL-Grenzen (z.B. Frankfurt/Oder zwischen BE/BB und benachbart) kann das zu sub-mm-Sprüngen führen — Lodapi mappt das **nicht** glatt. Für sub-mm-genaue Analysen das BL der Geometrie respektieren und im nativen UTM rechnen. ### Cesium-Building-Clamping Bei Mesh-Tilesets als 3D-Tilesets (nicht als Terrain-Provider) gibt es **kein automatisches Clamping** von Building-Geometrie. Lodapi-LoD2-Buildings haben absolute DHHN2016-Z aus dem CityGML — beide Layer liegen ohne Provider-Magic richtig übereinander. ## Bezug - [ADR-0002 — CRS-Strategie](../../adr/0002-crs-strategy.md) — vollständige Begründung. - [Datenmodell](./data-model.md) — DB-Indexierung pro CRS. --- # Datenmodell Lodapi hat zwei Schema-Layer: - **`lodapi_meta`** — Cross-BL Metadaten (Datasets, Tilesets, Terrain-Datasets, Lizenzen, Attribution). - **`bl_`** — Pro Bundesland ein Schema mit Postgres-Tabellen aus dem Import (LoD2-Slim-Variante + ggf. 3DCityDB-v5-Schema im Hintergrund). ## `lodapi_meta` — Plattform-Schicht ``` lodapi_meta.license (license_id PK, url, text, ...) lodapi_meta.attribution (attribution_id PK, bundesland_code, text, ...) lodapi_meta.dataset (dataset_id PK uuid, bundesland_code, snapshot_date, ...) lodapi_meta.tileset (tileset_id PK uuid, dataset_id FK, region_code, s3_key, bounding_volume, ...) lodapi_meta.terrain_dataset (terrain_dataset_id PK, bundesland_code, snapshot_date, source, format, ...) lodapi_meta.terrain_tile (tile_id PK, terrain_dataset_id FK, bbox, easting_km, northing_km, source_url, ...) lodapi_meta.terrain_mesh_dataset (mesh_dataset_id PK, bundesland_code, snapshot_date, tileset_s3_key, build_params jsonb, ...) ``` | API-Endpoint | Quelltabelle(n) | |---|---| | `/v1/datasets` | `dataset` JOIN `attribution` | | `/v1/tilesets` | `tileset` + spatial-index `bounding_volume` | | `/v1/tilesets/{id}` | `tileset` | | `/v1/terrain/datasets` | `terrain_dataset` (DISTINCT ON BL) | | `/v1/terrain/elevation` | `terrain_tile` (ST_Contains) → `/vsicurl/` | | `/v1/terrain/profile` | `terrain_tile` × N + batch-COG-sample | | `/v1/terrain-mesh/datasets` | `terrain_mesh_dataset` (DISTINCT ON BL) | | `/v1/admin/datasets` (POST) | UPSERT in `dataset` + `tileset` | ## `bl_` — Per-BL-Schicht Phase-1-Slim-Layout (Decision 2026-05-13, Surfaces-Pivot): ``` bl_.surfaces_slim (surface_id PK, building_id, gmlid, surface_class, geom MultiPolygonZ, ...) bl_.dataset_metadata (snapshot_date, building_count, ...) ``` `surface_class` ist die AdV-Klassifikation: - `709` = Wall (Fassade) - `710` = Ground (Bodenfläche) - `712` = Roof (Dach) | API-Endpoint | Quelltabelle | |---|---| | `/v1/buildings` (bbox) | `bl_.surfaces_slim` (DISTINCT building per bbox) | | `/v1/buildings/{gmlid}` | `bl_.surfaces_slim` GROUP BY building_id | | `/v1/buildings/3d.glb` | `bl_.surfaces_slim` mit class-Filter | ## Snapshot-Versionierung Jedes BL hat **ein** aktives Snapshot zu einem Zeitpunkt. Re-Imports überschreiben das Schema in einer Transaktion. `dataset.snapshot_date` ist die Identitätsachse. Cursor-Pagination in `/v1/buildings` ist **Federation-aware**: `:` als base64-encoded String. Behandle den Cursor als opak — `surface_id` ist nicht als stabile ID über Snapshots hinweg garantiert. ## Index-Strategie | Tabelle | Index | Zweck | |---|---|---| | `surfaces_slim` | GIST(geom) | Bbox-Queries | | `surfaces_slim` | btree(building_id) | GROUP BY für Detail-Endpoints | | `surfaces_slim` | btree(gmlid) | `/v1/buildings/{gmlid}` | | `terrain_tile` | GIST(bbox) | `/v1/terrain/elevation` ST_Contains | | `tileset` | GIST(bounding_volume) | `/v1/tilesets?bbox=` | ## CRS in der DB Per-BL unterschiedlich: | BL-Gruppe | Geometry-CRS | |---|---| | BW, BY, HB, HH, HE, NI, NW, RP, SL, SH | EPSG:25832 (UTM32N) | | BE, BB, MV, SN, ST, TH | EPSG:25833 (UTM33N) | Die API transformiert dynamisch zu WGS84 für GeoJSON-Output. COG-Sampling-Pfad nutzt das native Tile-CRS. ## Bezug - [ADR-0003 — CityDB v5 Schema Setup](../../adr/0003-citydb-v5-schema-setup.md) - [ADR-0006 — API-Layer](../../adr/0006-api-layer.md) - [ADR-0009 — Terrain-Layer](../../adr/0009-terrain-layer.md) ---