Files
skyview.astronomiemuseum.de/public/observation_suggestions_helpers.php

161 lines
7.1 KiB
PHP

<?php
declare(strict_types=1);
/** Catalog values are server-owned; the client only supplies selection filters. */
function observationDsoImage(array $row): ?string
{
$catalogs = [
['messier', 'messier', 'm'],
['ngc', 'ngc', 'ngc'],
['ic', 'ic', 'ic'],
];
foreach ($catalogs as [$field, $folder, $prefix]) {
$number = preg_replace('/\D+/', '', trim((string) ($row[$field] ?? '')));
if ($number === '' || !ctype_digit($number)) {
continue;
}
$relative = 'images/dso/' . $folder . '/' . $prefix . $number . '.png';
if (is_file(__DIR__ . '/' . $relative)) {
return $relative;
}
}
return null;
}
function observationCatalog(PDO $pdo, array $types): array
{
$mapping = [
'open_clusters' => ['OCl'], 'globular_clusters' => ['GCl'],
'galaxies' => ['G', 'GPair', 'GTrpl', 'GGroup'],
'nebulae' => ['Neb', 'EmN', 'HII', 'RfN', 'SNR', 'Cl+N'],
'planetary_nebulae' => ['PN'], 'double_stars' => ['**'],
];
$codes = [];
foreach ($mapping as $group => $groupCodes) {
if (in_array($group, $types, true)) {
foreach ($groupCodes as $code) {
$codes[$code] = $group;
}
}
}
$objects = [];
if ($codes !== []) {
$placeholders = implode(',', array_fill(0, count($codes), '?'));
$stmt = $pdo->prepare("SELECT id, name, messier, ngc, ic, common_names_german, common_names, object_type_abrev, ra_deg, dec_deg, major_axis, v_mag
FROM dso_objects WHERE object_type_abrev IN ($placeholders)
AND ra_deg IS NOT NULL AND dec_deg IS NOT NULL");
$stmt->execute(array_keys($codes));
foreach ($stmt as $row) {
$messier = trim((string) ($row['messier'] ?? ''));
$messierLabel = preg_replace('/^M\s*/i', 'M', $messier);
if ($messierLabel !== '' && !preg_match('/^M/i', $messierLabel)) {
$messierLabel = 'M' . $messierLabel;
}
$commonName = trim((string) ($row['common_names_german'] ?? ''));
if ($commonName === '') {
$commonName = trim((string) ($row['common_names'] ?? ''));
}
$displayName = $row['name'];
if ($messierLabel !== '') {
$displayName .= ' / ' . $messierLabel;
}
$objects[] = [
'id' => 'dso-' . $row['id'],
'name' => $displayName,
'common_name' => $commonName,
'group' => $codes[$row['object_type_abrev']],
'image' => observationDsoImage($row),
'ra_deg' => (float) $row['ra_deg'], 'dec_deg' => (float) $row['dec_deg'],
// OpenNGC MajAx is in arcminutes; GUI and Python use arcseconds.
'size_arcsec' => $row['major_axis'] !== null && (float) $row['major_axis'] > 0
? (float) $row['major_axis'] * 60 : null,
'magnitude' => $row['v_mag'] !== null ? (float) $row['v_mag'] : null,
];
}
}
if (in_array('variable_stars', $types, true)) {
$stmt = $pdo->query("SELECT id, hip, proper, var, con, ra, `dec`, mag FROM star_hipparcos
WHERE var IS NOT NULL AND TRIM(var) <> '' AND ra IS NOT NULL AND `dec` IS NOT NULL");
foreach ($stmt as $row) {
$name = trim((string) ($row['proper'] ?? ''));
$objects[] = [
'id' => 'star-' . $row['id'], 'name' => $name !== '' ? $name : trim($row['var'] . ' ' . ($row['con'] ?? '')),
'group' => 'variable_stars', 'ra_deg' => (float) $row['ra'] * 15,
'dec_deg' => (float) $row['dec'], 'size_arcsec' => null,
'magnitude' => $row['mag'] !== null ? (float) $row['mag'] : null,
'note' => 'HIP ' . $row['hip'] . ' · Keine Minima-/Maxima-Vorhersage',
];
}
}
if (in_array('comets', $types, true)) {
$stmt = $pdo->query('SELECT id, designation_and_name, year_of_perihelion, month_of_perihelion,
day_of_perihelion, perihelion_dist_au, eccentricity, arg_perihelion_deg, ascending_node_deg,
inclination_deg, absolute_magnitude_h, slope_parameter_g, epoch_date FROM comets_mpc');
foreach ($stmt as $row) {
$objects[] = ['id' => 'comet-' . $row['id'], 'name' => $row['designation_and_name'],
'group' => 'comets', 'size_arcsec' => null, 'orbit' => $row,
'note' => 'Bahnepoche: ' . ($row['epoch_date'] ?? 'unbekannt')];
}
}
return $objects;
}
function runObservationSuggestions(array $payload): array
{
$file = tempnam(sys_get_temp_dir(), 'skyview_observation_');
if ($file === false) {
throw new RuntimeException('Temporäre Datei konnte nicht angelegt werden.');
}
try {
if (file_put_contents($file, json_encode($payload, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE)) === false) {
throw new RuntimeException('Berechnungsdaten konnten nicht geschrieben werden.');
}
$candidates = PHP_OS_FAMILY === 'Windows' ? ['python', 'py'] : ['/usr/bin/python3', 'python3', 'python'];
foreach ($candidates as $binary) {
// Array command bypasses the shell; no user input is interpolated into a command.
$process = @proc_open([$binary, __DIR__ . '/py/observation_suggestions_api.py', $file],
[0 => ['pipe', 'r'], 1 => ['pipe', 'w'], 2 => ['pipe', 'w']], $pipes);
if (!is_resource($process)) {
continue;
}
fclose($pipes[0]);
stream_set_blocking($pipes[1], false);
stream_set_blocking($pipes[2], false);
$output = '';
$errors = '';
$deadline = microtime(true) + 90;
$timedOut = false;
do {
$output .= stream_get_contents($pipes[1]);
$errors .= stream_get_contents($pipes[2]);
$status = proc_get_status($process);
if (!$status['running']) {
break;
}
if (microtime(true) > $deadline) {
$timedOut = true;
proc_terminate($process);
break;
}
usleep(20000);
} while (true);
$output .= stream_get_contents($pipes[1]);
$errors .= stream_get_contents($pipes[2]);
fclose($pipes[1]);
fclose($pipes[2]);
proc_close($process);
if ($timedOut) {
throw new RuntimeException('Die Berechnung dauert zu lange. Bitte weniger Objekttypen auswählen.');
}
$decoded = json_decode($output, true);
if (is_array($decoded) && array_key_exists('ok', $decoded)) {
return $decoded;
}
error_log('Observation suggestions Python: ' . substr($errors, 0, 2000));
}
throw new RuntimeException('Die Berechnung ist derzeit nicht verfügbar. Bitte die Python-Installation prüfen.');
} finally {
@unlink($file);
}
}