ZOOM_MAX || $x === false || $x === null || $x < 0 || $x >= $grenze || $y === false || $y === null || $y < 0 || $y >= $grenze) { http_response_code(400); exit; } $pfad = speicherOrt() . "/$z/$x/$y.png"; /* -------------------------------------------------------------- Ausliefern */ $vorhanden = is_file($pfad); $veraltet = $vorhanden && (time() - filemtime($pfad) > AUFFRISCHEN_NACH); if (!$vorhanden || $veraltet) { if (holeUndSpeichere($z, $x, $y, $pfad)) { $vorhanden = true; } } if ($vorhanden) { liefereDatei($pfad); } else { liefereErsatzkachel(); } exit; /* ------------------------------------------------------------- Bausteine -- */ /** * Der erste beschreibbare Ort aus SPEICHER_ORTE, einmal je Aufruf ermittelt. * Existiert keiner, wird der letzte angelegt - der liegt im Systemverzeichnis * für flüchtige Dateien und ist immer beschreibbar. */ function speicherOrt() { static $ort = null; if ($ort !== null) { return $ort; } foreach (SPEICHER_ORTE as $kandidat) { if (is_dir($kandidat) && is_writable($kandidat)) { return $ort = $kandidat; } } $ort = SPEICHER_ORTE[count(SPEICHER_ORTE) - 1]; if (!is_dir($ort)) { @mkdir($ort, 0755, true); } return $ort; } /** * Eine Kachel bei der Quelle holen und ablegen. * Gibt true zurück, wenn danach eine brauchbare Datei vorliegt. */ function holeUndSpeichere($z, $x, $y, $pfad) { if (!bremseErlaubt()) { return is_file($pfad); // notfalls die veraltete Kachel } $url = str_replace(["{z}", "{x}", "{y}"], [$z, $x, $y], KACHEL_QUELLE); $ch = curl_init($url); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_USERAGENT => KENNUNG, CURLOPT_TIMEOUT => 8, CURLOPT_CONNECTTIMEOUT => 4, CURLOPT_FOLLOWLOCATION => true, CURLOPT_MAXREDIRS => 2, ]); $inhalt = curl_exec($ch); $status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE); curl_close($ch); // Auf den PNG-Kopf prüfen, statt nur auf den Statuscode: eine Fehlerseite // mit Status 200 wäre sonst als Kachel im Speicher gelandet und bliebe // dort für immer. if ($status !== 200 || $inhalt === false || strncmp($inhalt, "\x89PNG", 4) !== 0) { return is_file($pfad); } if (!is_dir(dirname($pfad)) && !mkdir(dirname($pfad), 0755, true)) { return is_file($pfad); } // Erst daneben schreiben, dann umbenennen. Sonst könnte ein zweiter // Aufruf eine halb geschriebene Datei ausliefern. $temp = $pfad . "." . getmypid() . ".teil"; if (file_put_contents($temp, $inhalt) === false) { return is_file($pfad); } if (!rename($temp, $pfad)) { unlink($temp); return is_file($pfad); } return true; } /** * Darf jetzt noch eine Kachel geholt werden? * * Gezählt wird in einer kleinen Datei, gegen gleichzeitige Zugriffe mit * flock abgesichert. Zwei Fenster: eines je Minute, eines je Stunde. Läuft * ein Fenster ab, fängt sein Zähler wieder bei null an. */ function bremseErlaubt() { $datei = speicherOrt() . "/.bremse.json"; $griff = fopen($datei, "c+"); if (!$griff) { return true; } if (!flock($griff, LOCK_EX)) { fclose($griff); return true; } $stand = json_decode(stream_get_contents($griff), true); if (!is_array($stand)) { $stand = ["minute" => 0, "min_n" => 0, "stunde" => 0, "std_n" => 0]; } $jetzt = time(); if ($jetzt - $stand["minute"] >= 60) { $stand["minute"] = $jetzt; $stand["min_n"] = 0; } if ($jetzt - $stand["stunde"] >= 3600) { $stand["stunde"] = $jetzt; $stand["std_n"] = 0; } $erlaubt = $stand["min_n"] < HOLEN_JE_MINUTE && $stand["std_n"] < HOLEN_JE_STUNDE; if ($erlaubt) { $stand["min_n"]++; $stand["std_n"]++; } ftruncate($griff, 0); rewind($griff); fwrite($griff, json_encode($stand)); flock($griff, LOCK_UN); fclose($griff); if (!$erlaubt) { error_log("tile.php: Bremse greift, keine weiteren Kacheln in diesem Fenster."); } return $erlaubt; } /** Eine gespeicherte Kachel ausliefern, mit Unterstützung für 304. */ function liefereDatei($pfad) { $zeit = filemtime($pfad); $marke = '"' . md5($pfad . $zeit) . '"'; header("Content-Type: image/png"); header("Cache-Control: public, max-age=" . BROWSER_FRIST); header("ETag: $marke"); header("Last-Modified: " . gmdate("D, d M Y H:i:s", $zeit) . " GMT"); $bekannt = isset($_SERVER["HTTP_IF_NONE_MATCH"]) ? trim($_SERVER["HTTP_IF_NONE_MATCH"]) : ""; if ($bekannt === $marke) { http_response_code(304); return; } header("Content-Length: " . filesize($pfad)); readfile($pfad); } /** * Graue Ersatzkachel, wenn nichts zu holen war - kein Internet, Quelle * gestört oder Bremse aktiv. Ein leeres Feld ist besser als ein kaputtes * Bildsymbol, und es wird bewusst nicht zwischengespeichert. */ function liefereErsatzkachel() { header("Content-Type: image/png"); header("Cache-Control: no-store"); // 1x1 Punkt in Grau, von Leaflet auf Kachelgröße gezogen. echo base64_decode( "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk" . "+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=="); }