MPC Kometen Import verbessert

This commit is contained in:
Eskimue
2026-04-10 14:45:02 +02:00
parent 25e3542d3c
commit 92c45a60af
3 changed files with 518 additions and 217 deletions
+1 -1
View File
@@ -299,7 +299,7 @@ Höhe 380px, volle Breite, abgerundete Ecken, Gold-Border.
|---|---| |---|---|
| `star_hipparcos` | ~118k Sterne aus dem Hipparcos-Katalog (ra, dec, mag, proper, spect) | | `star_hipparcos` | ~118k Sterne aus dem Hipparcos-Katalog (ra, dec, mag, proper, spect) |
| `dso_objects` | ~14k Deepsky-Objekte aus OpenNGC (NGC, IC, Messier, Typ, ra/dec, Helligkeit) | | `dso_objects` | ~14k Deepsky-Objekte aus OpenNGC (NGC, IC, Messier, Typ, ra/dec, Helligkeit) |
| `comets_mpc` | ~4600 Kometen vom Minor Planet Center (Bahnelemente) | | `comets_mpc` | Kometen aus MPC `Soft00Cmt.txt` (Fixed-width-Bahnelemente mit Epoche, H/G, Name, Referenz) |
| `constellations` | Sternbilder (ID = 3-Buchstaben-Kürzel, z. B. `ORI`) | | `constellations` | Sternbilder (ID = 3-Buchstaben-Kürzel, z. B. `ORI`) |
| `constellation_names` | Deutsche + lateinische Namen pro Sternbild | | `constellation_names` | Deutsche + lateinische Namen pro Sternbild |
| `constellation_lines` | Verbindungslinien zwischen Sternen | | `constellation_lines` | Verbindungslinien zwischen Sternen |
+20 -17
View File
@@ -119,31 +119,34 @@ CREATE TABLE IF NOT EXISTS `app_user_satellites` (
-- Exportiere Struktur von Tabelle d0467f49.comets_mpc -- Exportiere Struktur von Tabelle d0467f49.comets_mpc
CREATE TABLE IF NOT EXISTS `comets_mpc` ( CREATE TABLE IF NOT EXISTS `comets_mpc` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`orbit_type` char(1) NOT NULL COMMENT 'MPC Orbit_type, z.B. C fuer cometary orbit', `designation_packed` varchar(12) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT 'Fixed-width MPC-Packbezeichnung aus Soft00Cmt.txt, inkl. Fragmentkennung',
`provisional_packed_desig` varchar(32) DEFAULT NULL COMMENT 'Gepackte MPC-Bezeichnung, z.B. .53P010', `orbit_type` char(1) NOT NULL COMMENT 'Abgeleiteter Kometentyp, z. B. C, P oder I',
`year_of_perihelion` smallint(6) NOT NULL COMMENT 'Jahr des Periheldurchgangs, kann auch negativ sein', `year_of_perihelion` smallint(6) NOT NULL COMMENT 'Jahr des Periheldurchgangs',
`month_of_perihelion` tinyint(3) unsigned NOT NULL COMMENT 'Monat des Periheldurchgangs', `month_of_perihelion` tinyint(3) unsigned NOT NULL COMMENT 'Monat des Periheldurchgangs',
`day_of_perihelion` decimal(8,5) NOT NULL COMMENT 'Tag des Periheldurchgangs inklusive Tagesbruchteil', `day_of_perihelion` decimal(8,4) NOT NULL COMMENT 'Tag des Periheldurchgangs inklusive Tagesbruchteil',
`perihelion_dist` decimal(12,8) NOT NULL COMMENT 'Periheldistanz q in AE', `perihelion_dist_au` decimal(12,6) NOT NULL COMMENT 'Periheldistanz q in AE',
`eccentricity` decimal(12,8) NOT NULL COMMENT 'Exzentrizitaet e', `eccentricity` decimal(12,6) NOT NULL COMMENT 'Exzentrizitaet e',
`arg_perihelion_deg` decimal(10,6) NOT NULL COMMENT 'Argument des Perihels in Grad', `arg_perihelion_deg` decimal(9,4) NOT NULL COMMENT 'Argument des Perihels in Grad',
`ascending_node_deg` decimal(10,6) NOT NULL COMMENT 'Laenge des aufsteigenden Knotens in Grad', `ascending_node_deg` decimal(9,4) NOT NULL COMMENT 'Laenge des aufsteigenden Knotens in Grad',
`inclination_deg` decimal(10,6) NOT NULL COMMENT 'Bahnneigung in Grad', `inclination_deg` decimal(9,4) NOT NULL COMMENT 'Bahnneigung in Grad',
`absolute_magnitude_h` decimal(8,3) DEFAULT NULL COMMENT 'H-Magnitude laut MPC', `epoch_compact` char(8) DEFAULT NULL COMMENT 'Epoche des Elementsatzes als YYYYMMDD; kann bei Fragmenten fehlen',
`slope_parameter_g` decimal(8,3) DEFAULT NULL COMMENT 'G-Parameter laut MPC', `epoch_date` date DEFAULT NULL COMMENT 'Epoche des Elementsatzes als SQL-Datum',
`designation_and_name` varchar(255) NOT NULL COMMENT 'Lesbare Bezeichnung, z.B. C/-146 P1', `absolute_magnitude_h` decimal(5,1) DEFAULT NULL COMMENT 'Absolute Helligkeit H laut MPC',
`reference_text` varchar(64) DEFAULT NULL COMMENT 'Referenzfeld aus MPC, z.B. 31, 257', `slope_parameter_g` decimal(4,1) DEFAULT NULL COMMENT 'Steigungsparameter G laut MPC',
`raw_json` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT 'Originaler JSON-Datensatz fuer Nachvollziehbarkeit' CHECK (json_valid(`raw_json`)), `designation_and_name` varchar(255) NOT NULL COMMENT 'Lesbare Bezeichnung, z. B. C/1995 O1 (Hale-Bopp)',
`reference_text` varchar(64) DEFAULT NULL COMMENT 'Referenz aus der MPC-Datei, z. B. MPEC 2026-FC3',
`raw_record` varchar(255) DEFAULT NULL COMMENT 'Originale Fixed-width-Zeile aus Soft00Cmt.txt',
`source_url` varchar(255) DEFAULT NULL COMMENT 'Quelle des letzten Imports', `source_url` varchar(255) DEFAULT NULL COMMENT 'Quelle des letzten Imports',
`imported_at` datetime NOT NULL DEFAULT current_timestamp() COMMENT 'Zeitpunkt des letzten Imports', `imported_at` datetime NOT NULL DEFAULT current_timestamp() COMMENT 'Zeitpunkt des letzten Imports',
`created_at` datetime NOT NULL DEFAULT current_timestamp(), `created_at` datetime NOT NULL DEFAULT current_timestamp(),
`updated_at` datetime NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(), `updated_at` datetime NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
PRIMARY KEY (`id`), PRIMARY KEY (`id`),
UNIQUE KEY `uq_mpc_comets_designation_name` (`designation_and_name`), UNIQUE KEY `uq_mpc_comets_designation_packed` (`designation_packed`),
KEY `idx_mpc_comets_orbit_type` (`orbit_type`), KEY `idx_mpc_comets_orbit_type` (`orbit_type`),
KEY `idx_mpc_comets_perihelion_year_month` (`year_of_perihelion`,`month_of_perihelion`), KEY `idx_mpc_comets_perihelion_year_month` (`year_of_perihelion`,`month_of_perihelion`),
KEY `idx_mpc_comets_provisional_packed_desig` (`provisional_packed_desig`) KEY `idx_mpc_comets_epoch_compact` (`epoch_compact`),
) ENGINE=InnoDB AUTO_INCREMENT=4630 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Kometenelemente vom Minor Planet Center'; KEY `idx_mpc_comets_designation_and_name` (`designation_and_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Kometenelemente aus Soft00Cmt.txt des Minor Planet Center';
-- Daten-Export vom Benutzer nicht ausgewählt -- Daten-Export vom Benutzer nicht ausgewählt
+497 -199
View File
@@ -4,25 +4,39 @@ declare(strict_types=1);
/** /**
* Minor Planet Center Comet Import * Minor Planet Center Comet Import
* Quelle: * Quelle:
* https://www.minorplanetcenter.net/Extended_Files/allcometels.json.gz * https://www.minorplanetcenter.net/iau/Ephemerides/Comets/Soft00Cmt.txt
* *
* Zieltabelle: * Zieltabelle:
* - comets_mpc * - comets_mpc
*
* Nutzung:
* php scripts/import/import_mpc_comets.php
* php scripts/import/import_mpc_comets.php C:\pfad\zu\Soft00Cmt.txt
*/ */
ini_set('display_errors', '1');
error_reporting(E_ALL); error_reporting(E_ALL);
ini_set('display_errors', '1');
set_time_limit(0);
date_default_timezone_set('UTC'); date_default_timezone_set('UTC');
$config = require __DIR__ . '/../../config/database.php'; $rootDir = dirname(__DIR__, 2);
$config = require $rootDir . DIRECTORY_SEPARATOR . 'config' . DIRECTORY_SEPARATOR . 'database.php';
$sourceUrl = 'https://www.minorplanetcenter.net/Extended_Files/allcometels.json.gz'; if (!is_array($config)) {
writeError("config/database.php muss ein Array mit DB-Zugangsdaten zurueckgeben.");
exit(1);
}
$config = normalizeDbConfig($config);
$sourceUrl = 'https://www.minorplanetcenter.net/iau/Ephemerides/Comets/Soft00Cmt.txt';
$localSourcePath = $argv[1] ?? ($_GET['source'] ?? null);
$dsn = sprintf( $dsn = sprintf(
'mysql:host=%s;dbname=%s;charset=%s', 'mysql:host=%s;port=%d;dbname=%s;charset=%s',
$config['host'], $config['host'],
(int) $config['port'],
$config['dbname'], $config['dbname'],
$config['charset'] ?? 'utf8mb4' $config['charset']
); );
$pdo = new PDO( $pdo = new PDO(
@@ -35,7 +49,163 @@ $pdo = new PDO(
] ]
); );
function fetchRemoteJson(string $url): array $recordsReceived = 0;
$recordsImported = 0;
$recordsSkipped = 0;
try {
rebuildCometsTable($pdo);
[$lines, $resolvedSource] = resolveSourceLines($sourceUrl, $localSourcePath);
$statement = $pdo->prepare(
'INSERT INTO `comets_mpc` (
`designation_packed`,
`orbit_type`,
`year_of_perihelion`,
`month_of_perihelion`,
`day_of_perihelion`,
`perihelion_dist_au`,
`eccentricity`,
`arg_perihelion_deg`,
`ascending_node_deg`,
`inclination_deg`,
`epoch_compact`,
`epoch_date`,
`absolute_magnitude_h`,
`slope_parameter_g`,
`designation_and_name`,
`reference_text`,
`raw_record`,
`source_url`,
`imported_at`,
`created_at`,
`updated_at`
) VALUES (
:designation_packed,
:orbit_type,
:year_of_perihelion,
:month_of_perihelion,
:day_of_perihelion,
:perihelion_dist_au,
:eccentricity,
:arg_perihelion_deg,
:ascending_node_deg,
:inclination_deg,
:epoch_compact,
:epoch_date,
:absolute_magnitude_h,
:slope_parameter_g,
:designation_and_name,
:reference_text,
:raw_record,
:source_url,
NOW(),
NOW(),
NOW()
)'
);
$pdo->beginTransaction();
foreach ($lines as $lineNumber => $line) {
$trimmedLine = rtrim($line, "\r\n");
if (trim($trimmedLine) === '') {
continue;
}
$recordsReceived++;
try {
$record = parseSoft00CmtLine($trimmedLine);
$record[':source_url'] = $resolvedSource;
$statement->execute($record);
$recordsImported++;
} catch (Throwable $rowError) {
$recordsSkipped++;
echo 'Uebersprungen Zeile ' . ($lineNumber + 1) . ': ' . $rowError->getMessage() . "\n";
}
}
$pdo->commit();
echo "Import erfolgreich\n";
echo "Quelle: {$resolvedSource}\n";
echo "Empfangen: {$recordsReceived}\n";
echo "Importiert: {$recordsImported}\n";
echo "Uebersprungen: {$recordsSkipped}\n";
} catch (Throwable $e) {
if ($pdo->inTransaction()) {
$pdo->rollBack();
}
writeError('Fehler: ' . $e->getMessage());
exit(1);
}
function writeError(string $message): void
{
if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') {
fwrite(STDERR, $message . PHP_EOL);
return;
}
echo htmlspecialchars($message, ENT_QUOTES, 'UTF-8') . "<br>\n";
}
function fetchRemoteLines(string $url): array
{
if (function_exists('curl_init')) {
return fetchRemoteLinesViaCurl($url);
}
try {
return fetchRemoteLinesViaStream($url);
} catch (Throwable $streamError) {
return fetchRemoteLinesViaSocket($url, $streamError->getMessage());
}
}
function resolveSourceLines(string $remoteUrl, ?string $localSourcePath = null): array
{
if ($localSourcePath !== null && trim($localSourcePath) !== '') {
$lines = fetchLocalLines($localSourcePath);
return [$lines, realpath($localSourcePath) ?: $localSourcePath];
}
try {
$lines = fetchRemoteLines($remoteUrl);
return [$lines, $remoteUrl];
} catch (Throwable $remoteError) {
$defaultLocalPath = dirname(__FILE__) . DIRECTORY_SEPARATOR . 'Soft00Cmt.txt';
if (is_file($defaultLocalPath)) {
$lines = fetchLocalLines($defaultLocalPath);
return [$lines, realpath($defaultLocalPath) ?: $defaultLocalPath];
}
throw new RuntimeException(
$remoteError->getMessage()
. ' | Lege alternativ eine lokale Datei unter scripts/import/Soft00Cmt.txt ab '
. 'oder uebergib ihren Pfad als Argument bzw. ?source='
);
}
}
function fetchLocalLines(string $path): array
{
if (!is_file($path)) {
throw new RuntimeException('Lokale Datei nicht gefunden: ' . $path);
}
$contents = @file_get_contents($path);
if ($contents === false) {
$error = error_get_last();
throw new RuntimeException('Lokale Datei konnte nicht gelesen werden: ' . ($error['message'] ?? $path));
}
return preg_split("/\r\n|\n|\r/", $contents) ?: [];
}
function fetchRemoteLinesViaCurl(string $url): array
{ {
$ch = curl_init($url); $ch = curl_init($url);
@@ -44,10 +214,9 @@ function fetchRemoteJson(string $url): array
CURLOPT_FOLLOWLOCATION => true, CURLOPT_FOLLOWLOCATION => true,
CURLOPT_CONNECTTIMEOUT => 20, CURLOPT_CONNECTTIMEOUT => 20,
CURLOPT_TIMEOUT => 300, CURLOPT_TIMEOUT => 300,
CURLOPT_USERAGENT => 'AstroMINT MPC Comet Importer/1.0', CURLOPT_USERAGENT => 'AstroMINT MPC Comet Importer/2.0',
CURLOPT_HTTPHEADER => [ CURLOPT_HTTPHEADER => [
'Accept: application/json', 'Accept: text/plain',
'Accept-Encoding: gzip',
], ],
]); ]);
@@ -58,234 +227,363 @@ function fetchRemoteJson(string $url): array
} }
$httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE); $httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
$contentType = (string) curl_getinfo($ch, CURLINFO_CONTENT_TYPE);
if ($httpCode !== 200) { if ($httpCode !== 200) {
throw new RuntimeException('HTTP-Fehler beim Abruf: ' . $httpCode); throw new RuntimeException('HTTP-Fehler beim Abruf: ' . $httpCode);
} }
$isGzip = str_ends_with(strtolower($url), '.gz') return preg_split("/\r\n|\n|\r/", $response) ?: [];
|| str_contains(strtolower($contentType), 'gzip')
|| str_starts_with($response, "\x1f\x8b");
if ($isGzip) {
$decoded = gzdecode($response);
if ($decoded === false) {
throw new RuntimeException('GZip-Datei konnte nicht entpackt werden');
}
$response = $decoded;
}
$data = json_decode($response, true);
if (!is_array($data)) {
throw new RuntimeException('Ungültige JSON-Antwort');
}
return $data;
} }
function toNullableString(mixed $value): ?string function fetchRemoteLinesViaStream(string $url): array
{ {
if ($value === null) { $context = stream_context_create([
'http' => [
'method' => 'GET',
'timeout' => 300,
'ignore_errors' => true,
'header' => implode("\r\n", [
'User-Agent: AstroMINT MPC Comet Importer/2.0',
'Accept: text/plain',
]),
],
'ssl' => [
'verify_peer' => true,
'verify_peer_name' => true,
],
]);
$response = @file_get_contents($url, false, $context);
if ($response === false) {
$error = error_get_last();
throw new RuntimeException('Download fehlgeschlagen: ' . ($error['message'] ?? 'unbekannter Fehler'));
}
$statusLine = null;
foreach (($http_response_header ?? []) as $headerLine) {
if (stripos($headerLine, 'HTTP/') === 0) {
$statusLine = $headerLine;
break;
}
}
if ($statusLine !== null && preg_match('/\s(\d{3})\s/', $statusLine, $matches) === 1) {
$httpCode = (int) $matches[1];
if ($httpCode !== 200) {
throw new RuntimeException('HTTP-Fehler beim Abruf: ' . $httpCode);
}
}
return preg_split("/\r\n|\n|\r/", $response) ?: [];
}
function fetchRemoteLinesViaSocket(string $url, ?string $previousError = null): array
{
$parts = parse_url($url);
if (!is_array($parts) || ($parts['scheme'] ?? '') !== 'https' || !isset($parts['host'])) {
throw new RuntimeException('Socket-Fallback unterstuetzt nur HTTPS-URLs.');
}
$host = (string) $parts['host'];
$path = (string) ($parts['path'] ?? '/');
$query = isset($parts['query']) ? '?' . $parts['query'] : '';
$target = 'ssl://' . $host . ':443';
$context = stream_context_create([
'ssl' => [
'verify_peer' => true,
'verify_peer_name' => true,
'SNI_enabled' => true,
'peer_name' => $host,
],
]);
$errno = 0;
$errstr = '';
$socket = @stream_socket_client($target, $errno, $errstr, 30, STREAM_CLIENT_CONNECT, $context);
if ($socket === false) {
$detail = $errstr !== '' ? $errstr : 'Verbindung konnte nicht aufgebaut werden';
if ($previousError !== null && $previousError !== '') {
$detail .= ' | file_get_contents: ' . $previousError;
}
throw new RuntimeException('Download fehlgeschlagen: ' . $detail);
}
stream_set_timeout($socket, 300);
$request = implode("\r\n", [
'GET ' . $path . $query . ' HTTP/1.1',
'Host: ' . $host,
'User-Agent: AstroMINT MPC Comet Importer/2.0',
'Accept: text/plain',
'Connection: close',
'',
'',
]);
fwrite($socket, $request);
$response = stream_get_contents($socket);
fclose($socket);
if ($response === false || $response === '') {
throw new RuntimeException('Download fehlgeschlagen: leere Antwort vom Server');
}
$separatorPos = strpos($response, "\r\n\r\n");
if ($separatorPos === false) {
throw new RuntimeException('Download fehlgeschlagen: ungueltige HTTP-Antwort');
}
$headerText = substr($response, 0, $separatorPos);
$body = substr($response, $separatorPos + 4);
$headerLines = explode("\r\n", $headerText);
$statusLine = $headerLines[0] ?? '';
if (preg_match('/\s(\d{3})\s/', $statusLine, $matches) !== 1) {
throw new RuntimeException('Download fehlgeschlagen: HTTP-Status unbekannt');
}
$httpCode = (int) $matches[1];
if ($httpCode !== 200) {
throw new RuntimeException('HTTP-Fehler beim Abruf: ' . $httpCode);
}
$headers = [];
foreach (array_slice($headerLines, 1) as $headerLine) {
$parts = explode(':', $headerLine, 2);
if (count($parts) === 2) {
$headers[strtolower(trim($parts[0]))] = trim($parts[1]);
}
}
if (($headers['transfer-encoding'] ?? '') === 'chunked') {
$body = decodeChunkedHttpBody($body);
}
return preg_split("/\r\n|\n|\r/", $body) ?: [];
}
function decodeChunkedHttpBody(string $body): string
{
$decoded = '';
$offset = 0;
$length = strlen($body);
while ($offset < $length) {
$lineEnd = strpos($body, "\r\n", $offset);
if ($lineEnd === false) {
break;
}
$hexLength = trim(substr($body, $offset, $lineEnd - $offset));
if ($hexLength === '') {
break;
}
$chunkLength = hexdec($hexLength);
$offset = $lineEnd + 2;
if ($chunkLength === 0) {
break;
}
$decoded .= substr($body, $offset, $chunkLength);
$offset += $chunkLength + 2;
}
return $decoded;
}
function parseSoft00CmtLine(string $line): array
{
$padded = str_pad($line, 172, ' ');
$designationPacked = requireStringField(readFixedField($padded, 1, 12), 'designation_packed');
$designationAndName = requireStringField(readFixedField($padded, 103, 158), 'designation_and_name');
$epochCompact = toNullableCompactDate(readFixedField($padded, 82, 89));
return [
':designation_packed' => $designationPacked,
':orbit_type' => deriveOrbitType($designationAndName, $designationPacked),
':year_of_perihelion' => requireIntField(readFixedField($padded, 15, 18), 'year_of_perihelion'),
':month_of_perihelion' => requireIntField(readFixedField($padded, 20, 21), 'month_of_perihelion'),
':day_of_perihelion' => requireFloatField(readFixedField($padded, 23, 29), 'day_of_perihelion'),
':perihelion_dist_au' => requireFloatField(readFixedField($padded, 32, 39), 'perihelion_dist_au'),
':eccentricity' => requireFloatField(readFixedField($padded, 42, 49), 'eccentricity'),
':arg_perihelion_deg' => requireFloatField(readFixedField($padded, 52, 59), 'arg_perihelion_deg'),
':ascending_node_deg' => requireFloatField(readFixedField($padded, 62, 69), 'ascending_node_deg'),
':inclination_deg' => requireFloatField(readFixedField($padded, 72, 79), 'inclination_deg'),
':epoch_compact' => $epochCompact,
':epoch_date' => $epochCompact !== null ? compactDateToSql($epochCompact) : null,
':absolute_magnitude_h' => toNullableFloat(readFixedField($padded, 91, 95)),
':slope_parameter_g' => toNullableFloat(readFixedField($padded, 97, 101)),
':designation_and_name' => $designationAndName,
':reference_text' => toNullableString(readFixedField($padded, 160, 200)),
':raw_record' => $line,
];
}
function deriveOrbitType(string $designationAndName, string $designationPacked): string
{
if (preg_match('/^\d+([A-Z])\//', $designationAndName, $matches) === 1) {
return $matches[1];
}
if (preg_match('/^([A-Z])\//', $designationAndName, $matches) === 1) {
return $matches[1];
}
if (preg_match('/([A-Z])/', ltrim($designationPacked), $matches) === 1) {
return $matches[1];
}
throw new RuntimeException('orbit_type konnte nicht abgeleitet werden');
}
function readFixedField(string $line, int $startColumn, int $endColumn): ?string
{
$offset = $startColumn - 1;
$length = $endColumn - $startColumn + 1;
$value = substr($line, $offset, $length);
if ($value === false) {
return null; return null;
} }
$value = trim((string) $value); $value = trim($value);
return $value === '' ? null : $value; return $value === '' ? null : $value;
} }
function toNullableInt(mixed $value): ?int function requireStringField(?string $value, string $fieldName): string
{ {
if ($value === null || $value === '') { if ($value === null || $value === '') {
return null; throw new RuntimeException("Pflichtfeld fehlt oder ist ungueltig: {$fieldName}");
} }
if (!is_numeric($value)) { return $value;
return null; }
function requireIntField(?string $value, string $fieldName): int
{
$value = trim((string) $value);
if ($value === '' || preg_match('/^-?\d+$/', $value) !== 1) {
throw new RuntimeException("Pflichtfeld fehlt oder ist ungueltig: {$fieldName}");
} }
return (int) $value; return (int) $value;
} }
function toNullableFloat(mixed $value): ?float function requireFloatField(?string $value, string $fieldName): float
{ {
if ($value === null || $value === '') { $value = trim((string) $value);
if ($value === '' || !is_numeric($value)) {
throw new RuntimeException("Pflichtfeld fehlt oder ist ungueltig: {$fieldName}");
}
return (float) $value;
}
function toNullableCompactDate(?string $value): ?string
{
$value = trim((string) $value);
if ($value === '') {
return null; return null;
} }
if (!is_numeric($value)) { if (preg_match('/^\d{8}$/', $value) !== 1) {
throw new RuntimeException('epoch_compact ist ungueltig');
}
return $value;
}
function toNullableString(?string $value): ?string
{
if ($value === null) {
return null;
}
$value = trim($value);
return $value === '' ? null : $value;
}
function toNullableFloat(?string $value): ?float
{
if ($value === null) {
return null;
}
$value = trim($value);
if ($value === '' || !is_numeric($value)) {
return null; return null;
} }
return (float) $value; return (float) $value;
} }
function requireIntField(array $row, string $fieldName): int function compactDateToSql(string $value): ?string
{ {
$value = toNullableInt($row[$fieldName] ?? null); if (preg_match('/^\d{8}$/', $value) !== 1) {
if ($value === null) { return null;
throw new RuntimeException("Pflichtfeld fehlt oder ist ungültig: {$fieldName}");
} }
return $value; $year = (int) substr($value, 0, 4);
$month = (int) substr($value, 4, 2);
$day = (int) substr($value, 6, 2);
if (!checkdate($month, $day, $year)) {
return null;
}
return sprintf('%04d-%02d-%02d', $year, $month, $day);
} }
function requireFloatField(array $row, string $fieldName): float function normalizeDbConfig(array $config): array
{ {
$value = toNullableFloat($row[$fieldName] ?? null); return [
if ($value === null) { 'host' => (string) ($config['host'] ?? '127.0.0.1'),
throw new RuntimeException("Pflichtfeld fehlt oder ist ungültig: {$fieldName}"); 'port' => (int) ($config['port'] ?? 3306),
} 'dbname' => (string) ($config['dbname'] ?? ''),
'user' => (string) ($config['user'] ?? ''),
return $value; 'pass' => (string) ($config['pass'] ?? ''),
'charset' => (string) ($config['charset'] ?? 'utf8mb4'),
];
} }
function requireStringField(array $row, string $fieldName): string function rebuildCometsTable(PDO $pdo): void
{ {
$value = toNullableString($row[$fieldName] ?? null); $pdo->exec('DROP TABLE IF EXISTS `comets_mpc`');
if ($value === null) { $pdo->exec(<<<'SQL'
throw new RuntimeException("Pflichtfeld fehlt oder ist ungültig: {$fieldName}"); CREATE TABLE `comets_mpc` (
} `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`designation_packed` varchar(12) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT 'Fixed-width MPC-Packbezeichnung aus Soft00Cmt.txt, inkl. Fragmentkennung',
return $value; `orbit_type` char(1) NOT NULL COMMENT 'Abgeleiteter Kometentyp, z. B. C, P oder I',
} `year_of_perihelion` smallint(6) NOT NULL COMMENT 'Jahr des Periheldurchgangs',
`month_of_perihelion` tinyint(3) unsigned NOT NULL COMMENT 'Monat des Periheldurchgangs',
$recordsReceived = 0; `day_of_perihelion` decimal(8,4) NOT NULL COMMENT 'Tag des Periheldurchgangs inklusive Tagesbruchteil',
$recordsInserted = 0; `perihelion_dist_au` decimal(12,6) NOT NULL COMMENT 'Periheldistanz q in AE',
$recordsUpdated = 0; `eccentricity` decimal(12,6) NOT NULL COMMENT 'Exzentrizitaet e',
$recordsSkipped = 0; `arg_perihelion_deg` decimal(9,4) NOT NULL COMMENT 'Argument des Perihels in Grad',
`ascending_node_deg` decimal(9,4) NOT NULL COMMENT 'Laenge des aufsteigenden Knotens in Grad',
try { `inclination_deg` decimal(9,4) NOT NULL COMMENT 'Bahnneigung in Grad',
$rows = fetchRemoteJson($sourceUrl); `epoch_compact` char(8) DEFAULT NULL COMMENT 'Epoche des Elementsatzes als YYYYMMDD; kann bei Fragmenten fehlen',
`epoch_date` date DEFAULT NULL COMMENT 'Epoche des Elementsatzes als SQL-Datum',
if ($rows === [] || array_is_list($rows) === false) { `absolute_magnitude_h` decimal(5,1) DEFAULT NULL COMMENT 'Absolute Helligkeit H laut MPC',
throw new RuntimeException('Es wurde keine gültige Kometenliste empfangen'); `slope_parameter_g` decimal(4,1) DEFAULT NULL COMMENT 'Steigungsparameter G laut MPC',
} `designation_and_name` varchar(255) NOT NULL COMMENT 'Lesbare Bezeichnung, z. B. C/1995 O1 (Hale-Bopp)',
`reference_text` varchar(64) DEFAULT NULL COMMENT 'Referenz aus der MPC-Datei, z. B. MPEC 2026-FC3',
$stmtExists = $pdo->prepare(" `raw_record` varchar(255) DEFAULT NULL COMMENT 'Originale Fixed-width-Zeile aus Soft00Cmt.txt',
SELECT id `source_url` varchar(255) DEFAULT NULL COMMENT 'Quelle des letzten Imports',
FROM comets_mpc `imported_at` datetime NOT NULL DEFAULT current_timestamp() COMMENT 'Zeitpunkt des letzten Imports',
WHERE designation_and_name = :designation_and_name `created_at` datetime NOT NULL DEFAULT current_timestamp(),
LIMIT 1 `updated_at` datetime NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
"); PRIMARY KEY (`id`),
UNIQUE KEY `uq_mpc_comets_designation_packed` (`designation_packed`),
$stmtUpsert = $pdo->prepare(" KEY `idx_mpc_comets_orbit_type` (`orbit_type`),
INSERT INTO comets_mpc ( KEY `idx_mpc_comets_perihelion_year_month` (`year_of_perihelion`,`month_of_perihelion`),
orbit_type, KEY `idx_mpc_comets_epoch_compact` (`epoch_compact`),
provisional_packed_desig, KEY `idx_mpc_comets_designation_and_name` (`designation_and_name`)
year_of_perihelion, ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Kometenelemente aus Soft00Cmt.txt des Minor Planet Center'
month_of_perihelion, SQL
day_of_perihelion, );
perihelion_dist,
eccentricity,
arg_perihelion_deg,
ascending_node_deg,
inclination_deg,
absolute_magnitude_h,
slope_parameter_g,
designation_and_name,
reference_text,
raw_json,
source_url,
imported_at,
created_at,
updated_at
) VALUES (
:orbit_type,
:provisional_packed_desig,
:year_of_perihelion,
:month_of_perihelion,
:day_of_perihelion,
:perihelion_dist,
:eccentricity,
:arg_perihelion_deg,
:ascending_node_deg,
:inclination_deg,
:absolute_magnitude_h,
:slope_parameter_g,
:designation_and_name,
:reference_text,
:raw_json,
:source_url,
NOW(),
NOW(),
NOW()
)
ON DUPLICATE KEY UPDATE
orbit_type = VALUES(orbit_type),
provisional_packed_desig = VALUES(provisional_packed_desig),
year_of_perihelion = VALUES(year_of_perihelion),
month_of_perihelion = VALUES(month_of_perihelion),
day_of_perihelion = VALUES(day_of_perihelion),
perihelion_dist = VALUES(perihelion_dist),
eccentricity = VALUES(eccentricity),
arg_perihelion_deg = VALUES(arg_perihelion_deg),
ascending_node_deg = VALUES(ascending_node_deg),
inclination_deg = VALUES(inclination_deg),
absolute_magnitude_h = VALUES(absolute_magnitude_h),
slope_parameter_g = VALUES(slope_parameter_g),
reference_text = VALUES(reference_text),
raw_json = VALUES(raw_json),
source_url = VALUES(source_url),
imported_at = NOW(),
updated_at = NOW()
");
$pdo->beginTransaction();
foreach ($rows as $row) {
try {
$designationAndName = requireStringField($row, 'Designation_and_name');
$recordsReceived++;
$stmtExists->execute([
':designation_and_name' => $designationAndName,
]);
$exists = (bool) $stmtExists->fetchColumn();
$stmtUpsert->execute([
':orbit_type' => substr(requireStringField($row, 'Orbit_type'), 0, 1),
':provisional_packed_desig' => toNullableString($row['Provisional_packed_desig'] ?? null),
':year_of_perihelion' => requireIntField($row, 'Year_of_perihelion'),
':month_of_perihelion' => requireIntField($row, 'Month_of_perihelion'),
':day_of_perihelion' => requireFloatField($row, 'Day_of_perihelion'),
':perihelion_dist' => requireFloatField($row, 'Perihelion_dist'),
':eccentricity' => requireFloatField($row, 'e'),
':arg_perihelion_deg' => requireFloatField($row, 'Peri'),
':ascending_node_deg' => requireFloatField($row, 'Node'),
':inclination_deg' => requireFloatField($row, 'i'),
':absolute_magnitude_h' => toNullableFloat($row['H'] ?? null),
':slope_parameter_g' => toNullableFloat($row['G'] ?? null),
':designation_and_name' => $designationAndName,
':reference_text' => toNullableString($row['Ref'] ?? null),
':raw_json' => json_encode($row, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
':source_url' => $sourceUrl,
]);
if ($exists) {
$recordsUpdated++;
} else {
$recordsInserted++;
}
} catch (Throwable $rowError) {
$recordsSkipped++;
echo "Übersprungen: " . $rowError->getMessage() . "\n";
}
}
$pdo->commit();
echo "Import erfolgreich\n";
echo "Empfangen: {$recordsReceived}\n";
echo "Neu: {$recordsInserted}\n";
echo "Aktualisiert: {$recordsUpdated}\n";
echo "Übersprungen: {$recordsSkipped}\n";
} catch (Throwable $e) {
if ($pdo->inTransaction()) {
$pdo->rollBack();
}
echo "Fehler: " . $e->getMessage() . "\n";
exit(1);
} }