diff --git a/.eslintrc.js b/.eslintrc.js
index 1d1d403..a605438 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -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,
diff --git a/datagenerators/output/stations - Copy.js b/datagenerators/output/stations - Copy.js
index d8a0dc0..5688564 100644
--- a/datagenerators/output/stations - Copy.js
+++ b/datagenerators/output/stations - Copy.js
@@ -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,
},
-};
\ No newline at end of file
+};
diff --git a/server/scripts/data/stations.js b/server/scripts/data/stations.js
index 198e54d..04f4a68 100644
--- a/server/scripts/data/stations.js
+++ b/server/scripts/data/stations.js
@@ -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',
diff --git a/server/scripts/index.mjs b/server/scripts/index.mjs
index 8756c8b..0d59a89 100644
--- a/server/scripts/index.mjs
+++ b/server/scripts/index.mjs
@@ -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,
-};
diff --git a/server/scripts/modules/almanac.mjs b/server/scripts/modules/almanac.mjs
index fc63967..8a6c76c 100644
--- a/server/scripts/modules/almanac.mjs
+++ b/server/scripts/modules/almanac.mjs
@@ -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);
diff --git a/server/scripts/modules/config.mjs b/server/scripts/modules/config.mjs
deleted file mode 100644
index cb8de66..0000000
--- a/server/scripts/modules/config.mjs
+++ /dev/null
@@ -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;
diff --git a/server/scripts/modules/currentweather.mjs b/server/scripts/modules/currentweather.mjs
index 3af19f1..639f885 100644
--- a/server/scripts/modules/currentweather.mjs
+++ b/server/scripts/modules/currentweather.mjs
@@ -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);
diff --git a/server/scripts/modules/currentweatherscroll.mjs b/server/scripts/modules/currentweatherscroll.mjs
index 21404d9..9d278b1 100644
--- a/server/scripts/modules/currentweatherscroll.mjs
+++ b/server/scripts/modules/currentweatherscroll.mjs
@@ -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();
diff --git a/server/scripts/modules/extendedforecast.mjs b/server/scripts/modules/extendedforecast.mjs
index 904824d..e7f49d3 100644
--- a/server/scripts/modules/extendedforecast.mjs
+++ b/server/scripts/modules/extendedforecast.mjs
@@ -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'));
diff --git a/server/scripts/modules/hourly.mjs b/server/scripts/modules/hourly.mjs
index 7a174db..3ceaea6 100644
--- a/server/scripts/modules/hourly.mjs
+++ b/server/scripts/modules/hourly.mjs
@@ -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'));
diff --git a/server/scripts/modules/latestobservations.mjs b/server/scripts/modules/latestobservations.mjs
index eab40bd..259e732 100644
--- a/server/scripts/modules/latestobservations.mjs
+++ b/server/scripts/modules/latestobservations.mjs
@@ -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'));
diff --git a/server/scripts/modules/localforecast.mjs b/server/scripts/modules/localforecast.mjs
index e5b3931..086cf13 100644
--- a/server/scripts/modules/localforecast.mjs
+++ b/server/scripts/modules/localforecast.mjs
@@ -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'));
diff --git a/server/scripts/modules/navigation.js b/server/scripts/modules/navigation.js
deleted file mode 100644
index 3910803..0000000
--- a/server/scripts/modules/navigation.js
+++ /dev/null
@@ -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,
- };
-})();
diff --git a/server/scripts/modules/navigation.mjs b/server/scripts/modules/navigation.mjs
new file mode 100644
index 0000000..76d4e73
--- /dev/null
+++ b/server/scripts/modules/navigation.mjs
@@ -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,
+};
diff --git a/server/scripts/modules/progress.mjs b/server/scripts/modules/progress.mjs
index 3ff0458..b72325d 100644
--- a/server/scripts/modules/progress.mjs
+++ b/server/scripts/modules/progress.mjs
@@ -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);
diff --git a/server/scripts/modules/radar.mjs b/server/scripts/modules/radar.mjs
index 1dc11e3..0f2e956 100644
--- a/server/scripts/modules/radar.mjs
+++ b/server/scripts/modules/radar.mjs
@@ -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'));
diff --git a/server/scripts/modules/regionalforecast.mjs b/server/scripts/modules/regionalforecast.mjs
index 11b117a..d8f4587 100644
--- a/server/scripts/modules/regionalforecast.mjs
+++ b/server/scripts/modules/regionalforecast.mjs
@@ -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'));
diff --git a/server/scripts/modules/status.mjs b/server/scripts/modules/status.mjs
index b2e2102..8f290e0 100644
--- a/server/scripts/modules/status.mjs
+++ b/server/scripts/modules/status.mjs
@@ -7,4 +7,3 @@ const STATUS = {
};
export default STATUS;
-window.STATUS = STATUS;
diff --git a/server/scripts/modules/travelforecast.mjs b/server/scripts/modules/travelforecast.mjs
index 5499528..020b30c 100644
--- a/server/scripts/modules/travelforecast.mjs
+++ b/server/scripts/modules/travelforecast.mjs
@@ -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));
diff --git a/server/scripts/modules/utilities.js b/server/scripts/modules/utilities.js
deleted file mode 100644
index 6b95dfb..0000000
--- a/server/scripts/modules/utilities.js
+++ /dev/null
@@ -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,
- },
- };
-})();
diff --git a/server/scripts/modules/utils/nosleep.mjs b/server/scripts/modules/utils/nosleep.mjs
new file mode 100644
index 0000000..53fe5f9
--- /dev/null
+++ b/server/scripts/modules/utils/nosleep.mjs
@@ -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;
diff --git a/server/scripts/modules/utils/units.mjs b/server/scripts/modules/utils/units.mjs
index f69f236..00eb8cd 100644
--- a/server/scripts/modules/utils/units.mjs
+++ b/server/scripts/modules/utils/units.mjs
@@ -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;
diff --git a/server/scripts/modules/utils/weather.mjs b/server/scripts/modules/utils/weather.mjs
new file mode 100644
index 0000000..702aadd
--- /dev/null
+++ b/server/scripts/modules/utils/weather.mjs
@@ -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,
+};
diff --git a/server/scripts/modules/weatherdisplay.mjs b/server/scripts/modules/weatherdisplay.mjs
index 8892532..19c21fc 100644
--- a/server/scripts/modules/weatherdisplay.mjs
+++ b/server/scripts/modules/weatherdisplay.mjs
@@ -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 {
${this.name}`;
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,
});
diff --git a/server/scripts/vendor/auto/luxon.mjs b/server/scripts/vendor/auto/luxon.mjs
index 177d3a9..fb57de0 100644
--- a/server/scripts/vendor/auto/luxon.mjs
+++ b/server/scripts/vendor/auto/luxon.mjs
@@ -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
diff --git a/views/index.ejs b/views/index.ejs
index 84dbe3d..907614d 100644
--- a/views/index.ejs
+++ b/views/index.ejs
@@ -27,11 +27,11 @@
+
+
-
-
@@ -54,8 +54,6 @@
-
-
<% } %>