updated cloud detection and mixed in celestial weather

This commit is contained in:
2026-06-24 00:36:00 -04:00
parent 2985a62d7d
commit 6f24e74b72
3 changed files with 289 additions and 286 deletions

View File

@@ -1,228 +1,182 @@
const RAD = Math.PI / 180
const DEG = 180 / Math.PI
const RAD = Math.PI / 180;
const DEG = 180 / Math.PI;
function jdn(date) {
return (date instanceof Date ? date : new Date(date)) / 86400000 + 2440587.5
return (date instanceof Date ? date : new Date(date)) / 86400000 + 2440587.5;
}
function jdnToDate(jd) {
return new Date((jd - 2440587.5) * 86400000)
return new Date((jd - 2440587.5) * 86400000);
}
function sunPosition(lat, lon, date = new Date()) {
const d = jdn(date) - 2451545.0
const L = (280.46 + 0.9856474 * d) % 360
const g = (357.528 + 0.9856003 * d) % 360
const lam = L + 1.915 * Math.sin(g * RAD) + 0.02 * Math.sin(2 * g * RAD)
const eps = 23.439 - 0.0000004 * d
const sinL = Math.sin(lam * RAD)
const ra = Math.atan2(Math.cos(eps * RAD) * sinL, Math.cos(lam * RAD)) * DEG
const dec = Math.asin(Math.sin(eps * RAD) * sinL) * DEG
const UT = date.getUTCHours() + date.getUTCMinutes() / 60 + date.getUTCSeconds() / 3600
const GMST = (6.697375 + 0.0657098242 * d + UT) % 24
const LMST = (GMST + lon / 15) % 24
const ha = LMST * 15 - ra
const elev = Math.asin(
Math.sin(lat * RAD) * Math.sin(dec * RAD) +
Math.cos(lat * RAD) * Math.cos(dec * RAD) * Math.cos(ha * RAD)
) * DEG
const az = Math.atan2(
-Math.sin(ha * RAD),
Math.tan(dec * RAD) * Math.cos(lat * RAD) - Math.sin(lat * RAD) * Math.cos(ha * RAD)
) * DEG
return {
sun_elevation: Math.round(elev * 10) / 10,
sun_azimuth: Math.round(((az + 360) % 360) * 10) / 10,
}
const d = jdn(date) - 2451545.0;
const L = (280.46 + 0.9856474 * d) % 360;
const g = (357.528 + 0.9856003 * d) % 360;
const lam = L + 1.915 * Math.sin(g * RAD) + 0.02 * Math.sin(2 * g * RAD);
const eps = 23.439 - 0.0000004 * d;
const sinL = Math.sin(lam * RAD);
const ra = Math.atan2(Math.cos(eps * RAD) * sinL, Math.cos(lam * RAD)) * DEG;
const dec = Math.asin(Math.sin(eps * RAD) * sinL) * DEG;
const UT = date.getUTCHours() + date.getUTCMinutes() / 60 + date.getUTCSeconds() / 3600;
const GMST = (6.697375 + 0.0657098242 * d + UT) % 24;
const LMST = (GMST + lon / 15) % 24;
const ha = LMST * 15 - ra;
const elev = Math.asin(
Math.sin(lat * RAD) * Math.sin(dec * RAD) +
Math.cos(lat * RAD) * Math.cos(dec * RAD) * Math.cos(ha * RAD)
) * DEG;
const az = Math.atan2(
-Math.sin(ha * RAD),
Math.tan(dec * RAD) * Math.cos(lat * RAD) - Math.sin(lat * RAD) * Math.cos(ha * RAD)
) * DEG;
return {
sun_elevation: Math.round(elev * 10) / 10,
sun_azimuth: Math.round(((az + 360) % 360) * 10) / 10,
};
}
function sunriseSunset(lat, lon, date = new Date()) {
const d = Math.floor(jdn(date)) - 2451545
const noon = 2451545 + 0.0009 + ((-lon) / 360) + Math.round(d - (-lon) / 360)
const M = (357.5291 + 0.98560028 * (noon - 2451545)) % 360
const C = 1.9148 * Math.sin(M * RAD) + 0.02 * Math.sin(2 * M * RAD) + 0.0003 * Math.sin(3 * M * RAD)
const lam = (M + C + 180 + 102.9372) % 360
const jnoon = noon + 0.0053 * Math.sin(M * RAD) - 0.0069 * Math.sin(2 * lam * RAD)
const dec = Math.asin(Math.sin(23.4397 * RAD) * Math.sin(lam * RAD)) * DEG
const cosH = (Math.sin(-0.8333 * RAD) - Math.sin(lat * RAD) * Math.sin(dec * RAD)) / (Math.cos(lat * RAD) * Math.cos(dec * RAD))
const d = Math.floor(jdn(date)) - 2451545;
const noon = 2451545 + 0.0009 + ((-lon) / 360) + Math.round(d - (-lon) / 360);
const M = (357.5291 + 0.98560028 * (noon - 2451545)) % 360;
const C = 1.9148 * Math.sin(M * RAD) + 0.02 * Math.sin(2 * M * RAD) + 0.0003 * Math.sin(3 * M * RAD);
const lam = (M + C + 180 + 102.9372) % 360;
const jnoon = noon + 0.0053 * Math.sin(M * RAD) - 0.0069 * Math.sin(2 * lam * RAD);
const dec = Math.asin(Math.sin(23.4397 * RAD) * Math.sin(lam * RAD)) * DEG;
const cosH = (Math.sin(-0.8333 * RAD) - Math.sin(lat * RAD) * Math.sin(dec * RAD)) / (Math.cos(lat * RAD) * Math.cos(dec * RAD));
if (Math.abs(cosH) > 1) {
return { sun_sunrise: null, sun_sunset: null, sun_solar_noon: jdnToDate(jnoon).toISOString(), sun_polar: cosH < -1 ? 'day' : 'night' }
}
if(Math.abs(cosH) > 1) {
return {sunrise: null, sunset: null, daytime: cosH < -1};
}
const H = Math.acos(cosH) * DEG
const rise = jdnToDate(jnoon - H / 360)
const set = jdnToDate(jnoon + H / 360)
const noon_d = jdnToDate(jnoon)
const H = Math.acos(cosH) * DEG;
const rise = jdnToDate(jnoon - H / 360);
const set = jdnToDate(jnoon + H / 360);
const noon_d = jdnToDate(jnoon);
// Round to nearest second, drop milliseconds
const fmt = d => new Date(Math.round(d.getTime() / 1000) * 1000).toISOString()
// Round to nearest second, drop milliseconds
const fmt = d => new Date(Math.round(d.getTime() / 1000) * 1000).toISOString();
return {
sun_sunrise: fmt(rise),
sun_sunset: fmt(set),
sun_solar_noon: fmt(noon_d),
sun_day_length_hours: Math.round((set - rise) / 36000) / 100,
sun_golden_hour_morning_start: fmt(new Date(rise - 30 * 60000)),
sun_golden_hour_morning_end: fmt(new Date(rise + 30 * 60000)),
sun_golden_hour_evening_start: fmt(new Date(set - 30 * 60000)),
sun_golden_hour_evening_end: fmt(new Date(set + 30 * 60000)),
}
return {
sunrise: fmt(rise),
sunset: fmt(set),
daylight: Math.round((set - rise) / 36000) / 100,
daytime: cosH < -1
};
}
// Smooth sunspot number approximation from solar flux F10.7
// SSN ≈ 1.61 * F10.7 - 63.7 (linear regression, valid for F10.7 > 70)
export function ssnFromFlux(f107) {
if (!f107) return null
return Math.max(0, Math.round(1.61 * f107 - 63.7))
if(!f107) return null;
return Math.max(0, Math.round(1.61 * f107 - 63.7));
}
export function sunActivityLabel(f107) {
if (!f107) return null
if (f107 < 80) return 'Very Low'
if (f107 < 100) return 'Low'
if (f107 < 150) return 'Moderate'
if (f107 < 200) return 'High'
return 'Very High'
if(!f107) return null;
if(f107 < 80) return 'Very Low';
if(f107 < 100) return 'Low';
if(f107 < 150) return 'Moderate';
if(f107 < 200) return 'High';
return 'Very High';
}
function moonPhase(date = new Date()) {
const jd = jdn(date)
const cycle = 29.53058867
const known = 2451550.1
const phase = ((jd - known) % cycle + cycle) % cycle
const illum = Math.round((1 - Math.cos(phase / cycle * 2 * Math.PI)) / 2 * 100)
let name
if (phase < 1.85) name = 'New Moon'
else if (phase < 7.38) name = 'Waxing Crescent'
else if (phase < 9.22) name = 'First Quarter'
else if (phase < 14.76) name = 'Waxing Gibbous'
else if (phase < 16.61) name = 'Full Moon'
else if (phase < 22.15) name = 'Waning Gibbous'
else if (phase < 23.99) name = 'Last Quarter'
else name = 'Waning Crescent'
return { moon_phase: name, moon_illumination_pct: illum }
const jd = jdn(date);
const cycle = 29.53058867;
const known = 2451550.1;
const phase = ((jd - known) % cycle + cycle) % cycle;
const illum = Math.round((1 - Math.cos(phase / cycle * 2 * Math.PI)) / 2 * 100);
let name;
if(phase < 1.85) name = 'New Moon';
else if(phase < 7.38) name = 'Waxing Crescent';
else if(phase < 9.22) name = 'First Quarter';
else if(phase < 14.76) name = 'Waxing Gibbous';
else if(phase < 16.61) name = 'Full Moon';
else if(phase < 22.15) name = 'Waning Gibbous';
else if(phase < 23.99) name = 'Last Quarter';
else name = 'Waning Crescent';
return {moon_phase: name, moon_illumination: illum};
}
function nextMoonEvents(date = new Date()) {
const cycle = 29.53058867
const jd = jdn(date)
const known = 2451550.1
const phase = ((jd - known) % cycle + cycle) % cycle
const toNew = (cycle - phase) % cycle
const toFull = phase < 14.76 ? 14.76 - phase : cycle - phase + 14.76
return {
moon_next_new: jdnToDate(jd + toNew).toISOString(),
moon_next_full: jdnToDate(jd + toFull).toISOString(),
}
const cycle = 29.53058867;
const jd = jdn(date);
const known = 2451550.1;
const phase = ((jd - known) % cycle + cycle) % cycle;
const toNew = (cycle - phase) % cycle;
const toFull = phase < 14.76 ? 14.76 - phase : cycle - phase + 14.76;
return {
moon_new: jdnToDate(jd + toNew).toISOString(),
moon_full: jdnToDate(jd + toFull).toISOString(),
};
}
function moonriseMoonset(lat, lon, date = new Date()) {
const base = new Date(date)
base.setUTCHours(0, 0, 0, 0)
let prev = null
let moonrise = null
let moonset = null
const base = new Date(date);
base.setUTCHours(0, 0, 0, 0);
let prev = null;
let moonrise = null;
let moonset = null;
// Scan 48h in 10min steps — covers edge cases where moon rises/sets next UTC day
for (let m = 0; m <= 2880; m += 10) {
const t = new Date(base.getTime() + m * 60000)
const d = jdn(t) - 2451545
const L = (218.316 + 13.176396 * d) % 360
const M = (134.963 + 13.064993 * d) % 360
const F = (93.272 + 13.229350 * d) % 360
const lon_ = L + 6.289 * Math.sin(M * RAD)
const b = 5.128 * Math.sin(F * RAD)
const dec = Math.asin(
Math.sin(b * RAD) * Math.cos(23.4397 * RAD) +
Math.cos(b * RAD) * Math.sin(23.4397 * RAD) * Math.sin(lon_ * RAD)
) * DEG
const GMST = (6.697375 + 0.0657098242 * d + (t.getUTCHours() + t.getUTCMinutes() / 60)) % 24
const LMST = (GMST + lon / 15) % 24
const ra = Math.atan2(
Math.sin(lon_ * RAD) * Math.cos(23.4397 * RAD) - Math.tan(b * RAD) * Math.sin(23.4397 * RAD),
Math.cos(lon_ * RAD)
) * DEG
const ha = LMST * 15 - ra
const elev = Math.asin(
Math.sin(lat * RAD) * Math.sin(dec * RAD) +
Math.cos(lat * RAD) * Math.cos(dec * RAD) * Math.cos(ha * RAD)
) * DEG
// Scan 48h in 10min steps — covers edge cases where moon rises/sets next UTC day
for(let m = 0; m <= 2880; m += 10) {
const t = new Date(base.getTime() + m * 60000);
const d = jdn(t) - 2451545;
const L = (218.316 + 13.176396 * d) % 360;
const M = (134.963 + 13.064993 * d) % 360;
const F = (93.272 + 13.229350 * d) % 360;
const lon_ = L + 6.289 * Math.sin(M * RAD);
const b = 5.128 * Math.sin(F * RAD);
const dec = Math.asin(
Math.sin(b * RAD) * Math.cos(23.4397 * RAD) +
Math.cos(b * RAD) * Math.sin(23.4397 * RAD) * Math.sin(lon_ * RAD)
) * DEG;
const GMST = (6.697375 + 0.0657098242 * d + (t.getUTCHours() + t.getUTCMinutes() / 60)) % 24;
const LMST = (GMST + lon / 15) % 24;
const ra = Math.atan2(
Math.sin(lon_ * RAD) * Math.cos(23.4397 * RAD) - Math.tan(b * RAD) * Math.sin(23.4397 * RAD),
Math.cos(lon_ * RAD)
) * DEG;
const ha = LMST * 15 - ra;
const elev = Math.asin(
Math.sin(lat * RAD) * Math.sin(dec * RAD) +
Math.cos(lat * RAD) * Math.cos(dec * RAD) * Math.cos(ha * RAD)
) * DEG;
if (prev !== null) {
if (prev < 0 && elev >= 0 && !moonrise) moonrise = new Date(t.getTime() - 5 * 60000).toISOString()
if (prev >= 0 && elev < 0 && !moonset) moonset = new Date(t.getTime() - 5 * 60000).toISOString()
}
prev = elev
if (moonrise && moonset) break
}
if(prev !== null) {
if(prev < 0 && elev >= 0 && !moonrise) moonrise = new Date(t.getTime() - 5 * 60000).toISOString();
if(prev >= 0 && elev < 0 && !moonset) moonset = new Date(t.getTime() - 5 * 60000).toISOString();
}
prev = elev;
if(moonrise && moonset) break;
}
return { moon_moonrise: moonrise, moon_moonset: moonset }
return {moonrise: moonrise, moonset: moonset};
}
function nextSolsticeEquinox(date = new Date()) {
const y = date.getFullYear()
const events = [
{ name: 'March Equinox', date: new Date(Date.UTC(y, 2, 20)) },
{ name: 'June Solstice', date: new Date(Date.UTC(y, 5, 21)) },
{ name: 'September Equinox', date: new Date(Date.UTC(y, 8, 22)) },
{ name: 'December Solstice', date: new Date(Date.UTC(y, 11, 21)) },
{ name: 'March Equinox', date: new Date(Date.UTC(y+1, 2, 20)) },
]
const next = events.find(e => e.date > date)
const prev = [...events].reverse().find(e => e.date <= date)
return {
season_next_event: next.name,
season_next_event_date: next.date.toISOString(),
season_last_event: prev?.name || null,
season_last_event_date: prev?.date.toISOString() || null,
}
const y = date.getFullYear();
return {
summer_solstice: new Date(Date.UTC(y, 5, 21)),
winter_solstice: new Date(Date.UTC(y, 11, 21)),
vernal_equinox: new Date(Date.UTC(y, 2, 20)),
autumnal_equinox: new Date(Date.UTC(y + 1, 2, 20)),
};
}
export function getCelestialCurrent(lat, lon, date = new Date()) {
return {
...sunPosition(lat, lon, date),
...sunriseSunset(lat, lon, date),
...moonPhase(date),
...nextMoonEvents(date),
...moonriseMoonset(lat, lon, date),
...nextSolsticeEquinox(date),
}
return {
...sunPosition(lat, lon, date),
...sunriseSunset(lat, lon, date),
...moonPhase(date),
...nextMoonEvents(date),
...moonriseMoonset(lat, lon, date),
...nextSolsticeEquinox(date),
};
}
export function getCelestialHourly(lat, lon, startISO, endISO) {
const start = new Date(startISO)
const end = new Date(endISO)
const results = []
for (let d = new Date(start); d <= end; d = new Date(d.getTime() + 3600000)) {
const pos = sunPosition(lat, lon, d)
const phase = moonPhase(d)
results.push({
time: d.toISOString(),
...pos,
moon_illumination_pct: phase.moon_illumination_pct,
moon_phase: phase.moon_phase
})
}
return results
}
export function getCelestialDaily(lat, lon, startISO, endISO) {
const start = new Date(startISO)
const end = new Date(endISO)
const results = []
for (let d = new Date(start); d <= end; d = new Date(d.getTime() + 86400000)) {
results.push({
time: d.toISOString(),
...sunriseSunset(lat, lon, d),
...moonPhase(d),
...nextMoonEvents(d),
...moonriseMoonset(lat, lon, d),
})
}
return results
export function getCelestialForecast(lat, lon, hours) {
return hours.map((data) => Object.assign(data, getCelestialCurrent(lat, lon, new Date(data.time))))
}