prepare(" SELECT `id`, `observed_at_utc`, `jd`, `magnitude`, `magnitude_error`, `filter_code`, `observer_code` FROM `aavso_tcrb_observations` WHERE `filter_code` = :filter_code ORDER BY `observed_at_utc` DESC, `id` DESC LIMIT 1 "); $stmt->execute([':filter_code' => $filterCode]); $row = $stmt->fetch(); return is_array($row) ? $row : null; } function fetchRecentTcrbObservationsByFilterWindow(PDO $pdo, string $filterCode, string $windowSpec = '-2 months'): array { $windowStartUtc = (new DateTimeImmutable('now', new DateTimeZone('UTC'))) ->modify($windowSpec) ->format('Y-m-d H:i:s'); $stmt = $pdo->prepare(" SELECT `id`, `observed_at_utc`, `jd`, `magnitude`, `magnitude_error`, `filter_code`, `observer_code` FROM `aavso_tcrb_observations` WHERE `filter_code` = :filter_code AND `observed_at_utc` >= :window_start_utc ORDER BY `observed_at_utc` ASC, `id` ASC "); $stmt->execute([ ':filter_code' => $filterCode, ':window_start_utc' => $windowStartUtc, ]); $rows = $stmt->fetchAll(); if ($rows === []) { $fallbackStmt = $pdo->prepare(" SELECT `id`, `observed_at_utc`, `jd`, `magnitude`, `magnitude_error`, `filter_code`, `observer_code` FROM `aavso_tcrb_observations` WHERE `filter_code` = :filter_code ORDER BY `observed_at_utc` DESC, `id` DESC LIMIT 30 "); $fallbackStmt->execute([ ':filter_code' => $filterCode, ]); $rows = $fallbackStmt->fetchAll(); usort($rows, static function (array $a, array $b): int { return strcmp((string) ($a['observed_at_utc'] ?? ''), (string) ($b['observed_at_utc'] ?? '')); }); } return $rows; } function fetchTelegramRecipients(PDO $pdo): array { $stmt = $pdo->query(" SELECT `id`, `telegram_chat_id`, `telegram_username` FROM `app_users` WHERE `telegram_chat_id` IS NOT NULL AND `telegram_chat_id` <> '' AND `telegram_tcrb_v_enabled` = 1 AND `is_active` = 1 ORDER BY `id` ASC "); return $stmt->fetchAll(); } function buildTcrbLatestVMessage(array $observation, DateTimeImmutable $nowBerlin): string { $observedUtc = trim((string) ($observation['observed_at_utc'] ?? '')); $observedText = formatGermanDateTime($observedUtc); $magnitude = formatDecimal($observation['magnitude'] ?? null, 3); $magnitudeError = $observation['magnitude_error'] !== null ? ' ± ' . formatDecimal($observation['magnitude_error'], 3) : ''; $jd = formatDecimal($observation['jd'] ?? null, 6); $observerCode = trim((string) ($observation['observer_code'] ?? '')); $nextPossibleEruptionDate = calculateNextPossibleTcrbEruptionDate($nowBerlin); $lines = [ 'SkyViews TCrBotti mit dem Update', '', 'Magnitude: ' . $magnitude . $magnitudeError, 'Zeitpunkt: ' . $observedText, 'JD: ' . $jd, 'Nächster möglicher Ausbruch: ' . $nextPossibleEruptionDate, ]; return implode("\n", $lines); } function calculateNextPossibleTcrbEruptionDate(DateTimeImmutable $referenceDate): string { $eruptionIntervalDays = 227.5687; $lastEruptionUtc = new DateTimeImmutable('1946-02-09 00:00:00', new DateTimeZone('UTC')); $referenceUtc = $referenceDate->setTimezone(new DateTimeZone('UTC')); $secondsPerDay = 86400; $eruptionIntervalSeconds = (int) round($eruptionIntervalDays * $secondsPerDay); $elapsedSecondsSinceLastEruption = max(0, $referenceUtc->getTimestamp() - $lastEruptionUtc->getTimestamp()); $elapsedIntervals = (int) floor($elapsedSecondsSinceLastEruption / $eruptionIntervalSeconds); $nextPossibleEruptionUtc = $lastEruptionUtc->modify('+' . (($elapsedIntervals + 1) * $eruptionIntervalSeconds) . ' seconds'); return $nextPossibleEruptionUtc->format('d.m.Y'); } function formatGermanDateTime(?string $value): string { if ($value === null || trim($value) === '') { return 'unbekannt'; } try { $date = new DateTimeImmutable($value, new DateTimeZone('UTC')); } catch (Exception) { return $value; } return $date->format('d.m.Y H:i:s') . ' UTC'; } function buildTcrbVChartSvg(array $observations, array $latestObservation): string { $filterColors = loadFilterColors(); $vColor = (string) ($filterColors['V'] ?? '#92FF00'); $vStrokeColor = darkenHexColor($vColor, 0.35); $vHighlightColor = lightenHexColor($vColor, 0.35); $vHighlightStrokeColor = lightenHexColor($vColor, 0.6); $width = 1200; $height = 700; $paddingLeft = 90; $paddingRight = 40; $paddingTop = 80; $paddingBottom = 90; $plotWidth = $width - $paddingLeft - $paddingRight; $plotHeight = $height - $paddingTop - $paddingBottom; $plotBottomY = $paddingTop + $plotHeight; $plotRightX = $paddingLeft + $plotWidth; if (count($observations) < 2) { $observations = [$latestObservation]; } $timestamps = []; $magnitudes = []; foreach ($observations as $row) { $timestamps[] = strtotime((string) ($row['observed_at_utc'] ?? '')); $magnitudes[] = (float) ($row['magnitude'] ?? 0.0); } $timestamps = array_values(array_filter($timestamps, static fn ($value): bool => is_int($value) || is_float($value))); if ($timestamps === []) { $timestamps = [time() - 3600, time()]; } $minX = (float) min($timestamps); $maxX = (float) max($timestamps); if ($maxX <= $minX) { $maxX = $minX + 3600.0; } $minY = min($magnitudes); $maxY = max($magnitudes); if ($maxY <= $minY) { $maxY = $minY + 0.5; } $paddingMag = max(0.06, ($maxY - $minY) * 0.10); $axisMinY = $minY - $paddingMag; $axisMaxY = $maxY + $paddingMag; $scaleX = static function (float $timestamp) use ($minX, $maxX, $paddingLeft, $plotWidth): float { return $paddingLeft + (($timestamp - $minX) / ($maxX - $minX)) * $plotWidth; }; $scaleY = static function (float $magnitude) use ($axisMinY, $axisMaxY, $paddingTop, $plotHeight): float { $ratio = ($magnitude - $axisMinY) / ($axisMaxY - $axisMinY); return $paddingTop + $ratio * $plotHeight; }; $gridLines = []; for ($i = 0; $i <= 4; $i++) { $gridMag = $axisMinY + (($axisMaxY - $axisMinY) / 4) * $i; $y = $scaleY($gridMag); $gridLines[] = sprintf( '', $paddingLeft, $y, $paddingLeft + $plotWidth ); $gridLines[] = sprintf( '%3$s', $paddingLeft - 14, $y, htmlspecialchars(number_format($gridMag, 2, '.', ''), ENT_QUOTES, 'UTF-8') ); } $pathParts = []; $errorBars = []; $pointsSvg = []; foreach ($observations as $index => $row) { $timestamp = strtotime((string) ($row['observed_at_utc'] ?? '')); if ($timestamp === false) { continue; } $magnitude = (float) ($row['magnitude'] ?? 0.0); $x = $scaleX((float) $timestamp); $y = $scaleY($magnitude); $pathParts[] = sprintf('%s %.2f %.2f', $index === 0 ? 'M' : 'L', $x, $y); $error = isset($row['magnitude_error']) && $row['magnitude_error'] !== null ? (float) $row['magnitude_error'] : 0.0; if ($error > 0) { $yTop = $scaleY($magnitude - $error); $yBottom = $scaleY($magnitude + $error); $errorBars[] = sprintf( '' . '' . '', $x, $yTop, $yBottom, $x - 7, $x + 7, htmlspecialchars($vHighlightColor, ENT_QUOTES, 'UTF-8') ); } $isLatest = (string) ($row['observed_at_utc'] ?? '') === (string) ($latestObservation['observed_at_utc'] ?? '') && (string) ($row['jd'] ?? '') === (string) ($latestObservation['jd'] ?? ''); $pointsSvg[] = sprintf( '', $x, $y, $isLatest ? 7.0 : 4.2, $isLatest ? $vHighlightColor : $vColor, $isLatest ? $vHighlightStrokeColor : $vHighlightColor, $isLatest ? 2.6 : 1.4 ); } $firstLabel = date('d.m.Y H:i', (int) $minX) . ' UTC'; $lastLabel = date('d.m.Y H:i', (int) $maxX) . ' UTC'; $latestMagnitude = number_format((float) ($latestObservation['magnitude'] ?? 0.0), 3, '.', ''); $latestError = isset($latestObservation['magnitude_error']) && $latestObservation['magnitude_error'] !== null ? ' +/- ' . number_format((float) $latestObservation['magnitude_error'], 3, '.', '') : ''; $latestUtc = htmlspecialchars((string) ($latestObservation['observed_at_utc'] ?? ''), ENT_QUOTES, 'UTF-8'); $gridSvg = implode("\n ", $gridLines); $errorBarsSvg = implode("\n ", $errorBars); $pathSvg = implode(' ', $pathParts); $pointsSvgMarkup = implode("\n ", $pointsSvg); return sprintf( <<<'SVG' T CrB Lichtkurve - Filter V letzte 2 Monate - Astronomiemuseum der Sternwarte Sonneberg %s %s Aktuell: %s%s %s UTC %s %s %s SVG, $width, $height, $width, $height, $width, $height, $paddingLeft, $paddingTop, $plotWidth, $plotHeight, $paddingLeft, $paddingLeft, $paddingLeft, $height - 28, htmlspecialchars($firstLabel, ENT_QUOTES, 'UTF-8'), $width - $paddingRight, $height - 28, htmlspecialchars($lastLabel, ENT_QUOTES, 'UTF-8'), $width - $paddingRight, htmlspecialchars($vHighlightColor, ENT_QUOTES, 'UTF-8'), htmlspecialchars($latestMagnitude, ENT_QUOTES, 'UTF-8'), htmlspecialchars($latestError, ENT_QUOTES, 'UTF-8'), $width - $paddingRight, $latestUtc, $gridSvg, $paddingLeft, $paddingTop, $paddingLeft, $plotBottomY, $paddingLeft, $plotBottomY, $plotRightX, $plotBottomY, $errorBarsSvg, $pathSvg, htmlspecialchars($vStrokeColor, ENT_QUOTES, 'UTF-8'), $pointsSvgMarkup ); } function loadFilterColors(): array { static $colors = null; global $filterColorConfigPath; if (is_array($colors)) { return $colors; } $colors = []; if (!is_string($filterColorConfigPath) || !is_file($filterColorConfigPath)) { return $colors; } $raw = file_get_contents($filterColorConfigPath); $decoded = is_string($raw) ? json_decode($raw, true) : null; if (!is_array($decoded)) { return $colors; } foreach ($decoded as $filterCode => $config) { if (is_array($config) && !empty($config['color'])) { $colors[(string) $filterCode] = (string) $config['color']; } } return $colors; } function darkenHexColor(string $hexColor, float $amount): string { return adjustHexColor($hexColor, -abs($amount)); } function lightenHexColor(string $hexColor, float $amount): string { return adjustHexColor($hexColor, abs($amount)); } function adjustHexColor(string $hexColor, float $amount): string { $hex = ltrim(trim($hexColor), '#'); if (!preg_match('/^[0-9a-fA-F]{6}$/', $hex)) { return '#92FF00'; } $channels = [ hexdec(substr($hex, 0, 2)), hexdec(substr($hex, 2, 2)), hexdec(substr($hex, 4, 2)), ]; foreach ($channels as $index => $value) { if ($amount >= 0) { $channels[$index] = (int) round($value + ((255 - $value) * $amount)); } else { $channels[$index] = (int) round($value * (1 + $amount)); } $channels[$index] = max(0, min(255, $channels[$index])); } return sprintf('#%02X%02X%02X', $channels[0], $channels[1], $channels[2]); } function buildTcrbObservationSignature(array $observation): string { return implode('|', [ (string) ($observation['observed_at_utc'] ?? ''), (string) ($observation['jd'] ?? ''), (string) ($observation['magnitude'] ?? ''), (string) ($observation['magnitude_error'] ?? ''), (string) ($observation['filter_code'] ?? ''), (string) ($observation['observer_code'] ?? ''), ]); } function createTelegramChartPackage(string $svgContent): array { $directory = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'skyview_telegram_charts'; if (!is_dir($directory) && !mkdir($directory, 0775, true) && !is_dir($directory)) { throw new RuntimeException('Temp-Verzeichnis fuer Telegram-Diagramme konnte nicht erstellt werden.'); } $baseName = 'tcrb_v_latest_chart_' . date('Ymd_His') . '_' . bin2hex(random_bytes(4)); $svgPath = $directory . DIRECTORY_SEPARATOR . $baseName . '.svg'; if (@file_put_contents($svgPath, $svgContent) === false) { throw new RuntimeException('Diagrammdatei konnte nicht geschrieben werden: ' . $svgPath); } $pngPath = $directory . DIRECTORY_SEPARATOR . $baseName . '.png'; if (convertSvgToPng($svgPath, $pngPath)) { return [ 'kind' => 'photo', 'path' => $pngPath, 'cleanup' => [$svgPath, $pngPath], ]; } return [ 'kind' => 'document', 'path' => $svgPath, 'cleanup' => [$svgPath], ]; } function cleanupTelegramChartPackage(array $chartPackage): void { $files = is_array($chartPackage['cleanup'] ?? null) ? $chartPackage['cleanup'] : []; foreach ($files as $file) { if (is_string($file) && $file !== '') { @unlink($file); } } } function convertSvgToPng(string $svgPath, string $pngPath): bool { if (!is_file($svgPath)) { return false; } $commands = [ sprintf('rsvg-convert -o %s %s', escapeshellarg($pngPath), escapeshellarg($svgPath)), sprintf('magick %s %s', escapeshellarg($svgPath), escapeshellarg($pngPath)), sprintf('convert %s %s', escapeshellarg($svgPath), escapeshellarg($pngPath)), ]; foreach ($commands as $command) { if (!function_exists('shell_exec')) { continue; } @shell_exec($command . ' 2>&1'); if (is_file($pngPath) && filesize($pngPath) > 0) { return true; } } return false; }