ws4kp/server/scripts/modules/weatherdisplay.mjs

420 lines
13 KiB
JavaScript
Raw Permalink Normal View History

2020-09-04 18:02:20 +00:00
// base weather display class
import STATUS, { calcStatusClass, statusClasses } from './status.mjs';
2022-11-22 22:29:10 +00:00
import { DateTime } from '../vendor/auto/luxon.mjs';
2022-12-06 22:14:56 +00:00
import {
2022-12-21 21:17:50 +00:00
msg, displayNavMessage, isPlaying, updateStatus, timeZone,
2022-12-06 22:14:56 +00:00
} from './navigation.mjs';
2020-09-04 18:02:20 +00:00
class WeatherDisplay {
2022-11-22 03:50:22 +00:00
constructor(navId, elemId, name, defaultEnabled) {
2022-12-06 22:14:56 +00:00
// navId is used in messaging and sort order
2020-09-04 18:02:20 +00:00
this.navId = navId;
this.elemId = undefined;
this.data = undefined;
this.loadingStatus = STATUS.loading;
2020-10-29 21:44:28 +00:00
this.name = name ?? elemId;
2020-10-21 01:04:51 +00:00
this.getDataCallbacks = [];
2022-12-12 19:53:33 +00:00
this.stillWaitingCallbacks = [];
2022-12-06 22:14:56 +00:00
this.defaultEnabled = defaultEnabled;
this.okToDrawCurrentConditions = true;
this.okToDrawCurrentDateTime = true;
2022-12-14 22:28:33 +00:00
this.showOnProgress = true;
2020-09-04 18:02:20 +00:00
// default navigation timing
this.timing = {
totalScreens: 1,
2020-09-25 14:55:29 +00:00
baseDelay: 9000, // 5 seconds
2023-11-12 03:55:17 +00:00
delay: 2, // 1*1second = 1 second total display time
2020-09-04 18:02:20 +00:00
};
this.navBaseCount = 0;
2020-09-09 19:29:03 +00:00
this.screenIndex = -1; // special starting condition
2020-09-04 18:02:20 +00:00
2022-11-22 03:50:22 +00:00
// store elemId once
this.storeElemId(elemId);
2020-09-18 19:59:58 +00:00
2022-12-14 19:08:49 +00:00
if (this.isEnabled) {
2020-09-18 16:24:45 +00:00
this.setStatus(STATUS.loading);
} else {
this.setStatus(STATUS.disabled);
}
this.startNavCount();
2022-07-29 21:12:42 +00:00
// get any templates
2022-12-07 16:53:18 +00:00
document.addEventListener('DOMContentLoaded', () => {
this.loadTemplates();
});
2020-09-04 18:02:20 +00:00
}
2022-12-06 22:14:56 +00:00
generateCheckbox(defaultEnabled = true) {
// no checkbox if progress
if (this.elemId === 'progress') return false;
2020-09-18 16:24:45 +00:00
// get the saved status of the checkbox
2022-12-12 20:47:53 +00:00
let savedStatus = window.localStorage.getItem(`display-enabled: ${this.elemId}`);
2020-09-18 16:24:45 +00:00
if (savedStatus === null) savedStatus = defaultEnabled;
2023-01-06 20:39:39 +00:00
this.isEnabled = !!((savedStatus === 'true' || savedStatus === true));
2020-09-18 16:24:45 +00:00
2020-09-18 19:59:58 +00:00
// refresh (or initially store the state of the checkbox)
2022-12-14 19:08:49 +00:00
window.localStorage.setItem(`display-enabled: ${this.elemId}`, this.isEnabled);
2020-09-18 19:59:58 +00:00
2020-09-18 16:24:45 +00:00
// create a checkbox in the selected displays area
const label = document.createElement('label');
label.for = `${this.elemId}-checkbox`;
label.id = `${this.elemId}-label`;
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.value = true;
checkbox.id = `${this.elemId}-checkbox`;
checkbox.name = `${this.elemId}-checkbox`;
2022-12-14 19:08:49 +00:00
checkbox.checked = this.isEnabled;
checkbox.addEventListener('change', (e) => this.checkboxChange(e));
const span = document.createElement('span');
span.innerHTML = this.name;
2022-12-14 22:28:33 +00:00
const alert = document.createElement('span');
alert.innerHTML = '!!!';
alert.classList.add('alert');
2022-12-14 22:28:33 +00:00
label.append(checkbox, span, alert);
this.checkbox = label;
return label;
2020-09-18 16:24:45 +00:00
}
checkboxChange(e) {
2020-09-18 19:59:58 +00:00
// update the state
2022-12-14 19:08:49 +00:00
this.isEnabled = e.target.checked;
2020-09-18 19:59:58 +00:00
// store the value for the next load
2022-12-14 19:08:49 +00:00
window.localStorage.setItem(`display-enabled: ${this.elemId}`, this.isEnabled);
2020-09-18 19:59:58 +00:00
// calling get data will update the status and actually get the data if we're set to enabled
this.getData();
2020-09-18 16:24:45 +00:00
}
2020-09-04 18:02:20 +00:00
// set data status and send update to navigation module
setStatus(value) {
this.status = value;
2022-12-06 22:14:56 +00:00
updateStatus({
2020-09-04 18:02:20 +00:00
id: this.navId,
status: this.status,
});
// update coloring of checkbox at bottom of page
if (!this.checkbox) return;
this.checkbox.classList.remove(...statusClasses);
this.checkbox.classList.add(calcStatusClass(value));
2020-09-04 18:02:20 +00:00
}
get status() {
return this.loadingStatus;
}
set status(state) {
this.loadingStatus = state;
}
2022-11-22 03:50:22 +00:00
storeElemId(elemId) {
2020-09-04 18:02:20 +00:00
// only create it once
if (this.elemId) return;
this.elemId = elemId;
}
// get necessary data for this display
2020-09-25 14:55:29 +00:00
getData(weatherParameters) {
2020-09-04 18:02:20 +00:00
// clear current data
this.data = undefined;
2020-09-18 16:24:45 +00:00
2020-09-25 14:55:29 +00:00
// store weatherParameters locally in case we need them later
if (weatherParameters) this.weatherParameters = weatherParameters;
// set status
2022-12-14 19:08:49 +00:00
if (this.isEnabled) {
2020-09-18 16:24:45 +00:00
this.setStatus(STATUS.loading);
} else {
this.setStatus(STATUS.disabled);
return false;
}
2020-09-09 19:29:03 +00:00
// recalculate navigation timing (in case it was modified in the constructor)
this.calcNavTiming();
2020-09-18 16:24:45 +00:00
return true;
2020-09-04 18:02:20 +00:00
}
2020-10-21 01:04:51 +00:00
// return any data requested before it was available
getDataCallback() {
// call each callback
2020-10-29 21:44:28 +00:00
this.getDataCallbacks.forEach((fxn) => fxn(this.data));
2020-10-21 01:04:51 +00:00
// clear the callbacks
this.getDataCallbacks = [];
}
2020-09-04 18:02:20 +00:00
drawCanvas() {
2020-09-18 19:59:58 +00:00
// clean up the first-run flag in screen index
2020-10-29 21:44:28 +00:00
if (this.screenIndex < 0) this.screenIndex = 0;
if (this.okToDrawCurrentDateTime) this.drawCurrentDateTime();
2020-09-04 18:02:20 +00:00
}
finishDraw() {
2022-12-06 22:14:56 +00:00
// draw date and time
if (this.okToDrawCurrentDateTime) {
this.drawCurrentDateTime();
2020-09-04 18:02:20 +00:00
// auto clock refresh
if (!this.dateTimeInterval) {
// only draw if canvas is active to conserve battery
setInterval(() => this.active && this.drawCurrentDateTime(), 100);
2020-09-04 18:02:20 +00:00
}
}
}
2022-07-29 21:12:42 +00:00
drawCurrentDateTime() {
2020-09-04 18:02:20 +00:00
// Get the current date and time.
2022-12-21 21:17:50 +00:00
const now = DateTime.local().setZone(timeZone());
2020-09-04 18:02:20 +00:00
2020-10-29 21:44:28 +00:00
// time = "11:35:08 PM";
const time = now.toLocaleString(DateTime.TIME_WITH_SECONDS).padStart(11, ' ');
2022-12-21 21:17:50 +00:00
const date = now.toFormat(' ccc LLL ') + now.day.toString().padStart(2, ' ');
const dateElem = this.elem.querySelector('.date-time.date');
const timeElem = this.elem.querySelector('.date-time.time');
2020-09-04 18:02:20 +00:00
2022-12-21 21:17:50 +00:00
if (timeElem && this.lastTime !== time) {
timeElem.innerHTML = time.toUpperCase();
2020-09-04 18:02:20 +00:00
}
2022-07-29 21:12:42 +00:00
this.lastTime = time;
2020-09-04 18:02:20 +00:00
2022-12-21 21:17:50 +00:00
if (dateElem && this.lastDate !== date) {
dateElem.innerHTML = date.toUpperCase();
2020-09-04 18:02:20 +00:00
}
2022-07-29 21:12:42 +00:00
this.lastDate = date;
2020-09-04 18:02:20 +00:00
}
// show/hide the canvas and start/stop the navigation timer
showCanvas(navCmd) {
// reset timing if enabled
2020-09-04 18:02:20 +00:00
// if a nav command is present call it to set the screen index
2022-12-06 22:14:56 +00:00
if (navCmd === msg.command.firstFrame) this.navNext(navCmd);
if (navCmd === msg.command.lastFrame) this.navPrev(navCmd);
2020-09-04 18:02:20 +00:00
2022-07-29 21:12:42 +00:00
this.startNavCount();
2022-11-22 03:50:22 +00:00
this.elem.classList.add('show');
2020-09-04 18:02:20 +00:00
}
2020-10-29 21:44:28 +00:00
2020-09-04 18:02:20 +00:00
hideCanvas() {
this.resetNavBaseCount();
2022-11-22 03:50:22 +00:00
this.elem.classList.remove('show');
2020-09-04 18:02:20 +00:00
}
2022-12-14 19:08:49 +00:00
get active() {
2022-08-04 16:07:35 +00:00
return this.elem.offsetHeight !== 0;
2020-09-04 18:02:20 +00:00
}
2022-12-14 19:08:49 +00:00
get enabled() {
return this.isEnabled;
2020-09-17 21:34:38 +00:00
}
2020-09-04 18:02:20 +00:00
// navigation timings
// totalScreens = total number of screens that are available
// baseDelay = ms to delay before re-evaluating screenIndex
// delay: three options
// integer = each screen will display for this number of baseDelays
// [integer, integer, ...] = screenIndex 0 displays for integer[0]*baseDelay, etc.
// [{time, si}, ...] = time as above, si is specific screen index to display during this interval
// if the array forms are used totalScreens is overwritten by the size of the array
navBaseTime() {
// see if play is active and screen is active
2022-12-14 19:08:49 +00:00
if (!isPlaying() || !this.active) return;
2020-09-04 18:02:20 +00:00
// increment the base count
2020-10-29 21:44:28 +00:00
this.navBaseCount += 1;
2020-09-04 18:02:20 +00:00
// call base count change if available for this function
if (this.baseCountChange) this.baseCountChange(this.navBaseCount);
2020-09-04 18:02:20 +00:00
2020-09-09 19:29:03 +00:00
// handle base count/screen index changes
this.updateScreenFromBaseCount();
}
2020-09-17 21:34:38 +00:00
async updateScreenFromBaseCount() {
2020-09-09 19:29:03 +00:00
// get the next screen index
2020-10-29 21:44:28 +00:00
const nextScreenIndex = this.screenIndexFromBaseCount();
2020-09-09 19:29:03 +00:00
// special cases for first and last frame
// must compare with false as nextScreenIndex could be 0 which is valid
if (nextScreenIndex === false) {
2022-12-06 22:14:56 +00:00
this.sendNavDisplayMessage(msg.response.next);
2020-09-04 18:02:20 +00:00
return;
}
2020-09-09 19:29:03 +00:00
// test for no change and exit early
if (nextScreenIndex === this.screenIndex) return;
2020-09-17 21:34:38 +00:00
// test for -1 (no screen displayed yet)
2023-01-06 20:39:39 +00:00
this.screenIndex = nextScreenIndex === -1 ? 0 : nextScreenIndex;
2020-09-09 19:29:03 +00:00
// call the appropriate screen index change method
2023-01-06 20:39:39 +00:00
if (this.screenIndexChange) {
2020-09-09 19:29:03 +00:00
this.screenIndexChange(this.screenIndex);
2023-01-06 20:39:39 +00:00
} else {
await this.drawCanvas();
2020-09-09 19:29:03 +00:00
}
2022-12-14 22:28:33 +00:00
this.showCanvas();
2020-09-09 19:29:03 +00:00
}
// take the three timing formats shown above and break them into arrays for consistent usage in navigation functions
// this.timing.fullDelay = [end of screen index 0 in base counts, end of screen index 1...]
// this.timing.screenIndexes = [screen index to use during this.timing.fullDelay[0], screen index to use during this.timing.fullDelay[1], ...]
calcNavTiming() {
if (this.timing === false) return;
2020-09-09 19:29:03 +00:00
// update total screens
if (Array.isArray(this.timing.delay)) this.timing.totalScreens = this.timing.delay.length;
// if the delay is provided as a single value, expand it to a series of the same value
let intermediateDelay = [];
if (typeof this.timing.delay === 'number') {
2020-10-29 21:44:28 +00:00
for (let i = 0; i < this.timing.totalScreens; i += 1) intermediateDelay.push(this.timing.delay);
2020-09-09 19:29:03 +00:00
} else {
// map just the delays to the intermediate block
2020-10-29 21:44:28 +00:00
intermediateDelay = this.timing.delay.map((delay) => {
2020-09-09 19:29:03 +00:00
if (typeof delay === 'object') return delay.time;
return delay;
});
}
// calculate the cumulative end point of each delay
let sum = 0;
2020-10-29 21:44:28 +00:00
this.timing.fullDelay = intermediateDelay.map((val) => {
2020-09-09 19:29:03 +00:00
const calc = sum + val;
sum += val;
return calc;
});
// generate a list of screen either sequentially if not provided in an object or from the object
if (Array.isArray(this.timing.delay) && typeof this.timing.delay[0] === 'object') {
// extract screen indexes from objects
2020-10-29 21:44:28 +00:00
this.timing.screenIndexes = this.timing.delay.map((delay) => delay.si);
2020-09-09 19:29:03 +00:00
} else {
// generate sequential screen indexes
this.timing.screenIndexes = [];
2020-10-29 21:44:28 +00:00
for (let i = 0; i < this.timing.totalScreens; i += 1) this.timing.screenIndexes.push(i);
}
2020-09-04 18:02:20 +00:00
}
// navigate to next screen
navNext(command) {
// check for special 'first frame' command
2022-12-06 22:14:56 +00:00
if (command === msg.command.firstFrame) {
2020-09-04 18:02:20 +00:00
this.resetNavBaseCount();
} else {
2020-09-09 19:29:03 +00:00
// set the base count to the next available frame
2020-10-29 21:44:28 +00:00
const newBaseCount = this.timing.fullDelay.find((delay) => delay > this.navBaseCount);
2020-09-09 19:29:03 +00:00
this.navBaseCount = newBaseCount;
}
2020-09-09 19:29:03 +00:00
this.updateScreenFromBaseCount();
2020-09-04 18:02:20 +00:00
}
// navigate to previous screen
navPrev(command) {
// check for special 'last frame' command
2022-12-06 22:14:56 +00:00
if (command === msg.command.lastFrame) {
2020-10-29 21:44:28 +00:00
this.navBaseCount = this.timing.fullDelay[this.timing.totalScreens - 1] - 1;
} else {
2020-09-09 19:29:03 +00:00
// find the highest fullDelay that is less than the current base count
const newBaseCount = this.timing.fullDelay.reduce((acc, delay) => {
if (delay < this.navBaseCount) return delay;
return acc;
2020-10-29 21:44:28 +00:00
}, 0);
2020-09-09 19:29:03 +00:00
// if the new base count is zero then we're already at the first screen
if (newBaseCount === 0 && this.navBaseCount === 0) {
2022-12-06 22:14:56 +00:00
this.sendNavDisplayMessage(msg.response.previous);
2020-09-09 19:29:03 +00:00
return;
}
this.navBaseCount = newBaseCount;
}
2020-09-09 19:29:03 +00:00
this.updateScreenFromBaseCount();
}
2020-09-09 19:29:03 +00:00
// get the screen index for the current base count, returns false if past end of timing array (go to next screen, stop timing)
screenIndexFromBaseCount() {
2020-09-25 18:25:12 +00:00
// test for timing enabled
if (!this.timing) return 0;
2022-12-14 22:28:33 +00:00
if (this.timing.totalScreens === 0) return false;
2020-09-09 19:29:03 +00:00
// find the first timing in the timing array that is greater than the base count
2020-09-25 18:25:12 +00:00
if (this.timing && !this.timing.fullDelay) this.calcNavTiming();
2020-10-29 21:44:28 +00:00
const timingIndex = this.timing.fullDelay.findIndex((delay) => delay > this.navBaseCount);
2020-09-09 19:29:03 +00:00
if (timingIndex === -1) return false;
return this.timing.screenIndexes[timingIndex];
2020-09-04 18:02:20 +00:00
}
// start and stop base counter
startNavCount() {
2020-10-29 21:44:28 +00:00
if (!this.navInterval) this.navInterval = setInterval(() => this.navBaseTime(), this.timing.baseDelay);
2020-09-04 18:02:20 +00:00
}
2020-09-04 18:02:20 +00:00
resetNavBaseCount() {
this.navBaseCount = 0;
2020-09-09 19:29:03 +00:00
this.screenIndex = -1;
// reset the timing so we don't short-change the first screen
if (this.navInterval) {
clearInterval(this.navInterval);
this.navInterval = undefined;
}
2020-09-04 18:02:20 +00:00
}
sendNavDisplayMessage(message) {
2022-12-06 22:14:56 +00:00
displayNavMessage({
2020-09-04 18:02:20 +00:00
id: this.navId,
type: message,
});
}
2022-07-29 21:12:42 +00:00
loadTemplates() {
this.templates = {};
2023-01-06 20:39:39 +00:00
this.elem = document.querySelector(`#${this.elemId}-html`);
2022-08-05 17:05:14 +00:00
if (!this.elem) return;
const templates = this.elem.querySelectorAll('.template');
templates.forEach((template) => {
const className = template.classList[0];
const node = template.cloneNode(true);
node.classList.remove('template');
this.templates[className] = node;
template.remove();
});
2022-07-29 21:12:42 +00:00
}
2022-08-03 02:39:27 +00:00
fillTemplate(name, fillValues) {
// get the template
const templateNode = this.templates[name];
if (!templateNode) return false;
// clone it
const template = templateNode.cloneNode(true);
Object.entries(fillValues).forEach(([key, value]) => {
// get the specified element
const elem = template.querySelector(`.${key}`);
if (!elem) return;
// fill based on type provided
if (typeof value === 'string' || typeof value === 'number') {
// string and number fill the first found selector
elem.innerHTML = value;
} else if (value?.type === 'img') {
// fill the image source
elem.querySelector('img').src = value.src;
}
});
return template;
}
2022-12-12 19:53:33 +00:00
// still waiting for data (retries triggered)
stillWaiting() {
2022-12-14 19:08:49 +00:00
if (this.isEnabled) this.setStatus(STATUS.retrying);
2022-12-12 19:53:33 +00:00
// handle still waiting callbacks
this.stillWaitingCallbacks.forEach((callback) => callback());
this.stillWaitingCallbacks = [];
}
2020-10-29 21:44:28 +00:00
}
2022-11-22 22:29:10 +00:00
export default WeatherDisplay;