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 @@ - - <% } %>