Merge branch 'main' into remove-jquery

This commit is contained in:
Matt Walsh
2025-05-12 13:35:01 -05:00
84 changed files with 31678 additions and 49296 deletions

View File

Before

Width:  |  Height:  |  Size: 251 B

After

Width:  |  Height:  |  Size: 251 B

View File

Before

Width:  |  Height:  |  Size: 455 B

After

Width:  |  Height:  |  Size: 455 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 243 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

3
server/music/readme.txt Normal file
View File

@@ -0,0 +1,3 @@
.mp3 files placed in this folder will be available via the un-mute button in the application.
No subdirectories will be scanned, and music will be played in a random order.
The default folder will be used only if no .mp3 files are found in this /server/music folder

View File

@@ -2,8 +2,7 @@
// it is intended to allow for customizations that do not get published back to the git repo
// for example, changing the logo
// start running after all content is loaded
document.addEventListener('DOMContentLoaded', () => {
const customTask = () => {
// get all of the logo images
const logos = document.querySelectorAll('.logo img');
// loop through each logo
@@ -11,4 +10,16 @@ document.addEventListener('DOMContentLoaded', () => {
// change the source
elem.src = 'my-custom-logo.gif';
});
};
// start running after all content is loaded, or immediately if page content is already loaded
if (document.readyState === 'loading') {
// Loading hasn't finished yet
document.addEventListener('DOMContentLoaded', customTask);
} else {
// `DOMContentLoaded` has already fired
customTask();
}
document.addEventListener('DOMContentLoaded', () => {
});

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,243 +1,243 @@
// eslint-disable-next-line no-unused-vars
const TravelCities = [
{
Name: 'Atlanta',
Latitude: 33.749,
Longitude: -84.388,
point: {
x: 50,
y: 86,
wfo: 'FFC',
},
"Name": "Atlanta",
"Latitude": 33.749,
"Longitude": -84.388,
"point": {
"x": 51,
"y": 87,
"wfo": "FFC"
}
},
{
Name: 'Boston',
Latitude: 42.3584,
Longitude: -71.0598,
point: {
x: 71,
y: 90,
wfo: 'BOX',
},
"Name": "Boston",
"Latitude": 42.3584,
"Longitude": -71.0598,
"point": {
"x": 71,
"y": 90,
"wfo": "BOX"
}
},
{
Name: 'Chicago',
Latitude: 41.9796,
Longitude: -87.9045,
point: {
x: 65,
y: 76,
wfo: 'LOT',
},
"Name": "Chicago",
"Latitude": 41.9796,
"Longitude": -87.9045,
"point": {
"x": 66,
"y": 77,
"wfo": "LOT"
}
},
{
Name: 'Cleveland',
Latitude: 41.4995,
Longitude: -81.6954,
point: {
x: 82,
y: 64,
wfo: 'CLE',
},
"Name": "Cleveland",
"Latitude": 41.4995,
"Longitude": -81.6954,
"point": {
"x": 83,
"y": 65,
"wfo": "CLE"
}
},
{
Name: 'Dallas',
Latitude: 32.8959,
Longitude: -97.0372,
point: {
x: 79,
y: 108,
wfo: 'FWD',
},
"Name": "Dallas",
"Latitude": 32.8959,
"Longitude": -97.0372,
"point": {
"x": 80,
"y": 109,
"wfo": "FWD"
}
},
{
Name: 'Denver',
Latitude: 39.7391,
Longitude: -104.9847,
point: {
x: 62,
y: 60,
wfo: 'BOU',
},
"Name": "Denver",
"Latitude": 39.7391,
"Longitude": -104.9847,
"point": {
"x": 63,
"y": 61,
"wfo": "BOU"
}
},
{
Name: 'Detroit',
Latitude: 42.3314,
Longitude: -83.0457,
point: {
x: 65,
y: 33,
wfo: 'DTX',
},
"Name": "Detroit",
"Latitude": 42.3314,
"Longitude": -83.0457,
"point": {
"x": 66,
"y": 34,
"wfo": "DTX"
}
},
{
Name: 'Hartford',
Latitude: 41.7637,
Longitude: -72.6851,
point: {
x: 21,
y: 54,
wfo: 'BOX',
},
"Name": "Hartford",
"Latitude": 41.7637,
"Longitude": -72.6851,
"point": {
"x": 21,
"y": 54,
"wfo": "BOX"
}
},
{
Name: 'Houston',
Latitude: 29.7633,
Longitude: -95.3633,
point: {
x: 65,
y: 97,
wfo: 'HGX',
},
"Name": "Houston",
"Latitude": 29.7633,
"Longitude": -95.3633,
"point": {
"x": 65,
"y": 97,
"wfo": "HGX"
}
},
{
Name: 'Indianapolis',
Latitude: 39.7684,
Longitude: -86.158,
point: {
x: 57,
y: 68,
wfo: 'IND',
},
"Name": "Indianapolis",
"Latitude": 39.7684,
"Longitude": -86.158,
"point": {
"x": 58,
"y": 69,
"wfo": "IND"
}
},
{
Name: 'Los Angeles',
Latitude: 34.0522,
Longitude: -118.2437,
point: {
x: 154,
y: 44,
wfo: 'LOX',
},
"Name": "Los Angeles",
"Latitude": 34.0522,
"Longitude": -118.2437,
"point": {
"x": 155,
"y": 45,
"wfo": "LOX"
}
},
{
Name: 'Miami',
Latitude: 25.7743,
Longitude: -80.1937,
point: {
x: 109,
y: 50,
wfo: 'MFL',
},
"Name": "Miami",
"Latitude": 25.7743,
"Longitude": -80.1937,
"point": {
"x": 110,
"y": 51,
"wfo": "MFL"
}
},
{
Name: 'Minneapolis',
Latitude: 44.98,
Longitude: -93.2638,
point: {
x: 107,
y: 71,
wfo: 'MPX',
},
"Name": "Minneapolis",
"Latitude": 44.98,
"Longitude": -93.2638,
"point": {
"x": 108,
"y": 72,
"wfo": "MPX"
}
},
{
Name: 'New York',
Latitude: 40.7142,
Longitude: -74.0059,
point: {
x: 32,
y: 34,
wfo: 'OKX',
},
"Name": "New York",
"Latitude": 40.7142,
"Longitude": -74.0059,
"point": {
"x": 33,
"y": 35,
"wfo": "OKX"
}
},
{
Name: 'Norfolk',
Latitude: 36.8468,
Longitude: -76.2852,
point: {
x: 89,
y: 51,
wfo: 'AKQ',
},
"Name": "Norfolk",
"Latitude": 36.8468,
"Longitude": -76.2852,
"point": {
"x": 90,
"y": 52,
"wfo": "AKQ"
}
},
{
Name: 'Orlando',
Latitude: 28.5383,
Longitude: -81.3792,
point: {
x: 26,
y: 68,
wfo: 'MLB',
},
"Name": "Orlando",
"Latitude": 28.5383,
"Longitude": -81.3792,
"point": {
"x": 26,
"y": 68,
"wfo": "MLB"
}
},
{
Name: 'Philadelphia',
Latitude: 39.9523,
Longitude: -75.1638,
point: {
x: 49,
y: 75,
wfo: 'PHI',
},
"Name": "Philadelphia",
"Latitude": 39.9523,
"Longitude": -75.1638,
"point": {
"x": 50,
"y": 76,
"wfo": "PHI"
}
},
{
Name: 'Pittsburgh',
Latitude: 40.4406,
Longitude: -79.9959,
point: {
x: 77,
y: 65,
wfo: 'PBZ',
},
"Name": "Pittsburgh",
"Latitude": 40.4406,
"Longitude": -79.9959,
"point": {
"x": 78,
"y": 66,
"wfo": "PBZ"
}
},
{
Name: 'St. Louis',
Latitude: 38.6273,
Longitude: -90.1979,
point: {
x: 94,
y: 73,
wfo: 'LSX',
},
"Name": "St. Louis",
"Latitude": 38.6273,
"Longitude": -90.1979,
"point": {
"x": 95,
"y": 74,
"wfo": "LSX"
}
},
{
Name: 'San Francisco',
Latitude: 37.7749,
Longitude: -122.4194,
point: {
x: 85,
y: 105,
wfo: 'MTR',
},
"Name": "San Francisco",
"Latitude": 37.7749,
"Longitude": -122.4194,
"point": {
"x": 85,
"y": 105,
"wfo": "MTR"
}
},
{
Name: 'Seattle',
Latitude: 47.6062,
Longitude: -122.3321,
point: {
x: 124,
y: 67,
wfo: 'SEW',
},
"Name": "Seattle",
"Latitude": 47.6062,
"Longitude": -122.3321,
"point": {
"x": 125,
"y": 68,
"wfo": "SEW"
}
},
{
Name: 'Syracuse',
Latitude: 43.0481,
Longitude: -76.1474,
point: {
x: 51,
y: 98,
wfo: 'BGM',
},
"Name": "Syracuse",
"Latitude": 43.0481,
"Longitude": -76.1474,
"point": {
"x": 52,
"y": 99,
"wfo": "BGM"
}
},
{
Name: 'Tampa',
Latitude: 27.9475,
Longitude: -82.4584,
point: {
x: 70,
y: 96,
wfo: 'TBW',
},
"Name": "Tampa",
"Latitude": 27.9475,
"Longitude": -82.4584,
"point": {
"x": 71,
"y": 97,
"wfo": "TBW"
}
},
{
Name: 'Washington DC',
Latitude: 38.8951,
Longitude: -77.0364,
point: {
x: 97,
y: 71,
wfo: 'LWX',
},
},
];
"Name": "Washington DC",
"Latitude": 38.8951,
"Longitude": -77.0364,
"point": {
"x": 97,
"y": 71,
"wfo": "LWX"
}
}
]

View File

