modular
This commit is contained in:
parent
b71d696670
commit
6933e7b7f1
|
@ -10,6 +10,13 @@ module.exports = {
|
|||
globals: {
|
||||
Atomics: 'readonly',
|
||||
SharedArrayBuffer: 'readonly',
|
||||
StationInfo: 'readonly',
|
||||
RegionalCities: 'readonly',
|
||||
TravelCities: 'readonly',
|
||||
NoSleep: 'readonly',
|
||||
states: 'readonly',
|
||||
SunCalc: 'readonly',
|
||||
|
||||
},
|
||||
parserOptions: {
|
||||
ecmaVersion: 2021,
|
||||
|
|
|
@ -6796,13 +6796,6 @@ const StationInfo = {
|
|||
lat: 39.4429,
|
||||
lon: -87.32207,
|
||||
},
|
||||
KEYE: {
|
||||
id: 'KEYE',
|
||||
city: 'Indianapolis, Eagle Creek Airpark',
|
||||
state: 'IN',
|
||||
lat: 39.825,
|
||||
lon: -86.29583,
|
||||
},
|
||||
KGSH: {
|
||||
id: 'KGSH',
|
||||
city: 'Goshen, Goshen Municipal Airport',
|
||||
|
@ -19544,4 +19537,4 @@ const StationInfo = {
|
|||
lon: -78.5016,
|
||||
},
|
||||
|
||||
};
|
||||
};
|
||||
|
|
|
@ -6798,13 +6798,6 @@ const StationInfo = {
|
|||
lat: 39.4429,
|
||||
lon: -87.32207,
|
||||
},
|
||||
KEYE: {
|
||||
id: 'KEYE',
|
||||
city: 'Indianapolis, Eagle Creek Airpark',
|
||||
state: 'IN',
|
||||
lat: 39.825,
|
||||
lon: -86.29583,
|
||||
},
|
||||
KGSH: {
|
||||
id: 'KGSH',
|
||||
city: 'Goshen, Goshen Municipal Airport',
|
||||
|
|
|
@ -1,21 +1,17 @@
|
|||
import { UNITS } from './modules/config.mjs';
|
||||
import { setUnits } from './modules/utils/units.mjs';
|
||||
import { json } from './modules/utils/fetch.mjs';
|
||||
import noSleep from './modules/utils/nosleep.mjs';
|
||||
import {
|
||||
message as navMessage, isPlaying, resize, resetStatuses, latLonReceived, stopAutoRefreshTimer, registerRefreshData,
|
||||
} from './modules/navigation.mjs';
|
||||
|
||||
/* globals NoSleep, states, navigation */
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
init();
|
||||
});
|
||||
|
||||
const overrides = {};
|
||||
const AutoRefreshIntervalMs = 500;
|
||||
const AutoRefreshTotalIntervalMs = 600000; // 10 min.
|
||||
|
||||
let AutoSelectQuery = false;
|
||||
|
||||
let LastUpdate = null;
|
||||
let AutoRefreshIntervalId = null;
|
||||
let AutoRefreshCountMs = 0;
|
||||
|
||||
let FullScreenOverride = false;
|
||||
|
||||
const categories = [
|
||||
|
@ -35,6 +31,8 @@ const init = () => {
|
|||
e.target.select();
|
||||
});
|
||||
|
||||
registerRefreshData(LoadTwcData);
|
||||
|
||||
document.getElementById('NavigateMenu').addEventListener('click', btnNavigateMenuClick);
|
||||
document.getElementById('NavigateRefresh').addEventListener('click', btnNavigateRefreshClick);
|
||||
document.getElementById('NavigateNext').addEventListener('click', btnNavigateNextClick);
|
||||
|
@ -127,36 +125,15 @@ const init = () => {
|
|||
const TwcUnits = localStorage.getItem('TwcUnits');
|
||||
if (!TwcUnits || TwcUnits === 'ENGLISH') {
|
||||
document.getElementById('radEnglish').checked = true;
|
||||
navigation.message({ type: 'units', message: 'english' });
|
||||
setUnits('english');
|
||||
} else if (TwcUnits === 'METRIC') {
|
||||
document.getElementById('radMetric').checked = true;
|
||||
navigation.message({ type: 'units', message: 'metric' });
|
||||
setUnits('metric');
|
||||
}
|
||||
|
||||
document.getElementById('radEnglish').addEventListener('change', changeUnits);
|
||||
document.getElementById('radMetric').addEventListener('change', changeUnits);
|
||||
|
||||
document.getElementById('chkAutoRefresh').addEventListener('change', (e) => {
|
||||
const Checked = e.target.checked;
|
||||
|
||||
if (LastUpdate) {
|
||||
if (Checked) {
|
||||
StartAutoRefreshTimer();
|
||||
} else {
|
||||
StopAutoRefreshTimer();
|
||||
}
|
||||
}
|
||||
|
||||
localStorage.setItem('TwcAutoRefresh', Checked);
|
||||
});
|
||||
|
||||
const TwcAutoRefresh = localStorage.getItem('TwcAutoRefresh');
|
||||
if (!TwcAutoRefresh || TwcAutoRefresh === 'true') {
|
||||
document.getElementById('chkAutoRefresh').checked = true;
|
||||
} else {
|
||||
document.getElementById('chkAutoRefresh').checked = false;
|
||||
}
|
||||
|
||||
// swipe functionality
|
||||
document.getElementById('container').addEventListener('swiped-left', () => swipeCallBack('left'));
|
||||
document.getElementById('container').addEventListener('swiped-right', () => swipeCallBack('right'));
|
||||
|
@ -165,7 +142,6 @@ const init = () => {
|
|||
const changeUnits = (e) => {
|
||||
const Units = e.target.value;
|
||||
localStorage.setItem('TwcUnits', Units);
|
||||
AssignLastUpdate();
|
||||
postMessage('units', Units);
|
||||
};
|
||||
|
||||
|
@ -207,7 +183,7 @@ const btnFullScreenClick = () => {
|
|||
ExitFullscreen();
|
||||
}
|
||||
|
||||
if (navigation.isPlaying()) {
|
||||
if (isPlaying()) {
|
||||
noSleep(true);
|
||||
} else {
|
||||
noSleep(false);
|
||||
|
@ -233,7 +209,7 @@ const EnterFullScreen = () => {
|
|||
window.scrollTo(0, 0);
|
||||
FullScreenOverride = true;
|
||||
}
|
||||
navigation.resize();
|
||||
resize();
|
||||
UpdateFullScreenNavigate();
|
||||
|
||||
// change hover text
|
||||
|
@ -257,7 +233,7 @@ const ExitFullscreen = () => {
|
|||
} else if (document.msExitFullscreen) {
|
||||
document.msExitFullscreen();
|
||||
}
|
||||
navigation.resize();
|
||||
resize();
|
||||
// change hover text
|
||||
document.getElementById('ToggleFullScreen').title = 'Enter fullscreen';
|
||||
};
|
||||
|
@ -277,11 +253,8 @@ const LoadTwcData = (_latLon) => {
|
|||
if (!latLon) return;
|
||||
|
||||
document.getElementById('txtAddress').blur();
|
||||
StopAutoRefreshTimer();
|
||||
LastUpdate = null;
|
||||
AssignLastUpdate();
|
||||
|
||||
postMessage('latLon', latLon);
|
||||
stopAutoRefreshTimer();
|
||||
latLonReceived(latLon);
|
||||
};
|
||||
|
||||
const swipeCallBack = (direction) => {
|
||||
|
@ -297,29 +270,8 @@ const swipeCallBack = (direction) => {
|
|||
}
|
||||
};
|
||||
|
||||
const AssignLastUpdate = () => {
|
||||
if (LastUpdate) {
|
||||
switch (navigation.units()) {
|
||||
case UNITS.english:
|
||||
LastUpdate = LastUpdate.toLocaleString('en-US', {
|
||||
weekday: 'short', month: 'short', day: 'numeric', year: 'numeric', hour: 'numeric', minute: 'numeric', second: 'numeric', timeZoneName: 'short',
|
||||
});
|
||||
break;
|
||||
default:
|
||||
LastUpdate = LastUpdate.toLocaleString('en-GB', {
|
||||
weekday: 'short', month: 'short', day: 'numeric', year: 'numeric', hour: 'numeric', minute: 'numeric', second: 'numeric', timeZoneName: 'short',
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('spanLastRefresh').innerHTML = LastUpdate;
|
||||
|
||||
if (LastUpdate && document.getElementById('chkAutoRefresh').checked) StartAutoRefreshTimer();
|
||||
};
|
||||
|
||||
const btnNavigateRefreshClick = () => {
|
||||
navigation.resetStatuses();
|
||||
resetStatuses();
|
||||
LoadTwcData();
|
||||
UpdateFullScreenNavigate();
|
||||
|
||||
|
@ -410,77 +362,9 @@ const btnNavigatePlayClick = () => {
|
|||
return false;
|
||||
};
|
||||
|
||||
// read and dispatch an event from the iframe
|
||||
const message = (data) => {
|
||||
const playButton = document.getElementById('NavigatePlay');
|
||||
// dispatch event
|
||||
if (!data.type) return;
|
||||
switch (data.type) {
|
||||
case 'loaded':
|
||||
LastUpdate = new Date();
|
||||
AssignLastUpdate();
|
||||
break;
|
||||
|
||||
case 'weatherParameters':
|
||||
populateWeatherParameters(data.message);
|
||||
break;
|
||||
|
||||
case 'isPlaying':
|
||||
localStorage.setItem('TwcPlay', navigation.isPlaying());
|
||||
|
||||
if (navigation.isPlaying()) {
|
||||
noSleep(true);
|
||||
playButton.title = 'Pause';
|
||||
playButton.src = 'images/nav/ic_pause_white_24dp_1x.png';
|
||||
} else {
|
||||
noSleep(false);
|
||||
playButton.title = 'Play';
|
||||
playButton.src = 'images/nav/ic_play_arrow_white_24dp_1x.png';
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
console.error(`Unknown event '${data.eventType}`);
|
||||
}
|
||||
};
|
||||
|
||||
// post a message to the iframe
|
||||
const postMessage = (type, myMessage = {}) => {
|
||||
navigation.message({ type, message: myMessage });
|
||||
};
|
||||
|
||||
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 += AutoRefreshIntervalMs;
|
||||
|
||||
// Display the count down.
|
||||
let RemainingMs = (AutoRefreshTotalIntervalMs - AutoRefreshCountMs);
|
||||
if (RemainingMs < 0) {
|
||||
RemainingMs = 0;
|
||||
}
|
||||
const dt = new Date(RemainingMs);
|
||||
document.getElementById('spanRefreshCountDown').innerHTML = `${dt.getMinutes() < 10 ? `0${dt.getMinutes()}` : dt.getMinutes()}:${dt.getSeconds() < 10 ? `0${dt.getSeconds()}` : dt.getSeconds()}`;
|
||||
|
||||
// Time has elapsed.
|
||||
if (AutoRefreshCountMs >= AutoRefreshTotalIntervalMs && !navigation.isPlaying()) LoadTwcData();
|
||||
};
|
||||
AutoRefreshIntervalId = window.setInterval(AutoRefreshTimer, AutoRefreshIntervalMs);
|
||||
AutoRefreshTimer();
|
||||
};
|
||||
const StopAutoRefreshTimer = () => {
|
||||
if (AutoRefreshIntervalId) {
|
||||
window.clearInterval(AutoRefreshIntervalId);
|
||||
document.getElementById('spanRefreshCountDown').innerHTML = '--:--';
|
||||
AutoRefreshIntervalId = null;
|
||||
}
|
||||
navMessage({ type, message: myMessage });
|
||||
};
|
||||
|
||||
const btnGetGpsClick = async () => {
|
||||
|
@ -518,47 +402,3 @@ const btnGetGpsClick = async () => {
|
|||
// Save the query
|
||||
localStorage.setItem('TwcQuery', TwcQuery);
|
||||
};
|
||||
|
||||
const populateWeatherParameters = (weatherParameters) => {
|
||||
document.getElementById('spanCity').innerHTML = `${weatherParameters.city}, `;
|
||||
document.getElementById('spanState').innerHTML = weatherParameters.state;
|
||||
document.getElementById('spanStationId').innerHTML = weatherParameters.stationId;
|
||||
document.getElementById('spanRadarId').innerHTML = weatherParameters.radarId;
|
||||
document.getElementById('spanZoneId').innerHTML = weatherParameters.zoneId;
|
||||
};
|
||||
|
||||
// track state of nosleep locally to avoid a null case error
|
||||
// when nosleep.disable is called without first calling .enable
|
||||
let wakeLock = false;
|
||||
const noSleep = (enable = false) => {
|
||||
// get a nosleep controller
|
||||
if (!noSleep.controller) noSleep.controller = new NoSleep();
|
||||
// don't call anything if the states match
|
||||
if (wakeLock === enable) return false;
|
||||
// store the value
|
||||
wakeLock = enable;
|
||||
// call the function
|
||||
if (enable) return noSleep.controller.enable();
|
||||
return noSleep.controller.disable();
|
||||
};
|
||||
|
||||
const refreshCheck = () => {
|
||||
// Time has elapsed.
|
||||
if (AutoRefreshCountMs >= AutoRefreshTotalIntervalMs) {
|
||||
LoadTwcData();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
export {
|
||||
init,
|
||||
message,
|
||||
refreshCheck,
|
||||
};
|
||||
|
||||
window.index = {
|
||||
init,
|
||||
message,
|
||||
refreshCheck,
|
||||
};
|
||||
|
|
|
@ -3,8 +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';
|
||||
|
||||
/* globals SunCalc */
|
||||
import { registerDisplay } from './navigation.mjs';
|
||||
|
||||
class Almanac extends WeatherDisplay {
|
||||
constructor(navId, elemId) {
|
||||
|
@ -171,4 +170,8 @@ class Almanac extends WeatherDisplay {
|
|||
}
|
||||
}
|
||||
|
||||
export default Almanac;
|
||||
// register display
|
||||
const display = new Almanac(7, 'almanac');
|
||||
registerDisplay(display);
|
||||
|
||||
export default display.getSun.bind(display);
|
||||
|
|
|
@ -1,11 +0,0 @@
|
|||
const UNITS = {
|
||||
english: Symbol('english'),
|
||||
metric: Symbol('metric'),
|
||||
};
|
||||
|
||||
export {
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
UNITS,
|
||||
};
|
||||
|
||||
window.UNITS = UNITS;
|
|
@ -1,15 +1,13 @@
|
|||
// current weather conditions display
|
||||
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';
|
||||
import WeatherDisplay from './weatherdisplay.mjs';
|
||||
|
||||
/* globals navigation */
|
||||
import { registerDisplay } from './navigation.mjs';
|
||||
import { getUnits, UNITS, convert } from './utils/units.mjs';
|
||||
|
||||
class CurrentWeather extends WeatherDisplay {
|
||||
constructor(navId, elemId) {
|
||||
|
@ -100,20 +98,20 @@ class CurrentWeather extends WeatherDisplay {
|
|||
if (pressureDiff > 150) data.PressureDirection = 'R';
|
||||
if (pressureDiff < -150) data.PressureDirection = 'F';
|
||||
|
||||
if (navigation.units() === UNITS.english) {
|
||||
data.Temperature = units.celsiusToFahrenheit(data.Temperature);
|
||||
if (getUnits() === UNITS.english) {
|
||||
data.Temperature = convert.celsiusToFahrenheit(data.Temperature);
|
||||
data.TemperatureUnit = 'F';
|
||||
data.DewPoint = units.celsiusToFahrenheit(data.DewPoint);
|
||||
data.Ceiling = Math.round(units.metersToFeet(data.Ceiling) / 100) * 100;
|
||||
data.DewPoint = convert.celsiusToFahrenheit(data.DewPoint);
|
||||
data.Ceiling = Math.round(convert.metersToFeet(data.Ceiling) / 100) * 100;
|
||||
data.CeilingUnit = 'ft.';
|
||||
data.Visibility = units.kilometersToMiles(observations.visibility.value / 1000);
|
||||
data.Visibility = convert.kilometersToMiles(observations.visibility.value / 1000);
|
||||
data.VisibilityUnit = ' mi.';
|
||||
data.WindSpeed = units.kphToMph(data.WindSpeed);
|
||||
data.WindSpeed = convert.kphToMph(data.WindSpeed);
|
||||
data.WindUnit = 'MPH';
|
||||
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);
|
||||
data.Pressure = convert.pascalToInHg(data.Pressure).toFixed(2);
|
||||
data.HeatIndex = convert.celsiusToFahrenheit(data.HeatIndex);
|
||||
data.WindChill = convert.celsiusToFahrenheit(data.WindChill);
|
||||
data.WindGust = convert.kphToMph(data.WindGust);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
@ -191,4 +189,7 @@ class CurrentWeather extends WeatherDisplay {
|
|||
}
|
||||
}
|
||||
|
||||
export default CurrentWeather;
|
||||
const display = new CurrentWeather(0, 'current-weather');
|
||||
registerDisplay(display);
|
||||
|
||||
export default display.getCurrentWeather.bind(display);
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
/* globals navigation */
|
||||
import { locationCleanup } from './utils/string.mjs';
|
||||
import { elemForEach } from './utils/elem.mjs';
|
||||
import getCurrentWeather from './currentweather.mjs';
|
||||
import { currentDisplay } from './navigation.mjs';
|
||||
|
||||
// constants
|
||||
const degree = String.fromCharCode(176);
|
||||
|
@ -24,12 +25,17 @@ const start = () => {
|
|||
};
|
||||
|
||||
const stop = (reset) => {
|
||||
if (interval) interval = clearInterval(interval);
|
||||
if (reset) screenIndex = 0;
|
||||
};
|
||||
|
||||
// increment interval, roll over
|
||||
const incrementInterval = () => {
|
||||
// test current screen
|
||||
const display = currentDisplay();
|
||||
if (!display?.okToDrawCurrentConditions) {
|
||||
stop(display?.elemId === 'progress');
|
||||
return;
|
||||
}
|
||||
screenIndex = (screenIndex + 1) % (screens.length);
|
||||
// draw new text
|
||||
drawScreen();
|
||||
|
@ -37,7 +43,7 @@ const incrementInterval = () => {
|
|||
|
||||
const drawScreen = async () => {
|
||||
// get the conditions
|
||||
const data = await navigation.getCurrentWeather();
|
||||
const data = await getCurrentWeather();
|
||||
|
||||
// nothing to do if there's no data yet
|
||||
if (!data) return;
|
||||
|
@ -93,8 +99,4 @@ const drawCondition = (text) => {
|
|||
});
|
||||
};
|
||||
|
||||
// return the api
|
||||
export {
|
||||
start,
|
||||
stop,
|
||||
};
|
||||
start();
|
||||
|
|
|
@ -2,15 +2,13 @@
|
|||
// technically uses the same data as the local forecast, we'll let the browser do the caching of that
|
||||
|
||||
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 { UNITS, getUnits, convert } from './utils/units.mjs';
|
||||
import { getWeatherIconFromIconLink } from './icons.mjs';
|
||||
import { preloadImg } from './utils/image.mjs';
|
||||
import WeatherDisplay from './weatherdisplay.mjs';
|
||||
|
||||
/* globals navigation */
|
||||
import { registerDisplay } from './navigation.mjs';
|
||||
|
||||
class ExtendedForecast extends WeatherDisplay {
|
||||
constructor(navId, elemId) {
|
||||
|
@ -26,7 +24,7 @@ class ExtendedForecast extends WeatherDisplay {
|
|||
|
||||
// request us or si units
|
||||
let units = 'us';
|
||||
if (navigation.units() === UNITS.metric) units = 'si';
|
||||
if (getUnits() === UNITS.metric) units = 'si';
|
||||
let forecast;
|
||||
try {
|
||||
forecast = await json(weatherParameters.forecast, {
|
||||
|
@ -144,11 +142,11 @@ class ExtendedForecast extends WeatherDisplay {
|
|||
|
||||
let { low } = Day;
|
||||
if (low !== undefined) {
|
||||
if (navigation.units() === UNITS.metric) low = fahrenheitToCelsius(low);
|
||||
if (getUnits() === UNITS.metric) low = convert.fahrenheitToCelsius(low);
|
||||
fill['value-lo'] = Math.round(low);
|
||||
}
|
||||
let { high } = Day;
|
||||
if (navigation.units() === UNITS.metric) high = fahrenheitToCelsius(high);
|
||||
if (getUnits() === UNITS.metric) high = convert.fahrenheitToCelsius(high);
|
||||
fill['value-hi'] = Math.round(high);
|
||||
fill.condition = Day.text;
|
||||
|
||||
|
@ -167,4 +165,5 @@ class ExtendedForecast extends WeatherDisplay {
|
|||
}
|
||||
}
|
||||
|
||||
export default ExtendedForecast;
|
||||
// register display
|
||||
registerDisplay(new ExtendedForecast(6, 'extended-forecast'));
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
// hourly forecast list
|
||||
/* globals 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 { convert, UNITS, getUnits } 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 getSun from './almanac.mjs';
|
||||
|
||||
class Hourly extends WeatherDisplay {
|
||||
constructor(navId, elemId, defaultActive) {
|
||||
|
@ -62,7 +62,7 @@ class Hourly extends WeatherDisplay {
|
|||
const icons = await Hourly.determineIcon(skyCover, weather, iceAccumulation, probabilityOfPrecipitation, snowfallAmount, windSpeed);
|
||||
|
||||
return temperature.map((val, idx) => {
|
||||
if (navigation.units === UNITS.metric) {
|
||||
if (getUnits() === UNITS.metric) {
|
||||
return {
|
||||
temperature: temperature[idx],
|
||||
apparentTemperature: apparentTemperature[idx],
|
||||
|
@ -73,9 +73,9 @@ class Hourly extends WeatherDisplay {
|
|||
}
|
||||
|
||||
return {
|
||||
temperature: units.celsiusToFahrenheit(temperature[idx]),
|
||||
apparentTemperature: units.celsiusToFahrenheit(apparentTemperature[idx]),
|
||||
windSpeed: units.kilometersToMiles(windSpeed[idx]),
|
||||
temperature: convert.celsiusToFahrenheit(temperature[idx]),
|
||||
apparentTemperature: convert.celsiusToFahrenheit(apparentTemperature[idx]),
|
||||
windSpeed: convert.kilometersToMiles(windSpeed[idx]),
|
||||
windDirection: directionToNSEW(windDirection[idx]),
|
||||
icon: icons[idx],
|
||||
};
|
||||
|
@ -85,7 +85,7 @@ class Hourly extends WeatherDisplay {
|
|||
// given forecast paramaters determine a suitable icon
|
||||
static async determineIcon(skyCover, weather, iceAccumulation, probabilityOfPrecipitation, snowfallAmount, windSpeed) {
|
||||
const startOfHour = DateTime.local().startOf('hour');
|
||||
const sunTimes = (await navigation.getSun()).sun;
|
||||
const sunTimes = (await getSun()).sun;
|
||||
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) => {
|
||||
|
@ -198,4 +198,5 @@ class Hourly extends WeatherDisplay {
|
|||
}
|
||||
}
|
||||
|
||||
export default Hourly;
|
||||
// register display
|
||||
registerDisplay(new Hourly(2, 'hourly'));
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
// current weather conditions display
|
||||
/* globals 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';
|
||||
import { convert, UNITS, getUnits } from './utils/units.mjs';
|
||||
import WeatherDisplay from './weatherdisplay.mjs';
|
||||
import { registerDisplay } from './navigation.mjs';
|
||||
|
||||
class LatestObservations extends WeatherDisplay {
|
||||
constructor(navId, elemId) {
|
||||
|
@ -71,7 +70,7 @@ class LatestObservations extends WeatherDisplay {
|
|||
// sort array by station name
|
||||
const sortedConditions = conditions.sort((a, b) => ((a.Name < b.Name) ? -1 : 1));
|
||||
|
||||
if (navigation.units() === UNITS.english) {
|
||||
if (getUnits() === UNITS.english) {
|
||||
this.elem.querySelector('.column-headers .temp.english').classList.add('show');
|
||||
this.elem.querySelector('.column-headers .temp.metric').classList.remove('show');
|
||||
} else {
|
||||
|
@ -84,9 +83,9 @@ class LatestObservations extends WeatherDisplay {
|
|||
let WindSpeed = condition.windSpeed.value;
|
||||
const windDirection = directionToNSEW(condition.windDirection.value);
|
||||
|
||||
if (navigation.units() === UNITS.english) {
|
||||
Temperature = units.celsiusToFahrenheit(Temperature);
|
||||
WindSpeed = units.kphToMph(WindSpeed);
|
||||
if (getUnits() === UNITS.english) {
|
||||
Temperature = convert.celsiusToFahrenheit(Temperature);
|
||||
WindSpeed = convert.kphToMph(WindSpeed);
|
||||
}
|
||||
WindSpeed = Math.round(WindSpeed);
|
||||
Temperature = Math.round(Temperature);
|
||||
|
@ -132,3 +131,5 @@ class LatestObservations extends WeatherDisplay {
|
|||
return condition;
|
||||
}
|
||||
}
|
||||
// register display
|
||||
registerDisplay(new LatestObservations(1, 'latest-observations'));
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
// display text based local forecast
|
||||
|
||||
/* globals navigation */
|
||||
import STATUS from './status.mjs';
|
||||
import { UNITS } from './config.mjs';
|
||||
import { UNITS, getUnits } from './utils/units.mjs';
|
||||
import { json } from './utils/fetch.mjs';
|
||||
import WeatherDisplay from './weatherdisplay.mjs';
|
||||
import { registerDisplay } from './navigation.mjs';
|
||||
|
||||
class LocalForecast extends WeatherDisplay {
|
||||
constructor(navId, elemId) {
|
||||
|
@ -33,7 +33,7 @@ class LocalForecast extends WeatherDisplay {
|
|||
// process the text
|
||||
let text = `${condition.DayName.toUpperCase()}...`;
|
||||
let conditionText = condition.Text;
|
||||
if (navigation.units() === UNITS.metric) {
|
||||
if (getUnits() === UNITS.metric) {
|
||||
conditionText = condition.TextC;
|
||||
}
|
||||
text += conditionText.toUpperCase().replace('...', ' ');
|
||||
|
@ -63,7 +63,7 @@ class LocalForecast extends WeatherDisplay {
|
|||
async getRawData(weatherParameters) {
|
||||
// request us or si units
|
||||
let units = 'us';
|
||||
if (navigation.units() === UNITS.metric) units = 'si';
|
||||
if (getUnits() === UNITS.metric) units = 'si';
|
||||
try {
|
||||
return await json(weatherParameters.forecast, {
|
||||
data: {
|
||||
|
@ -97,3 +97,6 @@ class LocalForecast extends WeatherDisplay {
|
|||
}));
|
||||
}
|
||||
}
|
||||
|
||||
// register display
|
||||
registerDisplay(new LocalForecast(5, 'local-forecast'));
|
||||
|
|
|
@ -1,307 +0,0 @@
|
|||
// navigation handles progress, next/previous and initial load messages from the parent frame
|
||||
/* globals index, utils, StationInfo, STATUS, UNITS */
|
||||
/* globals CurrentWeather, LatestObservations, TravelForecast, RegionalForecast, LocalForecast, ExtendedForecast, Almanac, Radar, Progress, Hourly */
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
navigation.init();
|
||||
});
|
||||
|
||||
const navigation = (() => {
|
||||
let displays = [];
|
||||
let currentUnits;
|
||||
let playing = false;
|
||||
let progress;
|
||||
const weatherParameters = {};
|
||||
|
||||
// current conditions and sunrise/sunset are made available from the display below
|
||||
let currentWeather;
|
||||
let almanac;
|
||||
|
||||
const init = async () => {
|
||||
// set up resize handler
|
||||
window.addEventListener('resize', resize);
|
||||
currentUnits = UNITS.english;
|
||||
};
|
||||
|
||||
const message = (data) => {
|
||||
// dispatch event
|
||||
if (!data.type) return;
|
||||
switch (data.type) {
|
||||
case 'latLon':
|
||||
getWeather(data.message);
|
||||
break;
|
||||
|
||||
case 'units':
|
||||
setUnits(data.message);
|
||||
break;
|
||||
|
||||
case 'navButton':
|
||||
handleNavButton(data.message);
|
||||
break;
|
||||
|
||||
default:
|
||||
console.error(`Unknown event ${data.type}`);
|
||||
}
|
||||
};
|
||||
|
||||
const postMessage = (type, myMessage = {}) => {
|
||||
index.message({ type, message: myMessage });
|
||||
};
|
||||
|
||||
const getWeather = async (latLon) => {
|
||||
// get initial weather data
|
||||
const point = await utils.weather.getPoint(latLon.lat, latLon.lon);
|
||||
|
||||
// get stations
|
||||
const stations = await utils.fetch.json(point.properties.observationStations);
|
||||
|
||||
const StationId = stations.features[0].properties.stationIdentifier;
|
||||
|
||||
let { city } = point.properties.relativeLocation.properties;
|
||||
|
||||
if (StationId in StationInfo) {
|
||||
city = StationInfo[StationId].city;
|
||||
[city] = city.split('/');
|
||||
}
|
||||
|
||||
// populate the weather parameters
|
||||
weatherParameters.latitude = latLon.lat;
|
||||
weatherParameters.longitude = latLon.lon;
|
||||
weatherParameters.zoneId = point.properties.forecastZone.substr(-6);
|
||||
weatherParameters.radarId = point.properties.radarStation.substr(-3);
|
||||
weatherParameters.stationId = StationId;
|
||||
weatherParameters.weatherOffice = point.properties.cwa;
|
||||
weatherParameters.city = city;
|
||||
weatherParameters.state = point.properties.relativeLocation.properties.state;
|
||||
weatherParameters.timeZone = point.properties.relativeLocation.properties.timeZone;
|
||||
weatherParameters.forecast = point.properties.forecast;
|
||||
weatherParameters.forecastGridData = point.properties.forecastGridData;
|
||||
weatherParameters.stations = stations.features;
|
||||
|
||||
// update the main process for display purposes
|
||||
postMessage('weatherParameters', weatherParameters);
|
||||
|
||||
// draw the progress canvas and hide others
|
||||
hideAllCanvases();
|
||||
document.getElementById('loading').style.display = 'none';
|
||||
if (!progress) progress = new Progress(-1, 'progress');
|
||||
await progress.drawCanvas();
|
||||
progress.showCanvas();
|
||||
|
||||
// start loading canvases if necessary
|
||||
if (displays.length === 0) {
|
||||
currentWeather = new CurrentWeather(0, 'current-weather');
|
||||
almanac = new Almanac(7, 'almanac');
|
||||
displays = [
|
||||
currentWeather,
|
||||
new LatestObservations(1, 'latest-observations'),
|
||||
new Hourly(2, 'hourly'),
|
||||
new TravelForecast(3, 'travel', false), // not active by default
|
||||
new RegionalForecast(4, 'regional-forecast'),
|
||||
new LocalForecast(5, 'local-forecast'),
|
||||
new ExtendedForecast(6, 'extended-forecast'),
|
||||
almanac,
|
||||
new Radar(8, 'radar'),
|
||||
];
|
||||
}
|
||||
// call for new data on each display
|
||||
displays.forEach((display) => display.getData(weatherParameters));
|
||||
};
|
||||
|
||||
// receive a status update from a module {id, value}
|
||||
const updateStatus = (value) => {
|
||||
if (value.id < 0) return;
|
||||
progress.drawCanvas(displays, countLoadedCanvases());
|
||||
|
||||
// if this is the first display and we're playing, load it up so it starts playing
|
||||
if (isPlaying() && value.id === 0 && value.status === STATUS.loaded) {
|
||||
navTo(msg.command.firstFrame);
|
||||
}
|
||||
|
||||
// send loaded messaged to parent
|
||||
if (countLoadedCanvases() < displays.length) return;
|
||||
postMessage('loaded');
|
||||
};
|
||||
|
||||
const countLoadedCanvases = () => displays.reduce((acc, display) => {
|
||||
if (display.status !== STATUS.loading) return acc + 1;
|
||||
return acc;
|
||||
}, 0);
|
||||
|
||||
const hideAllCanvases = () => {
|
||||
displays.forEach((display) => display.hideCanvas());
|
||||
};
|
||||
|
||||
const units = () => currentUnits;
|
||||
const setUnits = (_unit) => {
|
||||
const unit = _unit.toLowerCase();
|
||||
if (unit === 'english') {
|
||||
currentUnits = UNITS.english;
|
||||
} else {
|
||||
currentUnits = UNITS.metric;
|
||||
}
|
||||
// TODO: refresh current screen
|
||||
};
|
||||
|
||||
// is playing interface
|
||||
const isPlaying = () => playing;
|
||||
|
||||
// navigation message constants
|
||||
const msg = {
|
||||
response: { // display to navigation
|
||||
previous: Symbol('previous'), // already at first frame, calling function should switch to previous canvas
|
||||
inProgress: Symbol('inProgress'), // have data to display, calling function should do nothing
|
||||
next: Symbol('next'), // end of frames reached, calling function should switch to next canvas
|
||||
},
|
||||
command: { // navigation to display
|
||||
firstFrame: Symbol('firstFrame'),
|
||||
previousFrame: Symbol('previousFrame'),
|
||||
nextFrame: Symbol('nextFrame'),
|
||||
lastFrame: Symbol('lastFrame'), // used when navigating backwards from the begining of the next canvas
|
||||
},
|
||||
};
|
||||
|
||||
// receive navigation messages from displays
|
||||
const displayNavMessage = (myMessage) => {
|
||||
if (myMessage.type === msg.response.previous) loadDisplay(-1);
|
||||
if (myMessage.type === msg.response.next) loadDisplay(1);
|
||||
};
|
||||
|
||||
// navigate to next or previous
|
||||
const navTo = (direction) => {
|
||||
// test for a current display
|
||||
const current = currentDisplay();
|
||||
progress.hideCanvas();
|
||||
if (!current) {
|
||||
// special case for no active displays (typically on progress screen)
|
||||
// find the first ready display
|
||||
let firstDisplay;
|
||||
let displayCount = 0;
|
||||
do {
|
||||
if (displays[displayCount].status === STATUS.loaded) firstDisplay = displays[displayCount];
|
||||
displayCount += 1;
|
||||
} while (!firstDisplay && displayCount < displays.length);
|
||||
|
||||
firstDisplay.navNext(msg.command.firstFrame);
|
||||
return;
|
||||
}
|
||||
if (direction === msg.command.nextFrame) currentDisplay().navNext();
|
||||
if (direction === msg.command.previousFrame) currentDisplay().navPrev();
|
||||
};
|
||||
|
||||
// find the next or previous available display
|
||||
const loadDisplay = (direction) => {
|
||||
const totalDisplays = displays.length;
|
||||
const curIdx = currentDisplayIndex();
|
||||
let idx;
|
||||
for (let i = 0; i < totalDisplays; i += 1) {
|
||||
// convert form simple 0-10 to start at current display index +/-1 and wrap
|
||||
idx = utils.calc.wrap(curIdx + (i + 1) * direction, totalDisplays);
|
||||
if (displays[idx].status === STATUS.loaded) break;
|
||||
}
|
||||
// if new display index is less than current display a wrap occurred, test for reload timeout
|
||||
if (idx <= curIdx) {
|
||||
if (index.refreshCheck()) return;
|
||||
}
|
||||
const newDisplay = displays[idx];
|
||||
// hide all displays
|
||||
hideAllCanvases();
|
||||
// show the new display and navigate to an appropriate display
|
||||
if (direction < 0) newDisplay.showCanvas(msg.command.lastFrame);
|
||||
if (direction > 0) newDisplay.showCanvas(msg.command.firstFrame);
|
||||
};
|
||||
|
||||
// get the current display index or value
|
||||
const currentDisplayIndex = () => {
|
||||
const index = displays.findIndex((display) => display.isActive());
|
||||
return index;
|
||||
};
|
||||
const currentDisplay = () => displays[currentDisplayIndex()];
|
||||
|
||||
const setPlaying = (newValue) => {
|
||||
playing = newValue;
|
||||
postMessage('isPlaying', playing);
|
||||
// if we're playing and on the progress screen jump to the next screen
|
||||
if (!progress) return;
|
||||
if (playing && !currentDisplay()) navTo(msg.command.firstFrame);
|
||||
};
|
||||
|
||||
// handle all navigation buttons
|
||||
const handleNavButton = (button) => {
|
||||
switch (button) {
|
||||
case 'play':
|
||||
setPlaying(true);
|
||||
break;
|
||||
case 'playToggle':
|
||||
setPlaying(!playing);
|
||||
break;
|
||||
case 'stop':
|
||||
setPlaying(false);
|
||||
break;
|
||||
case 'next':
|
||||
setPlaying(false);
|
||||
navTo(msg.command.nextFrame);
|
||||
break;
|
||||
case 'previous':
|
||||
setPlaying(false);
|
||||
navTo(msg.command.previousFrame);
|
||||
break;
|
||||
case 'menu':
|
||||
setPlaying(false);
|
||||
progress.showCanvas();
|
||||
hideAllCanvases();
|
||||
break;
|
||||
default:
|
||||
console.error(`Unknown navButton ${button}`);
|
||||
}
|
||||
};
|
||||
|
||||
// return the specificed display
|
||||
const getDisplay = (index) => displays[index];
|
||||
|
||||
// get current conditions
|
||||
const getCurrentWeather = () => {
|
||||
if (!currentWeather) return false;
|
||||
return currentWeather.getCurrentWeather();
|
||||
};
|
||||
|
||||
// get sunrise/sunset
|
||||
const getSun = () => {
|
||||
if (!almanac) return false;
|
||||
return almanac.getSun();
|
||||
};
|
||||
|
||||
// resize the container on a page resize
|
||||
const resize = () => {
|
||||
const widthZoomPercent = window.innerWidth / 640;
|
||||
const heightZoomPercent = window.innerHeight / 480;
|
||||
|
||||
const scale = Math.min(widthZoomPercent, heightZoomPercent);
|
||||
|
||||
if (scale < 1.0 || document.fullscreenElement) {
|
||||
document.getElementById('container').style.zoom = scale;
|
||||
} else {
|
||||
document.getElementById('container').style.zoom = 1;
|
||||
}
|
||||
};
|
||||
|
||||
// reset all statuses to loading on all displays, used to keep the progress bar accurate during refresh
|
||||
const resetStatuses = () => {
|
||||
displays.forEach((display) => { display.status = STATUS.loading; });
|
||||
};
|
||||
|
||||
return {
|
||||
init,
|
||||
message,
|
||||
updateStatus,
|
||||
units,
|
||||
isPlaying,
|
||||
displayNavMessage,
|
||||
msg,
|
||||
getDisplay,
|
||||
getCurrentWeather,
|
||||
getSun,
|
||||
resize,
|
||||
resetStatuses,
|
||||
};
|
||||
})();
|
390
server/scripts/modules/navigation.mjs
Normal file
390
server/scripts/modules/navigation.mjs
Normal file
|
@ -0,0 +1,390 @@
|
|||
// navigation handles progress, next/previous and initial load messages from the parent frame
|
||||
import noSleep from './utils/nosleep.mjs';
|
||||
import STATUS from './status.mjs';
|
||||
import { wrap } from './utils/calc.mjs';
|
||||
import { json } from './utils/fetch.mjs';
|
||||
import { getPoint } from './utils/weather.mjs';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
init();
|
||||
});
|
||||
|
||||
const displays = [];
|
||||
let playing = false;
|
||||
let progress;
|
||||
const weatherParameters = {};
|
||||
|
||||
// auto refresh
|
||||
const AUTO_REFRESH_INTERVAL_MS = 500;
|
||||
const AUTO_REFRESH_TIME_MS = 600000; // 10 min.
|
||||
let AutoRefreshIntervalId = null;
|
||||
let AutoRefreshCountMs = 0;
|
||||
|
||||
const init = async () => {
|
||||
// set up resize handler
|
||||
window.addEventListener('resize', resize);
|
||||
|
||||
// auto refresh
|
||||
const TwcAutoRefresh = localStorage.getItem('TwcAutoRefresh');
|
||||
if (!TwcAutoRefresh || TwcAutoRefresh === 'true') {
|
||||
document.getElementById('chkAutoRefresh').checked = true;
|
||||
} else {
|
||||
document.getElementById('chkAutoRefresh').checked = false;
|
||||
}
|
||||
document.getElementById('chkAutoRefresh').addEventListener('change', autoRefreshChange);
|
||||
};
|
||||
|
||||
const message = (data) => {
|
||||
// dispatch event
|
||||
if (!data.type) return;
|
||||
switch (data.type) {
|
||||
case 'navButton':
|
||||
handleNavButton(data.message);
|
||||
break;
|
||||
|
||||
default:
|
||||
console.error(`Unknown event ${data.type}`);
|
||||
}
|
||||
};
|
||||
|
||||
const getWeather = async (latLon) => {
|
||||
// get initial weather data
|
||||
const point = await getPoint(latLon.lat, latLon.lon);
|
||||
|
||||
// get stations
|
||||
const stations = await json(point.properties.observationStations);
|
||||
|
||||
const StationId = stations.features[0].properties.stationIdentifier;
|
||||
|
||||
let { city } = point.properties.relativeLocation.properties;
|
||||
|
||||
if (StationId in StationInfo) {
|
||||
city = StationInfo[StationId].city;
|
||||
[city] = city.split('/');
|
||||
}
|
||||
|
||||
// populate the weather parameters
|
||||
weatherParameters.latitude = latLon.lat;
|
||||
weatherParameters.longitude = latLon.lon;
|
||||
weatherParameters.zoneId = point.properties.forecastZone.substr(-6);
|
||||
weatherParameters.radarId = point.properties.radarStation.substr(-3);
|
||||
weatherParameters.stationId = StationId;
|
||||
weatherParameters.weatherOffice = point.properties.cwa;
|
||||
weatherParameters.city = city;
|
||||
weatherParameters.state = point.properties.relativeLocation.properties.state;
|
||||
weatherParameters.timeZone = point.properties.relativeLocation.properties.timeZone;
|
||||
weatherParameters.forecast = point.properties.forecast;
|
||||
weatherParameters.forecastGridData = point.properties.forecastGridData;
|
||||
weatherParameters.stations = stations.features;
|
||||
|
||||
// update the main process for display purposes
|
||||
populateWeatherParameters(weatherParameters);
|
||||
|
||||
// draw the progress canvas and hide others
|
||||
hideAllCanvases();
|
||||
document.getElementById('loading').style.display = 'none';
|
||||
if (progress) {
|
||||
await progress.drawCanvas();
|
||||
progress.showCanvas();
|
||||
}
|
||||
|
||||
// call for new data on each display
|
||||
displays.forEach((display) => display.getData(weatherParameters));
|
||||
};
|
||||
|
||||
// receive a status update from a module {id, value}
|
||||
const updateStatus = (value) => {
|
||||
if (value.id < 0) return;
|
||||
if (!progress) return;
|
||||
progress.drawCanvas(displays, countLoadedCanvases());
|
||||
|
||||
// if this is the first display and we're playing, load it up so it starts playing
|
||||
if (isPlaying() && value.id === 0 && value.status === STATUS.loaded) {
|
||||
navTo(msg.command.firstFrame);
|
||||
}
|
||||
|
||||
// send loaded messaged to parent
|
||||
if (countLoadedCanvases() < displays.length) return;
|
||||
|
||||
// everything loaded, set timestamps
|
||||
AssignLastUpdate(new Date());
|
||||
};
|
||||
|
||||
const countLoadedCanvases = () => displays.reduce((acc, display) => {
|
||||
if (display.status !== STATUS.loading) return acc + 1;
|
||||
return acc;
|
||||
}, 0);
|
||||
|
||||
const hideAllCanvases = () => {
|
||||
displays.forEach((display) => display.hideCanvas());
|
||||
};
|
||||
|
||||
// is playing interface
|
||||
const isPlaying = () => playing;
|
||||
|
||||
// navigation message constants
|
||||
const msg = {
|
||||
response: { // display to navigation
|
||||
previous: Symbol('previous'), // already at first frame, calling function should switch to previous canvas
|
||||
inProgress: Symbol('inProgress'), // have data to display, calling function should do nothing
|
||||
next: Symbol('next'), // end of frames reached, calling function should switch to next canvas
|
||||
},
|
||||
command: { // navigation to display
|
||||
firstFrame: Symbol('firstFrame'),
|
||||
previousFrame: Symbol('previousFrame'),
|
||||
nextFrame: Symbol('nextFrame'),
|
||||
lastFrame: Symbol('lastFrame'), // used when navigating backwards from the begining of the next canvas
|
||||
},
|
||||
};
|
||||
|
||||
// receive navigation messages from displays
|
||||
const displayNavMessage = (myMessage) => {
|
||||
if (myMessage.type === msg.response.previous) loadDisplay(-1);
|
||||
if (myMessage.type === msg.response.next) loadDisplay(1);
|
||||
};
|
||||
|
||||
// navigate to next or previous
|
||||
const navTo = (direction) => {
|
||||
// test for a current display
|
||||
const current = currentDisplay();
|
||||
progress.hideCanvas();
|
||||
if (!current) {
|
||||
// special case for no active displays (typically on progress screen)
|
||||
// find the first ready display
|
||||
let firstDisplay;
|
||||
let displayCount = 0;
|
||||
do {
|
||||
if (displays[displayCount].status === STATUS.loaded) firstDisplay = displays[displayCount];
|
||||
displayCount += 1;
|
||||
} while (!firstDisplay && displayCount < displays.length);
|
||||
|
||||
if (!firstDisplay) return;
|
||||
|
||||
firstDisplay.navNext(msg.command.firstFrame);
|
||||
return;
|
||||
}
|
||||
if (direction === msg.command.nextFrame) currentDisplay().navNext();
|
||||
if (direction === msg.command.previousFrame) currentDisplay().navPrev();
|
||||
};
|
||||
|
||||
// find the next or previous available display
|
||||
const loadDisplay = (direction) => {
|
||||
const totalDisplays = displays.length;
|
||||
const curIdx = currentDisplayIndex();
|
||||
let idx;
|
||||
for (let i = 0; i < totalDisplays; i += 1) {
|
||||
// convert form simple 0-10 to start at current display index +/-1 and wrap
|
||||
idx = wrap(curIdx + (i + 1) * direction, totalDisplays);
|
||||
if (displays[idx].status === STATUS.loaded) break;
|
||||
}
|
||||
// if new display index is less than current display a wrap occurred, test for reload timeout
|
||||
if (idx <= curIdx) {
|
||||
if (refreshCheck()) return;
|
||||
}
|
||||
const newDisplay = displays[idx];
|
||||
// hide all displays
|
||||
hideAllCanvases();
|
||||
// show the new display and navigate to an appropriate display
|
||||
if (direction < 0) newDisplay.showCanvas(msg.command.lastFrame);
|
||||
if (direction > 0) newDisplay.showCanvas(msg.command.firstFrame);
|
||||
};
|
||||
|
||||
// get the current display index or value
|
||||
const currentDisplayIndex = () => {
|
||||
const index = displays.findIndex((display) => display.isActive());
|
||||
return index;
|
||||
};
|
||||
const currentDisplay = () => displays[currentDisplayIndex()];
|
||||
|
||||
const setPlaying = (newValue) => {
|
||||
playing = newValue;
|
||||
const playButton = document.getElementById('NavigatePlay');
|
||||
localStorage.setItem('TwcPlay', playing);
|
||||
|
||||
if (playing) {
|
||||
noSleep(true);
|
||||
playButton.title = 'Pause';
|
||||
playButton.src = 'images/nav/ic_pause_white_24dp_1x.png';
|
||||
} else {
|
||||
noSleep(false);
|
||||
playButton.title = 'Play';
|
||||
playButton.src = 'images/nav/ic_play_arrow_white_24dp_1x.png';
|
||||
}
|
||||
// if we're playing and on the progress screen jump to the next screen
|
||||
if (!progress) return;
|
||||
if (playing && !currentDisplay()) navTo(msg.command.firstFrame);
|
||||
};
|
||||
|
||||
// handle all navigation buttons
|
||||
const handleNavButton = (button) => {
|
||||
switch (button) {
|
||||
case 'play':
|
||||
setPlaying(true);
|
||||
break;
|
||||
case 'playToggle':
|
||||
setPlaying(!playing);
|
||||
break;
|
||||
case 'stop':
|
||||
setPlaying(false);
|
||||
break;
|
||||
case 'next':
|
||||
setPlaying(false);
|
||||
navTo(msg.command.nextFrame);
|
||||
break;
|
||||
case 'previous':
|
||||
setPlaying(false);
|
||||
navTo(msg.command.previousFrame);
|
||||
break;
|
||||
case 'menu':
|
||||
setPlaying(false);
|
||||
progress.showCanvas();
|
||||
hideAllCanvases();
|
||||
break;
|
||||
default:
|
||||
console.error(`Unknown navButton ${button}`);
|
||||
}
|
||||
};
|
||||
|
||||
// return the specificed display
|
||||
const getDisplay = (index) => displays[index];
|
||||
|
||||
// resize the container on a page resize
|
||||
const resize = () => {
|
||||
const widthZoomPercent = window.innerWidth / 640;
|
||||
const heightZoomPercent = window.innerHeight / 480;
|
||||
|
||||
const scale = Math.min(widthZoomPercent, heightZoomPercent);
|
||||
|
||||
if (scale < 1.0 || document.fullscreenElement) {
|
||||
document.getElementById('container').style.zoom = scale;
|
||||
} else {
|
||||
document.getElementById('container').style.zoom = 1;
|
||||
}
|
||||
};
|
||||
|
||||
// reset all statuses to loading on all displays, used to keep the progress bar accurate during refresh
|
||||
const resetStatuses = () => {
|
||||
displays.forEach((display) => { display.status = STATUS.loading; });
|
||||
};
|
||||
|
||||
// allow displays to register themselves
|
||||
const registerDisplay = (display) => {
|
||||
displays[display.navId] = display;
|
||||
|
||||
// generate checkboxes
|
||||
const checkboxes = displays.map((d) => d.generateCheckbox()).filter((d) => d);
|
||||
|
||||
// write to page
|
||||
const availableDisplays = document.getElementById('enabledDisplays');
|
||||
availableDisplays.innerHTML = '';
|
||||
availableDisplays.append(...checkboxes);
|
||||
};
|
||||
|
||||
// special registration method for progress display
|
||||
const registerProgress = (_progress) => {
|
||||
progress = _progress;
|
||||
};
|
||||
|
||||
const populateWeatherParameters = (params) => {
|
||||
document.getElementById('spanCity').innerHTML = `${params.city}, `;
|
||||
document.getElementById('spanState').innerHTML = params.state;
|
||||
document.getElementById('spanStationId').innerHTML = params.stationId;
|
||||
document.getElementById('spanRadarId').innerHTML = params.radarId;
|
||||
document.getElementById('spanZoneId').innerHTML = params.zoneId;
|
||||
};
|
||||
|
||||
const autoRefreshChange = (e) => {
|
||||
const { checked } = e.target;
|
||||
|
||||
if (checked) {
|
||||
startAutoRefreshTimer();
|
||||
} else {
|
||||
stopAutoRefreshTimer();
|
||||
}
|
||||
|
||||
localStorage.setItem('TwcAutoRefresh', checked);
|
||||
};
|
||||
|
||||
const AssignLastUpdate = (date) => {
|
||||
if (date) {
|
||||
document.getElementById('spanLastRefresh').innerHTML = date.toLocaleString('en-US', {
|
||||
weekday: 'short', month: 'short', day: 'numeric', year: 'numeric', hour: 'numeric', minute: 'numeric', second: 'numeric', timeZoneName: 'short',
|
||||
});
|
||||
if (document.getElementById('chkAutoRefresh').checked) startAutoRefreshTimer();
|
||||
} else {
|
||||
document.getElementById('spanLastRefresh').innerHTML = '(none)';
|
||||
}
|
||||
};
|
||||
|
||||
const latLonReceived = (data) => {
|
||||
getWeather(data);
|
||||
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.getElementById('spanRefreshCountDown').innerHTML = `${dt.getMinutes() < 10 ? `0${dt.getMinutes()}` : dt.getMinutes()}:${dt.getSeconds() < 10 ? `0${dt.getSeconds()}` : dt.getSeconds()}`;
|
||||
|
||||
// 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.getElementById('spanRefreshCountDown').innerHTML = '--:--';
|
||||
AutoRefreshIntervalId = null;
|
||||
}
|
||||
};
|
||||
|
||||
const refreshCheck = () => {
|
||||
// Time has elapsed.
|
||||
if (AutoRefreshCountMs >= AUTO_REFRESH_TIME_MS) {
|
||||
loadTwcData();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const loadTwcData = () => {
|
||||
if (loadTwcData.callback) loadTwcData.callback();
|
||||
};
|
||||
|
||||
const registerRefreshData = (callback) => {
|
||||
loadTwcData.callback = callback;
|
||||
};
|
||||
|
||||
export {
|
||||
updateStatus,
|
||||
displayNavMessage,
|
||||
resetStatuses,
|
||||
isPlaying,
|
||||
resize,
|
||||
registerDisplay,
|
||||
registerProgress,
|
||||
currentDisplay,
|
||||
getDisplay,
|
||||
msg,
|
||||
message,
|
||||
latLonReceived,
|
||||
stopAutoRefreshTimer,
|
||||
registerRefreshData,
|
||||
};
|
|
@ -1,8 +1,10 @@
|
|||
// regional forecast and observations
|
||||
/* globals navigation */
|
||||
import { loadImg } from './utils/image.mjs';
|
||||
import STATUS from './status.mjs';
|
||||
import WeatherDisplay from './weatherdisplay.mjs';
|
||||
import {
|
||||
registerProgress, message, getDisplay, msg,
|
||||
} from './navigation.mjs';
|
||||
|
||||
class Progress extends WeatherDisplay {
|
||||
constructor(navId, elemId) {
|
||||
|
@ -18,6 +20,8 @@ class Progress extends WeatherDisplay {
|
|||
|
||||
// setup event listener
|
||||
this.elem.querySelector('.container').addEventListener('click', this.lineClick.bind(this));
|
||||
|
||||
this.okToDrawCurrentConditions = false;
|
||||
}
|
||||
|
||||
async drawCanvas(displays, loadedCount) {
|
||||
|
@ -93,16 +97,16 @@ class Progress extends WeatherDisplay {
|
|||
const index = +indexRaw;
|
||||
|
||||
// stop playing
|
||||
navigation.message('navButton');
|
||||
message('navButton');
|
||||
// use the y value to determine an index
|
||||
const display = navigation.getDisplay(index);
|
||||
const display = getDisplay(index);
|
||||
if (display && display.status === STATUS.loaded) {
|
||||
display.showCanvas(navigation.msg.command.firstFrame);
|
||||
display.showCanvas(msg.command.firstFrame);
|
||||
this.elem.classList.remove('show');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default Progress;
|
||||
|
||||
window.Progress = Progress;
|
||||
// register our own display
|
||||
const progress = new Progress(-1, 'progress');
|
||||
registerProgress(progress);
|
||||
|
|
|
@ -5,11 +5,15 @@ import { loadImg } from './utils/image.mjs';
|
|||
import { text } from './utils/fetch.mjs';
|
||||
import { rewriteUrl } from './utils/cors.mjs';
|
||||
import WeatherDisplay from './weatherdisplay.mjs';
|
||||
import { registerDisplay } from './navigation.mjs';
|
||||
|
||||
class Radar extends WeatherDisplay {
|
||||
constructor(navId, elemId) {
|
||||
super(navId, elemId, 'Local Radar', true);
|
||||
|
||||
this.okToDrawCurrentConditions = false;
|
||||
this.okToDrawCurrentDateTime = false;
|
||||
|
||||
// set max images
|
||||
this.dopplerRadarImageMax = 6;
|
||||
// update timing
|
||||
|
@ -397,4 +401,5 @@ class Radar extends WeatherDisplay {
|
|||
}
|
||||
}
|
||||
|
||||
export default Radar;
|
||||
// register display
|
||||
registerDisplay(new Radar(8, 'radar'));
|
||||
|
|
|
@ -1,16 +1,15 @@
|
|||
// regional forecast and observations
|
||||
// type 0 = observations, 1 = first forecast, 2 = second forecast
|
||||
|
||||
/* globals 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 { convert, UNITS, getUnits } from './utils/units.mjs';
|
||||
import { getWeatherRegionalIconFromIconLink } from './icons.mjs';
|
||||
import { preloadImg } from './utils/image.mjs';
|
||||
import { DateTime } from '../vendor/auto/luxon.mjs';
|
||||
import WeatherDisplay from './weatherdisplay.mjs';
|
||||
import { registerDisplay } from './navigation.mjs';
|
||||
|
||||
class RegionalForecast extends WeatherDisplay {
|
||||
constructor(navId, elemId) {
|
||||
|
@ -88,7 +87,7 @@ class RegionalForecast extends WeatherDisplay {
|
|||
// format the observation the same as the forecast
|
||||
const regionalObservation = {
|
||||
daytime: !!observation.icon.match(/\/day\//),
|
||||
temperature: units.celsiusToFahrenheit(observation.temperature.value),
|
||||
temperature: convert.celsiusToFahrenheit(observation.temperature.value),
|
||||
name: RegionalForecast.formatCity(city.city),
|
||||
icon: observation.icon,
|
||||
x: cityXY.x,
|
||||
|
@ -372,7 +371,7 @@ class RegionalForecast extends WeatherDisplay {
|
|||
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(units.fahrenheitToCelsius(temperature));
|
||||
if (getUnits() === UNITS.metric) temperature = Math.round(convert.fahrenheitToCelsius(temperature));
|
||||
fill.temp = temperature;
|
||||
|
||||
const elem = this.fillTemplate('location', fill);
|
||||
|
@ -390,4 +389,5 @@ class RegionalForecast extends WeatherDisplay {
|
|||
}
|
||||
}
|
||||
|
||||
export default RegionalForecast;
|
||||
// register display
|
||||
registerDisplay(new RegionalForecast(4, 'regional-forecast'));
|
||||
|
|
|
@ -7,4 +7,3 @@ const STATUS = {
|
|||
};
|
||||
|
||||
export default STATUS;
|
||||
window.STATUS = STATUS;
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
// travel forecast display
|
||||
/* globals 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 { convert, UNITS, getUnits } from './utils/units.mjs';
|
||||
import { DateTime } from '../vendor/auto/luxon.mjs';
|
||||
import WeatherDisplay from './weatherdisplay.mjs';
|
||||
import { registerDisplay } from './navigation.mjs';
|
||||
|
||||
class TravelForecast extends WeatherDisplay {
|
||||
constructor(navId, elemId, defaultActive) {
|
||||
|
@ -90,9 +89,9 @@ class TravelForecast extends WeatherDisplay {
|
|||
// get temperatures and convert if necessary
|
||||
let { low, high } = city;
|
||||
|
||||
if (navigation.units() === UNITS.metric) {
|
||||
low = fahrenheitToCelsius(low);
|
||||
high = fahrenheitToCelsius(high);
|
||||
if (getUnits() === UNITS.metric) {
|
||||
low = convert.fahrenheitToCelsius(low);
|
||||
high = convert.fahrenheitToCelsius(high);
|
||||
}
|
||||
|
||||
// convert to strings with no decimal
|
||||
|
@ -166,4 +165,5 @@ class TravelForecast extends WeatherDisplay {
|
|||
}
|
||||
}
|
||||
|
||||
export default TravelForecast;
|
||||
// register display, not active by default
|
||||
registerDisplay(new TravelForecast(3, 'travel', false));
|
||||
|
|
|
@ -1,236 +0,0 @@
|
|||
// radar utilities
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const utils = (() => {
|
||||
// ****************************** weather data ********************************
|
||||
const getPoint = async (lat, lon) => {
|
||||
try {
|
||||
return await json(`https://api.weather.gov/points/${lat},${lon}`);
|
||||
} catch (e) {
|
||||
console.log(`Unable to get point ${lat}, ${lon}`);
|
||||
console.error(e);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// ****************************** 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 preload = (src) => {
|
||||
if (cachedImages.includes(src)) return false;
|
||||
blob(src);
|
||||
// cachedImages.push(src);
|
||||
return true;
|
||||
};
|
||||
|
||||
// *********************************** unit conversions ***********************
|
||||
|
||||
Math.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) => Math.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) => Math.round2(Inches * 2.54, 2);
|
||||
const pascalToInHg = (Pascal) => Math.round2(Pascal * 0.0002953, 2);
|
||||
|
||||
// ***************************** calculations **********************************
|
||||
|
||||
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;
|
||||
|
||||
// ********************************* strings *********************************************
|
||||
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);
|
||||
};
|
||||
|
||||
// ********************************* cors ********************************************
|
||||
// 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;
|
||||
};
|
||||
|
||||
// ********************************* fetch ********************************************
|
||||
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;
|
||||
}
|
||||
};
|
||||
|
||||
const elemForEach = (selector, callback) => {
|
||||
[...document.querySelectorAll(selector)].forEach(callback);
|
||||
};
|
||||
|
||||
// return an orderly object
|
||||
return {
|
||||
elem: {
|
||||
forEach: elemForEach,
|
||||
},
|
||||
image: {
|
||||
load: loadImg,
|
||||
preload,
|
||||
},
|
||||
weather: {
|
||||
getPoint,
|
||||
},
|
||||
units: {
|
||||
mphToKph,
|
||||
kphToMph,
|
||||
celsiusToFahrenheit,
|
||||
fahrenheitToCelsius,
|
||||
milesToKilometers,
|
||||
kilometersToMiles,
|
||||
feetToMeters,
|
||||
metersToFeet,
|
||||
inchesToCentimeters,
|
||||
pascalToInHg,
|
||||
},
|
||||
calc: {
|
||||
relativeHumidity,
|
||||
heatIndex,
|
||||
windChill,
|
||||
directionToNSEW,
|
||||
distance,
|
||||
wrap,
|
||||
},
|
||||
string: {
|
||||
locationCleanup,
|
||||
},
|
||||
cors: {
|
||||
rewriteUrl,
|
||||
},
|
||||
fetch: {
|
||||
json,
|
||||
text,
|
||||
raw,
|
||||
blob,
|
||||
},
|
||||
};
|
||||
})();
|
18
server/scripts/modules/utils/nosleep.mjs
Normal file
18
server/scripts/modules/utils/nosleep.mjs
Normal file
|
@ -0,0 +1,18 @@
|
|||
// track state of nosleep locally to avoid a null case error
|
||||
// when nosleep.disable is called without first calling .enable
|
||||
|
||||
let wakeLock = false;
|
||||
|
||||
const noSleep = (enable = false) => {
|
||||
// get a nosleep controller
|
||||
if (!noSleep.controller) noSleep.controller = new NoSleep();
|
||||
// don't call anything if the states match
|
||||
if (wakeLock === enable) return false;
|
||||
// store the value
|
||||
wakeLock = enable;
|
||||
// call the function
|
||||
if (enable) return noSleep.controller.enable();
|
||||
return noSleep.controller.disable();
|
||||
};
|
||||
|
||||
export default noSleep;
|
|
@ -1,3 +1,23 @@
|
|||
const UNITS = {
|
||||
english: Symbol('english'),
|
||||
metric: Symbol('metric'),
|
||||
};
|
||||
|
||||
let currentUnits = UNITS.english;
|
||||
|
||||
const getUnits = () => currentUnits;
|
||||
const setUnits = (_unit) => {
|
||||
const unit = _unit.toLowerCase();
|
||||
if (unit === 'english') {
|
||||
currentUnits = UNITS.english;
|
||||
} else {
|
||||
currentUnits = UNITS.metric;
|
||||
}
|
||||
// TODO: refresh current screen
|
||||
};
|
||||
|
||||
// *********************************** unit conversions ***********************
|
||||
|
||||
const round2 = (value, decimals) => Number(`${Math.round(`${value}e${decimals}`)}e-${decimals}`);
|
||||
|
||||
const mphToKph = (Mph) => Math.round(Mph * 1.60934);
|
||||
|
@ -11,7 +31,7 @@ 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 {
|
||||
const convert = {
|
||||
mphToKph,
|
||||
kphToMph,
|
||||
celsiusToFahrenheit,
|
||||
|
@ -23,3 +43,12 @@ export {
|
|||
inchesToCentimeters,
|
||||
pascalToInHg,
|
||||
};
|
||||
|
||||
export {
|
||||
getUnits,
|
||||
setUnits,
|
||||
UNITS,
|
||||
convert,
|
||||
};
|
||||
|
||||
export default getUnits;
|
||||
|
|
16
server/scripts/modules/utils/weather.mjs
Normal file
16
server/scripts/modules/utils/weather.mjs
Normal file
|
@ -0,0 +1,16 @@
|
|||
import { json } from './fetch.mjs';
|
||||
|
||||
const getPoint = async (lat, lon) => {
|
||||
try {
|
||||
return await json(`https://api.weather.gov/points/${lat},${lon}`);
|
||||
} catch (e) {
|
||||
console.log(`Unable to get point ${lat}, ${lon}`);
|
||||
console.error(e);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export {
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
getPoint,
|
||||
};
|
|
@ -1,14 +1,15 @@
|
|||
// base weather display class
|
||||
|
||||
/* globals navigation */
|
||||
import STATUS from './status.mjs';
|
||||
import * as currentWeatherScroll from './currentweatherscroll.mjs';
|
||||
import { DateTime } from '../vendor/auto/luxon.mjs';
|
||||
import { elemForEach } from './utils/elem.mjs';
|
||||
import {
|
||||
msg, displayNavMessage, isPlaying, updateStatus,
|
||||
} from './navigation.mjs';
|
||||
|
||||
class WeatherDisplay {
|
||||
constructor(navId, elemId, name, defaultEnabled) {
|
||||
// navId is used in messaging
|
||||
// navId is used in messaging and sort order
|
||||
this.navId = navId;
|
||||
this.elemId = undefined;
|
||||
this.gifs = [];
|
||||
|
@ -16,6 +17,9 @@ class WeatherDisplay {
|
|||
this.loadingStatus = STATUS.loading;
|
||||
this.name = name ?? elemId;
|
||||
this.getDataCallbacks = [];
|
||||
this.defaultEnabled = defaultEnabled;
|
||||
this.okToDrawCurrentConditions = true;
|
||||
this.okToDrawCurrentDateTime = true;
|
||||
|
||||
// default navigation timing
|
||||
this.timing = {
|
||||
|
@ -29,7 +33,6 @@ class WeatherDisplay {
|
|||
// store elemId once
|
||||
this.storeElemId(elemId);
|
||||
|
||||
if (elemId !== 'progress') this.addCheckbox(defaultEnabled);
|
||||
if (this.enabled) {
|
||||
this.setStatus(STATUS.loading);
|
||||
} else {
|
||||
|
@ -41,7 +44,10 @@ class WeatherDisplay {
|
|||
this.loadTemplates();
|
||||
}
|
||||
|
||||
addCheckbox(defaultEnabled = true) {
|
||||
generateCheckbox(defaultEnabled = true) {
|
||||
// no checkbox if progress
|
||||
if (this.elemId === 'progress') return false;
|
||||
|
||||
// get the saved status of the checkbox
|
||||
let savedStatus = window.localStorage.getItem(`${this.elemId}Enabled`);
|
||||
if (savedStatus === null) savedStatus = defaultEnabled;
|
||||
|
@ -60,8 +66,8 @@ class WeatherDisplay {
|
|||
<input type="checkbox" value="true" id="${this.elemId}Enabled" name="${this.elemId}Enabled"${this.enabled ? ' checked' : ''}/>
|
||||
${this.name}</label>`;
|
||||
checkbox.content.firstChild.addEventListener('change', (e) => this.checkboxChange(e));
|
||||
const availableDisplays = document.getElementById('enabledDisplays');
|
||||
availableDisplays.appendChild(checkbox.content.firstChild);
|
||||
|
||||
return checkbox.content.firstChild;
|
||||
}
|
||||
|
||||
checkboxChange(e) {
|
||||
|
@ -76,7 +82,7 @@ class WeatherDisplay {
|
|||
// set data status and send update to navigation module
|
||||
setStatus(value) {
|
||||
this.status = value;
|
||||
navigation.updateStatus({
|
||||
updateStatus({
|
||||
id: this.navId,
|
||||
status: this.status,
|
||||
});
|
||||
|
@ -131,39 +137,14 @@ class WeatherDisplay {
|
|||
}
|
||||
|
||||
finishDraw() {
|
||||
let OkToDrawCurrentConditions = true;
|
||||
let OkToDrawCurrentDateTime = true;
|
||||
// let OkToDrawCustomScrollText = false;
|
||||
let bottom;
|
||||
|
||||
// visibility tests
|
||||
// if (_ScrollText !== '') OkToDrawCustomScrollText = true;
|
||||
if (this.elemId === 'progress') {
|
||||
OkToDrawCurrentConditions = false;
|
||||
}
|
||||
if (this.elemId === 'radar') {
|
||||
OkToDrawCurrentConditions = false;
|
||||
OkToDrawCurrentDateTime = false;
|
||||
}
|
||||
if (this.elemId === 'hazards') {
|
||||
bottom = true;
|
||||
}
|
||||
// draw functions
|
||||
if (OkToDrawCurrentDateTime) {
|
||||
this.drawCurrentDateTime(bottom);
|
||||
// draw date and time
|
||||
if (this.okToDrawCurrentDateTime) {
|
||||
this.drawCurrentDateTime();
|
||||
// auto clock refresh
|
||||
if (!this.dateTimeInterval) {
|
||||
setInterval(() => this.drawCurrentDateTime(bottom), 100);
|
||||
setInterval(() => this.drawCurrentDateTime(), 100);
|
||||
}
|
||||
}
|
||||
if (OkToDrawCurrentConditions) {
|
||||
currentWeatherScroll.start();
|
||||
} else {
|
||||
// cause a reset if the progress screen is displayed
|
||||
currentWeatherScroll.stop(this.elemId === 'progress');
|
||||
}
|
||||
// TODO: add custom scroll text
|
||||
// if (OkToDrawCustomScrollText) DrawCustomScrollText(WeatherParameters, context);
|
||||
}
|
||||
|
||||
drawCurrentDateTime() {
|
||||
|
@ -192,8 +173,8 @@ class WeatherDisplay {
|
|||
showCanvas(navCmd) {
|
||||
// reset timing if enabled
|
||||
// if a nav command is present call it to set the screen index
|
||||
if (navCmd === navigation.msg.command.firstFrame) this.navNext(navCmd);
|
||||
if (navCmd === navigation.msg.command.lastFrame) this.navPrev(navCmd);
|
||||
if (navCmd === msg.command.firstFrame) this.navNext(navCmd);
|
||||
if (navCmd === msg.command.lastFrame) this.navPrev(navCmd);
|
||||
|
||||
this.startNavCount();
|
||||
|
||||
|
@ -223,7 +204,7 @@ class WeatherDisplay {
|
|||
// if the array forms are used totalScreens is overwritten by the size of the array
|
||||
navBaseTime() {
|
||||
// see if play is active and screen is active
|
||||
if (!navigation.isPlaying() || !this.isActive()) return;
|
||||
if (!isPlaying() || !this.isActive()) return;
|
||||
// increment the base count
|
||||
this.navBaseCount += 1;
|
||||
|
||||
|
@ -241,7 +222,7 @@ class WeatherDisplay {
|
|||
// special cases for first and last frame
|
||||
// must compare with false as nextScreenIndex could be 0 which is valid
|
||||
if (nextScreenIndex === false) {
|
||||
this.sendNavDisplayMessage(navigation.msg.response.next);
|
||||
this.sendNavDisplayMessage(msg.response.next);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -306,7 +287,7 @@ class WeatherDisplay {
|
|||
// navigate to next screen
|
||||
navNext(command) {
|
||||
// check for special 'first frame' command
|
||||
if (command === navigation.msg.command.firstFrame) {
|
||||
if (command === msg.command.firstFrame) {
|
||||
this.resetNavBaseCount();
|
||||
} else {
|
||||
// set the base count to the next available frame
|
||||
|
@ -319,7 +300,7 @@ class WeatherDisplay {
|
|||
// navigate to previous screen
|
||||
navPrev(command) {
|
||||
// check for special 'last frame' command
|
||||
if (command === navigation.msg.command.lastFrame) {
|
||||
if (command === msg.command.lastFrame) {
|
||||
this.navBaseCount = this.timing.fullDelay[this.timing.totalScreens - 1] - 1;
|
||||
} else {
|
||||
// find the highest fullDelay that is less than the current base count
|
||||
|
@ -329,7 +310,7 @@ class WeatherDisplay {
|
|||
}, 0);
|
||||
// if the new base count is zero then we're already at the first screen
|
||||
if (newBaseCount === 0 && this.navBaseCount === 0) {
|
||||
this.sendNavDisplayMessage(navigation.msg.response.previous);
|
||||
this.sendNavDisplayMessage(msg.response.previous);
|
||||
return;
|
||||
}
|
||||
this.navBaseCount = newBaseCount;
|
||||
|
@ -364,7 +345,7 @@ class WeatherDisplay {
|
|||
}
|
||||
|
||||
sendNavDisplayMessage(message) {
|
||||
navigation.displayNavMessage({
|
||||
displayNavMessage({
|
||||
id: this.navId,
|
||||
type: message,
|
||||
});
|
||||
|
|
2
server/scripts/vendor/auto/luxon.mjs
vendored
2
server/scripts/vendor/auto/luxon.mjs
vendored
|
@ -7114,7 +7114,5 @@ function friendlyDateTime(dateTimeish) {
|
|||
|
||||
const VERSION = "3.1.0";
|
||||
|
||||
window.luxon = { DateTime, Duration, FixedOffsetZone, IANAZone, Info, Interval, InvalidZone, Settings, SystemZone, VERSION, Zone };
|
||||
|
||||
export { DateTime, Duration, FixedOffsetZone, IANAZone, Info, Interval, InvalidZone, Settings, SystemZone, VERSION, Zone };
|
||||
//# sourceMappingURL=luxon.js.map
|
||||
|
|
|
@ -27,11 +27,11 @@
|
|||
<script type="text/javascript" src="scripts/vendor/jquery.autocomplete.min.js"></script>
|
||||
<script type="text/javascript" src="scripts/vendor/auto/nosleep.js"></script>
|
||||
<script type="text/javascript" src="scripts/vendor/auto/swiped-events.js"></script>
|
||||
<script type="module" src="scripts/modules/navigation.mjs"></script>
|
||||
<script type="module" src="scripts/modules/currentweatherscroll.mjs"></script>
|
||||
|
||||
<!-- 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>
|
||||
|
@ -54,8 +54,6 @@
|
|||
<script type="text/javascript" src="scripts/data/stations.js"></script>
|
||||
|
||||
<script type="text/javascript" src="scripts/vendor/auto/suncalc.js"></script>
|
||||
<script type="text/javascript" src="scripts/modules/utilities.js"></script>
|
||||
<script type="text/javascript" src="scripts/modules/navigation.js"></script>
|
||||
<script type="text/javascript" src="scripts/custom.js?_=<%=production%>"></script>
|
||||
|
||||
<% } %>
|
||||
|
|
Loading…
Reference in a new issue