Files
Adminator fc8996f064 Atlas: DSGVO-Tile-Proxy + Logistik-Phase-bis-8 + Sounds-Anleitung
Platform:
- OSM-DE als dritter Tile-Proxy-Provider (deutsche Beschriftungen)
- logistik.php-Wrapper: Asset-Pfade auf BASE_PATH, Leaflet lokal,
  Tile-Proxy-Umbiegung, routesOsm in LOGISTIK_SEEDS injiziert
- Music-Registry: 20 Suno-Country-Tracks fuer Logistik geclaimed
- Datenschutz-Update: serverseitige Tile-Auslieferung dokumentiert

Logistik (Instanz-Lieferungen, parallel):
- Phase 0 bis 8 inkl. Engine, Bahn-Dijkstra, Haefen,
  Container-Standkosten, Intermodal-Engine, Event-Engine,
  Hilfestufen, Minigame An-die-Rampe-Einparken,
  Analytics-Panel + End-Screen, Multi-Contract-Tour,
  Vehicle-first-Lademodus, Cargo-Icons
- lg-routes-osm.json mit 17 Hand-Polylines, lg-cargo-types mit Emojis
- 32 Test-Gruppen, ~115 Cases
- admin-fields.json fuer Admin-UI-Integration

Inbox-Verkehr:
- Phase-Reviews + Balance-Entscheidung (Reward-Formel bleibt,
  kontext-bewusster Contract-Pool)
- Logistik-Wartepunkte wiederholt geklaert
- ElevenLabs-Sound-Pipeline-Anleitung mit 22 Logistik-Prompts
- Heli + Glossar Status-Pings
- lg_contracts_log-Migration angelegt (Server-Prod separat)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 08:15:00 +02:00

157 lines
5.6 KiB
PHP

