2552 lines
102 KiB
PHP
2552 lines
102 KiB
PHP
<?php
|
||
declare(strict_types=1);
|
||
|
||
ini_set('memory_limit', '512M');
|
||
|
||
session_start();
|
||
|
||
$loggedIn = isset($_SESSION['user_id']);
|
||
$pageTitle = 'Sternenhimmel - AstroTools';
|
||
$bodyClass = 'sternenhimmel-page';
|
||
|
||
$constellationData = ['constellations' => []];
|
||
$constellationDataError = null;
|
||
$starData = [];
|
||
$starDataError = null;
|
||
$messierData = [];
|
||
$messierDataError = null;
|
||
$availableLocations = [];
|
||
$starDbMagLimit = 12.0;
|
||
|
||
try {
|
||
$dbConfig = require __DIR__ . '/../config/database.php';
|
||
$dsn = sprintf(
|
||
'mysql:host=%s;dbname=%s;charset=%s',
|
||
$dbConfig['host'],
|
||
$dbConfig['dbname'],
|
||
$dbConfig['charset'] ?? 'utf8mb4'
|
||
);
|
||
$planetariumPdo = new PDO($dsn, $dbConfig['user'], $dbConfig['pass'], [
|
||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||
]);
|
||
|
||
$constellations = [];
|
||
$ensureConstellation = static function (string $id) use (&$constellations): void {
|
||
if (!isset($constellations[$id])) {
|
||
$constellations[$id] = [
|
||
'id' => $id,
|
||
'lines' => [],
|
||
'label_positions' => [],
|
||
'common_name' => [
|
||
'german' => null,
|
||
'native' => null,
|
||
'byname' => null,
|
||
'context' => null,
|
||
],
|
||
];
|
||
}
|
||
};
|
||
|
||
$nameStmt = $planetariumPdo->query(
|
||
"SELECT constellation_id, german_name, native_name, byname, context_text
|
||
FROM constellation_names
|
||
ORDER BY constellation_id"
|
||
);
|
||
foreach ($nameStmt as $row) {
|
||
$id = (string) $row['constellation_id'];
|
||
$ensureConstellation($id);
|
||
$constellations[$id]['common_name'] = [
|
||
'german' => $row['german_name'] !== null ? (string) $row['german_name'] : null,
|
||
'native' => $row['native_name'] !== null ? (string) $row['native_name'] : null,
|
||
'byname' => $row['byname'] !== null ? (string) $row['byname'] : null,
|
||
'context' => $row['context_text'] !== null ? (string) $row['context_text'] : null,
|
||
];
|
||
}
|
||
|
||
$labelStmt = $planetariumPdo->query(
|
||
"SELECT constellation_id, position_index, ra_hours, dec_degrees
|
||
FROM constellation_label_positions
|
||
ORDER BY constellation_id, position_index"
|
||
);
|
||
foreach ($labelStmt as $row) {
|
||
$id = (string) $row['constellation_id'];
|
||
$ensureConstellation($id);
|
||
$constellations[$id]['label_positions'][] = [
|
||
(float) $row['ra_hours'],
|
||
(float) $row['dec_degrees'],
|
||
];
|
||
}
|
||
|
||
$lineStmt = $planetariumPdo->query(
|
||
"SELECT cl.constellation_id, cl.line_index, clp.point_index, clp.hip_number
|
||
FROM constellation_lines cl
|
||
INNER JOIN constellation_line_points clp ON clp.line_id = cl.id
|
||
ORDER BY cl.constellation_id, cl.line_index, clp.point_index"
|
||
);
|
||
foreach ($lineStmt as $row) {
|
||
$id = (string) $row['constellation_id'];
|
||
$lineIndex = (int) $row['line_index'];
|
||
$ensureConstellation($id);
|
||
$constellations[$id]['lines'][$lineIndex] ??= [];
|
||
$constellations[$id]['lines'][$lineIndex][] = (int) $row['hip_number'];
|
||
}
|
||
|
||
foreach ($constellations as &$constellation) {
|
||
if ($constellation['lines']) {
|
||
ksort($constellation['lines']);
|
||
$constellation['lines'] = array_values($constellation['lines']);
|
||
}
|
||
}
|
||
unset($constellation);
|
||
|
||
uasort(
|
||
$constellations,
|
||
static fn(array $a, array $b): int => strcmp($a['id'], $b['id'])
|
||
);
|
||
|
||
$constellationData['constellations'] = array_values($constellations);
|
||
|
||
$starStmt = $planetariumPdo->query(
|
||
"SELECT hip, proper, bayer, con, spect, mag, ci, rarad, decrad
|
||
FROM star_hipparcos
|
||
WHERE mag IS NOT NULL
|
||
AND rarad IS NOT NULL
|
||
AND decrad IS NOT NULL
|
||
AND mag <= " . (float)$starDbMagLimit . "
|
||
ORDER BY mag ASC"
|
||
);
|
||
while ($row = $starStmt->fetch()) {
|
||
$starData[] = [
|
||
'hip' => $row['hip'] !== null ? (int)$row['hip'] : null,
|
||
'proper' => trim((string)($row['proper'] ?? '')),
|
||
'bayer' => trim((string)($row['bayer'] ?? '')),
|
||
'con' => trim((string)($row['con'] ?? '')),
|
||
'spect' => trim((string)($row['spect'] ?? '')),
|
||
'ra' => (float)$row['rarad'],
|
||
'dec' => (float)$row['decrad'],
|
||
'mag' => (float)$row['mag'],
|
||
'ci' => $row['ci'] !== null ? (float)$row['ci'] : null,
|
||
];
|
||
}
|
||
|
||
$messierStmt = $planetariumPdo->query(
|
||
"SELECT messier, name, object_type_abrev, ra_deg, dec_deg, v_mag, major_axis
|
||
FROM dso_objects
|
||
WHERE messier IS NOT NULL
|
||
AND messier <> ''
|
||
AND ra_deg IS NOT NULL
|
||
AND dec_deg IS NOT NULL
|
||
ORDER BY CAST(REGEXP_REPLACE(messier, '[^0-9]', '') AS UNSIGNED)"
|
||
);
|
||
while ($row = $messierStmt->fetch()) {
|
||
if (!preg_match('/(\d+)/', (string)$row['messier'], $m)) {
|
||
continue;
|
||
}
|
||
$messierData[] = [
|
||
'm' => (int)$m[1],
|
||
'name' => trim((string)($row['name'] ?? '')),
|
||
'type' => trim((string)($row['object_type_abrev'] ?? 'As')),
|
||
'ra' => ((float)$row['ra_deg']) / 15.0,
|
||
'dec' => (float)$row['dec_deg'],
|
||
'mag' => $row['v_mag'] !== null ? (float)$row['v_mag'] : 99.0,
|
||
'size' => $row['major_axis'] !== null ? (float)$row['major_axis'] : 8.0,
|
||
];
|
||
}
|
||
|
||
if ($loggedIn && isset($_SESSION['user_id'])) {
|
||
$userLocationStmt = $planetariumPdo->prepare(
|
||
"SELECT id, name, latitude, longitude, elevation, is_default
|
||
FROM app_user_locations
|
||
WHERE user_id = ?
|
||
ORDER BY is_default DESC, name ASC, id ASC"
|
||
);
|
||
$userLocationStmt->execute([(int)$_SESSION['user_id']]);
|
||
while ($row = $userLocationStmt->fetch()) {
|
||
if ($row['latitude'] === null || $row['longitude'] === null) {
|
||
continue;
|
||
}
|
||
$availableLocations[] = [
|
||
'id' => $row['id'] !== null ? (int)$row['id'] : null,
|
||
'name' => trim((string)($row['name'] ?? 'Standort')),
|
||
'latitude' => (float)$row['latitude'],
|
||
'longitude' => (float)$row['longitude'],
|
||
'elevation' => $row['elevation'] !== null ? (float)$row['elevation'] : 0.0,
|
||
'is_default' => !empty($row['is_default']),
|
||
];
|
||
}
|
||
}
|
||
} catch (Throwable $e) {
|
||
$constellationDataError = $e->getMessage();
|
||
$starDataError = $e->getMessage();
|
||
$messierDataError = $e->getMessage();
|
||
}
|
||
|
||
$constellationDataJson = json_encode(
|
||
$constellationData,
|
||
JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES
|
||
);
|
||
$constellationDataErrorJson = json_encode(
|
||
$constellationDataError,
|
||
JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES
|
||
);
|
||
$starDataJson = json_encode(
|
||
$starData,
|
||
JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES
|
||
);
|
||
$starDataErrorJson = json_encode(
|
||
$starDataError,
|
||
JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES
|
||
);
|
||
$messierDataJson = json_encode(
|
||
$messierData,
|
||
JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES
|
||
);
|
||
$messierDataErrorJson = json_encode(
|
||
$messierDataError,
|
||
JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES
|
||
);
|
||
?>
|
||
<?php require __DIR__ . '/header.php'; ?>
|
||
<?php
|
||
$planetariumLocation = [
|
||
'name' => 'Sternwarte Sonneberg',
|
||
'latitude' => 50.3547,
|
||
'longitude' => 11.1900,
|
||
'elevation' => 640.0,
|
||
];
|
||
|
||
if (!empty($currentLocation) && is_array($currentLocation)) {
|
||
$planetariumLocation = [
|
||
'id' => isset($currentLocation['id']) ? (int)$currentLocation['id'] : null,
|
||
'name' => (string)($currentLocation['name'] ?? 'Standard-Standort'),
|
||
'latitude' => isset($currentLocation['latitude']) ? (float)$currentLocation['latitude'] : 50.3547,
|
||
'longitude' => isset($currentLocation['longitude']) ? (float)$currentLocation['longitude'] : 11.1900,
|
||
'elevation' => isset($currentLocation['elevation']) && $currentLocation['elevation'] !== null ? (float)$currentLocation['elevation'] : 0.0,
|
||
];
|
||
}
|
||
|
||
if (!$availableLocations) {
|
||
$availableLocations[] = [
|
||
'id' => $planetariumLocation['id'] ?? null,
|
||
'name' => $planetariumLocation['name'],
|
||
'latitude' => $planetariumLocation['latitude'],
|
||
'longitude' => $planetariumLocation['longitude'],
|
||
'elevation' => $planetariumLocation['elevation'],
|
||
'is_default' => true,
|
||
];
|
||
}
|
||
?>
|
||
<script src="https://cdn.jsdelivr.net/npm/astronomy-engine@2.1.19/astronomy.browser.js"></script>
|
||
<style>
|
||
@import url('https://fonts.googleapis.com/css2?family=Rajdhani:wght@300;400;600&family=Share+Tech+Mono&display=swap');
|
||
|
||
:root {
|
||
--bg: #020408;
|
||
--accent: #4a9eff;
|
||
--accent2: #ff6b35;
|
||
--dim: #2a3a4a;
|
||
--text: #8ab0cc;
|
||
--text-bright: #c8dff0;
|
||
}
|
||
|
||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||
|
||
body.sternenhimmel-page {
|
||
background: var(--bg);
|
||
color: var(--text);
|
||
font-family: 'Rajdhani', sans-serif;
|
||
min-height: 100vh;
|
||
display: grid;
|
||
grid-template-rows: auto 1fr auto;
|
||
overflow-x: hidden;
|
||
}
|
||
|
||
body.sternenhimmel-page main {
|
||
display: flex;
|
||
flex-direction: column;
|
||
min-height: 0;
|
||
padding: 0;
|
||
}
|
||
|
||
body.sternenhimmel-page main > .container {
|
||
width: 100%;
|
||
max-width: none;
|
||
height: 100%;
|
||
min-height: 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
body.sternenhimmel-page footer {
|
||
margin-top: 0;
|
||
}
|
||
|
||
.planetarium-toolbar {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 8px 20px;
|
||
border-bottom: 1px solid #0d1f2d;
|
||
flex-shrink: 0;
|
||
background: rgba(2,4,8,0.95);
|
||
z-index: 10;
|
||
gap: 12px;
|
||
flex-wrap: wrap;
|
||
}
|
||
.toolbar-location {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
}
|
||
.toolbar-location-select {
|
||
min-width: 190px;
|
||
background: #08111b;
|
||
border: 1px solid #1a2e40;
|
||
color: var(--accent);
|
||
font-family: 'Rajdhani', sans-serif;
|
||
font-size: 0.86rem;
|
||
letter-spacing: 0.06em;
|
||
padding: 4px 28px 4px 8px;
|
||
outline: none;
|
||
}
|
||
.toolbar-location-select:focus,
|
||
.toolbar-location-select:hover {
|
||
border-color: var(--accent);
|
||
background: #0a1825;
|
||
}
|
||
|
||
.logo {
|
||
font-size: 1.05rem;
|
||
font-weight: 600;
|
||
letter-spacing: 0.15em;
|
||
color: var(--text-bright);
|
||
text-transform: uppercase;
|
||
white-space: nowrap;
|
||
}
|
||
.logo span { color: var(--accent); }
|
||
|
||
.info-bar {
|
||
font-family: 'Share Tech Mono', monospace;
|
||
font-size: 0.72rem;
|
||
color: var(--text);
|
||
display: flex;
|
||
gap: 16px;
|
||
align-items: center;
|
||
flex-wrap: wrap;
|
||
}
|
||
.info-bar .val { color: var(--accent); }
|
||
|
||
.controls {
|
||
display: flex;
|
||
gap: 10px;
|
||
align-items: center;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.ctrl-group {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
font-size: 0.78rem;
|
||
}
|
||
|
||
.time-steps {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 3px;
|
||
}
|
||
|
||
.step-row {
|
||
display: flex;
|
||
gap: 2px;
|
||
align-items: center;
|
||
}
|
||
|
||
.step-label {
|
||
font-family: 'Share Tech Mono', monospace;
|
||
font-size: 0.65rem;
|
||
color: var(--dim);
|
||
width: 42px;
|
||
text-align: center;
|
||
}
|
||
|
||
.step-btn {
|
||
background: #080f18;
|
||
border: 1px solid #1a2e40;
|
||
color: #5a8aaa;
|
||
padding: 2px 7px;
|
||
font-family: 'Share Tech Mono', monospace;
|
||
font-size: 0.72rem;
|
||
cursor: pointer;
|
||
transition: all 0.15s;
|
||
line-height: 1.4;
|
||
}
|
||
.step-btn:hover {
|
||
border-color: var(--accent);
|
||
color: var(--accent);
|
||
background: #0a1825;
|
||
}
|
||
|
||
input[type=datetime-local] {
|
||
background: #0a1520;
|
||
border: 1px solid var(--dim);
|
||
color: var(--text-bright);
|
||
padding: 4px 7px;
|
||
font-family: 'Share Tech Mono', monospace;
|
||
font-size: 0.72rem;
|
||
outline: none;
|
||
color-scheme: dark;
|
||
transition: border-color 0.2s;
|
||
}
|
||
input[type=datetime-local]:hover,
|
||
input[type=datetime-local]:focus { border-color: var(--accent); }
|
||
|
||
input[type=range] { width: 70px; accent-color: var(--accent); }
|
||
|
||
.btn {
|
||
background: #0a1520;
|
||
border: 1px solid var(--dim);
|
||
color: var(--text-bright);
|
||
padding: 5px 11px;
|
||
font-family: 'Rajdhani', sans-serif;
|
||
font-size: 0.82rem;
|
||
font-weight: 600;
|
||
letter-spacing: 0.07em;
|
||
cursor: pointer;
|
||
transition: all 0.2s;
|
||
text-transform: uppercase;
|
||
white-space: nowrap;
|
||
}
|
||
.btn:hover { border-color: var(--accent); color: var(--accent); background: #0d1e30; }
|
||
.btn.live { border-color: var(--accent2); color: var(--accent2); }
|
||
|
||
label.btn { display: inline-block; }
|
||
|
||
|
||
#main {
|
||
flex: 1;
|
||
min-height: 0;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
position: relative;
|
||
overflow: hidden;
|
||
}
|
||
|
||
#sky { display: block; cursor: crosshair; }
|
||
|
||
#zoomIndicator {
|
||
display: none;
|
||
position: absolute;
|
||
bottom: 14px;
|
||
right: 18px;
|
||
font-family: 'Share Tech Mono', monospace;
|
||
font-size: 0.75rem;
|
||
color: var(--accent);
|
||
background: rgba(2,4,8,0.75);
|
||
padding: 3px 8px;
|
||
border: 1px solid var(--dim);
|
||
pointer-events: none;
|
||
letter-spacing: 0.08em;
|
||
}
|
||
|
||
#tooltip {
|
||
position: fixed;
|
||
background: rgba(4, 12, 22, 0.95);
|
||
border: 1px solid var(--accent);
|
||
color: var(--text-bright);
|
||
font-family: 'Share Tech Mono', monospace;
|
||
font-size: 0.7rem;
|
||
padding: 8px 11px;
|
||
pointer-events: none;
|
||
opacity: 0;
|
||
transition: opacity 0.12s;
|
||
z-index: 20;
|
||
line-height: 1.65;
|
||
max-width: 215px;
|
||
}
|
||
#tooltip.visible { opacity: 1; }
|
||
#tooltip .tt-name {
|
||
font-family: 'Rajdhani', sans-serif;
|
||
font-size: 0.92rem;
|
||
font-weight: 600;
|
||
color: var(--accent);
|
||
display: block;
|
||
margin-bottom: 2px;
|
||
}
|
||
#tooltip .tt-planet { color: #ffdd88; }
|
||
|
||
/* ── STANDORT PANEL ─────────────────────────────────────────────────── */
|
||
#locationWrap {
|
||
position: relative;
|
||
}
|
||
|
||
#btnLocation {
|
||
background: #080f18;
|
||
border: 1px solid #1a2e40;
|
||
color: #5a8aaa;
|
||
padding: 4px 10px;
|
||
font-size: 1.05rem;
|
||
cursor: pointer;
|
||
transition: all 0.15s;
|
||
line-height: 1.4;
|
||
}
|
||
#btnLocation:hover, #btnLocation.open {
|
||
border-color: var(--accent);
|
||
background: #0a1825;
|
||
}
|
||
|
||
#locationPanel {
|
||
display: none;
|
||
position: absolute;
|
||
top: calc(100% + 8px);
|
||
right: 0;
|
||
width: 240px;
|
||
background: rgba(4,10,20,0.97);
|
||
border: 1px solid var(--accent);
|
||
z-index: 100;
|
||
padding: 12px 14px;
|
||
font-family: 'Rajdhani', sans-serif;
|
||
font-size: 0.82rem;
|
||
color: var(--text-bright);
|
||
box-shadow: 0 4px 24px rgba(0,0,0,0.7);
|
||
max-height: 80vh;
|
||
overflow-y: auto;
|
||
}
|
||
#locationPanel.open { display: block; }
|
||
|
||
.loc-list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 2px;
|
||
}
|
||
|
||
.loc-btn {
|
||
background: none;
|
||
border: 1px solid transparent;
|
||
border-radius: 2px;
|
||
color: var(--text);
|
||
padding: 5px 8px;
|
||
cursor: pointer;
|
||
text-align: left;
|
||
width: 100%;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 1px;
|
||
transition: all 0.12s;
|
||
}
|
||
.loc-btn:hover {
|
||
background: #0a1825;
|
||
border-color: #1a3040;
|
||
}
|
||
.loc-btn.active {
|
||
background: #091d30;
|
||
border-color: var(--accent);
|
||
}
|
||
.loc-name {
|
||
font-size: 0.82rem;
|
||
font-weight: 600;
|
||
color: var(--text-bright);
|
||
}
|
||
.loc-btn.active .loc-name { color: var(--accent); }
|
||
.loc-sub {
|
||
font-size: 0.68rem;
|
||
color: var(--dim);
|
||
font-family: 'Share Tech Mono', monospace;
|
||
}
|
||
|
||
.loc-custom {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 6px;
|
||
margin-top: 4px;
|
||
}
|
||
.loc-input-row {
|
||
display: grid;
|
||
grid-template-columns: auto 1fr auto 1fr auto 1fr;
|
||
gap: 4px;
|
||
align-items: center;
|
||
font-size: 0.72rem;
|
||
color: var(--dim);
|
||
}
|
||
.loc-input-row input {
|
||
background: #080f18;
|
||
border: 1px solid #1a2e40;
|
||
color: var(--text-bright);
|
||
padding: 3px 4px;
|
||
font-family: 'Share Tech Mono', monospace;
|
||
font-size: 0.68rem;
|
||
width: 100%;
|
||
min-width: 0;
|
||
}
|
||
.loc-set-btn {
|
||
background: #0a1f30;
|
||
border: 1px solid var(--accent);
|
||
color: var(--accent);
|
||
padding: 4px;
|
||
font-family: 'Rajdhani', sans-serif;
|
||
font-size: 0.78rem;
|
||
cursor: pointer;
|
||
letter-spacing: 0.06em;
|
||
width: 100%;
|
||
}
|
||
.loc-set-btn:hover { background: #0d2840; }
|
||
|
||
/* ── EINSTELLUNGEN PANEL ─────────────────────────────────────────────── */
|
||
#settingsWrap {
|
||
position: relative;
|
||
}
|
||
|
||
#btnSettings {
|
||
background: #080f18;
|
||
border: 1px solid #1a2e40;
|
||
color: #5a8aaa;
|
||
padding: 4px 10px;
|
||
font-size: 1.1rem;
|
||
cursor: pointer;
|
||
transition: all 0.15s;
|
||
line-height: 1.4;
|
||
}
|
||
#btnSettings:hover, #btnSettings.open {
|
||
border-color: var(--accent);
|
||
color: var(--accent);
|
||
background: #0a1825;
|
||
}
|
||
|
||
#settingsPanel {
|
||
display: none;
|
||
position: absolute;
|
||
top: calc(100% + 8px);
|
||
right: 0;
|
||
width: 280px;
|
||
background: rgba(4, 10, 20, 0.97);
|
||
border: 1px solid var(--accent);
|
||
z-index: 100;
|
||
padding: 12px 14px;
|
||
font-family: 'Rajdhani', sans-serif;
|
||
font-size: 0.82rem;
|
||
color: var(--text-bright);
|
||
box-shadow: 0 4px 24px rgba(0,0,0,0.7);
|
||
}
|
||
#settingsPanel.open { display: block; }
|
||
|
||
.sp-title {
|
||
font-size: 0.7rem;
|
||
letter-spacing: 0.12em;
|
||
color: var(--accent);
|
||
text-transform: uppercase;
|
||
margin-bottom: 10px;
|
||
border-bottom: 1px solid #0d2030;
|
||
padding-bottom: 5px;
|
||
}
|
||
|
||
.sp-row {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 4px 0;
|
||
border-bottom: 1px solid rgba(255,255,255,0.04);
|
||
}
|
||
.sp-row:last-child { border-bottom: none; }
|
||
|
||
.sp-label {
|
||
color: var(--text);
|
||
font-size: 0.80rem;
|
||
min-width: 90px;
|
||
}
|
||
|
||
.sp-toggles {
|
||
display: flex;
|
||
gap: 6px;
|
||
}
|
||
|
||
.sp-toggle {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
cursor: pointer;
|
||
color: var(--dim);
|
||
font-size: 0.72rem;
|
||
white-space: nowrap;
|
||
transition: color 0.12s;
|
||
}
|
||
.sp-toggle input { display: none; }
|
||
.sp-toggle .sp-pill {
|
||
width: 28px; height: 14px;
|
||
border-radius: 7px;
|
||
background: #111e2a;
|
||
border: 1px solid #1a3040;
|
||
position: relative;
|
||
transition: background 0.15s, border-color 0.15s;
|
||
flex-shrink: 0;
|
||
}
|
||
.sp-toggle .sp-pill::after {
|
||
content: '';
|
||
position: absolute;
|
||
width: 10px; height: 10px;
|
||
border-radius: 50%;
|
||
background: #2a4055;
|
||
top: 1px; left: 1px;
|
||
transition: left 0.15s, background 0.15s;
|
||
}
|
||
.sp-toggle input:checked + .sp-pill {
|
||
background: #0d2840;
|
||
border-color: var(--accent);
|
||
}
|
||
.sp-toggle input:checked + .sp-pill::after {
|
||
left: 15px;
|
||
background: var(--accent);
|
||
}
|
||
.sp-toggle input:checked ~ .sp-text { color: var(--text-bright); }
|
||
|
||
.sp-radio-group {
|
||
display: flex;
|
||
gap: 8px;
|
||
flex-wrap: wrap;
|
||
}
|
||
.sp-radio {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 5px;
|
||
cursor: pointer;
|
||
font-size: 0.75rem;
|
||
color: var(--dim);
|
||
transition: color 0.12s;
|
||
}
|
||
.sp-radio input { display: none; }
|
||
.sp-rbtn {
|
||
width: 14px; height: 14px;
|
||
border-radius: 50%;
|
||
border: 1px solid #1a3040;
|
||
background: #111e2a;
|
||
position: relative;
|
||
flex-shrink: 0;
|
||
transition: border-color 0.15s;
|
||
}
|
||
.sp-rbtn::after {
|
||
content: '';
|
||
position: absolute;
|
||
width: 6px; height: 6px;
|
||
border-radius: 50%;
|
||
background: var(--accent);
|
||
top: 3px; left: 3px;
|
||
opacity: 0;
|
||
transition: opacity 0.15s;
|
||
}
|
||
.sp-radio input:checked + .sp-rbtn { border-color: var(--accent); }
|
||
.sp-radio input:checked + .sp-rbtn::after { opacity: 1; }
|
||
.sp-radio input:checked ~ .sp-text { color: var(--text-bright); }
|
||
|
||
.sp-slider {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 7px;
|
||
width: 100%;
|
||
}
|
||
.sp-slider input[type=range] {
|
||
flex: 1;
|
||
accent-color: var(--accent);
|
||
height: 3px;
|
||
cursor: pointer;
|
||
}
|
||
.sp-slider .sp-val {
|
||
font-family: 'Share Tech Mono', monospace;
|
||
font-size: 0.70rem;
|
||
color: var(--accent);
|
||
min-width: 24px;
|
||
text-align: right;
|
||
}</style>
|
||
<main>
|
||
<div class="container">
|
||
|
||
<section class="planetarium-toolbar">
|
||
<div class="info-bar">
|
||
<label class="toolbar-location">
|
||
<span>Ort</span>
|
||
<select class="toolbar-location-select" id="locationSelect">
|
||
<?php foreach ($availableLocations as $locationOption): ?>
|
||
<option
|
||
value="<?= htmlspecialchars((string)($locationOption['id'] ?? ''), ENT_QUOTES, 'UTF-8') ?>"
|
||
<?= (($locationOption['id'] ?? null) === ($planetariumLocation['id'] ?? null)) ? ' selected' : '' ?>
|
||
>
|
||
<?= htmlspecialchars((string)$locationOption['name'], ENT_QUOTES, 'UTF-8') ?>
|
||
</option>
|
||
<?php endforeach; ?>
|
||
</select>
|
||
</label>
|
||
<span>LAT <span class="val" id="lblLat">50.3547° N</span></span>
|
||
<span>LON <span class="val" id="lblLon">11.1900° E</span></span>
|
||
<span>UTC <span class="val" id="lblTime">--:--:--</span></span>
|
||
<span>LST <span class="val" id="lblLST">--h--m</span></span>
|
||
<span>Sterne <span class="val" id="lblCount">0</span></span>
|
||
</div>
|
||
|
||
<div class="controls">
|
||
<div style="display:flex;flex-direction:column;gap:3px;">
|
||
<div class="ctrl-group">
|
||
<input type="datetime-local" id="dtPicker">
|
||
<button class="btn live" id="btnNow">● JETZT</button>
|
||
</div>
|
||
<!-- Zeitsprung-Buttons: Sterntag = 86164.0916s -->
|
||
<div class="time-steps">
|
||
<div class="step-row">
|
||
<button class="step-btn" data-sec="-300">−5m</button>
|
||
<button class="step-btn" data-sec="-900">−15m</button>
|
||
<button class="step-btn" data-sec="-3600">−1h</button>
|
||
<button class="step-btn" data-sec="-7200">−2h</button>
|
||
<button class="step-btn" data-sec="-43200">−12h</button>
|
||
<button class="step-btn" data-sec="-86164.0916">−1★</button>
|
||
<span class="step-label">◀ zurück</span>
|
||
</div>
|
||
<div class="step-row">
|
||
<button class="step-btn" data-sec="300">+5m</button>
|
||
<button class="step-btn" data-sec="900">+15m</button>
|
||
<button class="step-btn" data-sec="3600">+1h</button>
|
||
<button class="step-btn" data-sec="7200">+2h</button>
|
||
<button class="step-btn" data-sec="43200">+12h</button>
|
||
<button class="step-btn" data-sec="86164.0916">+1★</button>
|
||
<span class="step-label">vor ▶</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Einstellungen -->
|
||
<div id="settingsWrap">
|
||
<button id="btnSettings" title="Anzeigeeinstellungen">⚙</button>
|
||
<input type="file" id="fileInput" accept=".csv" style="display:none">
|
||
<div id="settingsPanel">
|
||
<div class="sp-title">⚙ Anzeigeeinstellungen</div>
|
||
|
||
<div class="sp-row" style="flex-direction:column;align-items:flex-start;gap:5px;">
|
||
<span class="sp-label">Koordinatengitter</span>
|
||
<div class="sp-radio-group">
|
||
<label class="sp-radio"><input type="radio" name="gridMode" id="gridEq" value="eq" checked><span class="sp-rbtn"></span><span class="sp-text">Äquatorial</span></label>
|
||
<label class="sp-radio"><input type="radio" name="gridMode" id="gridAltaz" value="altaz" ><span class="sp-rbtn"></span><span class="sp-text">Alt / Az</span></label>
|
||
<label class="sp-radio"><input type="radio" name="gridMode" id="gridOff" value="off" ><span class="sp-rbtn"></span><span class="sp-text">Aus</span></label>
|
||
</div>
|
||
<div class="sp-toggles" style="margin-top:2px;">
|
||
<label class="sp-toggle"><input type="checkbox" id="cfgEqLabels" checked><span class="sp-pill"></span><span class="sp-text">Beschrift.</span></label>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="sp-row">
|
||
<span class="sp-label">Ekliptik</span>
|
||
<div class="sp-toggles">
|
||
<label class="sp-toggle"><input type="checkbox" id="cfgEclipShow" checked><span class="sp-pill"></span><span class="sp-text">an</span></label>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="sp-row">
|
||
<span class="sp-label">Sternbilder</span>
|
||
<div class="sp-toggles">
|
||
<label class="sp-toggle"><input type="checkbox" id="cfgConstShow" checked><span class="sp-pill"></span><span class="sp-text">Linien</span></label>
|
||
<label class="sp-toggle"><input type="checkbox" id="cfgConstLabels" checked><span class="sp-pill"></span><span class="sp-text">Namen</span></label>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="sp-row">
|
||
<span class="sp-label">Sterne</span>
|
||
<div class="sp-toggles">
|
||
<label class="sp-toggle"><input type="checkbox" id="cfgStarsShow" checked><span class="sp-pill"></span><span class="sp-text">Sterne</span></label>
|
||
<label class="sp-toggle"><input type="checkbox" id="cfgStarsLabels" checked><span class="sp-pill"></span><span class="sp-text">Namen</span></label>
|
||
<label class="sp-toggle"><input type="checkbox" id="cfgStarsAutoMag" checked><span class="sp-pill"></span><span class="sp-text">Auto Zoom</span></label>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="sp-row" style="flex-direction:column;align-items:flex-start;gap:3px;">
|
||
<span class="sp-label">Sterne Mag-Limit</span>
|
||
<div class="sp-slider">
|
||
<input type="range" id="magLimit" min="1" max="12" step="0.5" value="12">
|
||
<span class="sp-val" id="magVal">12.0</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="sp-row">
|
||
<span class="sp-label">Planeten</span>
|
||
<div class="sp-toggles">
|
||
<label class="sp-toggle"><input type="checkbox" id="cfgPlanetsShow" checked><span class="sp-pill"></span><span class="sp-text">Objekte</span></label>
|
||
<label class="sp-toggle"><input type="checkbox" id="cfgPlanetsLabels" checked><span class="sp-pill"></span><span class="sp-text">Namen</span></label>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="sp-row">
|
||
<span class="sp-label">Sonne / Mond</span>
|
||
<div class="sp-toggles">
|
||
<label class="sp-toggle"><input type="checkbox" id="cfgSunMoonShow" checked><span class="sp-pill"></span><span class="sp-text">Objekte</span></label>
|
||
<label class="sp-toggle"><input type="checkbox" id="cfgSunMoonLabels" checked><span class="sp-pill"></span><span class="sp-text">Labels</span></label>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="sp-row">
|
||
<span class="sp-label">Messier</span>
|
||
<div class="sp-toggles">
|
||
<label class="sp-toggle"><input type="checkbox" id="cfgMessierShow" checked><span class="sp-pill"></span><span class="sp-text">Objekte</span></label>
|
||
<label class="sp-toggle"><input type="checkbox" id="cfgMessierLabels" checked><span class="sp-pill"></span><span class="sp-text">Namen</span></label>
|
||
<label class="sp-toggle"><input type="checkbox" id="cfgMessierAutoMag" checked><span class="sp-pill"></span><span class="sp-text">Auto Zoom</span></label>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="sp-row" style="flex-direction:column;align-items:flex-start;gap:3px;">
|
||
<span class="sp-label">Messier Mag-Limit</span>
|
||
<div class="sp-slider">
|
||
<input type="range" id="magLimitMessier" min="1" max="16" step="0.5" value="6">
|
||
<span class="sp-val" id="magValMessier">6.0</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="sp-title" style="margin-top:10px;">⬇ Bild exportieren</div>
|
||
|
||
<div class="sp-row">
|
||
<button id="btnExport" style="width:100%;background:#0a1f30;border:1px solid var(--accent);color:var(--accent);padding:5px;font-family:'Rajdhani',sans-serif;font-size:0.82rem;cursor:pointer;letter-spacing:0.08em;">
|
||
⬇ PNG speichern (1000 × 1000 px)
|
||
</button>
|
||
</div>
|
||
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<div id="main">
|
||
<canvas id="sky"></canvas>
|
||
<div id="tooltip"></div>
|
||
<div id="zoomIndicator"></div>
|
||
</div>
|
||
|
||
<script>
|
||
window.addEventListener('load', function() {
|
||
|
||
if (typeof Astronomy === 'undefined') {
|
||
console.warn('astronomy-engine.js nicht geladen');
|
||
return;
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════════════════════════════════
|
||
// KONFIGURATION – Defaults + optionaler fetch von config.json
|
||
// ═══════════════════════════════════════════════════════════════════════════════
|
||
const CFG_DEFAULT = {
|
||
equatorialGrid: {
|
||
decStep: 10, raStep: 2,
|
||
colorEquator: 'rgba(70,150,255,0.60)', colorLines: 'rgba(45,95,155,0.30)',
|
||
widthEquator: 1.1, widthLines: 0.5, dash: [3,6],
|
||
colorLabels: 'rgba(70,120,195,0.60)', labelDecStep: 20
|
||
},
|
||
altazGrid: {
|
||
altStep: 15, altMax: 75, azStep: 30,
|
||
colorCircles: 'rgba(28,55,80,0.40)', colorLines: 'rgba(28,55,80,0.30)',
|
||
width: 0.5, colorLabels: 'rgba(55,95,130,0.45)'
|
||
},
|
||
ecliptic: {
|
||
color: 'rgba(255,175,35,0.52)', width: 1.1, dash: [5,5]
|
||
},
|
||
constellations: {
|
||
color: 'rgba(100,160,220,0.38)', width: 0.8, dash: [4,4],
|
||
colorLabels: 'rgba(120,175,230,0.50)', fontLabels: '9px Rajdhani, sans-serif'
|
||
},
|
||
stars: {
|
||
maxRenderMag: 12.0, defaultMagLimit: 12.0,
|
||
glowBelowMag: 2.0, labelBelowMag: 2.5,
|
||
colorLabel: 'rgba(155,198,228,0.80)', fontLabel: '10px Rajdhani,sans-serif'
|
||
},
|
||
messier: {
|
||
colorOc:'#ffe08a', strokeOc:'#ffcc44',
|
||
colorGc:'#ffaa44', strokeGc:'#ff8800',
|
||
colorNb:'#66ccff', strokeNb:'#33aaff',
|
||
colorSc:'#88ddaa', strokeSc:'#44bb77',
|
||
colorPn:'#aaffaa', strokePn:'#55ee55',
|
||
colorGx:'#ffaacc', strokeGx:'#ff66aa',
|
||
colorAs:'#aaaaaa', strokeAs:'#888888',
|
||
fontLabels: '9px Rajdhani,sans-serif'
|
||
},
|
||
planets: {
|
||
Mercury: { color:'#b0b0b0', r:3.5 }, Venus: { color:'#fffacd', r:5.0 },
|
||
Mars: { color:'#ff5533', r:4.0 }, Jupiter: { color:'#ffddaa', r:5.5 },
|
||
Saturn: { color:'#ffe496', r:4.5 }, Uranus: { color:'#88ffee', r:3.5 },
|
||
Neptune: { color:'#4466ff', r:3.0 }
|
||
},
|
||
sun: {
|
||
radius:12, colorCenter:'#fff7aa', colorMid:'#ffdd00', colorEdge:'#ff9900',
|
||
colorLabel:'#ffdd44', colorLabelBelow:'#aa8800'
|
||
},
|
||
moon: {
|
||
radius:10, colorCenter:'#f0ead8', colorMid:'#ddd5be',
|
||
colorEdge:'#c8c0a8', colorLabel:'#c8c0a8'
|
||
},
|
||
horizon: { color:'rgba(50,165,85,0.38)', width:8 },
|
||
compass: {
|
||
colorMajor:'#4a9eff', colorMinor:'rgba(74,158,255,0.42)',
|
||
fontMajor:'bold 13px Rajdhani,sans-serif', fontMinor:'10px Rajdhani,sans-serif'
|
||
},
|
||
sky: {
|
||
colorCenter:'#070f1b', colorMid:'#040a11',
|
||
colorEdge:'#020508', background:'#020408'
|
||
}
|
||
};
|
||
|
||
// Tiefes Merge: cfg überschreibt defaults rekursiv
|
||
function deepMerge(def, over) {
|
||
const out = Object.assign({}, def);
|
||
for (const k of Object.keys(over || {})) {
|
||
if (over[k] && typeof over[k] === 'object' && !Array.isArray(over[k]))
|
||
out[k] = deepMerge(def[k] || {}, over[k]);
|
||
else
|
||
out[k] = over[k];
|
||
}
|
||
return out;
|
||
}
|
||
|
||
let CFG = CFG_DEFAULT; // wird ggf. durch config.json überschrieben
|
||
const HEADER_LOCATION = <?= json_encode($planetariumLocation, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>;
|
||
const USER_LOCATIONS = <?= json_encode($availableLocations, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>;
|
||
const LOCATION_BY_ID = new Map(USER_LOCATIONS.map(loc => [String(loc.id ?? ''), loc]));
|
||
|
||
// ═══════════════════════════════════════════════════════════════════════════════
|
||
// STANDORT – dynamisch (Standard: Sternwarte Sonneberg)
|
||
// ═══════════════════════════════════════════════════════════════════════════════
|
||
let LAT_DEG = Number(HEADER_LOCATION.latitude);
|
||
let LON_DEG = Number(HEADER_LOCATION.longitude);
|
||
let ELEV_M = Number(HEADER_LOCATION.elevation || 0);
|
||
let OBSERVER = new Astronomy.Observer(LAT_DEG, LON_DEG, ELEV_M);
|
||
|
||
function setLocation(lat, lon, elev, name = null, locationId = null) {
|
||
LAT_DEG = lat;
|
||
LON_DEG = lon;
|
||
ELEV_M = elev || 0;
|
||
OBSERVER = new Astronomy.Observer(LAT_DEG, LON_DEG, ELEV_M);
|
||
// Info-Leiste aktualisieren
|
||
const fmt = (v, pos, neg) => Math.abs(v).toFixed(4) + '° ' + (v >= 0 ? pos : neg);
|
||
const locationSelectEl = document.getElementById('locationSelect');
|
||
if (locationSelectEl && locationId !== null && locationId !== undefined) {
|
||
locationSelectEl.value = String(locationId);
|
||
}
|
||
document.getElementById('lblLat').textContent = fmt(LAT_DEG, 'N', 'S');
|
||
document.getElementById('lblLon').textContent = fmt(LON_DEG, 'E', 'W');
|
||
render();
|
||
}
|
||
|
||
function applyLocationOption(locationId) {
|
||
const location = LOCATION_BY_ID.get(String(locationId));
|
||
if (!location) return;
|
||
setLocation(
|
||
Number(location.latitude),
|
||
Number(location.longitude),
|
||
Number(location.elevation || 0),
|
||
location.name || 'Standort',
|
||
location.id ?? ''
|
||
);
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════════════════════════════════
|
||
// ZUSTAND
|
||
// ═══════════════════════════════════════════════════════════════════════════════
|
||
let stars = [];
|
||
let magLimit = 12.0;
|
||
let messierMagLimit = 6.0;
|
||
let autoStarMagByZoom = true;
|
||
let autoMessierMagByZoom = true;
|
||
let liveMode = true;
|
||
let simTime = new Date();
|
||
|
||
let showEq = true;
|
||
let showAltaz = false;
|
||
let showPlanets = true;
|
||
let showEclip = true;
|
||
let showMessier = true;
|
||
let showConst = true;
|
||
|
||
// Label-Sichtbarkeit (separat von Layer-Sichtbarkeit)
|
||
let labelEq = true;
|
||
let labelAltaz = true;
|
||
let labelConst = true;
|
||
let labelStars = true;
|
||
let labelPlanets = true;
|
||
let labelSunMoon = true;
|
||
let labelMessier = true;
|
||
let showStars = true;
|
||
|
||
// ═══════════════════════════════════════════════════════════════════════════════
|
||
// MESSIER KATALOG – 110 Objekte, J2000, Koordinaten nach SIMBAD/NED
|
||
// ra in Stunden, dec in Grad, mag: integrierte Helligkeit, size: Bogeminuten
|
||
// type: Oc=offener Haufen, Gc=Kugelhaufen, Nb=Nebel, Gx=Galaxie,
|
||
// Pn=Planetarischer Nebel, Sc=Sternhaufen+Nebel, As=Asterismus
|
||
// ═══════════════════════════════════════════════════════════════════════════════
|
||
const MESSIER_FALLBACK = [
|
||
{m:1, name:'Krebsnebel', type:'Pn', ra:5.5755, dec:22.0145, mag:8.4, size:7.0},
|
||
{m:2, name:'', type:'Gc', ra:21.5577, dec:-0.8233, mag:6.5, size:16.0},
|
||
{m:3, name:'', type:'Gc', ra:13.7033, dec:28.3772, mag:6.2, size:18.0},
|
||
{m:4, name:'', type:'Gc', ra:16.3933, dec:-26.5258, mag:5.9, size:26.0},
|
||
{m:5, name:'', type:'Gc', ra:15.3097, dec:2.0825, mag:5.8, size:17.4},
|
||
{m:6, name:'Schmetterlingshaufen', type:'Oc', ra:17.6675, dec:-32.2167, mag:4.2, size:25.0},
|
||
{m:7, name:'Ptolemäus-Haufen', type:'Oc', ra:17.8975, dec:-34.8167, mag:3.3, size:80.0},
|
||
{m:8, name:'Lagunennebel', type:'Nb', ra:18.0628, dec:-24.3833, mag:5.8, size:90.0},
|
||
{m:9, name:'', type:'Gc', ra:17.3197, dec:-18.5158, mag:7.9, size:9.3},
|
||
{m:10, name:'', type:'Gc', ra:16.9522, dec:-4.1006, mag:6.4, size:15.1},
|
||
{m:11, name:'Wildentenhaufen', type:'Oc', ra:18.8511, dec:-6.2667, mag:5.8, size:14.0},
|
||
{m:12, name:'', type:'Gc', ra:16.7872, dec:-1.9486, mag:6.7, size:14.5},
|
||
{m:13, name:'Herkuleshaufen', type:'Gc', ra:16.6947, dec:36.4603, mag:5.8, size:20.0},
|
||
{m:14, name:'', type:'Gc', ra:17.6261, dec:-3.2458, mag:7.6, size:11.7},
|
||
{m:15, name:'', type:'Gc', ra:21.4997, dec:12.1669, mag:6.2, size:12.3},
|
||
{m:16, name:'Adlernebel', type:'Sc', ra:18.3133, dec:-13.7833, mag:6.4, size:7.0},
|
||
{m:17, name:'Omeganebel', type:'Nb', ra:18.3461, dec:-16.1833, mag:6.0, size:11.0},
|
||
{m:18, name:'', type:'Oc', ra:18.3325, dec:-17.1333, mag:7.5, size:9.0},
|
||
{m:19, name:'', type:'Gc', ra:17.0433, dec:-26.2678, mag:6.8, size:13.5},
|
||
{m:20, name:'Trifidnebel', type:'Nb', ra:18.0436, dec:-23.0333, mag:6.3, size:28.0},
|
||
{m:21, name:'', type:'Oc', ra:18.0736, dec:-22.5000, mag:6.5, size:13.0},
|
||
{m:22, name:'', type:'Gc', ra:18.6053, dec:-23.9047, mag:5.1, size:24.0},
|
||
{m:23, name:'', type:'Oc', ra:17.9508, dec:-19.0167, mag:6.9, size:27.0},
|
||
{m:24, name:'Sagittarius-Sternwolke', type:'As', ra:18.2833, dec:-18.5500, mag:4.6, size:90.0},
|
||
{m:25, name:'', type:'Oc', ra:18.5258, dec:-19.2333, mag:4.6, size:32.0},
|
||
{m:26, name:'', type:'Oc', ra:18.7550, dec:-9.3833, mag:8.0, size:15.0},
|
||
{m:27, name:'Hantelnebel', type:'Pn', ra:19.9939, dec:22.7211, mag:7.5, size:8.0},
|
||
{m:28, name:'', type:'Gc', ra:18.4097, dec:-24.8697, mag:6.8, size:11.2},
|
||
{m:29, name:'', type:'Oc', ra:20.3992, dec:38.5333, mag:7.1, size:7.0},
|
||
{m:30, name:'', type:'Gc', ra:21.6728, dec:-23.1800, mag:7.2, size:11.0},
|
||
{m:31, name:'Andromedagalaxie', type:'Gx', ra:0.7122, dec:41.2692, mag:3.4, size:190.0},
|
||
{m:32, name:'', type:'Gx', ra:0.7114, dec:40.8656, mag:8.7, size:8.7},
|
||
{m:33, name:'Dreiecksgalaxie', type:'Gx', ra:1.5644, dec:30.6603, mag:5.7, size:62.0},
|
||
{m:34, name:'', type:'Oc', ra:2.7019, dec:42.7667, mag:5.5, size:35.0},
|
||
{m:35, name:'', type:'Oc', ra:6.1483, dec:24.3333, mag:5.3, size:28.0},
|
||
{m:36, name:'', type:'Oc', ra:5.6025, dec:34.1333, mag:6.3, size:12.0},
|
||
{m:37, name:'', type:'Oc', ra:5.8728, dec:32.5500, mag:6.2, size:24.0},
|
||
{m:38, name:'', type:'Oc', ra:5.4783, dec:35.8500, mag:7.4, size:21.0},
|
||
{m:39, name:'', type:'Oc', ra:21.5325, dec:48.4333, mag:4.6, size:32.0},
|
||
{m:40, name:'Winnecke 4', type:'As', ra:12.3722, dec:58.0833, mag:8.4, size:0.8},
|
||
{m:41, name:'', type:'Oc', ra:6.7669, dec:-20.7333, mag:4.5, size:38.0},
|
||
{m:42, name:'Orionnebel', type:'Nb', ra:5.5886, dec:-5.3911, mag:4.0, size:85.0},
|
||
{m:43, name:'', type:'Nb', ra:5.5944, dec:-5.2708, mag:9.0, size:20.0},
|
||
{m:44, name:'Krippenaufen/Praesepe', type:'Oc', ra:8.6678, dec:19.9833, mag:3.7, size:70.0},
|
||
{m:45, name:'Plejaden', type:'Oc', ra:3.7908, dec:24.1167, mag:1.6, size:110.0},
|
||
{m:46, name:'', type:'Oc', ra:7.6961, dec:-14.8167, mag:6.0, size:27.0},
|
||
{m:47, name:'', type:'Oc', ra:7.6103, dec:-14.5000, mag:5.2, size:30.0},
|
||
{m:48, name:'', type:'Oc', ra:8.2292, dec:-5.8000, mag:5.5, size:54.0},
|
||
{m:49, name:'', type:'Gx', ra:12.4961, dec:8.0003, mag:8.4, size:9.8},
|
||
{m:50, name:'', type:'Oc', ra:7.0494, dec:-8.3667, mag:6.3, size:16.0},
|
||
{m:51, name:'Strudelgalaxie', type:'Gx', ra:13.4997, dec:47.1953, mag:8.4, size:11.2},
|
||
{m:52, name:'', type:'Oc', ra:23.4006, dec:61.5833, mag:7.3, size:13.0},
|
||
{m:53, name:'', type:'Gc', ra:13.2158, dec:18.1681, mag:7.6, size:12.6},
|
||
{m:54, name:'', type:'Gc', ra:18.9178, dec:-30.4786, mag:7.7, size:9.1},
|
||
{m:55, name:'', type:'Gc', ra:19.6669, dec:-30.9644, mag:6.3, size:19.0},
|
||
{m:56, name:'', type:'Gc', ra:19.2758, dec:30.1847, mag:8.3, size:7.1},
|
||
{m:57, name:'Ringnebel', type:'Pn', ra:18.8931, dec:33.0289, mag:8.8, size:1.4},
|
||
{m:58, name:'', type:'Gx', ra:12.6278, dec:11.8181, mag:9.7, size:5.9},
|
||
{m:59, name:'', type:'Gx', ra:12.7003, dec:11.6472, mag:9.6, size:5.4},
|
||
{m:60, name:'', type:'Gx', ra:12.7272, dec:11.5528, mag:8.8, size:7.6},
|
||
{m:61, name:'', type:'Gx', ra:12.3650, dec:4.4736, mag:9.7, size:6.0},
|
||
{m:62, name:'', type:'Gc', ra:17.0211, dec:-30.1128, mag:6.5, size:14.1},
|
||
{m:63, name:'Sonnenblumengalaxie', type:'Gx', ra:13.2644, dec:42.0294, mag:8.6, size:12.6},
|
||
{m:64, name:'Blackeye-Galax.', type:'Gx', ra:12.9456, dec:21.6828, mag:8.5, size:9.3},
|
||
{m:65, name:'', type:'Gx', ra:11.3153, dec:13.0919, mag:9.3, size:8.7},
|
||
{m:66, name:'', type:'Gx', ra:11.3369, dec:12.9911, mag:8.9, size:9.1},
|
||
{m:67, name:'', type:'Oc', ra:8.8556, dec:11.8167, mag:6.9, size:25.0},
|
||
{m:68, name:'', type:'Gc', ra:12.6567, dec:-26.7444, mag:7.8, size:12.0},
|
||
{m:69, name:'', type:'Gc', ra:18.5231, dec:-32.3483, mag:7.6, size:7.1},
|
||
{m:70, name:'', type:'Gc', ra:18.7208, dec:-32.2917, mag:7.9, size:7.8},
|
||
{m:71, name:'', type:'Gc', ra:19.8964, dec:18.7792, mag:6.1, size:7.2},
|
||
{m:72, name:'', type:'Gc', ra:20.8908, dec:-12.5375, mag:9.3, size:5.9},
|
||
{m:73, name:'', type:'As', ra:20.9833, dec:-12.6333, mag:9.0, size:2.8},
|
||
{m:74, name:'', type:'Gx', ra:1.6114, dec:15.7836, mag:9.4, size:10.5},
|
||
{m:75, name:'', type:'Gc', ra:20.1014, dec:-21.9217, mag:8.5, size:6.0},
|
||
{m:76, name:'Kleiner Hantelnebel', type:'Pn', ra:1.7058, dec:51.5753, mag:10.1, size:2.7},
|
||
{m:77, name:'', type:'Gx', ra:2.7119, dec:-0.0133, mag:8.9, size:7.1},
|
||
{m:78, name:'', type:'Nb', ra:5.7789, dec:0.0833, mag:8.3, size:8.0},
|
||
{m:79, name:'', type:'Gc', ra:5.4017, dec:-24.5239, mag:7.7, size:9.6},
|
||
{m:80, name:'', type:'Gc', ra:16.2847, dec:-22.9758, mag:7.3, size:10.0},
|
||
{m:81, name:'Bodes Galaxie', type:'Gx', ra:9.9258, dec:69.0653, mag:6.9, size:21.0},
|
||
{m:82, name:'Zigarrengalaxie', type:'Gx', ra:9.9317, dec:69.6797, mag:8.4, size:11.2},
|
||
{m:83, name:'Südl. Feuerradgal.', type:'Gx', ra:13.6169, dec:-29.8658, mag:7.6, size:12.9},
|
||
{m:84, name:'', type:'Gx', ra:12.4197, dec:12.8869, mag:9.1, size:6.5},
|
||
{m:85, name:'', type:'Gx', ra:12.4233, dec:18.1911, mag:9.1, size:7.1},
|
||
{m:86, name:'', type:'Gx', ra:12.4361, dec:12.9461, mag:8.9, size:8.9},
|
||
{m:87, name:'Virgogalaxie', type:'Gx', ra:12.5136, dec:12.3911, mag:8.6, size:7.2},
|
||
{m:88, name:'', type:'Gx', ra:12.5319, dec:14.4197, mag:9.6, size:6.9},
|
||
{m:89, name:'', type:'Gx', ra:12.5942, dec:12.5564, mag:9.8, size:5.1},
|
||
{m:90, name:'', type:'Gx', ra:12.6133, dec:13.1628, mag:9.5, size:9.5},
|
||
{m:91, name:'', type:'Gx', ra:12.5917, dec:14.4961, mag:10.2, size:5.4},
|
||
{m:92, name:'', type:'Gc', ra:17.2850, dec:43.1358, mag:6.4, size:11.2},
|
||
{m:93, name:'', type:'Oc', ra:7.7417, dec:-23.8500, mag:6.0, size:22.0},
|
||
{m:94, name:'', type:'Gx', ra:12.8481, dec:41.1203, mag:8.2, size:7.4},
|
||
{m:95, name:'', type:'Gx', ra:10.7328, dec:11.7039, mag:9.7, size:7.4},
|
||
{m:96, name:'', type:'Gx', ra:10.7794, dec:11.8200, mag:9.2, size:7.6},
|
||
{m:97, name:'Eulennebel', type:'Pn', ra:11.2481, dec:55.0194, mag:9.9, size:3.4},
|
||
{m:98, name:'', type:'Gx', ra:12.2322, dec:14.9003, mag:10.1, size:9.8},
|
||
{m:99, name:'', type:'Gx', ra:12.3133, dec:14.4167, mag:9.9, size:5.4},
|
||
{m:100, name:'', type:'Gx', ra:12.3819, dec:15.8228, mag:9.3, size:7.4},
|
||
{m:101, name:'Feuerradgalaxie', type:'Gx', ra:14.0531, dec:54.3489, mag:7.9, size:22.0},
|
||
{m:102, name:'Spindelgalaxie', type:'Gx', ra:15.1086, dec:55.7631, mag:9.9, size:5.2},
|
||
{m:103, name:'', type:'Oc', ra:1.5553, dec:60.6500, mag:7.4, size:6.0},
|
||
{m:104, name:'Sombrerogalaxie', type:'Gx', ra:12.6664, dec:-11.6231, mag:8.0, size:8.7},
|
||
{m:105, name:'', type:'Gx', ra:10.7978, dec:12.5814, mag:9.8, size:4.6},
|
||
{m:106, name:'', type:'Gx', ra:12.3161, dec:47.3039, mag:8.4, size:18.6},
|
||
{m:107, name:'', type:'Gc', ra:16.5419, dec:-13.0531, mag:7.9, size:10.0},
|
||
{m:108, name:'', type:'Gx', ra:11.1917, dec:55.6744, mag:10.0, size:8.7},
|
||
{m:109, name:'', type:'Gx', ra:11.9578, dec:53.3747, mag:9.8, size:7.6},
|
||
{m:110, name:'', type:'Gx', ra:0.6733, dec:41.6853, mag:8.5, size:17.0},
|
||
];
|
||
|
||
const EMBEDDED_CONSTELLATION_DATA = <?php echo $constellationDataJson ?: '{"constellations":[]}'; ?>;
|
||
const EMBEDDED_CONSTELLATION_ERROR = <?php echo $constellationDataErrorJson ?: 'null'; ?>;
|
||
const EMBEDDED_STAR_DATA = <?php echo $starDataJson ?: '[]'; ?>;
|
||
const EMBEDDED_STAR_ERROR = <?php echo $starDataErrorJson ?: 'null'; ?>;
|
||
const EMBEDDED_MESSIER_DATA = <?php echo $messierDataJson ?: '[]'; ?>;
|
||
const EMBEDDED_MESSIER_ERROR = <?php echo $messierDataErrorJson ?: 'null'; ?>;
|
||
const MESSIER = Array.isArray(EMBEDDED_MESSIER_DATA) && EMBEDDED_MESSIER_DATA.length
|
||
? EMBEDDED_MESSIER_DATA
|
||
: MESSIER_FALLBACK;
|
||
let constellationData = EMBEDDED_CONSTELLATION_DATA;
|
||
let hipMap = new Map();
|
||
|
||
function buildMStyle() {
|
||
return {
|
||
Oc: { color: CFG.messier.colorOc, stroke: CFG.messier.strokeOc },
|
||
Gc: { color: CFG.messier.colorGc, stroke: CFG.messier.strokeGc },
|
||
Nb: { color: CFG.messier.colorNb, stroke: CFG.messier.strokeNb },
|
||
Sc: { color: CFG.messier.colorSc, stroke: CFG.messier.strokeSc },
|
||
Pn: { color: CFG.messier.colorPn, stroke: CFG.messier.strokePn },
|
||
Gx: { color: CFG.messier.colorGx, stroke: CFG.messier.strokeGx },
|
||
As: { color: CFG.messier.colorAs, stroke: CFG.messier.strokeAs },
|
||
};
|
||
}
|
||
let M_STYLE = buildMStyle();
|
||
|
||
function getTime() { return liveMode ? new Date() : simTime; }
|
||
|
||
// ═══════════════════════════════════════════════════════════════════════════════
|
||
// CANVAS
|
||
// ═══════════════════════════════════════════════════════════════════════════════
|
||
const canvas = document.getElementById('sky');
|
||
let ctx = canvas.getContext('2d');
|
||
let W, H, R, cx, cy;
|
||
let baseR = 0;
|
||
let zoomLevel = 1.0;
|
||
let panX = 0, panY = 0;
|
||
const ZOOM_MIN = 1.0, ZOOM_MAX = 12.0;
|
||
|
||
function resize() {
|
||
const el = document.getElementById('main');
|
||
W = el.clientWidth; H = el.clientHeight;
|
||
canvas.width = W; canvas.height = H;
|
||
baseR = Math.min(W, H) * 0.455;
|
||
R = baseR * zoomLevel;
|
||
cx = W / 2 + panX;
|
||
cy = H / 2 + panY;
|
||
render();
|
||
}
|
||
window.addEventListener('resize', resize);
|
||
|
||
// ═══════════════════════════════════════════════════════════════════════════════
|
||
// ZOOM & PAN
|
||
// ═══════════════════════════════════════════════════════════════════════════════
|
||
function applyZoom(newZoom, pivotX, pivotY) {
|
||
newZoom = Math.max(ZOOM_MIN, Math.min(ZOOM_MAX, newZoom));
|
||
const scale = newZoom / zoomLevel;
|
||
// Pivot-Punkt bleibt fixiert: panX/Y so anpassen dass der Himmelspunkt
|
||
// unter dem Mauszeiger stabil bleibt
|
||
panX = (pivotX - W / 2) * (1 - scale) + panX * scale;
|
||
panY = (pivotY - H / 2) * (1 - scale) + panY * scale;
|
||
zoomLevel = newZoom;
|
||
R = baseR * zoomLevel;
|
||
cx = W / 2 + panX;
|
||
cy = H / 2 + panY;
|
||
applyAutoMagLimitFromZoom();
|
||
applyAutoMessierMagLimitFromZoom();
|
||
updateZoomIndicator();
|
||
render();
|
||
}
|
||
|
||
function resetZoom() {
|
||
zoomLevel = 1.0; panX = 0; panY = 0;
|
||
R = baseR; cx = W / 2; cy = H / 2;
|
||
applyAutoMagLimitFromZoom();
|
||
applyAutoMessierMagLimitFromZoom();
|
||
updateZoomIndicator();
|
||
render();
|
||
}
|
||
|
||
function updateZoomIndicator() {
|
||
const el = document.getElementById('zoomIndicator');
|
||
if (!el) return;
|
||
if (zoomLevel <= 1.01) { el.style.display = 'none'; return; }
|
||
el.style.display = 'block';
|
||
el.textContent = zoomLevel.toFixed(1) + '×';
|
||
}
|
||
|
||
function getAutoMagLimitForZoom() {
|
||
const zoomClamped = Math.max(ZOOM_MIN, Math.min(ZOOM_MAX, zoomLevel));
|
||
const normalized = (zoomClamped - ZOOM_MIN) / (ZOOM_MAX - ZOOM_MIN);
|
||
const rawMag = 6 + normalized * 6;
|
||
return Math.round(rawMag * 2) / 2;
|
||
}
|
||
|
||
function getAutoMessierMagLimitForZoom() {
|
||
const zoomClamped = Math.max(ZOOM_MIN, Math.min(ZOOM_MAX, zoomLevel));
|
||
const normalized = (zoomClamped - ZOOM_MIN) / (ZOOM_MAX - ZOOM_MIN);
|
||
const rawMag = 6 + normalized * 10;
|
||
return Math.round(rawMag * 2) / 2;
|
||
}
|
||
|
||
function syncMagLimitUi() {
|
||
const slider = document.getElementById('magLimit');
|
||
const valueEl = document.getElementById('magVal');
|
||
if (slider) {
|
||
slider.value = String(magLimit);
|
||
slider.disabled = autoStarMagByZoom;
|
||
}
|
||
if (valueEl) {
|
||
valueEl.textContent = magLimit.toFixed(1);
|
||
}
|
||
}
|
||
|
||
function syncMessierMagLimitUi() {
|
||
const slider = document.getElementById('magLimitMessier');
|
||
const valueEl = document.getElementById('magValMessier');
|
||
if (slider) {
|
||
slider.value = String(messierMagLimit);
|
||
slider.disabled = autoMessierMagByZoom;
|
||
}
|
||
if (valueEl) {
|
||
valueEl.textContent = messierMagLimit.toFixed(1);
|
||
}
|
||
}
|
||
|
||
function applyAutoMagLimitFromZoom() {
|
||
if (!autoStarMagByZoom) return;
|
||
magLimit = getAutoMagLimitForZoom();
|
||
syncMagLimitUi();
|
||
}
|
||
|
||
function applyAutoMessierMagLimitFromZoom() {
|
||
if (!autoMessierMagByZoom) return;
|
||
messierMagLimit = getAutoMessierMagLimitForZoom();
|
||
syncMessierMagLimitUi();
|
||
}
|
||
|
||
// Mausrad → Zoom
|
||
canvas.addEventListener('wheel', function(e) {
|
||
e.preventDefault();
|
||
const rect = canvas.getBoundingClientRect();
|
||
const mx = e.clientX - rect.left;
|
||
const my = e.clientY - rect.top;
|
||
const factor = e.deltaY < 0 ? 1.15 : 1 / 1.15;
|
||
applyZoom(zoomLevel * factor, mx, my);
|
||
}, { passive: false });
|
||
|
||
// Doppelklick → Zoom zurücksetzen
|
||
canvas.addEventListener('dblclick', function(e) {
|
||
if (zoomLevel > 1.01) { resetZoom(); e.stopPropagation(); }
|
||
});
|
||
|
||
// Drag → Pan (nur wenn gezoomt)
|
||
let dragStart = null;
|
||
canvas.addEventListener('mousedown', function(e) {
|
||
if (zoomLevel > 1.01 && e.button === 0) {
|
||
dragStart = { x: e.clientX, y: e.clientY, px: panX, py: panY };
|
||
canvas.style.cursor = 'grabbing';
|
||
}
|
||
});
|
||
window.addEventListener('mousemove', function(e) {
|
||
if (!dragStart) return;
|
||
panX = dragStart.px + (e.clientX - dragStart.x);
|
||
panY = dragStart.py + (e.clientY - dragStart.y);
|
||
R = baseR * zoomLevel;
|
||
cx = W / 2 + panX;
|
||
cy = H / 2 + panY;
|
||
render();
|
||
});
|
||
window.addEventListener('mouseup', function(e) {
|
||
if (dragStart) { dragStart = null; canvas.style.cursor = 'crosshair'; }
|
||
});
|
||
|
||
// Touch-Pinch-Zoom
|
||
let lastTouchDist = null;
|
||
canvas.addEventListener('touchstart', function(e) {
|
||
if (e.touches.length === 2) lastTouchDist = Math.hypot(
|
||
e.touches[0].clientX - e.touches[1].clientX,
|
||
e.touches[0].clientY - e.touches[1].clientY);
|
||
}, { passive: true });
|
||
canvas.addEventListener('touchmove', function(e) {
|
||
if (e.touches.length !== 2 || !lastTouchDist) return;
|
||
e.preventDefault();
|
||
const dist = Math.hypot(
|
||
e.touches[0].clientX - e.touches[1].clientX,
|
||
e.touches[0].clientY - e.touches[1].clientY);
|
||
const rect = canvas.getBoundingClientRect();
|
||
const mx = (e.touches[0].clientX + e.touches[1].clientX) / 2 - rect.left;
|
||
const my = (e.touches[0].clientY + e.touches[1].clientY) / 2 - rect.top;
|
||
applyZoom(zoomLevel * (dist / lastTouchDist), mx, my);
|
||
lastTouchDist = dist;
|
||
}, { passive: false });
|
||
canvas.addEventListener('touchend', () => { lastTouchDist = null; });
|
||
// alt/az in Grad → Canvas-Pixel
|
||
// ═══════════════════════════════════════════════════════════════════════════════
|
||
function project(altDeg, azDeg) {
|
||
if (altDeg < -0.5) return null;
|
||
if (!isFinite(altDeg) || !isFinite(azDeg)) return null;
|
||
const rho = (90.0 - altDeg) / 90.0;
|
||
const azR = azDeg * Math.PI / 180.0;
|
||
const x = cx - rho * R * Math.sin(azR);
|
||
const y = cy - rho * R * Math.cos(azR);
|
||
if (!isFinite(x) || !isFinite(y)) return null;
|
||
return { x, y };
|
||
}
|
||
|
||
function createSafeRadialGradient(x0, y0, r0, x1, y1, r1) {
|
||
const values = [x0, y0, r0, x1, y1, r1];
|
||
if (!values.every(Number.isFinite)) return null;
|
||
if (r0 < 0 || r1 <= 0) return null;
|
||
try {
|
||
return ctx.createRadialGradient(x0, y0, r0, x1, y1, r1);
|
||
} catch {
|
||
return null;
|
||
}
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════════════════════════════════
|
||
// KOORDINATENUMRECHNUNG – ausschließlich via Astronomy Engine
|
||
// ═══════════════════════════════════════════════════════════════════════════════
|
||
// Sterne: ra/dec in Radiant (J2000) → Horizon
|
||
function starHorizon(star, date) {
|
||
const raH = star.ra * (12.0 / Math.PI); // rad → Stunden
|
||
const decD = star.dec * (180.0 / Math.PI); // rad → Grad
|
||
return Astronomy.Horizon(date, OBSERVER, raH, decD, 'normal');
|
||
}
|
||
|
||
// RA in Stunden, Dec in Grad → Horizon
|
||
function eqHorizon(raH, decD, date) {
|
||
return Astronomy.Horizon(date, OBSERVER, raH, decD, 'normal');
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════════════════════════════════
|
||
// PLANETEN
|
||
// ═══════════════════════════════════════════════════════════════════════════════
|
||
const PLANETS = [
|
||
{ body: Astronomy.Body.Mercury, name: 'Merkur', ...CFG.planets.Mercury },
|
||
{ body: Astronomy.Body.Venus, name: 'Venus', ...CFG.planets.Venus },
|
||
{ body: Astronomy.Body.Mars, name: 'Mars', ...CFG.planets.Mars },
|
||
{ body: Astronomy.Body.Jupiter, name: 'Jupiter', ...CFG.planets.Jupiter },
|
||
{ body: Astronomy.Body.Saturn, name: 'Saturn', ...CFG.planets.Saturn },
|
||
{ body: Astronomy.Body.Uranus, name: 'Uranus', ...CFG.planets.Uranus },
|
||
{ body: Astronomy.Body.Neptune, name: 'Neptun', ...CFG.planets.Neptune },
|
||
];
|
||
let planetCache = [];
|
||
|
||
// Sonne & Mond
|
||
let sunCache = null;
|
||
let moonCache = null;
|
||
|
||
function computeSunMoon(date) {
|
||
try {
|
||
// ── Sonne ────────────────────────────────────────────────────────────────
|
||
const sunEq = Astronomy.Equator(Astronomy.Body.Sun, date, OBSERVER, true, true);
|
||
const sunHor = eqHorizon(sunEq.ra, sunEq.dec, date);
|
||
sunCache = {
|
||
name: 'Sonne', ra: sunEq.ra, dec: sunEq.dec,
|
||
alt: sunHor.altitude, az: sunHor.azimuth, _xy: null
|
||
};
|
||
|
||
// ── Mond ────────────────────────────────────────────────────────────────
|
||
// Astronomy.Equator für Mond: geocentrisch=false → topozentrisch
|
||
const moonEq = Astronomy.Equator(Astronomy.Body.Moon, date, OBSERVER, true, true);
|
||
const moonHor = eqHorizon(moonEq.ra, moonEq.dec, date);
|
||
const moonIllum = Astronomy.Illumination(Astronomy.Body.Moon, date);
|
||
const moonPhase = Astronomy.MoonPhase(date); // 0=Neumond, 90=ErstesViertel, 180=Vollmond, 270=LetztesViertel
|
||
moonCache = {
|
||
name: 'Mond', ra: moonEq.ra, dec: moonEq.dec,
|
||
alt: moonHor.altitude, az: moonHor.azimuth,
|
||
phase_fraction: moonIllum.phase_fraction,
|
||
phase_angle: moonIllum.phase_angle,
|
||
moon_phase: moonPhase,
|
||
_xy: null
|
||
};
|
||
} catch(e) { console.warn('Sun/Moon error:', e); }
|
||
}
|
||
|
||
function drawSunMoon(date) {
|
||
computeSunMoon(date);
|
||
|
||
// ── SONNE ─────────────────────────────────────────────────────────────────
|
||
if (sunCache) {
|
||
sunCache._xy = null;
|
||
if (sunCache.alt >= 0) {
|
||
const pt = project(sunCache.alt, sunCache.az);
|
||
if (pt) {
|
||
sunCache._xy = pt;
|
||
const S = CFG.sun;
|
||
const r = S.radius;
|
||
|
||
const sunGlow = createSafeRadialGradient(pt.x, pt.y, r, pt.x, pt.y, r * 5);
|
||
if (sunGlow) {
|
||
sunGlow.addColorStop(0, 'rgba(255,220,80,0.25)');
|
||
sunGlow.addColorStop(1, 'rgba(255,180,0,0)');
|
||
ctx.fillStyle = sunGlow;
|
||
ctx.beginPath(); ctx.arc(pt.x, pt.y, r * 5, 0, Math.PI * 2); ctx.fill();
|
||
}
|
||
|
||
const sunDisk = createSafeRadialGradient(pt.x, pt.y, 0, pt.x, pt.y, r);
|
||
if (sunDisk) {
|
||
sunDisk.addColorStop(0, S.colorCenter);
|
||
sunDisk.addColorStop(0.6, S.colorMid);
|
||
sunDisk.addColorStop(1, S.colorEdge);
|
||
ctx.fillStyle = sunDisk;
|
||
} else {
|
||
ctx.fillStyle = S.colorMid;
|
||
}
|
||
ctx.beginPath(); ctx.arc(pt.x, pt.y, r, 0, Math.PI * 2); ctx.fill();
|
||
|
||
ctx.fillStyle = S.colorLabel;
|
||
ctx.font = 'bold 10px Rajdhani,sans-serif';
|
||
ctx.textAlign = 'left'; ctx.textBaseline = 'middle';
|
||
if (labelSunMoon) ctx.fillText('Sonne', pt.x + r + 4, pt.y);
|
||
}
|
||
}
|
||
}
|
||
|
||
// ── MOND ──────────────────────────────────────────────────────────────────
|
||
if (moonCache && moonCache.alt > 0) {
|
||
const pt = project(moonCache.alt, moonCache.az);
|
||
if (pt) {
|
||
moonCache._xy = pt;
|
||
const M = CFG.moon;
|
||
const r = M.radius;
|
||
|
||
const moonGlow = createSafeRadialGradient(pt.x, pt.y, r, pt.x, pt.y, r * 3.5);
|
||
if (moonGlow) {
|
||
moonGlow.addColorStop(0, 'rgba(210,200,170,0.18)');
|
||
moonGlow.addColorStop(1, 'rgba(180,170,140,0)');
|
||
ctx.fillStyle = moonGlow;
|
||
ctx.beginPath(); ctx.arc(pt.x, pt.y, r * 3.5, 0, Math.PI * 2); ctx.fill();
|
||
}
|
||
|
||
const moonDisk = createSafeRadialGradient(pt.x, pt.y, 0, pt.x, pt.y, r);
|
||
if (moonDisk) {
|
||
moonDisk.addColorStop(0, M.colorCenter);
|
||
moonDisk.addColorStop(0.7, M.colorMid);
|
||
moonDisk.addColorStop(1, M.colorEdge);
|
||
ctx.fillStyle = moonDisk;
|
||
} else {
|
||
ctx.fillStyle = M.colorMid;
|
||
}
|
||
ctx.beginPath(); ctx.arc(pt.x, pt.y, r, 0, Math.PI * 2); ctx.fill();
|
||
|
||
ctx.strokeStyle = 'rgba(200,190,160,0.45)';
|
||
ctx.lineWidth = 0.8;
|
||
ctx.stroke();
|
||
|
||
const pct = Math.round(moonCache.phase_fraction * 100);
|
||
ctx.fillStyle = M.colorLabel;
|
||
ctx.font = 'bold 10px Rajdhani,sans-serif';
|
||
ctx.textAlign = 'left'; ctx.textBaseline = 'middle';
|
||
if (labelSunMoon) ctx.fillText('Mond ' + pct + '%', pt.x + r + 4, pt.y);
|
||
}
|
||
}
|
||
|
||
}
|
||
|
||
function computePlanets(date) {
|
||
planetCache = [];
|
||
for (const p of PLANETS) {
|
||
try {
|
||
const eq = Astronomy.Equator(p.body, date, OBSERVER, true, true);
|
||
const hor = eqHorizon(eq.ra, eq.dec, date);
|
||
const illum = Astronomy.Illumination(p.body, date);
|
||
planetCache.push({ ...p, ra: eq.ra, dec: eq.dec,
|
||
alt: hor.altitude, az: hor.azimuth,
|
||
mag: illum.mag, _xy: null });
|
||
} catch(e) {}
|
||
}
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════════════════════════════════
|
||
// LAYER: ÄQUATORIALGITTER
|
||
// Dec-Kreise alle 10°, RA-Linien alle 2h – vollständig über Astronomy Engine
|
||
// ═══════════════════════════════════════════════════════════════════════════════
|
||
function drawEquatorialGrid(date) {
|
||
const STEPS = 200;
|
||
const C = CFG.equatorialGrid;
|
||
|
||
for (let dec = -80; dec <= 80; dec += C.decStep) {
|
||
const isEq = dec === 0;
|
||
ctx.strokeStyle = isEq ? C.colorEquator : C.colorLines;
|
||
ctx.lineWidth = isEq ? C.widthEquator : C.widthLines;
|
||
ctx.setLineDash(isEq ? [] : C.dash);
|
||
ctx.beginPath();
|
||
let pen = false;
|
||
for (let i = 0; i <= STEPS; i++) {
|
||
const raH = (i / STEPS) * 24.0;
|
||
const hor = eqHorizon(raH, dec, date);
|
||
const pt = project(hor.altitude, hor.azimuth);
|
||
if (!pt) { pen = false; continue; }
|
||
pen ? ctx.lineTo(pt.x, pt.y) : ctx.moveTo(pt.x, pt.y);
|
||
pen = true;
|
||
}
|
||
ctx.stroke();
|
||
|
||
// Label
|
||
if (labelEq && dec !== 0 && dec % C.labelDecStep === 0) {
|
||
for (let raH = 0; raH < 24; raH += 6) {
|
||
const hor = eqHorizon(raH, dec, date);
|
||
if (hor.altitude < 5 || hor.altitude > 80) continue;
|
||
const pt = project(hor.altitude, hor.azimuth);
|
||
if (!pt) continue;
|
||
ctx.fillStyle = C.colorLabels;
|
||
ctx.font = '8px Share Tech Mono,monospace';
|
||
ctx.textAlign = 'left'; ctx.textBaseline = 'middle';
|
||
ctx.fillText((dec > 0 ? '+' : '') + dec + '°', pt.x + 2, pt.y);
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
// ── Rektaszensionslinien ─────────────────────────────────────────────────
|
||
for (let raH = 0; raH < 24; raH += C.raStep) {
|
||
ctx.strokeStyle = C.colorLines;
|
||
ctx.lineWidth = C.widthLines;
|
||
ctx.setLineDash(C.dash);
|
||
ctx.beginPath();
|
||
let pen = false;
|
||
for (let i = 0; i <= STEPS; i++) {
|
||
const dec = -90 + (180 * i / STEPS);
|
||
const hor = eqHorizon(raH, dec, date);
|
||
const pt = project(hor.altitude, hor.azimuth);
|
||
if (!pt) { pen = false; continue; }
|
||
pen ? ctx.lineTo(pt.x, pt.y) : ctx.moveTo(pt.x, pt.y);
|
||
pen = true;
|
||
}
|
||
ctx.stroke();
|
||
|
||
// RA-Label nahe am Horizont
|
||
if (labelEq) for (let dec = -8; dec <= 8; dec += 2) {
|
||
const hor = eqHorizon(raH, dec, date);
|
||
if (hor.altitude < 1 || hor.altitude > 12) continue;
|
||
const pt = project(hor.altitude, hor.azimuth);
|
||
if (!pt) continue;
|
||
ctx.fillStyle = C.colorLabels;
|
||
ctx.font = '8px Share Tech Mono,monospace';
|
||
ctx.textAlign = 'center'; ctx.textBaseline = 'bottom';
|
||
ctx.fillText(raH + 'h', pt.x, pt.y - 2);
|
||
break;
|
||
}
|
||
}
|
||
|
||
ctx.setLineDash([]);
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════════════════════════════════
|
||
// LAYER: EKLIPTIK
|
||
// Schiefe der Ekliptik exakt nach IAU 2006 aus JD
|
||
// ═══════════════════════════════════════════════════════════════════════════════
|
||
function drawEcliptic(date) {
|
||
// Julianisches Datum
|
||
const atime = Astronomy.MakeTime(date);
|
||
const jd = atime.tt + 2451545.0;
|
||
const T = (jd - 2451545.0) / 36525.0;
|
||
// IAU 2006 Schiefe der Ekliptik in Bogensekunden → Grad
|
||
const epsArcsec = 84381.406
|
||
- 46.836769 * T
|
||
- 0.0001831 * T * T
|
||
+ 0.00200340 * T * T * T
|
||
- 0.000000576 * T * T * T * T
|
||
- 0.0000000434 * T * T * T * T * T;
|
||
const epsR = (epsArcsec / 3600.0) * Math.PI / 180.0;
|
||
|
||
ctx.strokeStyle = CFG.ecliptic.color;
|
||
ctx.lineWidth = CFG.ecliptic.width;
|
||
ctx.setLineDash(CFG.ecliptic.dash);
|
||
|
||
const STEPS = 360;
|
||
ctx.beginPath();
|
||
let pen = false;
|
||
for (let i = 0; i <= STEPS; i++) {
|
||
const lam = (i / STEPS) * 2 * Math.PI;
|
||
const raR = Math.atan2(Math.cos(epsR) * Math.sin(lam), Math.cos(lam));
|
||
const decR = Math.asin(Math.sin(epsR) * Math.sin(lam));
|
||
const raH = ((raR * 12.0 / Math.PI) + 24) % 24;
|
||
const decD = decR * 180.0 / Math.PI;
|
||
const hor = eqHorizon(raH, decD, date);
|
||
const pt = project(hor.altitude, hor.azimuth);
|
||
if (!pt) { pen = false; continue; }
|
||
pen ? ctx.lineTo(pt.x, pt.y) : ctx.moveTo(pt.x, pt.y);
|
||
pen = true;
|
||
}
|
||
ctx.stroke();
|
||
ctx.setLineDash([]);
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════════════════════════════════
|
||
// LAYER: HÖHEN-/AZIMUT-GITTER (rein geometrisch, kein Sternbezug)
|
||
// ═══════════════════════════════════════════════════════════════════════════════
|
||
function drawAltazGrid() {
|
||
const C = CFG.altazGrid;
|
||
ctx.lineWidth = C.width;
|
||
for (let alt = C.altStep; alt <= C.altMax; alt += C.altStep) {
|
||
const rho = (90 - alt) / 90;
|
||
ctx.strokeStyle = C.colorCircles;
|
||
ctx.beginPath(); ctx.arc(cx, cy, rho * R, 0, Math.PI * 2); ctx.stroke();
|
||
if (labelAltaz) {
|
||
ctx.fillStyle = C.colorLabels;
|
||
ctx.font = '8px Share Tech Mono,monospace';
|
||
ctx.textAlign = 'left'; ctx.textBaseline = 'middle';
|
||
ctx.fillText(alt + '°', cx + rho * R + 3, cy);
|
||
}
|
||
}
|
||
ctx.strokeStyle = C.colorLines;
|
||
for (let az = 0; az < 360; az += C.azStep) {
|
||
const azR = az * Math.PI / 180;
|
||
ctx.beginPath();
|
||
ctx.moveTo(cx, cy);
|
||
ctx.lineTo(cx - R * Math.sin(azR), cy - R * Math.cos(azR));
|
||
ctx.stroke();
|
||
}
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════════════════════════════════
|
||
// FARBE AUS B-V
|
||
// ═══════════════════════════════════════════════════════════════════════════════
|
||
function bvToColor(bv) {
|
||
if (isNaN(bv) || bv === undefined) return '#ffffff';
|
||
if (bv < -0.30) return '#9bb0ff';
|
||
if (bv < -0.10) return '#aabfff';
|
||
if (bv < 0.00) return '#cad7ff';
|
||
if (bv < 0.20) return '#f8f7ff';
|
||
if (bv < 0.40) return '#fff4e8';
|
||
if (bv < 0.60) return '#ffddb4';
|
||
if (bv < 0.80) return '#ffcc6f';
|
||
if (bv < 1.20) return '#ffaa44';
|
||
return '#ff6622';
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════════════════════════════════
|
||
// CSV PARSEN
|
||
// ═══════════════════════════════════════════════════════════════════════════════
|
||
function parseHYG(text) {
|
||
const lines = text.trim().split('\n');
|
||
const header = lines[0].split(',').map(h => h.trim().replace(/"/g, ''));
|
||
const idx = {};
|
||
header.forEach((h, i) => idx[h] = i);
|
||
|
||
const out = [];
|
||
for (let i = 1; i < lines.length; i++) {
|
||
const cols = lines[i].split(',');
|
||
|
||
// Sol (id=0) herausfiltern
|
||
if ((cols[idx['id']] || '').trim() === '0') continue;
|
||
|
||
const mag = parseFloat(cols[idx['mag']]);
|
||
if (isNaN(mag) || mag > 9.5) continue;
|
||
|
||
const ra = parseFloat(cols[idx['rarad']]);
|
||
const dec = parseFloat(cols[idx['decrad']]);
|
||
if (isNaN(ra) || isNaN(dec)) continue;
|
||
|
||
out.push({
|
||
hip: (cols[idx['hip']] || '').trim(),
|
||
proper: (cols[idx['proper']] || '').trim(),
|
||
bayer: (cols[idx['bayer']] || '').trim(),
|
||
con: (cols[idx['con']] || '').trim(),
|
||
spect: (cols[idx['spect']] || '').trim(),
|
||
ra, dec, mag,
|
||
ci: parseFloat(cols[idx['ci']]),
|
||
_x: 0, _y: 0, _visible: false
|
||
});
|
||
}
|
||
return out;
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════════════════════════════════
|
||
// LAYER: STERNBILDER
|
||
// Linien zwischen HIP-Sternen, Beschriftung mit deutschem Namen
|
||
// ═══════════════════════════════════════════════════════════════════════════════
|
||
function drawConstellations(date) {
|
||
if (!constellationData || hipMap.size === 0) return;
|
||
|
||
for (const con of constellationData.constellations) {
|
||
// Alle Polylinien des Sternbilds zeichnen
|
||
for (const segment of con.lines) {
|
||
ctx.beginPath();
|
||
ctx.strokeStyle = CFG.constellations.color;
|
||
ctx.lineWidth = CFG.constellations.width;
|
||
ctx.setLineDash(CFG.constellations.dash);
|
||
|
||
let pen = false;
|
||
for (const hip of segment) {
|
||
const star = hipMap.get(hip);
|
||
if (!star) { pen = false; continue; }
|
||
const hor = starHorizon(star, date);
|
||
if (hor.altitude < 0) { pen = false; continue; }
|
||
const pt = project(hor.altitude, hor.azimuth);
|
||
if (!pt) { pen = false; continue; }
|
||
pen ? ctx.lineTo(pt.x, pt.y) : ctx.moveTo(pt.x, pt.y);
|
||
pen = true;
|
||
}
|
||
ctx.stroke();
|
||
}
|
||
ctx.setLineDash([]);
|
||
|
||
const label = con.common_name?.german || con.common_name?.native || con.id;
|
||
if (!labelConst || !label) continue;
|
||
|
||
ctx.fillStyle = CFG.constellations.colorLabels;
|
||
ctx.font = CFG.constellations.fontLabels;
|
||
ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
|
||
|
||
const labelPositions = Array.isArray(con.label_positions)
|
||
? con.label_positions
|
||
.filter(pos => Array.isArray(pos) && pos.length >= 2)
|
||
.map(pos => [Number(pos[0]), Number(pos[1])])
|
||
.filter(([raH, decD]) => Number.isFinite(raH) && Number.isFinite(decD))
|
||
: [];
|
||
|
||
if (labelPositions.length > 0) {
|
||
for (const [raH, decD] of labelPositions) {
|
||
const hor = eqHorizon(raH, decD, date);
|
||
const pt = project(hor.altitude, hor.azimuth);
|
||
if (!pt) continue;
|
||
ctx.fillText(label, pt.x, pt.y);
|
||
}
|
||
continue;
|
||
}
|
||
|
||
// Fallback: Mittelpunkt aus allen beteiligten sichtbaren Sternen.
|
||
let sumX = 0, sumY = 0, count = 0;
|
||
const allHips = con.lines.flat();
|
||
const seen = new Set();
|
||
for (const hip of allHips) {
|
||
if (seen.has(hip)) continue;
|
||
seen.add(hip);
|
||
const star = hipMap.get(hip);
|
||
if (!star) continue;
|
||
const hor = starHorizon(star, date);
|
||
if (hor.altitude < 2) continue;
|
||
const pt = project(hor.altitude, hor.azimuth);
|
||
if (!pt) continue;
|
||
sumX += pt.x; sumY += pt.y; count++;
|
||
}
|
||
if (count > 0) {
|
||
ctx.fillText(label, sumX / count, sumY / count);
|
||
}
|
||
}
|
||
}
|
||
|
||
|
||
function drawMessier(date) {
|
||
for (const obj of MESSIER) {
|
||
if (obj.mag > messierMagLimit) { obj._xy = null; continue; }
|
||
const hor = eqHorizon(obj.ra, obj.dec, date);
|
||
if (hor.altitude < 0) continue;
|
||
const pt = project(hor.altitude, hor.azimuth);
|
||
if (!pt) continue;
|
||
|
||
obj._xy = pt;
|
||
obj._alt = hor.altitude;
|
||
obj._az = hor.azimuth;
|
||
|
||
const style = M_STYLE[obj.type] || M_STYLE.As;
|
||
// Symbolradius: min 4px, skaliert mit scheinbarer Größe (leicht)
|
||
const symR = Math.max(4, Math.min(14, obj.size * 0.08 + 4));
|
||
|
||
ctx.strokeStyle = style.stroke;
|
||
ctx.fillStyle = style.color + '33'; // halbtransparente Füllung
|
||
ctx.lineWidth = 1.0;
|
||
|
||
if (obj.type === 'Gc') {
|
||
// Kugelhaufen: Kreis mit Kreuz
|
||
ctx.beginPath(); ctx.arc(pt.x, pt.y, symR, 0, Math.PI * 2);
|
||
ctx.fill(); ctx.stroke();
|
||
ctx.beginPath();
|
||
ctx.moveTo(pt.x - symR, pt.y); ctx.lineTo(pt.x + symR, pt.y);
|
||
ctx.moveTo(pt.x, pt.y - symR); ctx.lineTo(pt.x, pt.y + symR);
|
||
ctx.strokeStyle = style.stroke + 'aa';
|
||
ctx.stroke();
|
||
|
||
} else if (obj.type === 'Oc') {
|
||
// Offener Haufen: gestrichelter Kreis
|
||
ctx.setLineDash([2, 2]);
|
||
ctx.beginPath(); ctx.arc(pt.x, pt.y, symR, 0, Math.PI * 2);
|
||
ctx.stroke();
|
||
ctx.setLineDash([]);
|
||
|
||
} else if (obj.type === 'Pn') {
|
||
// Planetarischer Nebel: Kreis mit vier Strichen außen
|
||
ctx.beginPath(); ctx.arc(pt.x, pt.y, symR * 0.6, 0, Math.PI * 2);
|
||
ctx.fill(); ctx.stroke();
|
||
for (let a = 0; a < 360; a += 90) {
|
||
const ar = a * Math.PI / 180;
|
||
ctx.beginPath();
|
||
ctx.moveTo(pt.x + symR * 0.8 * Math.cos(ar), pt.y + symR * 0.8 * Math.sin(ar));
|
||
ctx.lineTo(pt.x + symR * 1.4 * Math.cos(ar), pt.y + symR * 1.4 * Math.sin(ar));
|
||
ctx.stroke();
|
||
}
|
||
|
||
} else if (obj.type === 'Gx') {
|
||
// Galaxie: Ellipse
|
||
ctx.save();
|
||
ctx.translate(pt.x, pt.y);
|
||
ctx.rotate(Math.PI / 4);
|
||
ctx.beginPath();
|
||
ctx.ellipse(0, 0, symR, symR * 0.5, 0, 0, Math.PI * 2);
|
||
ctx.fill(); ctx.stroke();
|
||
ctx.restore();
|
||
|
||
} else if (obj.type === 'Nb' || obj.type === 'Sc') {
|
||
// Nebel: Rechteck
|
||
ctx.beginPath();
|
||
ctx.rect(pt.x - symR, pt.y - symR * 0.6, symR * 2, symR * 1.2);
|
||
ctx.fill(); ctx.stroke();
|
||
|
||
} else {
|
||
// Sonstige / Asterismus
|
||
ctx.beginPath(); ctx.arc(pt.x, pt.y, symR * 0.7, 0, Math.PI * 2);
|
||
ctx.fill(); ctx.stroke();
|
||
}
|
||
|
||
// Label: M-Nummer + Name
|
||
if (labelMessier) {
|
||
const label = 'M' + obj.m + (obj.name ? ' ' + obj.name : '');
|
||
ctx.fillStyle = style.stroke;
|
||
ctx.font = '9px Rajdhani,sans-serif';
|
||
ctx.textAlign = 'left'; ctx.textBaseline = 'middle';
|
||
ctx.fillText(label, pt.x + symR + 3, pt.y);
|
||
}
|
||
}
|
||
}
|
||
|
||
|
||
function render() {
|
||
const date = getTime();
|
||
ctx.clearRect(0, 0, W, H);
|
||
|
||
// Hintergrund
|
||
ctx.fillStyle = CFG.sky.background;
|
||
ctx.fillRect(0, 0, W, H);
|
||
|
||
ctx.save();
|
||
ctx.beginPath();
|
||
ctx.arc(cx, cy, R, 0, Math.PI * 2);
|
||
const skyG = createSafeRadialGradient(cx, cy, 0, cx, cy, R);
|
||
if (skyG) {
|
||
skyG.addColorStop(0, CFG.sky.colorCenter);
|
||
skyG.addColorStop(0.8, CFG.sky.colorMid);
|
||
skyG.addColorStop(1, CFG.sky.colorEdge);
|
||
ctx.fillStyle = skyG;
|
||
} else {
|
||
ctx.fillStyle = CFG.sky.background;
|
||
}
|
||
ctx.fill();
|
||
ctx.clip();
|
||
|
||
if (showAltaz) drawAltazGrid();
|
||
if (showEq) drawEquatorialGrid(date);
|
||
if (showEclip) drawEcliptic(date);
|
||
if (showConst) drawConstellations(date);
|
||
if (showMessier) drawMessier(date);
|
||
|
||
// ── Sterne ───────────────────────────────────────────────────────────────
|
||
const visStars = showStars ? stars.filter(s => s.mag <= magLimit) : [];
|
||
for (const s of visStars) {
|
||
s._visible = false;
|
||
const hor = starHorizon(s, date);
|
||
if (hor.altitude < 0) continue;
|
||
const pt = project(hor.altitude, hor.azimuth);
|
||
if (!pt) continue;
|
||
s._x = pt.x; s._y = pt.y; s._visible = true;
|
||
|
||
const t = (magLimit - s.mag) / (magLimit + 1.5);
|
||
const radius = Math.max(0.4, t * 3.6);
|
||
const color = bvToColor(s.ci);
|
||
|
||
if (s.mag < CFG.stars.glowBelowMag) {
|
||
const glow = createSafeRadialGradient(pt.x, pt.y, 0, pt.x, pt.y, radius * 6);
|
||
if (glow) {
|
||
glow.addColorStop(0, color + 'bb'); glow.addColorStop(0.3, color + '33');
|
||
glow.addColorStop(1, 'transparent');
|
||
ctx.fillStyle = glow;
|
||
ctx.beginPath(); ctx.arc(pt.x, pt.y, radius * 6, 0, Math.PI * 2); ctx.fill();
|
||
}
|
||
}
|
||
ctx.fillStyle = color;
|
||
ctx.beginPath(); ctx.arc(pt.x, pt.y, radius, 0, Math.PI * 2); ctx.fill();
|
||
|
||
if (labelStars && s.proper && s.mag < CFG.stars.labelBelowMag) {
|
||
ctx.fillStyle = CFG.stars.colorLabel;
|
||
ctx.font = CFG.stars.fontLabel;
|
||
ctx.textAlign = 'left'; ctx.textBaseline = 'middle';
|
||
ctx.fillText(s.proper, pt.x + radius + 3, pt.y - 2);
|
||
}
|
||
}
|
||
|
||
// ── Sonne & Mond ─────────────────────────────────────────────────────────
|
||
if (showPlanets) drawSunMoon(date);
|
||
|
||
// ── Planeten ─────────────────────────────────────────────────────────────
|
||
if (showPlanets) {
|
||
computePlanets(date);
|
||
for (const p of planetCache) {
|
||
if (!Number.isFinite(p.alt) || !Number.isFinite(p.az) || p.alt < 0) continue;
|
||
const pt = project(p.alt, p.az);
|
||
if (!pt) continue;
|
||
const radius = Number(p.r);
|
||
if (!Number.isFinite(radius) || radius <= 0) continue;
|
||
p._xy = pt;
|
||
|
||
const glow = createSafeRadialGradient(pt.x, pt.y, 0, pt.x, pt.y, radius * 4.5);
|
||
if (glow) {
|
||
glow.addColorStop(0, p.color + 'aa');
|
||
glow.addColorStop(0.4, p.color + '22');
|
||
glow.addColorStop(1, 'transparent');
|
||
ctx.fillStyle = glow;
|
||
ctx.beginPath(); ctx.arc(pt.x, pt.y, radius * 4.5, 0, Math.PI * 2); ctx.fill();
|
||
}
|
||
|
||
ctx.fillStyle = p.color;
|
||
ctx.beginPath(); ctx.arc(pt.x, pt.y, radius, 0, Math.PI * 2); ctx.fill();
|
||
ctx.strokeStyle = p.color + '99'; ctx.lineWidth = 0.8; ctx.stroke();
|
||
|
||
if (labelPlanets) {
|
||
ctx.fillStyle = p.color;
|
||
ctx.font = 'bold 10px Rajdhani,sans-serif';
|
||
ctx.textAlign = 'left'; ctx.textBaseline = 'middle';
|
||
ctx.fillText(p.name, pt.x + radius + 4, pt.y);
|
||
}
|
||
}
|
||
}
|
||
|
||
ctx.restore(); // Ende Clip
|
||
|
||
// Horizontring
|
||
const rim = createSafeRadialGradient(cx, cy, R * 0.95, cx, cy, R * 1.05);
|
||
if (rim) {
|
||
rim.addColorStop(0, 'transparent');
|
||
rim.addColorStop(0.5, CFG.horizon.color);
|
||
rim.addColorStop(1, 'transparent');
|
||
}
|
||
ctx.beginPath(); ctx.arc(cx, cy, R, 0, Math.PI * 2);
|
||
ctx.strokeStyle = rim || CFG.horizon.color; ctx.lineWidth = CFG.horizon.width; ctx.stroke();
|
||
|
||
// Himmelsrichtungen
|
||
const CC = CFG.compass;
|
||
for (const [lbl, az, maj] of [
|
||
['N',0,true],['NO',45,false],['O',90,true],['SO',135,false],
|
||
['S',180,true],['SW',225,false],['W',270,true],['NW',315,false]]) {
|
||
const azR = az * Math.PI / 180;
|
||
const dist = R + (maj ? 20 : 15);
|
||
ctx.fillStyle = maj ? CC.colorMajor : CC.colorMinor;
|
||
ctx.font = maj ? CC.fontMajor : CC.fontMinor;
|
||
ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
|
||
ctx.fillText(lbl, cx - dist * Math.sin(azR), cy - dist * Math.cos(azR));
|
||
}
|
||
|
||
// Zenith
|
||
ctx.fillStyle = CC.colorMajor + '88';
|
||
ctx.beginPath(); ctx.arc(cx, cy, 2.5, 0, Math.PI * 2); ctx.fill();
|
||
ctx.fillStyle = CC.colorMajor + '55';
|
||
ctx.font = '8px Share Tech Mono,monospace';
|
||
ctx.textAlign = 'center'; ctx.textBaseline = 'bottom';
|
||
ctx.fillText('ZEN', cx, cy - 5);
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════════════════════════════════
|
||
// INFOZEILE
|
||
// ═══════════════════════════════════════════════════════════════════════════════
|
||
function updateInfo() {
|
||
const d = getTime();
|
||
document.getElementById('lblTime').textContent = d.toUTCString().slice(17, 25);
|
||
// Lokale Sternzeit via Astronomy Engine (Greenwich Sternzeit in Stunden)
|
||
const gst = Astronomy.SiderealTime(Astronomy.MakeTime(d));
|
||
const lst = ((gst + LON_DEG / 15.0) % 24 + 24) % 24;
|
||
const lH = Math.floor(lst);
|
||
const lM = Math.floor((lst - lH) * 60);
|
||
document.getElementById('lblLST').textContent =
|
||
String(lH).padStart(2,'0') + 'h' + String(lM).padStart(2,'0') + 'm';
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════════════════════════════════
|
||
// DATETIME-PICKER
|
||
// ═══════════════════════════════════════════════════════════════════════════════
|
||
function syncPicker() {
|
||
const d = liveMode ? new Date() : simTime;
|
||
const loc = new Date(d.getTime() - d.getTimezoneOffset() * 60000);
|
||
document.getElementById('dtPicker').value = loc.toISOString().slice(0, 16);
|
||
}
|
||
syncPicker();
|
||
|
||
document.getElementById('dtPicker').addEventListener('change', function() {
|
||
if (!this.value) return;
|
||
simTime = new Date(this.value);
|
||
liveMode = false;
|
||
document.getElementById('btnNow').classList.remove('live');
|
||
updateInfo(); render();
|
||
});
|
||
|
||
document.getElementById('btnNow').addEventListener('click', function() {
|
||
liveMode = true;
|
||
this.classList.add('live');
|
||
syncPicker(); updateInfo(); render();
|
||
});
|
||
|
||
// Live-Tick alle 10 Sekunden
|
||
setInterval(() => {
|
||
if (!liveMode) return;
|
||
syncPicker(); updateInfo(); render();
|
||
}, 10000);
|
||
updateInfo();
|
||
|
||
// ═══════════════════════════════════════════════════════════════════════════════
|
||
// ZEITSPRUNG-BUTTONS (★ = Sterntag = 86164.0916 s)
|
||
// ═══════════════════════════════════════════════════════════════════════════════
|
||
document.querySelectorAll('.step-btn').forEach(btn => {
|
||
btn.addEventListener('click', function() {
|
||
const sec = parseFloat(this.dataset.sec);
|
||
// Wenn Live-Modus: aktuelle Zeit als Basis nehmen und einfrieren
|
||
if (liveMode) {
|
||
simTime = new Date();
|
||
liveMode = false;
|
||
document.getElementById('btnNow').classList.remove('live');
|
||
}
|
||
simTime = new Date(simTime.getTime() + sec * 1000);
|
||
syncPicker();
|
||
updateInfo();
|
||
render();
|
||
});
|
||
});
|
||
// ═══════════════════════════════════════════════════════════════════════════════
|
||
// URL-PARAMETER – ?img=true&lat=…&lon=…&elev=…&date=…
|
||
// ═══════════════════════════════════════════════════════════════════════════════
|
||
const URL_PARAMS = new URLSearchParams(window.location.search);
|
||
const AUTO_IMG = URL_PARAMS.get('img') === 'true';
|
||
|
||
function applyUrlParams() {
|
||
// Datum/Zeit (ISO 8601 Lokalzeit: 2026-03-12T21:00 oder …Z für UTC)
|
||
const pDate = URL_PARAMS.get('date');
|
||
if (pDate) {
|
||
const d = new Date(pDate);
|
||
if (isFinite(d)) {
|
||
simTime = d;
|
||
liveMode = false;
|
||
syncPicker();
|
||
}
|
||
}
|
||
}
|
||
|
||
function autoExportPng() {
|
||
const size = 1000;
|
||
const expCanvas = document.createElement('canvas');
|
||
expCanvas.width = size;
|
||
expCanvas.height = size;
|
||
|
||
const _ctx = ctx, _W = W, _H = H, _R = R, _cx = cx, _cy = cy;
|
||
ctx = expCanvas.getContext('2d');
|
||
W = size; H = size; R = size * 0.455; cx = size / 2; cy = size / 2;
|
||
render();
|
||
ctx = _ctx; W = _W; H = _H; R = _R; cx = _cx; cy = _cy;
|
||
|
||
const d = getTime();
|
||
const pad = n => String(n).padStart(2, '0');
|
||
const stamp = d.getFullYear() + pad(d.getMonth()+1) + pad(d.getDate())
|
||
+ '_' + pad(d.getHours()) + pad(d.getMinutes());
|
||
const fname = 'planetarium_sonneberg_' + stamp + '.png';
|
||
|
||
expCanvas.toBlob(blob => {
|
||
const url = URL.createObjectURL(blob);
|
||
// Neuer Tab: Browser zeigt das PNG direkt an → Rechtsklick/Strg+S zum Speichern
|
||
const win = window.open(url, '_blank');
|
||
// Falls Popup blockiert: Fallback-Link ins Dokument einblenden
|
||
if (!win) {
|
||
const bar = document.createElement('div');
|
||
bar.style.cssText = 'position:fixed;bottom:0;left:0;right:0;background:#050d18;'
|
||
+ 'border-top:1px solid #4a9eff;padding:10px 20px;z-index:9999;'
|
||
+ 'font-family:Rajdhani,sans-serif;font-size:14px;display:flex;'
|
||
+ 'align-items:center;gap:16px;';
|
||
bar.innerHTML = '<span style="color:#8ab0cc;">Popup blockiert –</span>'
|
||
+ `<a href="${url}" download="${fname}" style="color:#4a9eff;border:1px solid #4a9eff;`
|
||
+ 'padding:5px 16px;text-decoration:none;letter-spacing:0.08em;">'
|
||
+ '⬇ PNG speichern</a>'
|
||
+ '<span style="color:#2a3a4a;cursor:pointer;margin-left:auto;" '
|
||
+ 'onclick="this.parentNode.remove()">✕</span>';
|
||
document.body.appendChild(bar);
|
||
}
|
||
}, 'image/png');
|
||
}
|
||
|
||
|
||
// ═══════════════════════════════════════════════════════════════════════════════
|
||
const btnLocation = document.getElementById('btnLocation');
|
||
const locationPanel = document.getElementById('locationPanel');
|
||
const btnLocSet = document.getElementById('btnLocSet');
|
||
const locationSelect = document.getElementById('locationSelect');
|
||
|
||
if (locationSelect) {
|
||
locationSelect.addEventListener('change', function() {
|
||
applyLocationOption(this.value);
|
||
});
|
||
}
|
||
|
||
if (btnLocation && locationPanel) btnLocation.addEventListener('click', function(e) {
|
||
e.stopPropagation();
|
||
const open = locationPanel.classList.toggle('open');
|
||
btnLocation.classList.toggle('open', open);
|
||
// Koordinatenfelder mit aktuellem Standort befüllen
|
||
if (open) {
|
||
document.getElementById('locLat').value = LAT_DEG.toFixed(4);
|
||
document.getElementById('locLon').value = LON_DEG.toFixed(4);
|
||
document.getElementById('locElev').value = ELEV_M;
|
||
}
|
||
});
|
||
document.addEventListener('click', function(e) {
|
||
if (btnLocation && locationPanel && !locationPanel.contains(e.target) && e.target !== btnLocation) {
|
||
locationPanel.classList.remove('open');
|
||
btnLocation.classList.remove('open');
|
||
}
|
||
});
|
||
|
||
// Preset-Buttons
|
||
document.querySelectorAll('.loc-btn').forEach(btn => {
|
||
btn.addEventListener('click', function() {
|
||
document.querySelectorAll('.loc-btn').forEach(b => b.classList.remove('active'));
|
||
this.classList.add('active');
|
||
setLocation(
|
||
parseFloat(this.dataset.lat),
|
||
parseFloat(this.dataset.lon),
|
||
parseFloat(this.dataset.elev) || 0
|
||
);
|
||
// Koordinatenfelder aktualisieren
|
||
document.getElementById('locLat').value = parseFloat(this.dataset.lat).toFixed(4);
|
||
document.getElementById('locLon').value = parseFloat(this.dataset.lon).toFixed(4);
|
||
document.getElementById('locElev').value = this.dataset.elev || 0;
|
||
});
|
||
});
|
||
|
||
// Manuelle Koordinaten übernehmen
|
||
if (btnLocSet) btnLocSet.addEventListener('click', function() {
|
||
const lat = parseFloat(document.getElementById('locLat').value);
|
||
const lon = parseFloat(document.getElementById('locLon').value);
|
||
const elev = parseFloat(document.getElementById('locElev').value) || 0;
|
||
if (!isFinite(lat) || !isFinite(lon)) return;
|
||
// Keinen Preset als aktiv markieren
|
||
document.querySelectorAll('.loc-btn').forEach(b => b.classList.remove('active'));
|
||
setLocation(lat, lon, elev);
|
||
});
|
||
|
||
// ═══════════════════════════════════════════════════════════════════════════════
|
||
// EINSTELLUNGEN PANEL
|
||
// ═══════════════════════════════════════════════════════════════════════════════
|
||
const btnSettings = document.getElementById('btnSettings');
|
||
const settingsPanel = document.getElementById('settingsPanel');
|
||
|
||
btnSettings.addEventListener('click', function(e) {
|
||
e.stopPropagation();
|
||
const open = settingsPanel.classList.toggle('open');
|
||
btnSettings.classList.toggle('open', open);
|
||
});
|
||
document.addEventListener('click', function(e) {
|
||
if (!settingsPanel.contains(e.target) && e.target !== btnSettings) {
|
||
settingsPanel.classList.remove('open');
|
||
btnSettings.classList.remove('open');
|
||
}
|
||
});
|
||
|
||
function spBind(id, getter, setter) {
|
||
const el = document.getElementById(id);
|
||
if (!el) return;
|
||
el.checked = getter();
|
||
el.addEventListener('change', function() { setter(this.checked); render(); });
|
||
}
|
||
|
||
// Gitter-Auswahl (Radio)
|
||
document.querySelectorAll('input[name="gridMode"]').forEach(radio => {
|
||
radio.addEventListener('change', function() {
|
||
showEq = this.value === 'eq';
|
||
showAltaz = this.value === 'altaz';
|
||
render();
|
||
});
|
||
});
|
||
spBind('cfgEqLabels', () => labelEq, v => { labelEq = v; labelAltaz = v; });
|
||
spBind('cfgPlanetsShow', () => showPlanets, v => showPlanets = v);
|
||
spBind('cfgEclipShow', () => showEclip, v => showEclip = v);
|
||
spBind('cfgMessierShow', () => showMessier, v => showMessier = v);
|
||
spBind('cfgSunMoonShow', () => showPlanets, v => showPlanets = v);
|
||
|
||
spBind('cfgConstLabels', () => labelConst, v => labelConst = v);
|
||
spBind('cfgStarsShow', () => showStars, v => showStars = v);
|
||
spBind('cfgStarsLabels', () => labelStars, v => labelStars = v);
|
||
spBind('cfgStarsAutoMag', () => autoStarMagByZoom, v => {
|
||
autoStarMagByZoom = v;
|
||
if (autoStarMagByZoom) applyAutoMagLimitFromZoom();
|
||
else syncMagLimitUi();
|
||
});
|
||
spBind('cfgMessierAutoMag', () => autoMessierMagByZoom, v => {
|
||
autoMessierMagByZoom = v;
|
||
if (autoMessierMagByZoom) applyAutoMessierMagLimitFromZoom();
|
||
else syncMessierMagLimitUi();
|
||
});
|
||
spBind('cfgPlanetsLabels', () => labelPlanets, v => labelPlanets = v);
|
||
spBind('cfgSunMoonLabels', () => labelSunMoon, v => labelSunMoon = v);
|
||
spBind('cfgMessierLabels', () => labelMessier, v => labelMessier = v);
|
||
|
||
// ═══════════════════════════════════════════════════════════════════════════════
|
||
// BILD-EXPORT
|
||
// ═══════════════════════════════════════════════════════════════════════════════
|
||
document.getElementById('btnExport').addEventListener('click', function() {
|
||
const size = 1000;
|
||
|
||
const expCanvas = document.createElement('canvas');
|
||
expCanvas.width = size;
|
||
expCanvas.height = size;
|
||
|
||
const _ctx = ctx, _W = W, _H = H, _R = R, _cx = cx, _cy = cy;
|
||
ctx = expCanvas.getContext('2d');
|
||
W = size; H = size;
|
||
R = size * 0.455;
|
||
cx = size / 2; cy = size / 2;
|
||
|
||
render();
|
||
|
||
ctx = _ctx; W = _W; H = _H; R = _R; cx = _cx; cy = _cy;
|
||
|
||
const d = getTime();
|
||
const pad = n => String(n).padStart(2, '0');
|
||
const stamp = d.getFullYear() + pad(d.getMonth()+1) + pad(d.getDate())
|
||
+ '_' + pad(d.getHours()) + pad(d.getMinutes());
|
||
|
||
expCanvas.toBlob(blob => {
|
||
const a = document.createElement('a');
|
||
a.href = URL.createObjectURL(blob);
|
||
a.download = 'planetarium_sonneberg_' + stamp + '.png';
|
||
a.click();
|
||
URL.revokeObjectURL(a.href);
|
||
}, 'image/png');
|
||
});
|
||
|
||
// ═══════════════════════════════════════════════════════════════════════════════
|
||
// MAGNITUDE-REGLER
|
||
// ═══════════════════════════════════════════════════════════════════════════════
|
||
document.getElementById('magLimit').addEventListener('input', function() {
|
||
if (autoStarMagByZoom) return;
|
||
magLimit = parseFloat(this.value);
|
||
syncMagLimitUi();
|
||
if (stars.length > 0) render();
|
||
});
|
||
document.getElementById('magLimitMessier').addEventListener('input', function() {
|
||
if (autoMessierMagByZoom) return;
|
||
messierMagLimit = parseFloat(this.value);
|
||
syncMessierMagLimitUi();
|
||
render();
|
||
});
|
||
|
||
// ═══════════════════════════════════════════════════════════════════════════════
|
||
// KATALOG LADEN – automatisch per fetch, Fallback auf Datei-Dialog
|
||
// ═══════════════════════════════════════════════════════════════════════════════
|
||
// ═══════════════════════════════════════════════════════════════════════════════
|
||
// RESSOURCEN LADEN – config · Sterne aus DB
|
||
// Auto-Export erst wenn alles fertig ist
|
||
// ═══════════════════════════════════════════════════════════════════════════════
|
||
const pConfig = fetch('config.json')
|
||
.then(r => { if (!r.ok) throw new Error(); return r.json(); })
|
||
.then(data => { CFG = deepMerge(CFG_DEFAULT, data); M_STYLE = buildMStyle(); })
|
||
.catch(() => { /* Fallback auf CFG_DEFAULT – bereits gesetzt */ });
|
||
|
||
if (EMBEDDED_CONSTELLATION_ERROR) {
|
||
console.warn('Sternbilddaten aus der Datenbank nicht geladen:', EMBEDDED_CONSTELLATION_ERROR);
|
||
}
|
||
|
||
if (EMBEDDED_STAR_ERROR) {
|
||
console.warn('Sterndaten aus der Datenbank nicht geladen:', EMBEDDED_STAR_ERROR);
|
||
}
|
||
|
||
if (EMBEDDED_MESSIER_ERROR) {
|
||
console.warn('Messier-Daten aus der Datenbank nicht geladen:', EMBEDDED_MESSIER_ERROR);
|
||
}
|
||
|
||
const pStars = Promise.resolve().then(() => {
|
||
stars = Array.isArray(EMBEDDED_STAR_DATA)
|
||
? EMBEDDED_STAR_DATA.map(s => ({
|
||
hip: s.hip ?? null,
|
||
proper: s.proper || '',
|
||
bayer: s.bayer || '',
|
||
con: s.con || '',
|
||
spect: s.spect || '',
|
||
ra: Number(s.ra),
|
||
dec: Number(s.dec),
|
||
mag: Number(s.mag),
|
||
ci: s.ci === null || s.ci === undefined ? NaN : Number(s.ci),
|
||
_x: 0,
|
||
_y: 0,
|
||
_visible: false
|
||
})).filter(s =>
|
||
Number.isFinite(s.ra) &&
|
||
Number.isFinite(s.dec) &&
|
||
Number.isFinite(s.mag)
|
||
)
|
||
: [];
|
||
hipMap.clear();
|
||
for (const s of stars) {
|
||
if (s.hip !== null && Number.isFinite(Number(s.hip))) {
|
||
hipMap.set(parseInt(s.hip, 10), s);
|
||
}
|
||
}
|
||
document.getElementById('lblCount').textContent = stars.length.toLocaleString('de');
|
||
console.info('Sterne aus DB geladen:', stars.length);
|
||
});
|
||
|
||
Promise.all([pConfig, pStars]).then(() => {
|
||
applyAutoMagLimitFromZoom();
|
||
syncMagLimitUi();
|
||
applyAutoMessierMagLimitFromZoom();
|
||
syncMessierMagLimitUi();
|
||
applyUrlParams();
|
||
render();
|
||
if (AUTO_IMG) autoExportPng();
|
||
});
|
||
|
||
// Manueller Fallback über Datei-Dialog
|
||
document.getElementById('fileInput').addEventListener('change', function(e) {
|
||
const file = e.target.files[0];
|
||
if (!file) return;
|
||
console.info('Lade', file.name);
|
||
const reader = new FileReader();
|
||
reader.onload = ev => {
|
||
stars = parseHYG(ev.target.result);
|
||
hipMap.clear();
|
||
for (const s of stars) { if (s.hip) hipMap.set(parseInt(s.hip), s); }
|
||
document.getElementById('lblCount').textContent = stars.length.toLocaleString('de');
|
||
render();
|
||
};
|
||
reader.readAsText(file);
|
||
});
|
||
|
||
// ═══════════════════════════════════════════════════════════════════════════════
|
||
// TOOLTIP
|
||
// ═══════════════════════════════════════════════════════════════════════════════
|
||
let _mouseDownPos = null;
|
||
canvas.addEventListener('mousedown', function(e) {
|
||
_mouseDownPos = { x: e.clientX, y: e.clientY };
|
||
}, true);
|
||
|
||
canvas.addEventListener('click', function(e) {
|
||
// Drag ignorieren (Bewegung > 4px = kein Klick)
|
||
if (_mouseDownPos && Math.hypot(e.clientX - _mouseDownPos.x, e.clientY - _mouseDownPos.y) > 4) return;
|
||
const rect = canvas.getBoundingClientRect();
|
||
const mx = e.clientX - rect.left, my = e.clientY - rect.top;
|
||
const tip = document.getElementById('tooltip');
|
||
|
||
// Sonne & Mond zuerst
|
||
if (showPlanets) {
|
||
if (sunCache && sunCache._xy && Math.hypot(mx - sunCache._xy.x, my - sunCache._xy.y) < 18) {
|
||
tip.innerHTML =
|
||
'<span class="tt-name" style="color:#ffdd44">☀ Sonne</span>' +
|
||
'Alt: ' + sunCache.alt.toFixed(2) + '°<br>' +
|
||
'Az: ' + sunCache.az.toFixed(2) + '°<br>' +
|
||
'RA: ' + sunCache.ra.toFixed(4) + ' h<br>' +
|
||
'Dec: ' + sunCache.dec.toFixed(4) + '°';
|
||
tip.className = 'visible'; positionTooltip(e); return;
|
||
}
|
||
if (moonCache && moonCache._xy && Math.hypot(mx - moonCache._xy.x, my - moonCache._xy.y) < 18) {
|
||
const pct = Math.round(moonCache.phase_fraction * 100);
|
||
const mp = moonCache.moon_phase;
|
||
const phaseName = mp < 45 ? 'Neumond' : mp < 90 ? 'Zunehmende Sichel' :
|
||
mp < 135 ? 'Erstes Viertel' : mp < 180 ? 'Zunehmender Gibbous' :
|
||
mp < 225 ? 'Vollmond' : mp < 270 ? 'Abnehmender Gibbous' :
|
||
mp < 315 ? 'Letztes Viertel' : 'Abnehmende Sichel';
|
||
tip.innerHTML =
|
||
'<span class="tt-name" style="color:#c8c0a8">☽ Mond</span>' +
|
||
phaseName + '<br>' +
|
||
'Beleuchtung: ' + pct + '%<br>' +
|
||
'Phase: ' + mp.toFixed(1) + '°<br>' +
|
||
'Alt: ' + moonCache.alt.toFixed(2) + '°<br>' +
|
||
'Az: ' + moonCache.az.toFixed(2) + '°<br>' +
|
||
'RA: ' + moonCache.ra.toFixed(4) + ' h<br>' +
|
||
'Dec: ' + moonCache.dec.toFixed(4) + '°';
|
||
tip.className = 'visible'; positionTooltip(e); return;
|
||
}
|
||
}
|
||
|
||
// Planeten
|
||
let hit = null;
|
||
for (const p of planetCache) {
|
||
if (!p._xy) continue;
|
||
if (Math.hypot(mx - p._xy.x, my - p._xy.y) < 14) { hit = p; break; }
|
||
}
|
||
if (hit) {
|
||
const hor = eqHorizon(hit.ra, hit.dec, getTime());
|
||
tip.innerHTML =
|
||
'<span class="tt-name tt-planet">' + hit.name + '</span>' +
|
||
'Mag: ' + hit.mag.toFixed(2) + '<br>' +
|
||
'Alt: ' + hor.altitude.toFixed(2) + '°<br>' +
|
||
'Az: ' + hor.azimuth.toFixed(2) + '°<br>' +
|
||
'RA: ' + hit.ra.toFixed(5) + ' h<br>' +
|
||
'Dec: ' + hit.dec.toFixed(5) + '°';
|
||
tip.className = 'visible'; positionTooltip(e); return;
|
||
}
|
||
|
||
// Messier-Objekte
|
||
if (showMessier) {
|
||
for (const obj of MESSIER) {
|
||
if (!obj._xy) continue;
|
||
if (Math.hypot(mx - obj._xy.x, my - obj._xy.y) < 16) {
|
||
const typeNames = {Oc:'Offener Haufen',Gc:'Kugelhaufen',Nb:'Emissionsnebel',
|
||
Sc:'Haufen+Nebel',Pn:'Planetarischer Nebel',Gx:'Galaxie',As:'Asterismus'};
|
||
tip.innerHTML =
|
||
'<span class="tt-name" style="color:' + (M_STYLE[obj.type]||M_STYLE.As).stroke + '">M' + obj.m +
|
||
(obj.name ? ' – ' + obj.name : '') + '</span>' +
|
||
'Typ: ' + (typeNames[obj.type] || obj.type) + '<br>' +
|
||
'Mag: ' + obj.mag.toFixed(1) + '<br>' +
|
||
'Größe: '+ obj.size + '′<br>' +
|
||
'Alt: ' + obj._alt.toFixed(1) + '°<br>' +
|
||
'Az: ' + obj._az.toFixed(1) + '°<br>' +
|
||
'RA: ' + obj.ra.toFixed(4) + ' h Dec: ' + obj.dec.toFixed(3) + '°';
|
||
tip.className = 'visible'; positionTooltip(e); return;
|
||
}
|
||
}
|
||
}
|
||
|
||
// Sterne
|
||
let closest = null, minD = 10;
|
||
for (const s of stars) {
|
||
if (!s._visible) continue;
|
||
const d = Math.hypot(mx - s._x, my - s._y);
|
||
if (d < minD) { minD = d; closest = s; }
|
||
}
|
||
if (closest) {
|
||
const hor = starHorizon(closest, getTime());
|
||
const hasBayer = !!closest.bayer;
|
||
const name = closest.proper || closest.bayer || ('HIP ' + closest.hip);
|
||
tip.innerHTML =
|
||
'<span class="tt-name">' + name + '</span>' +
|
||
(hasBayer ? 'Bayer: ' + closest.bayer + ' ' + closest.con + '<br>' : 'HIP: ' + closest.hip + '<br>') +
|
||
'Kons: ' + (closest.con || '–') + '<br>' +
|
||
'Mag: ' + closest.mag.toFixed(2) + '<br>' +
|
||
'SpTyp: ' + (closest.spect.slice(0,6) || '–') + '<br>' +
|
||
'B-V: ' + (isNaN(closest.ci) ? '–' : closest.ci.toFixed(2)) + '<br>' +
|
||
'Alt: ' + hor.altitude.toFixed(2) + '°<br>' +
|
||
'Az: ' + hor.azimuth.toFixed(2) + '°';
|
||
tip.className = 'visible'; positionTooltip(e);
|
||
} else {
|
||
// Klick auf leeren Himmel → Tooltip schließen
|
||
tip.className = '';
|
||
}
|
||
});
|
||
|
||
function positionTooltip(e) {
|
||
const tip = document.getElementById('tooltip');
|
||
const tw = 225; // max-width aus CSS
|
||
const th = tip.offsetHeight > 0 ? tip.offsetHeight : 180;
|
||
const margin = 12;
|
||
let tx = e.clientX + 16;
|
||
let ty = e.clientY - 10;
|
||
if (tx + tw + margin > window.innerWidth) tx = e.clientX - tw - 16;
|
||
if (tx < margin) tx = margin;
|
||
if (ty + th + margin > window.innerHeight) ty = window.innerHeight - th - margin;
|
||
if (ty < margin) ty = margin;
|
||
tip.style.left = tx + 'px';
|
||
tip.style.top = ty + 'px';
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════════════════════════════════
|
||
// START
|
||
// ═══════════════════════════════════════════════════════════════════════════════
|
||
setLocation(LAT_DEG, LON_DEG, ELEV_M, HEADER_LOCATION.name || 'Standard-Standort', HEADER_LOCATION.id ?? '');
|
||
resize();
|
||
|
||
}); // end window.onload
|
||
</script>
|
||
<?php require __DIR__ . '/footer.php'; ?>
|