2020-09-04 18:02:20 +00:00
|
|
|
// current weather conditions display
|
2022-11-22 22:19:10 +00:00
|
|
|
import STATUS from './status.mjs';
|
|
|
|
import { loadImg, preloadImg } from './utils/image.mjs';
|
|
|
|
import { json } from './utils/fetch.mjs';
|
|
|
|
import { directionToNSEW } from './utils/calc.mjs';
|
|
|
|
import { locationCleanup } from './utils/string.mjs';
|
|
|
|
import { getWeatherIconFromIconLink } from './icons.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';
|
2022-12-06 22:25:28 +00:00
|
|
|
import {
|
|
|
|
celsiusToFahrenheit, kphToMph, pascalToInHg, metersToFeet, kilometersToMiles,
|
|
|
|
} from './utils/units.mjs';
|
2020-09-04 18:02:20 +00:00
|
|
|
|
2022-12-13 22:31:18 +00:00
|
|
|
// some stations prefixed do not provide all the necessary data
|
|
|
|
const skipStations = ['U', 'C', 'H', 'W', 'Y', 'T', 'S', 'M', 'O', 'L', 'A', 'F', 'B', 'N', 'V', 'R', 'D', 'E', 'I', 'G', 'J'];
|
|
|
|
|
2020-09-04 18:02:20 +00:00
|
|
|
class CurrentWeather extends WeatherDisplay {
|
2020-10-29 21:44:28 +00:00
|
|
|
constructor(navId, elemId) {
|
2022-11-22 03:50:22 +00:00
|
|
|
super(navId, elemId, 'Current Conditions', true);
|
2020-09-04 18:02:20 +00:00
|
|
|
// pre-load background image (returns promise)
|
2022-11-22 22:19:10 +00:00
|
|
|
this.backgroundImage = loadImg('images/BackGround1_1.png');
|
2020-09-04 18:02:20 +00:00
|
|
|
}
|
|
|
|
|
2020-10-29 21:44:28 +00:00
|
|
|
async getData(_weatherParameters) {
|
2022-12-12 19:53:33 +00:00
|
|
|
// always load the data for use in the lower scroll
|
|
|
|
const superResult = super.getData(_weatherParameters);
|
2020-10-29 21:44:28 +00:00
|
|
|
const weatherParameters = _weatherParameters ?? this.weatherParameters;
|
2020-09-25 14:55:29 +00:00
|
|
|
|
2022-12-13 22:31:18 +00:00
|
|
|
// filter for 4-letter observation stations, only those contain sky conditions and thus an icon
|
|
|
|
const filteredStations = weatherParameters.stations.filter((station) => station?.properties?.stationIdentifier?.length === 4 && !skipStations.includes(station.properties.stationIdentifier.slice(0, 1)));
|
|
|
|
|
2020-09-04 18:02:20 +00:00
|
|
|
// Load the observations
|
2023-01-17 17:26:57 +00:00
|
|
|
let observations;
|
|
|
|
let station;
|
|
|
|
|
2020-10-16 20:16:46 +00:00
|
|
|
// station number counter
|
|
|
|
let stationNum = 0;
|
2022-12-13 22:31:18 +00:00
|
|
|
while (!observations && stationNum < filteredStations.length) {
|
2020-10-16 20:16:46 +00:00
|
|
|
// get the station
|
2022-12-13 22:31:18 +00:00
|
|
|
station = filteredStations[stationNum];
|
2020-10-29 21:44:28 +00:00
|
|
|
stationNum += 1;
|
2020-10-16 20:16:46 +00:00
|
|
|
try {
|
|
|
|
// station observations
|
2022-03-01 21:54:19 +00:00
|
|
|
// eslint-disable-next-line no-await-in-loop
|
2022-11-22 22:19:10 +00:00
|
|
|
observations = await json(`${station.id}/observations`, {
|
2020-10-16 20:16:46 +00:00
|
|
|
cors: true,
|
|
|
|
data: {
|
|
|
|
limit: 2,
|
|
|
|
},
|
2022-12-12 19:53:33 +00:00
|
|
|
retryCount: 3,
|
|
|
|
stillWaiting: () => this.stillWaiting(),
|
2020-10-16 20:16:46 +00:00
|
|
|
});
|
2020-09-04 18:02:20 +00:00
|
|
|
|
2020-10-16 20:16:46 +00:00
|
|
|
// test data quality
|
2020-10-29 21:44:28 +00:00
|
|
|
if (observations.features[0].properties.temperature.value === null
|
|
|
|
|| observations.features[0].properties.windSpeed.value === null
|
2023-01-05 20:19:33 +00:00
|
|
|
|| observations.features[0].properties.textDescription === null
|
|
|
|
|| observations.features[0].properties.textDescription === ''
|
|
|
|
|| observations.features[0].properties.icon === null) {
|
2020-10-16 20:16:46 +00:00
|
|
|
observations = undefined;
|
|
|
|
throw new Error(`Unable to get observations: ${station.properties.stationIdentifier}, trying next station`);
|
|
|
|
}
|
2023-01-06 20:39:39 +00:00
|
|
|
} catch (error) {
|
|
|
|
console.error(error);
|
2020-10-16 20:16:46 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
// test for data received
|
|
|
|
if (!observations) {
|
|
|
|
console.error('All current weather stations exhausted');
|
2022-12-14 19:08:49 +00:00
|
|
|
if (this.isEnabled) this.setStatus(STATUS.failed);
|
2022-12-08 20:41:15 +00:00
|
|
|
// send failed to subscribers
|
|
|
|
this.getDataCallback(undefined);
|
2020-09-04 18:02:20 +00:00
|
|
|
return;
|
|
|
|
}
|
2020-09-23 16:49:15 +00:00
|
|
|
|
2020-09-04 18:02:20 +00:00
|
|
|
// we only get here if there was no error above
|
2023-01-17 17:26:57 +00:00
|
|
|
this.data = parseData({ ...observations, station });
|
2020-10-21 01:04:51 +00:00
|
|
|
this.getDataCallback();
|
2022-12-12 19:53:33 +00:00
|
|
|
|
|
|
|
// stop here if we're disabled
|
|
|
|
if (!superResult) return;
|
|
|
|
|
|
|
|
// preload the icon
|
|
|
|
preloadImg(getWeatherIconFromIconLink(observations.features[0].properties.icon));
|
|
|
|
this.setStatus(STATUS.loaded);
|
2020-09-04 18:02:20 +00:00
|
|
|
}
|
|
|
|
|
2020-10-29 21:44:28 +00:00
|
|
|
async drawCanvas() {
|
2020-09-25 03:44:51 +00:00
|
|
|
super.drawCanvas();
|
2020-09-04 18:02:20 +00:00
|
|
|
|
2023-01-17 17:26:57 +00:00
|
|
|
let condition = this.data.observations.textDescription;
|
|
|
|
if (condition.length > 15) {
|
|
|
|
condition = shortConditions(condition);
|
2020-09-04 18:02:20 +00:00
|
|
|
}
|
2022-08-03 02:39:27 +00:00
|
|
|
|
2023-01-17 17:26:57 +00:00
|
|
|
const fill = {
|
|
|
|
temp: this.data.Temperature + String.fromCharCode(176),
|
|
|
|
condition,
|
|
|
|
wind: this.data.WindDirection.padEnd(3, '') + this.data.WindSpeed.toString().padStart(3, ' '),
|
|
|
|
location: locationCleanup(this.data.station.properties.name).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 (this.data.WindGust) fill['wind-gusts'] = `Gusts to ${this.data.WindGust}`;
|
|
|
|
|
|
|
|
if (this.data.observations.heatIndex.value && this.data.HeatIndex !== this.data.Temperature) {
|
2022-08-03 02:39:27 +00:00
|
|
|
fill['heat-index-label'] = 'Heat Index:';
|
2023-01-17 17:26:57 +00:00
|
|
|
fill['heat-index'] = this.data.HeatIndex + String.fromCharCode(176);
|
|
|
|
} else if (this.data.observations.windChill.value && this.data.WindChill !== '' && this.data.WindChill < this.data.Temperature) {
|
2022-08-03 02:39:27 +00:00
|
|
|
fill['heat-index-label'] = 'Wind Chill:';
|
2023-01-17 17:26:57 +00:00
|
|
|
fill['heat-index'] = this.data.WindChill + String.fromCharCode(176);
|
2020-09-04 18:02:20 +00:00
|
|
|
}
|
2020-09-04 18:38:58 +00:00
|
|
|
|
2022-08-03 02:39:27 +00:00
|
|
|
const area = this.elem.querySelector('.main');
|
|
|
|
|
|
|
|
area.innerHTML = '';
|
|
|
|
area.append(this.fillTemplate('weather', fill));
|
2020-09-04 18:38:58 +00:00
|
|
|
|
2020-09-04 18:02:20 +00:00
|
|
|
this.finishDraw();
|
|
|
|
}
|
|
|
|
|
2020-10-21 01:04:51 +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 getCurrentWeather(stillWaiting) {
|
|
|
|
if (stillWaiting) this.stillWaitingCallbacks.push(stillWaiting);
|
2020-10-21 01:04:51 +00:00
|
|
|
return new Promise((resolve) => {
|
2023-01-17 17:26:57 +00:00
|
|
|
if (this.data) resolve(this.data);
|
2020-10-21 01:04:51 +00:00
|
|
|
// data not available, put it into the data callback queue
|
2023-01-17 17:26:57 +00:00
|
|
|
this.getDataCallbacks.push(() => resolve(this.data));
|
2020-10-21 01:04:51 +00:00
|
|
|
});
|
2020-09-25 03:44:51 +00:00
|
|
|
}
|
2020-10-29 21:44:28 +00:00
|
|
|
}
|
2022-12-09 19:51:51 +00:00
|
|
|
|
|
|
|
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(/Few/g, 'F');
|
|
|
|
condition = condition.replace(/Thunderstorm/g, 'T\'storm');
|
|
|
|
condition = condition.replace(/ in /g, '');
|
|
|
|
condition = condition.replace(/Vicinity/g, '');
|
|
|
|
condition = condition.replace(/ and /g, ' ');
|
|
|
|
condition = condition.replace(/Freezing Rain/g, 'Frz Rn');
|
|
|
|
condition = condition.replace(/Freezing/g, 'Frz');
|
|
|
|
condition = condition.replace(/Unknown Precip/g, '');
|
|
|
|
condition = condition.replace(/L Snow Fog/g, 'L Snw/Fog');
|
|
|
|
condition = condition.replace(/ with /g, '/');
|
|
|
|
return condition;
|
|
|
|
};
|
|
|
|
|
2023-01-17 17:26:57 +00:00
|
|
|
// format the received data
|
|
|
|
const parseData = (data) => {
|
|
|
|
const observations = data.features[0].properties;
|
|
|
|
// values from api are provided in metric
|
|
|
|
data.observations = observations;
|
|
|
|
data.Temperature = Math.round(observations.temperature.value);
|
|
|
|
data.TemperatureUnit = 'C';
|
|
|
|
data.DewPoint = Math.round(observations.dewpoint.value);
|
|
|
|
data.Ceiling = Math.round(observations.cloudLayers[0]?.base?.value ?? 0);
|
|
|
|
data.CeilingUnit = 'm.';
|
|
|
|
data.Visibility = Math.round(observations.visibility.value / 1000);
|
|
|
|
data.VisibilityUnit = ' km.';
|
|
|
|
data.WindSpeed = Math.round(observations.windSpeed.value);
|
|
|
|
data.WindDirection = directionToNSEW(observations.windDirection.value);
|
|
|
|
data.Pressure = Math.round(observations.barometricPressure.value);
|
|
|
|
data.HeatIndex = Math.round(observations.heatIndex.value);
|
|
|
|
data.WindChill = Math.round(observations.windChill.value);
|
|
|
|
data.WindGust = Math.round(observations.windGust.value);
|
|
|
|
data.WindUnit = 'KPH';
|
|
|
|
data.Humidity = Math.round(observations.relativeHumidity.value);
|
|
|
|
data.Icon = getWeatherIconFromIconLink(observations.icon);
|
|
|
|
data.PressureDirection = '';
|
|
|
|
data.TextConditions = observations.textDescription;
|
|
|
|
|
|
|
|
// difference since last measurement (pascals, looking for difference of more than 150)
|
|
|
|
const pressureDiff = (observations.barometricPressure.value - data.features[1].properties.barometricPressure.value);
|
|
|
|
if (pressureDiff > 150) data.PressureDirection = 'R';
|
|
|
|
if (pressureDiff < -150) data.PressureDirection = 'F';
|
|
|
|
|
|
|
|
// convert to us units
|
|
|
|
data.Temperature = celsiusToFahrenheit(data.Temperature);
|
|
|
|
data.TemperatureUnit = 'F';
|
|
|
|
data.DewPoint = celsiusToFahrenheit(data.DewPoint);
|
|
|
|
data.Ceiling = Math.round(metersToFeet(data.Ceiling) / 100) * 100;
|
|
|
|
data.CeilingUnit = 'ft.';
|
|
|
|
data.Visibility = kilometersToMiles(observations.visibility.value / 1000);
|
|
|
|
data.VisibilityUnit = ' mi.';
|
|
|
|
data.WindSpeed = kphToMph(data.WindSpeed);
|
|
|
|
data.WindUnit = 'MPH';
|
|
|
|
data.Pressure = pascalToInHg(data.Pressure).toFixed(2);
|
|
|
|
data.HeatIndex = celsiusToFahrenheit(data.HeatIndex);
|
|
|
|
data.WindChill = celsiusToFahrenheit(data.WindChill);
|
|
|
|
data.WindGust = kphToMph(data.WindGust);
|
|
|
|
return data;
|
|
|
|
};
|
|
|
|
|
2022-12-14 22:28:33 +00:00
|
|
|
const display = new CurrentWeather(1, 'current-weather');
|
2022-12-06 22:14:56 +00:00
|
|
|
registerDisplay(display);
|
|
|
|
|
|
|
|
export default display.getCurrentWeather.bind(display);
|