weather displays complete

This commit is contained in:
Matt Walsh
2022-11-22 16:19:10 -06:00
parent c28608bb39
commit cc61d2c6d1
34 changed files with 8106 additions and 9251 deletions

View File

@@ -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;

View File

@@ -4,8 +4,8 @@ const UNITS = {
};
export {
// eslint-disable-next-line import/prefer-default-export
UNITS,
};
window.UNITS = UNITS;
console.log('config');

View File

@@ -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;

View File

@@ -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,
};
})();

View 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,
};

View File

@@ -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,
};
})();

View File

@@ -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;

View File

@@ -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;

View File

@@ -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,
};
})();

View 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,
};

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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 : `&nbsp;${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;

View File

@@ -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;

View 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;

View File

@@ -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;

View 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,
};

View 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,
};

View File

@@ -0,0 +1,8 @@
const elemForEach = (selector, callback) => {
[...document.querySelectorAll(selector)].forEach(callback);
};
export {
// eslint-disable-next-line import/prefer-default-export
elemForEach,
};

View 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,
};

View 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,
};

View 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,
};

View 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,
};

View File

@@ -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 {