<?php
/**
* Tile-Proxy mit Disk-Cache — DSGVO-sicherer Ersatz für direkte
* Browser-Requests an OSM/CARTO-Tile-Server.
*
* Flow:
* Browser → tile-proxy.php (euer Server) → [Cache-Hit? ja: raus]
* → [Cache-Miss? Upstream holen, cachen, raus]
*
* Aufruf: /tile-proxy?z={z}&x={x}&y={y}&p={osm|carto}
*
* Konfiguration über .env:
* TILE_CACHE_DIR=/absoluter/pfad/zum/cache/ (optional;
* Default: APP_ROOT/data/tiles/)
*/
require_once __DIR__ . '/config/app.php';
// ============================================================
// Whitelist — was darf rein, was raus
// ============================================================
const PROVIDERS = [
'osm' => [
'label' => 'OpenStreetMap',
'url_pattern' => 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
'max_zoom' => 18,
],
'osm-de' => [
'label' => 'OpenStreetMap Deutsch (FOSSGIS e.V.)',
'url_pattern' => 'https://tile.openstreetmap.de/{z}/{x}/{y}.png',
'max_zoom' => 18,
],
'carto' => [
'label' => 'CARTO Positron',
'url_pattern' => 'https://cartodb-basemaps-a.global.ssl.fastly.net/light_all/{z}/{x}/{y}.png',
'max_zoom' => 18,
],
];
const MIN_ZOOM = 3;
const MAX_ZOOM = 18;
const CACHE_TTL_DAYS = 30; // Browser-Cache-Max-Age
const UPSTREAM_TIMEOUT = 10; // Sekunden
// ============================================================
// Parameter-Validierung
// ============================================================
$z = filter_input(INPUT_GET, 'z', FILTER_VALIDATE_INT, ['options' => ['min_range' => MIN_ZOOM, 'max_range' => MAX_ZOOM]]);
$x = filter_input(INPUT_GET, 'x', FILTER_VALIDATE_INT, ['options' => ['min_range' => 0, 'max_range' => 1 << MAX_ZOOM]]);
$y = filter_input(INPUT_GET, 'y', FILTER_VALIDATE_INT, ['options' => ['min_range' => 0, 'max_range' => 1 << MAX_ZOOM]]);
$p = filter_input(INPUT_GET, 'p', FILTER_CALLBACK, ['options' => fn($v) => is_string($v) && preg_match('/^[a-z][a-z0-9-]{0,15}$/', $v) ? $v : false]);
if ($z === null || $z === false || $x === false || $y === false || !$p || !isset(PROVIDERS[$p])) {
http_response_code(400);
header('Content-Type: text/plain; charset=utf-8');
exit("Ungültige Parameter. Erwartet: ?z=INT&x=INT&y=INT&p=osm|carto\n");
}
$xMax = (1 << $z) - 1;
if ($x < 0 || $x > $xMax || $y < 0 || $y > $xMax) {
http_response_code(400);
exit("Tile-Koordinaten außerhalb des Zoom-Bereichs.\n");
}
$providerConf = PROVIDERS[$p];
if ($z > $providerConf['max_zoom']) {
http_response_code(400);
exit("Zoom-Level {$z} wird von Provider {$p} nicht unterstützt.\n");
}
// ============================================================
// Cache-Pfad bestimmen
// ============================================================
$cacheDir = defined('TILE_CACHE_DIR')
? rtrim(TILE_CACHE_DIR, '/\\')
: rtrim(APP_ROOT, '/\\') . '/data/tiles';
$tileDir = $cacheDir . "/$p/$z/$x";
$tilePath = $tileDir . "/$y.png";
// ============================================================
// Cache-Hit: sofort ausliefern
// ============================================================
if (is_file($tilePath) && filesize($tilePath) > 0) {
_serveTile($tilePath);
exit;
}
// ============================================================
// Cache-Miss: upstream holen
// ============================================================
$upstreamUrl = strtr($providerConf['url_pattern'], [
'{z}' => $z, '{x}' => $x, '{y}' => $y,
]);
$ctx = stream_context_create([
'http' => [
'method' => 'GET',
'timeout' => UPSTREAM_TIMEOUT,
'user_agent' => 'GeoGraSim/1.0 (+https://geograsim.at; educational tile cache)',
'header' => "Accept: image/png,image/*\r\n",
],
'ssl' => [
'verify_peer' => true,
'verify_peer_name' => true,
],
]);
$data = @file_get_contents($upstreamUrl, false, $ctx);
if ($data === false || strlen($data) < 100) {
http_response_code(502);
header('Content-Type: text/plain; charset=utf-8');
exit("Upstream nicht erreichbar: {$providerConf['label']}\n");
}
// ============================================================
// In Cache schreiben (best-effort, Fehler ignorieren)
// ============================================================
if (!is_dir($tileDir)) {
@mkdir($tileDir, 0775, true);
}
if (is_dir($tileDir) && is_writable($tileDir)) {
@file_put_contents($tilePath, $data, LOCK_EX);
}
// ============================================================
// An Browser ausliefern
// ============================================================
header('Content-Type: image/png');
header('Cache-Control: public, max-age=' . (CACHE_TTL_DAYS * 86400) . ', immutable');
header('X-Tile-Cache: MISS');
echo $data;
// ============================================================
// Helper: Tile aus Cache servieren
// ============================================================
function _serveTile(string $path): void {
$etag = '"' . md5_file($path) . '"';
$mtime = filemtime($path);
$lastMod = gmdate('D, d M Y H:i:s', $mtime) . ' GMT';
header('Content-Type: image/png');
header('Cache-Control: public, max-age=' . (CACHE_TTL_DAYS * 86400) . ', immutable');
header('ETag: ' . $etag);
header('Last-Modified: ' . $lastMod);
header('X-Tile-Cache: HIT');
$ifNoneMatch = $_SERVER['HTTP_IF_NONE_MATCH'] ?? '';
$ifModSince = $_SERVER['HTTP_IF_MODIFIED_SINCE'] ?? '';
if ($ifNoneMatch === $etag || ($ifModSince && strtotime($ifModSince) >= $mtime)) {
http_response_code(304);
return;
}
header('Content-Length: ' . filesize($path));
readfile($path);
}