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",
|
||||
"private": true,
|
||||
"version": "1.6.1",
|
||||
"version": "1.7.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
+31
-6
@@ -1,7 +1,28 @@
|
||||
import { useState, useEffect, useRef } from 'react'
|
||||
import WeatherDashboard from './components/WeatherDashboard'
|
||||
import { getSunTimes, getMoonPhase, formatTime, formatMoonPhase } from './utils/astro'
|
||||
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
|
||||
const API_BASE = import.meta.env.DEV ? 'http://localhost:8000' : '/api'
|
||||
|
||||
@@ -193,11 +214,13 @@ function App() {
|
||||
minute: '2-digit'
|
||||
})
|
||||
|
||||
// TODO: Sonnenauf-/untergang und Mondphase berechnen
|
||||
// Aktuell Platzhalter - benoetigt Bibliothek wie 'suncalc'
|
||||
const sunrise = "06:45"
|
||||
const sunset = "18:30"
|
||||
const moonPhase = "abnehmend 50%"
|
||||
// Sonnenauf-/untergang fuer heute am Stationsstandort
|
||||
const sunTimes = getSunTimes(now, STATION.lat, STATION.lon, STATION.elevation)
|
||||
const sunrise = formatTime(sunTimes.sunrise)
|
||||
const sunset = formatTime(sunTimes.sunset)
|
||||
|
||||
// Mondphase zum aktuellen Zeitpunkt
|
||||
const moonPhase = formatMoonPhase(getMoonPhase(now))
|
||||
|
||||
return (
|
||||
<div className="app">
|
||||
@@ -205,7 +228,9 @@ function App() {
|
||||
<h1>Aktuelle Wetterdaten</h1>
|
||||
<div className="header-datetime">{dateStr} - {timeStr} Uhr</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">
|
||||
Sonnen-Aufgang: {sunrise} - Untergang: {sunset} Mond-Phase: {moonPhase}
|
||||
</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