['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); } }