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

@ -12,7 +12,7 @@ module.exports = {
SharedArrayBuffer: 'readonly',
},
parserOptions: {
ecmaVersion: 2020,
ecmaVersion: 2021,
},
rules: {
indent: [
@ -46,6 +46,13 @@ module.exports = {
allowSamePrecedence: true,
},
],
'import/extensions': [
'error',
{
mjs: 'always',
json: 'always',
},
],
},
ignorePatterns: [
'*.min.js',

View file

@ -35,7 +35,6 @@ const jsSources = [
'server/scripts/index.js',
'server/scripts/vendor/auto/luxon.js',
'server/scripts/vendor/auto/suncalc.js',
'server/scripts/modules/draw.js',
'server/scripts/modules/weatherdisplay.js',
'server/scripts/modules/icons.js',
'server/scripts/modules/utilities.js',

View file

@ -9,8 +9,8 @@ const clean = (cb) => {
};
const vendorFiles = [
'./node_modules/luxon/build/global/luxon.js',
'./node_modules/luxon/build/global/luxon.js.map',
'./node_modules/luxon/build/es6/luxon.js',
'./node_modules/luxon/build/es6/luxon.js.map',
'./node_modules/nosleep.js/dist/NoSleep.js',
'./node_modules/jquery/dist/jquery.js',
'./node_modules/suncalc/suncalc.js',
@ -22,6 +22,7 @@ const copy = () => gulp.src(vendorFiles)
path.dirname = path.dirname.toLowerCase();
path.basename = path.basename.toLowerCase();
path.extname = path.extname.toLowerCase();
if (path.basename === 'luxon') path.extname = '.mjs';
}))
.pipe(gulp.dest('./server/scripts/vendor/auto'));

View file

