// base weather display class /* globals navigation, utils, draw, UNITS, luxon */ const STATUS = { loading: Symbol('loading'), loaded: Symbol('loaded'), failed: Symbol('failed'), noData: Symbol('noData'), }; // eslint-disable-next-line no-unused-vars class WeatherDisplay { constructor(navId, elemId, name) { // navId is used in messaging this.navId = navId; this.elemId = undefined; this.gifs = []; this.data = undefined; this.loadingStatus = STATUS.loading; this.name = name?name:elemId; // default navigation timing this.timing = { totalScreens: 1, baseDelay: 5000, // 5 seconds delay: 1, // 1*1second = 1 second total display time }; this.navBaseCount = 0; this.screenIndex = -1; // special starting condition this.setStatus(STATUS.loading); this.createCanvas(elemId); } // set data status and send update to navigation module setStatus(value) { this.status = value; navigation.updateStatus({ id: this.navId, status: this.status, }); } get status() { return this.loadingStatus; } set status(state) { this.loadingStatus = state; } createCanvas(elemId, width = 640, height = 480) { // only create it once if (this.elemId) return; this.elemId = elemId; const container = document.getElementById('container'); container.innerHTML += `<canvas id='${elemId+'Canvas'}' width='${width}' height='${height}'/ style='display: none;'>`; } // get necessary data for this display getData() { // clear current data this.data = undefined; // set status this.setStatus(STATUS.loading); // recalculate navigation timing (in case it was modified in the constructor) this.calcNavTiming(); } drawCanvas() { // stop all gifs this.gifs.forEach(gif => gif.pause()); // delete the gifs this.gifs.length = 0; // refresh the canvas this.canvas = document.getElementById(this.elemId+'Canvas'); this.context = this.canvas.getContext('2d'); } finishDraw() { let OkToDrawCurrentConditions = true; let OkToDrawNoaaImage = true; let OkToDrawCurrentDateTime = true; let OkToDrawLogoImage = true; // let OkToDrawCustomScrollText = false; let bottom = undefined; // visibility tests // if (_ScrollText !== '') OkToDrawCustomScrollText = true; if (this.elemId === 'almanac') OkToDrawNoaaImage = false; if (this.elemId === 'travelForecast') OkToDrawNoaaImage = false; if (this.elemId === 'regionalForecast') OkToDrawNoaaImage = false; if (this.elemId === 'radar') { OkToDrawCurrentConditions = false; OkToDrawCurrentDateTime = false; OkToDrawNoaaImage = false; // OkToDrawCustomScrollText = false; } if (this.elemId === 'hazards') { OkToDrawNoaaImage = false; bottom = true; OkToDrawLogoImage = false; } // draw functions if (OkToDrawCurrentDateTime) { this.drawCurrentDateTime(bottom); // auto clock refresh if (!this.dateTimeInterval) { setInterval(() => this.drawCurrentDateTime(bottom), 100); } } if (OkToDrawLogoImage) this.drawLogoImage(); if (OkToDrawNoaaImage) this.drawNoaaImage(); // TODO: fix current conditions scroll // if (OkToDrawCurrentConditions) DrawCurrentConditions(WeatherParameters, this.context); // TODO: add custom scroll text // if (OkToDrawCustomScrollText) DrawCustomScrollText(WeatherParameters, context); } drawCurrentDateTime(bottom) { // only draw if canvas is active to conserve battery if (!this.isActive()) return; const {DateTime} = luxon; const font = 'Star4000 Small'; const size = '24pt'; const color = '#ffffff'; const shadow = 2; // on the first pass store the background for the date and time if (!this.dateTimeBackground) { const bg = this.context.getImageData(410, 30, 175, 60); // test background draw complete and skip drawing if there is no background yet if (bg.data[0] === 0) return; // store the background this.dateTimeBackground = bg; } // Clear the date and time area. if (bottom) { draw.box(this.context, 'rgb(25, 50, 112)', 0, 389, 640, 16); } else { this.context.putImageData(this.dateTimeBackground, 410, 30); } // Get the current date and time. const now = DateTime.local(); //time = "11:35:08 PM"; const time = now.toLocaleString(DateTime.TIME_WITH_SECONDS).padStart(11,' '); let x,y; if (bottom) { x = 400; y = 402; } else { x = 410; y = 65; } if (navigation.units() === UNITS.metric) { x += 45; } draw.text(this.context, font, size, color, x, y, time.toUpperCase(), shadow); //y += 20; const date = now.toFormat(' ccc LLL ') + now.day.toString().padStart(2,' '); if (bottom) { x = 55; y = 402; } else { x = 410; y = 85; } draw.text(this.context, font, size, color, x, y, date.toUpperCase(), shadow); } async drawNoaaImage () { // load the image and store locally if (!this.drawNoaaImage.image) { this.drawNoaaImage.image = utils.image.load('images/noaa5.gif'); } // wait for the image to load completely const img = await this.drawNoaaImage.image; this.context.drawImage(img, 356, 39); } async drawLogoImage () { // load the image and store locally if (!this.drawLogoImage.image) { this.drawLogoImage.image = utils.image.load('images/Logo3.png'); } // wait for the image load completely const img = await this.drawLogoImage.image; this.context.drawImage(img, 50, 30, 85, 67); } // show/hide the canvas and start/stop the navigation timer showCanvas(navCmd) { // if a nav command is present call it to set the screen index if (navCmd === navigation.msg.command.firstFrame) this.navNext(navCmd); if (navCmd === navigation.msg.command.lastFrame) this.navPrev(navCmd); // see if the canvas is already showing if (this.canvas.style.display === 'block') return false; // show the canvas this.canvas.style.display = 'block'; // stop if timing has been disabled if (!this.timing) return; // reset timing this.startNavCount(navigation.isPlaying()); } hideCanvas() { this.stopNavBaseCount(true); if (!this.canvas) return; this.canvas.style.display = 'none'; } isActive() { return document.getElementById(this.elemId+'Canvas').offsetParent !== null; } // 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 if (!navigation.isPlaying() || !this.isActive()) return; // increment the base count this.navBaseCount++; // call base count change if available for this function if (this.baseCountChange) this.baseCountChange(this.navBaseCount); // handle base count/screen index changes this.updateScreenFromBaseCount(); } updateScreenFromBaseCount() { // get the next screen index let nextScreenIndex = this.screenIndexFromBaseCount(); // special cases for first and last frame // must compare with false as nextScreenIndex could be 0 which is valid if (nextScreenIndex === false) { this.sendNavDisplayMessage(navigation.msg.response.next); this.stopNavBaseCount(); return; } // test for no change and exit early if (nextScreenIndex === this.screenIndex) return; this.screenIndex = nextScreenIndex; // call the appropriate screen index change method if (!this.screenIndexChange) { this.drawCanvas(); } else { this.screenIndexChange(this.screenIndex); } } // 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() { // 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') { for (let i = 0; i < this.timing.totalScreens; i++) intermediateDelay.push(this.timing.delay); } else { // map just the delays to the intermediate block intermediateDelay = this.timing.delay.map(delay => { if (typeof delay === 'object') return delay.time; return delay; }); } // calculate the cumulative end point of each delay let sum = 0; this.timing.fullDelay = intermediateDelay.map(val => { 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 this.timing.screenIndexes = this.timing.delay.map(delay => delay.si); } else { // generate sequential screen indexes this.timing.screenIndexes = []; for (let i = 0; i < this.timing.totalScreens; i++) this.timing.screenIndexes.push(i); } } // navigate to next screen navNext(command) { // check for special 'first frame' command if (command === navigation.msg.command.firstFrame) { this.resetNavBaseCount(); } else { // set the base count to the next available frame const newBaseCount = this.timing.fullDelay.find(delay => delay > this.navBaseCount); this.navBaseCount = newBaseCount; } this.updateScreenFromBaseCount(); } // navigate to previous screen navPrev(command) { // check for special 'last frame' command if (command === navigation.msg.command.lastFrame) { this.navBaseCount = this.timing.fullDelay[this.timing.totalScreens-1]-1; } else { // 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; },0); // if the new base count is zero then we're already at the first screen if (newBaseCount === 0 && this.navBaseCount === 0) { this.sendNavDisplayMessage(navigation.msg.response.previous); return; } this.navBaseCount = newBaseCount; } this.updateScreenFromBaseCount(); } // get the screen index for the current base count, returns false if past end of timing array (go to next screen, stop timing) screenIndexFromBaseCount() { // find the first timing in the timing array that is greater than the base count const timingIndex = this.timing.fullDelay.findIndex(delay => delay > this.navBaseCount); if (timingIndex === -1) return false; return this.timing.screenIndexes[timingIndex]; } // start and stop base counter startNavCount(reset) { if (reset) this.resetNavBaseCount(); if (!this.navInterval) this.navInterval = setInterval(()=>this.navBaseTime(), this.timing.baseDelay); } stopNavBaseCount(reset) { clearInterval(this.navInterval); this.navInterval = undefined; if (reset) this.resetNavBaseCount(); } resetNavBaseCount() { this.navBaseCount = 0; this.screenIndex = -1; } sendNavDisplayMessage(message) { navigation.displayNavMessage({ id: this.navId, type: message, }); } }