161 lines
7.1 KiB
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);
|
|
}
|
|
}
|