Telegram Cron umgebaut
This commit is contained in:
@@ -0,0 +1,514 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Fachliche Hilfsfunktionen fuer T-CrB im Telegram-Cron.
|
||||
*
|
||||
* Diese Datei enthaelt Datenbankabfragen zu T-CrB-Beobachtungen, Aufbau der
|
||||
* Telegram-Nachricht, Diagramm-Erzeugung als SVG/PNG sowie kleine Farb- und
|
||||
* Formatierungshelfer, die speziell fuer den T-CrB-Versand gebraucht werden.
|
||||
*/
|
||||
|
||||
function fetchLatestTcrbObservationByFilter(PDO $pdo, string $filterCode): ?array
|
||||
{
|
||||
$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
|
||||
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(
|
||||
'<line x1="%1$.2f" y1="%2$.2f" x2="%3$.2f" y2="%2$.2f" stroke="#243445" stroke-width="1" stroke-dasharray="6 6" />',
|
||||
$paddingLeft,
|
||||
$y,
|
||||
$paddingLeft + $plotWidth
|
||||
);
|
||||
$gridLines[] = sprintf(
|
||||
'<text x="%1$.2f" y="%2$.2f" fill="#dbe7f2" font-size="20" text-anchor="end" dominant-baseline="middle">%3$s</text>',
|
||||
$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(
|
||||
'<line x1="%1$.2f" y1="%2$.2f" x2="%1$.2f" y2="%3$.2f" stroke="%6$s" stroke-width="2" opacity="0.9" />' .
|
||||
'<line x1="%4$.2f" y1="%2$.2f" x2="%5$.2f" y2="%2$.2f" stroke="%6$s" stroke-width="2" opacity="0.9" />' .
|
||||
'<line x1="%4$.2f" y1="%3$.2f" x2="%5$.2f" y2="%3$.2f" stroke="%6$s" stroke-width="2" opacity="0.9" />',
|
||||
$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(
|
||||
'<circle cx="%1$.2f" cy="%2$.2f" r="%3$.2f" fill="%4$s" stroke="%5$s" stroke-width="%6$.2f" />',
|
||||
$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'
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="%d" height="%d" viewBox="0 0 %d %d">
|
||||
<defs>
|
||||
<linearGradient id="bg" x1="0%%" y1="0%%" x2="0%%" y2="100%%">
|
||||
<stop offset="0%%" stop-color="#07111d"/>
|
||||
<stop offset="100%%" stop-color="#0f2133"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect width="%d" height="%d" fill="url(#bg)"/>
|
||||
<rect x="%d" y="%d" width="%d" height="%d" rx="16" fill="#11263b" stroke="#2f4d68" stroke-width="1.5"/>
|
||||
<text x="%d" y="44" fill="#f8fafc" font-size="30" font-weight="700">T CrB Lichtkurve - Filter V</text>
|
||||
<text x="%d" y="70" fill="#cbd5e1" font-size="19">letzte 2 Monate - Astronomiemuseum der Sternwarte Sonneberg</text>
|
||||
<text x="%d" y="%d" fill="#cbd5e1" font-size="18">%s</text>
|
||||
<text x="%d" y="%d" fill="#cbd5e1" font-size="18" text-anchor="end">%s</text>
|
||||
<text x="%d" y="48" fill="%s" font-size="22" text-anchor="end">Aktuell: %s%s</text>
|
||||
<text x="%d" y="74" fill="#cbd5e1" font-size="18" text-anchor="end">%s UTC</text>
|
||||
%s
|
||||
<line x1="%d" y1="%d" x2="%d" y2="%d" stroke="#88a4bf" stroke-width="1.6"/>
|
||||
<line x1="%d" y1="%d" x2="%d" y2="%d" stroke="#88a4bf" stroke-width="1.6"/>
|
||||
%s
|
||||
<path d="%s" fill="none" stroke="%s" stroke-width="3" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
%s
|
||||
</svg>
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user