mirror of
https://github.com/netbymatt/ws4kp.git
synced 2026-04-23 12:09:30 -07:00
243 lines
8.0 KiB
JavaScript
243 lines
8.0 KiB
JavaScript
// display sun and moon data
|
|
import { preloadImg } from './utils/image.mjs';
|
|
import { DateTime } from '../vendor/auto/luxon.mjs';
|
|
import STATUS from './status.mjs';
|
|
import WeatherDisplay from './weatherdisplay.mjs';
|
|
import { registerDisplay, timeZone } from './navigation.mjs';
|
|
|
|
class Almanac extends WeatherDisplay {
|
|
constructor(navId, elemId) {
|
|
super(navId, elemId, 'Almanac', true);
|
|
|
|
// occasional degraded moon icon
|
|
this.iconPaths = {
|
|
Full: imageName(Math.random() > 0.995 ? 'Degraded' : 'Full'),
|
|
Last: imageName('Last'),
|
|
New: imageName('New'),
|
|
First: imageName('First'),
|
|
};
|
|
|
|
// preload the moon images
|
|
preloadImg(this.iconPaths.Full);
|
|
preloadImg(this.iconPaths.Last);
|
|
preloadImg(this.iconPaths.New);
|
|
preloadImg(this.iconPaths.First);
|
|
|
|
this.timing.totalScreens = 1;
|
|
}
|
|
|
|
async getData(weatherParameters, refresh) {
|
|
const superResponse = super.getData(weatherParameters, refresh);
|
|
|
|
// get sun/moon data
|
|
const { sun, moon, moonTransit } = this.calcSunMoonData(this.weatherParameters);
|
|
|
|
// store the data
|
|
this.data = {
|
|
sun,
|
|
moon,
|
|
moonTransit,
|
|
};
|
|
// share data
|
|
this.getDataCallback();
|
|
|
|
if (!superResponse) return;
|
|
|
|
// update status
|
|
this.setStatus(STATUS.loaded);
|
|
}
|
|
|
|
calcSunMoonData(weatherParameters) {
|
|
const dayOffsets = [0, 1, 2, 3, 4, 5, 6];
|
|
const sun = dayOffsets.map((days) => SunCalc.getTimes(DateTime.local().plus({ days }).toJSDate(), weatherParameters.latitude, weatherParameters.longitude));
|
|
const moonTransit = dayOffsets.map((days) => SunCalc.getMoonTimes(DateTime.local().plus({ days }).toJSDate(), weatherParameters.latitude, weatherParameters.longitude));
|
|
|
|
// brute force the moon phases by scanning the next 30 days
|
|
const moon = [];
|
|
// start with yesterday
|
|
let moonDate = DateTime.local().minus({ days: 1 });
|
|
let { phase } = SunCalc.getMoonIllumination(moonDate.toJSDate());
|
|
let iterations = 0;
|
|
do {
|
|
// get yesterday's moon info
|
|
const lastPhase = phase;
|
|
// calculate new values
|
|
moonDate = moonDate.plus({ days: 1 });
|
|
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
|
|
iterations += 1;
|
|
} while (iterations <= 45 && moon.length < 5);
|
|
|
|
return {
|
|
sun,
|
|
moon,
|
|
moonTransit,
|
|
};
|
|
}
|
|
|
|
// 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;
|
|
let { phase } = SunCalc.getMoonIllumination(moonDate.toJSDate());
|
|
let iterations = 0;
|
|
const step = {
|
|
hours: iteration === 0 ? -1 : 0,
|
|
minutes: iteration === 1 ? 1 : 0,
|
|
seconds: iteration === 2 ? -1 : 0,
|
|
milliseconds: iteration === 3 ? 1 : 0,
|
|
};
|
|
|
|
// increasing test
|
|
let test = (lastPhase, testPhase) => lastPhase < threshold && testPhase >= threshold;
|
|
// decreasing test
|
|
if (iteration % 2 === 0) test = (lastPhase, testPhase) => lastPhase > threshold && testPhase <= threshold;
|
|
|
|
do {
|
|
// store last phase
|
|
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
|
|
if (test(lastPhase, phase)) {
|
|
// last iteration is three, return value
|
|
if (iteration >= 3) break;
|
|
// iterate recursively
|
|
return this.getMoonTransition(threshold, phaseName, moonDate, iteration + 1);
|
|
}
|
|
iterations += 1;
|
|
} while (iterations < 1000);
|
|
|
|
return { phase: phaseName, date: moonDate };
|
|
}
|
|
|
|
async drawCanvas() {
|
|
super.drawCanvas();
|
|
const info = this.data;
|
|
|
|
// Generate sun data grid in reading order (left-to-right, top-to-bottom)
|
|
|
|
// Set day names and sunset times
|
|
const Today = DateTime.local();
|
|
const portraitLines = [];
|
|
const moonPortraitLines = [];
|
|
// fill all days, even if some are hidden by the mode selection.
|
|
for (let i = 0; i < 7; i += 1) {
|
|
// format some data
|
|
const dayName = Today.plus({ days: i }).toLocaleString({ weekday: 'long' });
|
|
const sunrise = formatTimeForColumn(DateTime.fromJSDate(info.sun[i].sunrise));
|
|
const sunset = formatTimeForColumn(DateTime.fromJSDate(info.sun[i].sunset));
|
|
|
|
// these only use the first 3 for standard and wide
|
|
if (i < 3) {
|
|
this.elem.querySelector(`.day-${i}`).textContent = dayName;
|
|
this.elem.querySelector(`.rise-${i}`).textContent = sunrise;
|
|
this.elem.querySelector(`.set-${i}`).textContent = sunset;
|
|
}
|
|
|
|
// and also fill the portrait oriented info
|
|
portraitLines.push(this.fillTemplate('dayname', { 'grid-item': dayName }));
|
|
portraitLines.push(this.fillTemplate('sunrise', { 'grid-item': sunrise }));
|
|
portraitLines.push(this.fillTemplate('sunset', { 'grid-item': sunset }));
|
|
|
|
// including the bonus moon rise/set data in portrait
|
|
const moonrise = formatTimeForColumn(DateTime.fromJSDate(info.moonTransit[i].rise));
|
|
const moonset = formatTimeForColumn(DateTime.fromJSDate(info.moonTransit[i].set));
|
|
|
|
moonPortraitLines.push(this.fillTemplate('dayname', { 'grid-item': dayName }));
|
|
moonPortraitLines.push(this.fillTemplate('sunrise', { 'grid-item': moonrise }));
|
|
moonPortraitLines.push(this.fillTemplate('sunset', { 'grid-item': moonset }));
|
|
}
|
|
|
|
// add the portrait lines to the page
|
|
const sunPortrait = this.elem.querySelector('.sun-portrait');
|
|
const replaceable = sunPortrait.querySelectorAll(':has(.replaceable)');
|
|
replaceable.forEach((elem) => elem.remove());
|
|
sunPortrait.append(...portraitLines);
|
|
|
|
// and the moon too
|
|
const moonPortrait = this.elem.querySelector('.moonrise.sun-portrait');
|
|
const moonReplaceable = moonPortrait.querySelectorAll(':has(.replaceable)');
|
|
moonReplaceable.forEach((elem) => elem.remove());
|
|
moonPortrait.append(...moonPortraitLines);
|
|
|
|
// Moon data
|
|
const days = info.moon.map((MoonPhase, idx) => {
|
|
const fill = {};
|
|
|
|
const date = MoonPhase.date.toLocaleString({ month: 'short', day: 'numeric' });
|
|
|
|
fill.date = date;
|
|
fill.type = MoonPhase.phase;
|
|
fill.icon = { type: 'img', src: this.iconPaths[MoonPhase.phase] };
|
|
|
|
const filledTemplate = this.fillTemplate('day', fill);
|
|
|
|
// add class to hide >4 moon phases when not wide-enhanced
|
|
if (idx > 3) {
|
|
filledTemplate.classList.add('wide-enhanced');
|
|
}
|
|
|
|
return filledTemplate;
|
|
});
|
|
|
|
const daysContainer = this.elem.querySelector('.moon .days');
|
|
daysContainer.innerHTML = '';
|
|
daysContainer.append(...days);
|
|
|
|
this.finishDraw();
|
|
}
|
|
|
|
// make sun and moon data available outside this class
|
|
// 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);
|
|
});
|
|
}
|
|
}
|
|
|
|
const imageName = (type) => {
|
|
switch (type) {
|
|
case 'Full':
|
|
return 'images/icons/moon-phases/Full-Moon.gif';
|
|
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';
|
|
}
|
|
};
|
|
|
|
const formatTimeForColumn = (time) => {
|
|
// moonrise and set may not have a time each day
|
|
if (!time.isValid) return '-';
|
|
const formatted = time.setZone(timeZone()).toFormat('h:mm a').toUpperCase();
|
|
|
|
// If mixed digit lengths, pad single-digit hours with non-breaking space
|
|
if (formatted.length === 8) {
|
|
return formatted;
|
|
}
|
|
return `\u00A0${formatted}`;
|
|
};
|
|
|
|
// register display
|
|
const display = new Almanac(9, 'almanac');
|
|
registerDisplay(display);
|
|
|
|
export default display.getSun.bind(display);
|