@@ -1,7 +1,7 @@
import { json } from './modules/utils/fetch.mjs';
import noSleep from './modules/utils/nosleep.mjs';
import {
message as navMessage, isPlaying, resize, resetStatuses, latLonReceived, stopAutoRefreshTimer, registerRefreshData,
message as navMessage, isPlaying, resize, resetStatuses, latLonReceived,
} from './modules/navigation.mjs';
import { round2 } from './modules/utils/units.mjs';
import { parseQueryString } from './modules/share.mjs';
@@ -10,6 +10,7 @@ import AutoComplete from './modules/autocomplete.mjs';
document.addEventListener('DOMContentLoaded', () => {
init();
getCustomCode();
});
const categories = [
@@ -32,8 +33,6 @@ const init = () => {
e.target.select();
});
registerRefreshData(loadData);
document.querySelector('#NavigateMenu').addEventListener('click', btnNavigateMenuClick);
document.querySelector('#NavigateRefresh').addEventListener('click', btnNavigateRefreshClick);
document.querySelector('#NavigateNext').addEventListener('click', btnNavigateNextClick);
@@ -245,7 +244,6 @@ const loadData = (_latLon, haveDataCallback) => {
if (!latLon) return;
document.querySelector(TXT_ADDRESS_SELECTOR).blur();
stopAutoRefreshTimer();
latLonReceived(latLon, haveDataCallback);
};
@@ -410,3 +408,15 @@ const fullScreenResizeCheck = () => {
// store state of fullscreen element for next change detection
fullScreenResizeCheck.wasFull = !!document.fullscreenElement;
};
const getCustomCode = async () => {
// fetch the custom file and see if it returns a 200 status
const response = await fetch('scripts/custom.js', { method: 'HEAD' });
if (response.ok) {
// add the script element to the page
const customElem = document.createElement('script');
customElem.src = 'scripts/custom.js';
customElem.type = 'text/javascript';
document.body.append(customElem);
}
};

View File

