mirror of
https://github.com/netbymatt/ws4kp.git
synced 2026-04-14 15:49:31 -07:00
weather displays complete
This commit is contained in:
@@ -1,20 +1,22 @@
|
||||
// display sun and moon data
|
||||
import { loadImg, preloadImg } from './utils/image.mjs';
|
||||
import { DateTime } from '../vendor/auto/luxon.mjs';
|
||||
import STATUS from './status.mjs';
|
||||
|
||||
/* globals WeatherDisplay, utils, STATUS, SunCalc, luxon */
|
||||
/* globals WeatherDisplay, SunCalc */
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
class Almanac extends WeatherDisplay {
|
||||
constructor(navId, elemId) {
|
||||
super(navId, elemId, 'Almanac', true);
|
||||
|
||||
// pre-load background images (returns promises)
|
||||
this.backgroundImage0 = utils.image.load('images/BackGround3_1.png');
|
||||
this.backgroundImage0 = loadImg('images/BackGround3_1.png');
|
||||
|
||||
// preload the moon images
|
||||
utils.image.preload('images/2/Full-Moon.gif');
|
||||
utils.image.preload('images/2/Last-Quarter.gif');
|
||||
utils.image.preload('images/2/New-Moon.gif');
|
||||
utils.image.preload('images/2/First-Quarter.gif');
|
||||
preloadImg('images/2/Full-Moon.gif');
|
||||
preloadImg('images/2/Last-Quarter.gif');
|
||||
preloadImg('images/2/New-Moon.gif');
|
||||
preloadImg('images/2/First-Quarter.gif');
|
||||
|
||||
this.timing.totalScreens = 1;
|
||||
}
|
||||
@@ -39,8 +41,6 @@ class Almanac extends WeatherDisplay {
|
||||
}
|
||||
|
||||
calcSunMoonData(weatherParameters) {
|
||||
const { DateTime } = luxon;
|
||||
|
||||
const sun = [
|
||||
SunCalc.getTimes(new Date(), weatherParameters.latitude, weatherParameters.longitude),
|
||||
SunCalc.getTimes(DateTime.local().plus({ days: 1 }).toJSDate(), weatherParameters.latitude, weatherParameters.longitude),
|
||||
@@ -115,7 +115,6 @@ class Almanac extends WeatherDisplay {
|
||||
async drawCanvas() {
|
||||
super.drawCanvas();
|
||||
const info = this.data;
|
||||
const { DateTime } = luxon;
|
||||
const Today = DateTime.local();
|
||||
const Tomorrow = Today.plus({ days: 1 });
|
||||
|
||||
@@ -170,3 +169,7 @@ class Almanac extends WeatherDisplay {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default Almanac;
|
||||
|
||||
window.Almanac = Almanac;
|
||||
@@ -4,8 +4,8 @@ const UNITS = {
|
||||
};
|
||||
|
||||
export {
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
UNITS,
|
||||
};
|
||||
|
||||
window.UNITS = UNITS;
|
||||
console.log('config');
|
||||
|
||||
@@ -1,12 +1,20 @@
|
||||
// current weather conditions display
|
||||
/* globals WeatherDisplay, utils, STATUS, icons, UNITS, navigation */
|
||||
import STATUS from './status.mjs';
|
||||
import { UNITS } from './config.mjs';
|
||||
import { loadImg, preloadImg } from './utils/image.mjs';
|
||||
import { json } from './utils/fetch.mjs';
|
||||
import { directionToNSEW } from './utils/calc.mjs';
|
||||
import * as units from './utils/units.mjs';
|
||||
import { locationCleanup } from './utils/string.mjs';
|
||||
import { getWeatherIconFromIconLink } from './icons.mjs';
|
||||
|
||||
/* globals WeatherDisplay, navigation */
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
class CurrentWeather extends WeatherDisplay {
|
||||
constructor(navId, elemId) {
|
||||
super(navId, elemId, 'Current Conditions', true);
|
||||
// pre-load background image (returns promise)
|
||||
this.backgroundImage = utils.image.load('images/BackGround1_1.png');
|
||||
this.backgroundImage = loadImg('images/BackGround1_1.png');
|
||||
}
|
||||
|
||||
async getData(_weatherParameters) {
|
||||
@@ -25,7 +33,7 @@ class CurrentWeather extends WeatherDisplay {
|
||||
try {
|
||||
// station observations
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
observations = await utils.fetch.json(`${station.id}/observations`, {
|
||||
observations = await json(`${station.id}/observations`, {
|
||||
cors: true,
|
||||
data: {
|
||||
limit: 2,
|
||||
@@ -50,7 +58,7 @@ class CurrentWeather extends WeatherDisplay {
|
||||
return;
|
||||
}
|
||||
// preload the icon
|
||||
utils.image.preload(icons.getWeatherIconFromIconLink(observations.features[0].properties.icon));
|
||||
preloadImg(getWeatherIconFromIconLink(observations.features[0].properties.icon));
|
||||
|
||||
// we only get here if there was no error above
|
||||
this.data = { ...observations, station };
|
||||
@@ -74,14 +82,14 @@ class CurrentWeather extends WeatherDisplay {
|
||||
data.Visibility = Math.round(observations.visibility.value / 1000);
|
||||
data.VisibilityUnit = ' km.';
|
||||
data.WindSpeed = Math.round(observations.windSpeed.value);
|
||||
data.WindDirection = utils.calc.directionToNSEW(observations.windDirection.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 = icons.getWeatherIconFromIconLink(observations.icon);
|
||||
data.Icon = getWeatherIconFromIconLink(observations.icon);
|
||||
data.PressureDirection = '';
|
||||
data.TextConditions = observations.textDescription;
|
||||
data.station = this.data.station;
|
||||
@@ -92,19 +100,19 @@ class CurrentWeather extends WeatherDisplay {
|
||||
if (pressureDiff < -150) data.PressureDirection = 'F';
|
||||
|
||||
if (navigation.units() === UNITS.english) {
|
||||
data.Temperature = utils.units.celsiusToFahrenheit(data.Temperature);
|
||||
data.Temperature = units.celsiusToFahrenheit(data.Temperature);
|
||||
data.TemperatureUnit = 'F';
|
||||
data.DewPoint = utils.units.celsiusToFahrenheit(data.DewPoint);
|
||||
data.Ceiling = Math.round(utils.units.metersToFeet(data.Ceiling) / 100) * 100;
|
||||
data.DewPoint = units.celsiusToFahrenheit(data.DewPoint);
|
||||
data.Ceiling = Math.round(units.metersToFeet(data.Ceiling) / 100) * 100;
|
||||
data.CeilingUnit = 'ft.';
|
||||
data.Visibility = utils.units.kilometersToMiles(observations.visibility.value / 1000);
|
||||
data.Visibility = units.kilometersToMiles(observations.visibility.value / 1000);
|
||||
data.VisibilityUnit = ' mi.';
|
||||
data.WindSpeed = utils.units.kphToMph(data.WindSpeed);
|
||||
data.WindSpeed = units.kphToMph(data.WindSpeed);
|
||||
data.WindUnit = 'MPH';
|
||||
data.Pressure = utils.units.pascalToInHg(data.Pressure).toFixed(2);
|
||||
data.HeatIndex = utils.units.celsiusToFahrenheit(data.HeatIndex);
|
||||
data.WindChill = utils.units.celsiusToFahrenheit(data.WindChill);
|
||||
data.WindGust = utils.units.kphToMph(data.WindGust);
|
||||
data.Pressure = units.pascalToInHg(data.Pressure).toFixed(2);
|
||||
data.HeatIndex = units.celsiusToFahrenheit(data.HeatIndex);
|
||||
data.WindChill = units.celsiusToFahrenheit(data.WindChill);
|
||||
data.WindGust = units.kphToMph(data.WindGust);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
@@ -126,7 +134,7 @@ class CurrentWeather extends WeatherDisplay {
|
||||
fill.wind = data.WindDirection.padEnd(3, '') + data.WindSpeed.toString().padStart(3, ' ');
|
||||
if (data.WindGust) fill['wind-gusts'] = `Gusts to ${data.WindGust}`;
|
||||
|
||||
fill.location = utils.string.locationCleanup(this.data.station.properties.name).substr(0, 20);
|
||||
fill.location = locationCleanup(this.data.station.properties.name).substr(0, 20);
|
||||
|
||||
fill.humidity = `${data.Humidity}%`;
|
||||
fill.dewpoint = data.DewPoint + String.fromCharCode(176);
|
||||
@@ -181,3 +189,7 @@ class CurrentWeather extends WeatherDisplay {
|
||||
return condition;
|
||||
}
|
||||
}
|
||||
|
||||
export default CurrentWeather;
|
||||
|
||||
window.CurrentWeather = CurrentWeather;
|
||||
@@ -1,101 +0,0 @@
|
||||
/* globals navigation, utils */
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const currentWeatherScroll = (() => {
|
||||
// constants
|
||||
const degree = String.fromCharCode(176);
|
||||
|
||||
// local variables
|
||||
let interval;
|
||||
let screenIndex = 0;
|
||||
|
||||
// start drawing conditions
|
||||
// reset starts from the first item in the text scroll list
|
||||
const start = () => {
|
||||
// store see if the context is new
|
||||
|
||||
// set up the interval if needed
|
||||
if (!interval) {
|
||||
interval = setInterval(incrementInterval, 4000);
|
||||
}
|
||||
|
||||
// draw the data
|
||||
drawScreen();
|
||||
};
|
||||
|
||||
const stop = (reset) => {
|
||||
if (interval) interval = clearInterval(interval);
|
||||
if (reset) screenIndex = 0;
|
||||
};
|
||||
|
||||
// increment interval, roll over
|
||||
const incrementInterval = () => {
|
||||
screenIndex = (screenIndex + 1) % (screens.length);
|
||||
// draw new text
|
||||
drawScreen();
|
||||
};
|
||||
|
||||
const drawScreen = async () => {
|
||||
// get the conditions
|
||||
const data = await navigation.getCurrentWeather();
|
||||
|
||||
// nothing to do if there's no data yet
|
||||
if (!data) return;
|
||||
|
||||
drawCondition(screens[screenIndex](data));
|
||||
};
|
||||
|
||||
// the "screens" are stored in an array for easy addition and removal
|
||||
const screens = [
|
||||
// station name
|
||||
(data) => `Conditions at ${utils.string.locationCleanup(data.station.properties.name).substr(0, 20)}`,
|
||||
|
||||
// temperature
|
||||
(data) => {
|
||||
let text = `Temp: ${data.Temperature}${degree} ${data.TemperatureUnit}`;
|
||||
if (data.observations.heatIndex.value) {
|
||||
text += ` Heat Index: ${data.HeatIndex}${degree} ${data.TemperatureUnit}`;
|
||||
} else if (data.observations.windChill.value) {
|
||||
text += ` Wind Chill: ${data.WindChill}${degree} ${data.TemperatureUnit}`;
|
||||
}
|
||||
return text;
|
||||
},
|
||||
|
||||
// humidity
|
||||
(data) => `Humidity: ${data.Humidity}${degree} ${data.TemperatureUnit} Dewpoint: ${data.DewPoint}${degree} ${data.TemperatureUnit}`,
|
||||
|
||||
// barometric pressure
|
||||
(data) => `Barometric Pressure: ${data.Pressure} ${data.PressureDirection}`,
|
||||
|
||||
// wind
|
||||
(data) => {
|
||||
let text = '';
|
||||
if (data.WindSpeed > 0) {
|
||||
text = `Wind: ${data.WindDirection} ${data.WindSpeed} ${data.WindUnit}`;
|
||||
} else {
|
||||
text = 'Wind: Calm';
|
||||
}
|
||||
if (data.WindGust > 0) {
|
||||
text += ` Gusts to ${data.WindGust}`;
|
||||
}
|
||||
return text;
|
||||
},
|
||||
|
||||
// visibility
|
||||
(data) => `Visib: ${data.Visibility} ${data.VisibilityUnit} Ceiling: ${data.Ceiling === 0 ? 'Unlimited' : `${data.Ceiling} ${data.CeilingUnit}`}`,
|
||||
];
|
||||
|
||||
// internal draw function with preset parameters
|
||||
const drawCondition = (text) => {
|
||||
// update all html scroll elements
|
||||
utils.elem.forEach('.weather-display .scroll .fixed', (elem) => {
|
||||
elem.innerHTML = text;
|
||||
});
|
||||
};
|
||||
|
||||
// return the api
|
||||
return {
|
||||
start,
|
||||
stop,
|
||||
};
|
||||
})();
|
||||
105
server/scripts/modules/currentweatherscroll.mjs
Normal file
105
server/scripts/modules/currentweatherscroll.mjs
Normal file
@@ -0,0 +1,105 @@
|
||||
/* globals navigation */
|
||||
import { locationCleanup } from './utils/string.mjs';
|
||||
import { elemForEach } from './utils/elem.mjs';
|
||||
|
||||
// constants
|
||||
const degree = String.fromCharCode(176);
|
||||
|
||||
// local variables
|
||||
let interval;
|
||||
let screenIndex = 0;
|
||||
|
||||
// start drawing conditions
|
||||
// reset starts from the first item in the text scroll list
|
||||
const start = () => {
|
||||
// store see if the context is new
|
||||
|
||||
// set up the interval if needed
|
||||
if (!interval) {
|
||||
interval = setInterval(incrementInterval, 4000);
|
||||
}
|
||||
|
||||
// draw the data
|
||||
drawScreen();
|
||||
};
|
||||
|
||||
const stop = (reset) => {
|
||||
if (interval) interval = clearInterval(interval);
|
||||
if (reset) screenIndex = 0;
|
||||
};
|
||||
|
||||
// increment interval, roll over
|
||||
const incrementInterval = () => {
|
||||
screenIndex = (screenIndex + 1) % (screens.length);
|
||||
// draw new text
|
||||
drawScreen();
|
||||
};
|
||||
|
||||
const drawScreen = async () => {
|
||||
// get the conditions
|
||||
const data = await navigation.getCurrentWeather();
|
||||
|
||||
// nothing to do if there's no data yet
|
||||
if (!data) return;
|
||||
|
||||
drawCondition(screens[screenIndex](data));
|
||||
};
|
||||
|
||||
// the "screens" are stored in an array for easy addition and removal
|
||||
const screens = [
|
||||
// station name
|
||||
(data) => `Conditions at ${locationCleanup(data.station.properties.name).substr(0, 20)}`,
|
||||
|
||||
// temperature
|
||||
(data) => {
|
||||
let text = `Temp: ${data.Temperature}${degree} ${data.TemperatureUnit}`;
|
||||
if (data.observations.heatIndex.value) {
|
||||
text += ` Heat Index: ${data.HeatIndex}${degree} ${data.TemperatureUnit}`;
|
||||
} else if (data.observations.windChill.value) {
|
||||
text += ` Wind Chill: ${data.WindChill}${degree} ${data.TemperatureUnit}`;
|
||||
}
|
||||
return text;
|
||||
},
|
||||
|
||||
// humidity
|
||||
(data) => `Humidity: ${data.Humidity}${degree} ${data.TemperatureUnit} Dewpoint: ${data.DewPoint}${degree} ${data.TemperatureUnit}`,
|
||||
|
||||
// barometric pressure
|
||||
(data) => `Barometric Pressure: ${data.Pressure} ${data.PressureDirection}`,
|
||||
|
||||
// wind
|
||||
(data) => {
|
||||
let text = '';
|
||||
if (data.WindSpeed > 0) {
|
||||
text = `Wind: ${data.WindDirection} ${data.WindSpeed} ${data.WindUnit}`;
|
||||
} else {
|
||||
text = 'Wind: Calm';
|
||||
}
|
||||
if (data.WindGust > 0) {
|
||||
text += ` Gusts to ${data.WindGust}`;
|
||||
}
|
||||
return text;
|
||||
},
|
||||
|
||||
// visibility
|
||||
(data) => `Visib: ${data.Visibility} ${data.VisibilityUnit} Ceiling: ${data.Ceiling === 0 ? 'Unlimited' : `${data.Ceiling} ${data.CeilingUnit}`}`,
|
||||
];
|
||||
|
||||
// internal draw function with preset parameters
|
||||
const drawCondition = (text) => {
|
||||
// update all html scroll elements
|
||||
elemForEach('.weather-display .scroll .fixed', (elem) => {
|
||||
elem.innerHTML = text;
|
||||
});
|
||||
};
|
||||
|
||||
// return the api
|
||||
export {
|
||||
start,
|
||||
stop,
|
||||
};
|
||||
|
||||
window.currentWeatherScroll = {
|
||||
start,
|
||||
stop,
|
||||
};
|
||||
@@ -1,100 +0,0 @@
|
||||
// drawing functionality and constants
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const draw = (() => {
|
||||
const horizontalGradient = (context, x1, y1, x2, y2, color1, color2) => {
|
||||
const linearGradient = context.createLinearGradient(0, y1, 0, y2);
|
||||
linearGradient.addColorStop(0, color1);
|
||||
linearGradient.addColorStop(0.4, color2);
|
||||
linearGradient.addColorStop(0.6, color2);
|
||||
linearGradient.addColorStop(1, color1);
|
||||
context.fillStyle = linearGradient;
|
||||
context.fillRect(x1, y1, x2 - x1, y2 - y1);
|
||||
};
|
||||
|
||||
const horizontalGradientSingle = (context, x1, y1, x2, y2, color1, color2) => {
|
||||
const linearGradient = context.createLinearGradient(0, y1, 0, y2);
|
||||
linearGradient.addColorStop(0, color1);
|
||||
linearGradient.addColorStop(1, color2);
|
||||
context.fillStyle = linearGradient;
|
||||
context.fillRect(x1, y1, x2 - x1, y2 - y1);
|
||||
};
|
||||
|
||||
const triangle = (context, color, x1, y1, x2, y2, x3, y3) => {
|
||||
context.fillStyle = color;
|
||||
context.beginPath();
|
||||
context.moveTo(x1, y1);
|
||||
context.lineTo(x2, y2);
|
||||
context.lineTo(x3, y3);
|
||||
context.fill();
|
||||
};
|
||||
|
||||
const titleText = (context, title1, title2) => {
|
||||
const font = 'Star4000';
|
||||
const size = '24pt';
|
||||
const color = '#ffff00';
|
||||
const shadow = 3;
|
||||
const x = 170;
|
||||
let y = 55;
|
||||
|
||||
if (title2) {
|
||||
text(context, font, size, color, x, y, title1, shadow); y += 30;
|
||||
text(context, font, size, color, x, y, title2, shadow); y += 30;
|
||||
} else {
|
||||
y += 15;
|
||||
text(context, font, size, color, x, y, title1, shadow); y += 30;
|
||||
}
|
||||
};
|
||||
|
||||
const text = (context, font, size, color, x, y, myText, shadow = 0, align = 'start') => {
|
||||
context.textAlign = align;
|
||||
context.font = `${size} '${font}'`;
|
||||
context.shadowColor = '#000000';
|
||||
context.shadowOffsetX = shadow;
|
||||
context.shadowOffsetY = shadow;
|
||||
context.strokeStyle = '#000000';
|
||||
context.lineWidth = 2;
|
||||
context.strokeText(myText, x, y);
|
||||
context.fillStyle = color;
|
||||
context.fillText(myText, x, y);
|
||||
context.fillStyle = '';
|
||||
context.strokeStyle = '';
|
||||
context.shadowOffsetX = 0;
|
||||
context.shadowOffsetY = 0;
|
||||
};
|
||||
|
||||
const box = (context, color, x, y, width, height) => {
|
||||
context.fillStyle = color;
|
||||
context.fillRect(x, y, width, height);
|
||||
};
|
||||
|
||||
const border = (context, color, lineWith, x, y, width, height) => {
|
||||
context.strokeStyle = color;
|
||||
context.lineWidth = lineWith;
|
||||
context.strokeRect(x, y, width, height);
|
||||
};
|
||||
|
||||
const theme = 1; // classic
|
||||
const topColor1 = 'rgb(192, 91, 2)';
|
||||
const topColor2 = 'rgb(72, 34, 64)';
|
||||
const sideColor1 = 'rgb(46, 18, 80)';
|
||||
const sideColor2 = 'rgb(192, 91, 2)';
|
||||
|
||||
return {
|
||||
// methods
|
||||
horizontalGradient,
|
||||
horizontalGradientSingle,
|
||||
triangle,
|
||||
titleText,
|
||||
text,
|
||||
box,
|
||||
border,
|
||||
|
||||
// constant-ish
|
||||
theme,
|
||||
topColor1,
|
||||
topColor2,
|
||||
sideColor1,
|
||||
sideColor2,
|
||||
};
|
||||
})();
|
||||
@@ -1,9 +1,16 @@
|
||||
// display extended forecast graphically
|
||||
// technically uses the same data as the local forecast, we'll let the browser do the caching of that
|
||||
|
||||
/* globals WeatherDisplay, utils, STATUS, UNITS, icons, navigation, luxon */
|
||||
import STATUS from './status.mjs';
|
||||
import { UNITS } from './config.mjs';
|
||||
import { json } from './utils/fetch.mjs';
|
||||
import { DateTime } from '../vendor/auto/luxon.mjs';
|
||||
import { fahrenheitToCelsius } from './utils/units.mjs';
|
||||
import { getWeatherIconFromIconLink } from './icons.mjs';
|
||||
import { preloadImg } from './utils/image.mjs';
|
||||
|
||||
/* globals WeatherDisplay, navigation */
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
class ExtendedForecast extends WeatherDisplay {
|
||||
constructor(navId, elemId) {
|
||||
super(navId, elemId, 'Extended Forecast', true);
|
||||
@@ -21,7 +28,7 @@ class ExtendedForecast extends WeatherDisplay {
|
||||
if (navigation.units() === UNITS.metric) units = 'si';
|
||||
let forecast;
|
||||
try {
|
||||
forecast = await utils.fetch.json(weatherParameters.forecast, {
|
||||
forecast = await json(weatherParameters.forecast, {
|
||||
data: {
|
||||
units,
|
||||
},
|
||||
@@ -44,7 +51,7 @@ class ExtendedForecast extends WeatherDisplay {
|
||||
const Days = [0, 1, 2, 3, 4, 5, 6];
|
||||
|
||||
const dates = Days.map((shift) => {
|
||||
const date = luxon.DateTime.local().startOf('day').plus({ days: shift });
|
||||
const date = DateTime.local().startOf('day').plus({ days: shift });
|
||||
return date.toLocaleString({ weekday: 'short' });
|
||||
});
|
||||
|
||||
@@ -61,12 +68,12 @@ class ExtendedForecast extends WeatherDisplay {
|
||||
// get the object to modify/populate
|
||||
const fDay = forecast[destIndex];
|
||||
// high temperature will always be last in the source array so it will overwrite the low values assigned below
|
||||
fDay.icon = icons.getWeatherIconFromIconLink(period.icon);
|
||||
fDay.icon = getWeatherIconFromIconLink(period.icon);
|
||||
fDay.text = ExtendedForecast.shortenExtendedForecastText(period.shortForecast);
|
||||
fDay.dayName = dates[destIndex];
|
||||
|
||||
// preload the icon
|
||||
utils.image.preload(fDay.icon);
|
||||
preloadImg(fDay.icon);
|
||||
|
||||
if (period.isDaytime) {
|
||||
// day time is the high temperature
|
||||
@@ -136,11 +143,11 @@ class ExtendedForecast extends WeatherDisplay {
|
||||
|
||||
let { low } = Day;
|
||||
if (low !== undefined) {
|
||||
if (navigation.units() === UNITS.metric) low = utils.units.fahrenheitToCelsius(low);
|
||||
if (navigation.units() === UNITS.metric) low = fahrenheitToCelsius(low);
|
||||
fill['value-lo'] = Math.round(low);
|
||||
}
|
||||
let { high } = Day;
|
||||
if (navigation.units() === UNITS.metric) high = utils.units.fahrenheitToCelsius(high);
|
||||
if (navigation.units() === UNITS.metric) high = fahrenheitToCelsius(high);
|
||||
fill['value-hi'] = Math.round(high);
|
||||
fill.condition = Day.text;
|
||||
|
||||
@@ -158,3 +165,7 @@ class ExtendedForecast extends WeatherDisplay {
|
||||
this.finishDraw();
|
||||
}
|
||||
}
|
||||
|
||||
export default ExtendedForecast;
|
||||
|
||||
window.ExtendedForecast = ExtendedForecast;
|
||||
@@ -1,7 +1,14 @@
|
||||
// hourly forecast list
|
||||
/* globals WeatherDisplay, utils, STATUS, UNITS, navigation, icons, luxon */
|
||||
/* globals WeatherDisplay, navigation */
|
||||
|
||||
import STATUS from './status.mjs';
|
||||
import { DateTime, Interval, Duration } from '../vendor/auto/luxon.mjs';
|
||||
import { json } from './utils/fetch.mjs';
|
||||
import { UNITS } from './config.mjs';
|
||||
import * as units from './utils/units.mjs';
|
||||
import { getHourlyIcon } from './icons.mjs';
|
||||
import { directionToNSEW } from './utils/calc.mjs';
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
class Hourly extends WeatherDisplay {
|
||||
constructor(navId, elemId, defaultActive) {
|
||||
// special height and width for scrolling
|
||||
@@ -25,7 +32,7 @@ class Hourly extends WeatherDisplay {
|
||||
let forecast;
|
||||
try {
|
||||
// get the forecast
|
||||
forecast = await utils.fetch.json(weatherParameters.forecastGridData);
|
||||
forecast = await json(weatherParameters.forecastGridData);
|
||||
} catch (e) {
|
||||
console.error('Get hourly forecast failed');
|
||||
console.error(e.status, e.responseJSON);
|
||||
@@ -59,16 +66,16 @@ class Hourly extends WeatherDisplay {
|
||||
temperature: temperature[idx],
|
||||
apparentTemperature: apparentTemperature[idx],
|
||||
windSpeed: windSpeed[idx],
|
||||
windDirection: utils.calc.directionToNSEW(windDirection[idx]),
|
||||
windDirection: directionToNSEW(windDirection[idx]),
|
||||
icon: icons[idx],
|
||||
};
|
||||
}
|
||||
|
||||
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]),
|
||||
temperature: units.celsiusToFahrenheit(temperature[idx]),
|
||||
apparentTemperature: units.celsiusToFahrenheit(apparentTemperature[idx]),
|
||||
windSpeed: units.kilometersToMiles(windSpeed[idx]),
|
||||
windDirection: directionToNSEW(windDirection[idx]),
|
||||
icon: icons[idx],
|
||||
};
|
||||
});
|
||||
@@ -76,24 +83,24 @@ class Hourly extends WeatherDisplay {
|
||||
|
||||
// given forecast paramaters determine a suitable icon
|
||||
static async determineIcon(skyCover, weather, iceAccumulation, probabilityOfPrecipitation, snowfallAmount, windSpeed) {
|
||||
const startOfHour = luxon.DateTime.local().startOf('hour');
|
||||
const startOfHour = 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);
|
||||
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 icons.getHourlyIcon(skyCover[idx], weather[idx], iceAccumulation[idx], probabilityOfPrecipitation[idx], snowfallAmount[idx], windSpeed[idx], isNight);
|
||||
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
|
||||
static expand(data) {
|
||||
const startOfHour = luxon.DateTime.utc().startOf('hour').toMillis();
|
||||
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 = luxon.Duration.fromISO(item.validTime.substr(item.validTime.indexOf('/') + 1)).shiftTo('milliseconds').values.milliseconds;
|
||||
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 {
|
||||
@@ -114,7 +121,7 @@ class Hourly extends WeatherDisplay {
|
||||
const list = this.elem.querySelector('.hourly-lines');
|
||||
list.innerHTML = '';
|
||||
|
||||
const startingHour = luxon.DateTime.local();
|
||||
const startingHour = DateTime.local();
|
||||
|
||||
const lines = this.data.map((data, index) => {
|
||||
const fillValues = {};
|
||||
@@ -177,7 +184,6 @@ class Hourly extends WeatherDisplay {
|
||||
}
|
||||
|
||||
static getTravelCitiesDayName(cities) {
|
||||
const { DateTime } = luxon;
|
||||
// effectively returns early on the first found date
|
||||
return cities.reduce((dayName, city) => {
|
||||
if (city && dayName === '') {
|
||||
@@ -190,3 +196,7 @@ class Hourly extends WeatherDisplay {
|
||||
}, '');
|
||||
}
|
||||
}
|
||||
|
||||
export default Hourly;
|
||||
|
||||
window.Hourly = Hourly;
|
||||
@@ -1,335 +0,0 @@
|
||||
/* spell-checker: disable */
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const icons = (() => {
|
||||
const getWeatherRegionalIconFromIconLink = (link, _isNightTime) => {
|
||||
// extract day or night if not provided
|
||||
const isNightTime = _isNightTime ?? link.indexOf('/night/') >= 0;
|
||||
// internal function to add path to returned icon
|
||||
const addPath = (icon) => `images/r/${icon}`;
|
||||
|
||||
// grab everything after the last slash ending at any of these: ?&,
|
||||
const afterLastSlash = link.toLowerCase().match(/[^/]+$/)[0];
|
||||
let conditionName = afterLastSlash.match(/(.*?)[,?&.]/)[1];
|
||||
// using probability as a crude heavy/light indication where possible
|
||||
const value = +(link.match(/,(\d{2,3})/) ?? [0, 100])[1];
|
||||
|
||||
// if a 'DualImage' is captured, adjust to just the j parameter
|
||||
if (conditionName === 'dualimage') {
|
||||
const match = link.match(/&j=(.*)&/);
|
||||
[, conditionName] = match;
|
||||
}
|
||||
|
||||
// find the icon
|
||||
switch (conditionName + (isNightTime ? '-n' : '')) {
|
||||
case 'skc':
|
||||
case 'hot':
|
||||
case 'haze':
|
||||
return addPath('Sunny.gif');
|
||||
|
||||
case 'skc-n':
|
||||
case 'nskc':
|
||||
case 'nskc-n':
|
||||
case 'cold-n':
|
||||
return addPath('Clear-1992.gif');
|
||||
|
||||
case 'bkn':
|
||||
return addPath('Mostly-Cloudy-1994-2.gif');
|
||||
|
||||
case 'bkn-n':
|
||||
case 'few-n':
|
||||
case 'nfew-n':
|
||||
case 'nfew':
|
||||
return addPath('Partly-Clear-1994-2.gif');
|
||||
|
||||
case 'sct':
|
||||
case 'few':
|
||||
return addPath('Partly-Cloudy.gif');
|
||||
|
||||
case 'sct-n':
|
||||
case 'nsct':
|
||||
case 'nsct-n':
|
||||
return addPath('Mostly-Clear.gif');
|
||||
|
||||
case 'ovc':
|
||||
case 'ovc-n':
|
||||
return addPath('Cloudy.gif');
|
||||
|
||||
case 'fog':
|
||||
case 'fog-n':
|
||||
return addPath('Fog.gif');
|
||||
|
||||
case 'rain_sleet':
|
||||
return addPath('Sleet.gif');
|
||||
|
||||
case 'rain_showers':
|
||||
case 'rain_showers_high':
|
||||
return addPath('Scattered-Showers-1994-2.gif');
|
||||
|
||||
case 'rain_showers-n':
|
||||
case 'rain_showers_high-n':
|
||||
return addPath('Scattered-Showers-Night-1994-2.gif');
|
||||
|
||||
case 'rain':
|
||||
case 'rain-n':
|
||||
return addPath('Rain-1992.gif');
|
||||
|
||||
// case 'snow':
|
||||
// return addPath('Light-Snow.gif');
|
||||
// break;
|
||||
|
||||
// case 'cc_snowshowers.gif':
|
||||
// //case "heavy-snow.gif":
|
||||
// return addPath('AM-Snow-1994.gif');
|
||||
// break;
|
||||
|
||||
case 'snow':
|
||||
case 'snow-n':
|
||||
if (value > 50) return addPath('Heavy-Snow-1994-2.gif');
|
||||
return addPath('Light-Snow.gif');
|
||||
|
||||
case 'rain_snow':
|
||||
return addPath('Rain-Snow-1992.gif');
|
||||
|
||||
case 'snow_fzra':
|
||||
case 'snow_fzra-n':
|
||||
return addPath('Freezing-Rain-Snow-1992.gif');
|
||||
|
||||
case 'fzra':
|
||||
case 'fzra-n':
|
||||
return addPath('Freezing-Rain-1992.gif');
|
||||
|
||||
case 'snow_sleet':
|
||||
case 'snow_sleet-n':
|
||||
return addPath('Snow and Sleet.gif');
|
||||
|
||||
case 'sleet':
|
||||
case 'sleet-n':
|
||||
return addPath('Sleet.gif');
|
||||
|
||||
case 'tsra_sct':
|
||||
case 'tsra':
|
||||
return addPath('Scattered-Tstorms-1994-2.gif');
|
||||
|
||||
case 'tsra_sct-n':
|
||||
case 'tsra-n':
|
||||
return addPath('Scattered-Tstorms-Night-1994-2.gif');
|
||||
|
||||
case 'tsra_hi':
|
||||
case 'tsra_hi-n':
|
||||
case 'hurricane':
|
||||
case 'tropical_storm':
|
||||
return addPath('Thunderstorm.gif');
|
||||
|
||||
case 'wind_few':
|
||||
case 'wind_sct':
|
||||
case 'wind_bkn':
|
||||
case 'wind_ovc':
|
||||
return addPath('Wind.gif');
|
||||
|
||||
case 'wind_skc':
|
||||
return addPath('Sunny-Wind-1994.gif');
|
||||
|
||||
case 'wind_skc-n':
|
||||
case 'wind_sct-n':
|
||||
return addPath('Clear-Wind-1994.gif');
|
||||
|
||||
case 'blizzard':
|
||||
return addPath('Blowing Snow.gif');
|
||||
|
||||
case 'cold':
|
||||
return addPath('cold.gif');
|
||||
|
||||
default:
|
||||
console.log(`Unable to locate regional icon for ${conditionName} ${link} ${isNightTime}`);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const getWeatherIconFromIconLink = (link, _isNightTime) => {
|
||||
if (!link) return false;
|
||||
|
||||
// internal function to add path to returned icon
|
||||
const addPath = (icon) => `images/${icon}`;
|
||||
// extract day or night if not provided
|
||||
const isNightTime = _isNightTime ?? link.indexOf('/night/') >= 0;
|
||||
|
||||
// grab everything after the last slash ending at any of these: ?&,
|
||||
const afterLastSlash = link.toLowerCase().match(/[^/]+$/)[0];
|
||||
let conditionName = afterLastSlash.match(/(.*?)[,?&.]/)[1];
|
||||
// using probability as a crude heavy/light indication where possible
|
||||
const value = +(link.match(/,(\d{2,3})/) ?? [0, 100])[1];
|
||||
|
||||
// if a 'DualImage' is captured, adjust to just the j parameter
|
||||
if (conditionName === 'dualimage') {
|
||||
const match = link.match(/&j=(.*)&/);
|
||||
[, conditionName] = match;
|
||||
}
|
||||
|
||||
// find the icon
|
||||
switch (conditionName + (isNightTime ? '-n' : '')) {
|
||||
case 'skc':
|
||||
case 'hot':
|
||||
case 'haze':
|
||||
case 'cold':
|
||||
return addPath('CC_Clear1.gif');
|
||||
|
||||
case 'skc-n':
|
||||
case 'nskc':
|
||||
case 'nskc-n':
|
||||
case 'cold-n':
|
||||
return addPath('CC_Clear0.gif');
|
||||
|
||||
case 'sct':
|
||||
case 'few':
|
||||
case 'bkn':
|
||||
return addPath('CC_PartlyCloudy1.gif');
|
||||
|
||||
case 'bkn-n':
|
||||
case 'few-n':
|
||||
case 'nfew-n':
|
||||
case 'nfew':
|
||||
case 'sct-n':
|
||||
case 'nsct':
|
||||
case 'nsct-n':
|
||||
return addPath('CC_PartlyCloudy0.gif');
|
||||
|
||||
case 'ovc':
|
||||
case 'novc':
|
||||
case 'ovc-n':
|
||||
return addPath('CC_Cloudy.gif');
|
||||
|
||||
case 'fog':
|
||||
case 'fog-n':
|
||||
return addPath('CC_Fog.gif');
|
||||
|
||||
case 'rain_sleet':
|
||||
return addPath('Sleet.gif');
|
||||
|
||||
case 'rain_showers':
|
||||
case 'rain_showers_high':
|
||||
return addPath('CC_Showers.gif');
|
||||
|
||||
case 'rain_showers-n':
|
||||
case 'rain_showers_high-n':
|
||||
return addPath('CC_Showers.gif');
|
||||
|
||||
case 'rain':
|
||||
case 'rain-n':
|
||||
return addPath('CC_Rain.gif');
|
||||
|
||||
// case 'snow':
|
||||
// return addPath('Light-Snow.gif');
|
||||
// break;
|
||||
|
||||
// case 'cc_snowshowers.gif':
|
||||
// //case "heavy-snow.gif":
|
||||
// return addPath('AM-Snow-1994.gif');
|
||||
// break;
|
||||
|
||||
case 'snow':
|
||||
case 'snow-n':
|
||||
if (value > 50) return addPath('CC_Snow.gif');
|
||||
return addPath('CC_SnowShowers.gif');
|
||||
|
||||
case 'rain_snow':
|
||||
return addPath('CC_RainSnow.gif');
|
||||
|
||||
case 'snow_fzra':
|
||||
case 'snow_fzra-n':
|
||||
case 'fzra':
|
||||
case 'fzra-n':
|
||||
return addPath('CC_FreezingRain.gif');
|
||||
|
||||
case 'snow_sleet':
|
||||
return addPath('Snow-Sleet.gif');
|
||||
|
||||
case 'tsra_sct':
|
||||
case 'tsra':
|
||||
return addPath('EF_ScatTstorms.gif');
|
||||
|
||||
case 'tsra_sct-n':
|
||||
case 'tsra-n':
|
||||
return addPath('CC_TStorm.gif');
|
||||
|
||||
case 'tsra_hi':
|
||||
case 'tsra_hi-n':
|
||||
case 'hurricane':
|
||||
case 'tropical_storm':
|
||||
return addPath('CC_TStorm.gif');
|
||||
|
||||
case 'wind_few':
|
||||
case 'wind_sct':
|
||||
case 'wind_bkn':
|
||||
case 'wind_ovc':
|
||||
return addPath('CC_Windy.gif');
|
||||
|
||||
case 'wind_skc':
|
||||
case 'wind_skc-n':
|
||||
case 'wind_sct-n':
|
||||
return addPath('CC_Windy.gif');
|
||||
|
||||
case 'blizzard':
|
||||
return addPath('Blowing-Snow.gif');
|
||||
|
||||
default:
|
||||
console.log(`Unable to locate icon for ${conditionName} ${link} ${isNightTime}`);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const getHourlyIcon = (skyCover, weather, iceAccumulation, probabilityOfPrecipitation, snowfallAmount, windSpeed, isNight = false) => {
|
||||
// internal function to add path to returned icon
|
||||
const addPath = (icon) => `images/r/${icon}`;
|
||||
|
||||
// possible phenomenon
|
||||
let thunder = false;
|
||||
let snow = false;
|
||||
let ice = false;
|
||||
let fog = false;
|
||||
let wind = false;
|
||||
|
||||
// test the phenomenon for various value if it is provided.
|
||||
weather.forEach((phenomenon) => {
|
||||
if (!phenomenon.weather) return;
|
||||
if (phenomenon.weather.toLowerCase().includes('thunder')) thunder = true;
|
||||
if (phenomenon.weather.toLowerCase().includes('snow')) snow = true;
|
||||
if (phenomenon.weather.toLowerCase().includes('ice')) ice = true;
|
||||
if (phenomenon.weather.toLowerCase().includes('fog')) fog = true;
|
||||
if (phenomenon.weather.toLowerCase().includes('wind')) wind = true;
|
||||
});
|
||||
|
||||
// first item in list is highest priority, units are metric where applicable
|
||||
if (iceAccumulation > 0 || ice) return addPath('Freezing-Rain-1992.gif');
|
||||
if (snowfallAmount > 10) {
|
||||
if (windSpeed > 30 || wind) return addPath('Blowing Snow.gif');
|
||||
return addPath('Heavy-Snow-1994.gif');
|
||||
}
|
||||
if ((snowfallAmount > 0 || snow) && thunder) return addPath('ThunderSnow.gif');
|
||||
if (snowfallAmount > 0 || snow) return addPath('Light-Snow.gif');
|
||||
if (thunder) return (addPath('Thunderstorm.gif'));
|
||||
if (probabilityOfPrecipitation > 70) return addPath('Rain-1992.gif');
|
||||
if (probabilityOfPrecipitation > 50) return addPath('Shower.gif');
|
||||
if (probabilityOfPrecipitation > 30) {
|
||||
if (!isNight) return addPath('Scattered-Showers-1994.gif');
|
||||
return addPath('Scattered-Showers-Night.gif');
|
||||
}
|
||||
if (fog) return addPath('Fog.gif');
|
||||
if (skyCover > 70) return addPath('Cloudy.gif');
|
||||
if (skyCover > 50) {
|
||||
if (!isNight) return addPath('Mostly-Cloudy-1994.gif');
|
||||
return addPath('Partly-Clear-1994.gif');
|
||||
}
|
||||
if (skyCover > 30) {
|
||||
if (!isNight) return addPath('Partly-Cloudy.gif');
|
||||
return addPath('Mostly-Clear.gif');
|
||||
}
|
||||
if (isNight) return addPath('Clear-1992.gif');
|
||||
return addPath('Sunny.gif');
|
||||
};
|
||||
|
||||
return {
|
||||
getWeatherIconFromIconLink,
|
||||
getWeatherRegionalIconFromIconLink,
|
||||
getHourlyIcon,
|
||||
};
|
||||
})();
|
||||
339
server/scripts/modules/icons.mjs
Normal file
339
server/scripts/modules/icons.mjs
Normal file
@@ -0,0 +1,339 @@
|
||||
/* spell-checker: disable */
|
||||
|
||||
const getWeatherRegionalIconFromIconLink = (link, _isNightTime) => {
|
||||
// extract day or night if not provided
|
||||
const isNightTime = _isNightTime ?? link.indexOf('/night/') >= 0;
|
||||
// internal function to add path to returned icon
|
||||
const addPath = (icon) => `images/r/${icon}`;
|
||||
|
||||
// grab everything after the last slash ending at any of these: ?&,
|
||||
const afterLastSlash = link.toLowerCase().match(/[^/]+$/)[0];
|
||||
let conditionName = afterLastSlash.match(/(.*?)[,?&.]/)[1];
|
||||
// using probability as a crude heavy/light indication where possible
|
||||
const value = +(link.match(/,(\d{2,3})/) ?? [0, 100])[1];
|
||||
|
||||
// if a 'DualImage' is captured, adjust to just the j parameter
|
||||
if (conditionName === 'dualimage') {
|
||||
const match = link.match(/&j=(.*)&/);
|
||||
[, conditionName] = match;
|
||||
}
|
||||
|
||||
// find the icon
|
||||
switch (conditionName + (isNightTime ? '-n' : '')) {
|
||||
case 'skc':
|
||||
case 'hot':
|
||||
case 'haze':
|
||||
return addPath('Sunny.gif');
|
||||
|
||||
case 'skc-n':
|
||||
case 'nskc':
|
||||
case 'nskc-n':
|
||||
case 'cold-n':
|
||||
return addPath('Clear-1992.gif');
|
||||
|
||||
case 'bkn':
|
||||
return addPath('Mostly-Cloudy-1994-2.gif');
|
||||
|
||||
case 'bkn-n':
|
||||
case 'few-n':
|
||||
case 'nfew-n':
|
||||
case 'nfew':
|
||||
return addPath('Partly-Clear-1994-2.gif');
|
||||
|
||||
case 'sct':
|
||||
case 'few':
|
||||
return addPath('Partly-Cloudy.gif');
|
||||
|
||||
case 'sct-n':
|
||||
case 'nsct':
|
||||
case 'nsct-n':
|
||||
return addPath('Mostly-Clear.gif');
|
||||
|
||||
case 'ovc':
|
||||
case 'ovc-n':
|
||||
return addPath('Cloudy.gif');
|
||||
|
||||
case 'fog':
|
||||
case 'fog-n':
|
||||
return addPath('Fog.gif');
|
||||
|
||||
case 'rain_sleet':
|
||||
return addPath('Sleet.gif');
|
||||
|
||||
case 'rain_showers':
|
||||
case 'rain_showers_high':
|
||||
return addPath('Scattered-Showers-1994-2.gif');
|
||||
|
||||
case 'rain_showers-n':
|
||||
case 'rain_showers_high-n':
|
||||
return addPath('Scattered-Showers-Night-1994-2.gif');
|
||||
|
||||
case 'rain':
|
||||
case 'rain-n':
|
||||
return addPath('Rain-1992.gif');
|
||||
|
||||
// case 'snow':
|
||||
// return addPath('Light-Snow.gif');
|
||||
// break;
|
||||
|
||||
// case 'cc_snowshowers.gif':
|
||||
// //case "heavy-snow.gif":
|
||||
// return addPath('AM-Snow-1994.gif');
|
||||
// break;
|
||||
|
||||
case 'snow':
|
||||
case 'snow-n':
|
||||
if (value > 50) return addPath('Heavy-Snow-1994-2.gif');
|
||||
return addPath('Light-Snow.gif');
|
||||
|
||||
case 'rain_snow':
|
||||
return addPath('Rain-Snow-1992.gif');
|
||||
|
||||
case 'snow_fzra':
|
||||
case 'snow_fzra-n':
|
||||
return addPath('Freezing-Rain-Snow-1992.gif');
|
||||
|
||||
case 'fzra':
|
||||
case 'fzra-n':
|
||||
return addPath('Freezing-Rain-1992.gif');
|
||||
|
||||
case 'snow_sleet':
|
||||
case 'snow_sleet-n':
|
||||
return addPath('Snow and Sleet.gif');
|
||||
|
||||
case 'sleet':
|
||||
case 'sleet-n':
|
||||
return addPath('Sleet.gif');
|
||||
|
||||
case 'tsra_sct':
|
||||
case 'tsra':
|
||||
return addPath('Scattered-Tstorms-1994-2.gif');
|
||||
|
||||
case 'tsra_sct-n':
|
||||
case 'tsra-n':
|
||||
return addPath('Scattered-Tstorms-Night-1994-2.gif');
|
||||
|
||||
case 'tsra_hi':
|
||||
case 'tsra_hi-n':
|
||||
case 'hurricane':
|
||||
case 'tropical_storm':
|
||||
return addPath('Thunderstorm.gif');
|
||||
|
||||
case 'wind_few':
|
||||
case 'wind_sct':
|
||||
case 'wind_bkn':
|
||||
case 'wind_ovc':
|
||||
return addPath('Wind.gif');
|
||||
|
||||
case 'wind_skc':
|
||||
return addPath('Sunny-Wind-1994.gif');
|
||||
|
||||
case 'wind_skc-n':
|
||||
case 'wind_sct-n':
|
||||
return addPath('Clear-Wind-1994.gif');
|
||||
|
||||
case 'blizzard':
|
||||
return addPath('Blowing Snow.gif');
|
||||
|
||||
case 'cold':
|
||||
return addPath('cold.gif');
|
||||
|
||||
default:
|
||||
console.log(`Unable to locate regional icon for ${conditionName} ${link} ${isNightTime}`);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const getWeatherIconFromIconLink = (link, _isNightTime) => {
|
||||
if (!link) return false;
|
||||
|
||||
// internal function to add path to returned icon
|
||||
const addPath = (icon) => `images/${icon}`;
|
||||
// extract day or night if not provided
|
||||
const isNightTime = _isNightTime ?? link.indexOf('/night/') >= 0;
|
||||
|
||||
// grab everything after the last slash ending at any of these: ?&,
|
||||
const afterLastSlash = link.toLowerCase().match(/[^/]+$/)[0];
|
||||
let conditionName = afterLastSlash.match(/(.*?)[,?&.]/)[1];
|
||||
// using probability as a crude heavy/light indication where possible
|
||||
const value = +(link.match(/,(\d{2,3})/) ?? [0, 100])[1];
|
||||
|
||||
// if a 'DualImage' is captured, adjust to just the j parameter
|
||||
if (conditionName === 'dualimage') {
|
||||
const match = link.match(/&j=(.*)&/);
|
||||
[, conditionName] = match;
|
||||
}
|
||||
|
||||
// find the icon
|
||||
switch (conditionName + (isNightTime ? '-n' : '')) {
|
||||
case 'skc':
|
||||
case 'hot':
|
||||
case 'haze':
|
||||
case 'cold':
|
||||
return addPath('CC_Clear1.gif');
|
||||
|
||||
case 'skc-n':
|
||||
case 'nskc':
|
||||
case 'nskc-n':
|
||||
case 'cold-n':
|
||||
return addPath('CC_Clear0.gif');
|
||||
|
||||
case 'sct':
|
||||
case 'few':
|
||||
case 'bkn':
|
||||
return addPath('CC_PartlyCloudy1.gif');
|
||||
|
||||
case 'bkn-n':
|
||||
case 'few-n':
|
||||
case 'nfew-n':
|
||||
case 'nfew':
|
||||
case 'sct-n':
|
||||
case 'nsct':
|
||||
case 'nsct-n':
|
||||
return addPath('CC_PartlyCloudy0.gif');
|
||||
|
||||
case 'ovc':
|
||||
case 'novc':
|
||||
case 'ovc-n':
|
||||
return addPath('CC_Cloudy.gif');
|
||||
|
||||
case 'fog':
|
||||
case 'fog-n':
|
||||
return addPath('CC_Fog.gif');
|
||||
|
||||
case 'rain_sleet':
|
||||
return addPath('Sleet.gif');
|
||||
|
||||
case 'rain_showers':
|
||||
case 'rain_showers_high':
|
||||
return addPath('CC_Showers.gif');
|
||||
|
||||
case 'rain_showers-n':
|
||||
case 'rain_showers_high-n':
|
||||
return addPath('CC_Showers.gif');
|
||||
|
||||
case 'rain':
|
||||
case 'rain-n':
|
||||
return addPath('CC_Rain.gif');
|
||||
|
||||
// case 'snow':
|
||||
// return addPath('Light-Snow.gif');
|
||||
// break;
|
||||
|
||||
// case 'cc_snowshowers.gif':
|
||||
// //case "heavy-snow.gif":
|
||||
// return addPath('AM-Snow-1994.gif');
|
||||
// break;
|
||||
|
||||
case 'snow':
|
||||
case 'snow-n':
|
||||
if (value > 50) return addPath('CC_Snow.gif');
|
||||
return addPath('CC_SnowShowers.gif');
|
||||
|
||||
case 'rain_snow':
|
||||
return addPath('CC_RainSnow.gif');
|
||||
|
||||
case 'snow_fzra':
|
||||
case 'snow_fzra-n':
|
||||
case 'fzra':
|
||||
case 'fzra-n':
|
||||
return addPath('CC_FreezingRain.gif');
|
||||
|
||||
case 'snow_sleet':
|
||||
return addPath('Snow-Sleet.gif');
|
||||
|
||||
case 'tsra_sct':
|
||||
case 'tsra':
|
||||
return addPath('EF_ScatTstorms.gif');
|
||||
|
||||
case 'tsra_sct-n':
|
||||
case 'tsra-n':
|
||||
return addPath('CC_TStorm.gif');
|
||||
|
||||
case 'tsra_hi':
|
||||
case 'tsra_hi-n':
|
||||
case 'hurricane':
|
||||
case 'tropical_storm':
|
||||
return addPath('CC_TStorm.gif');
|
||||
|
||||
case 'wind_few':
|
||||
case 'wind_sct':
|
||||
case 'wind_bkn':
|
||||
case 'wind_ovc':
|
||||
return addPath('CC_Windy.gif');
|
||||
|
||||
case 'wind_skc':
|
||||
case 'wind_skc-n':
|
||||
case 'wind_sct-n':
|
||||
return addPath('CC_Windy.gif');
|
||||
|
||||
case 'blizzard':
|
||||
return addPath('Blowing-Snow.gif');
|
||||
|
||||
default:
|
||||
console.log(`Unable to locate icon for ${conditionName} ${link} ${isNightTime}`);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const getHourlyIcon = (skyCover, weather, iceAccumulation, probabilityOfPrecipitation, snowfallAmount, windSpeed, isNight = false) => {
|
||||
// internal function to add path to returned icon
|
||||
const addPath = (icon) => `images/r/${icon}`;
|
||||
|
||||
// possible phenomenon
|
||||
let thunder = false;
|
||||
let snow = false;
|
||||
let ice = false;
|
||||
let fog = false;
|
||||
let wind = false;
|
||||
|
||||
// test the phenomenon for various value if it is provided.
|
||||
weather.forEach((phenomenon) => {
|
||||
if (!phenomenon.weather) return;
|
||||
if (phenomenon.weather.toLowerCase().includes('thunder')) thunder = true;
|
||||
if (phenomenon.weather.toLowerCase().includes('snow')) snow = true;
|
||||
if (phenomenon.weather.toLowerCase().includes('ice')) ice = true;
|
||||
if (phenomenon.weather.toLowerCase().includes('fog')) fog = true;
|
||||
if (phenomenon.weather.toLowerCase().includes('wind')) wind = true;
|
||||
});
|
||||
|
||||
// first item in list is highest priority, units are metric where applicable
|
||||
if (iceAccumulation > 0 || ice) return addPath('Freezing-Rain-1992.gif');
|
||||
if (snowfallAmount > 10) {
|
||||
if (windSpeed > 30 || wind) return addPath('Blowing Snow.gif');
|
||||
return addPath('Heavy-Snow-1994.gif');
|
||||
}
|
||||
if ((snowfallAmount > 0 || snow) && thunder) return addPath('ThunderSnow.gif');
|
||||
if (snowfallAmount > 0 || snow) return addPath('Light-Snow.gif');
|
||||
if (thunder) return (addPath('Thunderstorm.gif'));
|
||||
if (probabilityOfPrecipitation > 70) return addPath('Rain-1992.gif');
|
||||
if (probabilityOfPrecipitation > 50) return addPath('Shower.gif');
|
||||
if (probabilityOfPrecipitation > 30) {
|
||||
if (!isNight) return addPath('Scattered-Showers-1994.gif');
|
||||
return addPath('Scattered-Showers-Night.gif');
|
||||
}
|
||||
if (fog) return addPath('Fog.gif');
|
||||
if (skyCover > 70) return addPath('Cloudy.gif');
|
||||
if (skyCover > 50) {
|
||||
if (!isNight) return addPath('Mostly-Cloudy-1994.gif');
|
||||
return addPath('Partly-Clear-1994.gif');
|
||||
}
|
||||
if (skyCover > 30) {
|
||||
if (!isNight) return addPath('Partly-Cloudy.gif');
|
||||
return addPath('Mostly-Clear.gif');
|
||||
}
|
||||
if (isNight) return addPath('Clear-1992.gif');
|
||||
return addPath('Sunny.gif');
|
||||
};
|
||||
|
||||
export {
|
||||
getWeatherIconFromIconLink,
|
||||
getWeatherRegionalIconFromIconLink,
|
||||
getHourlyIcon,
|
||||
};
|
||||
|
||||
window.icons = {
|
||||
getWeatherIconFromIconLink,
|
||||
getWeatherRegionalIconFromIconLink,
|
||||
getHourlyIcon,
|
||||
};
|
||||
@@ -1,7 +1,12 @@
|
||||
// current weather conditions display
|
||||
/* globals WeatherDisplay, utils, STATUS, UNITS, navigation, StationInfo */
|
||||
/* globals WeatherDisplay, navigation, StationInfo */
|
||||
import { distance as calcDistance, directionToNSEW } from './utils/calc.mjs';
|
||||
import { json } from './utils/fetch.mjs';
|
||||
import STATUS from './status.mjs';
|
||||
import { locationCleanup } from './utils/string.mjs';
|
||||
import { UNITS } from './config.mjs';
|
||||
import * as units from './utils/units.mjs';
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
class LatestObservations extends WeatherDisplay {
|
||||
constructor(navId, elemId) {
|
||||
super(navId, elemId, 'Latest Observations', true);
|
||||
@@ -17,7 +22,7 @@ class LatestObservations extends WeatherDisplay {
|
||||
// calculate distance to each station
|
||||
const stationsByDistance = Object.keys(StationInfo).map((key) => {
|
||||
const station = StationInfo[key];
|
||||
const distance = utils.calc.distance(station.lat, station.lon, weatherParameters.latitude, weatherParameters.longitude);
|
||||
const distance = calcDistance(station.lat, station.lon, weatherParameters.latitude, weatherParameters.longitude);
|
||||
return { ...station, distance };
|
||||
});
|
||||
|
||||
@@ -29,7 +34,7 @@ class LatestObservations extends WeatherDisplay {
|
||||
// get data for regional stations
|
||||
const allConditions = await Promise.all(regionalStations.map(async (station) => {
|
||||
try {
|
||||
const data = await utils.fetch.json(`https://api.weather.gov/stations/${station.id}/observations/latest`);
|
||||
const data = await json(`https://api.weather.gov/stations/${station.id}/observations/latest`);
|
||||
// test for temperature, weather and wind values present
|
||||
if (data.properties.temperature.value === null
|
||||
|| data.properties.textDescription === ''
|
||||
@@ -76,17 +81,17 @@ class LatestObservations extends WeatherDisplay {
|
||||
const lines = sortedConditions.map((condition) => {
|
||||
let Temperature = condition.temperature.value;
|
||||
let WindSpeed = condition.windSpeed.value;
|
||||
const windDirection = utils.calc.directionToNSEW(condition.windDirection.value);
|
||||
const windDirection = directionToNSEW(condition.windDirection.value);
|
||||
|
||||
if (navigation.units() === UNITS.english) {
|
||||
Temperature = utils.units.celsiusToFahrenheit(Temperature);
|
||||
WindSpeed = utils.units.kphToMph(WindSpeed);
|
||||
Temperature = units.celsiusToFahrenheit(Temperature);
|
||||
WindSpeed = units.kphToMph(WindSpeed);
|
||||
}
|
||||
WindSpeed = Math.round(WindSpeed);
|
||||
Temperature = Math.round(Temperature);
|
||||
|
||||
const fill = {};
|
||||
fill.location = utils.string.locationCleanup(condition.city).substr(0, 14);
|
||||
fill.location = locationCleanup(condition.city).substr(0, 14);
|
||||
fill.temp = Temperature;
|
||||
fill.weather = LatestObservations.shortenCurrentConditions(condition.textDescription).substr(0, 9);
|
||||
if (WindSpeed > 0) {
|
||||
@@ -126,3 +131,5 @@ class LatestObservations extends WeatherDisplay {
|
||||
return condition;
|
||||
}
|
||||
}
|
||||
|
||||
window.LatestObservations = LatestObservations;
|
||||
@@ -1,8 +1,10 @@
|
||||
// display text based local forecast
|
||||
|
||||
/* globals WeatherDisplay, utils, STATUS, UNITS, navigation */
|
||||
/* globals WeatherDisplay, navigation */
|
||||
import STATUS from './status.mjs';
|
||||
import { UNITS } from './config.mjs';
|
||||
import { json } from './utils/fetch.mjs';
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
class LocalForecast extends WeatherDisplay {
|
||||
constructor(navId, elemId) {
|
||||
super(navId, elemId, 'Local Forecast', true);
|
||||
@@ -62,7 +64,7 @@ class LocalForecast extends WeatherDisplay {
|
||||
let units = 'us';
|
||||
if (navigation.units() === UNITS.metric) units = 'si';
|
||||
try {
|
||||
return await utils.fetch.json(weatherParameters.forecast, {
|
||||
return await json(weatherParameters.forecast, {
|
||||
data: {
|
||||
units,
|
||||
},
|
||||
@@ -94,3 +96,5 @@ class LocalForecast extends WeatherDisplay {
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
window.LocalForecast = LocalForecast;
|
||||
@@ -1,14 +1,14 @@
|
||||
// regional forecast and observations
|
||||
/* globals WeatherDisplay, navigation */
|
||||
import { loadImg } from './utils/image.mjs';
|
||||
import STATUS from './status.mjs';
|
||||
|
||||
/* globals WeatherDisplay, utils, STATUS, navigation */
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
class Progress extends WeatherDisplay {
|
||||
constructor(navId, elemId) {
|
||||
super(navId, elemId, '', false);
|
||||
|
||||
// pre-load background image (returns promise)
|
||||
this.backgroundImage = utils.image.load('images/BackGround1_1.png');
|
||||
this.backgroundImage = loadImg('images/BackGround1_1.png');
|
||||
|
||||
// disable any navigation timing
|
||||
this.timing = false;
|
||||
@@ -101,3 +101,5 @@ class Progress extends WeatherDisplay {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
window.Progress = Progress;
|
||||
@@ -1,7 +1,11 @@
|
||||
// current weather conditions display
|
||||
/* globals WeatherDisplay, utils, STATUS, luxon */
|
||||
/* globals WeatherDisplay */
|
||||
import STATUS from './status.mjs';
|
||||
import { DateTime } from '../vendor/auto/luxon.mjs';
|
||||
import { loadImg } from './utils/image.mjs';
|
||||
import { text } from './utils/fetch.mjs';
|
||||
import { rewriteUrl } from './utils/cors.mjs';
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
class Radar extends WeatherDisplay {
|
||||
constructor(navId, elemId) {
|
||||
super(navId, elemId, 'Local Radar', true);
|
||||
@@ -43,13 +47,10 @@ class Radar extends WeatherDisplay {
|
||||
return;
|
||||
}
|
||||
|
||||
// date and time parsing
|
||||
const { DateTime } = luxon;
|
||||
|
||||
// get the base map
|
||||
let src = 'images/4000RadarMap2.jpg';
|
||||
if (weatherParameters.State === 'HI') src = 'images/HawaiiRadarMap2.png';
|
||||
this.baseMap = await utils.image.load(src);
|
||||
this.baseMap = await loadImg(src);
|
||||
|
||||
const baseUrl = 'https://mesonet.agron.iastate.edu/archive/data/';
|
||||
const baseUrlEnd = '/GIS/uscomp/';
|
||||
@@ -65,7 +66,7 @@ class Radar extends WeatherDisplay {
|
||||
const lists = (await Promise.all(baseUrls.map(async (url) => {
|
||||
try {
|
||||
// get a list of available radars
|
||||
const radarHtml = await utils.fetch.text(url, { cors: true });
|
||||
const radarHtml = await text(url, { cors: true });
|
||||
return radarHtml;
|
||||
} catch (e) {
|
||||
console.log('Unable to get list of radars');
|
||||
@@ -130,7 +131,7 @@ class Radar extends WeatherDisplay {
|
||||
context.imageSmoothingEnabled = false;
|
||||
|
||||
// get the image
|
||||
const response = await fetch(utils.cors.rewriteUrl(url));
|
||||
const response = await fetch(rewriteUrl(url));
|
||||
|
||||
// test response
|
||||
if (!response.ok) throw new Error(`Unable to fetch radar error ${response.status} ${response.statusText} from ${response.url}`);
|
||||
@@ -157,7 +158,7 @@ class Radar extends WeatherDisplay {
|
||||
}
|
||||
|
||||
// assign to an html image element
|
||||
const imgBlob = await utils.image.load(blob);
|
||||
const imgBlob = await loadImg(blob);
|
||||
|
||||
// draw the entire image
|
||||
workingContext.clearRect(0, 0, width, 1600);
|
||||
@@ -204,7 +205,6 @@ class Radar extends WeatherDisplay {
|
||||
|
||||
async drawCanvas() {
|
||||
super.drawCanvas();
|
||||
const { DateTime } = luxon;
|
||||
const time = this.times[this.screenIndex].toLocaleString(DateTime.TIME_SIMPLE);
|
||||
const timePadded = time.length >= 8 ? time : ` ${time}`;
|
||||
this.elem.querySelector('.header .right .time').innerHTML = timePadded;
|
||||
@@ -396,3 +396,5 @@ class Radar extends WeatherDisplay {
|
||||
mapContext.drawImage(radarContext.canvas, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
window.Radar = Radar;
|
||||
@@ -1,9 +1,16 @@
|
||||
// regional forecast and observations
|
||||
// type 0 = observations, 1 = first forecast, 2 = second forecast
|
||||
|
||||
/* globals WeatherDisplay, utils, STATUS, icons, UNITS, navigation, luxon, StationInfo, RegionalCities */
|
||||
/* globals WeatherDisplay, navigation, StationInfo, RegionalCities */
|
||||
import STATUS from './status.mjs';
|
||||
import { UNITS } from './config.mjs';
|
||||
import { distance as calcDistance } from './utils/calc.mjs';
|
||||
import { json } from './utils/fetch.mjs';
|
||||
import * as units from './utils/units.mjs';
|
||||
import { getWeatherRegionalIconFromIconLink } from './icons.mjs';
|
||||
import { preloadImg } from './utils/image.mjs';
|
||||
import { DateTime } from '../vendor/auto/luxon.mjs';
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
class RegionalForecast extends WeatherDisplay {
|
||||
constructor(navId, elemId) {
|
||||
super(navId, elemId, 'Regional Forecast', true);
|
||||
@@ -55,7 +62,7 @@ class RegionalForecast extends WeatherDisplay {
|
||||
const targetDist = city.targetDistance || 1;
|
||||
// Only add the city as long as it isn't within set distance degree of any other city already in the array.
|
||||
const okToAddCity = regionalCities.reduce((acc, testCity) => {
|
||||
const distance = utils.calc.distance(city.lon, city.lat, testCity.lon, testCity.lat);
|
||||
const distance = calcDistance(city.lon, city.lat, testCity.lon, testCity.lat);
|
||||
return acc && distance >= targetDist;
|
||||
}, true);
|
||||
if (okToAddCity) regionalCities.push(city);
|
||||
@@ -70,7 +77,7 @@ class RegionalForecast extends WeatherDisplay {
|
||||
// start off the observation task
|
||||
const observationPromise = RegionalForecast.getRegionalObservation(city.point, city);
|
||||
|
||||
const forecast = await utils.fetch.json(`https://api.weather.gov/gridpoints/${city.point.wfo}/${city.point.x},${city.point.y}/forecast`);
|
||||
const forecast = await json(`https://api.weather.gov/gridpoints/${city.point.wfo}/${city.point.x},${city.point.y}/forecast`);
|
||||
|
||||
// get XY on map for city
|
||||
const cityXY = RegionalForecast.getXYForCity(city, minMaxLatLon.maxLat, minMaxLatLon.minLon, weatherParameters.state);
|
||||
@@ -80,7 +87,7 @@ class RegionalForecast extends WeatherDisplay {
|
||||
// format the observation the same as the forecast
|
||||
const regionalObservation = {
|
||||
daytime: !!observation.icon.match(/\/day\//),
|
||||
temperature: utils.units.celsiusToFahrenheit(observation.temperature.value),
|
||||
temperature: units.celsiusToFahrenheit(observation.temperature.value),
|
||||
name: RegionalForecast.formatCity(city.city),
|
||||
icon: observation.icon,
|
||||
x: cityXY.x,
|
||||
@@ -88,7 +95,7 @@ class RegionalForecast extends WeatherDisplay {
|
||||
};
|
||||
|
||||
// preload the icon
|
||||
utils.image.preload(icons.getWeatherRegionalIconFromIconLink(regionalObservation.icon, !regionalObservation.daytime));
|
||||
preloadImg(getWeatherRegionalIconFromIconLink(regionalObservation.icon, !regionalObservation.daytime));
|
||||
|
||||
// return a pared-down forecast
|
||||
// 0th object is the current conditions
|
||||
@@ -141,15 +148,15 @@ class RegionalForecast extends WeatherDisplay {
|
||||
static async getRegionalObservation(point, city) {
|
||||
try {
|
||||
// get stations
|
||||
const stations = await utils.fetch.json(`https://api.weather.gov/gridpoints/${city.point.wfo}/${city.point.x},${city.point.y}/stations`);
|
||||
const stations = await json(`https://api.weather.gov/gridpoints/${city.point.wfo}/${city.point.x},${city.point.y}/stations`);
|
||||
|
||||
// get the first station
|
||||
const station = stations.features[0].id;
|
||||
// get the observation data
|
||||
const observation = await utils.fetch.json(`${station}/observations/latest`);
|
||||
const observation = await json(`${station}/observations/latest`);
|
||||
// preload the image
|
||||
if (!observation.properties.icon) return false;
|
||||
utils.image.preload(icons.getWeatherRegionalIconFromIconLink(observation.properties.icon, !observation.properties.daytime));
|
||||
preloadImg(getWeatherRegionalIconFromIconLink(observation.properties.icon, !observation.properties.daytime));
|
||||
// return the observation
|
||||
return observation.properties;
|
||||
} catch (e) {
|
||||
@@ -328,7 +335,6 @@ class RegionalForecast extends WeatherDisplay {
|
||||
// break up data into useful values
|
||||
const { regionalData: data, sourceXY, offsetXY } = this.data;
|
||||
|
||||
const { DateTime } = luxon;
|
||||
// draw the header graphics
|
||||
|
||||
// draw the appropriate title
|
||||
@@ -362,10 +368,10 @@ class RegionalForecast extends WeatherDisplay {
|
||||
const fill = {};
|
||||
const period = city[this.screenIndex];
|
||||
|
||||
fill.icon = { type: 'img', src: icons.getWeatherRegionalIconFromIconLink(period.icon, !period.daytime) };
|
||||
fill.icon = { type: 'img', src: getWeatherRegionalIconFromIconLink(period.icon, !period.daytime) };
|
||||
fill.city = period.name;
|
||||
let { temperature } = period;
|
||||
if (navigation.units() === UNITS.metric) temperature = Math.round(utils.units.fahrenheitToCelsius(temperature));
|
||||
if (navigation.units() === UNITS.metric) temperature = Math.round(units.fahrenheitToCelsius(temperature));
|
||||
fill.temp = temperature;
|
||||
|
||||
const elem = this.fillTemplate('location', fill);
|
||||
@@ -382,3 +388,5 @@ class RegionalForecast extends WeatherDisplay {
|
||||
this.finishDraw();
|
||||
}
|
||||
}
|
||||
|
||||
window.RegionalForecast = RegionalForecast;
|
||||
10
server/scripts/modules/status.mjs
Normal file
10
server/scripts/modules/status.mjs
Normal file
@@ -0,0 +1,10 @@
|
||||
const STATUS = {
|
||||
loading: Symbol('loading'),
|
||||
loaded: Symbol('loaded'),
|
||||
failed: Symbol('failed'),
|
||||
noData: Symbol('noData'),
|
||||
disabled: Symbol('disabled'),
|
||||
};
|
||||
|
||||
export default STATUS;
|
||||
window.STATUS = STATUS;
|
||||
@@ -1,7 +1,12 @@
|
||||
// travel forecast display
|
||||
/* globals WeatherDisplay, utils, STATUS, UNITS, navigation, icons, luxon, TravelCities */
|
||||
/* globals WeatherDisplay, navigation, TravelCities */
|
||||
import STATUS from './status.mjs';
|
||||
import { UNITS } from './config.mjs';
|
||||
import { json } from './utils/fetch.mjs';
|
||||
import { getWeatherRegionalIconFromIconLink } from './icons.mjs';
|
||||
import { fahrenheitToCelsius } from './utils/units.mjs';
|
||||
import { DateTime } from '../vendor/auto/luxon.mjs';
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
class TravelForecast extends WeatherDisplay {
|
||||
constructor(navId, elemId, defaultActive) {
|
||||
// special height and width for scrolling
|
||||
@@ -30,7 +35,7 @@ class TravelForecast extends WeatherDisplay {
|
||||
try {
|
||||
// get point then forecast
|
||||
if (!city.point) throw new Error('No pre-loaded point');
|
||||
const forecast = await utils.fetch.json(`https://api.weather.gov/gridpoints/${city.point.wfo}/${city.point.x},${city.point.y}/forecast`);
|
||||
const forecast = await json(`https://api.weather.gov/gridpoints/${city.point.wfo}/${city.point.x},${city.point.y}/forecast`);
|
||||
// determine today or tomorrow (shift periods by 1 if tomorrow)
|
||||
const todayShift = forecast.properties.periods[0].isDaytime ? 0 : 1;
|
||||
// return a pared-down forecast
|
||||
@@ -39,7 +44,7 @@ class TravelForecast extends WeatherDisplay {
|
||||
high: forecast.properties.periods[todayShift].temperature,
|
||||
low: forecast.properties.periods[todayShift + 1].temperature,
|
||||
name: city.Name,
|
||||
icon: icons.getWeatherRegionalIconFromIconLink(forecast.properties.periods[todayShift].icon),
|
||||
icon: getWeatherRegionalIconFromIconLink(forecast.properties.periods[todayShift].icon),
|
||||
};
|
||||
} catch (e) {
|
||||
console.error(`GetTravelWeather for ${city.Name} failed`);
|
||||
@@ -85,8 +90,8 @@ class TravelForecast extends WeatherDisplay {
|
||||
let { low, high } = city;
|
||||
|
||||
if (navigation.units() === UNITS.metric) {
|
||||
low = utils.units.fahrenheitToCelsius(low);
|
||||
high = utils.units.fahrenheitToCelsius(high);
|
||||
low = fahrenheitToCelsius(low);
|
||||
high = fahrenheitToCelsius(high);
|
||||
}
|
||||
|
||||
// convert to strings with no decimal
|
||||
@@ -142,7 +147,6 @@ class TravelForecast extends WeatherDisplay {
|
||||
}
|
||||
|
||||
static getTravelCitiesDayName(cities) {
|
||||
const { DateTime } = luxon;
|
||||
// effectively returns early on the first found date
|
||||
return cities.reduce((dayName, city) => {
|
||||
if (city && dayName === '') {
|
||||
@@ -160,3 +164,5 @@ class TravelForecast extends WeatherDisplay {
|
||||
return this.longCanvas;
|
||||
}
|
||||
}
|
||||
|
||||
window.TravelForecast = TravelForecast;
|
||||
62
server/scripts/modules/utils/calc.mjs
Normal file
62
server/scripts/modules/utils/calc.mjs
Normal file
@@ -0,0 +1,62 @@
|
||||
const relativeHumidity = (Temperature, DewPoint) => {
|
||||
const T = Temperature;
|
||||
const TD = DewPoint;
|
||||
return Math.round(100 * (Math.exp((17.625 * TD) / (243.04 + TD)) / Math.exp((17.625 * T) / (243.04 + T))));
|
||||
};
|
||||
|
||||
const heatIndex = (Temperature, RelativeHumidity) => {
|
||||
const T = Temperature;
|
||||
const RH = RelativeHumidity;
|
||||
let HI = 0.5 * (T + 61.0 + ((T - 68.0) * 1.2) + (RH * 0.094));
|
||||
let ADJUSTMENT;
|
||||
|
||||
if (T >= 80) {
|
||||
HI = -42.379 + 2.04901523 * T + 10.14333127 * RH - 0.22475541 * T * RH - 0.00683783 * T * T - 0.05481717 * RH * RH + 0.00122874 * T * T * RH + 0.00085282 * T * RH * RH - 0.00000199 * T * T * RH * RH;
|
||||
|
||||
if (RH < 13 && (T > 80 && T < 112)) {
|
||||
ADJUSTMENT = ((13 - RH) / 4) * Math.sqrt((17 - Math.abs(T - 95)) / 17);
|
||||
HI -= ADJUSTMENT;
|
||||
} else if (RH > 85 && (T > 80 && T < 87)) {
|
||||
ADJUSTMENT = ((RH - 85) / 10) * ((87 - T) / 5);
|
||||
HI += ADJUSTMENT;
|
||||
}
|
||||
}
|
||||
|
||||
if (HI < Temperature) {
|
||||
HI = Temperature;
|
||||
}
|
||||
|
||||
return Math.round(HI);
|
||||
};
|
||||
|
||||
const windChill = (Temperature, WindSpeed) => {
|
||||
if (WindSpeed === '0' || WindSpeed === 'Calm' || WindSpeed === 'NA') {
|
||||
return '';
|
||||
}
|
||||
|
||||
const T = Temperature;
|
||||
const V = WindSpeed;
|
||||
|
||||
return Math.round(35.74 + (0.6215 * T) - (35.75 * (V ** 0.16)) + (0.4275 * T * (V ** 0.16)));
|
||||
};
|
||||
|
||||
// wind direction
|
||||
const directionToNSEW = (Direction) => {
|
||||
const val = Math.floor((Direction / 22.5) + 0.5);
|
||||
const arr = ['N', 'NNE', 'NE', 'ENE', 'E', 'ESE', 'SE', 'SSE', 'S', 'SSW', 'SW', 'WSW', 'W', 'WNW', 'NW', 'NNW'];
|
||||
return arr[(val % 16)];
|
||||
};
|
||||
|
||||
const distance = (x1, y1, x2, y2) => Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2);
|
||||
|
||||
// wrap a number to 0-m
|
||||
const wrap = (x, m) => ((x % m) + m) % m;
|
||||
|
||||
export {
|
||||
relativeHumidity,
|
||||
heatIndex,
|
||||
windChill,
|
||||
directionToNSEW,
|
||||
distance,
|
||||
wrap,
|
||||
};
|
||||
12
server/scripts/modules/utils/cors.mjs
Normal file
12
server/scripts/modules/utils/cors.mjs
Normal file
@@ -0,0 +1,12 @@
|
||||
// rewrite some urls for local server
|
||||
const rewriteUrl = (_url) => {
|
||||
let url = _url;
|
||||
url = url.replace('https://api.weather.gov/', window.location.href);
|
||||
url = url.replace('https://www.cpc.ncep.noaa.gov/', window.location.href);
|
||||
return url;
|
||||
};
|
||||
|
||||
export {
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
rewriteUrl,
|
||||
};
|
||||
8
server/scripts/modules/utils/elem.mjs
Normal file
8
server/scripts/modules/utils/elem.mjs
Normal file
@@ -0,0 +1,8 @@
|
||||
const elemForEach = (selector, callback) => {
|
||||
[...document.querySelectorAll(selector)].forEach(callback);
|
||||
};
|
||||
|
||||
export {
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
elemForEach,
|
||||
};
|
||||
55
server/scripts/modules/utils/fetch.mjs
Normal file
55
server/scripts/modules/utils/fetch.mjs
Normal file
@@ -0,0 +1,55 @@
|
||||
import { rewriteUrl } from './cors.mjs';
|
||||
|
||||
const json = (url, params) => fetchAsync(url, 'json', params);
|
||||
const text = (url, params) => fetchAsync(url, 'text', params);
|
||||
const raw = (url, params) => fetchAsync(url, '', params);
|
||||
const blob = (url, params) => fetchAsync(url, 'blob', params);
|
||||
|
||||
const fetchAsync = async (_url, responseType, _params = {}) => {
|
||||
// combine default and provided parameters
|
||||
const params = {
|
||||
method: 'GET',
|
||||
mode: 'cors',
|
||||
type: 'GET',
|
||||
..._params,
|
||||
};
|
||||
// build a url, including the rewrite for cors if necessary
|
||||
let corsUrl = _url;
|
||||
if (params.cors === true) corsUrl = rewriteUrl(_url);
|
||||
const url = new URL(corsUrl, `${window.location.origin}/`);
|
||||
// match the security protocol when not on localhost
|
||||
url.protocol = window.location.hostname !== 'localhost' ? window.location.protocol : url.protocol;
|
||||
// add parameters if necessary
|
||||
if (params.data) {
|
||||
Object.keys(params.data).forEach((key) => {
|
||||
// get the value
|
||||
const value = params.data[key];
|
||||
// add to the url
|
||||
url.searchParams.append(key, value);
|
||||
});
|
||||
}
|
||||
|
||||
// make the request
|
||||
const response = await fetch(url, params);
|
||||
|
||||
// check for ok response
|
||||
if (!response.ok) throw new Error(`Fetch error ${response.status} ${response.statusText} while fetching ${response.url}`);
|
||||
// return the requested response
|
||||
switch (responseType) {
|
||||
case 'json':
|
||||
return response.json();
|
||||
case 'text':
|
||||
return response.text();
|
||||
case 'blob':
|
||||
return response.blob();
|
||||
default:
|
||||
return response;
|
||||
}
|
||||
};
|
||||
|
||||
export {
|
||||
json,
|
||||
text,
|
||||
raw,
|
||||
blob,
|
||||
};
|
||||
34
server/scripts/modules/utils/image.mjs
Normal file
34
server/scripts/modules/utils/image.mjs
Normal file
@@ -0,0 +1,34 @@
|
||||
import { blob } from './fetch.mjs';
|
||||
import { rewriteUrl } from './cors.mjs';
|
||||
|
||||
// ****************************** load images *********************************
|
||||
// load an image from a blob or url
|
||||
const loadImg = (imgData, cors = false) => new Promise((resolve) => {
|
||||
const img = new Image();
|
||||
img.onload = (e) => {
|
||||
resolve(e.target);
|
||||
};
|
||||
if (imgData instanceof Blob) {
|
||||
img.src = window.URL.createObjectURL(imgData);
|
||||
} else {
|
||||
let url = imgData;
|
||||
if (cors) url = rewriteUrl(imgData);
|
||||
img.src = url;
|
||||
}
|
||||
});
|
||||
|
||||
// preload an image
|
||||
// the goal is to get it in the browser's cache so it is available more quickly when the browser needs it
|
||||
// a list of cached icons is used to avoid hitting the cache multiple times
|
||||
const cachedImages = [];
|
||||
const preloadImg = (src) => {
|
||||
if (cachedImages.includes(src)) return false;
|
||||
blob(src);
|
||||
// cachedImages.push(src);
|
||||
return true;
|
||||
};
|
||||
|
||||
export {
|
||||
loadImg,
|
||||
preloadImg,
|
||||
};
|
||||
19
server/scripts/modules/utils/string.mjs
Normal file
19
server/scripts/modules/utils/string.mjs
Normal file
@@ -0,0 +1,19 @@
|
||||
const locationCleanup = (input) => {
|
||||
// regexes to run
|
||||
const regexes = [
|
||||
// "Chicago / West Chicago", removes before slash
|
||||
/^[A-Za-z ]+ \/ /,
|
||||
// "Chicago/Waukegan" removes before slash
|
||||
/^[A-Za-z ]+\//,
|
||||
// "Chicago, Chicago O'hare" removes before comma
|
||||
/^[A-Za-z ]+, /,
|
||||
];
|
||||
|
||||
// run all regexes
|
||||
return regexes.reduce((value, regex) => value.replace(regex, ''), input);
|
||||
};
|
||||
|
||||
export {
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
locationCleanup,
|
||||
};
|
||||
25
server/scripts/modules/utils/units.mjs
Normal file
25
server/scripts/modules/utils/units.mjs
Normal file
@@ -0,0 +1,25 @@
|
||||
const round2 = (value, decimals) => Number(`${Math.round(`${value}e${decimals}`)}e-${decimals}`);
|
||||
|
||||
const mphToKph = (Mph) => Math.round(Mph * 1.60934);
|
||||
const kphToMph = (Kph) => Math.round(Kph / 1.60934);
|
||||
const celsiusToFahrenheit = (Celsius) => Math.round((Celsius * 9) / 5 + 32);
|
||||
const fahrenheitToCelsius = (Fahrenheit) => round2((((Fahrenheit) - 32) * 5) / 9, 1);
|
||||
const milesToKilometers = (Miles) => Math.round(Miles * 1.60934);
|
||||
const kilometersToMiles = (Kilometers) => Math.round(Kilometers / 1.60934);
|
||||
const feetToMeters = (Feet) => Math.round(Feet * 0.3048);
|
||||
const metersToFeet = (Meters) => Math.round(Meters / 0.3048);
|
||||
const inchesToCentimeters = (Inches) => round2(Inches * 2.54, 2);
|
||||
const pascalToInHg = (Pascal) => round2(Pascal * 0.0002953, 2);
|
||||
|
||||
export {
|
||||
mphToKph,
|
||||
kphToMph,
|
||||
celsiusToFahrenheit,
|
||||
fahrenheitToCelsius,
|
||||
milesToKilometers,
|
||||
kilometersToMiles,
|
||||
feetToMeters,
|
||||
metersToFeet,
|
||||
inchesToCentimeters,
|
||||
pascalToInHg,
|
||||
};
|
||||
@@ -1,14 +1,6 @@
|
||||
// base weather display class
|
||||
|
||||
/* globals navigation, utils, luxon, currentWeatherScroll */
|
||||
|
||||
const STATUS = {
|
||||
loading: Symbol('loading'),
|
||||
loaded: Symbol('loaded'),
|
||||
failed: Symbol('failed'),
|
||||
noData: Symbol('noData'),
|
||||
disabled: Symbol('disabled'),
|
||||
};
|
||||
/* globals navigation, utils, luxon, currentWeatherScroll, STATUS */
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
class WeatherDisplay {
|
||||
|
||||
Reference in New Issue
Block a user