first commit
This commit is contained in:
@@ -0,0 +1,464 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* CelesTrak JSON Import
|
||||
* Quelle:
|
||||
* https://celestrak.org/NORAD/elements/gp.php?GROUP=active&FORMAT=json
|
||||
*
|
||||
* Tabellen:
|
||||
* - sat_satellites
|
||||
* - sat_tle_current
|
||||
* - sat_import_runs
|
||||
*/
|
||||
|
||||
ini_set('display_errors', '1');
|
||||
error_reporting(E_ALL);
|
||||
date_default_timezone_set('UTC');
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Konfiguration laden
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
$config = require __DIR__ . '/../../config/database.php';
|
||||
|
||||
$sourceUrl = 'https://celestrak.org/NORAD/elements/gp.php?GROUP=active&FORMAT=json';
|
||||
$sourceName = 'celestrak';
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Datenbankverbindung
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
$dsn = sprintf(
|
||||
'mysql:host=%s;dbname=%s;charset=%s',
|
||||
$config['host'],
|
||||
$config['dbname'],
|
||||
$config['charset'] ?? 'utf8mb4'
|
||||
);
|
||||
|
||||
$pdo = new PDO(
|
||||
$dsn,
|
||||
$config['user'],
|
||||
$config['pass'],
|
||||
[
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||
]
|
||||
);
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Hilfsfunktionen
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
function fetchRemoteJson(string $url): array
|
||||
{
|
||||
$ch = curl_init($url);
|
||||
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_FOLLOWLOCATION => true,
|
||||
CURLOPT_CONNECTTIMEOUT => 20,
|
||||
CURLOPT_TIMEOUT => 180,
|
||||
CURLOPT_USERAGENT => 'Skyview CelesTrak Importer/1.0',
|
||||
CURLOPT_HTTPHEADER => [
|
||||
'Accept: application/json',
|
||||
'Accept-Encoding: gzip',
|
||||
],
|
||||
]);
|
||||
|
||||
$response = curl_exec($ch);
|
||||
|
||||
if ($response === false) {
|
||||
throw new RuntimeException('Download fehlgeschlagen: ' . curl_error($ch));
|
||||
}
|
||||
|
||||
$httpCode = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
$contentType = (string)curl_getinfo($ch, CURLINFO_CONTENT_TYPE);
|
||||
|
||||
curl_close($ch);
|
||||
|
||||
if ($httpCode !== 200) {
|
||||
throw new RuntimeException('HTTP-Fehler beim Abruf: ' . $httpCode);
|
||||
}
|
||||
|
||||
$isGzip = str_ends_with(strtolower($url), '.gz')
|
||||
|| 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 normalizeDateTime(?string $value): ?string
|
||||
{
|
||||
if ($value === null || trim($value) === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
$dt = new DateTimeImmutable($value, new DateTimeZone('UTC'));
|
||||
return $dt->format('Y-m-d H:i:s');
|
||||
} catch (Throwable $e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeDate(?string $value): ?string
|
||||
{
|
||||
if ($value === null || trim($value) === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
$dt = new DateTimeImmutable($value, new DateTimeZone('UTC'));
|
||||
return $dt->format('Y-m-d');
|
||||
} catch (Throwable $e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function toNullableString(mixed $value): ?string
|
||||
{
|
||||
if ($value === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$value = trim((string)$value);
|
||||
return $value === '' ? null : $value;
|
||||
}
|
||||
|
||||
function toNullableFloat(mixed $value): ?float
|
||||
{
|
||||
if ($value === null || $value === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!is_numeric($value)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (float)$value;
|
||||
}
|
||||
|
||||
function toNullableInt(mixed $value): ?int
|
||||
{
|
||||
if ($value === null || $value === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!is_numeric($value)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (int)$value;
|
||||
}
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Import
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
$importRunId = null;
|
||||
$recordsReceived = 0;
|
||||
$recordsInserted = 0;
|
||||
$recordsUpdated = 0;
|
||||
|
||||
try {
|
||||
$stmtImportStart = $pdo->prepare("
|
||||
INSERT INTO sat_import_runs (
|
||||
source,
|
||||
started_at,
|
||||
status,
|
||||
message
|
||||
) VALUES (
|
||||
:source,
|
||||
NOW(),
|
||||
'running',
|
||||
NULL
|
||||
)
|
||||
");
|
||||
$stmtImportStart->execute([
|
||||
':source' => $sourceName,
|
||||
]);
|
||||
|
||||
$importRunId = (int)$pdo->lastInsertId();
|
||||
|
||||
$rows = fetchRemoteJson($sourceUrl);
|
||||
|
||||
if (empty($rows)) {
|
||||
throw new RuntimeException('Keine Datensätze empfangen');
|
||||
}
|
||||
|
||||
$stmtFindSatellite = $pdo->prepare("
|
||||
SELECT id
|
||||
FROM sat_satellites
|
||||
WHERE norad_cat_id = :norad_cat_id
|
||||
LIMIT 1
|
||||
");
|
||||
|
||||
$stmtUpsertSatellite = $pdo->prepare("
|
||||
INSERT INTO sat_satellites (
|
||||
norad_cat_id,
|
||||
object_name,
|
||||
object_id,
|
||||
object_type,
|
||||
classification_type,
|
||||
country_code,
|
||||
launch_date,
|
||||
decay_date,
|
||||
rcs_size,
|
||||
active,
|
||||
created_at,
|
||||
updated_at
|
||||
) VALUES (
|
||||
:norad_cat_id,
|
||||
:object_name,
|
||||
:object_id,
|
||||
:object_type,
|
||||
:classification_type,
|
||||
:country_code,
|
||||
:launch_date,
|
||||
:decay_date,
|
||||
:rcs_size,
|
||||
1,
|
||||
NOW(),
|
||||
NOW()
|
||||
)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
object_name = VALUES(object_name),
|
||||
object_id = VALUES(object_id),
|
||||
object_type = VALUES(object_type),
|
||||
classification_type = VALUES(classification_type),
|
||||
country_code = VALUES(country_code),
|
||||
launch_date = VALUES(launch_date),
|
||||
decay_date = VALUES(decay_date),
|
||||
rcs_size = VALUES(rcs_size),
|
||||
active = 1,
|
||||
updated_at = NOW()
|
||||
");
|
||||
|
||||
$stmtCurrentExists = $pdo->prepare("
|
||||
SELECT id
|
||||
FROM sat_tle_current
|
||||
WHERE norad_cat_id = :norad_cat_id
|
||||
LIMIT 1
|
||||
");
|
||||
|
||||
$stmtUpsertCurrent = $pdo->prepare("
|
||||
INSERT INTO sat_tle_current (
|
||||
satellite_id,
|
||||
norad_cat_id,
|
||||
epoch,
|
||||
creation_date,
|
||||
mean_motion,
|
||||
eccentricity,
|
||||
inclination,
|
||||
ra_of_asc_node,
|
||||
arg_of_pericenter,
|
||||
mean_anomaly,
|
||||
ephemeris_type,
|
||||
element_set_no,
|
||||
rev_at_epoch,
|
||||
bstar,
|
||||
mean_motion_dot,
|
||||
mean_motion_ddot,
|
||||
center_name,
|
||||
ref_frame,
|
||||
time_system,
|
||||
mean_element_theory,
|
||||
raw_json,
|
||||
source,
|
||||
imported_at,
|
||||
updated_at
|
||||
) VALUES (
|
||||
:satellite_id,
|
||||
:norad_cat_id,
|
||||
:epoch,
|
||||
:creation_date,
|
||||
:mean_motion,
|
||||
:eccentricity,
|
||||
:inclination,
|
||||
:ra_of_asc_node,
|
||||
:arg_of_pericenter,
|
||||
:mean_anomaly,
|
||||
:ephemeris_type,
|
||||
:element_set_no,
|
||||
:rev_at_epoch,
|
||||
:bstar,
|
||||
:mean_motion_dot,
|
||||
:mean_motion_ddot,
|
||||
:center_name,
|
||||
:ref_frame,
|
||||
:time_system,
|
||||
:mean_element_theory,
|
||||
:raw_json,
|
||||
:source,
|
||||
NOW(),
|
||||
NOW()
|
||||
)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
satellite_id = VALUES(satellite_id),
|
||||
epoch = VALUES(epoch),
|
||||
creation_date = VALUES(creation_date),
|
||||
mean_motion = VALUES(mean_motion),
|
||||
eccentricity = VALUES(eccentricity),
|
||||
inclination = VALUES(inclination),
|
||||
ra_of_asc_node = VALUES(ra_of_asc_node),
|
||||
arg_of_pericenter = VALUES(arg_of_pericenter),
|
||||
mean_anomaly = VALUES(mean_anomaly),
|
||||
ephemeris_type = VALUES(ephemeris_type),
|
||||
element_set_no = VALUES(element_set_no),
|
||||
rev_at_epoch = VALUES(rev_at_epoch),
|
||||
bstar = VALUES(bstar),
|
||||
mean_motion_dot = VALUES(mean_motion_dot),
|
||||
mean_motion_ddot = VALUES(mean_motion_ddot),
|
||||
center_name = VALUES(center_name),
|
||||
ref_frame = VALUES(ref_frame),
|
||||
time_system = VALUES(time_system),
|
||||
mean_element_theory = VALUES(mean_element_theory),
|
||||
raw_json = VALUES(raw_json),
|
||||
source = VALUES(source),
|
||||
imported_at = NOW(),
|
||||
updated_at = NOW()
|
||||
");
|
||||
|
||||
$pdo->beginTransaction();
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$noradCatId = toNullableInt($row['NORAD_CAT_ID'] ?? null);
|
||||
|
||||
if ($noradCatId === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$recordsReceived++;
|
||||
|
||||
$stmtCurrentExists->execute([
|
||||
':norad_cat_id' => $noradCatId,
|
||||
]);
|
||||
$currentExists = (bool)$stmtCurrentExists->fetchColumn();
|
||||
|
||||
$stmtUpsertSatellite->execute([
|
||||
':norad_cat_id' => $noradCatId,
|
||||
':object_name' => toNullableString($row['OBJECT_NAME'] ?? null),
|
||||
':object_id' => toNullableString($row['OBJECT_ID'] ?? null),
|
||||
':object_type' => toNullableString($row['OBJECT_TYPE'] ?? null),
|
||||
':classification_type' => toNullableString($row['CLASSIFICATION_TYPE'] ?? null),
|
||||
':country_code' => toNullableString($row['COUNTRY_CODE'] ?? null),
|
||||
':launch_date' => normalizeDate($row['LAUNCH_DATE'] ?? null),
|
||||
':decay_date' => normalizeDate($row['DECAY_DATE'] ?? null),
|
||||
':rcs_size' => toNullableString($row['RCS_SIZE'] ?? null),
|
||||
]);
|
||||
|
||||
$stmtFindSatellite->execute([
|
||||
':norad_cat_id' => $noradCatId,
|
||||
]);
|
||||
$satelliteId = $stmtFindSatellite->fetchColumn();
|
||||
|
||||
if (!$satelliteId) {
|
||||
throw new RuntimeException('satellite_id konnte nicht ermittelt werden für NORAD ' . $noradCatId);
|
||||
}
|
||||
|
||||
$stmtUpsertCurrent->execute([
|
||||
':satellite_id' => (int)$satelliteId,
|
||||
':norad_cat_id' => $noradCatId,
|
||||
':epoch' => normalizeDateTime($row['EPOCH'] ?? null),
|
||||
':creation_date' => normalizeDateTime($row['CREATION_DATE'] ?? null),
|
||||
':mean_motion' => toNullableFloat($row['MEAN_MOTION'] ?? null),
|
||||
':eccentricity' => toNullableFloat($row['ECCENTRICITY'] ?? null),
|
||||
':inclination' => toNullableFloat($row['INCLINATION'] ?? null),
|
||||
':ra_of_asc_node' => toNullableFloat($row['RA_OF_ASC_NODE'] ?? null),
|
||||
':arg_of_pericenter' => toNullableFloat($row['ARG_OF_PERICENTER'] ?? null),
|
||||
':mean_anomaly' => toNullableFloat($row['MEAN_ANOMALY'] ?? null),
|
||||
':ephemeris_type' => toNullableInt($row['EPHEMERIS_TYPE'] ?? null),
|
||||
':element_set_no' => toNullableInt($row['ELEMENT_SET_NO'] ?? null),
|
||||
':rev_at_epoch' => toNullableInt($row['REV_AT_EPOCH'] ?? null),
|
||||
':bstar' => toNullableFloat($row['BSTAR'] ?? null),
|
||||
':mean_motion_dot' => toNullableFloat($row['MEAN_MOTION_DOT'] ?? null),
|
||||
':mean_motion_ddot' => toNullableFloat($row['MEAN_MOTION_DDOT'] ?? null),
|
||||
':center_name' => toNullableString($row['CENTER_NAME'] ?? null),
|
||||
':ref_frame' => toNullableString($row['REF_FRAME'] ?? null),
|
||||
':time_system' => toNullableString($row['TIME_SYSTEM'] ?? null),
|
||||
':mean_element_theory' => toNullableString($row['MEAN_ELEMENT_THEORY'] ?? null),
|
||||
':raw_json' => json_encode($row, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
|
||||
':source' => $sourceName,
|
||||
]);
|
||||
|
||||
if ($currentExists) {
|
||||
$recordsUpdated++;
|
||||
} else {
|
||||
$recordsInserted++;
|
||||
}
|
||||
}
|
||||
|
||||
$pdo->commit();
|
||||
|
||||
$stmtImportFinish = $pdo->prepare("
|
||||
UPDATE sat_import_runs
|
||||
SET
|
||||
finished_at = NOW(),
|
||||
records_received = :records_received,
|
||||
records_inserted = :records_inserted,
|
||||
records_updated = :records_updated,
|
||||
status = 'success',
|
||||
message = NULL
|
||||
WHERE id = :id
|
||||
");
|
||||
$stmtImportFinish->execute([
|
||||
':records_received' => $recordsReceived,
|
||||
':records_inserted' => $recordsInserted,
|
||||
':records_updated' => $recordsUpdated,
|
||||
':id' => $importRunId,
|
||||
]);
|
||||
|
||||
echo "Import erfolgreich\n";
|
||||
echo "Empfangen: {$recordsReceived}\n";
|
||||
echo "Neu: {$recordsInserted}\n";
|
||||
echo "Aktualisiert: {$recordsUpdated}\n";
|
||||
|
||||
} catch (Throwable $e) {
|
||||
if ($pdo->inTransaction()) {
|
||||
$pdo->rollBack();
|
||||
}
|
||||
|
||||
if ($importRunId !== null) {
|
||||
$stmtImportError = $pdo->prepare("
|
||||
UPDATE sat_import_runs
|
||||
SET
|
||||
finished_at = NOW(),
|
||||
records_received = :records_received,
|
||||
records_inserted = :records_inserted,
|
||||
records_updated = :records_updated,
|
||||
status = 'error',
|
||||
message = :message
|
||||
WHERE id = :id
|
||||
");
|
||||
$stmtImportError->execute([
|
||||
':records_received' => $recordsReceived,
|
||||
':records_inserted' => $recordsInserted,
|
||||
':records_updated' => $recordsUpdated,
|
||||
':message' => mb_substr($e->getMessage(), 0, 65000),
|
||||
':id' => $importRunId,
|
||||
]);
|
||||
}
|
||||
|
||||
echo "Fehler: " . $e->getMessage() . "\n";
|
||||
exit(1);
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Import RCS (Radar Cross Section) aus dem CelesTrak Satcat.
|
||||
*
|
||||
* Quelle: https://celestrak.org/pub/satcat.csv
|
||||
*
|
||||
* Relevante Spalten:
|
||||
* NORAD_CAT_ID – NORAD-Katalognummer
|
||||
* RCS – Radar Cross Section in m² (kann leer sein)
|
||||
*
|
||||
* Die RCS wird in sat_satellites.rcs gespeichert und ermöglicht eine
|
||||
* bessere Helligkeitsschätzung als die grobe LARGE/MEDIUM/SMALL-Kategorie.
|
||||
*
|
||||
* Formel: m1000 ≈ 7.5 − 2.5·log10(RCS)
|
||||
* (diffuse Kugel, 1000 km Entfernung, Phase 90°, kein Atm.)
|
||||
*
|
||||
* Voraussetzung:
|
||||
* ALTER TABLE sat_satellites ADD COLUMN rcs FLOAT DEFAULT NULL;
|
||||
*
|
||||
* Aufruf:
|
||||
* php import_celestrak_satcat.php
|
||||
*/
|
||||
|
||||
$config = require __DIR__ . '/../../config/database.php';
|
||||
|
||||
$dsn = sprintf(
|
||||
'mysql:host=%s;dbname=%s;charset=%s',
|
||||
$config['host'],
|
||||
$config['dbname'],
|
||||
$config['charset'] ?? 'utf8mb4'
|
||||
);
|
||||
|
||||
$pdo = new PDO($dsn, $config['user'], $config['pass'], [
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||
]);
|
||||
|
||||
// --- Download ---
|
||||
$url = 'https://celestrak.org/pub/satcat.csv';
|
||||
echo "Lade satcat.csv von CelesTrak...\n";
|
||||
$content = @file_get_contents($url);
|
||||
if ($content === false) {
|
||||
exit("Download fehlgeschlagen. Netzwerkverbindung prüfen.\n");
|
||||
}
|
||||
echo 'Download OK (' . number_format(strlen($content)) . " Bytes).\n";
|
||||
|
||||
// --- CSV einlesen ---
|
||||
$lines = explode("\n", str_replace("\r\n", "\n", $content));
|
||||
if (count($lines) < 2) {
|
||||
exit("CSV leer oder ungültig.\n");
|
||||
}
|
||||
|
||||
// Header-Zeile parsen
|
||||
$header = str_getcsv(array_shift($lines));
|
||||
$header = array_map('trim', $header);
|
||||
|
||||
$idxNorad = array_search('NORAD_CAT_ID', $header, true);
|
||||
$idxRcs = array_search('RCS', $header, true);
|
||||
|
||||
if ($idxNorad === false || $idxRcs === false) {
|
||||
exit("Erwartete Spalten NORAD_CAT_ID / RCS nicht gefunden. Header: " . implode(', ', $header) . "\n");
|
||||
}
|
||||
|
||||
// --- Update vorbereiten ---
|
||||
$stmt = $pdo->prepare("
|
||||
UPDATE `sat_satellites`
|
||||
SET `rcs` = :rcs
|
||||
WHERE `norad_cat_id` = :norad
|
||||
");
|
||||
|
||||
$updated = 0;
|
||||
$skipped = 0; // nicht in DB
|
||||
$noRcs = 0; // RCS leer in satcat
|
||||
$total = 0;
|
||||
|
||||
foreach ($lines as $line) {
|
||||
$line = trim($line);
|
||||
if ($line === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$cols = str_getcsv($line);
|
||||
$norad = isset($cols[$idxNorad]) ? (int)trim($cols[$idxNorad]) : 0;
|
||||
|
||||
if ($norad === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$total++;
|
||||
|
||||
$rcsRaw = isset($cols[$idxRcs]) ? trim($cols[$idxRcs]) : '';
|
||||
|
||||
if ($rcsRaw === '' || $rcsRaw === '0' || $rcsRaw === '0.0000') {
|
||||
$noRcs++;
|
||||
continue; // kein brauchbarer RCS-Wert
|
||||
}
|
||||
|
||||
$rcs = (float)$rcsRaw;
|
||||
if ($rcs <= 0) {
|
||||
$noRcs++;
|
||||
continue;
|
||||
}
|
||||
|
||||
$stmt->execute([':rcs' => $rcs, ':norad' => $norad]);
|
||||
if ($stmt->rowCount() > 0) {
|
||||
$updated++;
|
||||
} else {
|
||||
$skipped++;
|
||||
}
|
||||
}
|
||||
|
||||
echo "Fertig.\n";
|
||||
echo " Einträge gesamt: {$total}\n";
|
||||
echo " rcs aktualisiert: {$updated}\n";
|
||||
echo " Nicht in DB: {$skipped}\n";
|
||||
echo " Ohne RCS-Wert: {$noRcs}\n";
|
||||
@@ -0,0 +1,597 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
ini_set('display_errors', '1');
|
||||
error_reporting(E_ALL);
|
||||
session_start();
|
||||
|
||||
$loggedIn = isset($_SESSION['user_id']);
|
||||
$pageTitle = 'Krater-Import - AstroTools';
|
||||
$bodyClass = 'admin-page';
|
||||
|
||||
function h(string $value): string
|
||||
{
|
||||
return htmlspecialchars($value, ENT_QUOTES, 'UTF-8');
|
||||
}
|
||||
|
||||
function normalizeImportValue(?string $value): ?string
|
||||
{
|
||||
if ($value === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$value = trim($value);
|
||||
if ($value === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$value = str_replace(["\xC2\xA0", "\u{00A0}"], ' ', $value);
|
||||
|
||||
if (function_exists('mb_check_encoding') && !mb_check_encoding($value, 'UTF-8')) {
|
||||
$converted = @mb_convert_encoding($value, 'UTF-8', 'Windows-1252,ISO-8859-1,UTF-8');
|
||||
if (is_string($converted) && $converted !== '') {
|
||||
$value = $converted;
|
||||
}
|
||||
}
|
||||
|
||||
return trim($value);
|
||||
}
|
||||
|
||||
function parseNullableFloat(?string $value): ?float
|
||||
{
|
||||
$value = normalizeImportValue($value);
|
||||
if ($value === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$value = str_replace(',', '.', $value);
|
||||
return is_numeric($value) ? (float)$value : null;
|
||||
}
|
||||
|
||||
function parseNullableInt(?string $value): ?int
|
||||
{
|
||||
$value = normalizeImportValue($value);
|
||||
if ($value === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return preg_match('/^-?\d+$/', $value) ? (int)$value : null;
|
||||
}
|
||||
|
||||
function parseNullableDate(?string $value): ?string
|
||||
{
|
||||
$value = normalizeImportValue($value);
|
||||
if ($value === null || stripos($value, 'not') !== false || $value === '?') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$formats = ['d/m/Y H:i:s', 'd/m/Y', 'Y-m-d H:i:s', 'Y-m-d'];
|
||||
foreach ($formats as $format) {
|
||||
$dt = DateTimeImmutable::createFromFormat($format, $value);
|
||||
if ($dt instanceof DateTimeImmutable) {
|
||||
return $dt->format('Y-m-d');
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function translateCatalogValue(string $field, ?string $value): ?string
|
||||
{
|
||||
$value = normalizeImportValue($value);
|
||||
if ($value === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
static $maps = [
|
||||
'iau_type' => [
|
||||
'Crater' => 'Krater',
|
||||
],
|
||||
'type' => [
|
||||
'Crater' => 'Krater',
|
||||
],
|
||||
'subtype' => [
|
||||
'Not determined' => 'Nicht bestimmt',
|
||||
],
|
||||
'process' => [
|
||||
'Meteoritic impact' => 'Meteoriteneinschlag',
|
||||
],
|
||||
'period' => [
|
||||
'Formation period not found' => 'Entstehungszeit nicht bekannt',
|
||||
'Pre-Nectarian (From -4.55 billions years to -3.92 billions years)' => 'Prä-Nektarisch (von -4,55 bis -3,92 Milliarden Jahre)',
|
||||
'Nectarian (From -3.92 billions years to -3.85 billions years)' => 'Nektarisch (von -3,92 bis -3,85 Milliarden Jahre)',
|
||||
'Imbrian (From -3.85 billions years to -3.2 billions years) ?' => 'Imbrisch (von -3,85 bis -3,2 Milliarden Jahre)?',
|
||||
],
|
||||
'geology' => [
|
||||
'No details' => 'Keine Details',
|
||||
],
|
||||
'country' => [
|
||||
'Germany' => 'Deutschland',
|
||||
'Norway' => 'Norwegen',
|
||||
'U. S. A.' => 'USA',
|
||||
'Not appliable' => 'Nicht anwendbar',
|
||||
],
|
||||
'nationality' => [
|
||||
'German' => 'Deutsch',
|
||||
'Norwegian' => 'Norwegisch',
|
||||
'American' => 'Amerikanisch',
|
||||
'Not appliable' => 'Nicht anwendbar',
|
||||
],
|
||||
'face' => [
|
||||
'Nearside' => 'Vorderseite',
|
||||
'Farside' => 'Rückseite',
|
||||
'Librations zone' => 'Librationszone',
|
||||
],
|
||||
'quadrant' => [
|
||||
'North-East' => 'Nordost',
|
||||
'South-East' => 'Südost',
|
||||
'North-West' => 'Nordwest',
|
||||
'South-West' => 'Südwest',
|
||||
],
|
||||
'interest_c' => [
|
||||
'Low interest or difficult formation' => 'Geringes Interesse oder schwer beobachtbar',
|
||||
'Pretty interesting formation' => 'Ziemlich interessante Formation',
|
||||
'Not observable' => 'Nicht beobachtbar',
|
||||
],
|
||||
'moonday_s' => [
|
||||
'Not observable' => 'Nicht beobachtbar',
|
||||
'3 days after New Moon' => '3 Tage nach Neumond',
|
||||
'2 days after New Moon' => '2 Tage nach Neumond',
|
||||
],
|
||||
'moonday_m' => [
|
||||
'Not observable' => 'Nicht beobachtbar',
|
||||
'2 days after Full Moon' => '2 Tage nach Vollmond',
|
||||
'1 day after Full Moon' => '1 Tag nach Vollmond',
|
||||
],
|
||||
'diam_inst' => [
|
||||
'Not observable' => 'Nicht beobachtbar',
|
||||
'Naked eye' => 'Mit bloßem Auge',
|
||||
],
|
||||
'th_instru' => [
|
||||
'Not observable' => 'Nicht beobachtbar',
|
||||
'50 mm refractor' => '50-mm-Refraktor',
|
||||
'10x binoculars' => '10x-Fernglas',
|
||||
],
|
||||
'pr_instru' => [
|
||||
'Not observable' => 'Nicht beobachtbar',
|
||||
'100 mm refractor' => '100-mm-Refraktor',
|
||||
'10x binoculars' => '10x-Fernglas',
|
||||
],
|
||||
];
|
||||
|
||||
return $maps[$field][$value] ?? $value;
|
||||
}
|
||||
|
||||
function createMoonCraterTableSql(): string
|
||||
{
|
||||
return "CREATE TABLE moon_craters (
|
||||
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
lun VARCHAR(64) DEFAULT NULL,
|
||||
name_type VARCHAR(255) DEFAULT NULL,
|
||||
iau_type VARCHAR(100) DEFAULT NULL,
|
||||
iau_type_de VARCHAR(100) DEFAULT NULL,
|
||||
type VARCHAR(100) DEFAULT NULL,
|
||||
type_de VARCHAR(100) DEFAULT NULL,
|
||||
subtype VARCHAR(150) DEFAULT NULL,
|
||||
subtype_de VARCHAR(150) DEFAULT NULL,
|
||||
process VARCHAR(150) DEFAULT NULL,
|
||||
process_de VARCHAR(150) DEFAULT NULL,
|
||||
period VARCHAR(255) DEFAULT NULL,
|
||||
period_de VARCHAR(255) DEFAULT NULL,
|
||||
period_source TEXT DEFAULT NULL,
|
||||
geology VARCHAR(255) DEFAULT NULL,
|
||||
geology_de VARCHAR(255) DEFAULT NULL,
|
||||
name_detail VARCHAR(255) DEFAULT NULL,
|
||||
name_origin VARCHAR(255) DEFAULT NULL,
|
||||
iau_approval_date DATE DEFAULT NULL,
|
||||
langrenus VARCHAR(255) DEFAULT NULL,
|
||||
hevelius VARCHAR(255) DEFAULT NULL,
|
||||
riccioli VARCHAR(255) DEFAULT NULL,
|
||||
work VARCHAR(255) DEFAULT NULL,
|
||||
country VARCHAR(255) DEFAULT NULL,
|
||||
country_de VARCHAR(255) DEFAULT NULL,
|
||||
nationality VARCHAR(255) DEFAULT NULL,
|
||||
nationality_de VARCHAR(255) DEFAULT NULL,
|
||||
century_n VARCHAR(50) DEFAULT NULL,
|
||||
century_c VARCHAR(50) DEFAULT NULL,
|
||||
birth_place VARCHAR(255) DEFAULT NULL,
|
||||
birth_date VARCHAR(100) DEFAULT NULL,
|
||||
death_place VARCHAR(255) DEFAULT NULL,
|
||||
death_date VARCHAR(100) DEFAULT NULL,
|
||||
facts TEXT DEFAULT NULL,
|
||||
longi_n DECIMAL(10,6) DEFAULT NULL,
|
||||
longi_n_360 DECIMAL(10,6) DEFAULT NULL,
|
||||
longi_c VARCHAR(255) DEFAULT NULL,
|
||||
lati_n DECIMAL(10,6) DEFAULT NULL,
|
||||
lati_c VARCHAR(255) DEFAULT NULL,
|
||||
face VARCHAR(100) DEFAULT NULL,
|
||||
face_de VARCHAR(100) DEFAULT NULL,
|
||||
quadrant VARCHAR(100) DEFAULT NULL,
|
||||
quadrant_de VARCHAR(100) DEFAULT NULL,
|
||||
area VARCHAR(255) DEFAULT NULL,
|
||||
length_km DECIMAL(10,3) DEFAULT NULL,
|
||||
wide_km DECIMAL(10,3) DEFAULT NULL,
|
||||
length_arcsec DECIMAL(10,3) DEFAULT NULL,
|
||||
height_m VARCHAR(255) DEFAULT NULL,
|
||||
rapport VARCHAR(255) DEFAULT NULL,
|
||||
general_1 TEXT DEFAULT NULL,
|
||||
general_2 TEXT DEFAULT NULL,
|
||||
slopes TEXT DEFAULT NULL,
|
||||
walls TEXT DEFAULT NULL,
|
||||
floor TEXT DEFAULT NULL,
|
||||
interest_n INT DEFAULT NULL,
|
||||
interest_c VARCHAR(255) DEFAULT NULL,
|
||||
interest_c_de VARCHAR(255) DEFAULT NULL,
|
||||
lunation VARCHAR(100) DEFAULT NULL,
|
||||
moonday_s VARCHAR(255) DEFAULT NULL,
|
||||
moonday_s_de VARCHAR(255) DEFAULT NULL,
|
||||
moonday_m VARCHAR(255) DEFAULT NULL,
|
||||
moonday_m_de VARCHAR(255) DEFAULT NULL,
|
||||
diam_inst VARCHAR(255) DEFAULT NULL,
|
||||
diam_inst_de VARCHAR(255) DEFAULT NULL,
|
||||
th_instru VARCHAR(255) DEFAULT NULL,
|
||||
th_instru_de VARCHAR(255) DEFAULT NULL,
|
||||
pr_instru VARCHAR(255) DEFAULT NULL,
|
||||
pr_instru_de VARCHAR(255) DEFAULT NULL,
|
||||
source_row_json JSON DEFAULT NULL,
|
||||
imported_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uniq_lun (lun),
|
||||
KEY idx_name (name),
|
||||
KEY idx_type (type),
|
||||
KEY idx_face (face),
|
||||
KEY idx_quadrant (quadrant),
|
||||
KEY idx_longi_n_360 (longi_n_360),
|
||||
KEY idx_lati_n (lati_n)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci";
|
||||
}
|
||||
|
||||
function ensureMoonCraterTable(PDO $pdo): void
|
||||
{
|
||||
$tableExists = (bool)$pdo->query("SHOW TABLES LIKE 'moon_craters'")->fetchColumn();
|
||||
|
||||
if (!$tableExists) {
|
||||
$pdo->exec(createMoonCraterTableSql());
|
||||
return;
|
||||
}
|
||||
|
||||
$columns = [];
|
||||
$stmt = $pdo->query("SHOW COLUMNS FROM moon_craters");
|
||||
foreach ($stmt->fetchAll() as $row) {
|
||||
$columns[] = (string)($row['Field'] ?? '');
|
||||
}
|
||||
|
||||
$requiredColumns = ['name', 'lun', 'iau_type', 'type', 'longi_n_360', 'lati_n'];
|
||||
$missingColumns = array_values(array_diff($requiredColumns, $columns));
|
||||
|
||||
if ($missingColumns === []) {
|
||||
return;
|
||||
}
|
||||
|
||||
$backupTable = 'moon_craters_backup_' . date('Ymd_His');
|
||||
$pdo->exec("RENAME TABLE moon_craters TO `" . $backupTable . "`");
|
||||
$pdo->exec(createMoonCraterTableSql());
|
||||
}
|
||||
|
||||
$report = null;
|
||||
$defaultCsv = 'C:\Users\ich\Downloads\01_IAU_NAMED_EN.csv';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$csvPath = trim((string)($_POST['csv_path'] ?? $defaultCsv));
|
||||
$truncateFirst = isset($_POST['truncate_first']) && (string)$_POST['truncate_first'] === '1';
|
||||
$uploadedLabel = null;
|
||||
|
||||
try {
|
||||
if (
|
||||
isset($_FILES['csv_upload']) &&
|
||||
is_array($_FILES['csv_upload']) &&
|
||||
(int)($_FILES['csv_upload']['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_NO_FILE
|
||||
) {
|
||||
$uploadError = (int)($_FILES['csv_upload']['error'] ?? UPLOAD_ERR_OK);
|
||||
if ($uploadError !== UPLOAD_ERR_OK) {
|
||||
throw new RuntimeException('Datei-Upload fehlgeschlagen. Fehlercode: ' . $uploadError);
|
||||
}
|
||||
|
||||
$tmpName = (string)($_FILES['csv_upload']['tmp_name'] ?? '');
|
||||
if ($tmpName === '' || !is_uploaded_file($tmpName)) {
|
||||
throw new RuntimeException('Die hochgeladene Datei ist ungueltig.');
|
||||
}
|
||||
|
||||
$csvPath = $tmpName;
|
||||
$uploadedLabel = (string)($_FILES['csv_upload']['name'] ?? 'hochgeladene Datei');
|
||||
}
|
||||
|
||||
if (!is_file($csvPath)) {
|
||||
throw new RuntimeException('CSV-Datei nicht gefunden: ' . $csvPath);
|
||||
}
|
||||
|
||||
$config = require __DIR__ . '/../../config/database.php';
|
||||
$dsn = sprintf(
|
||||
'mysql:host=%s;dbname=%s;charset=%s',
|
||||
$config['host'],
|
||||
$config['dbname'],
|
||||
$config['charset'] ?? 'utf8mb4'
|
||||
);
|
||||
|
||||
$pdo = new PDO(
|
||||
$dsn,
|
||||
$config['user'],
|
||||
$config['pass'],
|
||||
[
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||
]
|
||||
);
|
||||
|
||||
ensureMoonCraterTable($pdo);
|
||||
|
||||
if ($truncateFirst) {
|
||||
$pdo->exec('TRUNCATE TABLE moon_craters');
|
||||
}
|
||||
|
||||
$handle = fopen($csvPath, 'rb');
|
||||
if ($handle === false) {
|
||||
throw new RuntimeException('CSV-Datei konnte nicht geöffnet werden.');
|
||||
}
|
||||
|
||||
$headers = fgetcsv($handle, 0, ';', '"', '\\');
|
||||
if (!is_array($headers)) {
|
||||
throw new RuntimeException('CSV-Kopfzeile konnte nicht gelesen werden.');
|
||||
}
|
||||
|
||||
$headers = array_map(static fn($value) => strtoupper((string)$value), $headers);
|
||||
|
||||
$insertSql = "INSERT INTO moon_craters (
|
||||
name, lun, name_type, iau_type, iau_type_de, type, type_de, subtype, subtype_de,
|
||||
process, process_de, period, period_de, period_source, geology, geology_de,
|
||||
name_detail, name_origin, iau_approval_date, langrenus, hevelius, riccioli, work,
|
||||
country, country_de, nationality, nationality_de, century_n, century_c, birth_place,
|
||||
birth_date, death_place, death_date, facts, longi_n, longi_n_360, longi_c, lati_n,
|
||||
lati_c, face, face_de, quadrant, quadrant_de, area, length_km, wide_km, length_arcsec,
|
||||
height_m, rapport, general_1, general_2, slopes, walls, floor, interest_n, interest_c,
|
||||
interest_c_de, lunation, moonday_s, moonday_s_de, moonday_m, moonday_m_de, diam_inst,
|
||||
diam_inst_de, th_instru, th_instru_de, pr_instru, pr_instru_de, source_row_json
|
||||
) VALUES (
|
||||
:name, :lun, :name_type, :iau_type, :iau_type_de, :type, :type_de, :subtype, :subtype_de,
|
||||
:process, :process_de, :period, :period_de, :period_source, :geology, :geology_de,
|
||||
:name_detail, :name_origin, :iau_approval_date, :langrenus, :hevelius, :riccioli, :work,
|
||||
:country, :country_de, :nationality, :nationality_de, :century_n, :century_c, :birth_place,
|
||||
:birth_date, :death_place, :death_date, :facts, :longi_n, :longi_n_360, :longi_c, :lati_n,
|
||||
:lati_c, :face, :face_de, :quadrant, :quadrant_de, :area, :length_km, :wide_km, :length_arcsec,
|
||||
:height_m, :rapport, :general_1, :general_2, :slopes, :walls, :floor, :interest_n, :interest_c,
|
||||
:interest_c_de, :lunation, :moonday_s, :moonday_s_de, :moonday_m, :moonday_m_de, :diam_inst,
|
||||
:diam_inst_de, :th_instru, :th_instru_de, :pr_instru, :pr_instru_de, :source_row_json
|
||||
)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
name = VALUES(name),
|
||||
name_type = VALUES(name_type),
|
||||
iau_type = VALUES(iau_type),
|
||||
iau_type_de = VALUES(iau_type_de),
|
||||
type = VALUES(type),
|
||||
type_de = VALUES(type_de),
|
||||
subtype = VALUES(subtype),
|
||||
subtype_de = VALUES(subtype_de),
|
||||
process = VALUES(process),
|
||||
process_de = VALUES(process_de),
|
||||
period = VALUES(period),
|
||||
period_de = VALUES(period_de),
|
||||
period_source = VALUES(period_source),
|
||||
geology = VALUES(geology),
|
||||
geology_de = VALUES(geology_de),
|
||||
name_detail = VALUES(name_detail),
|
||||
name_origin = VALUES(name_origin),
|
||||
iau_approval_date = VALUES(iau_approval_date),
|
||||
langrenus = VALUES(langrenus),
|
||||
hevelius = VALUES(hevelius),
|
||||
riccioli = VALUES(riccioli),
|
||||
work = VALUES(work),
|
||||
country = VALUES(country),
|
||||
country_de = VALUES(country_de),
|
||||
nationality = VALUES(nationality),
|
||||
nationality_de = VALUES(nationality_de),
|
||||
century_n = VALUES(century_n),
|
||||
century_c = VALUES(century_c),
|
||||
birth_place = VALUES(birth_place),
|
||||
birth_date = VALUES(birth_date),
|
||||
death_place = VALUES(death_place),
|
||||
death_date = VALUES(death_date),
|
||||
facts = VALUES(facts),
|
||||
longi_n = VALUES(longi_n),
|
||||
longi_n_360 = VALUES(longi_n_360),
|
||||
longi_c = VALUES(longi_c),
|
||||
lati_n = VALUES(lati_n),
|
||||
lati_c = VALUES(lati_c),
|
||||
face = VALUES(face),
|
||||
face_de = VALUES(face_de),
|
||||
quadrant = VALUES(quadrant),
|
||||
quadrant_de = VALUES(quadrant_de),
|
||||
area = VALUES(area),
|
||||
length_km = VALUES(length_km),
|
||||
wide_km = VALUES(wide_km),
|
||||
length_arcsec = VALUES(length_arcsec),
|
||||
height_m = VALUES(height_m),
|
||||
rapport = VALUES(rapport),
|
||||
general_1 = VALUES(general_1),
|
||||
general_2 = VALUES(general_2),
|
||||
slopes = VALUES(slopes),
|
||||
walls = VALUES(walls),
|
||||
floor = VALUES(floor),
|
||||
interest_n = VALUES(interest_n),
|
||||
interest_c = VALUES(interest_c),
|
||||
interest_c_de = VALUES(interest_c_de),
|
||||
lunation = VALUES(lunation),
|
||||
moonday_s = VALUES(moonday_s),
|
||||
moonday_s_de = VALUES(moonday_s_de),
|
||||
moonday_m = VALUES(moonday_m),
|
||||
moonday_m_de = VALUES(moonday_m_de),
|
||||
diam_inst = VALUES(diam_inst),
|
||||
diam_inst_de = VALUES(diam_inst_de),
|
||||
th_instru = VALUES(th_instru),
|
||||
th_instru_de = VALUES(th_instru_de),
|
||||
pr_instru = VALUES(pr_instru),
|
||||
pr_instru_de = VALUES(pr_instru_de),
|
||||
source_row_json = VALUES(source_row_json)";
|
||||
|
||||
$stmt = $pdo->prepare($insertSql);
|
||||
$imported = 0;
|
||||
$skipped = 0;
|
||||
|
||||
while (($row = fgetcsv($handle, 0, ';', '"', '\\')) !== false) {
|
||||
if ($row === [null] || $row === false) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$assoc = [];
|
||||
foreach ($headers as $index => $header) {
|
||||
$assoc[$header] = $row[$index] ?? null;
|
||||
}
|
||||
|
||||
$name = normalizeImportValue($assoc['NAME'] ?? null);
|
||||
if ($name === null) {
|
||||
$skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
$payload = [
|
||||
'name' => $name,
|
||||
'lun' => normalizeImportValue($assoc['LUN'] ?? null),
|
||||
'name_type' => normalizeImportValue($assoc['NAME_TYPE'] ?? null),
|
||||
'iau_type' => normalizeImportValue($assoc['IAU_TYPE'] ?? null),
|
||||
'iau_type_de' => translateCatalogValue('iau_type', $assoc['IAU_TYPE'] ?? null),
|
||||
'type' => normalizeImportValue($assoc['TYPE'] ?? null),
|
||||
'type_de' => translateCatalogValue('type', $assoc['TYPE'] ?? null),
|
||||
'subtype' => normalizeImportValue($assoc['SUBTYPE'] ?? null),
|
||||
'subtype_de' => translateCatalogValue('subtype', $assoc['SUBTYPE'] ?? null),
|
||||
'process' => normalizeImportValue($assoc['PROCESS'] ?? null),
|
||||
'process_de' => translateCatalogValue('process', $assoc['PROCESS'] ?? null),
|
||||
'period' => normalizeImportValue($assoc['PERIOD'] ?? null),
|
||||
'period_de' => translateCatalogValue('period', $assoc['PERIOD'] ?? null),
|
||||
'period_source' => normalizeImportValue($assoc['PERIOD_SOURCE'] ?? null),
|
||||
'geology' => normalizeImportValue($assoc['GEOLOGY'] ?? null),
|
||||
'geology_de' => translateCatalogValue('geology', $assoc['GEOLOGY'] ?? null),
|
||||
'name_detail' => normalizeImportValue($assoc['NAME_DETAIL'] ?? null),
|
||||
'name_origin' => normalizeImportValue($assoc['NAME_ORIGIN'] ?? null),
|
||||
'iau_approval_date' => parseNullableDate($assoc['IAU_APPROVAL'] ?? null),
|
||||
'langrenus' => normalizeImportValue($assoc['LANGRENUS'] ?? null),
|
||||
'hevelius' => normalizeImportValue($assoc['HEVELIUS'] ?? null),
|
||||
'riccioli' => normalizeImportValue($assoc['RICCIOLI'] ?? null),
|
||||
'work' => normalizeImportValue($assoc['WORK'] ?? null),
|
||||
'country' => normalizeImportValue($assoc['COUNTRY'] ?? null),
|
||||
'country_de' => translateCatalogValue('country', $assoc['COUNTRY'] ?? null),
|
||||
'nationality' => normalizeImportValue($assoc['NATIONLITY'] ?? null),
|
||||
'nationality_de' => translateCatalogValue('nationality', $assoc['NATIONLITY'] ?? null),
|
||||
'century_n' => normalizeImportValue($assoc['CENTURY_N'] ?? null),
|
||||
'century_c' => normalizeImportValue($assoc['CENTURY_C'] ?? null),
|
||||
'birth_place' => normalizeImportValue($assoc['BIRTH_PLACE'] ?? null),
|
||||
'birth_date' => normalizeImportValue($assoc['BIRTH_DATE'] ?? null),
|
||||
'death_place' => normalizeImportValue($assoc['DEATH_PLACE'] ?? null),
|
||||
'death_date' => normalizeImportValue($assoc['DEATH_DATE'] ?? null),
|
||||
'facts' => normalizeImportValue($assoc['FACTS'] ?? null),
|
||||
'longi_n' => parseNullableFloat($assoc['LONGI_N'] ?? null),
|
||||
'longi_n_360' => parseNullableFloat($assoc['LONGI_N_360'] ?? null),
|
||||
'longi_c' => normalizeImportValue($assoc['LONGI_C'] ?? null),
|
||||
'lati_n' => parseNullableFloat($assoc['LATI_N'] ?? null),
|
||||
'lati_c' => normalizeImportValue($assoc['LATI_C'] ?? null),
|
||||
'face' => normalizeImportValue($assoc['FACE'] ?? null),
|
||||
'face_de' => translateCatalogValue('face', $assoc['FACE'] ?? null),
|
||||
'quadrant' => normalizeImportValue($assoc['QUADRANT'] ?? null),
|
||||
'quadrant_de' => translateCatalogValue('quadrant', $assoc['QUADRANT'] ?? null),
|
||||
'area' => normalizeImportValue($assoc['AREA'] ?? null),
|
||||
'length_km' => parseNullableFloat($assoc['LENGTH_KM'] ?? null),
|
||||
'wide_km' => parseNullableFloat($assoc['WIDE_KM'] ?? null),
|
||||
'length_arcsec' => parseNullableFloat($assoc['LENGTH_ARCSEC'] ?? null),
|
||||
'height_m' => normalizeImportValue($assoc['HEIGHT_M'] ?? null),
|
||||
'rapport' => normalizeImportValue($assoc['RAPPORT'] ?? null),
|
||||
'general_1' => normalizeImportValue($assoc['GENERAL_1'] ?? null),
|
||||
'general_2' => normalizeImportValue($assoc['GENERAL_2'] ?? null),
|
||||
'slopes' => normalizeImportValue($assoc['SLOPES'] ?? null),
|
||||
'walls' => normalizeImportValue($assoc['WALLS'] ?? null),
|
||||
'floor' => normalizeImportValue($assoc['FLOOR'] ?? null),
|
||||
'interest_n' => parseNullableInt($assoc['INTEREST_N'] ?? null),
|
||||
'interest_c' => normalizeImportValue($assoc['INTEREST_C'] ?? null),
|
||||
'interest_c_de' => translateCatalogValue('interest_c', $assoc['INTEREST_C'] ?? null),
|
||||
'lunation' => normalizeImportValue($assoc['LUNATION'] ?? null),
|
||||
'moonday_s' => normalizeImportValue($assoc['MOONDAY_S'] ?? null),
|
||||
'moonday_s_de' => translateCatalogValue('moonday_s', $assoc['MOONDAY_S'] ?? null),
|
||||
'moonday_m' => normalizeImportValue($assoc['MOONDAY_M'] ?? null),
|
||||
'moonday_m_de' => translateCatalogValue('moonday_m', $assoc['MOONDAY_M'] ?? null),
|
||||
'diam_inst' => normalizeImportValue($assoc['DIAM_INST'] ?? null),
|
||||
'diam_inst_de' => translateCatalogValue('diam_inst', $assoc['DIAM_INST'] ?? null),
|
||||
'th_instru' => normalizeImportValue($assoc['TH_INSTRU'] ?? null),
|
||||
'th_instru_de' => translateCatalogValue('th_instru', $assoc['TH_INSTRU'] ?? null),
|
||||
'pr_instru' => normalizeImportValue($assoc['PR_INSTRU'] ?? null),
|
||||
'pr_instru_de' => translateCatalogValue('pr_instru', $assoc['PR_INSTRU'] ?? null),
|
||||
'source_row_json' => json_encode($assoc, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
|
||||
];
|
||||
|
||||
$stmt->execute($payload);
|
||||
$imported++;
|
||||
}
|
||||
|
||||
fclose($handle);
|
||||
|
||||
$report = [
|
||||
'ok' => true,
|
||||
'csv_path' => $csvPath,
|
||||
'csv_label' => $uploadedLabel ?? $csvPath,
|
||||
'imported' => $imported,
|
||||
'skipped' => $skipped,
|
||||
'truncate_first' => $truncateFirst,
|
||||
];
|
||||
} catch (Throwable $exception) {
|
||||
$report = [
|
||||
'ok' => false,
|
||||
'error' => $exception->getMessage(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
require __DIR__ . '/../../public/header.php';
|
||||
?>
|
||||
|
||||
<div class="container" style="max-width: 980px;">
|
||||
<section class="card" style="margin-top: 1.25rem;">
|
||||
<h1>Krater-Import</h1>
|
||||
<p class="hint">Importiert die IAU-Kraterdaten aus der CSV in die Tabelle <code>moon_craters</code>. Die Tabelle wird bei Bedarf automatisch angelegt.</p>
|
||||
|
||||
<?php if (is_array($report)): ?>
|
||||
<?php if ($report['ok'] ?? false): ?>
|
||||
<div class="msg-success">
|
||||
Import erfolgreich.<br>
|
||||
Datei: <?= h((string)($report['csv_label'] ?? $report['csv_path'])) ?><br>
|
||||
Importiert/Aktualisiert: <?= h((string)$report['imported']) ?><br>
|
||||
Uebersprungen: <?= h((string)$report['skipped']) ?>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="msg-error">
|
||||
Import fehlgeschlagen: <?= h((string)($report['error'] ?? 'Unbekannter Fehler')) ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
|
||||
<form method="post" enctype="multipart/form-data" class="form-stack" style="margin-top: 1rem;">
|
||||
<div class="form-group">
|
||||
<label for="csv_upload">CSV-Datei auswaehlen</label>
|
||||
<input type="file" id="csv_upload" name="csv_upload" accept=".csv,text/csv">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="csv_path">Oder Server-Pfad zur CSV-Datei</label>
|
||||
<input type="text" id="csv_path" name="csv_path" value="<?= h((string)($_POST['csv_path'] ?? $defaultCsv)) ?>">
|
||||
</div>
|
||||
|
||||
<label class="form-checkbox">
|
||||
<input type="checkbox" name="truncate_first" value="1"<?= isset($_POST['truncate_first']) ? ' checked' : '' ?>>
|
||||
Tabelle vor dem Import leeren
|
||||
</label>
|
||||
|
||||
<div>
|
||||
<button type="submit" class="btn btn-primary">Import starten</button>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<?php require __DIR__ . '/../../public/footer.php'; ?>
|
||||
@@ -0,0 +1,256 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Importiert hyg_v42.csv in die Tabelle star_hipparcos
|
||||
*
|
||||
* Erwartet:
|
||||
* - config/database.php
|
||||
* - CSV-Datei im selben Verzeichnis wie dieses Skript
|
||||
*/
|
||||
|
||||
ini_set('display_errors', '1');
|
||||
error_reporting(E_ALL);
|
||||
date_default_timezone_set('UTC');
|
||||
|
||||
$config = require __DIR__ . '/../../config/database.php';
|
||||
|
||||
$csvFile = __DIR__ . '/hyg_v42.csv';
|
||||
|
||||
$dsn = sprintf(
|
||||
'mysql:host=%s;dbname=%s;charset=%s',
|
||||
$config['host'],
|
||||
$config['dbname'],
|
||||
$config['charset'] ?? 'utf8mb4'
|
||||
);
|
||||
|
||||
$pdo = new PDO(
|
||||
$dsn,
|
||||
$config['user'],
|
||||
$config['pass'],
|
||||
[
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||
]
|
||||
);
|
||||
|
||||
function nullIfEmpty(mixed $value): mixed
|
||||
{
|
||||
if ($value === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (is_string($value)) {
|
||||
$value = trim($value);
|
||||
return $value === '' ? null : $value;
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
function toNullableInt(mixed $value): ?int
|
||||
{
|
||||
$value = nullIfEmpty($value);
|
||||
if ($value === null || !is_numeric((string)$value)) {
|
||||
return null;
|
||||
}
|
||||
return (int)$value;
|
||||
}
|
||||
|
||||
function toNullableFloat(mixed $value): ?float
|
||||
{
|
||||
$value = nullIfEmpty($value);
|
||||
if ($value === null || !is_numeric((string)$value)) {
|
||||
return null;
|
||||
}
|
||||
return (float)$value;
|
||||
}
|
||||
|
||||
function toNullableString(mixed $value): ?string
|
||||
{
|
||||
$value = nullIfEmpty($value);
|
||||
return $value === null ? null : (string)$value;
|
||||
}
|
||||
|
||||
if (!is_file($csvFile)) {
|
||||
exit("CSV-Datei nicht gefunden: {$csvFile}\n");
|
||||
}
|
||||
|
||||
$handle = fopen($csvFile, 'r');
|
||||
if ($handle === false) {
|
||||
exit("CSV-Datei konnte nicht geöffnet werden: {$csvFile}\n");
|
||||
}
|
||||
|
||||
/*
|
||||
* escape explizit setzen, damit keine PHP-8.4+-Deprecation erscheint
|
||||
*/
|
||||
$header = fgetcsv($handle, 0, ',', '"', '');
|
||||
if ($header === false) {
|
||||
fclose($handle);
|
||||
exit("CSV-Datei ist leer oder ungültig.\n");
|
||||
}
|
||||
|
||||
$header = array_map(static fn($v) => trim((string)$v), $header);
|
||||
|
||||
$requiredColumns = ['hip', 'ra', 'dec', 'mag', 'con'];
|
||||
foreach ($requiredColumns as $col) {
|
||||
if (!in_array($col, $header, true)) {
|
||||
fclose($handle);
|
||||
exit("Pflichtspalte fehlt in CSV: {$col}\n");
|
||||
}
|
||||
}
|
||||
|
||||
$sql = "
|
||||
INSERT INTO `star_hipparcos` (
|
||||
`hip`, `hd`, `hr`, `gl`, `bf`, `proper`,
|
||||
`ra`, `dec`, `dist`, `pmra`, `pmdec`, `rv`,
|
||||
`mag`, `absmag`, `spect`, `ci`,
|
||||
`x`, `y`, `z`, `vx`, `vy`, `vz`,
|
||||
`rarad`, `decrad`, `pmrarad`, `pmdecrad`,
|
||||
`bayer`, `flam`, `con`, `comp`, `comp_primary`,
|
||||
`base`, `lum`, `var`, `var_min`, `var_max`,
|
||||
`created_at`, `updated_at`
|
||||
) VALUES (
|
||||
:hip, :hd, :hr, :gl, :bf, :proper,
|
||||
:ra, :dec, :dist, :pmra, :pmdec, :rv,
|
||||
:mag, :absmag, :spect, :ci,
|
||||
:x, :y, :z, :vx, :vy, :vz,
|
||||
:rarad, :decrad, :pmrarad, :pmdecrad,
|
||||
:bayer, :flam, :con, :comp, :comp_primary,
|
||||
:base, :lum, :var, :var_min, :var_max,
|
||||
NOW(), NOW()
|
||||
)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
`hd` = VALUES(`hd`),
|
||||
`hr` = VALUES(`hr`),
|
||||
`gl` = VALUES(`gl`),
|
||||
`bf` = VALUES(`bf`),
|
||||
`proper` = VALUES(`proper`),
|
||||
`ra` = VALUES(`ra`),
|
||||
`dec` = VALUES(`dec`),
|
||||
`dist` = VALUES(`dist`),
|
||||
`pmra` = VALUES(`pmra`),
|
||||
`pmdec` = VALUES(`pmdec`),
|
||||
`rv` = VALUES(`rv`),
|
||||
`mag` = VALUES(`mag`),
|
||||
`absmag` = VALUES(`absmag`),
|
||||
`spect` = VALUES(`spect`),
|
||||
`ci` = VALUES(`ci`),
|
||||
`x` = VALUES(`x`),
|
||||
`y` = VALUES(`y`),
|
||||
`z` = VALUES(`z`),
|
||||
`vx` = VALUES(`vx`),
|
||||
`vy` = VALUES(`vy`),
|
||||
`vz` = VALUES(`vz`),
|
||||
`rarad` = VALUES(`rarad`),
|
||||
`decrad` = VALUES(`decrad`),
|
||||
`pmrarad` = VALUES(`pmrarad`),
|
||||
`pmdecrad` = VALUES(`pmdecrad`),
|
||||
`bayer` = VALUES(`bayer`),
|
||||
`flam` = VALUES(`flam`),
|
||||
`con` = VALUES(`con`),
|
||||
`comp` = VALUES(`comp`),
|
||||
`comp_primary` = VALUES(`comp_primary`),
|
||||
`base` = VALUES(`base`),
|
||||
`lum` = VALUES(`lum`),
|
||||
`var` = VALUES(`var`),
|
||||
`var_min` = VALUES(`var_min`),
|
||||
`var_max` = VALUES(`var_max`),
|
||||
`updated_at` = NOW()
|
||||
";
|
||||
|
||||
$stmt = $pdo->prepare($sql);
|
||||
|
||||
$rowCount = 0;
|
||||
$insertedOrUpdated = 0;
|
||||
$skipped = 0;
|
||||
|
||||
$pdo->beginTransaction();
|
||||
|
||||
try {
|
||||
while (($row = fgetcsv($handle, 0, ',', '"', '')) !== false) {
|
||||
$rowCount++;
|
||||
|
||||
if ($row === [null] || $row === false) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (count($row) !== count($header)) {
|
||||
$skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
$data = array_combine($header, $row);
|
||||
if ($data === false) {
|
||||
$skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
$hip = toNullableInt($data['hip'] ?? null);
|
||||
|
||||
if ($hip === null) {
|
||||
$skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
$stmt->execute([
|
||||
':hip' => $hip,
|
||||
':hd' => toNullableInt($data['hd'] ?? null),
|
||||
':hr' => toNullableInt($data['hr'] ?? null),
|
||||
':gl' => toNullableString($data['gl'] ?? null),
|
||||
':bf' => toNullableString($data['bf'] ?? null),
|
||||
':proper' => toNullableString($data['proper'] ?? null),
|
||||
|
||||
':ra' => toNullableFloat($data['ra'] ?? null),
|
||||
':dec' => toNullableFloat($data['dec'] ?? null),
|
||||
':dist' => toNullableFloat($data['dist'] ?? null),
|
||||
':pmra' => toNullableFloat($data['pmra'] ?? null),
|
||||
':pmdec' => toNullableFloat($data['pmdec'] ?? null),
|
||||
':rv' => toNullableFloat($data['rv'] ?? null),
|
||||
|
||||
':mag' => toNullableFloat($data['mag'] ?? null),
|
||||
':absmag' => toNullableFloat($data['absmag'] ?? null),
|
||||
':spect' => toNullableString($data['spect'] ?? null),
|
||||
':ci' => toNullableFloat($data['ci'] ?? null),
|
||||
|
||||
':x' => toNullableFloat($data['x'] ?? null),
|
||||
':y' => toNullableFloat($data['y'] ?? null),
|
||||
':z' => toNullableFloat($data['z'] ?? null),
|
||||
':vx' => toNullableFloat($data['vx'] ?? null),
|
||||
':vy' => toNullableFloat($data['vy'] ?? null),
|
||||
':vz' => toNullableFloat($data['vz'] ?? null),
|
||||
|
||||
':rarad' => toNullableFloat($data['rarad'] ?? null),
|
||||
':decrad' => toNullableFloat($data['decrad'] ?? null),
|
||||
':pmrarad' => toNullableFloat($data['pmrarad'] ?? null),
|
||||
':pmdecrad' => toNullableFloat($data['pmdecrad'] ?? null),
|
||||
|
||||
':bayer' => toNullableString($data['bayer'] ?? null),
|
||||
':flam' => toNullableString($data['flam'] ?? null),
|
||||
':con' => toNullableString($data['con'] ?? null),
|
||||
':comp' => toNullableInt($data['comp'] ?? null),
|
||||
':comp_primary' => toNullableInt($data['comp_primary'] ?? null),
|
||||
|
||||
':base' => toNullableString($data['base'] ?? null),
|
||||
':lum' => toNullableFloat($data['lum'] ?? null),
|
||||
':var' => toNullableString($data['var'] ?? null),
|
||||
':var_min' => toNullableFloat($data['var_min'] ?? null),
|
||||
':var_max' => toNullableFloat($data['var_max'] ?? null),
|
||||
]);
|
||||
|
||||
$insertedOrUpdated++;
|
||||
}
|
||||
|
||||
$pdo->commit();
|
||||
} catch (Throwable $e) {
|
||||
$pdo->rollBack();
|
||||
fclose($handle);
|
||||
exit("Fehler beim Import: " . $e->getMessage() . "\n");
|
||||
}
|
||||
|
||||
fclose($handle);
|
||||
|
||||
echo "HYG-Import abgeschlossen\n";
|
||||
echo "Verarbeitete Zeilen: {$rowCount}\n";
|
||||
echo "Importiert/Aktualisiert: {$insertedOrUpdated}\n";
|
||||
echo "Übersprungen: {$skipped}\n";
|
||||
@@ -0,0 +1,291 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Minor Planet Center Comet Import
|
||||
* Quelle:
|
||||
* https://www.minorplanetcenter.net/Extended_Files/allcometels.json.gz
|
||||
*
|
||||
* Zieltabelle:
|
||||
* - comets_mpc
|
||||
*/
|
||||
|
||||
ini_set('display_errors', '1');
|
||||
error_reporting(E_ALL);
|
||||
date_default_timezone_set('UTC');
|
||||
|
||||
$config = require __DIR__ . '/../../config/database.php';
|
||||
|
||||
$sourceUrl = 'https://www.minorplanetcenter.net/Extended_Files/allcometels.json.gz';
|
||||
|
||||
$dsn = sprintf(
|
||||
'mysql:host=%s;dbname=%s;charset=%s',
|
||||
$config['host'],
|
||||
$config['dbname'],
|
||||
$config['charset'] ?? 'utf8mb4'
|
||||
);
|
||||
|
||||
$pdo = new PDO(
|
||||
$dsn,
|
||||
$config['user'],
|
||||
$config['pass'],
|
||||
[
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||
]
|
||||
);
|
||||
|
||||
function fetchRemoteJson(string $url): array
|
||||
{
|
||||
$ch = curl_init($url);
|
||||
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_FOLLOWLOCATION => true,
|
||||
CURLOPT_CONNECTTIMEOUT => 20,
|
||||
CURLOPT_TIMEOUT => 300,
|
||||
CURLOPT_USERAGENT => 'AstroMINT MPC Comet Importer/1.0',
|
||||
CURLOPT_HTTPHEADER => [
|
||||
'Accept: application/json',
|
||||
'Accept-Encoding: gzip',
|
||||
],
|
||||
]);
|
||||
|
||||
$response = curl_exec($ch);
|
||||
|
||||
if ($response === false) {
|
||||
throw new RuntimeException('Download fehlgeschlagen: ' . curl_error($ch));
|
||||
}
|
||||
|
||||
$httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
$contentType = (string) curl_getinfo($ch, CURLINFO_CONTENT_TYPE);
|
||||
|
||||
if ($httpCode !== 200) {
|
||||
throw new RuntimeException('HTTP-Fehler beim Abruf: ' . $httpCode);
|
||||
}
|
||||
|
||||
$isGzip = str_ends_with(strtolower($url), '.gz')
|
||||
|| 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
|
||||
{
|
||||
if ($value === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$value = trim((string) $value);
|
||||
return $value === '' ? null : $value;
|
||||
}
|
||||
|
||||
function toNullableInt(mixed $value): ?int
|
||||
{
|
||||
if ($value === null || $value === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!is_numeric($value)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (int) $value;
|
||||
}
|
||||
|
||||
function toNullableFloat(mixed $value): ?float
|
||||
{
|
||||
if ($value === null || $value === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!is_numeric($value)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (float) $value;
|
||||
}
|
||||
|
||||
function requireIntField(array $row, string $fieldName): int
|
||||
{
|
||||
$value = toNullableInt($row[$fieldName] ?? null);
|
||||
if ($value === null) {
|
||||
throw new RuntimeException("Pflichtfeld fehlt oder ist ungültig: {$fieldName}");
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
function requireFloatField(array $row, string $fieldName): float
|
||||
{
|
||||
$value = toNullableFloat($row[$fieldName] ?? null);
|
||||
if ($value === null) {
|
||||
throw new RuntimeException("Pflichtfeld fehlt oder ist ungültig: {$fieldName}");
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
function requireStringField(array $row, string $fieldName): string
|
||||
{
|
||||
$value = toNullableString($row[$fieldName] ?? null);
|
||||
if ($value === null) {
|
||||
throw new RuntimeException("Pflichtfeld fehlt oder ist ungültig: {$fieldName}");
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
$recordsReceived = 0;
|
||||
$recordsInserted = 0;
|
||||
$recordsUpdated = 0;
|
||||
$recordsSkipped = 0;
|
||||
|
||||
try {
|
||||
$rows = fetchRemoteJson($sourceUrl);
|
||||
|
||||
if ($rows === [] || array_is_list($rows) === false) {
|
||||
throw new RuntimeException('Es wurde keine gültige Kometenliste empfangen');
|
||||
}
|
||||
|
||||
$stmtExists = $pdo->prepare("
|
||||
SELECT id
|
||||
FROM comets_mpc
|
||||
WHERE designation_and_name = :designation_and_name
|
||||
LIMIT 1
|
||||
");
|
||||
|
||||
$stmtUpsert = $pdo->prepare("
|
||||
INSERT INTO comets_mpc (
|
||||
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,
|
||||
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);
|
||||
}
|
||||
@@ -0,0 +1,818 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Importiert MPCORB.DAT in die Tabelle minorplanets_mpc.
|
||||
*
|
||||
* Nutzung:
|
||||
* php scripts/import/import_mpc_minorplanets.php
|
||||
* php scripts/import/import_mpc_minorplanets.php C:\pfad\zu\MPCORB.DAT
|
||||
*
|
||||
* Erwartet DB-Zugangsdaten aus config/database.php.
|
||||
*/
|
||||
|
||||
error_reporting(E_ALL);
|
||||
ini_set('display_errors', '1');
|
||||
set_time_limit(0);
|
||||
|
||||
$rootDir = dirname(__DIR__, 2);
|
||||
$config = require $rootDir . DIRECTORY_SEPARATOR . 'config' . DIRECTORY_SEPARATOR . 'database.php';
|
||||
|
||||
if (!is_array($config)) {
|
||||
writeMessage('config/database.php muss ein Array mit DB-Zugangsdaten zurueckgeben.', true);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
$config = normalizeDbConfig($config);
|
||||
|
||||
foreach (['host', 'dbname', 'user', 'pass'] as $requiredKey) {
|
||||
if (!array_key_exists($requiredKey, $config) || $config[$requiredKey] === '') {
|
||||
writeMessage("DB-Konfiguration unvollstaendig: {$requiredKey} fehlt.", true);
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
$defaultDatPath = $rootDir . DIRECTORY_SEPARATOR . 'scripts' . DIRECTORY_SEPARATOR . 'import' . DIRECTORY_SEPARATOR . 'MPCORB.DAT';
|
||||
$datPath = $argv[1] ?? $defaultDatPath;
|
||||
|
||||
if (!is_file($datPath)) {
|
||||
writeMessage("MPCORB.DAT nicht gefunden: {$datPath}", true);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
$dsn = sprintf(
|
||||
'mysql:host=%s;port=%d;dbname=%s;charset=%s',
|
||||
$config['host'],
|
||||
(int) $config['port'],
|
||||
$config['dbname'],
|
||||
$config['charset']
|
||||
);
|
||||
|
||||
$pdo = new PDO(
|
||||
$dsn,
|
||||
$config['user'],
|
||||
$config['pass'],
|
||||
[
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||
]
|
||||
);
|
||||
|
||||
$insertSql = <<<'SQL'
|
||||
INSERT INTO `minorplanets_mpc` (
|
||||
`desig_packed`,
|
||||
`mp_number`,
|
||||
`absolute_magnitude_h`,
|
||||
`slope_g`,
|
||||
`epoch_packed`,
|
||||
`mean_anomaly_deg`,
|
||||
`arg_perihelion_deg`,
|
||||
`ascending_node_deg`,
|
||||
`inclination_deg`,
|
||||
`eccentricity`,
|
||||
`mean_motion_deg_per_day`,
|
||||
`semimajor_axis_au`,
|
||||
`uncertainty_u`,
|
||||
`reference_code`,
|
||||
`num_observations`,
|
||||
`num_oppositions`,
|
||||
`arc_text`,
|
||||
`rms_residual`,
|
||||
`perturbers_1`,
|
||||
`perturbers_2`,
|
||||
`orbit_computer`,
|
||||
`hex_flags`,
|
||||
`designation_text`,
|
||||
`last_obs_compact`,
|
||||
`last_obs_date`,
|
||||
`raw_record`
|
||||
) VALUES (
|
||||
:desig_packed,
|
||||
:mp_number,
|
||||
:absolute_magnitude_h,
|
||||
:slope_g,
|
||||
:epoch_packed,
|
||||
:mean_anomaly_deg,
|
||||
:arg_perihelion_deg,
|
||||
:ascending_node_deg,
|
||||
:inclination_deg,
|
||||
:eccentricity,
|
||||
:mean_motion_deg_per_day,
|
||||
:semimajor_axis_au,
|
||||
:uncertainty_u,
|
||||
:reference_code,
|
||||
:num_observations,
|
||||
:num_oppositions,
|
||||
:arc_text,
|
||||
:rms_residual,
|
||||
:perturbers_1,
|
||||
:perturbers_2,
|
||||
:orbit_computer,
|
||||
:hex_flags,
|
||||
:designation_text,
|
||||
:last_obs_compact,
|
||||
:last_obs_date,
|
||||
:raw_record
|
||||
)
|
||||
SQL;
|
||||
|
||||
$statement = $pdo->prepare($insertSql);
|
||||
$isCli = PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg';
|
||||
$statePath = $rootDir . DIRECTORY_SEPARATOR . 'scripts' . DIRECTORY_SEPARATOR . 'import' . DIRECTORY_SEPARATOR . 'import_mpc_minorplanets.state.json';
|
||||
|
||||
if ($isCli) {
|
||||
runFullImport($pdo, $statement, $datPath);
|
||||
exit(0);
|
||||
}
|
||||
|
||||
runWebImportChunk($pdo, $statement, $datPath, $statePath);
|
||||
exit(0);
|
||||
|
||||
function runFullImport(PDO $pdo, PDOStatement $statement, string $datPath): void
|
||||
{
|
||||
writeMessage('Stelle sicher, dass desig_packed case-sensitive gespeichert wird...');
|
||||
ensureCaseSensitiveDesigPackedColumn($pdo);
|
||||
|
||||
$minorplanetAssignments = backupUserMinorplanetAssignments($pdo);
|
||||
writeMessage('Gesicherte Nutzer-Kleinplaneten-Zuordnungen: ' . count($minorplanetAssignments));
|
||||
|
||||
writeMessage('Leere app_user_minorplanets und minorplanets_mpc und setze AUTO_INCREMENT zurueck...');
|
||||
clearTableWithRetry($pdo, 'app_user_minorplanets');
|
||||
resetAutoIncrementWithRetry($pdo, 'app_user_minorplanets');
|
||||
clearTableWithRetry($pdo, 'minorplanets_mpc');
|
||||
resetAutoIncrementWithRetry($pdo, 'minorplanets_mpc');
|
||||
|
||||
$state = [
|
||||
'processed' => 0,
|
||||
'imported' => 0,
|
||||
'skipped' => 0,
|
||||
'in_data_section' => false,
|
||||
'byte_offset' => 0,
|
||||
'started_at' => microtime(true),
|
||||
'total_data_lines' => countMpcorbDataLines($datPath),
|
||||
'minorplanet_assignments' => $minorplanetAssignments,
|
||||
];
|
||||
|
||||
if ($state['total_data_lines'] > 0) {
|
||||
writeMessage("Import startet. Erwartete Datensaetze: {$state['total_data_lines']}.");
|
||||
} else {
|
||||
writeMessage('Import startet. Gesamtzahl der Datensaetze konnte nicht ermittelt werden.');
|
||||
}
|
||||
|
||||
$result = processImportChunk($pdo, $statement, $datPath, $state, PHP_INT_MAX, 10000, 5000);
|
||||
if (!$result['completed']) {
|
||||
writeMessage('Import wurde unerwartet nicht abgeschlossen.', true);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
writeMessage("Import abgeschlossen. Verarbeitet: {$result['state']['processed']}, importiert: {$result['state']['imported']}, uebersprungen: {$result['state']['skipped']}.");
|
||||
restoreUserMinorplanetAssignments($pdo, $result['state']['minorplanet_assignments'] ?? []);
|
||||
writeDatabaseCountSummary($pdo, $result['state']['imported']);
|
||||
}
|
||||
|
||||
function runWebImportChunk(PDO $pdo, PDOStatement $statement, string $datPath, string $statePath): void
|
||||
{
|
||||
$resetRequested = isset($_GET['reset']) && $_GET['reset'] === '1';
|
||||
$batchLineLimit = max(1000, min(50000, (int) ($_GET['batch'] ?? 20000)));
|
||||
$progressInterval = max(1000, min($batchLineLimit, (int) ($_GET['progress'] ?? 5000)));
|
||||
$commitInterval = max(1000, min($batchLineLimit, (int) ($_GET['commit'] ?? 5000)));
|
||||
|
||||
if ($resetRequested || !is_file($statePath)) {
|
||||
writeMessage('Initialisiere Web-Import...');
|
||||
ensureCaseSensitiveDesigPackedColumn($pdo);
|
||||
$minorplanetAssignments = backupUserMinorplanetAssignments($pdo);
|
||||
writeMessage('Gesicherte Nutzer-Kleinplaneten-Zuordnungen: ' . count($minorplanetAssignments));
|
||||
clearTableWithRetry($pdo, 'app_user_minorplanets');
|
||||
resetAutoIncrementWithRetry($pdo, 'app_user_minorplanets');
|
||||
clearTableWithRetry($pdo, 'minorplanets_mpc');
|
||||
resetAutoIncrementWithRetry($pdo, 'minorplanets_mpc');
|
||||
|
||||
$state = [
|
||||
'processed' => 0,
|
||||
'imported' => 0,
|
||||
'skipped' => 0,
|
||||
'in_data_section' => false,
|
||||
'byte_offset' => 0,
|
||||
'started_at' => microtime(true),
|
||||
'total_data_lines' => countMpcorbDataLines($datPath),
|
||||
'status' => 'running',
|
||||
'minorplanet_assignments' => $minorplanetAssignments,
|
||||
];
|
||||
saveImportState($statePath, $state);
|
||||
writeMessage("Web-Import vorbereitet. Erwartete Datensaetze: {$state['total_data_lines']}.");
|
||||
} else {
|
||||
$state = loadImportState($statePath);
|
||||
if (($state['status'] ?? 'running') === 'completed') {
|
||||
writeMessage('Vorheriger Import ist bereits abgeschlossen. Mit ?reset=1 neu starten.');
|
||||
writeDatabaseCountSummary($pdo, (int) ($state['imported'] ?? 0));
|
||||
return;
|
||||
}
|
||||
writeMessage("Setze Web-Import fort bei Datensatz {$state['processed']}...");
|
||||
}
|
||||
|
||||
$result = processImportChunk($pdo, $statement, $datPath, $state, $batchLineLimit, $progressInterval, $commitInterval);
|
||||
$state = $result['state'];
|
||||
|
||||
if ($result['completed']) {
|
||||
restoreUserMinorplanetAssignments($pdo, $state['minorplanet_assignments'] ?? []);
|
||||
$state['status'] = 'completed';
|
||||
saveImportState($statePath, $state);
|
||||
writeMessage("Import abgeschlossen. Verarbeitet: {$state['processed']}, importiert: {$state['imported']}, uebersprungen: {$state['skipped']}.");
|
||||
writeDatabaseCountSummary($pdo, (int) $state['imported']);
|
||||
writeRestartHint();
|
||||
return;
|
||||
}
|
||||
|
||||
$state['status'] = 'running';
|
||||
saveImportState($statePath, $state);
|
||||
writeMessage("Chunk fertig. Naechster Start bei Datensatz {$state['processed']}.");
|
||||
writeMessage('Naechster Chunk wird automatisch geladen...');
|
||||
writeAutoContinueScript($batchLineLimit, $progressInterval, $commitInterval);
|
||||
}
|
||||
|
||||
function processImportChunk(
|
||||
PDO $pdo,
|
||||
PDOStatement $statement,
|
||||
string $datPath,
|
||||
array $state,
|
||||
int $lineLimit,
|
||||
int $progressInterval,
|
||||
int $commitInterval
|
||||
): array {
|
||||
$handle = fopen($datPath, 'rb');
|
||||
if ($handle === false) {
|
||||
writeMessage("MPCORB.DAT konnte nicht geoeffnet werden: {$datPath}", true);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
if (($state['byte_offset'] ?? 0) > 0) {
|
||||
fseek($handle, (int) $state['byte_offset']);
|
||||
}
|
||||
|
||||
$processedInChunk = 0;
|
||||
$batchProcessed = 0;
|
||||
$completed = true;
|
||||
|
||||
$pdo->beginTransaction();
|
||||
|
||||
try {
|
||||
while (($line = fgets($handle)) !== false) {
|
||||
$state['byte_offset'] = ftell($handle);
|
||||
$line = rtrim($line, "\r\n");
|
||||
|
||||
if ($line === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!($state['in_data_section'] ?? false)) {
|
||||
if (preg_match('/^-{20,}$/', trim($line)) === 1) {
|
||||
$state['in_data_section'] = true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
$state['processed']++;
|
||||
$processedInChunk++;
|
||||
$parseResult = parseMpcorbLine($line);
|
||||
|
||||
if ($parseResult['record'] === null) {
|
||||
$state['skipped']++;
|
||||
writeMessage(formatSkipDetail((int) $state['processed'], $line, $parseResult['designation'], $parseResult['reason']), true);
|
||||
} else {
|
||||
try {
|
||||
$statement->execute($parseResult['record']);
|
||||
$state['imported']++;
|
||||
$batchProcessed++;
|
||||
} catch (Throwable $rowException) {
|
||||
$state['skipped']++;
|
||||
writeMessage(formatSkipDetail(
|
||||
(int) $state['processed'],
|
||||
$line,
|
||||
$parseResult['designation'],
|
||||
'DB-Fehler: ' . $rowException->getMessage()
|
||||
), true);
|
||||
}
|
||||
}
|
||||
|
||||
if ($batchProcessed >= $commitInterval) {
|
||||
$pdo->commit();
|
||||
writeMessage("Zwischenstand gespeichert bei {$state['processed']} verarbeiteten Datensaetzen.");
|
||||
$pdo->beginTransaction();
|
||||
$batchProcessed = 0;
|
||||
}
|
||||
|
||||
if ($state['processed'] % $progressInterval === 0) {
|
||||
writeMessage(buildProgressMessage(
|
||||
(int) $state['processed'],
|
||||
(int) $state['imported'],
|
||||
(int) $state['skipped'],
|
||||
(int) ($state['total_data_lines'] ?? 0),
|
||||
(float) ($state['started_at'] ?? microtime(true))
|
||||
));
|
||||
}
|
||||
|
||||
if ($processedInChunk >= $lineLimit) {
|
||||
$completed = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($pdo->inTransaction()) {
|
||||
$pdo->commit();
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
fclose($handle);
|
||||
if ($pdo->inTransaction()) {
|
||||
$pdo->rollBack();
|
||||
}
|
||||
writeMessage('Importfehler: ' . $e->getMessage(), true);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
if (feof($handle)) {
|
||||
$completed = true;
|
||||
}
|
||||
|
||||
fclose($handle);
|
||||
|
||||
return [
|
||||
'completed' => $completed,
|
||||
'state' => $state,
|
||||
];
|
||||
}
|
||||
|
||||
function loadImportState(string $statePath): array
|
||||
{
|
||||
$json = @file_get_contents($statePath);
|
||||
$data = is_string($json) ? json_decode($json, true) : null;
|
||||
|
||||
if (!is_array($data)) {
|
||||
return [
|
||||
'processed' => 0,
|
||||
'imported' => 0,
|
||||
'skipped' => 0,
|
||||
'in_data_section' => false,
|
||||
'byte_offset' => 0,
|
||||
'started_at' => microtime(true),
|
||||
'total_data_lines' => 0,
|
||||
'status' => 'running',
|
||||
];
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
function saveImportState(string $statePath, array $state): void
|
||||
{
|
||||
file_put_contents(
|
||||
$statePath,
|
||||
json_encode($state, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES),
|
||||
LOCK_EX
|
||||
);
|
||||
}
|
||||
|
||||
function writeDatabaseCountSummary(PDO $pdo, int $imported): void
|
||||
{
|
||||
$dbRowCount = (int) $pdo->query('SELECT COUNT(*) FROM `minorplanets_mpc`')->fetchColumn();
|
||||
writeMessage("Kontrolle Datenbank: minorplanets_mpc enthaelt {$dbRowCount} Zeilen.");
|
||||
|
||||
if ($dbRowCount !== $imported) {
|
||||
writeMessage(
|
||||
"WARNUNG: Importzaehler ({$imported}) und echte Datenbankzeilen ({$dbRowCount}) unterscheiden sich.",
|
||||
true
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function backupUserMinorplanetAssignments(PDO $pdo): array
|
||||
{
|
||||
$sql = <<<'SQL'
|
||||
SELECT
|
||||
aum.`user_id`,
|
||||
aum.`is_favorite`,
|
||||
aum.`created_at`,
|
||||
mp.`desig_packed`
|
||||
FROM `app_user_minorplanets` aum
|
||||
INNER JOIN `minorplanets_mpc` mp
|
||||
ON mp.`id` = aum.`minorplanet_id`
|
||||
ORDER BY aum.`user_id` ASC, aum.`minorplanet_id` ASC
|
||||
SQL;
|
||||
|
||||
return $pdo->query($sql)->fetchAll() ?: [];
|
||||
}
|
||||
|
||||
function restoreUserMinorplanetAssignments(PDO $pdo, array $assignments): void
|
||||
{
|
||||
if ($assignments === []) {
|
||||
writeMessage('Keine Nutzer-Kleinplaneten-Zuordnungen zum Wiederherstellen vorhanden.');
|
||||
return;
|
||||
}
|
||||
|
||||
$lookupStatement = $pdo->prepare('SELECT `id` FROM `minorplanets_mpc` WHERE `desig_packed` = :desig_packed LIMIT 1');
|
||||
$insertStatement = $pdo->prepare(
|
||||
'INSERT INTO `app_user_minorplanets` (`user_id`, `minorplanet_id`, `is_favorite`, `created_at`)
|
||||
VALUES (:user_id, :minorplanet_id, :is_favorite, :created_at)'
|
||||
);
|
||||
|
||||
$restored = 0;
|
||||
$missing = 0;
|
||||
|
||||
foreach ($assignments as $assignment) {
|
||||
$desigPacked = (string) ($assignment['desig_packed'] ?? '');
|
||||
if ($desigPacked === '') {
|
||||
$missing++;
|
||||
continue;
|
||||
}
|
||||
|
||||
$lookupStatement->execute([
|
||||
':desig_packed' => $desigPacked,
|
||||
]);
|
||||
$minorplanetId = $lookupStatement->fetchColumn();
|
||||
|
||||
if ($minorplanetId === false) {
|
||||
$missing++;
|
||||
writeMessage("Zuordnung konnte nicht wiederhergestellt werden: {$desigPacked}", true);
|
||||
continue;
|
||||
}
|
||||
|
||||
$insertStatement->execute([
|
||||
':user_id' => (int) $assignment['user_id'],
|
||||
':minorplanet_id' => (int) $minorplanetId,
|
||||
':is_favorite' => (int) $assignment['is_favorite'],
|
||||
':created_at' => (string) $assignment['created_at'],
|
||||
]);
|
||||
$restored++;
|
||||
}
|
||||
|
||||
writeMessage("Nutzer-Kleinplaneten-Zuordnungen wiederhergestellt: {$restored}.");
|
||||
if ($missing > 0) {
|
||||
writeMessage("Nicht wiederherstellbare Zuordnungen: {$missing}.", true);
|
||||
}
|
||||
}
|
||||
|
||||
function writeAutoContinueScript(int $batchLineLimit, int $progressInterval, int $commitInterval): void
|
||||
{
|
||||
$query = http_build_query([
|
||||
'batch' => $batchLineLimit,
|
||||
'progress' => $progressInterval,
|
||||
'commit' => $commitInterval,
|
||||
]);
|
||||
|
||||
echo '<script>setTimeout(function(){ window.location.search = ' . json_encode('?' . $query) . '; }, 800);</script>' . "\n";
|
||||
echo '<noscript><p><a href="?' . htmlspecialchars($query, ENT_QUOTES, 'UTF-8') . '">Naechsten Chunk starten</a></p></noscript>' . "\n";
|
||||
}
|
||||
|
||||
function writeRestartHint(): void
|
||||
{
|
||||
echo '<p><a href="?reset=1">Import neu starten</a></p>' . "\n";
|
||||
}
|
||||
|
||||
function countMpcorbDataLines(string $datPath): int
|
||||
{
|
||||
$handle = fopen($datPath, 'rb');
|
||||
if ($handle === false) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$inDataSection = false;
|
||||
$count = 0;
|
||||
|
||||
while (($line = fgets($handle)) !== false) {
|
||||
$line = rtrim($line, "\r\n");
|
||||
|
||||
if ($line === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!$inDataSection) {
|
||||
if (preg_match('/^-{20,}$/', trim($line)) === 1) {
|
||||
$inDataSection = true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
$count++;
|
||||
}
|
||||
|
||||
fclose($handle);
|
||||
return $count;
|
||||
}
|
||||
|
||||
function clearTableWithRetry(PDO $pdo, string $tableName, int $maxAttempts = 5, int $waitMilliseconds = 1500): void
|
||||
{
|
||||
$sql = "DELETE FROM `{$tableName}`";
|
||||
|
||||
for ($attempt = 1; $attempt <= $maxAttempts; $attempt++) {
|
||||
try {
|
||||
$pdo->exec($sql);
|
||||
return;
|
||||
} catch (PDOException $e) {
|
||||
if (!isLockTimeoutException($e) || $attempt === $maxAttempts) {
|
||||
throw $e;
|
||||
}
|
||||
|
||||
writeMessage("Warte auf Sperre beim Leeren von {$tableName} (Versuch {$attempt}/{$maxAttempts})...", true);
|
||||
usleep($waitMilliseconds * 1000);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function ensureCaseSensitiveDesigPackedColumn(PDO $pdo): void
|
||||
{
|
||||
$sql = <<<'SQL'
|
||||
ALTER TABLE `minorplanets_mpc`
|
||||
MODIFY `desig_packed` VARCHAR(16)
|
||||
CHARACTER SET utf8mb4
|
||||
COLLATE utf8mb4_bin
|
||||
NOT NULL
|
||||
COMMENT 'Spalte Des''n aus MPCORB.DAT, z. B. 00001 oder K07Tf8A'
|
||||
SQL;
|
||||
|
||||
$pdo->exec($sql);
|
||||
}
|
||||
|
||||
function resetAutoIncrementWithRetry(PDO $pdo, string $tableName, int $maxAttempts = 5, int $waitMilliseconds = 1500): void
|
||||
{
|
||||
$sql = "ALTER TABLE `{$tableName}` AUTO_INCREMENT = 1";
|
||||
|
||||
for ($attempt = 1; $attempt <= $maxAttempts; $attempt++) {
|
||||
try {
|
||||
$pdo->exec($sql);
|
||||
return;
|
||||
} catch (PDOException $e) {
|
||||
if (!isLockTimeoutException($e) || $attempt === $maxAttempts) {
|
||||
throw $e;
|
||||
}
|
||||
|
||||
writeMessage("Warte auf Sperre beim Zuruecksetzen von AUTO_INCREMENT fuer {$tableName} (Versuch {$attempt}/{$maxAttempts})...", true);
|
||||
usleep($waitMilliseconds * 1000);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function isLockTimeoutException(PDOException $e): bool
|
||||
{
|
||||
$message = $e->getMessage();
|
||||
return str_contains($message, '1205') || stripos($message, 'Lock wait timeout exceeded') !== false;
|
||||
}
|
||||
|
||||
function buildProgressMessage(int $processed, int $imported, int $skipped, int $totalDataLines, float $startedAt): string
|
||||
{
|
||||
$elapsedSeconds = max(microtime(true) - $startedAt, 0.001);
|
||||
$rate = $processed / $elapsedSeconds;
|
||||
$percentage = $totalDataLines > 0 ? ($processed / $totalDataLines) * 100 : null;
|
||||
$remainingSeconds = ($totalDataLines > 0 && $rate > 0)
|
||||
? max(($totalDataLines - $processed) / $rate, 0)
|
||||
: null;
|
||||
|
||||
$parts = [
|
||||
'Status',
|
||||
"verarbeitet {$processed}" . ($totalDataLines > 0 ? " von {$totalDataLines}" : ''),
|
||||
];
|
||||
|
||||
if ($percentage !== null) {
|
||||
$parts[] = sprintf('%.2f %%', $percentage);
|
||||
}
|
||||
|
||||
$parts[] = "importiert {$imported}";
|
||||
$parts[] = "uebersprungen {$skipped}";
|
||||
$parts[] = sprintf('%.0f Datensaetze/s', $rate);
|
||||
$parts[] = 'Laufzeit ' . formatDuration($elapsedSeconds);
|
||||
|
||||
if ($remainingSeconds !== null) {
|
||||
$parts[] = 'Rest ca. ' . formatDuration($remainingSeconds);
|
||||
}
|
||||
|
||||
return implode(' | ', $parts);
|
||||
}
|
||||
|
||||
function formatDuration(float $seconds): string
|
||||
{
|
||||
$seconds = (int) round($seconds);
|
||||
$hours = intdiv($seconds, 3600);
|
||||
$minutes = intdiv($seconds % 3600, 60);
|
||||
$secs = $seconds % 60;
|
||||
|
||||
if ($hours > 0) {
|
||||
return sprintf('%02d:%02d:%02d', $hours, $minutes, $secs);
|
||||
}
|
||||
|
||||
return sprintf('%02d:%02d', $minutes, $secs);
|
||||
}
|
||||
|
||||
function parseMpcorbLine(string $line): array
|
||||
{
|
||||
$trimmed = rtrim($line);
|
||||
$padded = str_pad($trimmed, 202, ' ');
|
||||
|
||||
$desigPacked = readFixedField($padded, 1, 7);
|
||||
$epochPacked = readFixedField($padded, 21, 25);
|
||||
|
||||
if ($desigPacked === null || $epochPacked === null) {
|
||||
return [
|
||||
'record' => null,
|
||||
'designation' => guessDesignationFromRawLine($trimmed),
|
||||
'reason' => 'Pflichtfelder in den festen MPC-Spalten fehlen',
|
||||
];
|
||||
}
|
||||
|
||||
$designationText = readFixedField($padded, 167, 194);
|
||||
|
||||
return [
|
||||
'record' => [
|
||||
':desig_packed' => $desigPacked,
|
||||
':mp_number' => extractMinorPlanetNumber((string) $designationText, $desigPacked),
|
||||
':absolute_magnitude_h' => toNullableDecimal((string) readFixedField($padded, 9, 13)),
|
||||
':slope_g' => toNullableDecimal((string) readFixedField($padded, 15, 19)),
|
||||
':epoch_packed' => $epochPacked,
|
||||
':mean_anomaly_deg' => (float) readFixedField($padded, 27, 35),
|
||||
':arg_perihelion_deg' => (float) readFixedField($padded, 38, 46),
|
||||
':ascending_node_deg' => (float) readFixedField($padded, 49, 57),
|
||||
':inclination_deg' => (float) readFixedField($padded, 60, 68),
|
||||
':eccentricity' => (float) readFixedField($padded, 71, 79),
|
||||
':mean_motion_deg_per_day' => (float) readFixedField($padded, 81, 91),
|
||||
':semimajor_axis_au' => (float) readFixedField($padded, 93, 103),
|
||||
':uncertainty_u' => nullableTrim(readFixedField($padded, 106, 106)),
|
||||
':reference_code' => nullableTrim(readFixedField($padded, 108, 116)),
|
||||
':num_observations' => toNullableInt(readFixedField($padded, 118, 122)),
|
||||
':num_oppositions' => toNullableInt(readFixedField($padded, 124, 126)),
|
||||
':arc_text' => nullableTrim(readFixedField($padded, 128, 136)),
|
||||
':rms_residual' => toNullableDecimal((string) readFixedField($padded, 138, 141)),
|
||||
':perturbers_1' => nullableTrim(readFixedField($padded, 143, 145)),
|
||||
':perturbers_2' => nullableTrim(readFixedField($padded, 147, 149)),
|
||||
':orbit_computer' => nullableTrim(readFixedField($padded, 151, 160)),
|
||||
':hex_flags' => nullableTrim(readFixedField($padded, 162, 165)),
|
||||
':designation_text' => $designationText,
|
||||
':last_obs_compact' => readFixedField($padded, 195, 202),
|
||||
':last_obs_date' => compactDateToSql(readFixedField($padded, 195, 202)),
|
||||
':raw_record' => $trimmed,
|
||||
],
|
||||
'designation' => $designationText ?? guessDesignationFromRawLine($trimmed),
|
||||
'reason' => null,
|
||||
];
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
$value = trim($value);
|
||||
return $value === '' ? null : $value;
|
||||
}
|
||||
|
||||
function guessDesignationFromRawLine(string $line): string
|
||||
{
|
||||
if ($line === '') {
|
||||
return '[leere Zeile]';
|
||||
}
|
||||
|
||||
if (preg_match('/\((\d+)\)\s+([^\r\n]+?)\s+\d{8}$/', $line, $matches) === 1) {
|
||||
return '(' . $matches[1] . ') ' . trim($matches[2]);
|
||||
}
|
||||
|
||||
if (preg_match('/([A-Za-z0-9\-\(\)\/ ]+)\s+\d{8}$/', $line, $matches) === 1) {
|
||||
return trim($matches[1]);
|
||||
}
|
||||
|
||||
return substr($line, 0, 80);
|
||||
}
|
||||
|
||||
function formatSkipDetail(int $lineNumber, string $rawLine, ?string $designation, ?string $reason): string
|
||||
{
|
||||
$objectLabel = $designation !== null && trim($designation) !== ''
|
||||
? trim($designation)
|
||||
: guessDesignationFromRawLine(rtrim($rawLine));
|
||||
|
||||
$reasonText = $reason !== null && trim($reason) !== ''
|
||||
? trim($reason)
|
||||
: 'Unbekannter Grund';
|
||||
|
||||
return "Datensatz {$lineNumber}: {$objectLabel} -> {$reasonText}";
|
||||
}
|
||||
|
||||
function extractMinorPlanetNumber(string $designationText, string $desigPacked): ?int
|
||||
{
|
||||
if (preg_match('/^\((\d+)\)/', $designationText, $matches) === 1) {
|
||||
return (int) $matches[1];
|
||||
}
|
||||
|
||||
if (ctype_digit($desigPacked)) {
|
||||
return (int) ltrim($desigPacked, '0');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function compactDateToSql(?string $value): ?string
|
||||
{
|
||||
if ($value === null || preg_match('/^\d{8}$/', $value) !== 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$year = substr($value, 0, 4);
|
||||
$month = substr($value, 4, 2);
|
||||
$day = substr($value, 6, 2);
|
||||
|
||||
if (!checkdate((int) $month, (int) $day, (int) $year)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $year . '-' . $month . '-' . $day;
|
||||
}
|
||||
|
||||
function toNullableDecimal(string $value): ?float
|
||||
{
|
||||
$value = trim($value);
|
||||
|
||||
if ($value === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return is_numeric($value) ? (float) $value : null;
|
||||
}
|
||||
|
||||
function toNullableInt(?string $value): ?int
|
||||
{
|
||||
if ($value === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$value = trim($value);
|
||||
if ($value === '' || !preg_match('/^-?\d+$/', $value)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (int) $value;
|
||||
}
|
||||
|
||||
function nullableTrim(?string $value): ?string
|
||||
{
|
||||
if ($value === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$value = trim($value);
|
||||
return $value === '' ? null : $value;
|
||||
}
|
||||
|
||||
function normalizeDbConfig(array $config): array
|
||||
{
|
||||
if (isset($config['password']) && !isset($config['pass'])) {
|
||||
$config['pass'] = $config['password'];
|
||||
}
|
||||
|
||||
if (isset($config['database']) && !isset($config['dbname'])) {
|
||||
$config['dbname'] = $config['database'];
|
||||
}
|
||||
|
||||
if (isset($config['db']) && !isset($config['dbname'])) {
|
||||
$config['dbname'] = $config['db'];
|
||||
}
|
||||
|
||||
if (!isset($config['port'])) {
|
||||
$config['port'] = 3306;
|
||||
}
|
||||
|
||||
if (!isset($config['charset'])) {
|
||||
$config['charset'] = 'utf8mb4';
|
||||
}
|
||||
|
||||
return $config;
|
||||
}
|
||||
|
||||
function writeMessage(string $message, bool $isError = false): void
|
||||
{
|
||||
$line = $message . PHP_EOL;
|
||||
$isCli = PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg';
|
||||
|
||||
if ($isCli && $isError && defined('STDERR')) {
|
||||
fwrite(STDERR, $line);
|
||||
if (function_exists('fflush')) {
|
||||
fflush(STDERR);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if ($isCli && !$isError && defined('STDOUT')) {
|
||||
fwrite(STDOUT, $line);
|
||||
if (function_exists('fflush')) {
|
||||
fflush(STDOUT);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
echo htmlspecialchars($message, ENT_QUOTES, 'UTF-8') . "<br>\n";
|
||||
|
||||
if (function_exists('ob_flush')) {
|
||||
@ob_flush();
|
||||
}
|
||||
if (function_exists('flush')) {
|
||||
flush();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,329 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
ini_set('display_errors', '1');
|
||||
error_reporting(E_ALL);
|
||||
date_default_timezone_set('UTC');
|
||||
|
||||
$config = require __DIR__ . '/../../config/database.php';
|
||||
$csvFile = __DIR__ . '/deepsky.csv';
|
||||
|
||||
$dsn = sprintf(
|
||||
'mysql:host=%s;dbname=%s;charset=%s',
|
||||
$config['host'],
|
||||
$config['dbname'],
|
||||
$config['charset'] ?? 'utf8mb4'
|
||||
);
|
||||
|
||||
$pdo = new PDO(
|
||||
$dsn,
|
||||
$config['user'],
|
||||
$config['pass'],
|
||||
[
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||
]
|
||||
);
|
||||
|
||||
function normalizeHeader(string $value): string
|
||||
{
|
||||
$value = trim($value);
|
||||
$value = strtolower($value);
|
||||
|
||||
$map = [
|
||||
' - ' => '_',
|
||||
'-' => '_',
|
||||
' ' => '_',
|
||||
'(' => '',
|
||||
')' => '',
|
||||
'.' => '',
|
||||
'/' => '_',
|
||||
];
|
||||
|
||||
$value = strtr($value, $map);
|
||||
$value = preg_replace('/[^a-z0-9_]+/', '', $value);
|
||||
$value = preg_replace('/_+/', '_', $value);
|
||||
|
||||
return trim((string)$value, '_');
|
||||
}
|
||||
|
||||
function nullIfEmpty(mixed $value): mixed
|
||||
{
|
||||
if ($value === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (is_string($value)) {
|
||||
$value = trim($value);
|
||||
return $value === '' ? null : $value;
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
function toNullableString(mixed $value): ?string
|
||||
{
|
||||
$value = nullIfEmpty($value);
|
||||
return $value === null ? null : (string)$value;
|
||||
}
|
||||
|
||||
function toNullableFloat(mixed $value): ?float
|
||||
{
|
||||
$value = nullIfEmpty($value);
|
||||
if ($value === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$value = str_replace(',', '.', (string)$value);
|
||||
return is_numeric($value) ? (float)$value : null;
|
||||
}
|
||||
|
||||
function hmsToDegrees(?string $ra): ?float
|
||||
{
|
||||
$ra = toNullableString($ra);
|
||||
if ($ra === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!preg_match('/^\s*(\d{1,2}):(\d{1,2}):(\d{1,2}(?:\.\d+)?)\s*$/', $ra, $m)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$hours = (float)$m[1];
|
||||
$minutes = (float)$m[2];
|
||||
$seconds = (float)$m[3];
|
||||
|
||||
$hoursDecimal = $hours + ($minutes / 60.0) + ($seconds / 3600.0);
|
||||
return $hoursDecimal * 15.0;
|
||||
}
|
||||
|
||||
function dmsToDegrees(?string $dec): ?float
|
||||
{
|
||||
$dec = toNullableString($dec);
|
||||
if ($dec === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!preg_match('/^\s*([+\-]?)(\d{1,2}):(\d{1,2}):(\d{1,2}(?:\.\d+)?)\s*$/', $dec, $m)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$sign = ($m[1] === '-') ? -1.0 : 1.0;
|
||||
$degrees = (float)$m[2];
|
||||
$minutes = (float)$m[3];
|
||||
$seconds = (float)$m[4];
|
||||
|
||||
$value = $degrees + ($minutes / 60.0) + ($seconds / 3600.0);
|
||||
return $sign * $value;
|
||||
}
|
||||
|
||||
if (!is_file($csvFile)) {
|
||||
exit("CSV-Datei nicht gefunden: {$csvFile}\n");
|
||||
}
|
||||
|
||||
$handle = fopen($csvFile, 'r');
|
||||
if ($handle === false) {
|
||||
exit("CSV-Datei konnte nicht geöffnet werden: {$csvFile}\n");
|
||||
}
|
||||
|
||||
$header = fgetcsv($handle, 0, ';', '"', '');
|
||||
if ($header === false) {
|
||||
fclose($handle);
|
||||
exit("CSV-Datei ist leer oder ungültig.\n");
|
||||
}
|
||||
|
||||
$header = array_map(
|
||||
static fn($value) => normalizeHeader((string)$value),
|
||||
$header
|
||||
);
|
||||
|
||||
$sql = "
|
||||
INSERT INTO `dso_objects` (
|
||||
`catalog_group`,
|
||||
`name`,
|
||||
`messier`,
|
||||
`ngc`,
|
||||
`ic`,
|
||||
`object_type_abrev`,
|
||||
`object_type`,
|
||||
`ra`,
|
||||
`dec`,
|
||||
`ra_deg`,
|
||||
`dec_deg`,
|
||||
`constellation`,
|
||||
`major_axis`,
|
||||
`minor_axis`,
|
||||
`position_angle`,
|
||||
`b_mag`,
|
||||
`v_mag`,
|
||||
`j_mag`,
|
||||
`h_mag`,
|
||||
`k_mag`,
|
||||
`surface_brightness`,
|
||||
`hubble_type`,
|
||||
`cstar_u_mag`,
|
||||
`cstar_b_mag`,
|
||||
`cstar_v_mag`,
|
||||
`cstar_names`,
|
||||
`identifiers`,
|
||||
`common_names`,
|
||||
`ned_notes`,
|
||||
`openngc_notes`,
|
||||
`image`,
|
||||
`created_at`,
|
||||
`updated_at`
|
||||
) VALUES (
|
||||
:catalog_group,
|
||||
:name,
|
||||
:messier,
|
||||
:ngc,
|
||||
:ic,
|
||||
:object_type_abrev,
|
||||
:object_type,
|
||||
:ra,
|
||||
:dec,
|
||||
:ra_deg,
|
||||
:dec_deg,
|
||||
:constellation,
|
||||
:major_axis,
|
||||
:minor_axis,
|
||||
:position_angle,
|
||||
:b_mag,
|
||||
:v_mag,
|
||||
:j_mag,
|
||||
:h_mag,
|
||||
:k_mag,
|
||||
:surface_brightness,
|
||||
:hubble_type,
|
||||
:cstar_u_mag,
|
||||
:cstar_b_mag,
|
||||
:cstar_v_mag,
|
||||
:cstar_names,
|
||||
:identifiers,
|
||||
:common_names,
|
||||
:ned_notes,
|
||||
:openngc_notes,
|
||||
:image,
|
||||
NOW(),
|
||||
NOW()
|
||||
)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
`catalog_group` = VALUES(`catalog_group`),
|
||||
`messier` = VALUES(`messier`),
|
||||
`ngc` = VALUES(`ngc`),
|
||||
`ic` = VALUES(`ic`),
|
||||
`object_type_abrev` = VALUES(`object_type_abrev`),
|
||||
`object_type` = VALUES(`object_type`),
|
||||
`ra` = VALUES(`ra`),
|
||||
`dec` = VALUES(`dec`),
|
||||
`ra_deg` = VALUES(`ra_deg`),
|
||||
`dec_deg` = VALUES(`dec_deg`),
|
||||
`constellation` = VALUES(`constellation`),
|
||||
`major_axis` = VALUES(`major_axis`),
|
||||
`minor_axis` = VALUES(`minor_axis`),
|
||||
`position_angle` = VALUES(`position_angle`),
|
||||
`b_mag` = VALUES(`b_mag`),
|
||||
`v_mag` = VALUES(`v_mag`),
|
||||
`j_mag` = VALUES(`j_mag`),
|
||||
`h_mag` = VALUES(`h_mag`),
|
||||
`k_mag` = VALUES(`k_mag`),
|
||||
`surface_brightness` = VALUES(`surface_brightness`),
|
||||
`hubble_type` = VALUES(`hubble_type`),
|
||||
`cstar_u_mag` = VALUES(`cstar_u_mag`),
|
||||
`cstar_b_mag` = VALUES(`cstar_b_mag`),
|
||||
`cstar_v_mag` = VALUES(`cstar_v_mag`),
|
||||
`cstar_names` = VALUES(`cstar_names`),
|
||||
`identifiers` = VALUES(`identifiers`),
|
||||
`common_names` = VALUES(`common_names`),
|
||||
`ned_notes` = VALUES(`ned_notes`),
|
||||
`openngc_notes` = VALUES(`openngc_notes`),
|
||||
`image` = VALUES(`image`),
|
||||
`updated_at` = NOW()
|
||||
";
|
||||
|
||||
$stmt = $pdo->prepare($sql);
|
||||
|
||||
$rowCount = 0;
|
||||
$imported = 0;
|
||||
$skipped = 0;
|
||||
|
||||
$pdo->beginTransaction();
|
||||
|
||||
try {
|
||||
while (($row = fgetcsv($handle, 0, ';', '"', '')) !== false) {
|
||||
$rowCount++;
|
||||
|
||||
if ($row === [null] || $row === false) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (count($row) !== count($header)) {
|
||||
$skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
$data = array_combine($header, $row);
|
||||
if ($data === false) {
|
||||
$skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
$name = toNullableString($data['name'] ?? null);
|
||||
if ($name === null) {
|
||||
$skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
$ra = toNullableString($data['ra'] ?? null);
|
||||
$dec = toNullableString($data['dec'] ?? null);
|
||||
|
||||
$stmt->execute([
|
||||
':catalog_group' => toNullableString($data['ic_ngc'] ?? null),
|
||||
':name' => $name,
|
||||
':messier' => toNullableString($data['messier'] ?? null),
|
||||
':ngc' => toNullableString($data['ngc'] ?? null),
|
||||
':ic' => toNullableString($data['ic'] ?? null),
|
||||
':object_type_abrev' => toNullableString($data['object_type_abrev'] ?? null),
|
||||
':object_type' => toNullableString($data['object_type'] ?? null),
|
||||
':ra' => $ra,
|
||||
':dec' => $dec,
|
||||
':ra_deg' => hmsToDegrees($ra),
|
||||
':dec_deg' => dmsToDegrees($dec),
|
||||
':constellation' => toNullableString($data['constellation'] ?? null),
|
||||
':major_axis' => toNullableFloat($data['major_axis'] ?? null),
|
||||
':minor_axis' => toNullableFloat($data['minor_axis'] ?? null),
|
||||
':position_angle' => toNullableFloat($data['position_angle'] ?? null),
|
||||
':b_mag' => toNullableFloat($data['b_mag'] ?? null),
|
||||
':v_mag' => toNullableFloat($data['v_mag'] ?? null),
|
||||
':j_mag' => toNullableFloat($data['j_mag'] ?? null),
|
||||
':h_mag' => toNullableFloat($data['h_mag'] ?? null),
|
||||
':k_mag' => toNullableFloat($data['k_mag'] ?? null),
|
||||
':surface_brightness' => toNullableFloat($data['surface_brigthness'] ?? null),
|
||||
':hubble_type' => toNullableString($data['hubble_only_galaxies'] ?? null),
|
||||
':cstar_u_mag' => toNullableFloat($data['cstar_u_mag_only_planetary_nebulae'] ?? null),
|
||||
':cstar_b_mag' => toNullableFloat($data['cstar_b_mag_only_planetary_nebulae'] ?? null),
|
||||
':cstar_v_mag' => toNullableFloat($data['cstar_v_mag_only_planetary_nebulae'] ?? null),
|
||||
':cstar_names' => toNullableString($data['cstar_names_only_planetary_nebulae'] ?? null),
|
||||
':identifiers' => toNullableString($data['identifiers'] ?? null),
|
||||
':common_names' => toNullableString($data['common_names'] ?? null),
|
||||
':ned_notes' => toNullableString($data['ned_notes'] ?? null),
|
||||
':openngc_notes' => toNullableString($data['openngc_notes'] ?? null),
|
||||
':image' => toNullableString($data['image'] ?? null),
|
||||
]);
|
||||
|
||||
$imported++;
|
||||
}
|
||||
|
||||
$pdo->commit();
|
||||
} catch (Throwable $e) {
|
||||
$pdo->rollBack();
|
||||
fclose($handle);
|
||||
exit("Fehler beim Import: " . $e->getMessage() . "\n");
|
||||
}
|
||||
|
||||
fclose($handle);
|
||||
|
||||
echo "Deep-Sky-Import abgeschlossen\n";
|
||||
echo "Verarbeitete Zeilen: {$rowCount}\n";
|
||||
echo "Importiert/Aktualisiert: {$imported}\n";
|
||||
echo "Übersprungen: {$skipped}\n";
|
||||
@@ -0,0 +1,99 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Import satellite standard magnitudes from the qs.mag file (Mike McCants).
|
||||
*
|
||||
* Source: https://www.mmccants.org/programs/sgp4/tle_satinfo.txt
|
||||
* Mirror: https://raw.githubusercontent.com/barolfe/satellite-tracker/master/qs.mag
|
||||
*
|
||||
* Format (fixed-width):
|
||||
* 0-4 NORAD catalog number
|
||||
* 6 Flag: 'd'=decayed, 't'=tumbling, ' '=normal
|
||||
* 8-14 International designation
|
||||
* 15-28 Satellite name
|
||||
* ~29+ Standard magnitude (at 1000 km range, 50% illumination phase)
|
||||
* followed by sizes and RCS
|
||||
*
|
||||
* Usage:
|
||||
* php import_qsmag.php [path-to-qs.mag]
|
||||
* php import_qsmag.php (downloads from mirror)
|
||||
*/
|
||||
|
||||
$config = require __DIR__ . '/../../config/database.php';
|
||||
|
||||
$dsn = sprintf(
|
||||
'mysql:host=%s;dbname=%s;charset=%s',
|
||||
$config['host'],
|
||||
$config['dbname'],
|
||||
$config['charset'] ?? 'utf8mb4'
|
||||
);
|
||||
|
||||
$pdo = new PDO($dsn, $config['user'], $config['pass'], [
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||
]);
|
||||
|
||||
// Quelldatei bestimmen
|
||||
$sourceFile = $argv[1] ?? null;
|
||||
$tempFile = null;
|
||||
|
||||
if ($sourceFile !== null) {
|
||||
if (!file_exists($sourceFile)) {
|
||||
exit("Datei nicht gefunden: {$sourceFile}\n");
|
||||
}
|
||||
$content = file_get_contents($sourceFile);
|
||||
} else {
|
||||
echo "Lade qs.mag von Mirror...\n";
|
||||
$url = 'https://raw.githubusercontent.com/barolfe/satellite-tracker/master/qs.mag';
|
||||
$content = @file_get_contents($url);
|
||||
if ($content === false) {
|
||||
exit("Download fehlgeschlagen. Bitte qs.mag manuell herunterladen und als Argument uebergeben.\n");
|
||||
}
|
||||
echo "Download OK (" . strlen($content) . " Bytes).\n";
|
||||
}
|
||||
|
||||
$lines = explode("\n", str_replace("\r\n", "\n", $content));
|
||||
|
||||
$stmt = $pdo->prepare("
|
||||
UPDATE `sat_satellites`
|
||||
SET `std_mag` = :std_mag
|
||||
WHERE `norad_cat_id` = :norad
|
||||
");
|
||||
|
||||
$updated = 0;
|
||||
$skipped = 0;
|
||||
$noMag = 0;
|
||||
|
||||
foreach ($lines as $line) {
|
||||
// Mindestlaenge und Header-Zeile ueberspringen
|
||||
if (strlen($line) < 20) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$norad = (int)substr($line, 0, 5);
|
||||
if ($norad === 0) {
|
||||
continue; // Header oder Leerzeile
|
||||
}
|
||||
|
||||
// Magnitude steht nach dem Namensfeld (ab ~Position 25),
|
||||
// als erster Float-Wert (kann negativ sein, z.B. -1.5 fuer ISS)
|
||||
if (!preg_match('/^.{25,}\s+([-\d]+\.?\d*)\s/', $line, $m)) {
|
||||
$noMag++;
|
||||
continue;
|
||||
}
|
||||
|
||||
$stdMag = (float)$m[1];
|
||||
|
||||
$stmt->execute([':std_mag' => $stdMag, ':norad' => $norad]);
|
||||
if ($stmt->rowCount() > 0) {
|
||||
$updated++;
|
||||
} else {
|
||||
$skipped++; // Satellit nicht in DB
|
||||
}
|
||||
}
|
||||
|
||||
echo "Fertig.\n";
|
||||
echo " Aktualisiert: {$updated}\n";
|
||||
echo " Nicht in DB: {$skipped}\n";
|
||||
echo " Ohne Mag: {$noMag}\n";
|
||||
Reference in New Issue
Block a user