2020-10-21 01:04:51 +00:00
|
|
|
// hourly forecast list
|
2022-07-29 21:12:42 +00:00
|
|
|
/* globals WeatherDisplay, utils, STATUS, UNITS, navigation, icons, luxon */
|
2020-10-21 01:04:51 +00:00
|
|
|
|
|
|
|
// eslint-disable-next-line no-unused-vars
|
|
|
|
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
|
|
|
|
if (!super.getData(weatherParameters)) return;
|
|
|
|
let forecast;
|
|
|
|
try {
|
|
|
|
// get the forecast
|
|
|
|
forecast = await utils.fetch.json(weatherParameters.forecastGridData);
|
|
|
|
} catch (e) {
|
|
|
|
console.error('Get hourly forecast failed');
|
|
|
|
console.error(e.status, e.responseJSON);
|
|
|
|
this.setStatus(STATUS.failed);
|
2022-08-05 01:12:44 +00:00
|
|
|
return;
|
2020-10-21 01:04:51 +00:00
|
|
|
}
|
|
|
|
|
2020-10-29 21:44:28 +00:00
|
|
|
this.data = await Hourly.parseForecast(forecast.properties);
|
2020-10-21 01:04:51 +00:00
|
|
|
|
|
|
|
this.setStatus(STATUS.loaded);
|
|
|
|
this.drawLongCanvas();
|
|
|
|
}
|
|
|
|
|
|
|
|
// extract specific values from forecast and format as an array
|
2020-10-29 21:44:28 +00:00
|
|
|
static async parseForecast(data) {
|
|
|
|
const temperature = Hourly.expand(data.temperature.values);
|
|
|
|
const apparentTemperature = Hourly.expand(data.apparentTemperature.values);
|
|
|
|
const windSpeed = Hourly.expand(data.windSpeed.values);
|
|
|
|
const windDirection = Hourly.expand(data.windDirection.values);
|
|
|
|
const skyCover = Hourly.expand(data.skyCover.values); // cloud icon
|
|
|
|
const weather = Hourly.expand(data.weather.values); // fog icon
|
|
|
|
const iceAccumulation = Hourly.expand(data.iceAccumulation.values); // ice icon
|
|
|
|
const probabilityOfPrecipitation = Hourly.expand(data.probabilityOfPrecipitation.values); // rain icon
|
|
|
|
const snowfallAmount = Hourly.expand(data.snowfallAmount.values); // snow icon
|
|
|
|
|
|
|
|
const icons = await Hourly.determineIcon(skyCover, weather, iceAccumulation, probabilityOfPrecipitation, snowfallAmount, windSpeed);
|
2020-10-21 01:04:51 +00:00
|
|
|
|
|
|
|
return temperature.map((val, idx) => {
|
2020-10-29 21:44:28 +00:00
|
|
|
if (navigation.units === UNITS.metric) {
|
|
|
|
return {
|
|
|
|
temperature: temperature[idx],
|
|
|
|
apparentTemperature: apparentTemperature[idx],
|
|
|
|
windSpeed: windSpeed[idx],
|
|
|
|
windDirection: utils.calc.directionToNSEW(windDirection[idx]),
|
|
|
|
icon: icons[idx],
|
|
|
|
};
|
|
|
|
}
|
2020-10-21 01:04:51 +00:00
|
|
|
|
|
|
|
return {
|
|
|
|
temperature: utils.units.celsiusToFahrenheit(temperature[idx]),
|
|
|
|
apparentTemperature: utils.units.celsiusToFahrenheit(apparentTemperature[idx]),
|
|
|
|
windSpeed: utils.units.kilometersToMiles(windSpeed[idx]),
|
|
|
|
windDirection: utils.calc.directionToNSEW(windDirection[idx]),
|
|
|
|
icon: icons[idx],
|
|
|
|
};
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
// given forecast paramaters determine a suitable icon
|
2020-10-29 21:44:28 +00:00
|
|
|
static async determineIcon(skyCover, weather, iceAccumulation, probabilityOfPrecipitation, snowfallAmount, windSpeed) {
|
2020-10-21 01:04:51 +00:00
|
|
|
const startOfHour = luxon.DateTime.local().startOf('hour');
|
|
|
|
const sunTimes = (await navigation.getSun()).sun;
|
|
|
|
const overnight = luxon.Interval.fromDateTimes(luxon.DateTime.fromJSDate(sunTimes[0].sunset), luxon.DateTime.fromJSDate(sunTimes[1].sunrise));
|
|
|
|
const tomorrowOvernight = luxon.DateTime.fromJSDate(sunTimes[1].sunset);
|
|
|
|
return skyCover.map((val, idx) => {
|
2020-10-29 21:44:28 +00:00
|
|
|
const hour = startOfHour.plus({ hours: idx });
|
2020-10-21 01:04:51 +00:00
|
|
|
const isNight = overnight.contains(hour) || (hour > tomorrowOvernight);
|
2020-10-26 19:45:03 +00:00
|
|
|
return icons.getHourlyIcon(skyCover[idx], weather[idx], iceAccumulation[idx], probabilityOfPrecipitation[idx], snowfallAmount[idx], windSpeed[idx], isNight);
|
2020-10-21 01:04:51 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
// expand a set of values with durations to an hour-by-hour array
|
2020-10-29 21:44:28 +00:00
|
|
|
static expand(data) {
|
2020-10-21 01:04:51 +00:00
|
|
|
const startOfHour = luxon.DateTime.utc().startOf('hour').toMillis();
|
|
|
|
const result = []; // resulting expanded values
|
2020-10-29 21:44:28 +00:00
|
|
|
data.forEach((item) => {
|
2020-10-21 01:04:51 +00:00
|
|
|
let startTime = Date.parse(item.validTime.substr(0, item.validTime.indexOf('/')));
|
2020-10-29 21:44:28 +00:00
|
|
|
const duration = luxon.Duration.fromISO(item.validTime.substr(item.validTime.indexOf('/') + 1)).shiftTo('milliseconds').values.milliseconds;
|
2020-10-21 01:04:51 +00:00
|
|
|
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
|
2020-10-29 21:44:28 +00:00
|
|
|
startTime += 3600000;
|
2020-10-21 01:04:51 +00:00
|
|
|
} while (startTime < endTime && result.length < 24);
|
|
|
|
}); // for each value
|
|
|
|
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
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
|
|
|
|
|
|
|
const startingHour = luxon.DateTime.local();
|
|
|
|
|
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
|
|
|
}
|
|
|
|
|
2020-10-29 21:44:28 +00:00
|
|
|
static getTravelCitiesDayName(cities) {
|
|
|
|
const { DateTime } = luxon;
|
2020-10-21 01:04:51 +00:00
|
|
|
// effectively returns early on the first found date
|
|
|
|
return cities.reduce((dayName, city) => {
|
|
|
|
if (city && dayName === '') {
|
|
|
|
// today or tomorrow
|
2020-10-29 21:44:28 +00:00
|
|
|
const day = DateTime.local().plus({ days: (city.today) ? 0 : 1 });
|
2020-10-21 01:04:51 +00:00
|
|
|
// return the day
|
2020-10-29 21:44:28 +00:00
|
|
|
return day.toLocaleString({ weekday: 'long' });
|
2020-10-21 01:04:51 +00:00
|
|
|
}
|
|
|
|
return dayName;
|
|
|
|
}, '');
|
|
|
|
}
|
2020-10-29 21:44:28 +00:00
|
|
|
}
|