@ -1,6 +1,7 @@
import { UNITS } from './modules/config.mjs';
import { json } from './modules/utils/fetch.mjs';
/* globals NoSleep, states, navigation, utils */
/* globals NoSleep, states, navigation */
document.addEventListener('DOMContentLoaded', () => {
init();
});
@ -175,7 +176,7 @@ const autocompleteOnSelect = async (suggestion, elem) => {
if (overrides[suggestion.value]) {
doRedirectToGeometry(overrides[suggestion.value]);
} else {
const data = await utils.fetch.json('https://geocode.arcgis.com/arcgis/rest/services/World/GeocodeServer/find', {
const data = await json('https://geocode.arcgis.com/arcgis/rest/services/World/GeocodeServer/find', {
data: {
text: suggestion.value,
magicKey: suggestion.data,
@ -492,7 +493,7 @@ const btnGetGpsClick = async () => {
let data;
try {
data = await utils.fetch.json('https://geocode.arcgis.com/arcgis/rest/services/World/GeocodeServer/reverseGeocode', {
data = await json('https://geocode.arcgis.com/arcgis/rest/services/World/GeocodeServer/reverseGeocode', {
data: {
location: `${longitude},${latitude}`,
distance: 1000, // Find location up to 1 KM.

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 {

View file

@ -1,5 +1,5 @@
/*!
* jQuery JavaScript Library v3.6.0
* jQuery JavaScript Library v3.6.1
* https://jquery.com/
*
* Includes Sizzle.js
@ -9,7 +9,7 @@
* Released under the MIT license
* https://jquery.org/license
*
* Date: 2021-03-02T17:08Z
* Date: 2022-08-26T17:52Z
*/
( function( global, factory ) {
@ -23,7 +23,7 @@
// (such as Node.js), expose a factory as module.exports.
// This accentuates the need for the creation of a real `window`.
// e.g. var jQuery = require("jquery")(window);
// See ticket #14549 for more info.
// See ticket trac-14549 for more info.
module.exports = global.document ?
factory( global, true ) :
function( w ) {
@ -151,7 +151,7 @@ function toType( obj ) {
var
version = "3.6.0",
version = "3.6.1",
// Define a local copy of jQuery
jQuery = function( selector, context ) {
@ -3129,8 +3129,8 @@ jQuery.fn.extend( {
var rootjQuery,
// A simple way to check for HTML strings
// Prioritize #id over <tag> to avoid XSS via location.hash (#9521)
// Strict HTML recognition (#11290: must start with <)
// Prioritize #id over <tag> to avoid XSS via location.hash (trac-9521)
// Strict HTML recognition (trac-11290: must start with <)
// Shortcut simple #id case for speed
rquickExpr = /^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]+))$/,
@ -4087,7 +4087,7 @@ jQuery.extend( {
isReady: false,
// A counter to track how many items to wait for before
// the ready event fires. See #6781
// the ready event fires. See trac-6781
readyWait: 1,
// Handle when the DOM is ready
@ -4215,7 +4215,7 @@ function fcamelCase( _all, letter ) {
// Convert dashed to camelCase; used by the css and data modules
// Support: IE <=9 - 11, Edge 12 - 15
// Microsoft forgot to hump their vendor prefix (#9572)
// Microsoft forgot to hump their vendor prefix (trac-9572)
function camelCase( string ) {
return string.replace( rmsPrefix, "ms-" ).replace( rdashAlpha, fcamelCase );
}
@ -4251,7 +4251,7 @@ Data.prototype = {
value = {};
// We can accept data for non-element nodes in modern browsers,
// but we should not, see #8335.
// but we should not, see trac-8335.
// Always return an empty object.
if ( acceptData( owner ) ) {
@ -4490,7 +4490,7 @@ jQuery.fn.extend( {
while ( i-- ) {
// Support: IE 11 only
// The attrs elements can be null (#14894)
// The attrs elements can be null (trac-14894)
if ( attrs[ i ] ) {
name = attrs[ i ].name;
if ( name.indexOf( "data-" ) === 0 ) {
@ -4913,9 +4913,9 @@ var rscriptType = ( /^$|^module$|\/(?:java|ecma)script/i );
input = document.createElement( "input" );
// Support: Android 4.0 - 4.3 only
// Check state lost if the name is set (#11217)
// Check state lost if the name is set (trac-11217)
// Support: Windows Web Apps (WWA)
// `name` and `type` must use .setAttribute for WWA (#14901)
// `name` and `type` must use .setAttribute for WWA (trac-14901)
input.setAttribute( "type", "radio" );
input.setAttribute( "checked", "checked" );
input.setAttribute( "name", "t" );
@ -4939,7 +4939,7 @@ var rscriptType = ( /^$|^module$|\/(?:java|ecma)script/i );
} )();
// We have to close these tags to support XHTML (#13200)
// We have to close these tags to support XHTML (trac-13200)
var wrapMap = {
// XHTML parsers do not magically insert elements in the
@ -4965,7 +4965,7 @@ if ( !support.option ) {
function getAll( context, tag ) {
// Support: IE <=9 - 11 only
// Use typeof to avoid zero-argument method invocation on host objects (#15151)
// Use typeof to avoid zero-argument method invocation on host objects (trac-15151)
var ret;
if ( typeof context.getElementsByTagName !== "undefined" ) {
@ -5048,7 +5048,7 @@ function buildFragment( elems, context, scripts, selection, ignored ) {
// Remember the top-level container
tmp = fragment.firstChild;
// Ensure the created nodes are orphaned (#12392)
// Ensure the created nodes are orphaned (trac-12392)
tmp.textContent = "";
}
}
@ -5469,15 +5469,15 @@ jQuery.event = {
for ( ; cur !== this; cur = cur.parentNode || this ) {
// Don't check non-elements (#13208)
// Don't process clicks on disabled elements (#6911, #8165, #11382, #11764)
// Don't check non-elements (trac-13208)
// Don't process clicks on disabled elements (trac-6911, trac-8165, trac-11382, trac-11764)
if ( cur.nodeType === 1 && !( event.type === "click" && cur.disabled === true ) ) {
matchedHandlers = [];
matchedSelectors = {};
for ( i = 0; i < delegateCount; i++ ) {
handleObj = handlers[ i ];
// Don't conflict with Object.prototype properties (#13203)
// Don't conflict with Object.prototype properties (trac-13203)
sel = handleObj.selector + " ";
if ( matchedSelectors[ sel ] === undefined ) {
@ -5731,7 +5731,7 @@ jQuery.Event = function( src, props ) {
// Create target properties
// Support: Safari <=6 - 7 only
// Target should not be a text node (#504, #13143)
// Target should not be a text node (trac-504, trac-13143)
this.target = ( src.target && src.target.nodeType === 3 ) ?
src.target.parentNode :
src.target;
@ -5854,10 +5854,10 @@ jQuery.each( { focus: "focusin", blur: "focusout" }, function( type, delegateTyp
return true;
},
// Suppress native focus or blur as it's already being fired
// in leverageNative.
_default: function() {
return true;
// Suppress native focus or blur if we're currently inside
// a leveraged native-event stack
_default: function( event ) {
return dataPriv.get( event.target, type );
},
delegateType: delegateType
@ -5956,7 +5956,8 @@ var
// checked="checked" or checked
rchecked = /checked\s*(?:[^=]|=\s*.checked.)/i,
rcleanScript = /^\s*<!(?:\[CDATA\[|--)|(?:\]\]|--)>\s*$/g;
rcleanScript = /^\s*<!\[CDATA\[|\]\]>\s*$/g;
// Prefer a tbody over its parent table for containing new rows
function manipulationTarget( elem, content ) {
@ -6070,7 +6071,7 @@ function domManip( collection, args, callback, ignored ) {
// Use the original fragment for the last item
// instead of the first because it can end up
// being emptied incorrectly in certain situations (#8070).
// being emptied incorrectly in certain situations (trac-8070).
for ( ; i < l; i++ ) {
node = fragment;
@ -6111,6 +6112,12 @@ function domManip( collection, args, callback, ignored ) {
}, doc );
}
} else {
// Unwrap a CDATA section containing script contents. This shouldn't be
// needed as in XML documents they're already not visible when
// inspecting element contents and in HTML documents they have no
// meaning but we're preserving that logic for backwards compatibility.
// This will be removed completely in 4.0. See gh-4904.
DOMEval( node.textContent.replace( rcleanScript, "" ), node, doc );
}
}
@ -6393,9 +6400,12 @@ jQuery.each( {
} );
var rnumnonpx = new RegExp( "^(" + pnum + ")(?!px)[a-z%]+$", "i" );
var rcustomProp = /^--/;
var getStyles = function( elem ) {
// Support: IE <=11 only, Firefox <=30 (#15098, #14150)
// Support: IE <=11 only, Firefox <=30 (trac-15098, trac-14150)
// IE throws on elements created in popups
// FF meanwhile throws on frame elements through "defaultView.getComputedStyle"
var view = elem.ownerDocument.defaultView;
@ -6430,6 +6440,15 @@ var swap = function( elem, options, callback ) {
var rboxStyle = new RegExp( cssExpand.join( "|" ), "i" );
var whitespace = "[\\x20\\t\\r\\n\\f]";
var rtrimCSS = new RegExp(
"^" + whitespace + "+|((?:^|[^\\\\])(?:\\\\.)*)" + whitespace + "+$",
"g"
);
( function() {
@ -6495,7 +6514,7 @@ var rboxStyle = new RegExp( cssExpand.join( "|" ), "i" );
}
// Support: IE <=9 - 11 only
// Style of cloned element affects source element cloned (#8908)
// Style of cloned element affects source element cloned (trac-8908)
div.style.backgroundClip = "content-box";
div.cloneNode( true ).style.backgroundClip = "";
support.clearCloneStyle = div.style.backgroundClip === "content-box";
@ -6575,6 +6594,7 @@ var rboxStyle = new RegExp( cssExpand.join( "|" ), "i" );
function curCSS( elem, name, computed ) {
var width, minWidth, maxWidth, ret,
isCustomProp = rcustomProp.test( name ),
// Support: Firefox 51+
// Retrieving style before computed somehow
@ -6585,11 +6605,22 @@ function curCSS( elem, name, computed ) {
computed = computed || getStyles( elem );
// getPropertyValue is needed for:
// .css('filter') (IE 9 only, #12537)
// .css('--customProperty) (#3144)
// .css('filter') (IE 9 only, trac-12537)
// .css('--customProperty) (gh-3144)
if ( computed ) {
ret = computed.getPropertyValue( name ) || computed[ name ];
// trim whitespace for custom property (issue gh-4926)
if ( isCustomProp ) {
// rtrim treats U+000D CARRIAGE RETURN and U+000C FORM FEED
// as whitespace while CSS does not, but this is not a problem
// because CSS preprocessing replaces them with U+000A LINE FEED
// (which *is* CSS whitespace)
// https://www.w3.org/TR/css-syntax-3/#input-preprocessing
ret = ret.replace( rtrimCSS, "$1" );
}
if ( ret === "" && !isAttached( elem ) ) {
ret = jQuery.style( elem, name );
}
@ -6685,7 +6716,6 @@ var
// except "table", "table-cell", or "table-caption"
// See here for display values: https://developer.mozilla.org/en-US/docs/CSS/display
rdisplayswap = /^(none|table(?!-c[ea]).+)/,
rcustomProp = /^--/,
cssShow = { position: "absolute", visibility: "hidden", display: "block" },
cssNormalTransform = {
letterSpacing: "0",
@ -6921,15 +6951,15 @@ jQuery.extend( {
if ( value !== undefined ) {
type = typeof value;
// Convert "+=" or "-=" to relative numbers (#7345)
// Convert "+=" or "-=" to relative numbers (trac-7345)
if ( type === "string" && ( ret = rcssNum.exec( value ) ) && ret[ 1 ] ) {
value = adjustCSS( elem, name, ret );
// Fixes bug #9237
// Fixes bug trac-9237
type = "number";
}
// Make sure that null and NaN values aren't set (#7116)
// Make sure that null and NaN values aren't set (trac-7116)
if ( value == null || value !== value ) {
return;
}
@ -7553,7 +7583,7 @@ function Animation( elem, properties, options ) {
remaining = Math.max( 0, animation.startTime + animation.duration - currentTime ),
// Support: Android 2.3 only
// Archaic crash bug won't allow us to use `1 - ( 0.5 || 0 )` (#12497)
// Archaic crash bug won't allow us to use `1 - ( 0.5 || 0 )` (trac-12497)
temp = remaining / animation.duration || 0,
percent = 1 - temp,
index = 0,
@ -7943,7 +7973,6 @@ jQuery.fx.speeds = {
// Based off of the plugin by Clint Helfers, with permission.
// https://web.archive.org/web/20100324014747/http://blindsignals.com/index.php/2009/07/jquery-delay/
jQuery.fn.delay = function( time, type ) {
time = jQuery.fx ? jQuery.fx.speeds[ time ] || time : time;
type = type || "fx";
@ -8168,8 +8197,7 @@ jQuery.extend( {
// Support: IE <=9 - 11 only
// elem.tabIndex doesn't always return the
// correct value when it hasn't been explicitly set
// https://web.archive.org/web/20141116233347/http://fluidproject.org/blog/2008/01/09/getting-setting-and-removing-tabindex-values-with-javascript/
// Use proper attribute retrieval(#12072)
// Use proper attribute retrieval (trac-12072)
var tabindex = jQuery.find.attr( elem, "tabindex" );
if ( tabindex ) {
@ -8273,8 +8301,7 @@ function classesToArray( value ) {
jQuery.fn.extend( {
addClass: function( value ) {
var classes, elem, cur, curValue, clazz, j, finalValue,
i = 0;
var classNames, cur, curValue, className, i, finalValue;
if ( isFunction( value ) ) {
return this.each( function( j ) {
@ -8282,36 +8309,35 @@ jQuery.fn.extend( {
} );
}
classes = classesToArray( value );
classNames = classesToArray( value );
if ( classes.length ) {
while ( ( elem = this[ i++ ] ) ) {
curValue = getClass( elem );
cur = elem.nodeType === 1 && ( " " + stripAndCollapse( curValue ) + " " );
if ( classNames.length ) {
return this.each( function() {
curValue = getClass( this );
cur = this.nodeType === 1 && ( " " + stripAndCollapse( curValue ) + " " );
if ( cur ) {
j = 0;
while ( ( clazz = classes[ j++ ] ) ) {
if ( cur.indexOf( " " + clazz + " " ) < 0 ) {
cur += clazz + " ";
for ( i = 0; i < classNames.length; i++ ) {
className = classNames[ i ];
if ( cur.indexOf( " " + className + " " ) < 0 ) {
cur += className + " ";
}
}
// Only assign if different to avoid unneeded rendering.
finalValue = stripAndCollapse( cur );
if ( curValue !== finalValue ) {
elem.setAttribute( "class", finalValue );
this.setAttribute( "class", finalValue );
}
}
}
} );
}
return this;
},
removeClass: function( value ) {
var classes, elem, cur, curValue, clazz, j, finalValue,
i = 0;
var classNames, cur, curValue, className, i, finalValue;
if ( isFunction( value ) ) {
return this.each( function( j ) {
@ -8323,45 +8349,42 @@ jQuery.fn.extend( {
return this.attr( "class", "" );
}
classes = classesToArray( value );
classNames = classesToArray( value );
if ( classes.length ) {
while ( ( elem = this[ i++ ] ) ) {
curValue = getClass( elem );
if ( classNames.length ) {
return this.each( function() {
curValue = getClass( this );
// This expression is here for better compressibility (see addClass)
cur = elem.nodeType === 1 && ( " " + stripAndCollapse( curValue ) + " " );
cur = this.nodeType === 1 && ( " " + stripAndCollapse( curValue ) + " " );
if ( cur ) {
j = 0;
while ( ( clazz = classes[ j++ ] ) ) {
for ( i = 0; i < classNames.length; i++ ) {
className = classNames[ i ];
// Remove *all* instances
while ( cur.indexOf( " " + clazz + " " ) > -1 ) {
cur = cur.replace( " " + clazz + " ", " " );
while ( cur.indexOf( " " + className + " " ) > -1 ) {
cur = cur.replace( " " + className + " ", " " );
}
}
// Only assign if different to avoid unneeded rendering.
finalValue = stripAndCollapse( cur );
if ( curValue !== finalValue ) {
elem.setAttribute( "class", finalValue );
this.setAttribute( "class", finalValue );
}
}
}
} );
}
return this;
},
toggleClass: function( value, stateVal ) {
var type = typeof value,
var classNames, className, i, self,
type = typeof value,
isValidValue = type === "string" || Array.isArray( value );
if ( typeof stateVal === "boolean" && isValidValue ) {
return stateVal ? this.addClass( value ) : this.removeClass( value );
}
if ( isFunction( value ) ) {
return this.each( function( i ) {
jQuery( this ).toggleClass(
@ -8371,17 +8394,20 @@ jQuery.fn.extend( {
} );
}
return this.each( function() {
var className, i, self, classNames;
if ( typeof stateVal === "boolean" && isValidValue ) {
return stateVal ? this.addClass( value ) : this.removeClass( value );
}
classNames = classesToArray( value );
return this.each( function() {
if ( isValidValue ) {
// Toggle individual class names
i = 0;
self = jQuery( this );
classNames = classesToArray( value );
while ( ( className = classNames[ i++ ] ) ) {
for ( i = 0; i < classNames.length; i++ ) {
className = classNames[ i ];
// Check each className given, space separated list
if ( self.hasClass( className ) ) {
@ -8515,7 +8541,7 @@ jQuery.extend( {
val :
// Support: IE <=10 - 11 only
// option.text throws exceptions (#14686, #14858)
// option.text throws exceptions (trac-14686, trac-14858)
// Strip and collapse whitespace
// https://html.spec.whatwg.org/#strip-and-collapse-whitespace
stripAndCollapse( jQuery.text( elem ) );
@ -8542,7 +8568,7 @@ jQuery.extend( {
option = options[ i ];
// Support: IE <=9 only
// IE8-9 doesn't update selected after form reset (#2551)
// IE8-9 doesn't update selected after form reset (trac-2551)
if ( ( option.selected || i === index ) &&
// Don't return options that are disabled or in a disabled optgroup
@ -8685,8 +8711,8 @@ jQuery.extend( jQuery.event, {
return;
}
// Determine event propagation path in advance, per W3C events spec (#9951)
// Bubble up to document, then to window; watch for a global ownerDocument var (#9724)
// Determine event propagation path in advance, per W3C events spec (trac-9951)
// Bubble up to document, then to window; watch for a global ownerDocument var (trac-9724)
if ( !onlyHandlers && !special.noBubble && !isWindow( elem ) ) {
bubbleType = special.delegateType || type;
@ -8738,7 +8764,7 @@ jQuery.extend( jQuery.event, {
acceptData( elem ) ) {
// Call a native DOM method on the target with the same name as the event.
// Don't do default actions on window, that's where global variables be (#6170)
// Don't do default actions on window, that's where global variables be (trac-6170)
if ( ontype && isFunction( elem[ type ] ) && !isWindow( elem ) ) {
// Don't re-trigger an onFOO event when we call its FOO() method
@ -9012,7 +9038,7 @@ var
rantiCache = /([?&])_=[^&]*/,
rheaders = /^(.*?):[ \t]*([^\r\n]*)$/mg,
// #7653, #8125, #8152: local protocol detection
// trac-7653, trac-8125, trac-8152: local protocol detection
rlocalProtocol = /^(?:about|app|app-storage|.+-extension|file|res|widget):$/,
rnoContent = /^(?:GET|HEAD)$/,
rprotocol = /^\/\//,
@ -9035,7 +9061,7 @@ var
*/
transports = {},
// Avoid comment-prolog char sequence (#10098); must appease lint and evade compression
// Avoid comment-prolog char sequence (trac-10098); must appease lint and evade compression
allTypes = "*/".concat( "*" ),
// Anchor tag for parsing the document origin
@ -9106,7 +9132,7 @@ function inspectPrefiltersOrTransports( structure, options, originalOptions, jqX
// A special extend for ajax options
// that takes "flat" options (not to be deep extended)
// Fixes #9887
// Fixes trac-9887
function ajaxExtend( target, src ) {
var key, deep,
flatOptions = jQuery.ajaxSettings.flatOptions || {};
@ -9517,12 +9543,12 @@ jQuery.extend( {
deferred.promise( jqXHR );
// Add protocol if not provided (prefilters might expect it)
// Handle falsy url in the settings object (#10093: consistency with old signature)
// Handle falsy url in the settings object (trac-10093: consistency with old signature)
// We also use the url parameter if available
s.url = ( ( url || s.url || location.href ) + "" )
.replace( rprotocol, location.protocol + "//" );
// Alias method option to type as per ticket #12004
// Alias method option to type as per ticket trac-12004
s.type = options.method || options.type || s.method || s.type;
// Extract dataTypes list
@ -9565,7 +9591,7 @@ jQuery.extend( {
}
// We can fire global events as of now if asked to
// Don't fire events if jQuery.event is undefined in an AMD-usage scenario (#15118)
// Don't fire events if jQuery.event is undefined in an AMD-usage scenario (trac-15118)
fireGlobals = jQuery.event && s.global;
// Watch for a new set of requests
@ -9594,7 +9620,7 @@ jQuery.extend( {
if ( s.data && ( s.processData || typeof s.data === "string" ) ) {
cacheURL += ( rquery.test( cacheURL ) ? "&" : "?" ) + s.data;
// #9682: remove data so that it's not used in an eventual retry
// trac-9682: remove data so that it's not used in an eventual retry
delete s.data;
}
@ -9867,7 +9893,7 @@ jQuery._evalUrl = function( url, options, doc ) {
return jQuery.ajax( {
url: url,
// Make this explicit, since user can override this through ajaxSetup (#11264)
// Make this explicit, since user can override this through ajaxSetup (trac-11264)
type: "GET",
dataType: "script",
cache: true,
@ -9976,7 +10002,7 @@ var xhrSuccessStatus = {
0: 200,
// Support: IE <=9 only
// #1450: sometimes IE returns 1223 when it should be 204
// trac-1450: sometimes IE returns 1223 when it should be 204
1223: 204
},
xhrSupported = jQuery.ajaxSettings.xhr();
@ -10048,7 +10074,7 @@ jQuery.ajaxTransport( function( options ) {
} else {
complete(
// File: protocol always yields status 0; see #8605, #14207
// File: protocol always yields status 0; see trac-8605, trac-14207
xhr.status,
xhr.statusText
);
@ -10109,7 +10135,7 @@ jQuery.ajaxTransport( function( options ) {
xhr.send( options.hasContent && options.data || null );
} catch ( e ) {
// #14683: Only rethrow if this hasn't been notified as an error yet
// trac-14683: Only rethrow if this hasn't been notified as an error yet
if ( callback ) {
throw e;
}
@ -10753,7 +10779,9 @@ jQuery.each(
// Support: Android <=4.0 only
// Make sure we trim BOM and NBSP
var rtrim = /^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g;
// Require that the "whitespace run" starts from a non-whitespace
// to avoid O(N^2) behavior when the engine would try matching "\s+$" at each space position.
var rtrim = /^[\s\uFEFF\xA0]+|([^\s\uFEFF\xA0])[\s\uFEFF\xA0]+$/g;
// Bind a function to a context, optionally partially applying any
// arguments.
@ -10820,7 +10848,7 @@ jQuery.isNumeric = function( obj ) {
jQuery.trim = function( text ) {
return text == null ?
"" :
( text + "" ).replace( rtrim, "" );
( text + "" ).replace( rtrim, "$1" );
};
@ -10868,8 +10896,8 @@ jQuery.noConflict = function( deep ) {
};
// Expose jQuery and $ identifiers, even in AMD
// (#7102#comment:10, https://github.com/jquery/jquery/pull/557)
// and CommonJS for browser emulators (#13566)
// (trac-7102#comment:10, https://github.com/jquery/jquery/pull/557)
// and CommonJS for browser emulators (trac-13566)
if ( typeof noGlobal === "undefined" ) {
window.jQuery = window.$ = jQuery;
}

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

7120
server/scripts/vendor/auto/luxon.mjs vendored Normal file

File diff suppressed because it is too large Load diff

View file

@ -29,30 +29,32 @@
<!-- to be removed-->
<script type="module" src="scripts/modules/config.mjs"></script>
<script type="module" src="scripts/modules/status.mjs"></script>
<script type="module" src="scripts/vendor/auto/luxon.mjs"></script>
<script type="module" src="scripts/modules/currentweather.mjs"></script>
<script type="module" src="scripts/modules/almanac.mjs"></script>
<script type="module" src="scripts/modules/icons.mjs"></script>
<script type="module" src="scripts/modules/currentweatherscroll.mjs"></script>
<script type="module" src="scripts/modules/extendedforecast.mjs"></script>
<script type="module" src="scripts/modules/hourly.mjs"></script>
<script type="module" src="scripts/modules/progress.mjs"></script>
<script type="module" src="scripts/modules/latestobservations.mjs"></script>
<script type="module" src="scripts/modules/localforecast.mjs"></script>
<script type="module" src="scripts/modules/radar.mjs"></script>
<script type="module" src="scripts/modules/regionalforecast.mjs"></script>
<script type="module" src="scripts/modules/travelforecast.mjs"></script>
<script type="module" src="scripts/index.mjs"></script>
<script type="text/javascript" src="scripts/data/states.js"></script>
<script type="text/javascript" src="scripts/vendor/auto/luxon.js"></script>
<script type="text/javascript" src="scripts/vendor/auto/suncalc.js"></script>
<!-- data -->
<script type="text/javascript" src="scripts/data/states.js"></script>
<script type="text/javascript" src="scripts/data/travelcities.js"></script>
<script type="text/javascript" src="scripts/data/regionalcities.js"></script>
<script type="text/javascript" src="scripts/data/stations.js"></script>
<script type="text/javascript" src="scripts/modules/draw.js"></script>
<script type="text/javascript" src="scripts/vendor/auto/suncalc.js"></script>
<script type="text/javascript" src="scripts/modules/weatherdisplay.js"></script>
<script type="text/javascript" src="scripts/modules/icons.js"></script>
<script type="text/javascript" src="scripts/modules/utilities.js"></script>
<script type="text/javascript" src="scripts/modules/currentweather.js"></script>
<script type="text/javascript" src="scripts/modules/currentweatherscroll.js"></script>
<script type="text/javascript" src="scripts/modules/latestobservations.js"></script>
<script type="text/javascript" src="scripts/modules/travelforecast.js"></script>
<script type="text/javascript" src="scripts/modules/regionalforecast.js"></script>
<script type="text/javascript" src="scripts/modules/localforecast.js"></script>
<script type="text/javascript" src="scripts/modules/extendedforecast.js"></script>
<script type="text/javascript" src="scripts/modules/almanac.js"></script>
<script type="text/javascript" src="scripts/modules/radar.js"></script>
<script type="text/javascript" src="scripts/modules/hourly.js"></script>
<script type="text/javascript" src="scripts/modules/progress.js"></script>
<script type="text/javascript" src="scripts/modules/navigation.js"></script>
<% } %>