ws4kp-linhanced/server/scripts/modules/currentweather.mjs
mrkmntal 8958ef4d38
Some checks are pending
build-docker / Build Image (push) Waiting to run
feat: prefer NOAA data for US subject locations
2026-04-13 22:34:42 -04:00

220 lines
8.6 KiB
JavaScript

// current weather conditions display
import STATUS from './status.mjs';
import { preloadImg } from './utils/image.mjs';
import { directionToNSEW } from './utils/calc.mjs';
import WeatherDisplay from './weatherdisplay.mjs';
import { registerDisplay } from './navigation.mjs';
import {
temperature, windSpeed, pressure, distanceKilometers, distanceMeters,
} from './utils/units.mjs';
import { getConditionTextWithWind, getBestUsCurrentObservation } from './utils/weather.mjs';
import { getLargeIconFromWmoCodeWithWind } from './icons.mjs';
class CurrentWeather extends WeatherDisplay {
constructor(navId, elemId) {
super(navId, elemId, 'Current Conditions', true);
}
async getData(weatherParameters, refresh) {
const superResult = super.getData(weatherParameters, refresh);
this.data = await parseData(this.weatherParameters);
if (!this.data) {
if (this.isEnabled) this.setStatus(STATUS.failed);
this.getDataCallback(undefined);
return;
}
this.getDataCallback();
if (!superResult) return;
this.timing.totalScreens = 1;
preloadImg(this.data.Icon);
this.setStatus(STATUS.loaded);
}
async drawCanvas() {
super.drawCanvas();
let condition = getConditionTextWithWind(
this.data.TextConditions,
this.data.WindSpeedRaw,
this.data.WindGustRaw
);
if (condition.length > 23) {
condition = shortConditions(condition);
}
const wind = this.data.WindSpeed > 0
? this.data.WindDirection.padEnd(3, '') + this.data.WindSpeed.toString().padStart(3, ' ')
: 'Calm';
const isDefaultTheme = (document.documentElement.dataset.theme ?? 'default') === 'default';
const windText = !isDefaultTheme && this.data.WindGust > 0
? `${wind} - Gusts to ${this.data.WindGust}`
: wind;
const fill = {
temp: this.data.Temperature + String.fromCharCode(176),
condition,
wind: windText,
location: this.data.city.substr(0, 20),
humidity: `${this.data.Humidity}%`,
dewpoint: this.data.DewPoint + String.fromCharCode(176),
ceiling: this.data.Ceiling === 0 ? 'Unlimited' : `${this.data.Ceiling}${this.data.CeilingUnit}`,
visibility: `${this.data.Visibility}${this.data.VisibilityUnit}`,
pressure: `${this.data.Pressure} ${this.data.PressureDirection}`,
icon: { type: 'img', src: this.data.Icon },
};
if (isDefaultTheme && this.data.WindGust > 0) fill['wind-gusts'] = `Gusts to ${this.data.WindGust}`;
const area = this.elem.querySelector('.main');
area.innerHTML = '';
area.append(this.fillTemplate('weather', fill));
this.finishDraw();
}
async getCurrentWeather(stillWaiting) {
this.setAutoReload();
if (stillWaiting) this.stillWaitingCallbacks.push(stillWaiting);
return new Promise((resolve) => {
if (this.data) resolve({ data: this.data, parameters: this.weatherParameters });
this.getDataCallbacks.push(() => resolve({ data: this.data, parameters: this.weatherParameters }));
});
}
}
const shortConditions = (_condition) => {
let condition = _condition;
condition = condition.replace(/Light/g, 'L');
condition = condition.replace(/Heavy/g, 'H');
condition = condition.replace(/Partly/g, 'P');
condition = condition.replace(/Mostly/g, 'M');
condition = condition.replace(/Thunderstorm/g, 'T\'storm');
condition = condition.replace(/ and /g, ' ');
condition = condition.replace(/Freezing Rain/g, 'Frz Rn');
condition = condition.replace(/Freezing/g, 'Frz');
condition = condition.replace(/ with /g, '/');
return condition;
};
const getCurrentWeatherByHourFromTime = (data) => {
const currentTime = new Date();
const currentDateKey = currentTime.toLocaleDateString('en-CA', { timeZone: data.timeZone });
const availableTimes = data.forecast[currentDateKey]?.hours ?? Object.values(data.forecast)[0]?.hours ?? [];
if (availableTimes.length === 0) return null;
const closestTime = availableTimes.reduce((prev, curr) => {
const prevDiff = Math.abs(new Date(prev.time) - currentTime);
const currDiff = Math.abs(new Date(curr.time) - currentTime);
return currDiff < prevDiff ? curr : prev;
});
const threeHoursAgo = new Date(currentTime.getTime() - 3 * 60 * 60 * 1000);
const previousHour = availableTimes
.filter((entry) => new Date(entry.time) <= currentTime && new Date(entry.time) >= threeHoursAgo)
.reduce((prev, curr) => {
const prevDiff = Math.abs(new Date(prev.time) - threeHoursAgo);
const currDiff = Math.abs(new Date(curr.time) - threeHoursAgo);
return currDiff < prevDiff ? curr : prev;
}, availableTimes[0]);
const diff = (closestTime.pressure_msl ?? 0) - (previousHour.pressure_msl ?? 0);
let pressureTrend = 'Steady';
if (diff > 0.5) pressureTrend = 'Rising';
if (diff < -0.5) pressureTrend = 'Falling';
closestTime.pressureTrend = pressureTrend;
closestTime.uv_index_max = data.forecast[currentDateKey]?.uv_index_max ?? closestTime.uv_index ?? 0;
return closestTime;
};
const parseData = async (weatherParameters) => {
if (weatherParameters.supportsNoaaDisplays && weatherParameters.stationId) {
const observationResult = await getBestUsCurrentObservation(weatherParameters);
if (observationResult?.observation) {
weatherParameters.primaryObservationSource = observationResult.source;
const currentForecast = getCurrentWeatherByHourFromTime(weatherParameters) ?? {};
const observation = observationResult.observation;
const temperatureConverter = temperature();
const windConverter = windSpeed();
const pressureConverter = pressure();
const ceilingConverter = distanceMeters();
const visibilityConverter = distanceKilometers();
const ceilingMeters = Math.max(0, ((observation.temperature ?? 0) - (observation.dewPoint ?? 0)) * 68);
const pressureValue = observation.pressure ?? currentForecast.pressure_msl ?? null;
return {
city: weatherParameters.city,
timeZone: weatherParameters.timeZone,
Temperature: temperatureConverter(observation.temperature),
TemperatureUnit: temperatureConverter.units,
DewPoint: temperatureConverter(observation.dewPoint),
Ceiling: ceilingConverter(ceilingMeters),
CeilingUnit: ceilingConverter.units,
Visibility: visibilityConverter(observation.visibility),
VisibilityUnit: visibilityConverter.units,
WindSpeed: windConverter(observation.windSpeed),
WindSpeedRaw: observation.windSpeed,
WindDirection: directionToNSEW(observation.windDirection ?? 0),
Pressure: pressureValue === null ? '-' : pressureConverter(pressureValue * 100),
PressureDirection: currentForecast.pressureTrend ?? 'Steady',
Humidity: Math.round(observation.relativeHumidity ?? currentForecast.relative_humidity_2m ?? 0),
WindGust: windConverter(observation.windGust),
WindGustRaw: observation.windGust,
WindUnit: windConverter.units,
TextConditions: Number(observation.weatherCode ?? 0),
Icon: getLargeIconFromWmoCodeWithWind(
observation.weatherCode,
Boolean(observation.isDay),
observation.windSpeed,
observation.windGust
),
};
}
}
weatherParameters.primaryObservationSource = 'forecast';
const currentForecast = getCurrentWeatherByHourFromTime(weatherParameters);
if (!currentForecast) return null;
const temperatureConverter = temperature();
const windConverter = windSpeed();
const pressureConverter = pressure();
const ceilingConverter = distanceMeters();
const visibilityConverter = distanceKilometers();
const ceilingMeters = Math.max(0, ((currentForecast.temperature_2m ?? 0) - (currentForecast.dew_point_2m ?? 0)) * 68);
return {
city: weatherParameters.city,
timeZone: weatherParameters.timeZone,
Temperature: temperatureConverter(currentForecast.temperature_2m),
TemperatureUnit: temperatureConverter.units,
DewPoint: temperatureConverter(currentForecast.dew_point_2m),
Ceiling: ceilingConverter(ceilingMeters),
CeilingUnit: ceilingConverter.units,
Visibility: visibilityConverter(currentForecast.visibility),
VisibilityUnit: visibilityConverter.units,
WindSpeed: windConverter(currentForecast.wind_speed_10m),
WindSpeedRaw: currentForecast.wind_speed_10m,
WindDirection: directionToNSEW(currentForecast.wind_direction_10m ?? 0),
Pressure: pressureConverter((currentForecast.pressure_msl ?? 0) * 100),
PressureDirection: currentForecast.pressureTrend,
Humidity: Math.round(currentForecast.relative_humidity_2m ?? 0),
WindGust: windConverter(currentForecast.wind_gusts_10m),
WindGustRaw: currentForecast.wind_gusts_10m,
WindUnit: windConverter.units,
TextConditions: Number(currentForecast.weather_code ?? 0),
Icon: getLargeIconFromWmoCodeWithWind(
currentForecast.weather_code,
Boolean(currentForecast.is_day),
currentForecast.wind_speed_10m,
currentForecast.wind_gusts_10m
),
};
};
const display = new CurrentWeather(1, 'current-weather');
registerDisplay(display);
export default display.getCurrentWeather.bind(display);