V 1.7.0 feat: Sonnenauf-/untergang und Mondphase berechnet
Die Header-Zeile zeigte bisher feste Platzhalter ("06:45", "18:30",
"abnehmend 50%"). Jetzt werden die Werte tatsaechlich gerechnet, ohne
neue Abhaengigkeit:
- utils/astro.js: getSunTimes() nach dem NOAA-Sonnenstandsalgorithmus
inkl. Hoehenkorrektur, getMoonPhase() nach gekuerzten Meeus-Reihen.
- Standort steht als Konstante STATION in App.jsx und speist sowohl
Anzeige als auch Berechnung; Koordinaten auf die echten Stationswerte
korrigiert (48° 52′ 27″ N, 9° 35′ 46″ E, 547 m NN).
Geprueft gegen unabhaengige Referenzimplementierungen: Sonnenzeiten
<= 50 s Abweichung gegen den vollstaendigen NOAA-Algorithmus, Neu- und
Vollmondzeitpunkte <= 39 min gegen Meeus Kap. 49.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "wetterstation-frontend",
|
"name": "wetterstation-frontend",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.6.1",
|
"version": "1.7.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
+31
-6
@@ -1,7 +1,28 @@
|
|||||||
import { useState, useEffect, useRef } from 'react'
|
import { useState, useEffect, useRef } from 'react'
|
||||||
import WeatherDashboard from './components/WeatherDashboard'
|
import WeatherDashboard from './components/WeatherDashboard'
|
||||||
|
import { getSunTimes, getMoonPhase, formatTime, formatMoonPhase } from './utils/astro'
|
||||||
import './App.css'
|
import './App.css'
|
||||||
|
|
||||||
|
// Grad/Minuten/Sekunden in Dezimalgrad
|
||||||
|
const dms = (deg, min, sec) => deg + min / 60 + sec / 3600
|
||||||
|
|
||||||
|
// Dezimalgrad zurueck nach Grad/Minuten/Sekunden fuer die Anzeige
|
||||||
|
function formatDms(value) {
|
||||||
|
let deg = Math.floor(value)
|
||||||
|
let min = Math.floor((value - deg) * 60)
|
||||||
|
let sec = Math.round(((value - deg) * 60 - min) * 60)
|
||||||
|
if (sec === 60) { sec = 0; min += 1 } // Rundungsuebertrag
|
||||||
|
if (min === 60) { min = 0; deg += 1 }
|
||||||
|
return `${deg}° ${min}′ ${sec}″`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Standort der Wetterstation — Basis fuer Header-Anzeige und Astro-Berechnung
|
||||||
|
const STATION = {
|
||||||
|
lat: dms(48, 52, 27), // noerdliche Breite
|
||||||
|
lon: dms(9, 35, 46), // oestliche Laenge
|
||||||
|
elevation: 547, // Meter ueber NN
|
||||||
|
}
|
||||||
|
|
||||||
// API-Basis-URL: in Dev direkt auf Backend, in Prod ueber Nginx-Proxy
|
// API-Basis-URL: in Dev direkt auf Backend, in Prod ueber Nginx-Proxy
|
||||||
const API_BASE = import.meta.env.DEV ? 'http://localhost:8000' : '/api'
|
const API_BASE = import.meta.env.DEV ? 'http://localhost:8000' : '/api'
|
||||||
|
|
||||||
@@ -193,11 +214,13 @@ function App() {
|
|||||||
minute: '2-digit'
|
minute: '2-digit'
|
||||||
})
|
})
|
||||||
|
|
||||||
// TODO: Sonnenauf-/untergang und Mondphase berechnen
|
// Sonnenauf-/untergang fuer heute am Stationsstandort
|
||||||
// Aktuell Platzhalter - benoetigt Bibliothek wie 'suncalc'
|
const sunTimes = getSunTimes(now, STATION.lat, STATION.lon, STATION.elevation)
|
||||||
const sunrise = "06:45"
|
const sunrise = formatTime(sunTimes.sunrise)
|
||||||
const sunset = "18:30"
|
const sunset = formatTime(sunTimes.sunset)
|
||||||
const moonPhase = "abnehmend 50%"
|
|
||||||
|
// Mondphase zum aktuellen Zeitpunkt
|
||||||
|
const moonPhase = formatMoonPhase(getMoonPhase(now))
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="app">
|
<div className="app">
|
||||||
@@ -205,7 +228,9 @@ function App() {
|
|||||||
<h1>Aktuelle Wetterdaten</h1>
|
<h1>Aktuelle Wetterdaten</h1>
|
||||||
<div className="header-datetime">{dateStr} - {timeStr} Uhr</div>
|
<div className="header-datetime">{dateStr} - {timeStr} Uhr</div>
|
||||||
<div className="header-spacer"></div>
|
<div className="header-spacer"></div>
|
||||||
<div className="header-coordinates">48.6 N - 9.6 E - 574 m NN</div>
|
<div className="header-coordinates">
|
||||||
|
{formatDms(STATION.lat)} N - {formatDms(STATION.lon)} E - {STATION.elevation} m NN
|
||||||
|
</div>
|
||||||
<div className="header-astro">
|
<div className="header-astro">
|
||||||
Sonnen-Aufgang: {sunrise} - Untergang: {sunset} Mond-Phase: {moonPhase}
|
Sonnen-Aufgang: {sunrise} - Untergang: {sunset} Mond-Phase: {moonPhase}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,213 @@
|
|||||||
|
// Astronomische Berechnungen fuer die Header-Anzeige: Sonnenauf-/untergang
|
||||||
|
// nach dem NOAA-Sonnenstandsalgorithmus (vereinfachte "Sunrise Equation") und
|
||||||
|
// die Mondphase nach gekuerzten Reihen aus Meeus, "Astronomical Algorithms".
|
||||||
|
// Genauigkeit reicht fuer die Anzeige und spart eine externe Bibliothek.
|
||||||
|
|
||||||
|
const DEG = Math.PI / 180
|
||||||
|
const J1970 = 2440588 // Julianischer Tag von 1970-01-01
|
||||||
|
const J2000 = 2451545 // Julianischer Tag von 2000-01-01 12:00 UT
|
||||||
|
const DAY_MS = 86400000
|
||||||
|
|
||||||
|
// Schiefe der Ekliptik
|
||||||
|
const OBLIQUITY = 23.4397
|
||||||
|
|
||||||
|
// Standard-Hoehe der Sonnenmitte bei Auf-/Untergang: -0.833 Grad
|
||||||
|
// (Refraktion + halber Sonnendurchmesser)
|
||||||
|
const ZENITH = -0.833
|
||||||
|
|
||||||
|
const toJulian = date => date.getTime() / DAY_MS - 0.5 + J1970
|
||||||
|
const fromJulian = j => new Date((j + 0.5 - J1970) * DAY_MS)
|
||||||
|
|
||||||
|
// Mittlere Entfernung Erde-Sonne in km (fuer den Phasenwinkel des Mondes)
|
||||||
|
const SUN_DISTANCE = 149598000
|
||||||
|
|
||||||
|
const sin = x => Math.sin(x * DEG)
|
||||||
|
const cos = x => Math.cos(x * DEG)
|
||||||
|
const tan = x => Math.tan(x * DEG)
|
||||||
|
|
||||||
|
// Mittlere Anomalie der Sonne, d Tage seit J2000
|
||||||
|
const sunMeanAnomaly = d => 357.5291 + 0.98560028 * d
|
||||||
|
|
||||||
|
// Ekliptikale Laenge der Sonne in Grad. Die mittlere Laenge hat eine leicht
|
||||||
|
// groessere Rate als die mittlere Anomalie — die Differenz ist die Praezession
|
||||||
|
// des Fruehlingspunkts. Wer sie weglaesst, liegt pro Jahr rund 1.7 Bogenminuten
|
||||||
|
// daneben; das verschiebt vor allem den berechneten Mondphasen-Zeitpunkt.
|
||||||
|
function sunLongitude(d) {
|
||||||
|
const M = sunMeanAnomaly(d)
|
||||||
|
const L0 = 280.4665 + 0.98564736 * d // mittlere Laenge
|
||||||
|
const C = 1.9148 * sin(M) + 0.02 * sin(2 * M) + 0.0003 * sin(3 * M)
|
||||||
|
return L0 + C // Mittelpunktsgleichung
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hoehenkorrektur: aus grosser Hoehe sieht man die Sonne frueher aufgehen.
|
||||||
|
// Naeherung: 2.076 * sqrt(Hoehe in m) Bogenminuten.
|
||||||
|
const elevationCorrection = elevation =>
|
||||||
|
elevation > 0 ? (-2.076 * Math.sqrt(elevation)) / 60 : 0
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Berechnet Sonnenauf- und -untergang fuer einen Tag und Standort.
|
||||||
|
*
|
||||||
|
* @param {Date} date Tag, fuer den gerechnet wird (lokale Zeit)
|
||||||
|
* @param {number} lat Breitengrad in Grad, Nord positiv
|
||||||
|
* @param {number} lon Laengengrad in Grad, Ost positiv
|
||||||
|
* @param {number} elevation Hoehe ueber NN in Metern
|
||||||
|
* @returns {{sunrise: Date|null, sunset: Date|null}}
|
||||||
|
* null bei Polartag/Polarnacht (Sonne geht nicht auf bzw. unter)
|
||||||
|
*/
|
||||||
|
export function getSunTimes(date, lat, lon, elevation = 0) {
|
||||||
|
// Bezugspunkt ist der lokale Kalendertag von `date`, nicht der UTC-Zeitpunkt.
|
||||||
|
// Sonst faellt z.B. 00:30 Sommerzeit (= 22:30 UTC des Vortags) noch auf den
|
||||||
|
// Vortag und wir zeigen nachts kurz die Zeiten von gestern an.
|
||||||
|
const noon = new Date(date.getFullYear(), date.getMonth(), date.getDate(), 12)
|
||||||
|
|
||||||
|
// Tagesnummer seit J2000 und der zugehoerige mittlere Sonnenmittag am
|
||||||
|
// Standort (oestliche Laenge => Mittag frueher in Weltzeit).
|
||||||
|
const n = Math.round(toJulian(noon) - J2000 + lon / 360)
|
||||||
|
const jStar = n - lon / 360
|
||||||
|
|
||||||
|
const M = sunMeanAnomaly(jStar)
|
||||||
|
const lambda = sunLongitude(jStar)
|
||||||
|
|
||||||
|
// Sonnenhoechststand (Transit)
|
||||||
|
const jTransit = J2000 + jStar + 0.0053 * sin(M) - 0.0069 * sin(2 * lambda)
|
||||||
|
|
||||||
|
// Deklination der Sonne
|
||||||
|
const sinDec = sin(lambda) * sin(OBLIQUITY)
|
||||||
|
const cosDec = Math.sqrt(1 - sinDec * sinDec)
|
||||||
|
|
||||||
|
// Stundenwinkel bei Auf-/Untergang
|
||||||
|
const h = ZENITH + elevationCorrection(elevation)
|
||||||
|
const cosOmega = (sin(h) - sin(lat) * sinDec) / (cos(lat) * cosDec)
|
||||||
|
|
||||||
|
// |cos| > 1 => Sonne bleibt den ganzen Tag ueber bzw. unter dem Horizont
|
||||||
|
if (cosOmega > 1) return { sunrise: null, sunset: null } // Polarnacht
|
||||||
|
if (cosOmega < -1) return { sunrise: null, sunset: null } // Polartag
|
||||||
|
|
||||||
|
const omega = Math.acos(cosOmega) / DEG
|
||||||
|
|
||||||
|
return {
|
||||||
|
sunrise: fromJulian(jTransit - omega / 360),
|
||||||
|
sunset: fromJulian(jTransit + omega / 360),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Mondphase ---------------------------------------------------------
|
||||||
|
|
||||||
|
// Umrechnung ekliptikaler in aequatoriale Koordinaten (alles in Grad)
|
||||||
|
const rightAscension = (l, b) =>
|
||||||
|
Math.atan2(sin(l) * cos(OBLIQUITY) - tan(b) * sin(OBLIQUITY), cos(l)) / DEG
|
||||||
|
const declination = (l, b) =>
|
||||||
|
Math.asin(sin(b) * cos(OBLIQUITY) + cos(b) * sin(OBLIQUITY) * sin(l)) / DEG
|
||||||
|
|
||||||
|
// Position der Sonne (aequatorial) fuer d Tage seit J2000
|
||||||
|
function sunPosition(d) {
|
||||||
|
const l = sunLongitude(d)
|
||||||
|
return { ra: rightAscension(l, 0), dec: declination(l, 0) }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Position des Mondes (aequatorial) und Entfernung in km, d Tage seit J2000.
|
||||||
|
// Gekuerzte Reihe nach Meeus: mittlere Elemente plus die groessten
|
||||||
|
// periodischen Stoerungen (Evektion, Variation, jaehrliche Gleichung).
|
||||||
|
function moonPosition(d) {
|
||||||
|
const L = 218.3165 + 13.17639648 * d // mittlere Laenge
|
||||||
|
const D = 297.8502 + 12.19074912 * d // mittlere Elongation
|
||||||
|
const M = 357.5291 + 0.98560028 * d // mittlere Anomalie der Sonne
|
||||||
|
const Mp = 134.9634 + 13.06499295 * d // mittlere Anomalie des Mondes
|
||||||
|
const F = 93.2721 + 13.22935024 * d // Argument der Breite
|
||||||
|
|
||||||
|
const l =
|
||||||
|
L +
|
||||||
|
6.289 * sin(Mp) +
|
||||||
|
1.274 * sin(2 * D - Mp) +
|
||||||
|
0.658 * sin(2 * D) +
|
||||||
|
0.214 * sin(2 * Mp) -
|
||||||
|
0.186 * sin(M) -
|
||||||
|
0.114 * sin(2 * F)
|
||||||
|
|
||||||
|
const b =
|
||||||
|
5.128 * sin(F) +
|
||||||
|
0.281 * sin(Mp + F) -
|
||||||
|
0.278 * sin(F - Mp) -
|
||||||
|
0.173 * sin(2 * D - F)
|
||||||
|
|
||||||
|
const dist =
|
||||||
|
385001 -
|
||||||
|
20905 * cos(Mp) -
|
||||||
|
3699 * cos(2 * D - Mp) -
|
||||||
|
2956 * cos(2 * D) -
|
||||||
|
570 * cos(2 * Mp)
|
||||||
|
|
||||||
|
return { ra: rightAscension(l, b), dec: declination(l, b), dist }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Berechnet die Mondphase zu einem Zeitpunkt.
|
||||||
|
*
|
||||||
|
* @param {Date} date Zeitpunkt der Berechnung
|
||||||
|
* @returns {{fraction: number, phase: number, waxing: boolean}}
|
||||||
|
* fraction: beleuchteter Anteil der Mondscheibe, 0 = neu, 1 = voll
|
||||||
|
* phase: Position im Zyklus, 0 = Neumond, 0.25 = erstes Viertel,
|
||||||
|
* 0.5 = Vollmond, 0.75 = letztes Viertel
|
||||||
|
* waxing: true, solange der Mond zunimmt
|
||||||
|
*/
|
||||||
|
export function getMoonPhase(date) {
|
||||||
|
const d = toJulian(date) - J2000
|
||||||
|
const s = sunPosition(d)
|
||||||
|
const m = moonPosition(d)
|
||||||
|
|
||||||
|
// Elongation Sonne-Mond von der Erde aus gesehen
|
||||||
|
const elongation =
|
||||||
|
Math.acos(
|
||||||
|
sin(s.dec) * sin(m.dec) + cos(s.dec) * cos(m.dec) * cos(s.ra - m.ra)
|
||||||
|
) / DEG
|
||||||
|
|
||||||
|
// Phasenwinkel: Winkel Sonne-Mond-Erde
|
||||||
|
const inc =
|
||||||
|
Math.atan2(
|
||||||
|
SUN_DISTANCE * sin(elongation),
|
||||||
|
m.dist - SUN_DISTANCE * cos(elongation)
|
||||||
|
) / DEG
|
||||||
|
|
||||||
|
// Vorzeichen entscheidet, ob zu- oder abnehmend
|
||||||
|
const angle =
|
||||||
|
Math.atan2(
|
||||||
|
cos(s.dec) * sin(s.ra - m.ra),
|
||||||
|
sin(s.dec) * cos(m.dec) - cos(s.dec) * sin(m.dec) * cos(s.ra - m.ra)
|
||||||
|
) / DEG
|
||||||
|
|
||||||
|
const waxing = angle < 0
|
||||||
|
return {
|
||||||
|
fraction: (1 + cos(inc)) / 2,
|
||||||
|
phase: 0.5 + (0.5 * inc * (waxing ? -1 : 1)) / 180,
|
||||||
|
waxing,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Abstand im Mondzyklus, ab dem eine Hauptphase namentlich genannt wird.
|
||||||
|
// 0.02 Zyklen entsprechen gut einem halben Tag.
|
||||||
|
const PHASE_TOLERANCE = 0.02
|
||||||
|
|
||||||
|
// Formatiert die Mondphase als deutschen Text, z.B. "Vollmond" oder
|
||||||
|
// "abnehmend 63%". Die Prozentangabe ist der beleuchtete Anteil der Scheibe.
|
||||||
|
export function formatMoonPhase({ fraction, phase, waxing }) {
|
||||||
|
// Abstand zyklisch messen, sonst faellt die Haelfte des Neumond-Fensters
|
||||||
|
// (phase kurz vor 1) durch das Raster.
|
||||||
|
const near = target => {
|
||||||
|
const diff = Math.abs(phase - target) % 1
|
||||||
|
return Math.min(diff, 1 - diff) < PHASE_TOLERANCE
|
||||||
|
}
|
||||||
|
|
||||||
|
if (near(0)) return 'Neumond'
|
||||||
|
if (near(0.25)) return 'erstes Viertel'
|
||||||
|
if (near(0.5)) return 'Vollmond'
|
||||||
|
if (near(0.75)) return 'letztes Viertel'
|
||||||
|
|
||||||
|
const percent = Math.round(fraction * 100)
|
||||||
|
return `${waxing ? 'zunehmend' : 'abnehmend'} ${percent}%`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Formatiert eine Zeit als "HH:MM" in lokaler Zeit; "--:--" wenn nicht vorhanden.
|
||||||
|
export function formatTime(date) {
|
||||||
|
if (!date) return '--:--'
|
||||||
|
return date.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' })
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user