ws4kp-linhanced/server/scripts/modules/almanac.mjs

211 lines
6.8 KiB
JavaScript
Raw Permalink Normal View History

2020-09-04 13:02:20 -05:00
// display sun and moon data
2025-05-29 08:30:01 -05:00
import { preloadImg } from './utils/image.mjs';
2022-11-22 16:19:10 -06:00
import { DateTime } from '../vendor/auto/luxon.mjs';
import STATUS from './status.mjs';
2022-11-22 16:29:10 -06:00
import WeatherDisplay from './weatherdisplay.mjs';
import { registerDisplay, timeZone } from './navigation.mjs';
2020-09-04 13:02:20 -05:00
class Almanac extends WeatherDisplay {
2020-10-29 16:44:28 -05:00
constructor(navId, elemId) {
2022-11-21 21:50:22 -06:00
super(navId, elemId, 'Almanac', true);
2020-09-04 13:02:20 -05:00
2025-08-10 20:05:07 -05:00
// occasional degraded moon icon
this.iconPaths = {
Full: imageName(Math.random() > 0.995 ? 'Degraded' : 'Full'),
Last: imageName('Last'),
New: imageName('New'),
First: imageName('First'),
};
2022-09-05 11:44:31 -05:00
// preload the moon images
2025-08-10 20:05:07 -05:00
preloadImg(this.iconPaths.Full);
preloadImg(this.iconPaths.Last);
preloadImg(this.iconPaths.New);
preloadImg(this.iconPaths.First);
2020-09-04 13:02:20 -05:00
this.timing.totalScreens = 1;
2020-09-04 13:02:20 -05:00
}
2025-04-02 20:58:53 -05:00
async getData(weatherParameters, refresh) {
const superResponse = super.getData(weatherParameters, refresh);
2020-09-04 13:02:20 -05:00
2020-09-23 14:10:25 -05:00
// get sun/moon data
2025-04-02 20:58:53 -05:00
const { sun, moon } = this.calcSunMoonData(this.weatherParameters);
2020-09-23 14:10:25 -05:00
// store the data
2020-10-29 16:44:28 -05:00
this.data = {
2020-09-23 14:10:25 -05:00
sun,
moon,
};
2020-10-20 20:04:51 -05:00
// share data
this.getDataCallback();
2022-12-19 11:27:02 -06:00
if (!superResponse) return;
// update status
this.setStatus(STATUS.loaded);
2020-09-23 14:10:25 -05:00
}
calcSunMoonData(weatherParameters) {
2020-09-04 13:02:20 -05:00
const sun = [
SunCalc.getTimes(new Date(), weatherParameters.latitude, weatherParameters.longitude),
2020-10-29 16:44:28 -05:00
SunCalc.getTimes(DateTime.local().plus({ days: 1 }).toJSDate(), weatherParameters.latitude, weatherParameters.longitude),
2020-09-04 13:02:20 -05:00
];
// brute force the moon phases by scanning the next 30 days
const moon = [];
// start with yesterday
2020-10-29 16:44:28 -05:00
let moonDate = DateTime.local().minus({ days: 1 });
let { phase } = SunCalc.getMoonIllumination(moonDate.toJSDate());
2020-09-04 13:02:20 -05:00
let iterations = 0;
do {
// get yesterday's moon info
const lastPhase = phase;
// calculate new values
2020-10-29 16:44:28 -05:00
moonDate = moonDate.plus({ days: 1 });
2020-09-04 13:02:20 -05:00
phase = SunCalc.getMoonIllumination(moonDate.toJSDate()).phase;
// check for 4 cases
if (lastPhase < 0.25 && phase >= 0.25) moon.push(this.getMoonTransition(0.25, 'First', moonDate));
if (lastPhase < 0.50 && phase >= 0.50) moon.push(this.getMoonTransition(0.50, 'Full', moonDate));
if (lastPhase < 0.75 && phase >= 0.75) moon.push(this.getMoonTransition(0.75, 'Last', moonDate));
if (lastPhase > phase) moon.push(this.getMoonTransition(0.00, 'New', moonDate));
// stop after 30 days or 4 moon phases
2020-10-29 16:44:28 -05:00
iterations += 1;
2020-09-04 13:02:20 -05:00
} while (iterations <= 30 && moon.length < 4);
2020-09-23 14:10:25 -05:00
return {
2020-09-04 13:02:20 -05:00
sun,
moon,
};
}
// get moon transition from one phase to the next by drilling down by hours, minutes and seconds
getMoonTransition(threshold, phaseName, start, iteration = 0) {
let moonDate = start;
2020-10-29 16:44:28 -05:00
let { phase } = SunCalc.getMoonIllumination(moonDate.toJSDate());
2020-09-04 13:02:20 -05:00
let iterations = 0;
const step = {
2020-10-29 16:44:28 -05:00
hours: iteration === 0 ? -1 : 0,
minutes: iteration === 1 ? 1 : 0,
seconds: iteration === 2 ? -1 : 0,
milliseconds: iteration === 3 ? 1 : 0,
2020-09-04 13:02:20 -05:00
};
// increasing test
2020-10-29 16:44:28 -05:00
let test = (lastPhase, testPhase) => lastPhase < threshold && testPhase >= threshold;
2020-09-04 13:02:20 -05:00
// decreasing test
2020-10-29 16:44:28 -05:00
if (iteration % 2 === 0) test = (lastPhase, testPhase) => lastPhase > threshold && testPhase <= threshold;
2020-09-04 13:02:20 -05:00
do {
// store last phase
2020-09-04 13:02:20 -05:00
const lastPhase = phase;
// calculate new phase after step
moonDate = moonDate.plus(step);
phase = SunCalc.getMoonIllumination(moonDate.toJSDate()).phase;
// wrap phases > 0.9 to -0.1 for ease of detection
if (phase > 0.9) phase -= 1.0;
// compare
2020-10-29 16:44:28 -05:00
if (test(lastPhase, phase)) {
// last iteration is three, return value
2020-09-04 13:02:20 -05:00
if (iteration >= 3) break;
// iterate recursively
2020-10-29 16:44:28 -05:00
return this.getMoonTransition(threshold, phaseName, moonDate, iteration + 1);
2020-09-04 13:02:20 -05:00
}
2020-10-29 16:44:28 -05:00
iterations += 1;
2020-09-04 13:02:20 -05:00
} while (iterations < 1000);
2020-10-29 16:44:28 -05:00
return { phase: phaseName, date: moonDate };
2020-09-04 13:02:20 -05:00
}
async drawCanvas() {
super.drawCanvas();
const info = this.data;
// Generate sun data grid in reading order (left-to-right, top-to-bottom)
2020-09-04 13:02:20 -05:00
// Set day names
const Today = DateTime.local();
const Tomorrow = Today.plus({ days: 1 });
this.elem.querySelector('.day-1').textContent = Today.toLocaleString({ weekday: 'long' });
this.elem.querySelector('.day-2').textContent = Tomorrow.toLocaleString({ weekday: 'long' });
const todaySunrise = DateTime.fromJSDate(info.sun[0].sunrise);
const todaySunset = DateTime.fromJSDate(info.sun[0].sunset);
const [todaySunriseFormatted, todaySunsetFormatted] = formatTimesForColumn([todaySunrise, todaySunset]);
this.elem.querySelector('.rise-1').textContent = todaySunriseFormatted;
this.elem.querySelector('.set-1').textContent = todaySunsetFormatted;
const tomorrowSunrise = DateTime.fromJSDate(info.sun[1].sunrise);
const tomorrowSunset = DateTime.fromJSDate(info.sun[1].sunset);
const [tomorrowSunriseFormatted, tomorrowSunsetformatted] = formatTimesForColumn([tomorrowSunrise, tomorrowSunset]);
this.elem.querySelector('.rise-2').textContent = tomorrowSunriseFormatted;
this.elem.querySelector('.set-2').textContent = tomorrowSunsetformatted;
// Moon data
2022-09-05 11:44:31 -05:00
const days = info.moon.map((MoonPhase) => {
const fill = {};
2020-09-04 13:02:20 -05:00
const date = MoonPhase.date.toLocaleString({ month: 'short', day: 'numeric' });
2020-09-04 13:02:20 -05:00
2022-09-05 11:44:31 -05:00
fill.date = date;
fill.type = MoonPhase.phase;
2025-08-10 20:05:07 -05:00
fill.icon = { type: 'img', src: this.iconPaths[MoonPhase.phase] };
2022-09-05 11:44:31 -05:00
return this.fillTemplate('day', fill);
});
2020-09-25 13:25:12 -05:00
2022-09-05 11:44:31 -05:00
const daysContainer = this.elem.querySelector('.moon .days');
daysContainer.innerHTML = '';
daysContainer.append(...days);
2020-09-25 13:25:12 -05:00
this.finishDraw();
2020-09-04 13:02:20 -05:00
}
2020-10-20 16:37:11 -05:00
// make sun and moon data available outside this class
2020-10-20 20:04:51 -05:00
// promise allows for data to be requested before it is available
async getSun() {
return new Promise((resolve) => {
if (this.data) resolve(this.data);
// data not available, put it into the data callback queue
this.getDataCallbacks.push(resolve);
});
2020-10-20 16:37:11 -05:00
}
2020-10-29 16:44:28 -05:00
}
2022-11-22 16:19:10 -06:00
2022-12-09 13:51:51 -06:00
const imageName = (type) => {
switch (type) {
case 'Full':
return 'images/icons/moon-phases/Full-Moon.gif';
2025-08-10 20:05:07 -05:00
case 'Degraded':
return 'images/icons/moon-phases/Full-Moon-Degraded.gif';
case 'Last':
return 'images/icons/moon-phases/Last-Quarter.gif';
case 'New':
return 'images/icons/moon-phases/New-Moon.gif';
case 'First':
default:
return 'images/icons/moon-phases/First-Quarter.gif';
2022-12-09 13:51:51 -06:00
}
};
const formatTimesForColumn = (times) => {
const formatted = times.map((dt) => dt.setZone(timeZone()).toFormat('h:mm a').toUpperCase());
// Check if any time has a 2-digit hour (starts with '1')
const hasTwoDigitHour = formatted.some((time) => time.startsWith('1'));
// If mixed digit lengths, pad single-digit hours with non-breaking space
if (hasTwoDigitHour) {
return formatted.map((time) => (time.startsWith('1') ? time : `\u00A0${time}`));
}
// Otherwise, no padding needed
return formatted;
};
2025-05-29 08:30:01 -05:00
2022-12-06 16:14:56 -06:00
// register display
2022-12-14 16:28:33 -06:00
const display = new Almanac(9, 'almanac');
2022-12-06 16:14:56 -06:00
registerDisplay(display);
export default display.getSun.bind(display);