Planetensichtbarkeit auf PY umgestellt
This commit is contained in:
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
ini_set('display_errors', '1');
|
||||
error_reporting(E_ALL);
|
||||
session_start();
|
||||
require_once __DIR__ . '/python_api.php';
|
||||
|
||||
$loggedIn = isset($_SESSION['user_id']);
|
||||
$pageTitle = 'Astronomische Umrechnungen';
|
||||
@@ -16,47 +17,6 @@ function h(?string $value): string
|
||||
return htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8');
|
||||
}
|
||||
|
||||
function runPythonApi(string $action, array $args): array
|
||||
{
|
||||
$scriptPath = __DIR__ . '/py/api.py';
|
||||
$pythonCandidates = PHP_OS_FAMILY === 'Windows'
|
||||
? ['python', 'py']
|
||||
: ['/usr/bin/python3', '/usr/bin/python', 'python3', 'python'];
|
||||
|
||||
$escapedArguments = array_map('escapeshellarg', array_merge([$scriptPath, $action], $args));
|
||||
$lastError = 'Python konnte nicht gestartet werden.';
|
||||
|
||||
foreach ($pythonCandidates as $pythonBinary) {
|
||||
$command = $pythonBinary . ' ' . implode(' ', $escapedArguments) . ' 2>&1';
|
||||
$output = [];
|
||||
$resultCode = 0;
|
||||
exec($command, $output, $resultCode);
|
||||
|
||||
$joined = trim(implode("\n", $output));
|
||||
$decoded = json_decode($joined, true);
|
||||
|
||||
if (is_array($decoded)) {
|
||||
return [
|
||||
'ok' => (bool) ($decoded['ok'] ?? false),
|
||||
'data' => $decoded,
|
||||
'raw' => $joined,
|
||||
'command' => $command,
|
||||
];
|
||||
}
|
||||
|
||||
if ($joined !== '') {
|
||||
$lastError = $joined;
|
||||
} elseif ($resultCode !== 0) {
|
||||
$lastError = 'Fehlercode ' . $resultCode . ' bei ' . $pythonBinary;
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'ok' => false,
|
||||
'error' => $lastError,
|
||||
];
|
||||
}
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && (string) ($_POST['ajax'] ?? '') === '1') {
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
|
||||
+1
-1
@@ -17,7 +17,7 @@ $publicBasePath = isset($publicBasePath) && is_string($publicBasePath) ? rtrim($
|
||||
<span class="footer-location-elev"><?= number_format((float)$currentLocation['elevation'], 0) ?> m</span>
|
||||
<?php endif; ?>
|
||||
<?php if ($loggedIn && empty($locationIsGlobal)): ?>
|
||||
<a href="<?= htmlspecialchars($publicBasePath . 'settings.php', ENT_QUOTES, 'UTF-8') ?>" class="footer-location-link">✎</a>
|
||||
<a href="<?= htmlspecialchars($publicBasePath . 'public/my_favorites.php', ENT_QUOTES, 'UTF-8') ?>" class="footer-location-link">✎</a>
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
</p>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
declare(strict_types=1);
|
||||
|
||||
session_start();
|
||||
require_once __DIR__ . '/python_api.php';
|
||||
|
||||
$loggedIn = isset($_SESSION['user_id']);
|
||||
$pageTitle = 'Meteorströme - AstroTools';
|
||||
@@ -81,47 +82,6 @@ function fetchCurrentSolarLongitude(DateTimeZone $displayTimezone): ?float
|
||||
return normalizeSolarLongitude((float) $solarLongitude);
|
||||
}
|
||||
|
||||
function runPythonApi(string $action, array $args): array
|
||||
{
|
||||
$scriptPath = __DIR__ . '/py/api.py';
|
||||
$pythonCandidates = PHP_OS_FAMILY === 'Windows'
|
||||
? ['python', 'py']
|
||||
: ['/usr/bin/python3', '/usr/bin/python', 'python3', 'python'];
|
||||
|
||||
$escapedArguments = array_map('escapeshellarg', array_merge([$scriptPath, $action], $args));
|
||||
$lastError = 'Python konnte nicht gestartet werden.';
|
||||
|
||||
foreach ($pythonCandidates as $pythonBinary) {
|
||||
$command = $pythonBinary . ' ' . implode(' ', $escapedArguments) . ' 2>&1';
|
||||
$output = [];
|
||||
$resultCode = 0;
|
||||
exec($command, $output, $resultCode);
|
||||
|
||||
$joined = trim(implode("\n", $output));
|
||||
$decoded = json_decode($joined, true);
|
||||
|
||||
if (is_array($decoded)) {
|
||||
return [
|
||||
'ok' => (bool) ($decoded['ok'] ?? false),
|
||||
'data' => $decoded,
|
||||
'raw' => $joined,
|
||||
'command' => $command,
|
||||
];
|
||||
}
|
||||
|
||||
if ($joined !== '') {
|
||||
$lastError = $joined;
|
||||
} elseif ($resultCode !== 0) {
|
||||
$lastError = 'Fehlercode ' . $resultCode . ' bei ' . $pythonBinary;
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'ok' => false,
|
||||
'error' => $lastError,
|
||||
];
|
||||
}
|
||||
|
||||
function isSolarLongitudeInRange(float $current, ?float $start, ?float $finish): bool
|
||||
{
|
||||
if ($start === null || $finish === null) {
|
||||
|
||||
+230
-377
@@ -5,26 +5,97 @@ ini_set('display_errors', '1');
|
||||
error_reporting(E_ALL);
|
||||
session_start();
|
||||
|
||||
require_once __DIR__ . '/auth_helpers.php';
|
||||
require_once __DIR__ . '/python_api.php';
|
||||
applyLocalDevLoginBypass();
|
||||
|
||||
$loggedIn = isset($_SESSION['user_id']);
|
||||
$pageTitle = 'Planetensichtbarkeit - AstroTools';
|
||||
$bodyClass = 'planetvis-page';
|
||||
?>
|
||||
<?php require __DIR__ . '/header.php'; ?>
|
||||
|
||||
<?php
|
||||
if (!isset($currentLocation) && $loggedIn && isset($_SESSION['user_id'])) {
|
||||
try {
|
||||
$cfg = require __DIR__ . '/../config/database.php';
|
||||
$pdo = new PDO(
|
||||
sprintf('mysql:host=%s;dbname=%s;charset=%s', $cfg['host'], $cfg['dbname'], $cfg['charset'] ?? 'utf8mb4'),
|
||||
$cfg['user'],
|
||||
$cfg['pass'],
|
||||
[
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||
]
|
||||
);
|
||||
|
||||
$stmtLoc = $pdo->prepare(
|
||||
'SELECT id, name, latitude, longitude, elevation, timezone
|
||||
FROM app_user_locations
|
||||
WHERE user_id = ? AND is_default = 1
|
||||
LIMIT 1'
|
||||
);
|
||||
$stmtLoc->execute([(int) $_SESSION['user_id']]);
|
||||
$currentLocation = $stmtLoc->fetch() ?: null;
|
||||
} catch (Throwable $e) {
|
||||
$currentLocation = null;
|
||||
}
|
||||
}
|
||||
|
||||
$pageLocation = null;
|
||||
if (!empty($currentLocation) && is_array($currentLocation)) {
|
||||
$pageLocation = [
|
||||
'name' => (string)($currentLocation['name'] ?? ''),
|
||||
'latitude' => isset($currentLocation['latitude']) ? (float)$currentLocation['latitude'] : null,
|
||||
'longitude' => isset($currentLocation['longitude']) ? (float)$currentLocation['longitude'] : null,
|
||||
'elevation' => isset($currentLocation['elevation']) && $currentLocation['elevation'] !== null ? (float)$currentLocation['elevation'] : 0.0,
|
||||
'timezone' => (string)($currentLocation['timezone'] ?? 'Europe/Berlin'),
|
||||
'name' => (string) ($currentLocation['name'] ?? ''),
|
||||
'latitude' => isset($currentLocation['latitude']) ? (float) $currentLocation['latitude'] : null,
|
||||
'longitude' => isset($currentLocation['longitude']) ? (float) $currentLocation['longitude'] : null,
|
||||
'elevation' => isset($currentLocation['elevation']) && $currentLocation['elevation'] !== null ? (float) $currentLocation['elevation'] : 0.0,
|
||||
'timezone' => (string) ($currentLocation['timezone'] ?? 'Europe/Berlin'),
|
||||
];
|
||||
}
|
||||
?>
|
||||
|
||||
<script src="js/astronomy.browser.min.js"></script>
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && (string) ($_POST['ajax'] ?? '') === '1') {
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
if ($pageLocation === null || $pageLocation['latitude'] === null || $pageLocation['longitude'] === null) {
|
||||
echo json_encode([
|
||||
'ok' => false,
|
||||
'error' => 'Kein gueltiger Standard-Standort verfuegbar.',
|
||||
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
exit;
|
||||
}
|
||||
|
||||
$dateText = trim((string) ($_POST['date'] ?? ''));
|
||||
$date = DateTimeImmutable::createFromFormat('Y-m-d', $dateText);
|
||||
$dateErrors = DateTimeImmutable::getLastErrors();
|
||||
if ($dateErrors === false) {
|
||||
$dateErrors = ['warning_count' => 0, 'error_count' => 0];
|
||||
}
|
||||
if ($date === false || ($dateErrors['warning_count'] ?? 0) > 0 || ($dateErrors['error_count'] ?? 0) > 0) {
|
||||
echo json_encode([
|
||||
'ok' => false,
|
||||
'error' => 'Datum muss im Format YYYY-MM-DD uebergeben werden.',
|
||||
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
exit;
|
||||
}
|
||||
|
||||
$pythonResult = runPythonApi('planet_visibility_chart', [
|
||||
(string) $pageLocation['latitude'],
|
||||
(string) $pageLocation['longitude'],
|
||||
(string) $pageLocation['elevation'],
|
||||
$dateText,
|
||||
(string) $pageLocation['timezone'],
|
||||
]);
|
||||
|
||||
if (!$pythonResult['ok']) {
|
||||
echo json_encode([
|
||||
'ok' => false,
|
||||
'error' => (string) ($pythonResult['error'] ?? 'Python-Fehler'),
|
||||
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
exit;
|
||||
}
|
||||
|
||||
echo json_encode($pythonResult['data'], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
exit;
|
||||
}
|
||||
?>
|
||||
<?php require __DIR__ . '/header.php'; ?>
|
||||
|
||||
<div class="planetvis-tool">
|
||||
<?php if ($pageLocation === null || $pageLocation['latitude'] === null || $pageLocation['longitude'] === null): ?>
|
||||
@@ -32,13 +103,13 @@ if (!empty($currentLocation) && is_array($currentLocation)) {
|
||||
<h2>Kein Standard-Standort gesetzt</h2>
|
||||
<p class="hint">
|
||||
Diese Seite verwendet den im Header geladenen Standard-Standort aus der Datenbank.
|
||||
Bitte wähle in den <a href="settings.php">Einstellungen</a> einen Standard-Standort aus.
|
||||
Bitte waehle in den <a href="settings.php">Einstellungen</a> einen Standard-Standort aus.
|
||||
</p>
|
||||
</section>
|
||||
<?php else: ?>
|
||||
<section class="card planetvis-card">
|
||||
<p class="planetvis-intro">
|
||||
Sichtbarkeit der Planeten und des Mondes über 24 Stunden für deinen Standard-Standort aus der Datenbank.
|
||||
Sichtbarkeit der Planeten und des Mondes über 24 Stunden fuer deinen Standard-Standort aus der Datenbank.
|
||||
</p>
|
||||
|
||||
<div class="date-section">
|
||||
@@ -58,7 +129,7 @@ if (!empty($currentLocation) && is_array($currentLocation)) {
|
||||
</div>
|
||||
<div class="legend-item">
|
||||
<div class="leg-line"></div>
|
||||
<span>Ueber Horizont</span>
|
||||
<span>Über Horizont</span>
|
||||
</div>
|
||||
<div class="legend-item">
|
||||
<div class="leg-line leg-line-best"></div>
|
||||
@@ -97,19 +168,6 @@ const LAT = Number(SITE_LOCATION.latitude);
|
||||
const LNG = Number(SITE_LOCATION.longitude);
|
||||
const ELEV = Number(SITE_LOCATION.elevation);
|
||||
|
||||
const DAYS_DE = ['SO', 'MO', 'DI', 'MI', 'DO', 'FR', 'SA'];
|
||||
const MONTHS_DE = ['JAN', 'FEB', 'MRZ', 'APR', 'MAI', 'JUN', 'JUL', 'AUG', 'SEP', 'OKT', 'NOV', 'DEZ'];
|
||||
|
||||
const PLANET_DEFS = [
|
||||
{name:'MERKUR', body:'Mercury', symbol:'Me', color:'#b0b0c8', glow:'rgba(176,176,200,.45)'},
|
||||
{name:'VENUS', body:'Venus', symbol:'Ve', color:'#f5e090', glow:'rgba(245,224,144,.55)'},
|
||||
{name:'MARS', body:'Mars', symbol:'Ma', color:'#e8602a', glow:'rgba(232,96,42,.5)'},
|
||||
{name:'JUPITER', body:'Jupiter', symbol:'Ju', color:'#d4b880', glow:'rgba(212,184,128,.5)'},
|
||||
{name:'SATURN', body:'Saturn', symbol:'Sa', color:'#e8d898', glow:'rgba(232,216,152,.45)'},
|
||||
{name:'URANUS', body:'Uranus', symbol:'Ur', color:'#70e0e8', glow:'rgba(112,224,232,.4)'},
|
||||
{name:'NEPTUN', body:'Neptune', symbol:'Ne', color:'#4070e0', glow:'rgba(64,112,224,.4)'},
|
||||
];
|
||||
|
||||
const hToP = h => Math.min(100, Math.max(0, h / 24 * 100));
|
||||
const fmt2 = n => String(n).padStart(2, '0');
|
||||
const fmtDate = d => `${fmt2(d.getDate())}.${fmt2(d.getMonth() + 1)}.${d.getFullYear()}`;
|
||||
@@ -126,148 +184,10 @@ function hStr(h) {
|
||||
return `${fmt2(Math.floor(t))}:${fmt2(Math.floor((t - Math.floor(t)) * 60))}`;
|
||||
}
|
||||
|
||||
function getTzH(date) {
|
||||
function lastSunUTC(y, m) {
|
||||
const d = new Date(Date.UTC(y, m + 1, 1));
|
||||
d.setUTCDate(d.getUTCDate() - ((d.getUTCDay() + 7) % 7 || 7));
|
||||
return d;
|
||||
}
|
||||
|
||||
const lsm = lastSunUTC(date.getFullYear(), 2);
|
||||
const lso = lastSunUTC(date.getFullYear(), 9);
|
||||
return (date >= lsm && date < lso) ? 2 : 1;
|
||||
}
|
||||
|
||||
function localMidUTC(localDate, tzH) {
|
||||
return new Date(Date.UTC(localDate.getFullYear(), localDate.getMonth(), localDate.getDate()) - tzH * 3600000);
|
||||
}
|
||||
|
||||
function atToH(at, midMs) {
|
||||
return at ? (at.date.getTime() - midMs) / 3600000 : null;
|
||||
}
|
||||
|
||||
function buildSegments(bodyName, obs, midUTC, midMs) {
|
||||
const Ast = Astronomy;
|
||||
|
||||
let isUpAtMid = false;
|
||||
try {
|
||||
const tMid = Ast.MakeTime(midUTC);
|
||||
const eq = Ast.Equator(bodyName, tMid, obs, true, true);
|
||||
const hz = Ast.Horizon(tMid, obs, eq.ra, eq.dec, 'normal');
|
||||
isUpAtMid = hz.altitude > 0;
|
||||
} catch (error) {
|
||||
console.warn('Startstatus konnte nicht berechnet werden.', error);
|
||||
}
|
||||
|
||||
const events = [];
|
||||
|
||||
let tSearch = new Date(midMs - 15 * 60000);
|
||||
for (let i = 0; i < 3; i += 1) {
|
||||
const rise = Ast.SearchRiseSet(bodyName, obs, +1, tSearch, 1.05);
|
||||
if (!rise) {
|
||||
break;
|
||||
}
|
||||
const h = (rise.date.getTime() - midMs) / 3600000;
|
||||
if (h > 24.1) {
|
||||
break;
|
||||
}
|
||||
events.push({ h, type: 'rise' });
|
||||
tSearch = new Date(rise.date.getTime() + 60000);
|
||||
}
|
||||
|
||||
tSearch = new Date(midMs - 15 * 60000);
|
||||
for (let i = 0; i < 3; i += 1) {
|
||||
const set = Ast.SearchRiseSet(bodyName, obs, -1, tSearch, 1.05);
|
||||
if (!set) {
|
||||
break;
|
||||
}
|
||||
const h = (set.date.getTime() - midMs) / 3600000;
|
||||
if (h > 24.1) {
|
||||
break;
|
||||
}
|
||||
events.push({ h, type: 'set' });
|
||||
tSearch = new Date(set.date.getTime() + 60000);
|
||||
}
|
||||
|
||||
events.sort((a, b) => a.h - b.h);
|
||||
|
||||
const segs = [];
|
||||
let isUp = isUpAtMid;
|
||||
let segStart = isUp ? 0 : null;
|
||||
|
||||
for (const ev of events) {
|
||||
const clampedH = Math.max(-0.1, Math.min(24.1, ev.h));
|
||||
if (ev.type === 'rise' && !isUp) {
|
||||
segStart = Math.max(0, clampedH);
|
||||
isUp = true;
|
||||
} else if (ev.type === 'set' && isUp) {
|
||||
const segEnd = Math.min(24, clampedH);
|
||||
if (segEnd > (segStart ?? 0)) {
|
||||
segs.push({ from: segStart ?? 0, to: segEnd });
|
||||
}
|
||||
segStart = null;
|
||||
isUp = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (isUp && segStart !== null) {
|
||||
segs.push({ from: segStart, to: 24 });
|
||||
}
|
||||
|
||||
return segs;
|
||||
}
|
||||
|
||||
function getTwilight(obs, midUTC, midMs) {
|
||||
const Ast = Astronomy;
|
||||
|
||||
function searchAlt(dir, alt, startOffsetH) {
|
||||
const t0 = new Date(midMs + startOffsetH * 3600000);
|
||||
try {
|
||||
return Ast.SearchAltitude('Sun', obs, dir, t0, 0.6, alt);
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
sunRise: Ast.SearchRiseSet('Sun', obs, +1, midUTC, 1),
|
||||
sunSet: Ast.SearchRiseSet('Sun', obs, -1, midUTC, 1),
|
||||
civDawn: searchAlt(+1, -6, 0),
|
||||
civDusk: searchAlt(-1, -6, 10),
|
||||
nautDawn: searchAlt(+1, -12, 0),
|
||||
nautDusk: searchAlt(-1, -12, 10),
|
||||
astroDawn: searchAlt(+1, -18, 0),
|
||||
astroDusk: searchAlt(-1, -18, 10),
|
||||
};
|
||||
}
|
||||
|
||||
function getMoonInfo(midUTC) {
|
||||
const Ast = Astronomy;
|
||||
const t = Ast.MakeTime(new Date(midUTC.getTime() + 43200000));
|
||||
const phase = Ast.MoonPhase(t);
|
||||
const illum = Ast.Illumination('Moon', t);
|
||||
const waning = phase > 180;
|
||||
let label = 'NM';
|
||||
|
||||
if (phase < 22.5 || phase >= 337.5) label = 'NM';
|
||||
else if (!waning && phase < 67.5) label = 'ZS';
|
||||
else if (!waning && phase < 112.5) label = 'ZH';
|
||||
else if (!waning && phase < 157.5) label = 'ZG';
|
||||
else if (phase >= 157.5 && phase < 202.5) label = 'VM';
|
||||
else if (waning && phase < 247.5) label = 'AG';
|
||||
else if (waning && phase < 292.5) label = 'AH';
|
||||
else label = 'AS';
|
||||
|
||||
return {
|
||||
phase,
|
||||
illum: illum.phase_fraction ?? ((1 + Math.cos(illum.phase_angle * Math.PI / 180)) / 2),
|
||||
waning,
|
||||
label
|
||||
};
|
||||
}
|
||||
|
||||
function buildGradient(tw, midMs) {
|
||||
const p = at => at ? Math.max(0, Math.min(100, (at.date.getTime() - midMs) / 3600000 / 24 * 100)).toFixed(2) : null;
|
||||
function buildGradient(tw) {
|
||||
const p = hourValue => hourValue === null || hourValue === undefined
|
||||
? null
|
||||
: Math.max(0, Math.min(100, hourValue / 24 * 100)).toFixed(2);
|
||||
const stops = [];
|
||||
const add = (pos, col) => {
|
||||
if (pos !== null) {
|
||||
@@ -275,14 +195,14 @@ function buildGradient(tw, midMs) {
|
||||
}
|
||||
};
|
||||
|
||||
const sr = p(tw.sunRise);
|
||||
const ss = p(tw.sunSet);
|
||||
const cd = p(tw.civDawn);
|
||||
const cu = p(tw.civDusk);
|
||||
const nd = p(tw.nautDawn);
|
||||
const nu = p(tw.nautDusk);
|
||||
const ad = p(tw.astroDawn);
|
||||
const au = p(tw.astroDusk);
|
||||
const sr = p(tw.sunRiseH);
|
||||
const ss = p(tw.sunSetH);
|
||||
const cd = p(tw.civDawnH);
|
||||
const cu = p(tw.civDuskH);
|
||||
const nd = p(tw.nautDawnH);
|
||||
const nu = p(tw.nautDuskH);
|
||||
const ad = p(tw.astroDawnH);
|
||||
const au = p(tw.astroDuskH);
|
||||
|
||||
add('0', '#010108');
|
||||
if (ad) add(Math.max(0, parseFloat(ad) - 2).toFixed(2), '#010108');
|
||||
@@ -337,30 +257,6 @@ function updateNeedles() {
|
||||
}
|
||||
window.setInterval(updateNeedles, 15000);
|
||||
|
||||
function computeBestSegs(segs, astroDawnH, astroDuskH) {
|
||||
if (astroDawnH === null || astroDuskH === null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const result = [];
|
||||
for (const seg of segs) {
|
||||
if (seg.from < astroDawnH) {
|
||||
const to = Math.min(seg.to, astroDawnH);
|
||||
if (to - seg.from > 0.05) {
|
||||
result.push({ from: seg.from, to });
|
||||
}
|
||||
}
|
||||
if (seg.to > astroDuskH) {
|
||||
const from = Math.max(seg.from, astroDuskH);
|
||||
const to = Math.min(seg.to, 24);
|
||||
if (to - from > 0.05) {
|
||||
result.push({ from, to });
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function addStripe(bar, from, to, def) {
|
||||
if (to - from < 0.05) {
|
||||
return;
|
||||
@@ -395,177 +291,6 @@ function addBest(bar, from, to, def) {
|
||||
}
|
||||
|
||||
const tooltip = document.getElementById('tooltip');
|
||||
|
||||
function renderChart(localDate) {
|
||||
const container = document.getElementById('chartRows');
|
||||
const infoDiv = document.getElementById('infoCards');
|
||||
const loadDiv = document.getElementById('loadOverlay');
|
||||
container.innerHTML = '';
|
||||
infoDiv.innerHTML = '';
|
||||
needleEls = [];
|
||||
loadDiv.innerHTML = '<div class="spin-ring"></div>Berechne Positionen...';
|
||||
loadDiv.classList.remove('is-hidden');
|
||||
|
||||
window.setTimeout(() => {
|
||||
try {
|
||||
const tzH = getTzH(localDate);
|
||||
const midUTC = localMidUTC(localDate, tzH);
|
||||
const midMs = midUTC.getTime();
|
||||
const obs = new Astronomy.Observer(LAT, LNG, ELEV);
|
||||
|
||||
const tw = getTwilight(obs, midUTC, midMs);
|
||||
const gradient = buildGradient(tw, midMs);
|
||||
|
||||
const astroDawnH = atToH(tw.astroDawn, midMs);
|
||||
const astroDuskH = atToH(tw.astroDusk, midMs);
|
||||
|
||||
const moonInfo = getMoonInfo(midUTC);
|
||||
const moonSegs = buildSegments('Moon', obs, midUTC, midMs);
|
||||
const moonBestSegs = computeBestSegs(moonSegs, astroDawnH, astroDuskH);
|
||||
|
||||
const allDefs = [
|
||||
...PLANET_DEFS.map(d => ({ ...d, type: 'planet' })),
|
||||
{
|
||||
name: 'MOND',
|
||||
body: 'Moon',
|
||||
symbol: moonInfo.label,
|
||||
type: 'moon',
|
||||
color: '#d0d8f0',
|
||||
glow: 'rgba(208,216,240,.55)',
|
||||
segs: moonSegs,
|
||||
bestSegs: moonBestSegs,
|
||||
illum: moonInfo.illum,
|
||||
waning: moonInfo.waning,
|
||||
label: moonInfo.label
|
||||
}
|
||||
];
|
||||
|
||||
allDefs.forEach((def, idx) => {
|
||||
if (def.type === 'moon') {
|
||||
container.appendChild(Object.assign(document.createElement('div'), { className: 'moon-sep' }));
|
||||
}
|
||||
|
||||
let segs = def.segs || [];
|
||||
let bestSegs = def.bestSegs || [];
|
||||
let mag = '-';
|
||||
|
||||
if (def.type === 'planet') {
|
||||
segs = buildSegments(def.body, obs, midUTC, midMs);
|
||||
bestSegs = computeBestSegs(segs, astroDawnH, astroDuskH);
|
||||
try {
|
||||
const il = Astronomy.Illumination(def.body, Astronomy.MakeTime(new Date(midMs + 43200000)));
|
||||
mag = (il.mag >= 0 ? '+' : '') + il.mag.toFixed(1);
|
||||
} catch (error) {
|
||||
console.warn('Magnitude konnte nicht berechnet werden.', error);
|
||||
}
|
||||
}
|
||||
|
||||
let actualRiseH = segs.length ? segs[0].from : null;
|
||||
let actualSetH = segs.length ? segs[segs.length - 1].to : null;
|
||||
if (segs.length && segs[0].from < 0.08) {
|
||||
try {
|
||||
const rise = Astronomy.SearchRiseSet(def.body, obs, +1, new Date(midMs - 36 * 3600000), 2.5);
|
||||
if (rise) {
|
||||
const rh = (rise.date.getTime() - midMs) / 3600000;
|
||||
if (rh < 0.08) {
|
||||
actualRiseH = rh;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Aufgang ausserhalb des Fensters konnte nicht berechnet werden.', error);
|
||||
}
|
||||
}
|
||||
if (segs.length && segs[segs.length - 1].to > 23.95) {
|
||||
try {
|
||||
const set = Astronomy.SearchRiseSet(def.body, obs, -1, new Date(midMs + 23 * 3600000), 2.0);
|
||||
if (set) {
|
||||
const sh = (set.date.getTime() - midMs) / 3600000;
|
||||
if (sh > 23.95) {
|
||||
actualSetH = sh;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Untergang ausserhalb des Fensters konnte nicht berechnet werden.', error);
|
||||
}
|
||||
}
|
||||
|
||||
const hStrDay = h => h === null ? '--:--' : hStr(h);
|
||||
|
||||
const row = document.createElement('div');
|
||||
row.className = 'planet-row';
|
||||
const metaStr = def.type === 'moon'
|
||||
? `${Math.round(def.illum * 100)}% ${def.waning ? 'abnehmend' : 'zunehmend'}`
|
||||
: `Mag ${mag}`;
|
||||
|
||||
row.innerHTML = `
|
||||
<div class="planet-label">
|
||||
<div class="planet-orb">${def.symbol}</div>
|
||||
<div><div class="planet-name">${def.name}</div><div class="planet-meta">${metaStr}</div></div>
|
||||
</div>
|
||||
`;
|
||||
const orbEl = row.querySelector('.planet-orb');
|
||||
orbEl.style.setProperty('--planet-orb-color', def.color);
|
||||
orbEl.style.setProperty('--planet-orb-glow', def.glow);
|
||||
|
||||
const barOuter = document.createElement('div');
|
||||
barOuter.className = 'bar-outer';
|
||||
const barBg = document.createElement('div');
|
||||
barBg.className = 'bar-bg';
|
||||
barBg.style.background = gradient;
|
||||
barOuter.appendChild(barBg);
|
||||
|
||||
segs.forEach(s => addStripe(barOuter, s.from, s.to, def));
|
||||
bestSegs.forEach(s => addBest(barOuter, s.from, s.to, def));
|
||||
|
||||
const needle = document.createElement('div');
|
||||
needle.className = 'needle';
|
||||
needle.innerHTML = `${idx === 0 ? '<div class="needle-lbl">JETZT</div>' : ''}<div class="needle-glow"></div><div class="needle-cap"></div>`;
|
||||
barOuter.appendChild(needle);
|
||||
needleEls.push(needle);
|
||||
|
||||
barOuter.addEventListener('mousemove', e => {
|
||||
const rect = barOuter.getBoundingClientRect();
|
||||
const h = ((e.clientX - rect.left) / rect.width) * 24;
|
||||
const above = segs.some(s => h >= s.from && h <= s.to);
|
||||
const rStr = segs.length ? hStrDay(actualRiseH) : '--:--';
|
||||
const sStr = segs.length ? hStrDay(actualSetH) : '--:--';
|
||||
document.getElementById('tt-title').textContent = def.name;
|
||||
document.getElementById('tt-body').innerHTML =
|
||||
`Uhrzeit: ${hStr(h)}<br>Aufgang: ${rStr}<br>Untergang: ${sStr}<br>` +
|
||||
(def.type === 'moon' ? `Beleuchtung: ${Math.round(def.illum * 100)}%<br>` : `Magnitude: ${mag}<br>`) +
|
||||
`Status: ${above ? '<span class="tt-up">über Horizont</span>' : '<span class="tt-down">unter Horizont</span>'}`;
|
||||
tooltip.style.left = (e.clientX + 16) + 'px';
|
||||
tooltip.style.top = (e.clientY - 14) + 'px';
|
||||
tooltip.classList.add('vis');
|
||||
});
|
||||
barOuter.addEventListener('mouseleave', () => tooltip.classList.remove('vis'));
|
||||
|
||||
row.appendChild(barOuter);
|
||||
container.appendChild(row);
|
||||
if (idx < allDefs.length - 1 && allDefs[idx + 1].type !== 'moon' && def.type !== 'moon') {
|
||||
container.appendChild(Object.assign(document.createElement('div'), { className: 'planet-div' }));
|
||||
}
|
||||
});
|
||||
|
||||
const hF = at => at ? hStr(atToH(at, midMs)) + ' Uhr' : '-';
|
||||
infoDiv.innerHTML = `
|
||||
<div class="info-card"><strong>Standort</strong><br>${SITE_LOCATION.name}<br>${LAT.toFixed(3)}deg N · ${LNG.toFixed(3)}deg O · ${ELEV} m</div>
|
||||
<div class="info-card"><strong>Sonne (${fmtDate(localDate)})</strong><br>Aufgang: ${hF(tw.sunRise)}<br>Untergang: ${hF(tw.sunSet)}</div>
|
||||
<div class="info-card"><strong>Dämmerung</strong><br>Bürgerlich: ${hF(tw.civDawn)} / ${hF(tw.civDusk)}<br>Nautisch: ${hF(tw.nautDawn)} / ${hF(tw.nautDusk)}<br>Astronomisch: ${hF(tw.astroDawn)} / ${hF(tw.astroDusk)}</div>
|
||||
<div class="info-card"><strong>Mond (${fmtDate(localDate)})</strong><br>${Math.round(moonInfo.illum * 100)}% ${moonInfo.waning ? 'abnehmend' : 'zunehmend'}<br>Phase: ${moonInfo.phase.toFixed(1)}deg</div>
|
||||
<div class="info-card"><strong>Zeitzone</strong><br>${tzH === 2 ? 'MESZ (UTC+2)' : 'MEZ (UTC+1)'}<br>${SITE_LOCATION.timezone}</div>
|
||||
`;
|
||||
|
||||
loadDiv.classList.add('is-hidden');
|
||||
updateNeedles();
|
||||
} catch (error) {
|
||||
loadDiv.innerHTML = `<span class="planetvis-error-text">Fehler: ${error.message}</span>`;
|
||||
loadDiv.classList.remove('is-hidden');
|
||||
console.error(error);
|
||||
}
|
||||
}, 30);
|
||||
}
|
||||
|
||||
const datePicker = document.getElementById('datePicker');
|
||||
const localDateStr = d => `${d.getFullYear()}-${fmt2(d.getMonth() + 1)}-${fmt2(d.getDate())}`;
|
||||
const pickerDate = () => {
|
||||
@@ -573,6 +298,139 @@ const pickerDate = () => {
|
||||
return new Date(y, m - 1, d, 12);
|
||||
};
|
||||
|
||||
async function fetchVisibilityData(localDate) {
|
||||
const formData = new FormData();
|
||||
formData.set('ajax', '1');
|
||||
formData.set('date', localDateStr(localDate));
|
||||
|
||||
const response = await fetch('planetensichtbarkeit.php', {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
headers: { 'X-Requested-With': 'XMLHttpRequest' }
|
||||
});
|
||||
|
||||
const payload = await response.json();
|
||||
if (!response.ok || !payload.ok) {
|
||||
throw new Error(payload.error || 'Die Sichtbarkeitsdaten konnten nicht geladen werden.');
|
||||
}
|
||||
|
||||
return payload;
|
||||
}
|
||||
|
||||
function renderChartRows(rows, gradient) {
|
||||
const container = document.getElementById('chartRows');
|
||||
container.innerHTML = '';
|
||||
needleEls = [];
|
||||
|
||||
rows.forEach((def, idx) => {
|
||||
if (def.type === 'moon') {
|
||||
container.appendChild(Object.assign(document.createElement('div'), { className: 'moon-sep' }));
|
||||
}
|
||||
|
||||
const segs = Array.isArray(def.segs) ? def.segs : [];
|
||||
const bestSegs = Array.isArray(def.bestSegs) ? def.bestSegs : [];
|
||||
const magnitudeText = def.type === 'planet' && typeof def.magnitude === 'number'
|
||||
? `${def.magnitude >= 0 ? '+' : ''}${def.magnitude.toFixed(1)}`
|
||||
: '-';
|
||||
const metaStr = def.type === 'moon'
|
||||
? `${Math.round((def.illum || 0) * 100)}% ${def.waning ? 'abnehmend' : 'zunehmend'}`
|
||||
: `Mag ${magnitudeText}`;
|
||||
|
||||
const row = document.createElement('div');
|
||||
row.className = 'planet-row';
|
||||
row.innerHTML = `
|
||||
<div class="planet-label">
|
||||
<div class="planet-orb">${def.symbol}</div>
|
||||
<div><div class="planet-name">${def.name}</div><div class="planet-meta">${metaStr}</div></div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const orbEl = row.querySelector('.planet-orb');
|
||||
orbEl.style.setProperty('--planet-orb-color', def.color);
|
||||
orbEl.style.setProperty('--planet-orb-glow', def.glow);
|
||||
|
||||
const barOuter = document.createElement('div');
|
||||
barOuter.className = 'bar-outer';
|
||||
const barBg = document.createElement('div');
|
||||
barBg.className = 'bar-bg';
|
||||
barBg.style.background = gradient;
|
||||
barOuter.appendChild(barBg);
|
||||
|
||||
segs.forEach(s => addStripe(barOuter, s.from, s.to, def));
|
||||
bestSegs.forEach(s => addBest(barOuter, s.from, s.to, def));
|
||||
|
||||
const needle = document.createElement('div');
|
||||
needle.className = 'needle';
|
||||
needle.innerHTML = `${idx === 0 ? '<div class="needle-lbl">JETZT</div>' : ''}<div class="needle-glow"></div><div class="needle-cap"></div>`;
|
||||
barOuter.appendChild(needle);
|
||||
needleEls.push(needle);
|
||||
|
||||
barOuter.addEventListener('mousemove', e => {
|
||||
const rect = barOuter.getBoundingClientRect();
|
||||
const h = ((e.clientX - rect.left) / rect.width) * 24;
|
||||
const above = segs.some(s => h >= s.from && h <= s.to);
|
||||
document.getElementById('tt-title').textContent = def.name;
|
||||
document.getElementById('tt-body').innerHTML =
|
||||
`Uhrzeit: ${hStr(h)}<br>Aufgang: ${hStr(def.actualRiseH)}<br>Untergang: ${hStr(def.actualSetH)}<br>` +
|
||||
(def.type === 'moon'
|
||||
? `Beleuchtung: ${Math.round((def.illum || 0) * 100)}%<br>`
|
||||
: `Magnitude: ${magnitudeText}<br>`) +
|
||||
`Status: ${above ? '<span class="tt-up">über Horizont</span>' : '<span class="tt-down">unter Horizont</span>'}`;
|
||||
tooltip.style.left = (e.clientX + 16) + 'px';
|
||||
tooltip.style.top = (e.clientY - 14) + 'px';
|
||||
tooltip.classList.add('vis');
|
||||
});
|
||||
barOuter.addEventListener('mouseleave', () => tooltip.classList.remove('vis'));
|
||||
|
||||
row.appendChild(barOuter);
|
||||
container.appendChild(row);
|
||||
if (idx < rows.length - 1 && rows[idx + 1].type !== 'moon' && def.type !== 'moon') {
|
||||
container.appendChild(Object.assign(document.createElement('div'), { className: 'planet-div' }));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function renderInfoCards(localDate, data) {
|
||||
const infoDiv = document.getElementById('infoCards');
|
||||
const tw = data.twilight || {};
|
||||
const moon = data.moon || {};
|
||||
const offset = Number(data.utc_offset_hours);
|
||||
const tzLabel = Number.isFinite(offset)
|
||||
? `${data.timezone_abbr} (UTC${offset >= 0 ? '+' : ''}${offset})`
|
||||
: (data.timezone_abbr || SITE_LOCATION.timezone);
|
||||
const hF = hourValue => hourValue === null || hourValue === undefined ? '-' : `${hStr(hourValue)} Uhr`;
|
||||
|
||||
infoDiv.innerHTML = `
|
||||
<div class="info-card"><strong>Standort</strong><br>${SITE_LOCATION.name}<br>${LAT.toFixed(3)}deg N | ${LNG.toFixed(3)}deg O | ${ELEV} m</div>
|
||||
<div class="info-card"><strong>Sonne (${fmtDate(localDate)})</strong><br>Aufgang: ${hF(tw.sunRiseH)}<br>Untergang: ${hF(tw.sunSetH)}</div>
|
||||
<div class="info-card"><strong>Dämmerung</strong><br>Buergerlich: ${hF(tw.civDawnH)} / ${hF(tw.civDuskH)}<br>Nautisch: ${hF(tw.nautDawnH)} / ${hF(tw.nautDuskH)}<br>Astronomisch: ${hF(tw.astroDawnH)} / ${hF(tw.astroDuskH)}</div>
|
||||
<div class="info-card"><strong>Mond (${fmtDate(localDate)})</strong><br>${Math.round((moon.illum || 0) * 100)}% ${moon.waning ? 'abnehmend' : 'zunehmend'}<br>Phase: ${typeof moon.phase === 'number' ? moon.phase.toFixed(1) : '--'}deg</div>
|
||||
<div class="info-card"><strong>Zeitzone</strong><br>${tzLabel}<br>${SITE_LOCATION.timezone}</div>
|
||||
`;
|
||||
}
|
||||
|
||||
async function renderChart(localDate) {
|
||||
const loadDiv = document.getElementById('loadOverlay');
|
||||
document.getElementById('chartRows').innerHTML = '';
|
||||
document.getElementById('infoCards').innerHTML = '';
|
||||
needleEls = [];
|
||||
loadDiv.innerHTML = '<div class="spin-ring"></div>Berechne Positionen...';
|
||||
loadDiv.classList.remove('is-hidden');
|
||||
|
||||
try {
|
||||
const data = await fetchVisibilityData(localDate);
|
||||
const gradient = buildGradient(data.twilight || {});
|
||||
renderChartRows(Array.isArray(data.rows) ? data.rows : [], gradient);
|
||||
renderInfoCards(localDate, data);
|
||||
loadDiv.classList.add('is-hidden');
|
||||
updateNeedles();
|
||||
} catch (error) {
|
||||
loadDiv.innerHTML = `<span class="planetvis-error-text">Fehler: ${error.message}</span>`;
|
||||
loadDiv.classList.remove('is-hidden');
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('btnToday').addEventListener('click', () => {
|
||||
const t = new Date();
|
||||
datePicker.value = localDateStr(t);
|
||||
@@ -591,12 +449,7 @@ function init() {
|
||||
renderChart(new Date(today.getFullYear(), today.getMonth(), today.getDate(), 12));
|
||||
}
|
||||
|
||||
if (typeof Astronomy !== 'undefined') {
|
||||
init();
|
||||
} else {
|
||||
document.getElementById('loadOverlay').innerHTML =
|
||||
'<span class="planetvis-error-text">Bibliothek konnte nicht geladen werden.</span>';
|
||||
}
|
||||
init();
|
||||
</script>
|
||||
<?php endif; ?>
|
||||
|
||||
|
||||
+361
-1
@@ -396,6 +396,361 @@ PLANETS = [
|
||||
("Neptun", astronomy.Body.Neptune),
|
||||
]
|
||||
|
||||
PLANET_VISIBILITY_ROWS = [
|
||||
{
|
||||
"key": "mercury",
|
||||
"name": "MERKUR",
|
||||
"body": astronomy.Body.Mercury,
|
||||
"symbol": "Me",
|
||||
"color": "#b0b0c8",
|
||||
"glow": "rgba(176,176,200,.45)",
|
||||
"type": "planet",
|
||||
},
|
||||
{
|
||||
"key": "venus",
|
||||
"name": "VENUS",
|
||||
"body": astronomy.Body.Venus,
|
||||
"symbol": "Ve",
|
||||
"color": "#f5e090",
|
||||
"glow": "rgba(245,224,144,.55)",
|
||||
"type": "planet",
|
||||
},
|
||||
{
|
||||
"key": "mars",
|
||||
"name": "MARS",
|
||||
"body": astronomy.Body.Mars,
|
||||
"symbol": "Ma",
|
||||
"color": "#e8602a",
|
||||
"glow": "rgba(232,96,42,.5)",
|
||||
"type": "planet",
|
||||
},
|
||||
{
|
||||
"key": "jupiter",
|
||||
"name": "JUPITER",
|
||||
"body": astronomy.Body.Jupiter,
|
||||
"symbol": "Ju",
|
||||
"color": "#d4b880",
|
||||
"glow": "rgba(212,184,128,.5)",
|
||||
"type": "planet",
|
||||
},
|
||||
{
|
||||
"key": "saturn",
|
||||
"name": "SATURN",
|
||||
"body": astronomy.Body.Saturn,
|
||||
"symbol": "Sa",
|
||||
"color": "#e8d898",
|
||||
"glow": "rgba(232,216,152,.45)",
|
||||
"type": "planet",
|
||||
},
|
||||
{
|
||||
"key": "uranus",
|
||||
"name": "URANUS",
|
||||
"body": astronomy.Body.Uranus,
|
||||
"symbol": "Ur",
|
||||
"color": "#70e0e8",
|
||||
"glow": "rgba(112,224,232,.4)",
|
||||
"type": "planet",
|
||||
},
|
||||
{
|
||||
"key": "neptune",
|
||||
"name": "NEPTUN",
|
||||
"body": astronomy.Body.Neptune,
|
||||
"symbol": "Ne",
|
||||
"color": "#4070e0",
|
||||
"glow": "rgba(64,112,224,.4)",
|
||||
"type": "planet",
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def hour_from_utc_start(event_time: astronomy.Time | None, utc_start: datetime) -> float | None:
|
||||
if event_time is None:
|
||||
return None
|
||||
return (time_to_datetime(event_time) - utc_start).total_seconds() / 3600.0
|
||||
|
||||
|
||||
def build_visibility_segments(
|
||||
body: astronomy.Body,
|
||||
observer: astronomy.Observer,
|
||||
utc_start: datetime,
|
||||
utc_end: datetime,
|
||||
) -> tuple[list[dict], float | None, float | None]:
|
||||
start_time = dt_to_time(utc_start)
|
||||
start_eq = astronomy.Equator(body, start_time, observer, True, True)
|
||||
start_hor = astronomy.Horizon(start_time, observer, start_eq.ra, start_eq.dec, astronomy.Refraction.Normal)
|
||||
is_up = float(start_hor.altitude) > 0.0
|
||||
|
||||
events: list[dict] = []
|
||||
|
||||
def gather(direction: astronomy.Direction) -> None:
|
||||
search_dt = utc_start - timedelta(minutes=15)
|
||||
while True:
|
||||
limit_days = max(0.01, min(3.0, (utc_end + timedelta(hours=2) - search_dt).total_seconds() / 86400.0))
|
||||
event_time = astronomy.SearchRiseSet(body, observer, direction, dt_to_time(search_dt), limit_days, 1.05)
|
||||
if event_time is None:
|
||||
break
|
||||
|
||||
hour_value = hour_from_utc_start(event_time, utc_start)
|
||||
if hour_value is None or hour_value > 24.1:
|
||||
break
|
||||
|
||||
events.append({
|
||||
"hour": hour_value,
|
||||
"type": "rise" if direction == astronomy.Direction.Rise else "set",
|
||||
})
|
||||
search_dt = time_to_datetime(event_time) + timedelta(minutes=1)
|
||||
|
||||
gather(astronomy.Direction.Rise)
|
||||
gather(astronomy.Direction.Set)
|
||||
events.sort(key=lambda item: item["hour"])
|
||||
|
||||
segments: list[dict] = []
|
||||
segment_start = 0.0 if is_up else None
|
||||
|
||||
for event in events:
|
||||
clamped_hour = max(-0.1, min(24.1, float(event["hour"])))
|
||||
if event["type"] == "rise" and not is_up:
|
||||
segment_start = max(0.0, clamped_hour)
|
||||
is_up = True
|
||||
elif event["type"] == "set" and is_up:
|
||||
segment_end = min(24.0, clamped_hour)
|
||||
if segment_start is not None and segment_end > segment_start:
|
||||
segments.append({"from": segment_start, "to": segment_end})
|
||||
segment_start = None
|
||||
is_up = False
|
||||
|
||||
if is_up and segment_start is not None:
|
||||
segments.append({"from": segment_start, "to": 24.0})
|
||||
|
||||
actual_rise_hour = segments[0]["from"] if segments else None
|
||||
actual_set_hour = segments[-1]["to"] if segments else None
|
||||
|
||||
if segments and segments[0]["from"] < 0.08:
|
||||
search_dt = utc_start - timedelta(hours=36)
|
||||
previous_rise: astronomy.Time | None = None
|
||||
while True:
|
||||
limit_days = max(0.01, min(4.0, (utc_start - search_dt).total_seconds() / 86400.0 + 1.0))
|
||||
rise_time = astronomy.SearchRiseSet(body, observer, astronomy.Direction.Rise, dt_to_time(search_dt), limit_days, 2.5)
|
||||
if rise_time is None:
|
||||
break
|
||||
rise_dt = time_to_datetime(rise_time)
|
||||
if rise_dt >= utc_start:
|
||||
break
|
||||
previous_rise = rise_time
|
||||
search_dt = rise_dt + timedelta(minutes=1)
|
||||
previous_rise_hour = hour_from_utc_start(previous_rise, utc_start)
|
||||
if previous_rise_hour is not None:
|
||||
actual_rise_hour = previous_rise_hour
|
||||
|
||||
if segments and segments[-1]["to"] > 23.95:
|
||||
limit_days = 2.0
|
||||
next_set = astronomy.SearchRiseSet(
|
||||
body,
|
||||
observer,
|
||||
astronomy.Direction.Set,
|
||||
dt_to_time(utc_start + timedelta(hours=23)),
|
||||
limit_days,
|
||||
2.0,
|
||||
)
|
||||
next_set_hour = hour_from_utc_start(next_set, utc_start)
|
||||
if next_set_hour is not None and next_set_hour > 23.95:
|
||||
actual_set_hour = next_set_hour
|
||||
|
||||
return segments, actual_rise_hour, actual_set_hour
|
||||
|
||||
|
||||
def compute_best_segments(
|
||||
segments: list[dict],
|
||||
astro_dawn_hour: float | None,
|
||||
astro_dusk_hour: float | None,
|
||||
) -> list[dict]:
|
||||
if astro_dawn_hour is None or astro_dusk_hour is None:
|
||||
return []
|
||||
|
||||
result: list[dict] = []
|
||||
for segment in segments:
|
||||
start_hour = float(segment["from"])
|
||||
end_hour = float(segment["to"])
|
||||
|
||||
if start_hour < astro_dawn_hour:
|
||||
best_end = min(end_hour, astro_dawn_hour)
|
||||
if best_end - start_hour > 0.05:
|
||||
result.append({"from": start_hour, "to": best_end})
|
||||
|
||||
if end_hour > astro_dusk_hour:
|
||||
best_start = max(start_hour, astro_dusk_hour)
|
||||
best_end = min(end_hour, 24.0)
|
||||
if best_end - best_start > 0.05:
|
||||
result.append({"from": best_start, "to": best_end})
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def moon_phase_code(phase_angle: float) -> tuple[str, bool]:
|
||||
waning = phase_angle > 180.0
|
||||
if phase_angle < 22.5 or phase_angle >= 337.5:
|
||||
return "NM", waning
|
||||
if not waning and phase_angle < 67.5:
|
||||
return "ZS", waning
|
||||
if not waning and phase_angle < 112.5:
|
||||
return "ZH", waning
|
||||
if not waning and phase_angle < 157.5:
|
||||
return "ZG", waning
|
||||
if 157.5 <= phase_angle < 202.5:
|
||||
return "VM", waning
|
||||
if waning and phase_angle < 247.5:
|
||||
return "AG", waning
|
||||
if waning and phase_angle < 292.5:
|
||||
return "AH", waning
|
||||
return "AS", waning
|
||||
|
||||
|
||||
def action_planet_visibility_chart(args: list[str]) -> dict:
|
||||
if len(args) != 5:
|
||||
fail(
|
||||
"Aktion planet_visibility_chart erwartet 5 Argumente: latitude longitude elevation date timezone",
|
||||
extra={"argv": args},
|
||||
)
|
||||
|
||||
latitude = parse_float(args[0], "Latitude")
|
||||
longitude = parse_float(args[1], "Longitude")
|
||||
elevation = parse_float(args[2], "Elevation")
|
||||
date_text = args[3]
|
||||
timezone_name = args[4]
|
||||
|
||||
try:
|
||||
local_start = datetime.strptime(date_text, "%Y-%m-%d")
|
||||
except ValueError:
|
||||
fail("Datum muss im Format YYYY-MM-DD uebergeben werden.", extra={"date": date_text})
|
||||
|
||||
try:
|
||||
tz = ZoneInfo(timezone_name)
|
||||
except Exception as exc:
|
||||
fail("Zeitzone ist ungueltig oder auf dem Server nicht verfuegbar.", extra={"details": str(exc)})
|
||||
|
||||
local_start = local_start.replace(tzinfo=tz)
|
||||
local_noon = local_start + timedelta(hours=12)
|
||||
local_end = local_start + timedelta(days=1)
|
||||
utc_start = local_start.astimezone(timezone.utc)
|
||||
utc_end = local_end.astimezone(timezone.utc)
|
||||
observer = astronomy.Observer(latitude, longitude, elevation)
|
||||
start_time = dt_to_time(utc_start)
|
||||
|
||||
def search_altitude(direction: astronomy.Direction, altitude_deg: float) -> astronomy.Time | None:
|
||||
limit_days = (utc_end - utc_start).total_seconds() / 86400.0
|
||||
result = astronomy.SearchAltitude(astronomy.Body.Sun, observer, direction, start_time, limit_days, altitude_deg)
|
||||
if result is None:
|
||||
return None
|
||||
if time_to_datetime(result) >= utc_end:
|
||||
return None
|
||||
return result
|
||||
|
||||
sun_rise = search_event(astronomy.Body.Sun, astronomy.Direction.Rise, observer, start_time, utc_end)
|
||||
sun_set = search_event(astronomy.Body.Sun, astronomy.Direction.Set, observer, start_time, utc_end)
|
||||
civil_dawn = search_altitude(astronomy.Direction.Rise, -6.0)
|
||||
civil_dusk = search_altitude(astronomy.Direction.Set, -6.0)
|
||||
nautical_dawn = search_altitude(astronomy.Direction.Rise, -12.0)
|
||||
nautical_dusk = search_altitude(astronomy.Direction.Set, -12.0)
|
||||
astro_dawn = search_altitude(astronomy.Direction.Rise, -18.0)
|
||||
astro_dusk = search_altitude(astronomy.Direction.Set, -18.0)
|
||||
|
||||
twilight = {
|
||||
"sunRiseH": hour_from_utc_start(sun_rise, utc_start),
|
||||
"sunSetH": hour_from_utc_start(sun_set, utc_start),
|
||||
"civDawnH": hour_from_utc_start(civil_dawn, utc_start),
|
||||
"civDuskH": hour_from_utc_start(civil_dusk, utc_start),
|
||||
"nautDawnH": hour_from_utc_start(nautical_dawn, utc_start),
|
||||
"nautDuskH": hour_from_utc_start(nautical_dusk, utc_start),
|
||||
"astroDawnH": hour_from_utc_start(astro_dawn, utc_start),
|
||||
"astroDuskH": hour_from_utc_start(astro_dusk, utc_start),
|
||||
}
|
||||
|
||||
moon_time = dt_to_time(local_noon.astimezone(timezone.utc))
|
||||
moon_phase = float(astronomy.MoonPhase(moon_time))
|
||||
moon_illumination = astronomy.Illumination(astronomy.Body.Moon, moon_time)
|
||||
moon_label, moon_waning = moon_phase_code(moon_phase)
|
||||
moon_segments, moon_rise_hour, moon_set_hour = build_visibility_segments(
|
||||
astronomy.Body.Moon,
|
||||
observer,
|
||||
utc_start,
|
||||
utc_end,
|
||||
)
|
||||
moon_best_segments = compute_best_segments(
|
||||
moon_segments,
|
||||
twilight["astroDawnH"],
|
||||
twilight["astroDuskH"],
|
||||
)
|
||||
|
||||
rows: list[dict] = []
|
||||
for row_def in PLANET_VISIBILITY_ROWS:
|
||||
midday_time = dt_to_time(local_noon.astimezone(timezone.utc))
|
||||
illumination = astronomy.Illumination(row_def["body"], midday_time)
|
||||
segments, rise_hour, set_hour = build_visibility_segments(
|
||||
row_def["body"],
|
||||
observer,
|
||||
utc_start,
|
||||
utc_end,
|
||||
)
|
||||
rows.append({
|
||||
"key": row_def["key"],
|
||||
"type": row_def["type"],
|
||||
"name": row_def["name"],
|
||||
"symbol": row_def["symbol"],
|
||||
"color": row_def["color"],
|
||||
"glow": row_def["glow"],
|
||||
"segs": segments,
|
||||
"bestSegs": compute_best_segments(
|
||||
segments,
|
||||
twilight["astroDawnH"],
|
||||
twilight["astroDuskH"],
|
||||
),
|
||||
"actualRiseH": rise_hour,
|
||||
"actualSetH": set_hour,
|
||||
"magnitude": float(illumination.mag),
|
||||
})
|
||||
|
||||
rows.append({
|
||||
"key": "moon",
|
||||
"type": "moon",
|
||||
"name": "MOND",
|
||||
"symbol": moon_label,
|
||||
"color": "#d0d8f0",
|
||||
"glow": "rgba(208,216,240,.55)",
|
||||
"segs": moon_segments,
|
||||
"bestSegs": moon_best_segments,
|
||||
"actualRiseH": moon_rise_hour,
|
||||
"actualSetH": moon_set_hour,
|
||||
"illum": float(moon_illumination.phase_fraction),
|
||||
"waning": moon_waning,
|
||||
"phase": moon_phase,
|
||||
"label": moon_label,
|
||||
})
|
||||
|
||||
utc_offset_hours = local_noon.utcoffset().total_seconds() / 3600.0
|
||||
timezone_abbr = local_noon.tzname() or timezone_name
|
||||
|
||||
return {
|
||||
"ok": True,
|
||||
"action": "planet_visibility_chart",
|
||||
"date": date_text,
|
||||
"timezone": timezone_name,
|
||||
"timezone_abbr": timezone_abbr,
|
||||
"utc_offset_hours": utc_offset_hours,
|
||||
"location": {
|
||||
"latitude": latitude,
|
||||
"longitude": longitude,
|
||||
"elevation": elevation,
|
||||
},
|
||||
"twilight": twilight,
|
||||
"rows": rows,
|
||||
"moon": {
|
||||
"illum": float(moon_illumination.phase_fraction),
|
||||
"waning": moon_waning,
|
||||
"phase": moon_phase,
|
||||
"label": moon_label,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def action_planet_rise_set(args: list[str]) -> dict:
|
||||
if len(args) != 5:
|
||||
@@ -4259,6 +4614,11 @@ def main() -> None:
|
||||
print(json.dumps(result, ensure_ascii=True))
|
||||
return
|
||||
|
||||
if action == "planet_visibility_chart":
|
||||
result = action_planet_visibility_chart(args)
|
||||
print(json.dumps(result, ensure_ascii=True))
|
||||
return
|
||||
|
||||
if action == "month_sky_context":
|
||||
result = action_month_sky_context(args)
|
||||
print(json.dumps(result, ensure_ascii=True))
|
||||
@@ -4354,7 +4714,7 @@ def main() -> None:
|
||||
print(json.dumps(result, ensure_ascii=True))
|
||||
return
|
||||
|
||||
fail("Unbekannte Aktion.", extra={"action": action, "available_actions": ["sun_moon_rise_set", "solar_longitude_to_datetime", "current_solar_longitude", "astronomical_conversions", "comet_brightnesses", "moon_star_occultations", "moon_phase_details", "satellite_passes", "planet_rise_set", "month_sky_context", "moon_phases_for_month", "golden_handle_for_month", "season_changes_for_month", "time_changes_for_month", "moon_planet_approaches", "moon_deep_sky_approaches_for_month", "planet_bright_star_approaches_for_month", "planet_conjunctions_for_month", "eclipses_for_month", "moon_apsides_for_month", "sun_apsides_for_month", "inner_planet_elongations_for_month", "venus_peak_magnitude_for_month", "outer_planet_events_for_month", "outer_planet_stations_for_month", "jupiter_moons_one_side_for_month", "mercury_good_visibility_for_month", "planet_parades_for_month", "planet_constellation_changes_for_month", "moon_star_occultations_for_month"]})
|
||||
fail("Unbekannte Aktion.", extra={"action": action, "available_actions": ["sun_moon_rise_set", "solar_longitude_to_datetime", "current_solar_longitude", "astronomical_conversions", "comet_brightnesses", "moon_star_occultations", "moon_phase_details", "satellite_passes", "planet_rise_set", "planet_visibility_chart", "month_sky_context", "moon_phases_for_month", "golden_handle_for_month", "season_changes_for_month", "time_changes_for_month", "moon_planet_approaches", "moon_deep_sky_approaches_for_month", "planet_bright_star_approaches_for_month", "planet_conjunctions_for_month", "eclipses_for_month", "moon_apsides_for_month", "sun_apsides_for_month", "inner_planet_elongations_for_month", "venus_peak_magnitude_for_month", "outer_planet_events_for_month", "outer_planet_stations_for_month", "jupiter_moons_one_side_for_month", "mercury_good_visibility_for_month", "planet_parades_for_month", "planet_constellation_changes_for_month", "moon_star_occultations_for_month"]})
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
function runPythonApi(string $action, array $args): array
|
||||
{
|
||||
$scriptPath = __DIR__ . '/py/api.py';
|
||||
$pythonCandidates = PHP_OS_FAMILY === 'Windows'
|
||||
? ['python', 'py']
|
||||
: ['/usr/bin/python3', '/usr/bin/python', 'python3', 'python'];
|
||||
|
||||
$escapedArguments = array_map('escapeshellarg', array_merge([$scriptPath, $action], $args));
|
||||
$lastError = 'Python konnte nicht gestartet werden.';
|
||||
|
||||
foreach ($pythonCandidates as $pythonBinary) {
|
||||
$command = $pythonBinary . ' ' . implode(' ', $escapedArguments) . ' 2>&1';
|
||||
$output = [];
|
||||
$resultCode = 0;
|
||||
exec($command, $output, $resultCode);
|
||||
|
||||
$joined = trim(implode("\n", $output));
|
||||
$decoded = json_decode($joined, true);
|
||||
|
||||
if (is_array($decoded)) {
|
||||
return [
|
||||
'ok' => (bool) ($decoded['ok'] ?? false),
|
||||
'data' => $decoded,
|
||||
'raw' => $joined,
|
||||
'command' => $command,
|
||||
];
|
||||
}
|
||||
|
||||
if ($joined !== '') {
|
||||
$lastError = $joined;
|
||||
} elseif ($resultCode !== 0) {
|
||||
$lastError = 'Fehlercode ' . $resultCode . ' bei ' . $pythonBinary;
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'ok' => false,
|
||||
'error' => $lastError,
|
||||
];
|
||||
}
|
||||
|
||||
function runPythonApiWithJsonPayloadFile(string $action, array $payload): array
|
||||
{
|
||||
$tempFile = tempnam(sys_get_temp_dir(), 'skyview_py_');
|
||||
if ($tempFile === false) {
|
||||
return [
|
||||
'ok' => false,
|
||||
'error' => 'Temporäre Datei für Python-Payload konnte nicht erstellt werden.',
|
||||
];
|
||||
}
|
||||
|
||||
$jsonPayload = json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
if ($jsonPayload === false) {
|
||||
@unlink($tempFile);
|
||||
return [
|
||||
'ok' => false,
|
||||
'error' => 'Python-Payload konnte nicht als JSON kodiert werden.',
|
||||
];
|
||||
}
|
||||
|
||||
if (file_put_contents($tempFile, $jsonPayload) === false) {
|
||||
@unlink($tempFile);
|
||||
return [
|
||||
'ok' => false,
|
||||
'error' => 'Temporäre Datei für Python-Payload konnte nicht geschrieben werden.',
|
||||
];
|
||||
}
|
||||
|
||||
try {
|
||||
return runPythonApi($action, ['@' . $tempFile]);
|
||||
} finally {
|
||||
@unlink($tempFile);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user