437 lines
18 KiB
PHP
437 lines
18 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
// Die Datei MeteorShowers.json stammt aus Stellarium und dient hier als Importquelle.
|
|
|
|
session_start();
|
|
|
|
$loggedIn = isset($_SESSION['user_id']);
|
|
$isAdmin = $loggedIn && (($_SESSION['role'] ?? '') === 'admin');
|
|
$remoteAddr = (string)($_SERVER['REMOTE_ADDR'] ?? '');
|
|
$serverName = strtolower((string)($_SERVER['SERVER_NAME'] ?? ''));
|
|
$isLocalRequest = in_array($remoteAddr, ['127.0.0.1', '::1'], true)
|
|
|| in_array($serverName, ['localhost', '127.0.0.1', '::1'], true);
|
|
$canUseImportPage = $isAdmin || $isLocalRequest;
|
|
$pageTitle = 'Meteor-Import';
|
|
|
|
function h(string $value): string
|
|
{
|
|
return htmlspecialchars($value, ENT_QUOTES, 'UTF-8');
|
|
}
|
|
|
|
function toNullableFloat(mixed $value): ?float
|
|
{
|
|
if ($value === null || $value === '') {
|
|
return null;
|
|
}
|
|
|
|
if (is_string($value)) {
|
|
$value = str_replace(',', '.', trim($value));
|
|
}
|
|
|
|
return is_numeric($value) ? (float)$value : null;
|
|
}
|
|
|
|
function toNullableInt(mixed $value): ?int
|
|
{
|
|
if ($value === null || $value === '') {
|
|
return null;
|
|
}
|
|
|
|
if (is_string($value)) {
|
|
$value = trim($value);
|
|
}
|
|
|
|
return preg_match('/^\d+$/', (string)$value) ? (int)$value : null;
|
|
}
|
|
|
|
function ensureMeteorShowersTable(PDO $pdo): void
|
|
{
|
|
$pdo->exec(
|
|
"CREATE TABLE IF NOT EXISTS `meteorshowers` (
|
|
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT 'Eindeutige interne ID des Meteorstroms',
|
|
`shower_code` varchar(16) NOT NULL COMMENT 'Kurzer IAU- oder Projektcode des Meteorstroms, z. B. PER oder GEM',
|
|
`iau_no` smallint(5) unsigned DEFAULT NULL COMMENT 'Offizielle IAU-Nummer des Meteorstroms, falls vorhanden',
|
|
`designation` varchar(255) NOT NULL COMMENT 'Vollstaendige Bezeichnung des Meteorstroms',
|
|
`speed_kms` decimal(6,2) DEFAULT NULL COMMENT 'Geozentrische Eintrittsgeschwindigkeit der Meteore in km/s',
|
|
`radiant_alpha_deg` decimal(7,3) DEFAULT NULL COMMENT 'Rektaszension des Radianten in Grad',
|
|
`radiant_delta_deg` decimal(7,3) DEFAULT NULL COMMENT 'Deklination des Radianten in Grad',
|
|
`drift_alpha_deg` decimal(7,3) DEFAULT NULL COMMENT 'Taegliche Aenderung der Radianten-Rektaszension in Grad',
|
|
`drift_delta_deg` decimal(7,3) DEFAULT NULL COMMENT 'Taegliche Aenderung der Radianten-Deklination in Grad',
|
|
`parent_object` varchar(255) DEFAULT NULL COMMENT 'Vermuteter oder bekannter Mutterkoerper, z. B. ein Komet oder Asteroid',
|
|
`population_index` decimal(4,2) DEFAULT NULL COMMENT 'Populationsindex r als Mass fuer die Helligkeitsverteilung der Meteore',
|
|
`active_start_sol` decimal(6,2) DEFAULT NULL COMMENT 'Beginn der Aktivitaet in Sonnenlaenge (Grad, 0-360)',
|
|
`active_finish_sol` decimal(6,2) DEFAULT NULL COMMENT 'Ende der Aktivitaet in Sonnenlaenge (Grad, 0-360)',
|
|
`peak_sol` decimal(6,2) DEFAULT NULL COMMENT 'Maximum des Meteorstroms in Sonnenlaenge (Grad, 0-360)',
|
|
`zhr` int(11) DEFAULT NULL COMMENT 'Zenithal Hourly Rate des generischen Aktivitaetseintrags',
|
|
`zhr_variable` varchar(32) DEFAULT NULL COMMENT 'Textuelle variable ZHR-Angabe, z. B. 5-20 oder 20-700',
|
|
`disttype` tinyint(3) unsigned DEFAULT NULL COMMENT 'Typ der Aktivitaetsverteilung aus der Quelldatei',
|
|
`b1` decimal(8,4) DEFAULT NULL COMMENT 'Parameter b1 fuer das Aktivitaetsprofil aus der Quelldatei',
|
|
`b2` decimal(8,4) DEFAULT NULL COMMENT 'Parameter b2 fuer das Aktivitaetsprofil aus der Quelldatei',
|
|
`activity_json` json DEFAULT NULL COMMENT 'Komplettes activity-Array aus der JSON-Datei mit generischen und jahresspezifischen Aktivitaetsdaten',
|
|
`colors_json` json DEFAULT NULL COMMENT 'Optionale Farbverteilung der Meteore aus der JSON-Datei',
|
|
`source_entry_json` json DEFAULT NULL COMMENT 'Originaler kompletter JSON-Eintrag des Meteorstroms zur Nachvollziehbarkeit',
|
|
`has_yearly_variants` tinyint(1) NOT NULL DEFAULT 0 COMMENT '1, wenn neben generic auch jahresspezifische Aktivitaetseintraege vorhanden sind',
|
|
`dataset_version` int(10) unsigned DEFAULT NULL COMMENT 'Versionsnummer des importierten Quelldatensatzes',
|
|
`updated_at` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp() COMMENT 'Zeitpunkt der letzten Aktualisierung des Datensatzes',
|
|
`created_at` timestamp NOT NULL DEFAULT current_timestamp() COMMENT 'Zeitpunkt der Erstellung des Datensatzes',
|
|
PRIMARY KEY (`id`),
|
|
UNIQUE KEY `uq_meteorshowers_code` (`shower_code`),
|
|
UNIQUE KEY `uq_meteorshowers_iau_no` (`iau_no`),
|
|
KEY `idx_meteorshowers_designation` (`designation`),
|
|
KEY `idx_meteorshowers_active_start` (`active_start_sol`),
|
|
KEY `idx_meteorshowers_active_finish` (`active_finish_sol`),
|
|
KEY `idx_meteorshowers_peak` (`peak_sol`),
|
|
KEY `idx_meteorshowers_zhr` (`zhr`)
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
|
COMMENT='Meteorstroeme mit Stammdaten sowie generischen und historischen Aktivitaetsinformationen'"
|
|
);
|
|
|
|
$requiredColumns = [
|
|
'shower_code',
|
|
'iau_no',
|
|
'designation',
|
|
'speed_kms',
|
|
'radiant_alpha_deg',
|
|
'radiant_delta_deg',
|
|
'drift_alpha_deg',
|
|
'drift_delta_deg',
|
|
'parent_object',
|
|
'population_index',
|
|
'active_start_sol',
|
|
'active_finish_sol',
|
|
'peak_sol',
|
|
'zhr',
|
|
'zhr_variable',
|
|
'disttype',
|
|
'b1',
|
|
'b2',
|
|
'activity_json',
|
|
'colors_json',
|
|
'source_entry_json',
|
|
'has_yearly_variants',
|
|
'dataset_version',
|
|
];
|
|
|
|
$stmt = $pdo->query('SHOW COLUMNS FROM `meteorshowers`');
|
|
$existingColumns = $stmt->fetchAll(PDO::FETCH_COLUMN);
|
|
$missingColumns = array_values(array_diff($requiredColumns, $existingColumns));
|
|
|
|
if ($missingColumns !== []) {
|
|
throw new RuntimeException(
|
|
'Tabelle `meteorshowers` hat nicht das erwartete Schema. Fehlende Spalten: '
|
|
. implode(', ', $missingColumns)
|
|
);
|
|
}
|
|
}
|
|
|
|
function findGenericActivity(array $activity): ?array
|
|
{
|
|
foreach ($activity as $entry) {
|
|
if (!is_array($entry)) {
|
|
continue;
|
|
}
|
|
|
|
if (strtolower(trim((string)($entry['year'] ?? ''))) === 'generic') {
|
|
return $entry;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
function hasYearlyVariants(array $activity): bool
|
|
{
|
|
foreach ($activity as $entry) {
|
|
if (!is_array($entry)) {
|
|
continue;
|
|
}
|
|
|
|
$year = strtolower(trim((string)($entry['year'] ?? '')));
|
|
if ($year !== '' && $year !== 'generic') {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
function toNullableString(mixed $value): ?string
|
|
{
|
|
if ($value === null) {
|
|
return null;
|
|
}
|
|
|
|
$value = trim((string)$value);
|
|
|
|
return $value === '' ? null : $value;
|
|
}
|
|
|
|
$defaultJsonPath = realpath(__DIR__ . '/MeteorShowers.json') ?: (__DIR__ . '/MeteorShowers.json');
|
|
$selectedJsonPath = (string)$defaultJsonPath;
|
|
$report = null;
|
|
$errorMessage = null;
|
|
|
|
if ($canUseImportPage && $_SERVER['REQUEST_METHOD'] === 'POST') {
|
|
$jsonPath = trim((string)($_POST['json_path'] ?? ''));
|
|
if ($jsonPath === '') {
|
|
$jsonPath = (string)$defaultJsonPath;
|
|
}
|
|
$selectedJsonPath = $jsonPath;
|
|
|
|
try {
|
|
if (!is_file($jsonPath)) {
|
|
throw new RuntimeException('JSON-Datei nicht gefunden: ' . $jsonPath);
|
|
}
|
|
|
|
$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,
|
|
]
|
|
);
|
|
|
|
ensureMeteorShowersTable($pdo);
|
|
|
|
$json = file_get_contents($jsonPath);
|
|
if ($json === false) {
|
|
throw new RuntimeException('JSON-Datei konnte nicht gelesen werden.');
|
|
}
|
|
|
|
$data = json_decode($json, true, 512, JSON_THROW_ON_ERROR);
|
|
$showers = $data['showers'] ?? null;
|
|
if (!is_array($showers)) {
|
|
throw new RuntimeException('Ungueltige JSON-Struktur: "showers" fehlt oder ist kein Objekt.');
|
|
}
|
|
|
|
$datasetName = toNullableString($data['shortName'] ?? null);
|
|
$datasetVersion = toNullableInt($data['version'] ?? null);
|
|
|
|
$selectStmt = $pdo->prepare('SELECT `id` FROM `meteorshowers` WHERE `shower_code` = :shower_code LIMIT 1');
|
|
$upsertStmt = $pdo->prepare(
|
|
"INSERT INTO `meteorshowers` (
|
|
`shower_code`,
|
|
`designation`,
|
|
`iau_no`,
|
|
`speed_kms`,
|
|
`radiant_alpha_deg`,
|
|
`radiant_delta_deg`,
|
|
`drift_alpha_deg`,
|
|
`drift_delta_deg`,
|
|
`parent_object`,
|
|
`population_index`,
|
|
`active_start_sol`,
|
|
`active_finish_sol`,
|
|
`peak_sol`,
|
|
`zhr`,
|
|
`zhr_variable`,
|
|
`disttype`,
|
|
`b1`,
|
|
`b2`,
|
|
`activity_json`,
|
|
`colors_json`,
|
|
`source_entry_json`,
|
|
`has_yearly_variants`,
|
|
`dataset_version`
|
|
) VALUES (
|
|
:shower_code,
|
|
:designation,
|
|
:iau_no,
|
|
:speed_kms,
|
|
:radiant_alpha_deg,
|
|
:radiant_delta_deg,
|
|
:drift_alpha_deg,
|
|
:drift_delta_deg,
|
|
:parent_object,
|
|
:population_index,
|
|
:active_start_sol,
|
|
:active_finish_sol,
|
|
:peak_sol,
|
|
:zhr,
|
|
:zhr_variable,
|
|
:disttype,
|
|
:b1,
|
|
:b2,
|
|
:activity_json,
|
|
:colors_json,
|
|
:source_entry_json,
|
|
:has_yearly_variants,
|
|
:dataset_version
|
|
)
|
|
ON DUPLICATE KEY UPDATE
|
|
`designation` = VALUES(`designation`),
|
|
`iau_no` = VALUES(`iau_no`),
|
|
`speed_kms` = VALUES(`speed_kms`),
|
|
`radiant_alpha_deg` = VALUES(`radiant_alpha_deg`),
|
|
`radiant_delta_deg` = VALUES(`radiant_delta_deg`),
|
|
`drift_alpha_deg` = VALUES(`drift_alpha_deg`),
|
|
`drift_delta_deg` = VALUES(`drift_delta_deg`),
|
|
`parent_object` = VALUES(`parent_object`),
|
|
`population_index` = VALUES(`population_index`),
|
|
`active_start_sol` = VALUES(`active_start_sol`),
|
|
`active_finish_sol` = VALUES(`active_finish_sol`),
|
|
`peak_sol` = VALUES(`peak_sol`),
|
|
`zhr` = VALUES(`zhr`),
|
|
`zhr_variable` = VALUES(`zhr_variable`),
|
|
`disttype` = VALUES(`disttype`),
|
|
`b1` = VALUES(`b1`),
|
|
`b2` = VALUES(`b2`),
|
|
`activity_json` = VALUES(`activity_json`),
|
|
`colors_json` = VALUES(`colors_json`),
|
|
`source_entry_json` = VALUES(`source_entry_json`),
|
|
`has_yearly_variants` = VALUES(`has_yearly_variants`),
|
|
`dataset_version` = VALUES(`dataset_version`),
|
|
`updated_at` = CURRENT_TIMESTAMP()"
|
|
);
|
|
|
|
$processed = 0;
|
|
$inserted = 0;
|
|
$updated = 0;
|
|
|
|
$pdo->beginTransaction();
|
|
foreach ($showers as $code => $entry) {
|
|
if (!is_array($entry)) {
|
|
continue;
|
|
}
|
|
|
|
$showerCode = strtoupper(trim((string)$code));
|
|
if ($showerCode === '') {
|
|
continue;
|
|
}
|
|
|
|
$selectStmt->execute([':shower_code' => $showerCode]);
|
|
$exists = $selectStmt->fetchColumn() !== false;
|
|
|
|
$designation = trim((string)($entry['designation'] ?? $showerCode));
|
|
if ($designation === '') {
|
|
$designation = $showerCode;
|
|
}
|
|
|
|
$activity = isset($entry['activity']) && is_array($entry['activity']) ? $entry['activity'] : [];
|
|
$genericActivity = findGenericActivity($activity);
|
|
$colors = isset($entry['colors']) && is_array($entry['colors']) ? $entry['colors'] : [];
|
|
|
|
$upsertStmt->execute([
|
|
':shower_code' => $showerCode,
|
|
':designation' => $designation,
|
|
':iau_no' => toNullableInt($entry['IAUNo'] ?? null),
|
|
':speed_kms' => toNullableFloat($entry['speed'] ?? null),
|
|
':radiant_alpha_deg' => toNullableFloat($entry['radiantAlpha'] ?? null),
|
|
':radiant_delta_deg' => toNullableFloat($entry['radiantDelta'] ?? null),
|
|
':drift_alpha_deg' => toNullableFloat($entry['driftAlpha'] ?? null),
|
|
':drift_delta_deg' => toNullableFloat($entry['driftDelta'] ?? null),
|
|
':parent_object' => toNullableString($entry['parentObj'] ?? null),
|
|
':population_index' => toNullableFloat($entry['pidx'] ?? null),
|
|
':active_start_sol' => toNullableFloat($genericActivity['start'] ?? null),
|
|
':active_finish_sol' => toNullableFloat($genericActivity['finish'] ?? null),
|
|
':peak_sol' => toNullableFloat($genericActivity['peak'] ?? null),
|
|
':zhr' => toNullableInt($genericActivity['zhr'] ?? null),
|
|
':zhr_variable' => toNullableString($genericActivity['variable'] ?? null),
|
|
':disttype' => toNullableInt($genericActivity['disttype'] ?? null),
|
|
':b1' => toNullableFloat($genericActivity['b1'] ?? null),
|
|
':b2' => toNullableFloat($genericActivity['b2'] ?? null),
|
|
':activity_json' => json_encode($activity, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
|
|
':colors_json' => $colors !== [] ? json_encode($colors, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) : null,
|
|
':source_entry_json' => json_encode($entry, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
|
|
':has_yearly_variants' => hasYearlyVariants($activity) ? 1 : 0,
|
|
':dataset_version' => $datasetVersion,
|
|
]);
|
|
|
|
$processed++;
|
|
if ($exists) {
|
|
$updated++;
|
|
} else {
|
|
$inserted++;
|
|
}
|
|
}
|
|
$pdo->commit();
|
|
|
|
$tableCount = (int)$pdo->query('SELECT COUNT(*) FROM `meteorshowers`')->fetchColumn();
|
|
$report = [
|
|
'json_path' => $jsonPath,
|
|
'dataset_name' => $datasetName,
|
|
'dataset_version' => $datasetVersion,
|
|
'processed' => $processed,
|
|
'inserted' => $inserted,
|
|
'updated' => $updated,
|
|
'table_count' => $tableCount,
|
|
];
|
|
} catch (Throwable $e) {
|
|
if (isset($pdo) && $pdo instanceof PDO && $pdo->inTransaction()) {
|
|
$pdo->rollBack();
|
|
}
|
|
$errorMessage = $e->getMessage();
|
|
}
|
|
}
|
|
?>
|
|
<!DOCTYPE html>
|
|
<html lang="de">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title><?= h($pageTitle) ?></title>
|
|
</head>
|
|
<body style="margin:0;padding:24px;font-family:Arial,sans-serif;background:#f5f5f5;color:#111;">
|
|
<div style="max-width:900px;margin:0 auto;background:#fff;border:1px solid #ccc;padding:24px;">
|
|
<h1 style="margin:0 0 12px;">Meteor-Import</h1>
|
|
<p style="margin:0 0 18px;">
|
|
Importiert <code>MeteorShowers.json</code> in die Tabelle <code>meteorshowers</code>.
|
|
Die Tabelle wird bei Bedarf automatisch angelegt.
|
|
</p>
|
|
|
|
<?php if (!$canUseImportPage): ?>
|
|
<div style="border:1px solid #c33;background:#fff3f3;color:#900;padding:14px;">
|
|
Diese Seite ist nur fuer Admins oder lokale Aufrufe auf localhost freigegeben.
|
|
</div>
|
|
<?php else: ?>
|
|
<form method="post" style="display:grid;gap:12px;">
|
|
<label style="display:grid;gap:6px;">
|
|
<span>JSON-Pfad</span>
|
|
<input
|
|
type="text"
|
|
name="json_path"
|
|
value="<?= h($selectedJsonPath) ?>"
|
|
style="width:100%;padding:10px 12px;border:1px solid #999;color:#111;"
|
|
>
|
|
</label>
|
|
<div>
|
|
<button type="submit" style="padding:10px 16px;border:1px solid #333;background:#eee;color:#111;cursor:pointer;">
|
|
Import Starten
|
|
</button>
|
|
</div>
|
|
</form>
|
|
|
|
<?php if ($errorMessage !== null): ?>
|
|
<div style="margin-top:18px;border:1px solid #c33;background:#fff3f3;color:#900;padding:14px;">
|
|
<strong>Fehler:</strong> <?= h($errorMessage) ?>
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
<?php if (is_array($report)): ?>
|
|
<div style="margin-top:18px;border:1px solid #3a7a3a;background:#f1fff1;color:#145214;padding:14px;line-height:1.7;">
|
|
<strong>Import abgeschlossen.</strong><br>
|
|
Datei: <?= h((string)$report['json_path']) ?><br>
|
|
Datensatz: <?= h((string)($report['dataset_name'] ?? '-')) ?><br>
|
|
Version: <?= h((string)($report['dataset_version'] ?? '-')) ?><br>
|
|
Verarbeitet: <?= h((string)$report['processed']) ?><br>
|
|
Neu: <?= h((string)$report['inserted']) ?><br>
|
|
Aktualisiert: <?= h((string)$report['updated']) ?><br>
|
|
Zeilen in `meteorshowers`: <?= h((string)$report['table_count']) ?>
|
|
</div>
|
|
<?php endif; ?>
|
|
<?php endif; ?>
|
|
</div>
|
|
</body>
|
|
</html>
|