ws4kp/server/scripts/modules/hourly.mjs

198 lines
7.2 KiB
JavaScript
Raw Permalink Normal View History

2020-10-21 01:04:51 +00:00
// hourly forecast list
2022-11-22 22:19:10 +00:00
import STATUS from './status.mjs';
import { DateTime, Interval, Duration } from '../vendor/auto/luxon.mjs';
import { json } from './utils/fetch.mjs';
2022-12-06 22:25:28 +00:00
import { celsiusToFahrenheit, kilometersToMiles } from './utils/units.mjs';
2022-11-22 22:19:10 +00:00
import { getHourlyIcon } from './icons.mjs';
import { directionToNSEW } from './utils/calc.mjs';
2022-11-22 22:29:10 +00:00
import WeatherDisplay from './weatherdisplay.mjs';
2022-12-06 22:14:56 +00:00
import { registerDisplay } from './navigation.mjs';
import getSun from './almanac.mjs';
2020-10-21 01:04:51 +00:00
class Hourly extends WeatherDisplay {
constructor(navId, elemId, defaultActive) {
// special height and width for scrolling
2022-11-22 03:50:22 +00:00
super(navId, elemId, 'Hourly Forecast', defaultActive);
2020-10-21 01:04:51 +00:00
// set up the timing
this.timing.baseDelay = 20;
// 24 hours = 6 pages
const pages = 4; // first page is already displayed, last page doesn't happen
2022-07-29 21:12:42 +00:00
const timingStep = 75 * 4;
2020-10-29 21:44:28 +00:00
this.timing.delay = [150 + timingStep];
2020-10-21 01:04:51 +00:00
// add additional pages
2020-10-29 21:44:28 +00:00
for (let i = 0; i < pages; i += 1) this.timing.delay.push(timingStep);
2020-10-21 01:04:51 +00:00
// add the final 3 second delay
this.timing.delay.push(150);
}
async getData(weatherParameters) {
// super checks for enabled
2022-12-07 21:36:02 +00:00
const superResponse = super.getData(weatherParameters);
2020-10-21 01:04:51 +00:00
let forecast;
try {
// get the forecast
2022-12-12 19:53:33 +00:00
forecast = await json(weatherParameters.forecastGridData, { retryCount: 3, stillWaiting: () => this.stillWaiting() });
2023-01-06 20:39:39 +00:00
} catch (error) {
2020-10-21 01:04:51 +00:00
console.error('Get hourly forecast failed');
2023-01-06 20:39:39 +00:00
console.error(error.status, error.responseJSON);
2022-12-14 19:08:49 +00:00
if (this.isEnabled) this.setStatus(STATUS.failed);
2022-12-08 20:41:15 +00:00
// return undefined to other subscribers
this.getDataCallback(undefined);
return;
2020-10-21 01:04:51 +00:00
}
2022-12-09 19:51:51 +00:00
this.data = await parseForecast(forecast.properties);
2022-12-07 21:36:02 +00:00
this.getDataCallback();
if (!superResponse) return;
2020-10-21 01:04:51 +00:00
this.setStatus(STATUS.loaded);
this.drawLongCanvas();
}
2020-10-29 21:44:28 +00:00
async drawLongCanvas() {
2022-07-29 21:12:42 +00:00
// get the list element and populate
const list = this.elem.querySelector('.hourly-lines');
list.innerHTML = '';
2020-10-21 01:04:51 +00:00
2022-11-22 22:19:10 +00:00
const startingHour = DateTime.local();
2020-10-21 01:04:51 +00:00
2022-07-29 21:12:42 +00:00
const lines = this.data.map((data, index) => {
2022-08-03 02:39:27 +00:00
const fillValues = {};
2020-10-21 01:04:51 +00:00
// hour
2020-10-29 21:44:28 +00:00
const hour = startingHour.plus({ hours: index });
const formattedHour = hour.toLocaleString({ weekday: 'short', hour: 'numeric' });
2022-08-03 02:39:27 +00:00
fillValues.hour = formattedHour;
2020-10-21 01:04:51 +00:00
// temperatures, convert to strings with no decimal
const temperature = Math.round(data.temperature).toString().padStart(3);
const feelsLike = Math.round(data.apparentTemperature).toString().padStart(3);
2022-08-03 02:39:27 +00:00
fillValues.temp = temperature;
2020-10-21 01:04:51 +00:00
// only plot apparent temperature if there is a difference
2022-08-03 02:39:27 +00:00
// if (temperature !== feelsLike) line.querySelector('.like').innerHTML = feelsLike;
if (temperature !== feelsLike) fillValues.like = feelsLike;
2020-10-21 01:04:51 +00:00
// wind
let wind = 'Calm';
if (data.windSpeed > 0) {
const windSpeed = Math.round(data.windSpeed).toString();
wind = data.windDirection + (Array(6 - data.windDirection.length - windSpeed.length).join(' ')) + windSpeed;
}
2022-08-03 02:39:27 +00:00
fillValues.wind = wind;
2020-10-21 01:04:51 +00:00
2022-07-29 21:12:42 +00:00
// image
2022-08-03 02:39:27 +00:00
fillValues.icon = { type: 'img', src: data.icon };
2020-10-21 01:04:51 +00:00
2022-08-03 02:39:27 +00:00
return this.fillTemplate('hourly-row', fillValues);
2022-07-29 21:12:42 +00:00
});
2020-10-21 01:04:51 +00:00
2022-07-29 21:12:42 +00:00
list.append(...lines);
}
2020-10-21 01:04:51 +00:00
2022-07-29 21:12:42 +00:00
drawCanvas() {
super.drawCanvas();
2020-10-21 01:04:51 +00:00
this.finishDraw();
}
2022-07-29 21:12:42 +00:00
showCanvas() {
// special to hourly to draw the remainder of the canvas
this.drawCanvas();
2020-10-21 01:04:51 +00:00
super.showCanvas();
}
// screen index change callback just runs the base count callback
screenIndexChange() {
this.baseCountChange(this.navBaseCount);
}
// base count change callback
baseCountChange(count) {
// calculate scroll offset and don't go past end
2022-07-29 21:12:42 +00:00
let offsetY = Math.min(this.elem.querySelector('.hourly-lines').getBoundingClientRect().height - 289, (count - 150));
2020-10-21 01:04:51 +00:00
// don't let offset go negative
if (offsetY < 0) offsetY = 0;
// copy the scrolled portion of the canvas
2022-07-29 21:12:42 +00:00
this.elem.querySelector('.main').scrollTo(0, offsetY);
2020-10-21 01:04:51 +00:00
}
2022-12-07 21:36:02 +00:00
// make data available outside this class
// promise allows for data to be requested before it is available
2022-12-12 19:53:33 +00:00
async getCurrentData(stillWaiting) {
if (stillWaiting) this.stillWaitingCallbacks.push(stillWaiting);
2022-12-07 21:36:02 +00:00
return new Promise((resolve) => {
if (this.data) resolve(this.data);
// data not available, put it into the data callback queue
this.getDataCallbacks.push(() => resolve(this.data));
});
}
2020-10-29 21:44:28 +00:00
}
2022-11-22 22:19:10 +00:00
2022-12-09 19:51:51 +00:00
// extract specific values from forecast and format as an array
const parseForecast = async (data) => {
const temperature = expand(data.temperature.values);
const apparentTemperature = expand(data.apparentTemperature.values);
const windSpeed = expand(data.windSpeed.values);
const windDirection = expand(data.windDirection.values);
const skyCover = expand(data.skyCover.values); // cloud icon
const weather = expand(data.weather.values); // fog icon
const iceAccumulation = expand(data.iceAccumulation.values); // ice icon
const probabilityOfPrecipitation = expand(data.probabilityOfPrecipitation.values); // rain icon
const snowfallAmount = expand(data.snowfallAmount.values); // snow icon
const icons = await determineIcon(skyCover, weather, iceAccumulation, probabilityOfPrecipitation, snowfallAmount, windSpeed);
return temperature.map((val, idx) => ({
temperature: celsiusToFahrenheit(temperature[idx]),
apparentTemperature: celsiusToFahrenheit(apparentTemperature[idx]),
windSpeed: kilometersToMiles(windSpeed[idx]),
windDirection: directionToNSEW(windDirection[idx]),
probabilityOfPrecipitation: probabilityOfPrecipitation[idx],
skyCover: skyCover[idx],
icon: icons[idx],
}));
};
// given forecast paramaters determine a suitable icon
const determineIcon = async (skyCover, weather, iceAccumulation, probabilityOfPrecipitation, snowfallAmount, windSpeed) => {
const startOfHour = DateTime.local().startOf('hour');
const sunTimes = (await getSun()).sun;
const overnight = Interval.fromDateTimes(DateTime.fromJSDate(sunTimes[0].sunset), DateTime.fromJSDate(sunTimes[1].sunrise));
const tomorrowOvernight = DateTime.fromJSDate(sunTimes[1].sunset);
return skyCover.map((val, idx) => {
const hour = startOfHour.plus({ hours: idx });
const isNight = overnight.contains(hour) || (hour > tomorrowOvernight);
return getHourlyIcon(skyCover[idx], weather[idx], iceAccumulation[idx], probabilityOfPrecipitation[idx], snowfallAmount[idx], windSpeed[idx], isNight);
});
};
// expand a set of values with durations to an hour-by-hour array
const expand = (data) => {
const startOfHour = DateTime.utc().startOf('hour').toMillis();
const result = []; // resulting expanded values
data.forEach((item) => {
let startTime = Date.parse(item.validTime.substr(0, item.validTime.indexOf('/')));
const duration = Duration.fromISO(item.validTime.substr(item.validTime.indexOf('/') + 1)).shiftTo('milliseconds').values.milliseconds;
const endTime = startTime + duration;
// loop through duration at one hour intervals
do {
// test for timestamp greater than now
if (startTime >= startOfHour && result.length < 24) {
result.push(item.value); // push data array
} // timestamp is after now
// increment start time by 1 hour
2023-01-06 20:39:39 +00:00
startTime += 3_600_000;
2022-12-09 19:51:51 +00:00
} while (startTime < endTime && result.length < 24);
}); // for each value
return result;
};
2022-12-06 22:14:56 +00:00
// register display
2022-12-14 22:28:33 +00:00
const display = new Hourly(3, 'hourly', false);
2022-12-07 21:36:02 +00:00
registerDisplay(display);
export default display.getCurrentData.bind(display);