@@ -3,7 +3,7 @@ import { loadImg, preloadImg } from './utils/image.mjs';
import { DateTime } from '../vendor/auto/luxon.mjs';
import STATUS from './status.mjs';
import WeatherDisplay from './weatherdisplay.mjs';
import { registerDisplay } from './navigation.mjs';
import { registerDisplay, timeZone } from './navigation.mjs';
class Almanac extends WeatherDisplay {
constructor(navId, elemId) {
@@ -21,12 +21,11 @@ class Almanac extends WeatherDisplay {
this.timing.totalScreens = 1;
}
async getData(_weatherParameters) {
const superResponse = super.getData(_weatherParameters);
const weatherParameters = _weatherParameters ?? this.weatherParameters;
async getData(weatherParameters, refresh) {
const superResponse = super.getData(weatherParameters, refresh);
// get sun/moon data
const { sun, moon } = this.calcSunMoonData(weatherParameters);
const { sun, moon } = this.calcSunMoonData(this.weatherParameters);
// store the data
this.data = {
@@ -123,10 +122,10 @@ class Almanac extends WeatherDisplay {
// sun and moon data
this.elem.querySelector('.day-1').innerHTML = Today.toLocaleString({ weekday: 'long' });
this.elem.querySelector('.day-2').innerHTML = Tomorrow.toLocaleString({ weekday: 'long' });
this.elem.querySelector('.rise-1').innerHTML = DateTime.fromJSDate(info.sun[0].sunrise).toLocaleString(DateTime.TIME_SIMPLE).toLowerCase();
this.elem.querySelector('.rise-2').innerHTML = DateTime.fromJSDate(info.sun[1].sunrise).toLocaleString(DateTime.TIME_SIMPLE).toLowerCase();
this.elem.querySelector('.set-1').innerHTML = DateTime.fromJSDate(info.sun[0].sunset).toLocaleString(DateTime.TIME_SIMPLE).toLowerCase();
this.elem.querySelector('.set-2').innerHTML = DateTime.fromJSDate(info.sun[1].sunset).toLocaleString(DateTime.TIME_SIMPLE).toLowerCase();
this.elem.querySelector('.rise-1').innerHTML = DateTime.fromJSDate(info.sun[0].sunrise).setZone(timeZone()).toLocaleString(DateTime.TIME_SIMPLE).toLowerCase();
this.elem.querySelector('.rise-2').innerHTML = DateTime.fromJSDate(info.sun[1].sunrise).setZone(timeZone()).toLocaleString(DateTime.TIME_SIMPLE).toLowerCase();
this.elem.querySelector('.set-1').innerHTML = DateTime.fromJSDate(info.sun[0].sunset).setZone(timeZone()).toLocaleString(DateTime.TIME_SIMPLE).toLowerCase();
this.elem.querySelector('.set-2').innerHTML = DateTime.fromJSDate(info.sun[1].sunset).setZone(timeZone()).toLocaleString(DateTime.TIME_SIMPLE).toLowerCase();
const days = info.moon.map((MoonPhase) => {
const fill = {};

View File

@@ -8,7 +8,7 @@ import { getWeatherIconFromIconLink } from './icons.mjs';
import WeatherDisplay from './weatherdisplay.mjs';
import { registerDisplay } from './navigation.mjs';
import {
celsiusToFahrenheit, kphToMph, pascalToInHg, metersToFeet, kilometersToMiles,
temperature, windSpeed, pressure, distanceMeters, distanceKilometers,
} from './utils/units.mjs';
// some stations prefixed do not provide all the necessary data
@@ -21,13 +21,14 @@ class CurrentWeather extends WeatherDisplay {
this.backgroundImage = loadImg('images/BackGround1_1.png');
}
async getData(_weatherParameters) {
async getData(weatherParameters, refresh) {
// always load the data for use in the lower scroll
const superResult = super.getData(_weatherParameters);
const weatherParameters = _weatherParameters ?? this.weatherParameters;
const superResult = super.getData(weatherParameters, refresh);
// note: current weather does not use old data on a silent refresh
// this is deliberate because it can pull data from more than one station in sequence
// filter for 4-letter observation stations, only those contain sky conditions and thus an icon
const filteredStations = weatherParameters.stations.filter((station) => station?.properties?.stationIdentifier?.length === 4 && !skipStations.includes(station.properties.stationIdentifier.slice(0, 1)));
const filteredStations = this.weatherParameters.stations.filter((station) => station?.properties?.stationIdentifier?.length === 4 && !skipStations.includes(station.properties.stationIdentifier.slice(0, 1)));
// Load the observations
let observations;
@@ -129,6 +130,8 @@ class CurrentWeather extends WeatherDisplay {
// make data available outside this class
// promise allows for data to be requested before it is available
async getCurrentWeather(stillWaiting) {
// an external caller has requested data, set up auto reload
this.setAutoReload();
if (stillWaiting) this.stillWaitingCallbacks.push(stillWaiting);
return new Promise((resolve) => {
if (this.data) resolve(this.data);
@@ -159,23 +162,32 @@ const shortConditions = (_condition) => {
// format the received data
const parseData = (data) => {
// get the unit converter
const windConverter = windSpeed();
const temperatureConverter = temperature();
const metersConverter = distanceMeters();
const kilometersConverter = distanceKilometers();
const pressureConverter = pressure();
const observations = data.features[0].properties;
// values from api are provided in metric
data.observations = observations;
data.Temperature = Math.round(observations.temperature.value);
data.TemperatureUnit = 'C';
data.DewPoint = Math.round(observations.dewpoint.value);
data.Ceiling = Math.round(observations.cloudLayers[0]?.base?.value ?? 0);
data.CeilingUnit = 'm.';
data.Visibility = Math.round(observations.visibility.value / 1000);
data.VisibilityUnit = ' km.';
data.WindSpeed = Math.round(observations.windSpeed.value);
data.Temperature = temperatureConverter(observations.temperature.value);
data.TemperatureUnit = temperatureConverter.units;
data.DewPoint = temperatureConverter(observations.dewpoint.value);
data.Ceiling = metersConverter(observations.cloudLayers[0]?.base?.value ?? 0);
data.CeilingUnit = metersConverter.units;
data.Visibility = kilometersConverter(observations.visibility.value);
data.VisibilityUnit = kilometersConverter.units;
data.Pressure = pressureConverter(observations.barometricPressure.value);
data.PressureUnit = pressureConverter.units;
data.HeatIndex = temperatureConverter(observations.heatIndex.value);
data.WindChill = temperatureConverter(observations.windChill.value);
data.WindSpeed = windConverter(observations.windSpeed.value);
data.WindDirection = directionToNSEW(observations.windDirection.value);
data.Pressure = Math.round(observations.barometricPressure.value);
data.HeatIndex = Math.round(observations.heatIndex.value);
data.WindChill = Math.round(observations.windChill.value);
data.WindGust = Math.round(observations.windGust.value);
data.WindUnit = 'KPH';
data.WindGust = windConverter(observations.windGust.value);
data.WindSpeed = windConverter(data.WindSpeed);
data.WindUnit = windConverter.units;
data.Humidity = Math.round(observations.relativeHumidity.value);
data.Icon = getWeatherIconFromIconLink(observations.icon);
data.PressureDirection = '';
@@ -186,20 +198,6 @@ const parseData = (data) => {
if (pressureDiff > 150) data.PressureDirection = 'R';
if (pressureDiff < -150) data.PressureDirection = 'F';
// convert to us units
data.Temperature = celsiusToFahrenheit(data.Temperature);
data.TemperatureUnit = 'F';
data.DewPoint = celsiusToFahrenheit(data.DewPoint);
data.Ceiling = Math.round(metersToFeet(data.Ceiling) / 100) * 100;
data.CeilingUnit = 'ft.';
data.Visibility = kilometersToMiles(observations.visibility.value / 1000);
data.VisibilityUnit = ' mi.';
data.WindSpeed = kphToMph(data.WindSpeed);
data.WindUnit = 'MPH';
data.Pressure = pascalToInHg(data.Pressure).toFixed(2);
data.HeatIndex = celsiusToFahrenheit(data.HeatIndex);
data.WindChill = celsiusToFahrenheit(data.WindChill);
data.WindGust = kphToMph(data.WindGust);
return data;
};

View File

@@ -71,7 +71,7 @@ const screens = [
(data) => `Humidity: ${data.Humidity}% Dewpoint: ${data.DewPoint}${degree}${data.TemperatureUnit}`,
// barometric pressure
(data) => `Barometric Pressure: ${data.Pressure} ${data.PressureDirection}`,
(data) => `Barometric Pressure: ${data.Pressure} ${data.PressureUnit} ${data.PressureDirection}`,
// wind
(data) => {

View File

@@ -8,6 +8,7 @@ import { getWeatherIconFromIconLink } from './icons.mjs';
import { preloadImg } from './utils/image.mjs';
import WeatherDisplay from './weatherdisplay.mjs';
import { registerDisplay } from './navigation.mjs';
import settings from './settings.mjs';
class ExtendedForecast extends WeatherDisplay {
constructor(navId, elemId) {
@@ -17,16 +18,14 @@ class ExtendedForecast extends WeatherDisplay {
this.timing.totalScreens = 2;
}
async getData(_weatherParameters) {
if (!super.getData(_weatherParameters)) return;
const weatherParameters = _weatherParameters ?? this.weatherParameters;
async getData(weatherParameters, refresh) {
if (!super.getData(weatherParameters, refresh)) return;
// request us or si units
let forecast;
try {
forecast = await json(weatherParameters.forecast, {
this.data = await json(this.weatherParameters.forecast, {
data: {
units: 'us',
units: settings.units.value,
},
retryCount: 3,
stillWaiting: () => this.stillWaiting(),
@@ -34,11 +33,13 @@ class ExtendedForecast extends WeatherDisplay {
} catch (error) {
console.error('Unable to get extended forecast');
console.error(error.status, error.responseJSON);
this.setStatus(STATUS.failed);
return;
// if there's no previous data, fail
if (!this.data) {
this.setStatus(STATUS.failed);
return;
}
}
// we only get here if there was no error above
this.data = parse(forecast.properties.periods);
this.screenIndex = 0;
this.setStatus(STATUS.loaded);
}
@@ -48,7 +49,7 @@ class ExtendedForecast extends WeatherDisplay {
// determine bounds
// grab the first three or second set of three array elements
const forecast = this.data.slice(0 + 3 * this.screenIndex, 3 + this.screenIndex * 3);
const forecast = parse(this.data.properties.periods).slice(0 + 3 * this.screenIndex, 3 + this.screenIndex * 3);
// create each day template
const days = forecast.map((Day) => {
@@ -131,7 +132,7 @@ const shortenExtendedForecastText = (long) => {
[/dense /gi, ''],
[/Thunderstorm/g, 'T\'Storm'],
];
// run all regexes
// run all regexes
const short = regexList.reduce((working, [regex, replace]) => working.replace(regex, replace), long);
let conditions = short.split(' ');

View File

@@ -26,9 +26,11 @@ class Hazards extends WeatherDisplay {
this.timing.totalScreens = 0;
}
async getData(weatherParameters) {
async getData(weatherParameters, refresh) {
// super checks for enabled
const superResult = super.getData(weatherParameters);
const superResult = super.getData(weatherParameters, refresh);
// hazards performs a silent refresh, but does not fall back to a previous fetch if no data is available
// this is intentional to ensure the latest alerts only are displayed.
const alert = this.checkbox.querySelector('.alert');
alert.classList.remove('show');
@@ -122,7 +124,7 @@ class Hazards extends WeatherDisplay {
// base count change callback
baseCountChange(count) {
// calculate scroll offset and don't go past end
let offsetY = Math.min(this.elem.querySelector('.hazard-lines').getBoundingClientRect().height - 390, (count - 150));
let offsetY = Math.min(this.elem.querySelector('.hazard-lines').offsetHeight - 390, (count - 150));
// don't let offset go negative
if (offsetY < 0) offsetY = 0;

View File

@@ -3,7 +3,7 @@
import STATUS from './status.mjs';
import getHourlyData from './hourly.mjs';
import WeatherDisplay from './weatherdisplay.mjs';
import { registerDisplay } from './navigation.mjs';
import { registerDisplay, timeZone } from './navigation.mjs';
import { DateTime } from '../vendor/auto/luxon.mjs';
class HourlyGraph extends WeatherDisplay {
@@ -23,8 +23,8 @@ class HourlyGraph extends WeatherDisplay {
this.elem.querySelector('.header .right').append(header);
}
async getData() {
if (!super.getData()) return;
async getData(weatherParameters, refresh) {
if (!super.getData(undefined, refresh)) return;
const data = await getHourlyData(() => this.stillWaiting());
if (data === undefined) {
@@ -38,7 +38,7 @@ class HourlyGraph extends WeatherDisplay {
const skyCover = data.map((d) => d.skyCover);
this.data = {
skyCover, temperature, probabilityOfPrecipitation,
skyCover, temperature, probabilityOfPrecipitation, temperatureUnit: data[0].temperatureUnit,
};
this.setStatus(STATUS.loaded);
@@ -107,6 +107,9 @@ class HourlyGraph extends WeatherDisplay {
// set the image source
this.image.src = canvas.toDataURL();
// change the units in the header
this.elem.querySelector('.temperature').innerHTML = `Temperature ${String.fromCharCode(176)}${this.data.temperatureUnit}`;
super.drawCanvas();
this.finishDraw();
}
@@ -142,7 +145,7 @@ const drawPath = (path, ctx, options) => {
};
// format as 1p, 12a, etc.
const formatTime = (time) => time.toFormat('ha').slice(0, -1);
const formatTime = (time) => time.setZone(timeZone()).toFormat('ha').slice(0, -1);
// register display
registerDisplay(new HourlyGraph(4, 'hourly-graph'));

View File

@@ -3,11 +3,11 @@
import STATUS from './status.mjs';
import { DateTime, Interval, Duration } from '../vendor/auto/luxon.mjs';
import { json } from './utils/fetch.mjs';
import { celsiusToFahrenheit, kilometersToMiles } from './utils/units.mjs';
import { temperature as temperatureUnit, distanceKilometers } from './utils/units.mjs';
import { getHourlyIcon } from './icons.mjs';
import { directionToNSEW } from './utils/calc.mjs';
import WeatherDisplay from './weatherdisplay.mjs';
import { registerDisplay } from './navigation.mjs';
import { registerDisplay, timeZone } from './navigation.mjs';
import getSun from './almanac.mjs';
class Hourly extends WeatherDisplay {
@@ -27,23 +27,30 @@ class Hourly extends WeatherDisplay {
this.timing.delay.push(150);
}
async getData(weatherParameters) {
async getData(weatherParameters, refresh) {
// super checks for enabled
const superResponse = super.getData(weatherParameters);
const superResponse = super.getData(weatherParameters, refresh);
let forecast;
try {
// get the forecast
forecast = await json(weatherParameters.forecastGridData, { retryCount: 3, stillWaiting: () => this.stillWaiting() });
forecast = await json(this.weatherParameters.forecastGridData, { retryCount: 3, stillWaiting: () => this.stillWaiting() });
// parse the forecast
this.data = await parseForecast(forecast.properties);
} catch (error) {
console.error('Get hourly forecast failed');
console.error(error.status, error.responseJSON);
if (this.isEnabled) this.setStatus(STATUS.failed);
// return undefined to other subscribers
this.getDataCallback(undefined);
return;
// use old data if available
if (this.data) {
console.log('Using previous hourly forecast');
// don't return, this.data is usable from the previous update
} else {
if (this.isEnabled) this.setStatus(STATUS.failed);
// return undefined to other subscribers
this.getDataCallback(undefined);
return;
}
}
this.data = await parseForecast(forecast.properties);
this.getDataCallback();
if (!superResponse) return;
@@ -56,7 +63,7 @@ class Hourly extends WeatherDisplay {
const list = this.elem.querySelector('.hourly-lines');
list.innerHTML = '';
const startingHour = DateTime.local();
const startingHour = DateTime.local().setZone(timeZone());
const lines = this.data.map((data, index) => {
const fillValues = {};
@@ -66,12 +73,12 @@ class Hourly extends WeatherDisplay {
fillValues.hour = formattedHour;
// temperatures, convert to strings with no decimal
const temperature = Math.round(data.temperature).toString().padStart(3);
const feelsLike = Math.round(data.apparentTemperature).toString().padStart(3);
const temperature = data.temperature.toString().padStart(3);
const feelsLike = data.apparentTemperature.toString().padStart(3);
fillValues.temp = temperature;
// only plot apparent temperature if there is a difference
// if (temperature !== feelsLike) line.querySelector('.like').innerHTML = feelsLike;
if (temperature !== feelsLike) fillValues.like = feelsLike;
// apparent temperature is color coded if different from actual temperature (after fill is applied)
fillValues.like = feelsLike;
// wind
let wind = 'Calm';
@@ -84,7 +91,17 @@ class Hourly extends WeatherDisplay {
// image
fillValues.icon = { type: 'img', src: data.icon };
return this.fillTemplate('hourly-row', fillValues);
const filledRow = this.fillTemplate('hourly-row', fillValues);
// alter the color of the feels like column to reflect wind chill or heat index
if (feelsLike < temperature) {
filledRow.querySelector('.like').classList.add('wind-chill');
}
if (feelsLike > temperature) {
filledRow.querySelector('.like').classList.add('heat-index');
}
return filledRow;
});
list.append(...lines);
@@ -109,7 +126,7 @@ class Hourly extends WeatherDisplay {
// base count change callback
baseCountChange(count) {
// calculate scroll offset and don't go past end
let offsetY = Math.min(this.elem.querySelector('.hourly-lines').getBoundingClientRect().height - 289, (count - 150));
let offsetY = Math.min(this.elem.querySelector('.hourly-lines').offsetHeight - 289, (count - 150));
// don't let offset go negative
if (offsetY < 0) offsetY = 0;
@@ -122,6 +139,8 @@ class Hourly extends WeatherDisplay {
// promise allows for data to be requested before it is available
async getCurrentData(stillWaiting) {
if (stillWaiting) this.stillWaitingCallbacks.push(stillWaiting);
// an external caller has requested data, set up auto reload
this.setAutoReload();
return new Promise((resolve) => {
if (this.data) resolve(this.data);
// data not available, put it into the data callback queue
@@ -132,6 +151,11 @@ class Hourly extends WeatherDisplay {
// extract specific values from forecast and format as an array
const parseForecast = async (data) => {
// get unit converters
const temperatureConverter = temperatureUnit();
const distanceConverter = distanceKilometers();
// parse data
const temperature = expand(data.temperature.values);
const apparentTemperature = expand(data.apparentTemperature.values);
const windSpeed = expand(data.windSpeed.values);
@@ -145,9 +169,11 @@ const parseForecast = async (data) => {
const icons = await determineIcon(skyCover, weather, iceAccumulation, probabilityOfPrecipitation, snowfallAmount, windSpeed);
return temperature.map((val, idx) => ({
temperature: celsiusToFahrenheit(temperature[idx]),
apparentTemperature: celsiusToFahrenheit(apparentTemperature[idx]),
windSpeed: kilometersToMiles(windSpeed[idx]),
temperature: temperatureConverter(temperature[idx]),
temperatureUnit: temperatureConverter.units,
apparentTemperature: temperatureConverter(apparentTemperature[idx]),
windSpeed: distanceConverter(windSpeed[idx]),
windUnit: distanceConverter.units,
windDirection: directionToNSEW(windDirection[idx]),
probabilityOfPrecipitation: probabilityOfPrecipitation[idx],
skyCover: skyCover[idx],

View File

@@ -3,9 +3,10 @@ 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 { celsiusToFahrenheit, kphToMph } from './utils/units.mjs';
import { temperature, windSpeed } from './utils/units.mjs';
import WeatherDisplay from './weatherdisplay.mjs';
import { registerDisplay } from './navigation.mjs';
import settings from './settings.mjs';
class LatestObservations extends WeatherDisplay {
constructor(navId, elemId) {
@@ -15,14 +16,15 @@ class LatestObservations extends WeatherDisplay {
this.MaximumRegionalStations = 7;
}
async getData(_weatherParameters) {
if (!super.getData(_weatherParameters)) return;
const weatherParameters = _weatherParameters ?? this.weatherParameters;
async getData(weatherParameters, refresh) {
if (!super.getData(weatherParameters, refresh)) return;
// latest observations does a silent refresh but will not fall back to previously fetched data
// this is intentional because up to 30 stations are available to pull data from
// calculate distance to each station
const stationsByDistance = Object.keys(StationInfo).map((key) => {
const station = StationInfo[key];
const distance = calcDistance(station.lat, station.lon, weatherParameters.latitude, weatherParameters.longitude);
const distance = calcDistance(station.lat, station.lon, this.weatherParameters.latitude, this.weatherParameters.longitude);
return { ...station, distance };
});
@@ -64,14 +66,22 @@ class LatestObservations extends WeatherDisplay {
// sort array by station name
const sortedConditions = conditions.sort((a, b) => ((a.Name < b.Name) ? -1 : 1));
this.elem.querySelector('.column-headers .temp.english').classList.add('show');
this.elem.querySelector('.column-headers .temp.metric').classList.remove('show');
if (settings.units.value === 'us') {
this.elem.querySelector('.column-headers .temp.english').classList.add('show');
this.elem.querySelector('.column-headers .temp.metric').classList.remove('show');
} else {
this.elem.querySelector('.column-headers .temp.english').classList.remove('show');
this.elem.querySelector('.column-headers .temp.metric').classList.add('show');
}
// get unit converters
const windConverter = windSpeed();
const temperatureConverter = temperature();
const lines = sortedConditions.map((condition) => {
const windDirection = directionToNSEW(condition.windDirection.value);
const Temperature = Math.round(celsiusToFahrenheit(condition.temperature.value));
const WindSpeed = Math.round(kphToMph(condition.windSpeed.value));
const Temperature = temperatureConverter(condition.temperature.value);
const WindSpeed = windConverter(condition.windSpeed.value);
const fill = {
location: locationCleanup(condition.city).substr(0, 14),
@@ -94,6 +104,8 @@ class LatestObservations extends WeatherDisplay {
linesContainer.innerHTML = '';
linesContainer.append(...lines);
// update temperature unit header
this.finishDraw();
}
}
@@ -122,8 +134,8 @@ const getStations = async (stations) => {
const data = await json(`https://api.weather.gov/stations/${station.id}/observations/latest`, { retryCount: 1, stillWaiting: () => this.stillWaiting() });
// test for temperature, weather and wind values present
if (data.properties.temperature.value === null
|| data.properties.textDescription === ''
|| data.properties.windSpeed.value === null) return false;
|| data.properties.textDescription === ''
|| data.properties.windSpeed.value === null) return false;
// format the return values
return {
...data.properties,

View File

@@ -4,6 +4,7 @@ import STATUS from './status.mjs';
import { json } from './utils/fetch.mjs';
import WeatherDisplay from './weatherdisplay.mjs';
import { registerDisplay } from './navigation.mjs';
import settings from './settings.mjs';
class LocalForecast extends WeatherDisplay {
constructor(navId, elemId) {
@@ -13,19 +14,21 @@ class LocalForecast extends WeatherDisplay {
this.timing.baseDelay = 5000;
}
async getData(_weatherParameters) {
if (!super.getData(_weatherParameters)) return;
const weatherParameters = _weatherParameters ?? this.weatherParameters;
async getData(weatherParameters, refresh) {
if (!super.getData(weatherParameters, refresh)) return;
// get raw data
const rawData = await this.getRawData(weatherParameters);
// check for data
if (!rawData) {
const rawData = await this.getRawData(this.weatherParameters);
// check for data, or if there's old data available
if (!rawData && !this.data) {
// fail for no old or new data
this.setStatus(STATUS.failed);
return;
}
// store the data
this.data = rawData || this.data;
// parse raw data
const conditions = parse(rawData);
const conditions = parse(this.data);
// read each text
this.screenTexts = conditions.map((condition) => {
@@ -61,7 +64,7 @@ class LocalForecast extends WeatherDisplay {
try {
return await json(weatherParameters.forecast, {
data: {
units: 'us',
units: settings.units.value,
},
retryCount: 3,
stillWaiting: () => this.stillWaiting(),
@@ -69,7 +72,6 @@ class LocalForecast extends WeatherDisplay {
} catch (error) {
console.error(`GetWeatherForecast failed: ${weatherParameters.forecast}`);
console.error(error.status, error.responseJSON);
this.setStatus(STATUS.failed);
return false;
}
}

View File

@@ -0,0 +1,168 @@
import { json } from './utils/fetch.mjs';
import Setting from './utils/setting.mjs';
let playlist;
let currentTrack = 0;
let player;
const mediaPlaying = new Setting('mediaPlaying', {
name: 'Media Playing',
type: 'boolean',
defaultValue: false,
sticky: true,
});
document.addEventListener('DOMContentLoaded', () => {
// add the event handler to the page
document.getElementById('ToggleMedia').addEventListener('click', toggleMedia);
// get the playlist
getMedia();
});
const getMedia = async () => {
try {
// fetch the playlist
const rawPlaylist = await json('playlist.json');
// store the playlist
playlist = rawPlaylist;
// enable the media player
enableMediaPlayer();
} catch (e) {
console.error("Couldn't get playlist");
console.error(e);
}
};
const enableMediaPlayer = () => {
// see if files are available
if (playlist?.availableFiles?.length > 0) {
// randomize the list
randomizePlaylist();
// enable the icon
const icon = document.getElementById('ToggleMedia');
icon.classList.add('available');
// set the button type
setIcon();
// if we're already playing (sticky option) then try to start playing
if (mediaPlaying.value === true) {
startMedia();
}
}
};
const setIcon = () => {
// get the icon
const icon = document.getElementById('ToggleMedia');
if (mediaPlaying.value === true) {
icon.classList.add('playing');
} else {
icon.classList.remove('playing');
}
};
const toggleMedia = (forcedState) => {
// handle forcing
if (typeof forcedState === 'boolean') {
mediaPlaying.value = forcedState;
} else {
// toggle the state
mediaPlaying.value = !mediaPlaying.value;
}
// handle the state change
stateChanged();
};
const startMedia = async () => {
// if there's not media player yet, enable it
if (!player) {
initializePlayer();
} else {
try {
await player.play();
} catch (e) {
// report the error
console.error('Couldn\'t play music');
console.error(e);
// set state back to not playing for good UI experience
mediaPlaying.value = false;
stateChanged();
}
}
};
const stopMedia = () => {
if (!player) return;
player.pause();
};
const stateChanged = () => {
// update the icon
setIcon();
// react to the new state
if (mediaPlaying.value) {
startMedia();
} else {
stopMedia();
}
};
const randomizePlaylist = () => {
let availableFiles = [...playlist.availableFiles];
const randomPlaylist = [];
while (availableFiles.length > 0) {
// get a randon item from the available files
const i = Math.floor(Math.random() * availableFiles.length);
// add it to the final list
randomPlaylist.push(availableFiles[i]);
// remove the file from the available files
availableFiles = availableFiles.filter((file, index) => index !== i);
}
playlist.availableFiles = randomPlaylist;
};
const initializePlayer = () => {
// basic sanity checks
if (!playlist.availableFiles || playlist?.availableFiles.length === 0) {
throw new Error('No playlist available');
}
if (player) {
return;
}
// create the player
player = new Audio();
// reset the playlist index
currentTrack = 0;
// add event handlers
player.addEventListener('canplay', playerCanPlay);
player.addEventListener('ended', playerEnded);
// get the first file
player.src = `music/${playlist.availableFiles[currentTrack]}`;
player.type = 'audio/mpeg';
};
const playerCanPlay = async () => {
// check to make sure they user still wants music (protect against slow loading music)
if (!mediaPlaying.value) return;
// start playing
startMedia();
};
const playerEnded = () => {
// next track
currentTrack += 1;
// roll over and re-randomize the tracks
if (currentTrack >= playlist.availableFiles.length) {
randomizePlaylist();
currentTrack = 0;
}
// update the player source
player.src = `music/${playlist.availableFiles[currentTrack]}`;
};
export {
// eslint-disable-next-line import/prefer-default-export
toggleMedia,
};

View File

@@ -15,26 +15,11 @@ let playing = false;
let progress;
const weatherParameters = {};
// auto refresh
const AUTO_REFRESH_INTERVAL_MS = 500;
const AUTO_REFRESH_TIME_MS = 600_000; // 10 min.
const CHK_AUTO_REFRESH_SELECTOR = '#chkAutoRefresh';
let AutoRefreshIntervalId = null;
let AutoRefreshCountMs = 0;
const init = async () => {
// set up resize handler
window.addEventListener('resize', resize);
resize();
// auto refresh
const autoRefresh = localStorage.getItem('autoRefresh');
if (!autoRefresh || autoRefresh === 'true') {
document.querySelector(CHK_AUTO_REFRESH_SELECTOR).checked = true;
} else {
document.querySelector(CHK_AUTO_REFRESH_SELECTOR).checked = false;
}
document.querySelector(CHK_AUTO_REFRESH_SELECTOR).addEventListener('change', autoRefreshChange);
generateCheckboxes();
};
@@ -123,12 +108,6 @@ const updateStatus = (value) => {
if (isPlaying() && value.id === firstDisplayIndex && value.status === STATUS.loaded) {
navTo(msg.command.firstFrame);
}
// send loaded messaged to parent
if (countLoadedDisplays() < displays.length) return;
// everything loaded, set timestamps
AssignLastUpdate(new Date());
};
// note: a display that is "still waiting"/"retrying" is considered loaded intentionally
@@ -202,8 +181,6 @@ const loadDisplay = (direction) => {
idx = wrap(curIdx + (i + 1) * direction, totalDisplays);
if (displays[idx].status === STATUS.loaded && displays[idx].timing.totalScreens > 0) break;
}
// if new display index is less than current display a wrap occurred, test for reload timeout
if (idx <= curIdx && refreshCheck()) return;
const newDisplay = displays[idx];
// hide all displays
hideAllCanvases();
@@ -320,83 +297,8 @@ const populateWeatherParameters = (params) => {
document.querySelector('#spanZoneId').innerHTML = params.zoneId;
};
const autoRefreshChange = (e) => {
const { checked } = e.target;
if (checked) {
startAutoRefreshTimer();
} else {
stopAutoRefreshTimer();
}
localStorage.setItem('autoRefresh', checked);
};
const AssignLastUpdate = (date) => {
if (date) {
document.querySelector('#spanLastRefresh').innerHTML = date.toLocaleString('en-US', {
weekday: 'short', month: 'short', day: 'numeric', year: 'numeric', hour: 'numeric', minute: 'numeric', second: 'numeric', timeZoneName: 'short',
});
if (document.querySelector(CHK_AUTO_REFRESH_SELECTOR).checked) startAutoRefreshTimer();
} else {
document.querySelector('#spanLastRefresh').innerHTML = '(none)';
}
};
const latLonReceived = (data, haveDataCallback) => {
getWeather(data, haveDataCallback);
AssignLastUpdate(null);
};
const startAutoRefreshTimer = () => {
// Ensure that any previous timer has already stopped.
// check if timer is running
if (AutoRefreshIntervalId) return;
// Reset the time elapsed.
AutoRefreshCountMs = 0;
const AutoRefreshTimer = () => {
// Increment the total time elapsed.
AutoRefreshCountMs += AUTO_REFRESH_INTERVAL_MS;
// Display the count down.
let RemainingMs = (AUTO_REFRESH_TIME_MS - AutoRefreshCountMs);
if (RemainingMs < 0) {
RemainingMs = 0;
}
const dt = new Date(RemainingMs);
document.querySelector('#spanRefreshCountDown').innerHTML = `${dt.getMinutes().toString().padStart(2, '0')}:${dt.getSeconds().toString().padStart(2, '0')}`;
// Time has elapsed.
if (AutoRefreshCountMs >= AUTO_REFRESH_TIME_MS && !isPlaying()) loadTwcData();
};
AutoRefreshIntervalId = window.setInterval(AutoRefreshTimer, AUTO_REFRESH_INTERVAL_MS);
AutoRefreshTimer();
};
const stopAutoRefreshTimer = () => {
if (AutoRefreshIntervalId) {
window.clearInterval(AutoRefreshIntervalId);
document.querySelector('#spanRefreshCountDown').innerHTML = '--:--';
AutoRefreshIntervalId = null;
}
};
const refreshCheck = () => {
// Time has elapsed.
if (AutoRefreshCountMs >= AUTO_REFRESH_TIME_MS && isPlaying()) {
loadTwcData();
return true;
}
return false;
};
const loadTwcData = () => {
if (loadTwcData.callback) loadTwcData.callback();
};
const registerRefreshData = (callback) => {
loadTwcData.callback = callback;
};
const timeZone = () => weatherParameters.timeZone;
@@ -414,7 +316,5 @@ export {
msg,
message,
latLonReceived,
stopAutoRefreshTimer,
registerRefreshData,
timeZone,
};

View File

@@ -42,19 +42,17 @@ class Radar extends WeatherDisplay {
];
}
async getData(_weatherParameters) {
if (!super.getData(_weatherParameters)) return;
const weatherParameters = _weatherParameters ?? this.weatherParameters;
async getData(weatherParameters, refresh) {
if (!super.getData(weatherParameters, refresh)) return;
// ALASKA AND HAWAII AREN'T SUPPORTED!
if (weatherParameters.state === 'AK' || weatherParameters.state === 'HI') {
if (this.weatherParameters.state === 'AK' || this.weatherParameters.state === 'HI') {
this.setStatus(STATUS.noData);
return;
}
// get the base map
let src = 'images/4000RadarMap2.jpg';
if (weatherParameters.State === 'HI') src = 'images/HawaiiRadarMap2.png';
const src = 'images/4000RadarMap2.jpg';
this.baseMap = await loadImg(src);
const baseUrl = 'https://mesonet.agron.iastate.edu/archive/data/';
@@ -70,7 +68,7 @@ class Radar extends WeatherDisplay {
const lists = (await Promise.all(baseUrls.map(async (url) => {
try {
// get a list of available radars
// get a list of available radars
return text(url, { cors: true });
} catch (error) {
console.log('Unable to get list of radars');
@@ -91,7 +89,7 @@ class Radar extends WeatherDisplay {
const anchors = xmlDoc.querySelectorAll('a');
const urls = [];
Array.from(anchors).forEach((elem) => {
if (elem.innerHTML?.match(/n0r_\d{12}\.png/)) {
if (elem.innerHTML?.match(/n0r_\d{12}\.png/)) {
urls.push(elem.href);
}
});
@@ -110,19 +108,12 @@ class Radar extends WeatherDisplay {
const height = 1600;
offsetX *= 2;
offsetY *= 2;
const sourceXY = utils.getXYFromLatitudeLongitudeMap(weatherParameters, offsetX, offsetY);
// create working context for manipulation
const workingCanvas = document.createElement('canvas');
workingCanvas.width = width;
workingCanvas.height = height;
const workingContext = workingCanvas.getContext('2d');
workingContext.imageSmoothingEnabled = false;
const sourceXY = utils.getXYFromLatitudeLongitudeMap(this.weatherParameters, offsetX, offsetY);
// calculate radar offsets
const radarOffsetX = 120;
const radarOffsetY = 70;
const radarSourceXY = utils.getXYFromLatitudeLongitudeDoppler(weatherParameters, offsetX, offsetY);
const radarSourceXY = utils.getXYFromLatitudeLongitudeDoppler(this.weatherParameters, offsetX, offsetY);
const radarSourceX = radarSourceXY.x / 2;
const radarSourceY = radarSourceXY.y / 2;
@@ -135,6 +126,13 @@ class Radar extends WeatherDisplay {
const context = canvas.getContext('2d');
context.imageSmoothingEnabled = false;
// create working context for manipulation
const workingCanvas = document.createElement('canvas');
workingCanvas.width = width;
workingCanvas.height = height;
const workingContext = workingCanvas.getContext('2d');
workingContext.imageSmoothingEnabled = false;
// get the image
const response = await fetch(rewriteUrl(url));
@@ -170,7 +168,7 @@ class Radar extends WeatherDisplay {
workingContext.drawImage(imgBlob, 0, 0, width, 1600);
// get the base map
context.drawImage(await this.baseMap, sourceXY.x, sourceXY.y, offsetX * 2, offsetY * 2, 0, 0, 640, 367);
context.drawImage(this.baseMap, sourceXY.x, sourceXY.y, offsetX * 2, offsetY * 2, 0, 0, 640, 367);
// crop the radar image
const cropCanvas = document.createElement('canvas');

View File

@@ -1,16 +1,21 @@
import { getWeatherRegionalIconFromIconLink } from './icons.mjs';
import { preloadImg } from './utils/image.mjs';
import { json } from './utils/fetch.mjs';
import { temperature as temperatureUnit } from './utils/units.mjs';
const buildForecast = (forecast, city, cityXY) => ({
daytime: forecast.isDaytime,
temperature: forecast.temperature || 0,
name: formatCity(city.city),
icon: forecast.icon,
x: cityXY.x,
y: cityXY.y,
time: forecast.startTime,
});
const buildForecast = (forecast, city, cityXY) => {
// get a unit converter
const temperatureConverter = temperatureUnit('us');
return {
daytime: forecast.isDaytime,
temperature: temperatureConverter(forecast.temperature || 0),
name: formatCity(city.city),
icon: forecast.icon,
x: cityXY.x,
y: cityXY.y,
time: forecast.startTime,
};
};
const getRegionalObservation = async (point, city) => {
try {

View File

@@ -4,7 +4,7 @@
import STATUS from './status.mjs';
import { distance as calcDistance } from './utils/calc.mjs';
import { json } from './utils/fetch.mjs';
import { celsiusToFahrenheit } from './utils/units.mjs';
import { temperature as temperatureUnit } from './utils/units.mjs';
import { getWeatherRegionalIconFromIconLink } from './icons.mjs';
import { preloadImg } from './utils/image.mjs';
import { DateTime } from '../vendor/auto/luxon.mjs';
@@ -21,9 +21,11 @@ class RegionalForecast extends WeatherDisplay {
this.timing.totalScreens = 3;
}
async getData(_weatherParameters) {
if (!super.getData(_weatherParameters)) return;
const weatherParameters = _weatherParameters ?? this.weatherParameters;
async getData(weatherParameters, refresh) {
if (!super.getData(weatherParameters, refresh)) return;
// regional forecast implements a silent reload
// but it will not fall back to previously loaded data if data can not be loaded
// there are enough other cities available to populate the map sufficiently even if some do not load
// pre-load the base map
let baseMap = 'images/Basemap2.png';
@@ -40,14 +42,14 @@ class RegionalForecast extends WeatherDisplay {
y: 117,
};
// get user's location in x/y
const sourceXY = utils.getXYFromLatitudeLongitude(weatherParameters.latitude, weatherParameters.longitude, offsetXY.x, offsetXY.y, weatherParameters.state);
const sourceXY = utils.getXYFromLatitudeLongitude(this.weatherParameters.latitude, this.weatherParameters.longitude, offsetXY.x, offsetXY.y, weatherParameters.state);
// get latitude and longitude limits
const minMaxLatLon = utils.getMinMaxLatitudeLongitude(sourceXY.x, sourceXY.y, offsetXY.x, offsetXY.y, weatherParameters.state);
const minMaxLatLon = utils.getMinMaxLatitudeLongitude(sourceXY.x, sourceXY.y, offsetXY.x, offsetXY.y, this.weatherParameters.state);
// get a target distance
let targetDistance = 2.5;
if (weatherParameters.state === 'HI') targetDistance = 1;
if (this.weatherParameters.state === 'HI') targetDistance = 1;
// make station info into an array
const stationInfoArray = Object.values(StationInfo).map((value) => ({ ...value, targetDistance }));
@@ -59,7 +61,7 @@ class RegionalForecast extends WeatherDisplay {
const regionalCities = [];
combinedCities.forEach((city) => {
if (city.lat > minMaxLatLon.minLat && city.lat < minMaxLatLon.maxLat
&& city.lon > minMaxLatLon.minLon && city.lon < minMaxLatLon.maxLon - 1) {
&& city.lon > minMaxLatLon.minLon && city.lon < minMaxLatLon.maxLon - 1) {
// default to 1 for cities loaded from RegionalCities, use value calculate above for remaining stations
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.
@@ -71,6 +73,9 @@ class RegionalForecast extends WeatherDisplay {
}
});
// get a unit converter
const temperatureConverter = temperatureUnit();
// get regional forecasts and observations (the two are intertwined due to the design of api.weather.gov)
const regionalDataAll = await Promise.all(regionalCities.map(async (city) => {
try {
@@ -83,7 +88,7 @@ class RegionalForecast extends WeatherDisplay {
const forecast = await json(`https://api.weather.gov/gridpoints/${point.wfo}/${point.x},${point.y}/forecast`);
// get XY on map for city
const cityXY = utils.getXYForCity(city, minMaxLatLon.maxLat, minMaxLatLon.minLon, weatherParameters.state);
const cityXY = utils.getXYForCity(city, minMaxLatLon.maxLat, minMaxLatLon.minLon, this.weatherParameters.state);
// wait for the regional observation if it's not done yet
const observation = await observationPromise;
@@ -93,7 +98,7 @@ class RegionalForecast extends WeatherDisplay {
// format the observation the same as the forecast
const regionalObservation = {
daytime: !!/\/day\//.test(observation.icon),
temperature: celsiusToFahrenheit(observation.temperature.value),
temperature: temperatureConverter(observation.temperature.value),
name: utils.formatCity(city.city),
icon: observation.icon,
x: cityXY.x,

View File

@@ -8,16 +8,54 @@ document.addEventListener('DOMContentLoaded', () => {
const settings = { speed: { value: 1.0 } };
const init = () => {
// create settings
settings.wide = new Setting('wide', 'Widescreen', 'boolean', false, wideScreenChange, true);
settings.kiosk = new Setting('kiosk', 'Kiosk', 'boolean', false, kioskChange, false);
settings.speed = new Setting('speed', 'Speed', 'select', 1.0, null, true, [
[0.5, 'Very Fast'],
[0.75, 'Fast'],
[1.0, 'Normal'],
[1.25, 'Slow'],
[1.5, 'Very Slow'],
]);
// create settings see setting.mjs for defaults
settings.wide = new Setting('wide', {
name: 'Widescreen',
defaultValue: false,
changeAction: wideScreenChange,
sticky: true,
});
settings.kiosk = new Setting('kiosk', {
name: 'Kiosk',
defaultValue: false,
changeAction: kioskChange,
sticky: false,
});
settings.speed = new Setting('speed', {
name: 'Speed',
type: 'select',
defaultValue: 1.0,
values: [
[0.5, 'Very Fast'],
[0.75, 'Fast'],
[1.0, 'Normal'],
[1.25, 'Slow'],
[1.5, 'Very Slow'],
],
});
settings.units = new Setting('units', {
name: 'Units',
type: 'select',
defaultValue: 'us',
changeAction: unitChange,
values: [
['us', 'US'],
['si', 'Metric'],
],
});
settings.refreshTime = new Setting('refreshTime', {
type: 'select',
defaultValue: 600_000,
sticky: false,
values: [
[30_000, 'TESTING'],
[300_000, '5 minutes'],
[600_000, '10 minutes'],
[900_000, '15 minutes'],
[1_800_000, '30 minutes'],
],
visible: false,
});
// generate html objects
const settingHtml = Object.values(settings).map((d) => d.generate());
@@ -47,4 +85,13 @@ const kioskChange = (value) => {
}
};
const unitChange = () => {
// reload the data at the top level to refresh units
// after the initial load
if (unitChange.firstRunDone) {
window.location.reload();
}
unitChange.firstRunDone = true;
};
export default settings;

View File

@@ -5,6 +5,7 @@ import { getWeatherRegionalIconFromIconLink } from './icons.mjs';
import { DateTime } from '../vendor/auto/luxon.mjs';
import WeatherDisplay from './weatherdisplay.mjs';
import { registerDisplay } from './navigation.mjs';
import settings from './settings.mjs';
class TravelForecast extends WeatherDisplay {
constructor(navId, elemId, defaultActive) {
@@ -25,16 +26,42 @@ class TravelForecast extends WeatherDisplay {
if (extra !== 0) this.timing.delay.push(Math.round(this.extra * this.cityHeight));
// add the final 3 second delay
this.timing.delay.push(150);
// add previous data cache
this.previousData = [];
}
async getData() {
async getData(weatherParameters, refresh) {
// super checks for enabled
if (!super.getData()) return;
const forecastPromises = TravelCities.map(async (city) => {
if (!super.getData(weatherParameters, refresh)) return;
// clear stored data if not refresh
if (!refresh) {
this.previousData = [];
}
const forecastPromises = TravelCities.map(async (city, index) => {
try {
// get point then forecast
if (!city.point) throw new Error('No pre-loaded point');
const forecast = await json(`https://api.weather.gov/gridpoints/${city.point.wfo}/${city.point.x},${city.point.y}/forecast`);
let forecast;
try {
forecast = await json(`https://api.weather.gov/gridpoints/${city.point.wfo}/${city.point.x},${city.point.y}/forecast`, {
data: {
units: settings.units.value,
},
});
// store for the next run
this.previousData[index] = forecast;
} catch (e) {
// if there's previous data use it
if (this.previousData?.[index]) {
forecast = this.previousData?.[index];
} else {
// otherwise re-throw for the standard error handling
throw (e);
}
}
// determine today or tomorrow (shift periods by 1 if tomorrow)
const todayShift = forecast.properties.periods[0].isDaytime ? 0 : 1;
// return a pared-down forecast
@@ -131,7 +158,7 @@ class TravelForecast extends WeatherDisplay {
// base count change callback
baseCountChange(count) {
// calculate scroll offset and don't go past end
let offsetY = Math.min(this.elem.querySelector('.travel-lines').getBoundingClientRect().height - 289, (count - 150));
let offsetY = Math.min(this.elem.querySelector('.travel-lines').offsetHeight - 289, (count - 150));
// don't let offset go negative
if (offsetY < 0) offsetY = 0;

View File

@@ -73,7 +73,7 @@ const doFetch = (url, params) => new Promise((resolve, reject) => {
// out of retries
return resolve(response);
})
.catch((error) => reject(error));
.catch(reject);
});
const delay = (time, func, ...args) => new Promise((resolve) => {

View File

@@ -2,37 +2,58 @@ import { parseQueryString } from '../share.mjs';
const SETTINGS_KEY = 'Settings';
const DEFAULTS = {
shortName: undefined,
name: undefined,
type: 'checkbox',
defaultValue: undefined,
changeAction: () => { },
sticky: true,
values: [],
visible: true,
};
class Setting {
constructor(shortName, name, type, defaultValue, changeAction, sticky, values) {
// store values
constructor(shortName, _options) {
if (shortName === undefined) {
throw new Error('No name provided for setting');
}
// merge options with defaults
const options = { ...DEFAULTS, ...(_options ?? {}) };
// store values and combine with defaults
this.shortName = shortName;
this.name = name;
this.defaultValue = defaultValue;
this.myValue = defaultValue;
this.type = type ?? 'checkbox';
this.sticky = sticky;
this.values = values;
// a default blank change function is provided
this.changeAction = changeAction ?? (() => { });
this.name = options.name ?? shortName;
this.defaultValue = options.defaultValue;
this.myValue = this.defaultValue;
this.type = options?.type;
this.sticky = options.sticky;
this.values = options.values;
this.visible = options.visible;
this.changeAction = options.changeAction;
// get value from url
const urlValue = parseQueryString()?.[`settings-${shortName}-${type}`];
const urlValue = parseQueryString()?.[`settings-${shortName}-${this.type}`];
let urlState;
if (type === 'checkbox' && urlValue !== undefined) {
if (this.type === 'checkbox' && urlValue !== undefined) {
urlState = urlValue === 'true';
}
if (type === 'select' && urlValue !== undefined) {
if (this.type === 'select' && urlValue !== undefined) {
urlState = parseFloat(urlValue);
}
if (this.type === 'select' && urlValue !== undefined && Number.isNaN(urlState)) {
// couldn't parse as a float, store as a string
urlState = urlValue;
}
// get existing value if present
const storedValue = urlState ?? this.getFromLocalStorage();
if (sticky && storedValue !== null) {
if ((this.sticky || urlValue !== undefined) && storedValue !== null) {
this.myValue = storedValue;
}
// call the change function on startup
switch (type) {
switch (this.type) {
case 'select':
this.selectChange({ target: { value: this.myValue } });
break;
@@ -59,7 +80,11 @@ class Setting {
this.values.forEach(([value, text]) => {
const option = document.createElement('option');
option.value = value.toFixed(2);
if (typeof value === 'number') {
option.value = value.toFixed(2);
} else {
option.value = value;
}
option.innerHTML = text;
select.append(option);
@@ -108,6 +133,10 @@ class Setting {
selectChange(e) {
// update the value
this.myValue = parseFloat(e.target.value);
if (Number.isNaN(this.myValue)) {
// was a string, store as such
this.myValue = e.target.value;
}
this.storeToLocalStorage(this.myValue);
// call the change action
@@ -130,6 +159,7 @@ class Setting {
if (storedValue !== undefined) {
switch (this.type) {
case 'boolean':
case 'checkbox':
return storedValue;
case 'select':
return storedValue;
@@ -155,6 +185,8 @@ class Setting {
case 'select':
this.selectHighlight(newValue);
break;
case 'boolean':
break;
case 'checkbox':
default:
this.element.checked = newValue;
@@ -167,12 +199,15 @@ class Setting {
selectHighlight(newValue) {
// set the dropdown to the provided value
this.element.querySelectorAll('option').forEach((elem) => {
elem.selected = newValue.toFixed(2) === elem.value;
this?.element?.querySelectorAll('option')?.forEach?.((elem) => {
elem.selected = (newValue?.toFixed?.(2) === elem.value) || (newValue === elem.value);
});
}
generate() {
// don't generate a control for not visible items
if (!this.visible) return '';
// call the appropriate control generator
switch (this.type) {
case 'select':
return this.generateSelect();

View File

@@ -1,18 +1,113 @@
// get the settings for units
import settings from '../settings.mjs';
// *********************************** unit conversions ***********************
// round 2 provided for lat/lon formatting
const round2 = (value, decimals) => Math.trunc(value * 10 ** decimals) / 10 ** decimals;
const kphToMph = (Kph) => Math.round(Kph / 1.609_34);
const celsiusToFahrenheit = (Celsius) => Math.round((Celsius * 9) / 5 + 32);
const fahrenheitToCelsius = (Fahrenheit) => Math.round((Fahrenheit - 32) * 5 / 9);
const kilometersToMiles = (Kilometers) => Math.round(Kilometers / 1.609_34);
const metersToFeet = (Meters) => Math.round(Meters / 0.3048);
const pascalToInHg = (Pascal) => round2(Pascal * 0.000_295_3, 2);
// each module/page/slide creates it's own unit converter as needed by providing the base units available
// the factory function then returns an appropriate converter or pass-thru function for use on the page
const windSpeed = (defaultUnit = 'si') => {
// default to passthru
let converter = (passthru) => Math.round(passthru);
// change the converter if there is a mismatch
if (defaultUnit !== settings.units.value) {
converter = kphToMph;
}
// append units
if (settings.units.value === 'si') {
converter.units = 'kph';
} else {
converter.units = 'MPH';
}
return converter;
};
const temperature = (defaultUnit = 'si') => {
// default to passthru
let converter = (passthru) => Math.round(passthru);
// change the converter if there is a mismatch
if (defaultUnit !== settings.units.value) {
if (defaultUnit === 'us') {
converter = fahrenheitToCelsius;
} else {
converter = celsiusToFahrenheit;
}
}
// append units
if (settings.units.value === 'si') {
converter.units = 'C';
} else {
converter.units = 'F';
}
return converter;
};
const distanceMeters = (defaultUnit = 'si') => {
// default to passthru
let converter = (passthru) => Math.round(passthru);
// change the converter if there is a mismatch
if (defaultUnit !== settings.units.value) {
// rounded to the nearest 100 (ceiling)
converter = (value) => Math.round(metersToFeet(value) / 100) * 100;
}
// append units
if (settings.units.value === 'si') {
converter.units = 'm.';
} else {
converter.units = 'ft.';
}
return converter;
};
const distanceKilometers = (defaultUnit = 'si') => {
// default to passthru
let converter = (passthru) => Math.round(passthru / 1000);
// change the converter if there is a mismatch
if (defaultUnit !== settings.units.value) {
converter = (value) => Math.round(kilometersToMiles(value) / 1000);
}
// append units
if (settings.units.value === 'si') {
converter.units = ' km.';
} else {
converter.units = ' mi.';
}
return converter;
};
const pressure = (defaultUnit = 'si') => {
// default to passthru (millibar)
let converter = (passthru) => Math.round(passthru / 100);
// change the converter if there is a mismatch
if (defaultUnit !== settings.units.value) {
converter = (value) => pascalToInHg(value).toFixed(2);
}
// append units
if (settings.units.value === 'si') {
converter.units = ' mbar';
} else {
converter.units = ' in.hg';
}
return converter;
};
export {
kphToMph,
celsiusToFahrenheit,
kilometersToMiles,
metersToFeet,
pascalToInHg,
// unit conversions
windSpeed,
temperature,
distanceMeters,
distanceKilometers,
pressure,
// formatter
round2,
};

View File

@@ -22,6 +22,7 @@ class WeatherDisplay {
this.okToDrawCurrentConditions = true;
this.okToDrawCurrentDateTime = true;
this.showOnProgress = true;
this.autoRefreshHandle = null;
// default navigation timing
this.timing = {
@@ -129,9 +130,13 @@ class WeatherDisplay {
}
// get necessary data for this display
getData(weatherParameters) {
// clear current data
this.data = undefined;
getData(weatherParameters, refresh) {
// refresh doesn't delete existing data, and is reused if the silent refresh fails
if (!refresh) {
this.data = undefined;
// clear any refresh timers
this.clearAutoReload();
}
// store weatherParameters locally in case we need them later
if (weatherParameters) this.weatherParameters = weatherParameters;
@@ -144,6 +149,9 @@ class WeatherDisplay {
return false;
}
// set up auto reload if necessary
this.setAutoReload();
// recalculate navigation timing (in case it was modified in the constructor)
this.calcNavTiming();
return true;
@@ -426,6 +434,15 @@ class WeatherDisplay {
this.stillWaitingCallbacks.forEach((callback) => callback());
this.stillWaitingCallbacks = [];
}
clearAutoReload() {
clearInterval(this.autoRefreshHandle);
this.autoRefreshHandle = null;
}
setAutoReload() {
this.autoRefreshHandle = this.autoRefreshHandle ?? setInterval(() => this.getData(false, true), settings.refreshTime.value);
}
}
export default WeatherDisplay;

File diff suppressed because one or more lines are too long

View File

@@ -392,12 +392,13 @@ class SystemZone extends Zone {
}
}
let dtfCache = {};
function makeDTF(zone) {
if (!dtfCache[zone]) {
dtfCache[zone] = new Intl.DateTimeFormat("en-US", {
const dtfCache = new Map();
function makeDTF(zoneName) {
let dtf = dtfCache.get(zoneName);
if (dtf === undefined) {
dtf = new Intl.DateTimeFormat("en-US", {
hour12: false,
timeZone: zone,
timeZone: zoneName,
year: "numeric",
month: "2-digit",
day: "2-digit",
@@ -406,8 +407,9 @@ function makeDTF(zone) {
second: "2-digit",
era: "short",
});
dtfCache.set(zoneName, dtf);
}
return dtfCache[zone];
return dtf;
}
const typeToPos = {
@@ -443,7 +445,7 @@ function partsOffset(dtf, date) {
return filled;
}
let ianaZoneCache = {};
const ianaZoneCache = new Map();
/**
* A zone identified by an IANA identifier, like America/New_York
* @implements {Zone}
@@ -454,10 +456,11 @@ class IANAZone extends Zone {
* @return {IANAZone}
*/
static create(name) {
if (!ianaZoneCache[name]) {
ianaZoneCache[name] = new IANAZone(name);
let zone = ianaZoneCache.get(name);
if (zone === undefined) {
ianaZoneCache.set(name, (zone = new IANAZone(name)));
}
return ianaZoneCache[name];
return zone;
}
/**
@@ -465,8 +468,8 @@ class IANAZone extends Zone {
* @return {void}
*/
static resetCache() {
ianaZoneCache = {};
dtfCache = {};
ianaZoneCache.clear();
dtfCache.clear();
}
/**
@@ -569,6 +572,7 @@ class IANAZone extends Zone {
* @return {number}
*/
offset(ts) {
if (!this.valid) return NaN;
const date = new Date(ts);
if (isNaN(date)) return NaN;
@@ -634,36 +638,36 @@ function getCachedLF(locString, opts = {}) {
return dtf;
}
let intlDTCache = {};
const intlDTCache = new Map();
function getCachedDTF(locString, opts = {}) {
const key = JSON.stringify([locString, opts]);
let dtf = intlDTCache[key];
if (!dtf) {
let dtf = intlDTCache.get(key);
if (dtf === undefined) {
dtf = new Intl.DateTimeFormat(locString, opts);
intlDTCache[key] = dtf;
intlDTCache.set(key, dtf);
}
return dtf;
}
let intlNumCache = {};
const intlNumCache = new Map();
function getCachedINF(locString, opts = {}) {
const key = JSON.stringify([locString, opts]);
let inf = intlNumCache[key];
if (!inf) {
let inf = intlNumCache.get(key);
if (inf === undefined) {
inf = new Intl.NumberFormat(locString, opts);
intlNumCache[key] = inf;
intlNumCache.set(key, inf);
}
return inf;
}
let intlRelCache = {};
const intlRelCache = new Map();
function getCachedRTF(locString, opts = {}) {
const { base, ...cacheKeyOpts } = opts; // exclude `base` from the options
const key = JSON.stringify([locString, cacheKeyOpts]);
let inf = intlRelCache[key];
if (!inf) {
let inf = intlRelCache.get(key);
if (inf === undefined) {
inf = new Intl.RelativeTimeFormat(locString, opts);
intlRelCache[key] = inf;
intlRelCache.set(key, inf);
}
return inf;
}
@@ -678,14 +682,28 @@ function systemLocale() {
}
}
let weekInfoCache = {};
const intlResolvedOptionsCache = new Map();
function getCachedIntResolvedOptions(locString) {
let opts = intlResolvedOptionsCache.get(locString);
if (opts === undefined) {
opts = new Intl.DateTimeFormat(locString).resolvedOptions();
intlResolvedOptionsCache.set(locString, opts);
}
return opts;
}
const weekInfoCache = new Map();
function getCachedWeekInfo(locString) {
let data = weekInfoCache[locString];
let data = weekInfoCache.get(locString);
if (!data) {
const locale = new Intl.Locale(locString);
// browsers currently implement this as a property, but spec says it should be a getter function
data = "getWeekInfo" in locale ? locale.getWeekInfo() : locale.weekInfo;
weekInfoCache[locString] = data;
// minimalDays was removed from WeekInfo: https://github.com/tc39/proposal-intl-locale-info/issues/86
if (!("minimalDays" in data)) {
data = { ...fallbackWeekSettings, ...data };
}
weekInfoCache.set(locString, data);
}
return data;
}
@@ -784,7 +802,7 @@ function supportsFastNumbers(loc) {
loc.numberingSystem === "latn" ||
!loc.locale ||
loc.locale.startsWith("en") ||
new Intl.DateTimeFormat(loc.intl).resolvedOptions().numberingSystem === "latn"
getCachedIntResolvedOptions(loc.locale).numberingSystem === "latn"
);
}
}
@@ -943,7 +961,6 @@ const fallbackWeekSettings = {
/**
* @private
*/
class Locale {
static fromOpts(opts) {
return Locale.create(
@@ -967,9 +984,11 @@ class Locale {
static resetCache() {
sysLocaleCache = null;
intlDTCache = {};
intlNumCache = {};
intlRelCache = {};
intlDTCache.clear();
intlNumCache.clear();
intlRelCache.clear();
intlResolvedOptionsCache.clear();
weekInfoCache.clear();
}
static fromObject({ locale, numberingSystem, outputCalendar, weekSettings } = {}) {
@@ -1123,7 +1142,7 @@ class Locale {
return (
this.locale === "en" ||
this.locale.toLowerCase() === "en-us" ||
new Intl.DateTimeFormat(this.intl).resolvedOptions().locale.startsWith("en-us")
getCachedIntResolvedOptions(this.intl).locale.startsWith("en-us")
);
}
@@ -1461,22 +1480,26 @@ function parseDigits(str) {
}
// cache of {numberingSystem: {append: regex}}
let digitRegexCache = {};
const digitRegexCache = new Map();
function resetDigitRegexCache() {
digitRegexCache = {};
digitRegexCache.clear();
}
function digitRegex({ numberingSystem }, append = "") {
const ns = numberingSystem || "latn";
if (!digitRegexCache[ns]) {
digitRegexCache[ns] = {};
let appendCache = digitRegexCache.get(ns);
if (appendCache === undefined) {
appendCache = new Map();
digitRegexCache.set(ns, appendCache);
}
if (!digitRegexCache[ns][append]) {
digitRegexCache[ns][append] = new RegExp(`${numberingSystems[ns]}${append}`);
let regex = appendCache.get(append);
if (regex === undefined) {
regex = new RegExp(`${numberingSystems[ns]}${append}`);
appendCache.set(append, regex);
}
return digitRegexCache[ns][append];
return regex;
}
let now = () => Date.now(),
@@ -4227,6 +4250,14 @@ class Interval {
return this.isValid ? this.e : null;
}
/**
* Returns the last DateTime included in the interval (since end is not part of the interval)
* @type {DateTime}
*/
get lastDateTime() {
return this.isValid ? (this.e ? this.e.minus(1) : null) : null;
}
/**
* Returns whether this Interval's end is at least its start, meaning that the Interval isn't 'backwards'.
* @type {boolean}
@@ -4491,8 +4522,11 @@ class Interval {
}
/**
* Merge an array of Intervals into a equivalent minimal set of Intervals.
* Merge an array of Intervals into an equivalent minimal set of Intervals.
* Combines overlapping and adjacent Intervals.
* The resulting array will contain the Intervals in ascending order, that is, starting with the earliest Interval
* and ending with the latest.
*
* @param {Array} intervals
* @return {Array}
*/
@@ -5815,15 +5849,27 @@ function normalizeUnitWithLocalWeeks(unit) {
// This is safe for quickDT (used by local() and utc()) because we don't fill in
// higher-order units from tsNow (as we do in fromObject, this requires that
// offset is calculated from tsNow).
/**
* @param {Zone} zone
* @return {number}
*/
function guessOffsetForZone(zone) {
if (!zoneOffsetGuessCache[zone]) {
if (zoneOffsetTs === undefined) {
zoneOffsetTs = Settings.now();
}
zoneOffsetGuessCache[zone] = zone.offset(zoneOffsetTs);
if (zoneOffsetTs === undefined) {
zoneOffsetTs = Settings.now();
}
return zoneOffsetGuessCache[zone];
// Do not cache anything but IANA zones, because it is not safe to do so.
// Guessing an offset which is not present in the zone can cause wrong results from fixOffset
if (zone.type !== "iana") {
return zone.offset(zoneOffsetTs);
}
const zoneName = zone.name;
let offsetGuess = zoneOffsetGuessCache.get(zoneName);
if (offsetGuess === undefined) {
offsetGuess = zone.offset(zoneOffsetTs);
zoneOffsetGuessCache.set(zoneName, offsetGuess);
}
return offsetGuess;
}
// this is a dumbed down version of fromObject() that runs about 60% faster
@@ -5913,7 +5959,7 @@ let zoneOffsetTs;
* This optimizes quickDT via guessOffsetForZone to avoid repeated calls of
* zone.offset().
*/
let zoneOffsetGuessCache = {};
const zoneOffsetGuessCache = new Map();
/**
* A DateTime is an immutable data structure representing a specific date and time and accompanying methods. It contains class and instance methods for creating, parsing, interrogating, transforming, and formatting them.
@@ -6478,7 +6524,7 @@ class DateTime {
static resetCache() {
zoneOffsetTs = undefined;
zoneOffsetGuessCache = {};
zoneOffsetGuessCache.clear();
}
// INFO
@@ -7247,7 +7293,7 @@ class DateTime {
* @example DateTime.now().toISO() //=> '2017-04-22T20:47:05.335-04:00'
* @example DateTime.now().toISO({ includeOffset: false }) //=> '2017-04-22T20:47:05.335'
* @example DateTime.now().toISO({ format: 'basic' }) //=> '20170422T204705.335-0400'
* @return {string}
* @return {string|null}
*/
toISO({
format = "extended",
@@ -7274,7 +7320,7 @@ class DateTime {
* @param {string} [opts.format='extended'] - choose between the basic and extended format
* @example DateTime.utc(1982, 5, 25).toISODate() //=> '1982-05-25'
* @example DateTime.utc(1982, 5, 25).toISODate({ format: 'basic' }) //=> '19820525'
* @return {string}
* @return {string|null}
*/
toISODate({ format = "extended" } = {}) {
if (!this.isValid) {
@@ -7359,7 +7405,7 @@ class DateTime {
/**
* Returns a string representation of this DateTime appropriate for use in SQL Date
* @example DateTime.utc(2014, 7, 13).toSQLDate() //=> '2014-07-13'
* @return {string}
* @return {string|null}
*/
toSQLDate() {
if (!this.isValid) {
@@ -7454,7 +7500,7 @@ class DateTime {
}
/**
* Returns the epoch seconds of this DateTime.
* Returns the epoch seconds (including milliseconds in the fractional part) of this DateTime.
* @return {number}
*/
toSeconds() {
@@ -7561,7 +7607,7 @@ class DateTime {
/**
* Return an Interval spanning between this DateTime and another DateTime
* @param {DateTime} otherDateTime - the other end point of the Interval
* @return {Interval}
* @return {Interval|DateTime}
*/
until(otherDateTime) {
return this.isValid ? Interval.fromDateTimes(this, otherDateTime) : this;
@@ -7979,7 +8025,7 @@ function friendlyDateTime(dateTimeish) {
}
}
const VERSION = "3.5.0";
const VERSION = "3.6.1";
export { DateTime, Duration, FixedOffsetZone, IANAZone, Info, Interval, InvalidZone, Settings, SystemZone, VERSION, Zone };
//# sourceMappingURL=luxon.js.map

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -82,6 +82,14 @@
.like {
left: 425px;
&.heat-index {
color: #e00;
}
&.wind-chill {
color: c.$extended-low;
}
}
.wind {

View File

@@ -0,0 +1,34 @@
.media {
display: none;
}
#ToggleMedia {
display: none;
&.available {
display: inline-block;
img.on {
display: none;
}
img.off {
display: block;
}
// icon switch is handled by adding/removing the .playing class
&.playing {
img.on {
display: block;
}
img.off {
display: none;
}
}
}
}

View File

@@ -748,8 +748,7 @@ body {
>.heading,
#enabledDisplays,
#settings,
#divInfo,
#divRefresh {
#divInfo {
display: none;
}
}

View File

@@ -11,4 +11,5 @@
@import 'radar';
@import 'regional-forecast';
@import 'almanac';
@import 'hazards';
@import 'hazards';
@import 'media';