Files
skyview.astronomiemuseum.de/public/sternenhimmel_old.php
T
2026-03-31 19:51:56 +02:00

2309 lines
94 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
declare(strict_types=1);
$constellationData = ['constellations' => []];
$constellationDataError = null;
try {
$dbConfig = require __DIR__ . '/../planetenvorhersage/db.php';
$dsn = sprintf(
'mysql:host=%s;dbname=%s;charset=%s',
$dbConfig['host'],
$dbConfig['dbname'],
$dbConfig['charset'] ?? 'utf8mb4'
);
$pdo = 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 = $pdo->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 = $pdo->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 = $pdo->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);
} catch (Throwable $e) {
$constellationDataError = $e->getMessage();
}
$constellationDataJson = json_encode(
$constellationData,
JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES
);
$constellationDataErrorJson = json_encode(
$constellationDataError,
JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES
);
?>
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Planetarium – Sternwarte Sonneberg</title>
<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 {
background: var(--bg);
color: var(--text);
font-family: 'Rajdhani', sans-serif;
height: 100vh;
display: flex;
flex-direction: column;
overflow: hidden;
}
header {
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;
}
.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;
display: flex;
align-items: center;
justify-content: center;
position: relative;
overflow: hidden;
}
canvas { 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>
</head>
<body>
<header>
<div class="logo">☆ <span>Sternwarte</span> Sonneberg</div>
<div class="info-bar">
<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>
<!-- Standort -->
<div id="locationWrap">
<button id="btnLocation" title="Standort wählen">🌍</button>
<div id="locationPanel">
<div class="sp-title">🌍 Standort</div>
<div class="loc-list">
<button class="loc-btn active" data-lat="50.3547" data-lon="11.1900" data-elev="640">
<span class="loc-name">Sternwarte Sonneberg</span>
<span class="loc-sub">Sonneberg, Deutschland</span>
</button>
<button class="loc-btn" data-lat="52.5200" data-lon="13.4050" data-elev="34">
<span class="loc-name">Berlin</span>
<span class="loc-sub">Deutschland</span>
</button>
<button class="loc-btn" data-lat="48.1351" data-lon="11.5820" data-elev="520">
<span class="loc-name">München</span>
<span class="loc-sub">Deutschland</span>
</button>
<button class="loc-btn" data-lat="51.5074" data-lon="-0.1278" data-elev="11">
<span class="loc-name">London</span>
<span class="loc-sub">United Kingdom</span>
</button>
<button class="loc-btn" data-lat="48.8566" data-lon="2.3522" data-elev="35">
<span class="loc-name">Paris</span>
<span class="loc-sub">Frankreich</span>
</button>
<button class="loc-btn" data-lat="40.7128" data-lon="-74.0060" data-elev="10">
<span class="loc-name">New York</span>
<span class="loc-sub">USA</span>
</button>
<button class="loc-btn" data-lat="34.0522" data-lon="-118.2437" data-elev="71">
<span class="loc-name">Los Angeles</span>
<span class="loc-sub">USA</span>
</button>
<button class="loc-btn" data-lat="19.4326" data-lon="-99.1332" data-elev="2240">
<span class="loc-name">Mexiko-Stadt</span>
<span class="loc-sub">Mexiko</span>
</button>
<button class="loc-btn" data-lat="-22.9068" data-lon="-43.1729" data-elev="10">
<span class="loc-name">Rio de Janeiro</span>
<span class="loc-sub">Brasilien</span>
</button>
<button class="loc-btn" data-lat="51.1789" data-lon="-1.8262" data-elev="98">
<span class="loc-name">Stonehenge</span>
<span class="loc-sub">England</span>
</button>
<button class="loc-btn" data-lat="29.9792" data-lon="31.1342" data-elev="60">
<span class="loc-name">Gizeh (Pyramiden)</span>
<span class="loc-sub">Ägypten</span>
</button>
<button class="loc-btn" data-lat="35.6762" data-lon="139.6503" data-elev="40">
<span class="loc-name">Tokio</span>
<span class="loc-sub">Japan</span>
</button>
<button class="loc-btn" data-lat="-33.8688" data-lon="151.2093" data-elev="58">
<span class="loc-name">Sydney</span>
<span class="loc-sub">Australien</span>
</button>
<button class="loc-btn" data-lat="-29.0500" data-lon="-70.7000" data-elev="2400">
<span class="loc-name">La Serena (Atacama)</span>
<span class="loc-sub">Chile</span>
</button>
<button class="loc-btn" data-lat="19.8968" data-lon="-155.5828" data-elev="4205">
<span class="loc-name">Mauna Kea</span>
<span class="loc-sub">Hawaii, USA</span>
</button>
<button class="loc-btn" data-lat="78.9290" data-lon="11.9220" data-elev="10">
<span class="loc-name">Longyearbyen</span>
<span class="loc-sub">Spitzbergen, Norwegen</span>
</button>
<button class="loc-btn" data-lat="-90.0000" data-lon="0.0000" data-elev="2835">
<span class="loc-name">Südpol</span>
<span class="loc-sub">Antarktis</span>
</button>
</div>
<div class="sp-title" style="margin-top:10px;">✎ Koordinaten</div>
<div class="loc-custom">
<div class="loc-input-row">
<label>Lat</label>
<input type="number" id="locLat" min="-90" max="90" step="0.0001" placeholder="50.3547">
<label>Lon</label>
<input type="number" id="locLon" min="-180" max="180" step="0.0001" placeholder="11.1900">
<label>Höhe</label>
<input type="number" id="locElev" min="0" max="9000" step="1" placeholder="640">
</div>
<button id="btnLocSet" class="loc-set-btn">Übernehmen</button>
</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>
</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="9" step="0.5" value="6.5">
<span class="sp-val" id="magVal">6.5</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>
</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="12" step="0.5" value="12">
<span class="sp-val" id="magValMessier">12.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>
</header>
<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: 9.5, defaultMagLimit: 6.5,
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
// ═══════════════════════════════════════════════════════════════════════════════
// STANDORT – dynamisch (Standard: Sternwarte Sonneberg)
// ═══════════════════════════════════════════════════════════════════════════════
let LAT_DEG = 50.3547;
let LON_DEG = 11.1900;
let ELEV_M = 640;
let OBSERVER = new Astronomy.Observer(LAT_DEG, LON_DEG, ELEV_M);
function setLocation(lat, lon, elev) {
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);
document.getElementById('lblLat').textContent = fmt(LAT_DEG, 'N', 'S');
document.getElementById('lblLon').textContent = fmt(LON_DEG, 'E', 'W');
render();
}
// ═══════════════════════════════════════════════════════════════════════════════
// ZUSTAND
// ═══════════════════════════════════════════════════════════════════════════════
let stars = [];
let magLimit = 6.5;
let messierMagLimit = 12.0;
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 = [
{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'; ?>;
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;
updateZoomIndicator();
render();
}
function resetZoom() {
zoomLevel = 1.0; panX = 0; panY = 0;
R = baseR; cx = W / 2; cy = H / 2;
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) + '×';
}
// 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 };
}
// ═══════════════════════════════════════════════════════════════════════════════
// 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 = ctx.createRadialGradient(pt.x, pt.y, r, pt.x, pt.y, r * 5);
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 = ctx.createRadialGradient(pt.x, pt.y, 0, pt.x, pt.y, r);
sunDisk.addColorStop(0, S.colorCenter);
sunDisk.addColorStop(0.6, S.colorMid);
sunDisk.addColorStop(1, S.colorEdge);
ctx.fillStyle = sunDisk;
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 = ctx.createRadialGradient(pt.x, pt.y, r, pt.x, pt.y, r * 3.5);
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 = ctx.createRadialGradient(pt.x, pt.y, 0, pt.x, pt.y, r);
moonDisk.addColorStop(0, M.colorCenter);
moonDisk.addColorStop(0.7, M.colorMid);
moonDisk.addColorStop(1, M.colorEdge);
ctx.fillStyle = moonDisk;
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 = ctx.createRadialGradient(cx, cy, 0, cx, cy, R);
skyG.addColorStop(0, CFG.sky.colorCenter);
skyG.addColorStop(0.8, CFG.sky.colorMid);
skyG.addColorStop(1, CFG.sky.colorEdge);
ctx.fillStyle = skyG;
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 = ctx.createRadialGradient(pt.x, pt.y, 0, pt.x, pt.y, radius * 6);
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 (p.alt < 0) continue;
const pt = project(p.alt, p.az);
if (!pt) continue;
p._xy = pt;
const glow = ctx.createRadialGradient(pt.x, pt.y, 0, pt.x, pt.y, p.r * 4.5);
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, p.r * 4.5, 0, Math.PI * 2); ctx.fill();
ctx.fillStyle = p.color;
ctx.beginPath(); ctx.arc(pt.x, pt.y, p.r, 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 + p.r + 4, pt.y);
}
}
}
ctx.restore(); // Ende Clip
// Horizontring
const rim = ctx.createRadialGradient(cx, cy, R * 0.95, cx, cy, R * 1.05);
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; 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() {
// Standort
const pLat = parseFloat(URL_PARAMS.get('lat'));
const pLon = parseFloat(URL_PARAMS.get('lon'));
const pElev = parseFloat(URL_PARAMS.get('elev')) || 0;
if (isFinite(pLat) && isFinite(pLon)) {
setLocation(pLat, pLon, pElev);
document.querySelectorAll('.loc-btn').forEach(b => {
b.classList.toggle('active',
Math.abs(parseFloat(b.dataset.lat) - pLat) < 0.001 &&
Math.abs(parseFloat(b.dataset.lon) - pLon) < 0.001);
});
}
// 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');
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 (!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
document.getElementById('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('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() {
magLimit = parseFloat(this.value);
document.getElementById('magVal').textContent = magLimit.toFixed(1);
if (stars.length > 0) render();
});
document.getElementById('magLimitMessier').addEventListener('input', function() {
messierMagLimit = parseFloat(this.value);
document.getElementById('magValMessier').textContent = messierMagLimit.toFixed(1);
render();
});
// ═══════════════════════════════════════════════════════════════════════════════
// KATALOG LADEN – automatisch per fetch, Fallback auf Datei-Dialog
// ═══════════════════════════════════════════════════════════════════════════════
// ═══════════════════════════════════════════════════════════════════════════════
// RESSOURCEN LADEN – config · HYG (parallel)
// 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);
}
const pHYG = fetch('hyg_v42.csv')
.then(r => { if (!r.ok) throw new Error('HTTP ' + r.status); return r.text(); })
.then(text => {
stars = parseHYG(text);
hipMap.clear();
for (const s of stars) { if (s.hip) hipMap.set(parseInt(s.hip), s); }
document.getElementById('lblCount').textContent = stars.length.toLocaleString('de');
console.info('HYG geladen:', stars.length, 'Sterne');
})
.catch(err => console.warn('hyg_v42.csv nicht gefunden:', err.message));
Promise.all([pConfig, pHYG]).then(() => {
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
// ═══════════════════════════════════════════════════════════════════════════════
resize();
}); // end window.onload
</script>
</body>
</html>