ws4kp-linhanced/server/scripts/modules/hourly.mjs

161 lines
5.4 KiB
JavaScript

// hourly forecast list
import STATUS from './status.mjs';
import { DateTime } from '../vendor/auto/luxon.mjs';
import { temperature as temperatureUnit, windSpeed as windUnit } from './utils/units.mjs';
import { directionToNSEW } from './utils/calc.mjs';
import WeatherDisplay from './weatherdisplay.mjs';
import { registerDisplay, timeZone } from './navigation.mjs';
import calculateScrollTiming from './utils/scroll-timing.mjs';
import { getSmallIconFromWmoCode } from './icons.mjs';
class Hourly extends WeatherDisplay {
constructor(navId, elemId, defaultActive) {
super(navId, elemId, 'Hourly Forecast', defaultActive);
this.scrollCache = {
displayHeight: 0,
contentHeight: 0,
maxOffset: 0,
hourlyLines: null,
};
}
async getData(weatherParameters, refresh) {
const superResponse = super.getData(weatherParameters, refresh);
this.data = parseForecast(this.weatherParameters);
if (!this.data?.length) {
if (this.isEnabled) this.setStatus(STATUS.failed);
this.getDataCallback(undefined);
return;
}
this.getDataCallback();
if (!superResponse) return;
this.setStatus(STATUS.loaded);
this.drawLongCanvas();
}
async drawLongCanvas() {
const list = this.elem.querySelector('.hourly-lines');
list.innerHTML = '';
const startingHour = DateTime.local().setZone(timeZone());
const shortData = this.data.slice(0, 24);
const lines = shortData.map((data, index) => {
const hour = startingHour.plus({ hours: index });
const fillValues = {
hour: hour.toLocaleString({ weekday: 'short', hour: 'numeric' }),
temp: data.temperature.toString().padStart(3),
like: data.apparentTemperature.toString().padStart(3),
wind: data.windSpeed > 0
? data.windDirection + (Array(6 - data.windDirection.length - Math.round(data.windSpeed).toString().length).join(' ')) + Math.round(data.windSpeed).toString()
: 'Calm',
icon: { type: 'img', src: data.icon },
};
const filledRow = this.fillTemplate('hourly-row', fillValues);
if (data.apparentTemperature < data.temperature) {
filledRow.querySelector('.like').classList.add('wind-chill');
} else if (data.apparentTemperature > data.temperature) {
filledRow.querySelector('.like').classList.add('heat-index');
}
return filledRow;
});
list.append(...lines);
this.setTiming(list);
}
drawCanvas() {
super.drawCanvas();
this.finishDraw();
}
showCanvas() {
this.drawCanvas();
super.showCanvas();
}
screenIndexChange() {
this.baseCountChange(this.navBaseCount);
}
baseCountChange(count) {
const hourlyLines = this.elem.querySelector('.hourly-lines');
if (!hourlyLines) return;
if (this.scrollCache.hourlyLines !== hourlyLines || this.scrollCache.displayHeight === 0) {
this.scrollCache.displayHeight = this.elem.querySelector('.main').offsetHeight;
this.scrollCache.contentHeight = hourlyLines.offsetHeight;
this.scrollCache.maxOffset = Math.max(0, this.scrollCache.contentHeight - this.scrollCache.displayHeight);
this.scrollCache.hourlyLines = hourlyLines;
hourlyLines.style.willChange = 'transform';
hourlyLines.style.backfaceVisibility = 'hidden';
}
let offsetY = Math.min(this.scrollCache.maxOffset, (count - this.scrollTiming.initialCounts) * this.scrollTiming.pixelsPerCount);
if (offsetY < 0) offsetY = 0;
hourlyLines.style.transform = `translateY(-${Math.round(offsetY)}px)`;
}
async getHourlyData(stillWaiting) {
if (stillWaiting) this.stillWaitingCallbacks.push(stillWaiting);
this.setAutoReload();
return new Promise((resolve) => {
if (this.data) resolve(this.data);
this.getDataCallbacks.push(() => resolve(this.data));
});
}
setTiming(list) {
const container = this.elem.querySelector('.main');
const timingConfig = calculateScrollTiming(list, container);
this.timing.baseDelay = timingConfig.baseDelay;
this.timing.delay = timingConfig.delay;
this.scrollTiming = timingConfig.scrollTiming;
this.calcNavTiming();
}
}
const parseForecast = (weatherParameters) => {
const temperatureConverter = temperatureUnit();
const windConverter = windUnit();
const currentTime = new Date();
const todayKey = currentTime.toLocaleDateString('en-CA', { timeZone: weatherParameters.timeZone });
const tomorrowKey = DateTime.fromISO(todayKey).plus({ days: 1 }).toISODate();
const availableTimes = [
...(weatherParameters.forecast[todayKey]?.hours ?? []),
...(weatherParameters.forecast[tomorrowKey]?.hours ?? []),
];
if (!availableTimes.length) return [];
let closestIndex = 0;
let minDiff = Math.abs(new Date(availableTimes[0].time) - currentTime);
availableTimes.forEach((entry, index) => {
const diff = Math.abs(new Date(entry.time) - currentTime);
if (diff < minDiff) {
minDiff = diff;
closestIndex = index;
}
});
return availableTimes.slice(closestIndex).map((hour) => ({
temperature: temperatureConverter(hour.temperature_2m),
temperatureUnit: temperatureConverter.units,
apparentTemperature: temperatureConverter(hour.apparent_temperature),
windSpeed: windConverter(hour.wind_speed_10m),
windUnit: windConverter.units,
windDirection: directionToNSEW(hour.wind_direction_10m ?? 0),
probabilityOfPrecipitation: Math.round(hour.precipitation_probability ?? 0),
skyCover: Math.round(hour.cloud_cover ?? 0),
icon: getSmallIconFromWmoCode(hour.weather_code, Boolean(hour.is_day)),
dewpoint: temperatureConverter(hour.dew_point_2m),
}));
};
const display = new Hourly(3, 'hourly', true);
registerDisplay(display);
export default display.getHourlyData.bind(display);