Merge branch 'html'

This commit is contained in:
Matt Walsh 2022-11-21 22:01:39 -06:00
commit 33570a8030
64 changed files with 16917 additions and 17055 deletions

10
.vscode/launch.json vendored
View file

@ -4,18 +4,17 @@
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0", "version": "0.2.0",
"configurations": [ "configurations": [
{ {
"name": "Frontend", "name": "Frontend",
"request": "launch", "request": "launch",
"type": "pwa-chrome", "type": "chrome",
"url": "http://localhost:8080", "url": "http://localhost:8080",
"webRoot": "${workspaceFolder}/server", "webRoot": "${workspaceFolder}/server",
"skipFiles": [ "skipFiles": [
"<node_internals>/**", "<node_internals>/**",
"**/*.min.js", "**/*.min.js",
"**/vendor/**" "**/vendor/**"
] ],
}, },
{ {
"name": "Data:stations", "name": "Data:stations",
@ -40,7 +39,10 @@
"compounds": [ "compounds": [
{ {
"name": "Compound", "name": "Compound",
"configurations": ["Frontend", "Server"] "configurations": [
"Frontend",
"Server"
]
} }
] ]
} }

18
.vscode/settings.json vendored
View file

@ -1,5 +1,21 @@
{ {
"cSpell.enableFiletypes": [ "cSpell.enableFiletypes": [
"javascript" "javascript"
] ],
"liveSassCompile.settings.formats": [
{
"format": "compressed",
"extensionName": ".css",
"savePath": "/server/styles",
"savePathSegmentKeys": null,
"savePathReplaceSegmentsWith": null
}
],
"search.exclude": {
"**/node_modules": true,
"**/bower_components": true,
"**/*.code-search": true,
"**/compiled.css": true,
"**/*.min.js": true,
},
} }

View file

@ -22,10 +22,24 @@ There are a lot of CORS considerations and issues with api.weather.gov that are
``` ```
git clone https://github.com/netbymatt/ws4kp.git git clone https://github.com/netbymatt/ws4kp.git
cd ws4kp cd ws4kp
npm i
node index.js node index.js
``` ```
Open your web browser: http://localhost:8080/ Open your web browser: http://localhost:8080/
## Updates in 5.0
The change to 5.0 changes from drawing the weather graphics on canvas elements and instead uses HTML and CSS to style all of the weather graphics. A lot of other changes and fixes were implemented at the same time.
* Replace all canvas elements with HTML and CSS
* City and airport names are better parsed to only show the city name.
* Remove the dependency on libgif-js
* Use browser for text wrapping where necessary
* Some new weather icons
* Refresh only on slideshow repeat
* Removed Almanac 30-day outlook
* Fixed startup issue when current conditions are unavailable
*
## Why the fork? ## Why the fork?
The fork is a result of wanting a more manageable, modern code base to work with. Part of it is an exercise in my education in JavaScript. There are several technical changes that were made behind the scenes. The fork is a result of wanting a more manageable, modern code base to work with. Part of it is an exercise in my education in JavaScript. There are several technical changes that were made behind the scenes.

390
dist/index.html vendored
View file

@ -1 +1,389 @@
<!DOCTYPE html><html lang="en" xmlns="http://www.w3.org/1999/xhtml"><head><meta charset="utf-8"><link rel="preload" href="fonts/Star4000.woff" as="font" crossorigin="anonymous"><link rel="preload" href="fonts/Star 4 Radar.woff" as="font" crossorigin="anonymous"><link rel="preload" href="fonts/Star4000 Extended.woff" as="font" crossorigin="anonymous"><link rel="preload" href="fonts/Star4000 Large Compressed.woff" as="font" crossorigin="anonymous"><link rel="preload" href="fonts/Star4000 Large.woff" as="font" crossorigin="anonymous"><link rel="preload" href="fonts/Star4000 Small.woff" as="font" crossorigin="anonymous"><title>WeatherStar 4000+</title><meta name="description" content="Web based WeatherStar 4000 simulator that reports current and forecast weather conditions plus a few extras!"><meta name="keywords" content="WeatherStar 4000+"><meta name="author" content="Matt Walsh"><meta name="application-name" content="WeatherStar 4000+"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="apple-mobile-web-app-capable" content="yes"><meta name="apple-mobile-web-app-status-bar-style" content="black-translucent"><link rel="manifest" href="manifest.json"><link rel="icon" href="images/Logo192.png"><link rel="stylesheet" type="text/css" href="resources/ws.min.css?_=4.1.5"><script type="text/javascript" src="resources/data.min.js?_=4.1.5"></script><script type="text/javascript" src="resources/ws.min.js?_=4.1.5"></script></head><body><div id="divQuery"><form id="frmGetLatLng"><input id="txtAddress" type="text" value="" placeholder="Zip or City, State"><button id="btnGetGps" type="button" title="Get GPS Location"><img id="imgGetGps" src="images/nav/ic_gps_fixed_black_18dp_1x.png"></button> <input id="btnGetLatLng" type="submit" value="GO"> <input id="btnClearQuery" type="reset" value="Reset"></form><div id="divLat"></div><div id="divLng"></div></div><br><img id="imgPause1x" src="images/nav/ic_pause_white_24dp_1x.png"> <img id="imgPause2x" src="images/nav/ic_pause_white_24dp_2x.png"><div id="version" style="display:none">4.1.5</div><div id="divTwc"><div id="container"><div id="loading" width="640" height="480"><div><div class="title">WeatherStar 4000+</div><div class="instructions">Enter your location above to continue</div></div></div></div><div id="divTwcBottom"><div id="divTwcBottomLeft"><img id="NavigateMenu" class="navButton" src="images/nav/ic_menu_white_24dp_1x.png" title="Menu"> <img id="NavigatePrevious" class="navButton" src="images/nav/ic_skip_previous_white_24dp_1x.png" title="Previous"> <img id="NavigateNext" class="navButton" src="images/nav/ic_skip_next_white_24dp_1x.png" title="Next"> <img id="NavigatePlay" class="navButton" src="images/nav/ic_play_arrow_white_24dp_1x.png" title="Play"></div><div id="divTwcBottomMiddle"><img id="NavigateRefresh" class="navButton" src="images/nav/ic_refresh_white_24dp_1x.png" title="Refresh"></div><div id="divTwcBottomRight"><img id="ToggleFullScreen" class="navButton" src="images/nav/ic_fullscreen_exit_white_24dp_1x.png" title="Exit Fullscreen"></div></div></div><br><div class="info"><a href="https://github.com/netbymatt/ws4kp#weatherstar-4000">More information</a></div><div class="heading">Selected displays</div><div id="enabledDisplays"></div><div id="divInfo">Location: <span id="spanCity"></span> <span id="spanState"></span><br>Station Id: <span id="spanStationId"></span><br>Radar Id: <span id="spanRadarId"></span><br>Zone Id: <span id="spanZoneId"></span><br></div><div id="divRefresh">Last Update: <span id="spanLastRefresh">(None)</span><br><input id="chkAutoRefresh" name="chkAutoRefresh" type="checkbox"><label id="lblRefreshCountDown" for="chkAutoRefresh">Auto Refresh: <span id="spanRefreshCountDown">--:--</span></label></div><div id="divUnits">Units: <input id="radEnglish" name="radUnits" type="radio" value="ENGLISH"><label for="radEnglish">English</label> <input id="radMetric" name="radUnits" type="radio" value="METRIC"><label for="radMetric">Metric</label></div></body></html> <!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta charset="utf-8">
<title>WeatherStar 4000+</title>
<meta name="description" content="Web based WeatherStar 4000 simulator that reports current and forecast weather conditions plus a few extras!">
<meta name="keywords" content="WeatherStar 4000+">
<meta name="author" content="Matt Walsh">
<meta name="application-name" content="WeatherStar 4000+">
<meta name="viewport" content="width=device-width,initial-scale=1">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<link rel="manifest" href="manifest.json">
<link rel="icon" href="images/Logo192.png">
<link rel="stylesheet" type="text/css" href="resources/ws.min.css?_=5.0.0">
<script type="text/javascript" src="resources/data.min.js?_=5.0.0"></script>
<script type="text/javascript" src="resources/ws.min.js?_=5.0.0"></script>
</head>
<body>
<div id="divQuery">
<form id="frmGetLatLng"><input id="txtAddress" type="text" value="" placeholder="Zip or City, State"><button id="btnGetGps" type="button" title="Get GPS Location"><img id="imgGetGps" src="images/nav/ic_gps_fixed_black_18dp_1x.png"></button> <input id="btnGetLatLng" type="submit" value="GO"> <input id="btnClearQuery" type="reset" value="Reset"></form>
<div id="divLat"></div>
<div id="divLng"></div>
</div><br><img id="imgPause1x" src="images/nav/ic_pause_white_24dp_1x.png"> <img id="imgPause2x" src="images/nav/ic_pause_white_24dp_2x.png">
<div id="version" style="display:none">5.0.0</div>
<div id="divTwc">
<div id="container">
<div id="loading" width="640" height="480">
<div>
<div class="title">WeatherStar 4000+</div>
<div class="instructions">Enter your location above to continue</div>
</div>
</div>
<div id="progress-html" class="weather-display">
<div class="header">
<div class="logo"><img src="images/Logo3.png"></div>
<div class="title dual">
<div class="top">WeatherStar</div>
<div class="bottom">4000+ v5.0.0</div>
</div>
<div class="date-time date"></div>
<div class="date-time time"></div>
</div>
<div class="main has-box progress">
<div class="container">
<div class="item template">
<div class="name">Current Conditions</div>
<div class="links loading">
<div class="loading">Loading</div>
<div class="press-here">Press Here</div>
<div class="failed">Failed</div>
<div class="no-data">No Data</div>
<div class="disabled">Disabled</div>
</div>
</div>
</div>
</div>
<div class="scroll">
<div class="progress-bar-container show">
<div class="progress-bar"></div>
<div class="cover"></div>
</div>
</div>
</div>
<div id="hourly-html" class="weather-display">
<div class="header">
<div class="logo"><img src="images/Logo3.png"></div>
<div class="title single">Hourly Forecast</div>
<div class="date-time date"></div>
<div class="date-time time"></div>
</div>
<div class="main has-scroll hourly">
<div class="column-headers">
<div class="temp">TEMP</div>
<div class="like">LIKE</div>
<div class="wind">WIND</div>
</div>
<div class="hourly-lines">
<div class="hourly-row template">
<div class="hour"></div>
<div class="icon"><img></div>
<div class="temp"></div>
<div class="like"></div>
<div class="wind"></div>
</div>
</div>
</div>
<div class="scroll">
<div class="scrolling template"></div>
<div class="fixed"></div>
</div>
</div>
<div id="travel-html" class="weather-display">
<div class="header">
<div class="logo"><img src="images/Logo3.png"></div>
<div class="title dual">
<div class="top">Travel Forecast</div>
<div class="bottom">For</div>
</div>
<div class="date-time date"></div>
<div class="date-time time"></div>
</div>
<div class="main has-scroll travel">
<div class="column-headers">
<div class="temp low">LOW</div>
<div class="temp high">HIGH</div>
</div>
<div class="travel-lines">
<div class="travel-row template">
<div class="city"></div>
<div class="icon"><img></div>
<div class="temp low"></div>
<div class="temp high"></div>
</div>
</div>
</div>
<div class="scroll">
<div class="scrolling template"></div>
<div class="fixed"></div>
</div>
</div>
<div id="current-weather-html" class="weather-display">
<div class="header">
<div class="logo"><img src="images/Logo3.png"></div>
<div class="title dual">
<div class="top">Current</div>
<div class="bottom">Conditions</div>
</div>
<div class="noaa-logo"><img src="images/noaa5.gif"></div>
</div>
<div class="main has-scroll has-box current-weather">
<div class="weather template">
<div class="left col">
<div class="temp center"></div>
<div class="condition center"></div>
<div class="icon center"><img src=""></div>
<div class="wind-container">
<div class="wind-label">Wind:</div>
<div class="wind"></div>
</div>
<div class="wind-gusts"></div>
</div>
<div class="right col">
<div class="location"></div>
<div class="row">
<div class="label">Humidity:</div>
<div class="humidity value"></div>
</div>
<div class="row">
<div class="label">Dewpoint:</div>
<div class="dewpoint value"></div>
</div>
<div class="row">
<div class="label">Ceiling:</div>
<div class="ceiling value"></div>
</div>
<div class="row">
<div class="label">Visibility:</div>
<div class="visibility value"></div>
</div>
<div class="row">
<div class="label">Pressure:</div>
<div class="pressure value"></div>
</div>
<div class="row">
<div class="heat-index-label label"></div>
<div class="heat-index value"></div>
</div>
</div>
</div>
</div>
<div class="scroll">
<div class="scrolling template"></div>
<div class="fixed"></div>
</div>
</div>
<div id="local-forecast-html" class="weather-display">
<div class="header">
<div class="logo"><img src="images/Logo3.png"></div>
<div class="title dual">
<div class="top">Local</div>
<div class="bottom">Forecast</div>
</div>
<div class="date-time date"></div>
<div class="date-time time"></div>
<div class="noaa-logo"><img src="images/noaa5.gif"></div>
</div>
<div class="main has-scroll has-box local-forecast">
<div class="container">
<div class="forecasts">
<div class="forecast template">
<div class="text"></div>
</div>
</div>
</div>
</div>
<div class="scroll">
<div class="scrolling template"></div>
<div class="fixed"></div>
</div>
</div>
<div id="latest-observations-html" class="weather-display">
<div class="header">
<div class="logo"><img src="images/Logo3.png"></div>
<div class="title dual">
<div class="top">Latest</div>
<div class="bottom">Observations</div>
</div>
<div class="noaa-logo"><img src="images/noaa5.gif"></div>
</div>
<div class="main has-scroll latest-observations has-box">
<div class="container">
<div class="column-headers">
<div class="temp english">&deg;F</div>
<div class="temp metric">&deg;C</div>
<div class="weather">Weather</div>
<div class="wind">Wind</div>
</div>
<div class="observation-lines">
<div class="observation-row template">
<div class="location"></div>
<div class="temp"></div>
<div class="weather"></div>
<div class="wind"></div>
</div>
</div>
</div>
</div>
<div class="scroll">
<div class="scrolling template"></div>
<div class="fixed"></div>
</div>
</div>
<div id="regional-forecast-html" class="weather-display">
<div class="header">
<div class="logo"><img src="images/Logo3.png"></div>
<div class="title dual">
<div class="top">Regional</div>
<div class="bottom">Observations</div>
</div>
<div class="date-time date"></div>
<div class="date-time time"></div>
</div>
<div class="main has-scroll regional-forecast">
<div class="map"><img src="images/Basemap2.png"></div>
<div class="location-container">
<div class="location template">
<div class="icon"><img src=""></div>
<div class="city"></div>
<div class="temp"></div>
</div>
</div>
</div>
<div class="scroll">
<div class="scrolling template"></div>
<div class="fixed"></div>
</div>
</div>
<div id="almanac-html" class="weather-display">
<div class="header">
<div class="logo"><img src="images/Logo3.png"></div>
<div class="title single">Almanac</div>
<div class="date-time date"></div>
<div class="date-time time"></div>
</div>
<div class="main has-scroll almanac">
<div class="sun">
<div class="days">
<div class="day"></div>
<div class="day day-1">Monday</div>
<div class="day day-2">Tuesday</div>
</div>
<div class="times times-1">
<div class="name">Sunrise:</div>
<div class="sun-time rise-1">6:24 am</div>
<div class="sun-time rise-2">6:25 am</div>
</div>
<div class="times times-2">
<div class="name">Sunset:</div>
<div class="sun-time set-1">6:24 am</div>
<div class="sun-time set-2">6:25 am</div>
</div>
</div>
<div class="moon">
<div class="title">Moon Data:</div>
<div class="days">
<div class="day template">
<div class="type"></div>
<div class="icon"><img></div>
<div class="date"></div>
</div>
</div>
</div>
</div>
<div class="scroll">
<div class="scrolling template"></div>
<div class="fixed"></div>
</div>
</div>
<div id="extended-forecast-html" class="weather-display">
<div class="header">
<div class="logo"><img src="images/Logo3.png"></div>
<div class="title dual">
<div class="top">Extended</div>
<div class="bottom">Forecast</div>
</div>
<div class="date-time date"></div>
<div class="date-time time"></div>
</div>
<div class="main has-scroll extended-forecast">
<div class="day-container">
<div class="day template">
<div class="date"></div>
<div class="icon"><img src=""></div>
<div class="condition"></div>
<div class="temperatures">
<div class="temperature-block lo">
<div class="label">Lo</div>
<div class="value value-lo"></div>
</div>
<div class="temperature-block hi">
<div class="label">Hi</div>
<div class="value value-hi"></div>
</div>
</div>
</div>
</div>
</div>
<div class="scroll">
<div class="scrolling template"></div>
<div class="fixed"></div>
</div>
</div>
<div id="radar-html" class="weather-display">
<div class="header">
<div class="logo"><img src="images/Logo3.png"></div>
<div class="title dual">
<div class="top">Local</div>
<div class="bottom">Radar</div>
</div>
<div class="right">
<div class="precip">
<div class="precip-header">PRECIP</div>
<div class="scale">
<div class="text">Light</div>
<div class="scale-table">
<div class="box box-1"></div>
<div class="box box-2"></div>
<div class="box box-3"></div>
<div class="box box-4"></div>
<div class="box box-5"></div>
<div class="box box-6"></div>
<div class="box box-7"></div>
<div class="box box-7"></div>
</div>
<div class="text">Heavy</div>
</div>
<div class="time"></div>
</div>
</div>
</div>
<div class="main radar">
<div class="container">
<div class="scroll-area">
<div class="frame template">
<div class="map"><img src="images/4000RadarMap2.jpg"></div>
</div>
</div>
</div>
</div>
</div>
</div>
<div id="divTwcBottom">
<div id="divTwcBottomLeft"><img id="NavigateMenu" class="navButton" src="images/nav/ic_menu_white_24dp_1x.png" title="Menu"> <img id="NavigatePrevious" class="navButton" src="images/nav/ic_skip_previous_white_24dp_1x.png" title="Previous"> <img id="NavigateNext" class="navButton" src="images/nav/ic_skip_next_white_24dp_1x.png" title="Next"> <img id="NavigatePlay" class="navButton" src="images/nav/ic_play_arrow_white_24dp_1x.png" title="Play"></div>
<div id="divTwcBottomMiddle"><img id="NavigateRefresh" class="navButton" src="images/nav/ic_refresh_white_24dp_1x.png" title="Refresh"></div>
<div id="divTwcBottomRight"><img id="ToggleFullScreen" class="navButton" src="images/nav/ic_fullscreen_exit_white_24dp_1x.png" title="Enter Fullscreen"></div>
</div>
</div><br>
<div class="info"><a href="https://github.com/netbymatt/ws4kp#weatherstar-4000">More information</a></div>
<div class="heading">Selected displays</div>
<div id="enabledDisplays"></div>
<div id="divInfo">Location: <span id="spanCity"></span> <span id="spanState"></span><br>Station Id: <span id="spanStationId"></span><br>Radar Id: <span id="spanRadarId"></span><br>Zone Id: <span id="spanZoneId"></span><br></div>
<div id="divRefresh">Last Update: <span id="spanLastRefresh">(None)</span><br><input id="chkAutoRefresh" name="chkAutoRefresh" type="checkbox"><label id="lblRefreshCountDown" for="chkAutoRefresh">Auto Refresh: <span id="spanRefreshCountDown">--:--</span></label></div>
<div id="divUnits">Units: <input id="radEnglish" name="radUnits" type="radio" value="ENGLISH"><label for="radEnglish">English</label> <input id="radMetric" name="radUnits" type="radio" value="METRIC"><label for="radMetric">Metric</label></div>
</body>
</html>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -2,7 +2,6 @@
const gulp = require('gulp'); const gulp = require('gulp');
const concat = require('gulp-concat'); const concat = require('gulp-concat');
const terser = require('gulp-terser'); const terser = require('gulp-terser');
const cleanCSS = require('gulp-clean-css');
const ejs = require('gulp-ejs'); const ejs = require('gulp-ejs');
const rename = require('gulp-rename'); const rename = require('gulp-rename');
const htmlmin = require('gulp-htmlmin'); const htmlmin = require('gulp-htmlmin');
@ -34,7 +33,6 @@ const jsSources = [
'server/scripts/vendor/auto/nosleep.js', 'server/scripts/vendor/auto/nosleep.js',
'server/scripts/vendor/auto/swiped-events.js', 'server/scripts/vendor/auto/swiped-events.js',
'server/scripts/index.js', 'server/scripts/index.js',
'server/scripts/libgif.js',
'server/scripts/vendor/auto/luxon.js', 'server/scripts/vendor/auto/luxon.js',
'server/scripts/vendor/auto/suncalc.js', 'server/scripts/vendor/auto/suncalc.js',
'server/scripts/modules/draw.js', 'server/scripts/modules/draw.js',
@ -60,11 +58,10 @@ gulp.task('compress_js', () => gulp.src(jsSources)
.pipe(gulp.dest('./dist/resources'))); .pipe(gulp.dest('./dist/resources')));
const cssSources = [ const cssSources = [
'server/styles/index.css', 'server/styles/main.css',
]; ];
gulp.task('compress_css', () => gulp.src(cssSources) gulp.task('copy_css', () => gulp.src(cssSources)
.pipe(concat('ws.min.css')) .pipe(concat('ws.min.css'))
.pipe(cleanCSS())
.pipe(gulp.dest('./dist/resources'))); .pipe(gulp.dest('./dist/resources')));
const htmlSources = [ const htmlSources = [
@ -121,4 +118,4 @@ gulp.task('invalidate', async () => cloudfront.createInvalidation({
}, },
}).promise()); }).promise());
module.exports = gulp.series(clean, gulp.parallel('compress_js', 'compress_js_data', 'compress_css', 'compress_html', 'copy_other_files'), 'upload', 'invalidate'); module.exports = gulp.series(clean, gulp.parallel('compress_js', 'compress_js_data', 'copy_css', 'compress_html', 'copy_other_files'), 'upload', 'invalidate');

28240
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,41 +1,47 @@
{ {
"name": "ws4kp", "name": "ws4kp",
"version": "4.1.5", "version": "5.0.0",
"description": "Welcome to the WeatherStar 4000+ project page!", "description": "Welcome to the WeatherStar 4000+ project page!",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
"test": "echo \"Error: no test specified\" && exit 1" "test": "echo \"Error: no test specified\" && exit 1",
}, "build:css": "sass ./server/styles/scss/style.scss ./server/styles/compiled.css",
"repository": { "lint": "eslint ./server/scripts/**",
"type": "git", "lint:fix": "eslint --fix ./server/scripts/**"
"url": "git+https://github.com/netbymatt/ws4kp.git" },
}, "repository": {
"author": "Matt Walsh", "type": "git",
"license": "MIT", "url": "git+https://github.com/netbymatt/ws4kp.git"
"bugs": { },
"url": "https://github.com/netbymatt/ws4kp/issues" "author": "Matt Walsh",
}, "license": "MIT",
"homepage": "https://github.com/netbymatt/ws4kp#readme", "bugs": {
"devDependencies": { "url": "https://github.com/netbymatt/ws4kp/issues"
"del": "^6.0.0", },
"ejs": "^3.1.5", "homepage": "https://github.com/netbymatt/ws4kp#readme",
"eslint": "^8.10.0", "devDependencies": {
"eslint-config-airbnb-base": "^15.0.0", "del": "^6.0.0",
"eslint-plugin-import": "^2.22.1", "ejs": "^3.1.5",
"express": "^4.17.1", "eslint-config-airbnb-base": "^15.0.0",
"gulp": "^4.0.2", "express": "^4.17.1",
"gulp-clean-css": "^4.3.0", "gulp": "^4.0.2",
"gulp-concat": "^2.6.1", "gulp-concat": "^2.6.1",
"gulp-ejs": "^5.1.0", "gulp-ejs": "^5.1.0",
"gulp-htmlmin": "^5.0.1", "gulp-htmlmin": "^5.0.1",
"gulp-rename": "^2.0.0", "gulp-rename": "^2.0.0",
"gulp-s3-upload": "^1.7.3", "gulp-s3-upload": "^1.7.3",
"gulp-terser": "^2.0.0", "gulp-sass": "^5.1.0",
"jquery": "^3.6.0", "gulp-terser": "^2.0.0",
"jquery-touchswipe": "^1.6.19", "jquery": "^3.6.0",
"luxon": "^3.0.0", "jquery-touchswipe": "^1.6.19",
"nosleep.js": "^0.12.0", "luxon": "^3.0.0",
"suncalc": "^1.8.0", "nosleep.js": "^0.12.0",
"swiped-events": "^1.1.4" "sass": "^1.54.0",
} "suncalc": "^1.8.0",
"swiped-events": "^1.1.4"
},
"dependencies": {
"eslint": "^8.21.0",
"eslint-plugin-import": "^2.26.0"
}
} }

Binary file not shown.

Binary file not shown.

BIN
server/images/r/cold.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

View file

@ -581,4 +581,3 @@ const RegionalCities = [
lon: -110.9698, lon: -110.9698,
}, },
]; ];

View file

@ -4,9 +4,7 @@ document.addEventListener('DOMContentLoaded', () => {
}); });
const index = (() => { const index = (() => {
const overrides = { const overrides = {};
// '32899, Orlando, Florida, USA': { x: -80.6774, y: 28.6143 },
};
const AutoRefreshIntervalMs = 500; const AutoRefreshIntervalMs = 500;
const AutoRefreshTotalIntervalMs = 600000; // 10 min. const AutoRefreshTotalIntervalMs = 600000; // 10 min.
@ -127,8 +125,10 @@ const index = (() => {
const TwcUnits = localStorage.getItem('TwcUnits'); const TwcUnits = localStorage.getItem('TwcUnits');
if (!TwcUnits || TwcUnits === 'ENGLISH') { if (!TwcUnits || TwcUnits === 'ENGLISH') {
document.getElementById('radEnglish').checked = true; document.getElementById('radEnglish').checked = true;
navigation.message({ type: 'units', message: 'english' });
} else if (TwcUnits === 'METRIC') { } else if (TwcUnits === 'METRIC') {
document.getElementById('radMetric').checked = true; document.getElementById('radMetric').checked = true;
navigation.message({ type: 'units', message: 'metric' });
} }
document.getElementById('radEnglish').addEventListener('change', changeUnits); document.getElementById('radEnglish').addEventListener('change', changeUnits);
@ -231,8 +231,11 @@ const index = (() => {
window.scrollTo(0, 0); window.scrollTo(0, 0);
FullScreenOverride = true; FullScreenOverride = true;
} }
navigation.resize();
UpdateFullScreenNavigate(); UpdateFullScreenNavigate();
// change hover text
document.getElementById('ToggleFullScreen').title = 'Exit fullscreen';
}; };
const ExitFullscreen = () => { const ExitFullscreen = () => {
@ -252,6 +255,9 @@ const index = (() => {
} else if (document.msExitFullscreen) { } else if (document.msExitFullscreen) {
document.msExitFullscreen(); document.msExitFullscreen();
} }
navigation.resize();
// change hover text
document.getElementById('ToggleFullScreen').title = 'Enter fullscreen';
}; };
const btnNavigateMenuClick = () => { const btnNavigateMenuClick = () => {
@ -311,6 +317,7 @@ const index = (() => {
}; };
const btnNavigateRefreshClick = () => { const btnNavigateRefreshClick = () => {
navigation.resetStatuses();
LoadTwcData(); LoadTwcData();
UpdateFullScreenNavigate(); UpdateFullScreenNavigate();

File diff suppressed because it is too large Load diff

View file

@ -1,50 +1,35 @@
// display sun and moon data // display sun and moon data
/* globals WeatherDisplay, utils, STATUS, draw, SunCalc, luxon */ /* globals WeatherDisplay, utils, STATUS, SunCalc, luxon */
// eslint-disable-next-line no-unused-vars // eslint-disable-next-line no-unused-vars
class Almanac extends WeatherDisplay { class Almanac extends WeatherDisplay {
constructor(navId, elemId) { constructor(navId, elemId) {
super(navId, elemId, 'Almanac'); super(navId, elemId, 'Almanac', true);
// pre-load background images (returns promises) // pre-load background images (returns promises)
this.backgroundImage0 = utils.image.load('images/BackGround3_1.png'); this.backgroundImage0 = utils.image.load('images/BackGround3_1.png');
this.backgroundImage1 = utils.image.load('images/BackGround1_1.png');
// load all images in parallel (returns promises) // preload the moon images
this.moonImages = [ utils.image.preload('images/2/Full-Moon.gif');
utils.image.load('images/2/Full-Moon.gif'), utils.image.preload('images/2/Last-Quarter.gif');
utils.image.load('images/2/Last-Quarter.gif'), utils.image.preload('images/2/New-Moon.gif');
utils.image.load('images/2/New-Moon.gif'), utils.image.preload('images/2/First-Quarter.gif');
utils.image.load('images/2/First-Quarter.gif'),
];
this.timing.totalScreens = 2; this.timing.totalScreens = 1;
} }
async getData(_weatherParameters) { async getData(_weatherParameters) {
super.getData(_weatherParameters); super.getData(_weatherParameters);
const weatherParameters = _weatherParameters ?? this.weatherParameters; const weatherParameters = _weatherParameters ?? this.weatherParameters;
// get images for outlook
const imagePromises = [
utils.image.load('https://www.cpc.ncep.noaa.gov/products/predictions/30day/off14_temp.gif', true),
utils.image.load('https://www.cpc.ncep.noaa.gov/products/predictions/30day/off14_prcp.gif', true),
];
// get sun/moon data // get sun/moon data
const { sun, moon } = this.calcSunMoonData(weatherParameters); const { sun, moon } = this.calcSunMoonData(weatherParameters);
// process images for outlook
const [outlookTemp, outlookPrecip] = await Promise.all(imagePromises);
const outlook = Almanac.parseOutlooks(weatherParameters.latitude, weatherParameters.longitude, outlookTemp, outlookPrecip);
// store the data // store the data
this.data = { this.data = {
sun, sun,
moon, moon,
outlook,
}; };
// update status // update status
this.setStatus(STATUS.loaded); this.setStatus(STATUS.loaded);
@ -127,115 +112,6 @@ class Almanac extends WeatherDisplay {
return { phase: phaseName, date: moonDate }; return { phase: phaseName, date: moonDate };
} }
// use the color of the pixel to determine the outlook
static parseOutlooks(lat, lon, temp, precip) {
const { DateTime } = luxon;
const month = DateTime.local();
const thisMonth = month.toLocaleString({ month: 'short' });
const nextMonth = month.plus({ months: 1 }).toLocaleString({ month: 'short' });
// draw the images on the canvases
const tempContext = utils.image.drawLocalCanvas(temp);
const precipContext = utils.image.drawLocalCanvas(precip);
// get the color from each canvas
const tempColor = Almanac.getOutlookColor(lat, lon, tempContext);
const precipColor = Almanac.getOutlookColor(lat, lon, precipContext);
return {
thisMonth,
nextMonth,
temperature: Almanac.getOutlookTemperatureIndicator(tempColor),
precipitation: Almanac.getOutlookPrecipitationIndicator(precipColor),
};
}
static getOutlookColor(lat, lon, context) {
let x = 0;
let y = 0;
// The height is in the range of latitude 75'N (top) - 15'N (bottom)
y = ((75 - lat) / 53) * 707;
if (lat < 48.83) {
y -= Math.abs(48.83 - lat) * 2.9;
}
if (lon < -100.46) {
y -= Math.abs(-100.46 - lon) * 1.7;
} else {
y -= Math.abs(-100.46 - lon) * 1.7;
}
// The width is in the range of the longitude ???
x = ((-155 - lon) / -110) * 719; // -155 - -40
if (lon < -100.46) {
x -= Math.abs(-100.46 - lon) * 1;
if (lat > 40) {
x += Math.abs(40 - lat) * 4;
} else {
x -= Math.abs(40 - lat) * 4;
}
} else {
x += Math.abs(-100.46 - lon) * 2;
if (lat < 36 && lon > -90) {
x += Math.abs(36 - lat) * 8;
} else {
x -= Math.abs(36 - lat) * 6;
}
}
// The further left and right from lat 45 and lon -97 the y increases
x = Math.round(x);
y = Math.round(y);
// Determine if there is any "non-white" colors around the area.
// Search a 16x16 region.
for (let colorX = x - 8; colorX <= x + 8; colorX += 1) {
for (let colorY = y - 8; colorY <= y + 8; colorY += 1) {
const pixelColor = Almanac.getPixelColor(context, colorX, colorY);
if ((pixelColor.r !== 0 && pixelColor.g !== 0 && pixelColor.b !== 0)
|| (pixelColor.r !== 255 && pixelColor.g !== 255 && pixelColor.b !== 255)) {
return pixelColor;
}
}
}
return false;
}
// get rgb values of a pixel
static getPixelColor(context, x, y) {
const pixelData = context.getImageData(x, y, 1, 1).data;
return {
r: pixelData[0],
g: pixelData[1],
b: pixelData[2],
};
}
// get temperature outlook from color
static getOutlookTemperatureIndicator(pixelColor) {
if (pixelColor.b > pixelColor.r) {
return 'Below Normal';
} if (pixelColor.r > pixelColor.b) {
return 'Above Normal';
}
return 'Normal';
}
// get precipitation outlook from color
static getOutlookPrecipitationIndicator(pixelColor) {
if (pixelColor.g > pixelColor.r) {
return 'Above Normal';
} if (pixelColor.r > pixelColor.g) {
return 'Below Normal';
}
return 'Normal';
}
async drawCanvas() { async drawCanvas() {
super.drawCanvas(); super.drawCanvas();
const info = this.data; const info = this.data;
@ -243,81 +119,47 @@ class Almanac extends WeatherDisplay {
const Today = DateTime.local(); const Today = DateTime.local();
const Tomorrow = Today.plus({ days: 1 }); const Tomorrow = Today.plus({ days: 1 });
// extract moon images // sun and moon data
const [FullMoonImage, LastMoonImage, NewMoonImage, FirstMoonImage] = await Promise.all(this.moonImages); this.elem.querySelector('.day-1').innerHTML = Today.toLocaleString({ weekday: 'long' });
this.elem.querySelector('.day-2').innerHTML = Tomorrow.toLocaleString({ weekday: 'long' });
this.elem.querySelector('.rise-1').innerHTML = DateTime.fromJSDate(info.sun[0].sunrise).toLocaleString(DateTime.TIME_SIMPLE).toLowerCase();
this.elem.querySelector('.rise-2').innerHTML = DateTime.fromJSDate(info.sun[1].sunrise).toLocaleString(DateTime.TIME_SIMPLE).toLowerCase();
this.elem.querySelector('.set-1').innerHTML = DateTime.fromJSDate(info.sun[0].sunset).toLocaleString(DateTime.TIME_SIMPLE).toLowerCase();
this.elem.querySelector('.set-2').innerHTML = DateTime.fromJSDate(info.sun[1].sunset).toLocaleString(DateTime.TIME_SIMPLE).toLowerCase();
switch (this.screenIndex) { const days = info.moon.map((MoonPhase) => {
case 1: { const fill = {};
this.context.drawImage(await this.backgroundImage1, 0, 0);
draw.horizontalGradientSingle(this.context, 0, 30, 500, 90, draw.topColor1, draw.topColor2);
draw.triangle(this.context, 'rgb(28, 10, 87)', 500, 30, 450, 90, 500, 90);
draw.horizontalGradientSingle(this.context, 0, 90, 52, 399, draw.sideColor1, draw.sideColor2);
draw.horizontalGradientSingle(this.context, 584, 90, 640, 399, draw.sideColor1, draw.sideColor2);
draw.titleText(this.context, 'Almanac', 'Outlook'); const date = MoonPhase.date.toLocaleString({ month: 'short', day: 'numeric' });
draw.text(this.context, 'Star4000', '24pt', '#FFFFFF', 320, 180, '30 Day Outlook', 2, 'center'); fill.date = date;
fill.type = MoonPhase.phase;
fill.icon = { type: 'img', src: Almanac.imageName(MoonPhase.Phase) };
const DateRange = `MID-${info.outlook.thisMonth.toUpperCase()} TO MID-${info.outlook.nextMonth.toUpperCase()}`; return this.fillTemplate('day', fill);
draw.text(this.context, 'Star4000', '24pt', '#FFFFFF', 320, 220, DateRange, 2, 'center'); });
const Temperature = info.outlook.temperature; const daysContainer = this.elem.querySelector('.moon .days');
draw.text(this.context, 'Star4000', '24pt', '#FFFFFF', 70, 300, `Temperatures: ${Temperature}`, 2); daysContainer.innerHTML = '';
daysContainer.append(...days);
const Precipitation = info.outlook.precipitation;
draw.text(this.context, 'Star4000', '24pt', '#FFFFFF', 70, 380, `Precipitation: ${Precipitation}`, 2);
break;
}
case 0:
default:
// sun and moon data
this.context.drawImage(await this.backgroundImage0, 0, 0);
draw.horizontalGradientSingle(this.context, 0, 30, 500, 90, draw.topColor1, draw.topColor2);
draw.triangle(this.context, 'rgb(28, 10, 87)', 500, 30, 450, 90, 500, 90);
draw.horizontalGradientSingle(this.context, 0, 90, 640, 190, draw.sideColor1, draw.sideColor2);
draw.titleText(this.context, 'Almanac', 'Astronomical');
draw.text(this.context, 'Star4000', '24pt', '#FFFF00', 320, 120, Today.toLocaleString({ weekday: 'long' }), 2, 'center');
draw.text(this.context, 'Star4000', '24pt', '#FFFF00', 500, 120, Tomorrow.toLocaleString({ weekday: 'long' }), 2, 'center');
draw.text(this.context, 'Star4000', '24pt', '#FFFFFF', 70, 150, 'Sunrise:', 2);
draw.text(this.context, 'Star4000', '24pt', '#FFFFFF', 270, 150, DateTime.fromJSDate(info.sun[0].sunrise).toLocaleString(DateTime.TIME_SIMPLE).toLowerCase(), 2);
draw.text(this.context, 'Star4000', '24pt', '#FFFFFF', 450, 150, DateTime.fromJSDate(info.sun[1].sunrise).toLocaleString(DateTime.TIME_SIMPLE).toLowerCase(), 2);
draw.text(this.context, 'Star4000', '24pt', '#FFFFFF', 70, 180, ' Sunset:', 2);
draw.text(this.context, 'Star4000', '24pt', '#FFFFFF', 270, 180, DateTime.fromJSDate(info.sun[0].sunset).toLocaleString(DateTime.TIME_SIMPLE).toLowerCase(), 2);
draw.text(this.context, 'Star4000', '24pt', '#FFFFFF', 450, 180, DateTime.fromJSDate(info.sun[1].sunset).toLocaleString(DateTime.TIME_SIMPLE).toLowerCase(), 2);
draw.text(this.context, 'Star4000', '24pt', '#FFFF00', 70, 220, 'Moon Data:', 2);
info.moon.forEach((MoonPhase, Index) => {
const date = MoonPhase.date.toLocaleString({ month: 'short', day: 'numeric' });
draw.text(this.context, 'Star4000', '24pt', '#FFFFFF', 120 + Index * 130, 260, MoonPhase.phase, 2, 'center');
draw.text(this.context, 'Star4000', '24pt', '#FFFFFF', 120 + Index * 130, 390, date, 2, 'center');
const image = (() => {
switch (MoonPhase.phase) {
case 'Full':
return FullMoonImage;
case 'Last':
return LastMoonImage;
case 'New':
return NewMoonImage;
case 'First':
default:
return FirstMoonImage;
}
})();
this.context.drawImage(image, 75 + Index * 130, 270);
});
break;
}
this.finishDraw(); this.finishDraw();
} }
static imageName(type) {
switch (type) {
case 'Full':
return 'images/2/Full-Moon.gif';
case 'Last':
return 'images/2/Last-Quarter.gif';
case 'New':
return 'images/2/New-Moon.gif';
case 'First':
default:
return 'images/2/First-Quarter.gif';
}
}
// make sun and moon data available outside this class // make sun and moon data available outside this class
// promise allows for data to be requested before it is available // promise allows for data to be requested before it is available
async getSun() { async getSun() {

View file

@ -1,10 +1,10 @@
// current weather conditions display // current weather conditions display
/* globals WeatherDisplay, utils, STATUS, icons, UNITS, draw, navigation */ /* globals WeatherDisplay, utils, STATUS, icons, UNITS, navigation */
// eslint-disable-next-line no-unused-vars // eslint-disable-next-line no-unused-vars
class CurrentWeather extends WeatherDisplay { class CurrentWeather extends WeatherDisplay {
constructor(navId, elemId) { constructor(navId, elemId) {
super(navId, elemId, 'Current Conditions'); super(navId, elemId, 'Current Conditions', true);
// pre-load background image (returns promise) // pre-load background image (returns promise)
this.backgroundImage = utils.image.load('images/BackGround1_1.png'); this.backgroundImage = utils.image.load('images/BackGround1_1.png');
} }
@ -69,7 +69,7 @@ class CurrentWeather extends WeatherDisplay {
data.Temperature = Math.round(observations.temperature.value); data.Temperature = Math.round(observations.temperature.value);
data.TemperatureUnit = 'C'; data.TemperatureUnit = 'C';
data.DewPoint = Math.round(observations.dewpoint.value); data.DewPoint = Math.round(observations.dewpoint.value);
data.Ceiling = Math.round(observations.cloudLayers[0].base.value); data.Ceiling = Math.round(observations.cloudLayers[0]?.base?.value ?? 0);
data.CeilingUnit = 'm.'; data.CeilingUnit = 'm.';
data.Visibility = Math.round(observations.visibility.value / 1000); data.Visibility = Math.round(observations.visibility.value / 1000);
data.VisibilityUnit = ' km.'; data.VisibilityUnit = ' km.';
@ -111,94 +111,43 @@ class CurrentWeather extends WeatherDisplay {
async drawCanvas() { async drawCanvas() {
super.drawCanvas(); super.drawCanvas();
const fill = {};
// parse each time to deal with a change in units if necessary // parse each time to deal with a change in units if necessary
const data = this.parseData(); const data = this.parseData();
this.context.drawImage(await this.backgroundImage, 0, 0); fill.temp = data.Temperature + String.fromCharCode(176);
draw.horizontalGradientSingle(this.context, 0, 30, 500, 90, draw.topColor1, draw.topColor2);
draw.triangle(this.context, 'rgb(28, 10, 87)', 500, 30, 450, 90, 500, 90);
draw.horizontalGradientSingle(this.context, 0, 90, 52, 399, draw.sideColor1, draw.sideColor2);
draw.horizontalGradientSingle(this.context, 584, 90, 640, 399, draw.sideColor1, draw.sideColor2);
draw.titleText(this.context, 'Current', 'Conditions');
draw.text(this.context, 'Star4000 Large', '24pt', '#FFFFFF', 170, 135, data.Temperature + String.fromCharCode(176), 2);
let Conditions = data.observations.textDescription; let Conditions = data.observations.textDescription;
if (Conditions.length > 15) { if (Conditions.length > 15) {
Conditions = this.shortConditions(Conditions); Conditions = this.shortConditions(Conditions);
} }
draw.text(this.context, 'Star4000 Extended', '24pt', '#FFFFFF', 195, 170, Conditions, 2, 'center'); fill.condition = Conditions;
draw.text(this.context, 'Star4000 Extended', '24pt', '#FFFFFF', 80, 330, 'Wind:', 2); fill.wind = data.WindDirection.padEnd(3, '') + data.WindSpeed.toString().padStart(3, ' ');
draw.text(this.context, 'Star4000 Extended', '24pt', '#FFFFFF', 300, 330, `${data.WindDirection} ${data.WindSpeed}`, 2, 'right'); if (data.WindGust) fill['wind-gusts'] = `Gusts to ${data.WindGust}`;
if (data.WindGust) draw.text(this.context, 'Star4000 Extended', '24pt', '#FFFFFF', 80, 375, `Gusts to ${data.WindGust}`, 2); fill.location = utils.string.locationCleanup(this.data.station.properties.name).substr(0, 20);
draw.text(this.context, 'Star4000 Large', 'bold 16pt', '#FFFF00', 315, 120, this.data.station.properties.name.substr(0, 20), 2); fill.humidity = `${data.Humidity}%`;
fill.dewpoint = data.DewPoint + String.fromCharCode(176);
draw.text(this.context, 'Star4000 Large', 'bold 16pt', '#FFFFFF', 340, 165, 'Humidity:', 2); fill.ceiling = (data.Ceiling === 0 ? 'Unlimited' : data.Ceiling + data.CeilingUnit);
draw.text(this.context, 'Star4000 Large', 'bold 16pt', '#FFFFFF', 560, 165, `${data.Humidity}%`, 2, 'right'); fill.visibility = data.Visibility + data.VisibilityUnit;
fill.pressure = `${data.Pressure} ${data.PressureDirection}`;
draw.text(this.context, 'Star4000 Large', 'bold 16pt', '#FFFFFF', 340, 205, 'Dewpoint:', 2);
draw.text(this.context, 'Star4000 Large', 'bold 16pt', '#FFFFFF', 560, 205, data.DewPoint + String.fromCharCode(176), 2, 'right');
draw.text(this.context, 'Star4000 Large', 'bold 16pt', '#FFFFFF', 340, 245, 'Ceiling:', 2);
draw.text(this.context, 'Star4000 Large', 'bold 16pt', '#FFFFFF', 560, 245, (data.Ceiling === '' ? 'Unlimited' : data.Ceiling + data.CeilingUnit), 2, 'right');
draw.text(this.context, 'Star4000 Large', 'bold 16pt', '#FFFFFF', 340, 285, 'Visibility:', 2);
draw.text(this.context, 'Star4000 Large', 'bold 16pt', '#FFFFFF', 560, 285, data.Visibility + data.VisibilityUnit, 2, 'right');
draw.text(this.context, 'Star4000 Large', 'bold 16pt', '#FFFFFF', 340, 325, 'Pressure:', 2);
draw.text(this.context, 'Star4000 Large', 'bold 16pt', '#FFFFFF', 535, 325, data.Pressure, 2, 'right');
switch (data.PressureDirection) {
case 'R':
// Shadow
draw.triangle(this.context, '#000000', 552, 302, 542, 312, 562, 312);
draw.box(this.context, '#000000', 549, 312, 6, 15);
// Border
draw.triangle(this.context, '#000000', 550, 300, 540, 310, 560, 310);
draw.box(this.context, '#000000', 547, 310, 6, 15);
// Fill
draw.triangle(this.context, '#FFFF00', 550, 301, 541, 309, 559, 309);
draw.box(this.context, '#FFFF00', 548, 309, 4, 15);
break;
case 'F':
// Shadow
draw.triangle(this.context, '#000000', 552, 327, 542, 317, 562, 317);
draw.box(this.context, '#000000', 549, 302, 6, 15);
// Border
draw.triangle(this.context, '#000000', 550, 325, 540, 315, 560, 315);
draw.box(this.context, '#000000', 547, 300, 6, 15);
// Fill
draw.triangle(this.context, '#FFFF00', 550, 324, 541, 314, 559, 314);
draw.box(this.context, '#FFFF00', 548, 301, 4, 15);
break;
default:
}
if (data.observations.heatIndex.value && data.HeatIndex !== data.Temperature) { if (data.observations.heatIndex.value && data.HeatIndex !== data.Temperature) {
draw.text(this.context, 'Star4000 Large', 'bold 16pt', '#FFFFFF', 340, 365, 'Heat Index:', 2); fill['heat-index-label'] = 'Heat Index:';
draw.text(this.context, 'Star4000 Large', 'bold 16pt', '#FFFFFF', 560, 365, data.HeatIndex + String.fromCharCode(176), 2, 'right'); fill['heat-index'] = data.HeatIndex + String.fromCharCode(176);
} else if (data.observations.windChill.value && data.WindChill !== '' && data.WindChill < data.Temperature) { } else if (data.observations.windChill.value && data.WindChill !== '' && data.WindChill < data.Temperature) {
draw.text(this.context, 'Star4000 Large', 'bold 16pt', '#FFFFFF', 340, 365, 'Wind Chill:', 2); fill['heat-index-label'] = 'Wind Chill:';
draw.text(this.context, 'Star4000 Large', 'bold 16pt', '#FFFFFF', 560, 365, data.WindChill + String.fromCharCode(176), 2, 'right'); fill['heat-index'] = data.WindChill + String.fromCharCode(176);
} }
// get main icon fill.icon = { type: 'img', src: data.Icon };
this.gifs.push(await utils.image.superGifAsync({
src: data.Icon, const area = this.elem.querySelector('.main');
auto_play: true,
canvas: this.canvas, area.innerHTML = '';
x: 140, area.append(this.fillTemplate('weather', fill));
y: 175,
max_width: 126,
}));
this.finishDraw(); this.finishDraw();
} }

View file

@ -1,4 +1,4 @@
/* globals draw, navigation */ /* globals navigation, utils */
// eslint-disable-next-line no-unused-vars // eslint-disable-next-line no-unused-vars
const currentWeatherScroll = (() => { const currentWeatherScroll = (() => {
@ -6,25 +6,13 @@ const currentWeatherScroll = (() => {
const degree = String.fromCharCode(176); const degree = String.fromCharCode(176);
// local variables // local variables
let context; // currently active context
let blankDrawArea; // original state of context
let interval; let interval;
let screenIndex = 0; let screenIndex = 0;
// start drawing conditions // start drawing conditions
// reset starts from the first item in the text scroll list // reset starts from the first item in the text scroll list
const start = (_context) => { const start = () => {
// see if there is a context available
if (!_context) return;
// store see if the context is new // store see if the context is new
if (_context !== context) {
// clean the outgoing context
cleanLastContext();
// store the new blank context
blankDrawArea = _context.getImageData(0, 405, 640, 75);
}
// store the context locally
context = _context;
// set up the interval if needed // set up the interval if needed
if (!interval) { if (!interval) {
@ -36,17 +24,10 @@ const currentWeatherScroll = (() => {
}; };
const stop = (reset) => { const stop = (reset) => {
cleanLastContext();
if (interval) interval = clearInterval(interval); if (interval) interval = clearInterval(interval);
if (reset) screenIndex = 0; if (reset) screenIndex = 0;
}; };
const cleanLastContext = () => {
if (blankDrawArea) context.putImageData(blankDrawArea, 0, 405);
blankDrawArea = undefined;
context = undefined;
};
// increment interval, roll over // increment interval, roll over
const incrementInterval = () => { const incrementInterval = () => {
screenIndex = (screenIndex + 1) % (screens.length); screenIndex = (screenIndex + 1) % (screens.length);
@ -61,16 +42,13 @@ const currentWeatherScroll = (() => {
// nothing to do if there's no data yet // nothing to do if there's no data yet
if (!data) return; if (!data) return;
// clean up any old text
context.putImageData(blankDrawArea, 0, 405);
drawCondition(screens[screenIndex](data)); drawCondition(screens[screenIndex](data));
}; };
// the "screens" are stored in an array for easy addition and removal // the "screens" are stored in an array for easy addition and removal
const screens = [ const screens = [
// station name // station name
(data) => `Conditions at ${data.station.properties.name.substr(0, 20)}`, (data) => `Conditions at ${utils.string.locationCleanup(data.station.properties.name).substr(0, 20)}`,
// temperature // temperature
(data) => { (data) => {
@ -109,7 +87,10 @@ const currentWeatherScroll = (() => {
// internal draw function with preset parameters // internal draw function with preset parameters
const drawCondition = (text) => { const drawCondition = (text) => {
draw.text(context, 'Star4000', '24pt', '#ffffff', 70, 430, text, 2); // update all html scroll elements
utils.elem.forEach('.weather-display .scroll .fixed', (elem) => {
elem.innerHTML = text;
});
}; };
// return the api // return the api

View file

@ -1,18 +1,15 @@
// display extended forecast graphically // display extended forecast graphically
// technically uses the same data as the local forecast, we'll let the browser do the caching of that // technically uses the same data as the local forecast, we'll let the browser do the caching of that
/* globals WeatherDisplay, utils, STATUS, UNITS, draw, icons, navigation, luxon */ /* globals WeatherDisplay, utils, STATUS, UNITS, icons, navigation, luxon */
// eslint-disable-next-line no-unused-vars // eslint-disable-next-line no-unused-vars
class ExtendedForecast extends WeatherDisplay { class ExtendedForecast extends WeatherDisplay {
constructor(navId, elemId) { constructor(navId, elemId) {
super(navId, elemId, 'Extended Forecast'); super(navId, elemId, 'Extended Forecast', true);
// set timings // set timings
this.timing.totalScreens = 2; this.timing.totalScreens = 2;
// pre-load background image (returns promise)
this.backgroundImage = utils.image.load('images/BackGround2_1.png');
} }
async getData(_weatherParameters) { async getData(_weatherParameters) {
@ -85,14 +82,18 @@ class ExtendedForecast extends WeatherDisplay {
} }
static shortenExtendedForecastText(long) { static shortenExtendedForecastText(long) {
let short = long; const regexList = [
short = short.replace(/ and /g, ' '); [/ and /ig, ' '],
short = short.replace(/Slight /g, ''); [/Slight /ig, ''],
short = short.replace(/Chance /g, ''); [/Chance /ig, ''],
short = short.replace(/Very /g, ''); [/Very /ig, ''],
short = short.replace(/Patchy /g, ''); [/Patchy /ig, ''],
short = short.replace(/Areas /g, ''); [/Areas /ig, ''],
short = short.replace(/Dense /g, ''); [/Dense /ig, ''],
[/Thunderstorm/g, 'T\'Storm'],
];
// run all regexes
const short = regexList.reduce((working, [regex, replace]) => working.replace(regex, replace), long);
let conditions = short.split(' '); let conditions = short.split(' ');
if (short.indexOf('then') !== -1) { if (short.indexOf('then') !== -1) {
@ -113,12 +114,12 @@ class ExtendedForecast extends WeatherDisplay {
short2 = ''; short2 = '';
} }
} }
short = short1; let result = short1;
if (short2 !== '') { if (short2 !== '') {
short += ` ${short2}`; result += ` ${short2}`;
} }
return [short, short1, short2]; return result;
} }
async drawCanvas() { async drawCanvas() {
@ -128,45 +129,32 @@ class ExtendedForecast extends WeatherDisplay {
// grab the first three or second set of three array elements // grab the first three or second set of three array elements
const forecast = this.data.slice(0 + 3 * this.screenIndex, 3 + this.screenIndex * 3); const forecast = this.data.slice(0 + 3 * this.screenIndex, 3 + this.screenIndex * 3);
const backgroundImage = await this.backgroundImage; // create each day template
const days = forecast.map((Day) => {
const fill = {};
fill.date = Day.dayName;
this.context.drawImage(backgroundImage, 0, 0);
draw.horizontalGradientSingle(this.context, 0, 30, 500, 90, draw.topColor1, draw.topColor2);
draw.triangle(this.context, 'rgb(28, 10, 87)', 500, 30, 450, 90, 500, 90);
draw.horizontalGradientSingle(this.context, 0, 90, 640, 399, draw.sideColor1, draw.sideColor2);
this.context.drawImage(backgroundImage, 38, 100, 174, 297, 38, 100, 174, 297);
this.context.drawImage(backgroundImage, 232, 100, 174, 297, 232, 100, 174, 297);
this.context.drawImage(backgroundImage, 426, 100, 174, 297, 426, 100, 174, 297);
draw.titleText(this.context, 'Extended', 'Forecast');
await Promise.all(forecast.map(async (Day, Index) => {
const offset = Index * 195;
draw.text(this.context, 'Star4000', '24pt', '#FFFF00', 100 + offset, 135, Day.dayName.toUpperCase(), 2);
draw.text(this.context, 'Star4000', '24pt', '#8080FF', 85 + offset, 345, 'Lo', 2, 'center');
draw.text(this.context, 'Star4000', '24pt', '#FFFF00', 165 + offset, 345, 'Hi', 2, 'center');
let { low } = Day; let { low } = Day;
if (low !== undefined) { if (low !== undefined) {
if (navigation.units() === UNITS.metric) low = utils.units.fahrenheitToCelsius(low); if (navigation.units() === UNITS.metric) low = utils.units.fahrenheitToCelsius(low);
draw.text(this.context, 'Star4000 Large', '24pt', '#FFFFFF', 85 + offset, 385, low, 2, 'center'); fill['value-lo'] = Math.round(low);
} }
let { high } = Day; let { high } = Day;
if (navigation.units() === UNITS.metric) high = utils.units.fahrenheitToCelsius(high); if (navigation.units() === UNITS.metric) high = utils.units.fahrenheitToCelsius(high);
draw.text(this.context, 'Star4000 Large', '24pt', '#FFFFFF', 165 + offset, 385, high, 2, 'center'); fill['value-hi'] = Math.round(high);
draw.text(this.context, 'Star4000', '24pt', '#FFFFFF', 120 + offset, 270, Day.text[1], 2, 'center'); fill.condition = Day.text;
draw.text(this.context, 'Star4000', '24pt', '#FFFFFF', 120 + offset, 310, Day.text[2], 2, 'center');
// draw the icon // draw the icon
this.gifs.push(await utils.image.superGifAsync({ fill.icon = { type: 'img', src: Day.icon };
src: Day.icon,
auto_play: true,
canvas: this.canvas,
x: 70 + Index * 195,
y: 150,
max_height: 75,
}));
}));
// return the filled template
return this.fillTemplate('day', fill);
});
// empty and update the container
const dayContainer = this.elem.querySelector('.day-container');
dayContainer.innerHTML = '';
dayContainer.append(...days);
this.finishDraw(); this.finishDraw();
} }
} }

View file

@ -1,22 +1,17 @@
// hourly forecast list // hourly forecast list
/* globals WeatherDisplay, utils, STATUS, UNITS, draw, navigation, icons, luxon */ /* globals WeatherDisplay, utils, STATUS, UNITS, navigation, icons, luxon */
// eslint-disable-next-line no-unused-vars // eslint-disable-next-line no-unused-vars
class Hourly extends WeatherDisplay { class Hourly extends WeatherDisplay {
constructor(navId, elemId, defaultActive) { constructor(navId, elemId, defaultActive) {
// special height and width for scrolling // special height and width for scrolling
super(navId, elemId, 'Hourly Forecast', defaultActive); super(navId, elemId, 'Hourly Forecast', defaultActive);
// pre-load background image (returns promise)
this.backgroundImage = utils.image.load('images/BackGround6_1.png');
// height of one hour in the forecast
this.hourHeight = 72;
// set up the timing // set up the timing
this.timing.baseDelay = 20; this.timing.baseDelay = 20;
// 24 hours = 6 pages // 24 hours = 6 pages
const pages = 4; // first page is already displayed, last page doesn't happen const pages = 4; // first page is already displayed, last page doesn't happen
const timingStep = this.hourHeight * 4; const timingStep = 75 * 4;
this.timing.delay = [150 + timingStep]; this.timing.delay = [150 + timingStep];
// add additional pages // add additional pages
for (let i = 0; i < pages; i += 1) this.timing.delay.push(timingStep); for (let i = 0; i < pages; i += 1) this.timing.delay.push(timingStep);
@ -35,6 +30,7 @@ class Hourly extends WeatherDisplay {
console.error('Get hourly forecast failed'); console.error('Get hourly forecast failed');
console.error(e.status, e.responseJSON); console.error(e.status, e.responseJSON);
this.setStatus(STATUS.failed); this.setStatus(STATUS.failed);
return;
} }
this.data = await Hourly.parseForecast(forecast.properties); this.data = await Hourly.parseForecast(forecast.properties);
@ -114,52 +110,26 @@ class Hourly extends WeatherDisplay {
} }
async drawLongCanvas() { async drawLongCanvas() {
// create the "long" canvas if necessary // get the list element and populate
if (!this.longCanvas) { const list = this.elem.querySelector('.hourly-lines');
this.longCanvas = document.createElement('canvas'); list.innerHTML = '';
this.longCanvas.width = 640;
this.longCanvas.height = 24 * this.hourHeight;
this.longContext = this.longCanvas.getContext('2d');
this.longCanvasGifs = [];
}
// stop all gifs
this.longCanvasGifs.forEach((gif) => gif.pause());
// delete the gifs
this.longCanvasGifs.length = 0;
// clean up existing gifs
this.gifs.forEach((gif) => gif.pause());
// delete the gifs
this.gifs.length = 0;
this.longContext.clearRect(0, 0, this.longCanvas.width, this.longCanvas.height);
// draw the "long" canvas with all cities
draw.box(this.longContext, 'rgb(35, 50, 112)', 0, 0, 640, 24 * this.hourHeight);
for (let i = 0; i <= 4; i += 1) {
const y = i * 346;
draw.horizontalGradient(this.longContext, 0, y, 640, y + 346, '#102080', '#001040');
}
const startingHour = luxon.DateTime.local(); const startingHour = luxon.DateTime.local();
await Promise.all(this.data.map(async (data, index) => { const lines = this.data.map((data, index) => {
// calculate base y value const fillValues = {};
const y = 50 + this.hourHeight * index;
// hour // hour
const hour = startingHour.plus({ hours: index }); const hour = startingHour.plus({ hours: index });
const formattedHour = hour.toLocaleString({ weekday: 'short', hour: 'numeric' }); const formattedHour = hour.toLocaleString({ weekday: 'short', hour: 'numeric' });
draw.text(this.longContext, 'Star4000 Large Compressed', '24pt', '#FFFF00', 80, y, formattedHour, 2); fillValues.hour = formattedHour;
// temperatures, convert to strings with no decimal // temperatures, convert to strings with no decimal
const temperature = Math.round(data.temperature).toString().padStart(3); const temperature = Math.round(data.temperature).toString().padStart(3);
const feelsLike = Math.round(data.apparentTemperature).toString().padStart(3); const feelsLike = Math.round(data.apparentTemperature).toString().padStart(3);
draw.text(this.longContext, 'Star4000 Large', '24pt', '#FFFF00', 390, y, temperature, 2, 'center'); fillValues.temp = temperature;
// only plot apparent temperature if there is a difference // only plot apparent temperature if there is a difference
if (temperature !== feelsLike) draw.text(this.longContext, 'Star4000 Large', '24pt', '#FFFF00', 470, y, feelsLike, 2, 'center'); // if (temperature !== feelsLike) line.querySelector('.like').innerHTML = feelsLike;
if (temperature !== feelsLike) fillValues.like = feelsLike;
// wind // wind
let wind = 'Calm'; let wind = 'Calm';
@ -167,44 +137,25 @@ class Hourly extends WeatherDisplay {
const windSpeed = Math.round(data.windSpeed).toString(); const windSpeed = Math.round(data.windSpeed).toString();
wind = data.windDirection + (Array(6 - data.windDirection.length - windSpeed.length).join(' ')) + windSpeed; wind = data.windDirection + (Array(6 - data.windDirection.length - windSpeed.length).join(' ')) + windSpeed;
} }
draw.text(this.longContext, 'Star4000 Large', '24pt', '#FFFF00', 580, y, wind, 2, 'center'); fillValues.wind = wind;
this.longCanvasGifs.push(await utils.image.superGifAsync({ // image
src: data.icon, fillValues.icon = { type: 'img', src: data.icon };
auto_play: true,
canvas: this.longCanvas, return this.fillTemplate('hourly-row', fillValues);
x: 290, });
y: y - 35,
max_width: 47, list.append(...lines);
}));
}));
} }
async drawCanvas() { drawCanvas() {
// there are technically 2 canvases: the standard canvas and the extra-long canvas that contains the complete
// list of cities. The second canvas is copied into the standard canvas to create the scroll
super.drawCanvas(); super.drawCanvas();
// draw the standard context
this.context.drawImage(await this.backgroundImage, 0, 0);
draw.horizontalGradientSingle(this.context, 0, 30, 500, 90, draw.topColor1, draw.topColor2);
draw.triangle(this.context, 'rgb(28, 10, 87)', 500, 30, 450, 90, 500, 90);
draw.titleText(this.context, 'Hourly Forecast');
draw.text(this.context, 'Star4000 Small', '24pt', '#FFFF00', 390, 105, 'TEMP', 2, 'center');
draw.text(this.context, 'Star4000 Small', '24pt', '#FFFF00', 470, 105, 'LIKE', 2, 'center');
draw.text(this.context, 'Star4000 Small', '24pt', '#FFFF00', 580, 105, 'WIND', 2, 'center');
// copy the scrolled portion of the canvas for the initial run before the scrolling starts
this.context.drawImage(this.longCanvas, 0, 0, 640, 289, 0, 110, 640, 289);
this.finishDraw(); this.finishDraw();
} }
async showCanvas() { showCanvas() {
// special to travel forecast to draw the remainder of the canvas // special to hourly to draw the remainder of the canvas
await this.drawCanvas(); this.drawCanvas();
super.showCanvas(); super.showCanvas();
} }
@ -215,17 +166,14 @@ class Hourly extends WeatherDisplay {
// base count change callback // base count change callback
baseCountChange(count) { baseCountChange(count) {
// get a fresh canvas
const longCanvas = this.getLongCanvas();
// calculate scroll offset and don't go past end // calculate scroll offset and don't go past end
let offsetY = Math.min(longCanvas.height - 289, (count - 150)); let offsetY = Math.min(this.elem.querySelector('.hourly-lines').getBoundingClientRect().height - 289, (count - 150));
// don't let offset go negative // don't let offset go negative
if (offsetY < 0) offsetY = 0; if (offsetY < 0) offsetY = 0;
// copy the scrolled portion of the canvas // copy the scrolled portion of the canvas
this.context.drawImage(longCanvas, 0, offsetY, 640, 289, 0, 110, 640, 289); this.elem.querySelector('.main').scrollTo(0, offsetY);
} }
static getTravelCitiesDayName(cities) { static getTravelCitiesDayName(cities) {
@ -241,9 +189,4 @@ class Hourly extends WeatherDisplay {
return dayName; return dayName;
}, ''); }, '');
} }
// necessary to get the lastest long canvas when scrolling
getLongCanvas() {
return this.longCanvas;
}
} }

View file

@ -29,6 +29,7 @@ const icons = (() => {
case 'skc-n': case 'skc-n':
case 'nskc': case 'nskc':
case 'nskc-n': case 'nskc-n':
case 'cold-n':
return addPath('Clear-1992.gif'); return addPath('Clear-1992.gif');
case 'bkn': case 'bkn':
@ -135,6 +136,9 @@ const icons = (() => {
case 'blizzard': case 'blizzard':
return addPath('Blowing Snow.gif'); return addPath('Blowing Snow.gif');
case 'cold':
return addPath('cold.gif');
default: default:
console.log(`Unable to locate regional icon for ${conditionName} ${link} ${isNightTime}`); console.log(`Unable to locate regional icon for ${conditionName} ${link} ${isNightTime}`);
return false; return false;
@ -142,6 +146,8 @@ const icons = (() => {
}; };
const getWeatherIconFromIconLink = (link, _isNightTime) => { const getWeatherIconFromIconLink = (link, _isNightTime) => {
if (!link) return false;
// internal function to add path to returned icon // internal function to add path to returned icon
const addPath = (icon) => `images/${icon}`; const addPath = (icon) => `images/${icon}`;
// extract day or night if not provided // extract day or night if not provided
@ -164,11 +170,13 @@ const icons = (() => {
case 'skc': case 'skc':
case 'hot': case 'hot':
case 'haze': case 'haze':
case 'cold':
return addPath('CC_Clear1.gif'); return addPath('CC_Clear1.gif');
case 'skc-n': case 'skc-n':
case 'nskc': case 'nskc':
case 'nskc-n': case 'nskc-n':
case 'cold-n':
return addPath('CC_Clear0.gif'); return addPath('CC_Clear0.gif');
case 'sct': case 'sct':

View file

@ -1,12 +1,10 @@
// current weather conditions display // current weather conditions display
/* globals WeatherDisplay, utils, STATUS, UNITS, draw, navigation, StationInfo */ /* globals WeatherDisplay, utils, STATUS, UNITS, navigation, StationInfo */
// eslint-disable-next-line no-unused-vars // eslint-disable-next-line no-unused-vars
class LatestObservations extends WeatherDisplay { class LatestObservations extends WeatherDisplay {
constructor(navId, elemId) { constructor(navId, elemId) {
super(navId, elemId, 'Latest Observations'); super(navId, elemId, 'Latest Observations', true);
// pre-load background image (returns promise)
this.backgroundImage = utils.image.load('images/BackGround1_1.png');
// constants // constants
this.MaximumRegionalStations = 7; this.MaximumRegionalStations = 7;
@ -67,25 +65,15 @@ class LatestObservations extends WeatherDisplay {
// sort array by station name // sort array by station name
const sortedConditions = conditions.sort((a, b) => ((a.Name < b.Name) ? -1 : 1)); const sortedConditions = conditions.sort((a, b) => ((a.Name < b.Name) ? -1 : 1));
this.context.drawImage(await this.backgroundImage, 0, 0);
draw.horizontalGradientSingle(this.context, 0, 30, 500, 90, draw.topColor1, draw.topColor2);
draw.triangle(this.context, 'rgb(28, 10, 87)', 500, 30, 450, 90, 500, 90);
draw.horizontalGradientSingle(this.context, 0, 90, 52, 399, draw.sideColor1, draw.sideColor2);
draw.horizontalGradientSingle(this.context, 584, 90, 640, 399, draw.sideColor1, draw.sideColor2);
draw.titleText(this.context, 'Latest', 'Observations');
if (navigation.units() === UNITS.english) { if (navigation.units() === UNITS.english) {
draw.text(this.context, 'Star4000 Small', '24pt', '#FFFFFF', 295, 105, `${String.fromCharCode(176)}F`, 2); this.elem.querySelector('.column-headers .temp.english').classList.add('show');
this.elem.querySelector('.column-headers .temp.metric').classList.remove('show');
} else { } else {
draw.text(this.context, 'Star4000 Small', '24pt', '#FFFFFF', 295, 105, `${String.fromCharCode(176)}C`, 2); this.elem.querySelector('.column-headers .temp.english').classList.remove('show');
this.elem.querySelector('.column-headers .temp.metric').classList.add('show');
} }
draw.text(this.context, 'Star4000 Small', '24pt', '#FFFFFF', 345, 105, 'WEATHER', 2);
draw.text(this.context, 'Star4000 Small', '24pt', '#FFFFFF', 495, 105, 'WIND', 2);
let y = 140; const lines = sortedConditions.map((condition) => {
sortedConditions.forEach((condition) => {
let Temperature = condition.temperature.value; let Temperature = condition.temperature.value;
let WindSpeed = condition.windSpeed.value; let WindSpeed = condition.windSpeed.value;
const windDirection = utils.calc.directionToNSEW(condition.windDirection.value); const windDirection = utils.calc.directionToNSEW(condition.windDirection.value);
@ -94,23 +82,28 @@ class LatestObservations extends WeatherDisplay {
Temperature = utils.units.celsiusToFahrenheit(Temperature); Temperature = utils.units.celsiusToFahrenheit(Temperature);
WindSpeed = utils.units.kphToMph(WindSpeed); WindSpeed = utils.units.kphToMph(WindSpeed);
} }
WindSpeed = Math.round(WindSpeed);
Temperature = Math.round(Temperature);
draw.text(this.context, 'Star4000', '24pt', '#FFFFFF', 65, y, condition.city.substr(0, 14), 2); const fill = {};
draw.text(this.context, 'Star4000', '24pt', '#FFFFFF', 345, y, LatestObservations.shortenCurrentConditions(condition.textDescription).substr(0, 9), 2); fill.location = utils.string.locationCleanup(condition.city).substr(0, 14);
fill.temp = Temperature;
fill.weather = LatestObservations.shortenCurrentConditions(condition.textDescription).substr(0, 9);
if (WindSpeed > 0) { if (WindSpeed > 0) {
draw.text(this.context, 'Star4000', '24pt', '#FFFFFF', 495, y, windDirection + (Array(6 - windDirection.length - WindSpeed.toString().length).join(' ')) + WindSpeed.toString(), 2); fill.wind = windDirection + (Array(6 - windDirection.length - WindSpeed.toString().length).join(' ')) + WindSpeed.toString();
} else if (WindSpeed === 'NA') { } else if (WindSpeed === 'NA') {
draw.text(this.context, 'Star4000', '24pt', '#FFFFFF', 495, y, 'NA', 2); fill.wind = 'NA';
} else { } else {
draw.text(this.context, 'Star4000', '24pt', '#FFFFFF', 495, y, 'Calm', 2); fill.wind = 'Calm';
} }
const x = (325 - (Temperature.toString().length * 15)); return this.fillTemplate('observation-row', fill);
draw.text(this.context, 'Star4000', '24pt', '#FFFFFF', x, y, Temperature, 2);
y += 40;
}); });
const linesContainer = this.elem.querySelector('.observation-lines');
linesContainer.innerHTML = '';
linesContainer.append(...lines);
this.finishDraw(); this.finishDraw();
} }

View file

@ -1,17 +1,14 @@
// display text based local forecast // display text based local forecast
/* globals WeatherDisplay, utils, STATUS, UNITS, draw, navigation */ /* globals WeatherDisplay, utils, STATUS, UNITS, navigation */
// eslint-disable-next-line no-unused-vars // eslint-disable-next-line no-unused-vars
class LocalForecast extends WeatherDisplay { class LocalForecast extends WeatherDisplay {
constructor(navId, elemId) { constructor(navId, elemId) {
super(navId, elemId, 'Local Forecast'); super(navId, elemId, 'Local Forecast', true);
// set timings // set timings
this.timing.baseDelay = 5000; this.timing.baseDelay = 5000;
// pre-load background image (returns promise)
this.backgroundImage = utils.image.load('images/BackGround1_1.png');
} }
async getData(_weatherParameters) { async getData(_weatherParameters) {
@ -28,14 +25,8 @@ class LocalForecast extends WeatherDisplay {
// parse raw data // parse raw data
const conditions = LocalForecast.parse(rawData); const conditions = LocalForecast.parse(rawData);
// split this forecast into the correct number of screens
const maxRows = 7;
const maxCols = 32;
this.screenTexts = [];
// read each text // read each text
conditions.forEach((condition) => { this.screenTexts = conditions.map((condition) => {
// process the text // process the text
let text = `${condition.DayName.toUpperCase()}...`; let text = `${condition.DayName.toUpperCase()}...`;
let conditionText = condition.Text; let conditionText = condition.Text;
@ -44,44 +35,23 @@ class LocalForecast extends WeatherDisplay {
} }
text += conditionText.toUpperCase().replace('...', ' '); text += conditionText.toUpperCase().replace('...', ' ');
text = utils.string.wordWrap(text, maxCols, '\n'); return text;
const lines = text.split('\n');
const lineCount = lines.length;
let ScreenText = '';
const maxRowCount = maxRows;
let rowCount = 0;
// if (PrependAlert) {
// ScreenText = LocalForecastScreenTexts[LocalForecastScreenTexts.length - 1];
// rowCount = ScreenText.split('\n').length - 1;
// }
for (let i = 0; i <= lineCount - 1; i += 1) {
if (lines[i] !== '') {
if (rowCount > maxRowCount - 1) {
// if (PrependAlert) {
// LocalForecastScreenTexts[LocalForecastScreenTexts.length - 1] = ScreenText;
// PrependAlert = false;
// } else {
this.screenTexts.push(ScreenText);
// }
ScreenText = '';
rowCount = 0;
}
ScreenText += `${lines[i]}\n`;
rowCount += 1;
}
}
// if (PrependAlert) {
// this.screenTexts[this.screenTexts.length - 1] = ScreenText;
// PrependAlert = false;
// } else {
this.screenTexts.push(ScreenText);
// }
}); });
this.timing.totalScreens = this.screenTexts.length; // fill the forecast texts
const templates = this.screenTexts.map((text) => this.fillTemplate('forecast', { text }));
const forecastsElem = this.elem.querySelector('.forecasts');
forecastsElem.innerHTML = '';
forecastsElem.append(...templates);
// increase each forecast height to a multiple of container height
this.pageHeight = forecastsElem.parentNode.getBoundingClientRect().height;
templates.forEach((forecast) => {
const newHeight = Math.ceil(forecast.scrollHeight / this.pageHeight) * this.pageHeight;
forecast.style.height = `${newHeight}px`;
});
this.timing.totalScreens = forecastsElem.scrollHeight / this.pageHeight;
this.calcNavTiming(); this.calcNavTiming();
this.setStatus(STATUS.loaded); this.setStatus(STATUS.loaded);
} }
@ -105,25 +75,12 @@ class LocalForecast extends WeatherDisplay {
} }
} }
// TODO: alerts needs a cleanup
// TODO: second page of screenTexts when needed
async drawCanvas() { async drawCanvas() {
super.drawCanvas(); super.drawCanvas();
this.context.drawImage(await this.backgroundImage, 0, 0); const top = -this.screenIndex * this.pageHeight;
draw.horizontalGradientSingle(this.context, 0, 30, 500, 90, draw.topColor1, draw.topColor2); this.elem.querySelector('.forecasts').style.top = `${top}px`;
draw.triangle(this.context, 'rgb(28, 10, 87)', 500, 30, 450, 90, 500, 90);
draw.horizontalGradientSingle(this.context, 0, 90, 52, 399, draw.sideColor1, draw.sideColor2);
draw.horizontalGradientSingle(this.context, 584, 90, 640, 399, draw.sideColor1, draw.sideColor2);
draw.titleText(this.context, 'Local ', 'Forecast');
// clear existing text
draw.box(this.context, 'rgb(33, 40, 90)', 65, 105, 505, 280);
// Draw the text.
this.screenTexts[this.screenIndex].split('\n').forEach((text, index) => {
draw.text(this.context, 'Star4000', '24pt', '#FFFFFF', 75, 140 + 40 * index, text, 2);
});
this.finishDraw(); this.finishDraw();
} }

View file

@ -23,7 +23,8 @@ const navigation = (() => {
let almanac; let almanac;
const init = async () => { const init = async () => {
// nothing to do // set up resize handler
window.addEventListener('resize', resize);
}; };
const message = (data) => { const message = (data) => {
@ -87,22 +88,22 @@ const navigation = (() => {
// draw the progress canvas and hide others // draw the progress canvas and hide others
hideAllCanvases(); hideAllCanvases();
document.getElementById('loading').style.display = 'none'; document.getElementById('loading').style.display = 'none';
progress = new Progress(-1, 'progress'); if (!progress) progress = new Progress(-1, 'progress');
await progress.drawCanvas(); await progress.drawCanvas();
progress.showCanvas(); progress.showCanvas();
// start loading canvases if necessary // start loading canvases if necessary
if (displays.length === 0) { if (displays.length === 0) {
currentWeather = new CurrentWeather(0, 'currentWeather'); currentWeather = new CurrentWeather(0, 'current-weather');
almanac = new Almanac(7, 'almanac'); almanac = new Almanac(7, 'almanac');
displays = [ displays = [
currentWeather, currentWeather,
new LatestObservations(1, 'latestObservations'), new LatestObservations(1, 'latest-observations'),
new Hourly(2, 'hourly'), new Hourly(2, 'hourly'),
new TravelForecast(3, 'travelForecast', false), // not active by default new TravelForecast(3, 'travel', false), // not active by default
new RegionalForecast(4, 'regionalForecast'), new RegionalForecast(4, 'regional-forecast'),
new LocalForecast(5, 'localForecast'), new LocalForecast(5, 'local-forecast'),
new ExtendedForecast(6, 'extendedForecast'), new ExtendedForecast(6, 'extended-forecast'),
almanac, almanac,
new Radar(8, 'radar'), new Radar(8, 'radar'),
]; ];
@ -177,7 +178,15 @@ const navigation = (() => {
progress.hideCanvas(); progress.hideCanvas();
if (!current) { if (!current) {
// special case for no active displays (typically on progress screen) // special case for no active displays (typically on progress screen)
displays[0].navNext(msg.command.firstFrame); // 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; return;
} }
if (direction === msg.command.nextFrame) currentDisplay().navNext(); if (direction === msg.command.nextFrame) currentDisplay().navNext();
@ -266,6 +275,25 @@ const navigation = (() => {
return almanac.getSun(); 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 { return {
init, init,
message, message,
@ -277,5 +305,7 @@ const navigation = (() => {
getDisplay, getDisplay,
getCurrentWeather, getCurrentWeather,
getSun, getSun,
resize,
resetStatuses,
}; };
})(); })();

View file

@ -1,11 +1,11 @@
// regional forecast and observations // regional forecast and observations
/* globals WeatherDisplay, utils, STATUS, draw, navigation */ /* globals WeatherDisplay, utils, STATUS, navigation */
// eslint-disable-next-line no-unused-vars // eslint-disable-next-line no-unused-vars
class Progress extends WeatherDisplay { class Progress extends WeatherDisplay {
constructor(navId, elemId) { constructor(navId, elemId) {
super(navId, elemId); super(navId, elemId, '', false);
// pre-load background image (returns promise) // pre-load background image (returns promise)
this.backgroundImage = utils.image.load('images/BackGround1_1.png'); this.backgroundImage = utils.image.load('images/BackGround1_1.png');
@ -14,101 +14,90 @@ class Progress extends WeatherDisplay {
this.timing = false; this.timing = false;
this.version = document.getElementById('version').innerHTML; this.version = document.getElementById('version').innerHTML;
// setup event listener
this.elem.querySelector('.container').addEventListener('click', this.lineClick.bind(this));
} }
async drawCanvas(displays, loadedCount) { async drawCanvas(displays, loadedCount) {
super.drawCanvas(); super.drawCanvas();
// set up an event listener
if (!this.eventListener) {
this.eventListener = true;
this.canvas.addEventListener('click', (e) => this.canvasClick(e), false);
}
// get the background image // get the progress bar cover (makes percentage)
const backgroundImage = await this.backgroundImage; if (!this.progressCover) this.progressCover = this.elem.querySelector('.scroll .cover');
// only draw the background once
if (!this.backgroundDrawn) {
this.context.drawImage(backgroundImage, 0, 0, 640, 480, 0, 0, 640, 480);
draw.horizontalGradientSingle(this.context, 0, 90, 52, 399, draw.sideColor1, draw.sideColor2);
draw.horizontalGradientSingle(this.context, 584, 90, 640, 399, draw.sideColor1, draw.sideColor2);
draw.horizontalGradientSingle(this.context, 0, 30, 500, 90, draw.topColor1, draw.topColor2);
draw.triangle(this.context, 'rgb(28, 10, 87)', 500, 30, 450, 90, 500, 90);
draw.titleText(this.context, 'WeatherStar', `4000+ ${this.version}`);
}
this.finishDraw();
// if no displays provided just draw the backgrounds (above) // if no displays provided just draw the backgrounds (above)
if (!displays) return; if (!displays) return;
displays.forEach((display, idx) => { const lines = displays.map((display, index) => {
const y = 120 + idx * 29; const fill = {};
const dots = Array(120 - Math.floor(display.name.length * 2.5)).join('.');
draw.text(this.context, 'Star4000 Extended', '19pt', '#ffffff', 70, y, display.name + dots, 2);
let statusText; fill.name = display.name;
let statusColor;
let statusClass;
switch (display.status) { switch (display.status) {
case STATUS.loading: case STATUS.loading:
statusText = 'Loading'; statusClass = 'loading';
statusColor = '#ffff00';
break; break;
case STATUS.loaded: case STATUS.loaded:
statusText = 'Press Here'; statusClass = 'press-here';
statusColor = '#00ff00';
this.context.drawImage(backgroundImage, 440, y - 20, 75, 25, 440, y - 20, 75, 25);
break; break;
case STATUS.failed: case STATUS.failed:
statusText = 'Failed'; statusClass = 'failed';
statusColor = '#ff0000';
break; break;
case STATUS.noData: case STATUS.noData:
statusText = 'No Data'; statusClass = 'no-data';
statusColor = '#C0C0C0';
draw.box(this.context, 'rgb(33, 40, 90)', 475, y - 15, 75, 15);
break; break;
case STATUS.disabled: case STATUS.disabled:
statusText = 'Disabled'; statusClass = 'disabled';
statusColor = '#C0C0C0';
this.context.drawImage(backgroundImage, 470, y - 20, 45, 25, 470, y - 20, 45, 25);
break; break;
default: default:
} }
// Erase any dots that spill into the status text.
this.context.drawImage(backgroundImage, 475, y - 20, 165, 30, 475, y - 20, 165, 30); // make the line
draw.text(this.context, 'Star4000 Extended', '19pt', statusColor, 565, y, statusText, 2, 'end'); const line = this.fillTemplate('item', fill);
}); // because of timing, this might get called before the template is loaded
if (!line) return false;
// update the status
const links = line.querySelector('.links');
links.classList.remove('loading');
links.classList.add(statusClass);
links.dataset.index = index;
return line;
}).filter((d) => d);
// get the container and update
const container = this.elem.querySelector('.container');
container.innerHTML = '';
container.append(...lines);
this.finishDraw();
// calculate loaded percent // calculate loaded percent
const loadedPercent = (loadedCount / displays.length); const loadedPercent = (loadedCount / displays.length);
this.progressCover.style.width = `${(1.0 - loadedPercent) * 100}%`;
if (loadedPercent < 1.0) { if (loadedPercent < 1.0) {
// Draw a box for the progress. // show the progress bar and set width
draw.box(this.context, '#000000', 51, 428, 534, 22); this.progressCover.parentNode.classList.add('show');
draw.box(this.context, '#ffffff', 53, 430, 530, 18);
// update the progress gif
draw.box(this.context, '#1d7fff', 55, 432, 526 * loadedPercent, 14);
} else { } else {
// restore the background // hide the progressbar after 1 second (lines up with with width transition animation)
this.context.drawImage(backgroundImage, 51, 428, 534, 22, 51, 428, 534, 22); setTimeout(() => this.progressCover.parentNode.classList.remove('show'), 1000);
} }
} }
canvasClick(e) { lineClick(e) {
const x = e.offsetX; // get index
const y = e.offsetY; const indexRaw = e.target?.parentNode?.dataset?.index;
// eliminate off canvas and outside area clicks if (indexRaw === undefined) return;
if (!this.isActive()) return; const index = +indexRaw;
if (y < 100 || y > 410) return;
if (x < 440 || x > 570) return;
// stop playing // stop playing
navigation.message('navButton'); navigation.message('navButton');
// use the y value to determine an index // use the y value to determine an index
const index = Math.floor((y - 100) / 29);
const display = navigation.getDisplay(index); const display = navigation.getDisplay(index);
if (display && display.status === STATUS.loaded) { if (display && display.status === STATUS.loaded) {
display.showCanvas(navigation.msg.command.firstFrame); display.showCanvas(navigation.msg.command.firstFrame);
this.hideCanvas(); this.elem.classList.remove('show');
} }
} }
} }

View file

@ -1,10 +1,10 @@
// current weather conditions display // current weather conditions display
/* globals WeatherDisplay, utils, STATUS, draw, luxon */ /* globals WeatherDisplay, utils, STATUS, luxon */
// eslint-disable-next-line no-unused-vars // eslint-disable-next-line no-unused-vars
class Radar extends WeatherDisplay { class Radar extends WeatherDisplay {
constructor(navId, elemId) { constructor(navId, elemId) {
super(navId, elemId, 'Local Radar'); super(navId, elemId, 'Local Radar', true);
// set max images // set max images
this.dopplerRadarImageMax = 6; this.dopplerRadarImageMax = 6;
@ -31,9 +31,6 @@ class Radar extends WeatherDisplay {
{ time: 1, si: 4 }, { time: 1, si: 4 },
{ time: 12, si: 5 }, { time: 12, si: 5 },
]; ];
// pre-load background image (returns promise)
this.backgroundImage = utils.image.load('images/BackGround4_1.png');
} }
async getData(_weatherParameters) { async getData(_weatherParameters) {
@ -163,7 +160,6 @@ class Radar extends WeatherDisplay {
const imgBlob = await utils.image.load(blob); const imgBlob = await utils.image.load(blob);
// draw the entire image // draw the entire image
workingContext.clearRect(0, 0, width, 1600); workingContext.clearRect(0, 0, width, 1600);
workingContext.drawImage(imgBlob, 0, 0, width, 1600); workingContext.drawImage(imgBlob, 0, 0, width, 1600);
@ -174,7 +170,7 @@ class Radar extends WeatherDisplay {
const cropCanvas = document.createElement('canvas'); const cropCanvas = document.createElement('canvas');
cropCanvas.width = 640; cropCanvas.width = 640;
cropCanvas.height = 367; cropCanvas.height = 367;
const cropContext = cropCanvas.getContext('2d'); const cropContext = cropCanvas.getContext('2d', { willReadFrequently: true });
cropContext.imageSmoothingEnabled = false; cropContext.imageSmoothingEnabled = false;
cropContext.drawImage(workingCanvas, radarSourceX, radarSourceY, (radarOffsetX * 2), (radarOffsetY * 2.33), 0, 0, 640, 367); cropContext.drawImage(workingCanvas, radarSourceX, radarSourceY, (radarOffsetX * 2), (radarOffsetY * 2.33), 0, 0, 640, 367);
// clean the image // clean the image
@ -183,11 +179,20 @@ class Radar extends WeatherDisplay {
// merge the radar and map // merge the radar and map
Radar.mergeDopplerRadarImage(context, cropContext); Radar.mergeDopplerRadarImage(context, cropContext);
const elem = this.fillTemplate('frame', { map: { type: 'img', src: canvas.toDataURL() } });
return { return {
canvas, canvas,
time, time,
elem,
}; };
})); }));
// put the elements in the container
const scrollArea = this.elem.querySelector('.scroll-area');
scrollArea.innerHTML = '';
scrollArea.append(...radarInfo.map((r) => r.elem));
// set max length // set max length
this.timing.totalScreens = radarInfo.length; this.timing.totalScreens = radarInfo.length;
// store the images // store the images
@ -199,31 +204,13 @@ class Radar extends WeatherDisplay {
async drawCanvas() { async drawCanvas() {
super.drawCanvas(); super.drawCanvas();
if (this.screenIndex === -1) return;
this.context.drawImage(await this.backgroundImage, 0, 0);
const { DateTime } = luxon; const { DateTime } = luxon;
// Title const time = this.times[this.screenIndex].toLocaleString(DateTime.TIME_SIMPLE);
draw.text(this.context, 'Arial', 'bold 28pt', '#ffffff', 155, 60, 'Local', 2); const timePadded = time.length >= 8 ? time : `&nbsp;${time}`;
draw.text(this.context, 'Arial', 'bold 28pt', '#ffffff', 155, 95, 'Radar', 2); this.elem.querySelector('.header .right .time').innerHTML = timePadded;
draw.text(this.context, 'Star4000', 'bold 18pt', '#ffffff', 438, 49, 'PRECIP', 2, 'center'); // scroll to image
draw.text(this.context, 'Star4000', 'bold 18pt', '#ffffff', 298, 73, 'Light', 2); this.elem.querySelector('.scroll-area').style.top = `${-this.screenIndex * 371}px`;
draw.text(this.context, 'Star4000', 'bold 18pt', '#ffffff', 517, 73, 'Heavy', 2);
let x = 362;
const y = 52;
draw.box(this.context, '#000000', x - 2, y - 2, 154, 28);
draw.box(this.context, 'rgb(49, 210, 22)', x, y, 17, 24); x += 19;
draw.box(this.context, 'rgb(28, 138, 18)', x, y, 17, 24); x += 19;
draw.box(this.context, 'rgb(20, 90, 15)', x, y, 17, 24); x += 19;
draw.box(this.context, 'rgb(10, 40, 10)', x, y, 17, 24); x += 19;
draw.box(this.context, 'rgb(196, 179, 70)', x, y, 17, 24); x += 19;
draw.box(this.context, 'rgb(190, 72, 19)', x, y, 17, 24); x += 19;
draw.box(this.context, 'rgb(171, 14, 14)', x, y, 17, 24); x += 19;
draw.box(this.context, 'rgb(115, 31, 4)', x, y, 17, 24); x += 19;
this.context.drawImage(this.data[this.screenIndex], 0, 0, 640, 367, 0, 113, 640, 367);
draw.text(this.context, 'Star4000 Small', '24pt', '#ffffff', 438, 105, this.times[this.screenIndex].toLocaleString(DateTime.TIME_SIMPLE), 2, 'center');
this.finishDraw(); this.finishDraw();
} }

View file

@ -1,15 +1,12 @@
// regional forecast and observations // regional forecast and observations
// type 0 = observations, 1 = first forecast, 2 = second forecast // type 0 = observations, 1 = first forecast, 2 = second forecast
/* globals WeatherDisplay, utils, STATUS, icons, UNITS, draw, navigation, luxon, StationInfo, RegionalCities */ /* globals WeatherDisplay, utils, STATUS, icons, UNITS, navigation, luxon, StationInfo, RegionalCities */
// eslint-disable-next-line no-unused-vars // eslint-disable-next-line no-unused-vars
class RegionalForecast extends WeatherDisplay { class RegionalForecast extends WeatherDisplay {
constructor(navId, elemId) { constructor(navId, elemId) {
super(navId, elemId, 'Regional Forecast'); super(navId, elemId, 'Regional Forecast', true);
// pre-load background image (returns promise)
this.backgroundImage = utils.image.load('images/BackGround5_1.png');
// timings // timings
this.timing.totalScreens = 3; this.timing.totalScreens = 3;
@ -19,14 +16,14 @@ class RegionalForecast extends WeatherDisplay {
super.getData(_weatherParameters); super.getData(_weatherParameters);
const weatherParameters = _weatherParameters ?? this.weatherParameters; const weatherParameters = _weatherParameters ?? this.weatherParameters;
// pre-load the base map (returns promise) // pre-load the base map
let src = 'images/Basemap2.png'; let baseMap = 'images/Basemap2.png';
if (weatherParameters.state === 'HI') { if (weatherParameters.state === 'HI') {
src = 'images/HawaiiRadarMap4.png'; baseMap = 'images/HawaiiRadarMap4.png';
} else if (weatherParameters.state === 'AK') { } else if (weatherParameters.state === 'AK') {
src = 'images/AlaskaRadarMap6.png'; baseMap = 'images/AlaskaRadarMap6.png';
} }
this.baseMap = utils.image.load(src); this.elem.querySelector('.map img').src = baseMap;
// map offset // map offset
const offsetXY = { const offsetXY = {
@ -34,10 +31,10 @@ class RegionalForecast extends WeatherDisplay {
y: 117, y: 117,
}; };
// get user's location in x/y // get user's location in x/y
const sourceXY = this.getXYFromLatitudeLongitude(weatherParameters.latitude, weatherParameters.longitude, offsetXY.x, offsetXY.y, weatherParameters.state); const sourceXY = RegionalForecast.getXYFromLatitudeLongitude(weatherParameters.latitude, weatherParameters.longitude, offsetXY.x, offsetXY.y, weatherParameters.state);
// get latitude and longitude limits // get latitude and longitude limits
const minMaxLatLon = this.getMinMaxLatitudeLongitude(sourceXY.x, sourceXY.y, offsetXY.x, offsetXY.y, weatherParameters.state); const minMaxLatLon = RegionalForecast.getMinMaxLatitudeLongitude(sourceXY.x, sourceXY.y, offsetXY.x, offsetXY.y, weatherParameters.state);
// get a target distance // get a target distance
let targetDistance = 2.5; let targetDistance = 2.5;
@ -66,7 +63,7 @@ class RegionalForecast extends WeatherDisplay {
}); });
// get regional forecasts and observations (the two are intertwined due to the design of api.weather.gov) // get regional forecasts and observations (the two are intertwined due to the design of api.weather.gov)
const regionalForecastPromises = regionalCities.map(async (city) => { const regionalDataAll = await Promise.all(regionalCities.map(async (city) => {
try { try {
// get the point first, then break down into forecast and observations // get the point first, then break down into forecast and observations
const point = await utils.weather.getPoint(city.lat, city.lon); const point = await utils.weather.getPoint(city.lat, city.lon);
@ -77,7 +74,7 @@ class RegionalForecast extends WeatherDisplay {
const forecast = await utils.fetch.json(point.properties.forecast); const forecast = await utils.fetch.json(point.properties.forecast);
// get XY on map for city // get XY on map for city
const cityXY = this.getXYForCity(city, minMaxLatLon.maxLat, minMaxLatLon.minLon, weatherParameters.state); const cityXY = RegionalForecast.getXYForCity(city, minMaxLatLon.maxLat, minMaxLatLon.minLon, weatherParameters.state);
// wait for the regional observation if it's not done yet // wait for the regional observation if it's not done yet
const observation = await observationPromise; const observation = await observationPromise;
@ -105,14 +102,12 @@ class RegionalForecast extends WeatherDisplay {
RegionalForecast.buildForecast(forecast.properties.periods[2], city, cityXY), RegionalForecast.buildForecast(forecast.properties.periods[2], city, cityXY),
]; ];
} catch (e) { } catch (e) {
console.log(`No regional forecast data for '${city.name}'`); console.log(`No regional forecast data for '${city.name ?? city.city}'`);
console.log(e); console.log(e);
return false; return false;
} }
}); }));
// wait for the forecasts
const regionalDataAll = await Promise.all(regionalForecastPromises);
// filter out any false (unavailable data) // filter out any false (unavailable data)
const regionalData = regionalDataAll.filter((data) => data); const regionalData = regionalDataAll.filter((data) => data);
@ -154,20 +149,21 @@ class RegionalForecast extends WeatherDisplay {
// get the observation data // get the observation data
const observation = await utils.fetch.json(`${station}/observations/latest`); const observation = await utils.fetch.json(`${station}/observations/latest`);
// preload the image // preload the image
if (!observation.properties.icon) return false;
utils.image.preload(icons.getWeatherRegionalIconFromIconLink(observation.properties.icon, !observation.properties.daytime)); utils.image.preload(icons.getWeatherRegionalIconFromIconLink(observation.properties.icon, !observation.properties.daytime));
// return the observation // return the observation
return observation.properties; return observation.properties;
} catch (e) { } catch (e) {
console.log(`Unable to get regional observations for ${city.Name}`); console.log(`Unable to get regional observations for ${city.Name ?? city.city}`);
console.error(e.status, e.responseJSON); console.error(e.status, e.responseJSON);
return false; return false;
} }
} }
// utility latitude/pixel conversions // utility latitude/pixel conversions
getXYFromLatitudeLongitude(Latitude, Longitude, OffsetX, OffsetY, state) { static getXYFromLatitudeLongitude(Latitude, Longitude, OffsetX, OffsetY, state) {
if (state === 'AK') return this.getXYFromLatitudeLongitudeAK(Latitude, Longitude, OffsetX, OffsetY); if (state === 'AK') return RegionalForecast.getXYFromLatitudeLongitudeAK(Latitude, Longitude, OffsetX, OffsetY);
if (state === 'HI') return this.getXYFromLatitudeLongitudeHI(Latitude, Longitude, OffsetX, OffsetY); if (state === 'HI') return RegionalForecast.getXYFromLatitudeLongitudeHI(Latitude, Longitude, OffsetX, OffsetY);
let y = 0; let y = 0;
let x = 0; let x = 0;
const ImgHeight = 1600; const ImgHeight = 1600;
@ -248,9 +244,9 @@ class RegionalForecast extends WeatherDisplay {
return { x, y }; return { x, y };
} }
getMinMaxLatitudeLongitude(X, Y, OffsetX, OffsetY, state) { static getMinMaxLatitudeLongitude(X, Y, OffsetX, OffsetY, state) {
if (state === 'AK') return this.getMinMaxLatitudeLongitudeAK(X, Y, OffsetX, OffsetY); if (state === 'AK') return RegionalForecast.getMinMaxLatitudeLongitudeAK(X, Y, OffsetX, OffsetY);
if (state === 'HI') return this.getMinMaxLatitudeLongitudeHI(X, Y, OffsetX, OffsetY); if (state === 'HI') return RegionalForecast.getMinMaxLatitudeLongitudeHI(X, Y, OffsetX, OffsetY);
const maxLat = ((Y / 55.2) - 50.5) * -1; const maxLat = ((Y / 55.2) - 50.5) * -1;
const minLat = (((Y + (OffsetY * 2)) / 55.2) - 50.5) * -1; const minLat = (((Y + (OffsetY * 2)) / 55.2) - 50.5) * -1;
const minLon = (((X * -1) / 41.775) + 127.5) * -1; const minLon = (((X * -1) / 41.775) + 127.5) * -1;
@ -283,9 +279,9 @@ class RegionalForecast extends WeatherDisplay {
}; };
} }
getXYForCity(City, MaxLatitude, MinLongitude, state) { static getXYForCity(City, MaxLatitude, MinLongitude, state) {
if (state === 'AK') this.getXYForCityAK(City, MaxLatitude, MinLongitude); if (state === 'AK') RegionalForecast.getXYForCityAK(City, MaxLatitude, MinLongitude);
if (state === 'HI') this.getXYForCityHI(City, MaxLatitude, MinLongitude); if (state === 'HI') RegionalForecast.getXYForCityHI(City, MaxLatitude, MinLongitude);
let x = (City.lon - MinLongitude) * 57; let x = (City.lon - MinLongitude) * 57;
let y = (MaxLatitude - City.lat) * 70; let y = (MaxLatitude - City.lat) * 70;
@ -328,61 +324,61 @@ class RegionalForecast extends WeatherDisplay {
return city.match(/[^-;/\\,]*/)[0].substr(0, 12); return city.match(/[^-;/\\,]*/)[0].substr(0, 12);
} }
async drawCanvas() { drawCanvas() {
super.drawCanvas(); super.drawCanvas();
// break up data into useful values // break up data into useful values
const { regionalData: data, sourceXY, offsetXY } = this.data; const { regionalData: data, sourceXY, offsetXY } = this.data;
// fixed offset for all y values when drawing to the map
const mapYOff = 90;
const { DateTime } = luxon; const { DateTime } = luxon;
// draw the header graphics // draw the header graphics
this.context.drawImage(await this.backgroundImage, 0, 0);
draw.horizontalGradientSingle(this.context, 0, 30, 500, 90, draw.topColor1, draw.topColor2);
draw.triangle(this.context, 'rgb(28, 10, 87)', 500, 30, 450, 90, 500, 90);
// draw the appropriate title // draw the appropriate title
const titleTop = this.elem.querySelector('.title.dual .top');
const titleBottom = this.elem.querySelector('.title.dual .bottom');
if (this.screenIndex === 0) { if (this.screenIndex === 0) {
draw.titleText(this.context, 'Regional', 'Observations'); titleTop.innerHTML = 'Regional';
titleBottom.innerHTML = 'Observations';
} else { } else {
const forecastDate = DateTime.fromISO(data[0][this.screenIndex].time); const forecastDate = DateTime.fromISO(data[0][this.screenIndex].time);
// get the name of the day // get the name of the day
const dayName = forecastDate.toLocaleString({ weekday: 'long' }); const dayName = forecastDate.toLocaleString({ weekday: 'long' });
titleTop.innerHTML = 'Forecast for';
// draw the title // draw the title
if (data[0][this.screenIndex].daytime) { if (data[0][this.screenIndex].daytime) {
draw.titleText(this.context, 'Forecast for', dayName); titleBottom.innerHTML = dayName;
} else { } else {
draw.titleText(this.context, 'Forecast for', `${dayName} Night`); titleBottom.innerHTML = `${dayName} Night`;
} }
} }
// draw the map // draw the map
this.context.drawImage(await this.baseMap, sourceXY.x, sourceXY.y, (offsetXY.x * 2), (offsetXY.y * 2), 0, mapYOff, 640, 312); const scale = 640 / (offsetXY.x * 2);
await Promise.all(data.map(async (city) => { const map = this.elem.querySelector('.map');
map.style.zoom = scale;
map.style.top = `-${sourceXY.y}px`;
map.style.left = `-${sourceXY.x}px`;
const cities = data.map((city) => {
const fill = {};
const period = city[this.screenIndex]; const period = city[this.screenIndex];
// draw the icon if possible
const icon = icons.getWeatherRegionalIconFromIconLink(period.icon, !period.daytime);
if (icon) {
this.gifs.push(await utils.image.superGifAsync({
src: icon,
max_width: 42,
auto_play: true,
canvas: this.canvas,
x: period.x,
y: period.y - 15 + mapYOff,
}));
}
// City Name fill.icon = { type: 'img', src: icons.getWeatherRegionalIconFromIconLink(period.icon, !period.daytime) };
draw.text(this.context, 'Star4000', '20px', '#ffffff', period.x - 40, period.y - 15 + mapYOff, period.name, 2); fill.city = period.name;
// Temperature
let { temperature } = period; let { temperature } = period;
if (navigation.units() === UNITS.metric) temperature = Math.round(utils.units.fahrenheitToCelsius(temperature)); if (navigation.units() === UNITS.metric) temperature = Math.round(utils.units.fahrenheitToCelsius(temperature));
draw.text(this.context, 'Star4000 Large Compressed', '28px', '#ffff00', period.x - (temperature.toString().length * 15), period.y + 20 + mapYOff, temperature, 2); fill.temp = temperature;
}));
const elem = this.fillTemplate('location', fill);
elem.style.left = `${period.x}px`;
elem.style.top = `${period.y}px`;
return elem;
});
const locationContainer = this.elem.querySelector('.location-container');
locationContainer.innerHTML = '';
locationContainer.append(...cities);
this.finishDraw(); this.finishDraw();
} }

View file

@ -1,16 +1,11 @@
// travel forecast display // travel forecast display
/* globals WeatherDisplay, utils, STATUS, UNITS, draw, navigation, icons, luxon, TravelCities */ /* globals WeatherDisplay, utils, STATUS, UNITS, navigation, icons, luxon, TravelCities */
// eslint-disable-next-line no-unused-vars // eslint-disable-next-line no-unused-vars
class TravelForecast extends WeatherDisplay { class TravelForecast extends WeatherDisplay {
constructor(navId, elemId, defaultActive) { constructor(navId, elemId, defaultActive) {
// special height and width for scrolling // special height and width for scrolling
super(navId, elemId, 'Travel Forecast', defaultActive); super(navId, elemId, 'Travel Forecast', defaultActive);
// pre-load background image (returns promise)
this.backgroundImage = utils.image.load('images/BackGround6_1.png');
// height of one city in the travel forecast
this.cityHeight = 72;
// set up the timing // set up the timing
this.timing.baseDelay = 20; this.timing.baseDelay = 20;
@ -18,7 +13,7 @@ class TravelForecast extends WeatherDisplay {
const pagesFloat = TravelCities.length / 4; const pagesFloat = TravelCities.length / 4;
const pages = Math.floor(pagesFloat) - 2; // first page is already displayed, last page doesn't happen const pages = Math.floor(pagesFloat) - 2; // first page is already displayed, last page doesn't happen
const extra = pages % 1; const extra = pages % 1;
const timingStep = this.cityHeight * 4; const timingStep = 75 * 4;
this.timing.delay = [150 + timingStep]; this.timing.delay = [150 + timingStep];
// add additional pages // add additional pages
for (let i = 0; i < pages; i += 1) this.timing.delay.push(timingStep); for (let i = 0; i < pages; i += 1) this.timing.delay.push(timingStep);
@ -49,7 +44,7 @@ class TravelForecast extends WeatherDisplay {
} catch (e) { } catch (e) {
console.error(`GetTravelWeather for ${city.Name} failed`); console.error(`GetTravelWeather for ${city.Name} failed`);
console.error(e.status, e.responseJSON); console.error(e.status, e.responseJSON);
return { name: city.Name }; return { name: city.Name, error: true };
} }
}); });
@ -69,47 +64,23 @@ class TravelForecast extends WeatherDisplay {
} }
async drawLongCanvas() { async drawLongCanvas() {
// create the "long" canvas if necessary // get the element and populate
if (!this.longCanvas) { const list = this.elem.querySelector('.travel-lines');
this.longCanvas = document.createElement('canvas'); list.innerHTML = '';
this.longCanvas.width = 640;
this.longCanvas.height = 1728;
this.longContext = this.longCanvas.getContext('2d');
this.longCanvasGifs = [];
}
// stop all gifs
this.longCanvasGifs.forEach((gif) => gif.pause());
// delete the gifs
this.longCanvasGifs.length = 0;
// set up variables // set up variables
const cities = this.data; const cities = this.data;
// clean up existing gifs const lines = cities.map((city) => {
this.gifs.forEach((gif) => gif.pause()); if (city.error) return false;
// delete the gifs const fillValues = {};
this.gifs.length = 0;
this.longContext.clearRect(0, 0, this.longCanvas.width, this.longCanvas.height);
// draw the "long" canvas with all cities
draw.box(this.longContext, 'rgb(35, 50, 112)', 0, 0, 640, TravelCities.length * this.cityHeight);
for (let i = 0; i <= 4; i += 1) {
const y = i * 346;
draw.horizontalGradient(this.longContext, 0, y, 640, y + 346, '#102080', '#001040');
}
await Promise.all(cities.map(async (city, index) => {
// calculate base y value
const y = 50 + this.cityHeight * index;
// city name // city name
draw.text(this.longContext, 'Star4000 Large Compressed', '24pt', '#FFFF00', 80, y, city.name, 2); fillValues.city = city;
// check for forecast data // check for forecast data
if (city.icon) { if (city.icon) {
fillValues.city = city.name;
// get temperatures and convert if necessary // get temperatures and convert if necessary
let { low, high } = city; let { low, high } = city;
@ -122,25 +93,16 @@ class TravelForecast extends WeatherDisplay {
const lowString = Math.round(low).toString(); const lowString = Math.round(low).toString();
const highString = Math.round(high).toString(); const highString = Math.round(high).toString();
const xLow = (500 - (lowString.length * 20)); fillValues.low = lowString;
draw.text(this.longContext, 'Star4000 Large', '24pt', '#FFFF00', xLow, y, lowString, 2); fillValues.high = highString;
const xHigh = (560 - (highString.length * 20)); fillValues.icon = { type: 'img', src: city.icon };
draw.text(this.longContext, 'Star4000 Large', '24pt', '#FFFF00', xHigh, y, highString, 2);
this.longCanvasGifs.push(await utils.image.superGifAsync({
src: city.icon,
auto_play: true,
canvas: this.longCanvas,
x: 330,
y: y - 35,
max_width: 47,
}));
} else { } else {
draw.text(this.longContext, 'Star4000 Small', '24pt', '#FFFFFF', 400, y - 18, 'NO TRAVEL', 2); fillValues.error = 'NO TRAVEL DATA AVAILABLE';
draw.text(this.longContext, 'Star4000 Small', '24pt', '#FFFFFF', 400, y, 'DATA AVAILABLE', 2);
} }
})); return this.fillTemplate('travel-row', fillValues);
}).filter((d) => d);
list.append(...lines);
} }
async drawCanvas() { async drawCanvas() {
@ -151,18 +113,7 @@ class TravelForecast extends WeatherDisplay {
// set up variables // set up variables
const cities = this.data; const cities = this.data;
// draw the standard context this.elem.querySelector('.header .title.dual .bottom').innerHTML = `For ${TravelForecast.getTravelCitiesDayName(cities)}`;
this.context.drawImage(await this.backgroundImage, 0, 0);
draw.horizontalGradientSingle(this.context, 0, 30, 500, 90, draw.topColor1, draw.topColor2);
draw.triangle(this.context, 'rgb(28, 10, 87)', 500, 30, 450, 90, 500, 90);
draw.titleText(this.context, 'Travel Forecast', `For ${TravelForecast.getTravelCitiesDayName(cities)}`);
draw.text(this.context, 'Star4000 Small', '24pt', '#FFFF00', 455, 105, 'LOW', 2);
draw.text(this.context, 'Star4000 Small', '24pt', '#FFFF00', 510, 105, 'HIGH', 2);
// copy the scrolled portion of the canvas for the initial run before the scrolling starts
this.context.drawImage(this.longCanvas, 0, 0, 640, 289, 0, 110, 640, 289);
this.finishDraw(); this.finishDraw();
} }
@ -180,17 +131,14 @@ class TravelForecast extends WeatherDisplay {
// base count change callback // base count change callback
baseCountChange(count) { baseCountChange(count) {
// get a fresh canvas
const longCanvas = this.getLongCanvas();
// calculate scroll offset and don't go past end // calculate scroll offset and don't go past end
let offsetY = Math.min(longCanvas.height - 289, (count - 150)); let offsetY = Math.min(this.elem.querySelector('.travel-lines').getBoundingClientRect().height - 289, (count - 150));
// don't let offset go negative // don't let offset go negative
if (offsetY < 0) offsetY = 0; if (offsetY < 0) offsetY = 0;
// copy the scrolled portion of the canvas // copy the scrolled portion of the canvas
this.context.drawImage(longCanvas, 0, offsetY, 640, 289, 0, 110, 640, 289); this.elem.querySelector('.main').scrollTo(0, offsetY);
} }
static getTravelCitiesDayName(cities) { static getTravelCitiesDayName(cities) {

View file

@ -1,6 +1,5 @@
// radar utilities // radar utilities
/* globals SuperGif */
// eslint-disable-next-line no-unused-vars // eslint-disable-next-line no-unused-vars
const utils = (() => { const utils = (() => {
// ****************************** weather data ******************************** // ****************************** weather data ********************************
@ -30,40 +29,17 @@ const utils = (() => {
} }
}); });
// async version of SuperGif
const superGifAsync = (e) => new Promise((resolve) => {
const gif = new SuperGif(e);
gif.load(() => resolve(gif));
});
// preload an image // 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 // 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 // a list of cached icons is used to avoid hitting the cache multiple times
const cachedImages = []; const cachedImages = [];
const preload = (src) => { const preload = (src) => {
if (cachedImages.includes(src)) return false; if (cachedImages.includes(src)) return false;
const img = new Image(); blob(src);
img.scr = src; // cachedImages.push(src);
cachedImages.push(src);
return true; return true;
}; };
// draw an image on a local canvas and return the context
const drawLocalCanvas = (img) => {
// create a canvas
const canvas = document.createElement('canvas');
canvas.width = img.width;
canvas.height = img.height;
// get the context
const context = canvas.getContext('2d');
context.imageSmoothingEnabled = false;
// draw the image
context.drawImage(img, 0, 0);
return context;
};
// *********************************** unit conversions *********************** // *********************************** unit conversions ***********************
Math.round2 = (value, decimals) => Number(`${Math.round(`${value}e${decimals}`)}e-${decimals}`); Math.round2 = (value, decimals) => Number(`${Math.round(`${value}e${decimals}`)}e-${decimals}`);
@ -136,99 +112,21 @@ const utils = (() => {
const wrap = (x, m) => ((x % m) + m) % m; const wrap = (x, m) => ((x % m) + m) % m;
// ********************************* strings ********************************************* // ********************************* strings *********************************************
const wordWrap = (_str, ...rest) => { const locationCleanup = (input) => {
// discuss at: https://locutus.io/php/wordwrap/ // regexes to run
// original by: Jonas Raoni Soares Silva (https://www.jsfromhell.com) const regexes = [
// improved by: Nick Callen // "Chicago / West Chicago", removes before slash
// improved by: Kevin van Zonneveld (https://kvz.io) /^[A-Za-z ]+ \/ /,
// improved by: Sakimori // "Chicago/Waukegan" removes before slash
// revised by: Jonas Raoni Soares Silva (https://www.jsfromhell.com) /^[A-Za-z ]+\//,
// bugfixed by: Michael Grier // "Chicago, Chicago O'hare" removes before comma
// bugfixed by: Feras ALHAEK /^[A-Za-z ]+, /,
// improved by: Rafał Kukawski (https://kukawski.net) ];
// example 1: wordwrap('Kevin van Zonneveld', 6, '|', true)
// returns 1: 'Kevin|van|Zonnev|eld'
// example 2: wordwrap('The quick brown fox jumped over the lazy dog.', 20, '<br />\n')
// returns 2: 'The quick brown fox<br />\njumped over the lazy<br />\ndog.'
// example 3: wordwrap('Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.')
// returns 3: 'Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod\ntempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim\nveniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea\ncommodo consequat.'
const intWidth = rest[0] ?? 75;
const strBreak = rest[1] ?? '\n';
const cut = rest[2] ?? false;
let i; // run all regexes
let j; return regexes.reduce((value, regex) => value.replace(regex, ''), input);
let line;
let str = _str;
str += '';
if (intWidth < 1) {
return str;
}
const reLineBreaks = /\r\n|\n|\r/;
const reBeginningUntilFirstWhitespace = /^\S*/;
const reLastCharsWithOptionalTrailingWhitespace = /\S*(\s)?$/;
const lines = str.split(reLineBreaks);
const l = lines.length;
let match;
// for each line of text
// eslint-disable-next-line no-plusplus
for (i = 0; i < l; lines[i++] += line) {
line = lines[i];
lines[i] = '';
while (line.length > intWidth) {
// get slice of length one char above limit
const slice = line.slice(0, intWidth + 1);
// remove leading whitespace from rest of line to parse
let ltrim = 0;
// remove trailing whitespace from new line content
let rtrim = 0;
match = slice.match(reLastCharsWithOptionalTrailingWhitespace);
// if the slice ends with whitespace
if (match[1]) {
// then perfect moment to cut the line
j = intWidth;
ltrim = 1;
} else {
// otherwise cut at previous whitespace
j = slice.length - match[0].length;
if (j) {
rtrim = 1;
}
// but if there is no previous whitespace
// and cut is forced
// cut just at the defined limit
if (!j && cut && intWidth) {
j = intWidth;
}
// if cut wasn't forced
// cut at next possible whitespace after the limit
if (!j) {
const charsUntilNextWhitespace = (line.slice(intWidth).match(reBeginningUntilFirstWhitespace) || [''])[0];
j = slice.length + charsUntilNextWhitespace.length;
}
}
lines[i] += line.slice(0, j - rtrim);
line = line.slice(j + ltrim);
lines[i] += line.length ? strBreak : '';
}
}
return lines.join('\n');
}; };
// ********************************* cors ******************************************** // ********************************* cors ********************************************
// rewrite some urls for local server // rewrite some urls for local server
const rewriteUrl = (_url) => { const rewriteUrl = (_url) => {
@ -255,9 +153,9 @@ const utils = (() => {
// build a url, including the rewrite for cors if necessary // build a url, including the rewrite for cors if necessary
let corsUrl = _url; let corsUrl = _url;
if (params.cors === true) corsUrl = rewriteUrl(_url); if (params.cors === true) corsUrl = rewriteUrl(_url);
const url = new URL(corsUrl); const url = new URL(corsUrl, `${window.location.origin}/`);
// match the security protocol // match the security protocol when not on localhost
url.protocol = window.location.protocol; url.protocol = window.location.hostname !== 'localhost' ? window.location.protocol : url.protocol;
// add parameters if necessary // add parameters if necessary
if (params.data) { if (params.data) {
Object.keys(params.data).forEach((key) => { Object.keys(params.data).forEach((key) => {
@ -286,13 +184,18 @@ const utils = (() => {
} }
}; };
const elemForEach = (selector, callback) => {
[...document.querySelectorAll(selector)].forEach(callback);
};
// return an orderly object // return an orderly object
return { return {
elem: {
forEach: elemForEach,
},
image: { image: {
load: loadImg, load: loadImg,
superGifAsync,
preload, preload,
drawLocalCanvas,
}, },
weather: { weather: {
getPoint, getPoint,
@ -318,7 +221,7 @@ const utils = (() => {
wrap, wrap,
}, },
string: { string: {
wordWrap, locationCleanup,
}, },
cors: { cors: {
rewriteUrl, rewriteUrl,

View file

@ -1,6 +1,6 @@
// base weather display class // base weather display class
/* globals navigation, utils, draw, UNITS, luxon, currentWeatherScroll */ /* globals navigation, utils, luxon, currentWeatherScroll */
const STATUS = { const STATUS = {
loading: Symbol('loading'), loading: Symbol('loading'),
@ -31,8 +31,8 @@ class WeatherDisplay {
this.navBaseCount = 0; this.navBaseCount = 0;
this.screenIndex = -1; // special starting condition this.screenIndex = -1; // special starting condition
// create the canvas, also stores this.elemId // store elemId once
this.createCanvas(elemId); this.storeElemId(elemId);
if (elemId !== 'progress') this.addCheckbox(defaultEnabled); if (elemId !== 'progress') this.addCheckbox(defaultEnabled);
if (this.enabled) { if (this.enabled) {
@ -41,6 +41,9 @@ class WeatherDisplay {
this.setStatus(STATUS.disabled); this.setStatus(STATUS.disabled);
} }
this.startNavCount(); this.startNavCount();
// get any templates
this.loadTemplates();
} }
addCheckbox(defaultEnabled = true) { addCheckbox(defaultEnabled = true) {
@ -92,18 +95,10 @@ class WeatherDisplay {
this.loadingStatus = state; this.loadingStatus = state;
} }
createCanvas(elemId, width = 640, height = 480) { storeElemId(elemId) {
// only create it once // only create it once
if (this.elemId) return; if (this.elemId) return;
this.elemId = elemId; this.elemId = elemId;
// create a canvas
const canvas = document.createElement('template');
canvas.innerHTML = `<canvas id='${`${elemId}Canvas`}' width='${width}' height='${height}' style='display: none;' />`;
// add to the page
const container = document.getElementById('container');
container.appendChild(canvas.content.firstChild);
} }
// get necessary data for this display // get necessary data for this display
@ -136,46 +131,27 @@ class WeatherDisplay {
} }
drawCanvas() { drawCanvas() {
// stop all gifs
this.gifs.forEach((gif) => gif.pause());
// delete the gifs
this.gifs.length = 0;
// refresh the canvas
this.canvas = document.getElementById(`${this.elemId}Canvas`);
this.context = this.canvas.getContext('2d');
// clean up the first-run flag in screen index // clean up the first-run flag in screen index
if (this.screenIndex < 0) this.screenIndex = 0; if (this.screenIndex < 0) this.screenIndex = 0;
} }
finishDraw() { finishDraw() {
let OkToDrawCurrentConditions = true; let OkToDrawCurrentConditions = true;
let OkToDrawNoaaImage = true;
let OkToDrawCurrentDateTime = true; let OkToDrawCurrentDateTime = true;
let OkToDrawLogoImage = true;
// let OkToDrawCustomScrollText = false; // let OkToDrawCustomScrollText = false;
let bottom; let bottom;
// visibility tests // visibility tests
// if (_ScrollText !== '') OkToDrawCustomScrollText = true; // if (_ScrollText !== '') OkToDrawCustomScrollText = true;
if (this.elemId === 'almanac') OkToDrawNoaaImage = false;
if (this.elemId === 'travelForecast') OkToDrawNoaaImage = false;
if (this.elemId === 'hourly') OkToDrawNoaaImage = false;
if (this.elemId === 'regionalForecast') OkToDrawNoaaImage = false;
if (this.elemId === 'progress') { if (this.elemId === 'progress') {
OkToDrawCurrentConditions = false; OkToDrawCurrentConditions = false;
OkToDrawNoaaImage = false;
} }
if (this.elemId === 'radar') { if (this.elemId === 'radar') {
OkToDrawCurrentConditions = false; OkToDrawCurrentConditions = false;
OkToDrawCurrentDateTime = false; OkToDrawCurrentDateTime = false;
OkToDrawNoaaImage = false;
// OkToDrawCustomScrollText = false;
} }
if (this.elemId === 'hazards') { if (this.elemId === 'hazards') {
OkToDrawNoaaImage = false;
bottom = true; bottom = true;
OkToDrawLogoImage = false;
} }
// draw functions // draw functions
if (OkToDrawCurrentDateTime) { if (OkToDrawCurrentDateTime) {
@ -185,10 +161,8 @@ class WeatherDisplay {
setInterval(() => this.drawCurrentDateTime(bottom), 100); setInterval(() => this.drawCurrentDateTime(bottom), 100);
} }
} }
if (OkToDrawLogoImage) this.drawLogoImage();
if (OkToDrawNoaaImage) this.drawNoaaImage();
if (OkToDrawCurrentConditions) { if (OkToDrawCurrentConditions) {
currentWeatherScroll.start(this.context); currentWeatherScroll.start();
} else { } else {
// cause a reset if the progress screen is displayed // cause a reset if the progress screen is displayed
currentWeatherScroll.stop(this.elemId === 'progress'); currentWeatherScroll.stop(this.elemId === 'progress');
@ -197,81 +171,27 @@ class WeatherDisplay {
// if (OkToDrawCustomScrollText) DrawCustomScrollText(WeatherParameters, context); // if (OkToDrawCustomScrollText) DrawCustomScrollText(WeatherParameters, context);
} }
drawCurrentDateTime(bottom) { drawCurrentDateTime() {
// only draw if canvas is active to conserve battery // only draw if canvas is active to conserve battery
if (!this.isActive()) return; if (!this.isActive()) return;
const { DateTime } = luxon; const { DateTime } = luxon;
const font = 'Star4000 Small';
const size = '24pt';
const color = '#ffffff';
const shadow = 2;
// on the first pass store the background for the date and time
if (!this.dateTimeBackground) {
const bg = this.context.getImageData(410, 30, 175, 60);
// test background draw complete and skip drawing if there is no background yet
if (bg.data[0] === 0) return;
// store the background
this.dateTimeBackground = bg;
}
// Clear the date and time area.
if (bottom) {
draw.box(this.context, 'rgb(25, 50, 112)', 0, 389, 640, 16);
} else {
this.context.putImageData(this.dateTimeBackground, 410, 30);
}
// Get the current date and time. // Get the current date and time.
const now = DateTime.local(); const now = DateTime.local();
// time = "11:35:08 PM"; // time = "11:35:08 PM";
const time = now.toLocaleString(DateTime.TIME_WITH_SECONDS).padStart(11, ' '); const time = now.toLocaleString(DateTime.TIME_WITH_SECONDS).padStart(11, ' ');
let x; let y; if (this.lastTime !== time) {
if (bottom) { utils.elem.forEach('.date-time.time', (elem) => { elem.innerHTML = time.toUpperCase(); });
x = 400;
y = 402;
} else {
x = 410;
y = 65;
} }
if (navigation.units() === UNITS.metric) { this.lastTime = time;
x += 45;
}
draw.text(this.context, font, size, color, x, y, time.toUpperCase(), shadow); // y += 20;
const date = now.toFormat(' ccc LLL ') + now.day.toString().padStart(2, ' '); const date = now.toFormat(' ccc LLL ') + now.day.toString().padStart(2, ' ');
if (bottom) { if (this.lastDate !== date) {
x = 55; utils.elem.forEach('.date-time.date', (elem) => { elem.innerHTML = date.toUpperCase(); });
y = 402;
} else {
x = 410;
y = 85;
} }
draw.text(this.context, font, size, color, x, y, date.toUpperCase(), shadow); this.lastDate = date;
}
async drawNoaaImage() {
// load the image and store locally
if (!this.drawNoaaImage.image) {
this.drawNoaaImage.image = utils.image.load('images/noaa5.gif');
}
// wait for the image to load completely
const img = await this.drawNoaaImage.image;
this.context.drawImage(img, 356, 39);
}
async drawLogoImage() {
// load the image and store locally
if (!this.drawLogoImage.image) {
this.drawLogoImage.image = utils.image.load('images/Logo3.png');
}
// wait for the image load completely
const img = await this.drawLogoImage.image;
this.context.drawImage(img, 50, 30, 85, 67);
} }
// show/hide the canvas and start/stop the navigation timer // show/hide the canvas and start/stop the navigation timer
@ -281,23 +201,18 @@ class WeatherDisplay {
if (navCmd === navigation.msg.command.firstFrame) this.navNext(navCmd); if (navCmd === navigation.msg.command.firstFrame) this.navNext(navCmd);
if (navCmd === navigation.msg.command.lastFrame) this.navPrev(navCmd); if (navCmd === navigation.msg.command.lastFrame) this.navPrev(navCmd);
// see if the canvas is already showing this.startNavCount();
if (this.canvas.style.display === 'block') return false;
// show the canvas this.elem.classList.add('show');
this.canvas.style.display = 'block';
return false;
} }
hideCanvas() { hideCanvas() {
this.resetNavBaseCount(); this.resetNavBaseCount();
this.elem.classList.remove('show');
if (!this.canvas) return;
this.canvas.style.display = 'none';
} }
isActive() { isActive() {
return document.getElementById(`${this.elemId}Canvas`).offsetParent !== null; return this.elem.offsetHeight !== 0;
} }
isEnabled() { isEnabled() {
@ -452,7 +367,6 @@ class WeatherDisplay {
clearInterval(this.navInterval); clearInterval(this.navInterval);
this.navInterval = undefined; this.navInterval = undefined;
} }
this.startNavCount();
} }
sendNavDisplayMessage(message) { sendNavDisplayMessage(message) {
@ -461,4 +375,44 @@ class WeatherDisplay {
type: message, type: message,
}); });
} }
loadTemplates() {
this.templates = {};
this.elem = document.getElementById(`${this.elemId}-html`);
if (!this.elem) return;
const templates = this.elem.querySelectorAll('.template');
templates.forEach((template) => {
const className = template.classList[0];
const node = template.cloneNode(true);
node.classList.remove('template');
this.templates[className] = node;
template.remove();
});
}
fillTemplate(name, fillValues) {
// get the template
const templateNode = this.templates[name];
if (!templateNode) return false;
// clone it
const template = templateNode.cloneNode(true);
Object.entries(fillValues).forEach(([key, value]) => {
// get the specified element
const elem = template.querySelector(`.${key}`);
if (!elem) return;
// fill based on type provided
if (typeof value === 'string' || typeof value === 'number') {
// string and number fill the first found selector
elem.innerHTML = value;
} else if (value?.type === 'img') {
// fill the image source
elem.querySelector('img').src = value.src;
}
});
return template;
}
} }

View file

@ -1,288 +0,0 @@
var GetWeatherHazards3 = function (WeatherParameters) {
var ZoneId = WeatherParameters.ZoneId;
var HazardUrls = [];
var HazardCounter = 0;
WeatherParameters.WeatherHazardConditions =
{
ZoneId: WeatherParameters.ZoneId,
Hazards: [],
};
var Url = 'https://alerts.weather.gov/cap/wwaatmget.php?x=' + ZoneId + '&y=0';
// Load the xml file using ajax
$.ajaxCORS({
type: 'GET',
url: Url,
dataType: 'text',
crossDomain: true,
cache: false,
success: function (text) {
// IE doesn't support XML tags with colons.
text = text.replaceAll('<cap:', '<cap_');
text = text.replaceAll('</cap:', '</cap_');
var $xml = $(text);
//console.log(xml);
$xml.find('entry').each(function () {
var entry = $(this);
// Skip non-alerts.
var cap_msgType = entry.find('cap_msgType');
if (cap_msgType.text() !== 'Alert') {
return true;
}
var link = entry.find('link');
var Url = link.attr('href');
HazardUrls.push(Url);
});
if (HazardUrls.length === 0) {
PopulateHazardConditions(WeatherParameters);
console.log(WeatherParameters.WeatherHazardConditions);
return;
}
$(HazardUrls).each(function () {
var Url = this.toString();
$.ajaxCORS({
type: 'GET',
url: Url,
dataType: 'xml',
crossDomain: true,
cache: true,
success: function (xml) {
var $xml = $(xml);
console.log(xml);
var description = $xml.find('description');
WeatherParameters.WeatherHazardConditions.Hazards.push(description.text());
HazardCounter++;
if (HazardCounter === HazardUrls.length) {
PopulateHazardConditions(WeatherParameters);
console.log(WeatherParameters.WeatherHazardConditions);
}
},
error: function () {
console.error('GetWeatherHazards3 failed for Url: ' + Url);
WeatherParameters.Progress.Hazards = LoadStatuses.Failed;
},
});
});
},
error: function (xhr, error, errorThrown) {
console.error('GetWeatherHazards3 failed: ' + errorThrown);
WeatherParameters.Progress.Hazards = LoadStatuses.Failed;
},
});
};
var canvasProgress_mousemove = function (e) {
canvasProgress.css('cursor', '');
var RatioX = canvasProgress.width() / 640;
var RatioY = canvasProgress.height() / 480;
if (e.offsetX >= (70 * RatioX) && e.offsetX <= (565 * RatioX)) {
//if (e.offsetY >= (105 * RatioY) && e.offsetY <= (350 * RatioY))
if (e.offsetY >= (100 * RatioY) && e.offsetY <= (385 * RatioY)) {
// Show hand cursor.
canvasProgress.css('cursor', 'pointer');
}
}
};
var PopulateHazardConditions = function (WeatherParameters) {
if (WeatherParameters === null || (_DontLoadGifs && WeatherParameters.Progress.Hazards !== LoadStatuses.Loaded)) {
return;
}
var WeatherHazardConditions = WeatherParameters.WeatherHazardConditions;
var ZoneId = WeatherHazardConditions.ZoneId;
var Text;
var Line;
var SkipLine;
var DontLoadGifs = _DontLoadGifs;
divHazards.empty();
$(WeatherHazardConditions.Hazards).each(function () {
//Text = this.replaceAll("\n", "<br/>");
//divHazards.html(divHazards.html() + "<br/><br/>" + Text);
Text = this.toString();
SkipLine = false;
Text = Text.replaceAll('\n', ' ');
//Text = Text.replaceAll("*** ", "");
//$(Text.split("\n")).each(function ()
$(Text.split(' ')).each(function () {
Line = this.toString();
Line = Line.toUpperCase();
if (Line.startsWith('&&')) {
return false;
} else if (Line.startsWith('$$')) {
return false;
}
if (SkipLine) {
if (Line === '') {
SkipLine = false;
return true;
}
return true;
}
if (Line.startsWith(ZoneId)) {
SkipLine = true;
return true;
} else if (Line.indexOf('>') !== -1) {
SkipLine = true;
return true;
} else if (Line.indexOf('LAT...LON ') !== -1) {
SkipLine = true;
return true;
}
//divHazards.html(divHazards.html() + "<br/>" + Line);
if (Line.indexOf('.') === 0 || Line.indexOf('*') === 0) {
divHazards.html(divHazards.html() + '<br/><br/>');
if (Line.indexOf('.') === 0 && Line.indexOf('...') !== 0) {
Line = Line.substr(1);
}
}
divHazards.html(divHazards.html() + Line + ' ');
});
divHazards.html(divHazards.html() + '<br/><br/>');
});
var DrawHazards = function () {
// Draw canvas
var canvas = canvasHazards[0];
var context = canvas.getContext('2d');
var BackGroundImage = new Image();
BackGroundImage.onload = function () {
context.drawImage(BackGroundImage, 0, 0);
if (DontLoadGifs) {
UpdateHazards();
}
if (WeatherHazardConditions.Hazards.length > 0) {
WeatherParameters.Progress.Hazards = LoadStatuses.Loaded;
} else {
WeatherParameters.Progress.Hazards = LoadStatuses.NoData;
}
UpdateWeatherCanvas(WeatherParameters, canvasHazards);
};
BackGroundImage.src = 'images/BackGround7.png';
};
var HazardsText = divHazards.html();
HazardsText = HazardsText.replaceAll('<br>', '\n');
HazardsText = HazardsText.replaceAll('<br/>', '\n');
HazardsText = HazardsText.replaceAll('<br />', '\n');
HazardsText = HazardsText.replaceAll('<br></br>', '\n');
WeatherHazardConditions.HazardsText = HazardsText;
WeatherHazardConditions.HazardsTextC = ConvertConditionsToMetric(HazardsText);
if (_Units === Units.Metric) {
HazardsText = WeatherHazardConditions.HazardsTextC;
}
var HazardsWrapped = HazardsText.wordWrap(32);
var cnvHazardsScroll;
var ShowHazardsScroll = function () {
var cnvHazardsScrollId;
var context;
cnvHazardsScrollId = 'cnvHazardsScroll';
var HazardsWrappedLines = HazardsWrapped.split('\n');
var MaxHazardsWrappedLines = 365;
if (_OperatingSystem === OperatingSystems.Andriod) {
MaxHazardsWrappedLines = 92;
}
if (HazardsWrappedLines.length > MaxHazardsWrappedLines) {
HazardsWrappedLines = HazardsWrappedLines.splice(0, MaxHazardsWrappedLines - 1);
}
var height = 0 + (HazardsWrappedLines.length * 45);
if (DontLoadGifs === false) {
// Clear the current image.
divHazardsScroll.empty();
divHazardsScroll.html('<canvas id=\'' + cnvHazardsScrollId + '\' />');
cnvHazardsScroll = $('#' + cnvHazardsScrollId);
cnvHazardsScroll.attr('width', '640'); // For Chrome.
cnvHazardsScroll.attr('height', height); // For Chrome.
}
cnvHazardsScroll = $('#' + cnvHazardsScrollId);
context = cnvHazardsScroll[0].getContext('2d');
DrawBox(context, 'rgb(112, 35, 35)', 0, 0, 640, height);
//var y = 0;
var y = 45;
$(HazardsWrappedLines).each(function () {
var HazardLine = this.toString();
DrawText(context, 'Star4000', '24pt', '#FFFFFF', 80, y, HazardLine, 1);
y += 45;
});
DrawHazards();
};
ShowHazardsScroll();
};
var UpdateHazards = function (Offset) {
var canvas = canvasHazards[0];
var context = canvas.getContext('2d');
var cnvHazardsScroll = $('#cnvHazardsScroll');
switch (Offset) {
case undefined:
break;
case 0:
_UpdateHazardsY = 0;
break;
case Infinity:
_UpdateHazardsY = cnvHazardsScroll.height();
break;
default:
_UpdateHazardsY += (385 * Offset);
if (_UpdateHazardsY > cnvHazardsScroll.height()) {
_UpdateHazardsY = cnvHazardsScroll.height();
} else if (_UpdateHazardsY < 0) {
_UpdateHazardsY = 0;
}
break;
}
DrawBox(context, 'rgb(112, 35,35)', 0, 0, 640, 385);
context.drawImage(cnvHazardsScroll[0], 0, _UpdateHazardsY, 640, 385, 0, 0, 640, 385);
};

File diff suppressed because one or more lines are too long

View file

@ -1,364 +0,0 @@
@font-face
{
font-family: "Star4000";
src: url('../fonts/Star4000.woff') format('woff');
}
body
{
font-family: "Star4000";
}
input, button
{
font-family: "Star4000";
}
#imgGetGps
{
height: 13px;
vertical-align: middle;
}
#txtAddress
{
width: 490px;
font-size: 16pt;
}
#btnGetGps, #btnGetLatLng, #btnClearQuery
{
font-size: 16pt;
}
.autocomplete-suggestions
{
background-color: #ffffff;
border: 1px solid #000000;
/*overflow: auto;*/
}
.autocomplete-suggestion
{
/*padding: 2px 5px;*/
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
font-size: 16pt;
}
.autocomplete-selected
{
background-color: #0000ff;
color: #ffffff;
}
#divTwc
{
display: block;
background-color: #000000;
color: #ffffff;
width: 100%;
max-width: 640px;
}
#divTwcLeft
{
display: none;
text-align: right;
flex-direction: column;
vertical-align: middle;
}
#divTwcLeft > div
{
flex: 1;
padding-right: 12px;
display: flex;
flex-direction: column;
justify-content: center;
}
#divTwcRight
{
text-align: left;
display: none;
flex-direction: column;
vertical-align: middle;
}
#divTwcRight > div
{
flex: 1;
padding-left: 12px;
display: flex;
flex-direction: column;
justify-content: center;
}
#divTwcBottom
{
/* visibility: hidden; */
display: flex;
flex-direction: row;
background-color: #000000;
color: #ffffff;
width: 100%;
}
#divTwcBottom > div
{
padding-left: 6px;
padding-right: 6px;
}
#divTwcBottomLeft
{
flex: 1;
text-align: left;
}
#divTwcBottomMiddle
{
flex: 0;
text-align: center;
}
#divTwcBottomRight
{
flex: 1;
text-align: right;
}
#divTwcNavContainer
{
display: none;
}
#divTwcNav
{
width: 100%;
display: flex;
flex-direction: row;
background-color: #000000;
color: #ffffff;
max-width: 640px;
}
#divTwcNav > div
{
padding-left: 6px;
padding-right: 6px;
}
#divTwcNavLeft
{
flex: 1;
text-align: left;
}
#divTwcNavMiddle
{
flex: 0;
text-align: center;
}
#divTwcNavRight
{
flex: 1;
text-align: right;
}
#imgPause1x, #imgPause2x
{
visibility: hidden;
position: absolute;
}
.HideCursor
{
cursor: none !important;
}
#txtScrollText
{
width: 475px;
}
@font-face
{
font-family: "Star4000";
src: url('../fonts/Star4000.woff') format('woff');
}
@font-face
{
font-family: "Star 4 Radar";
src: url('../fonts/Star 4 Radar.woff') format('woff');
}
@font-face
{
font-family: 'Star4000 Extended';
src: url('../fonts/Star4000 Extended.woff') format('woff');
}
@font-face
{
font-family: 'Star4000LCN';
src: url('../fonts/Star4000LCN.woff') format('woff');
}
@font-face
{
font-family: 'Star4000 Large Compressed';
src: url('../fonts/Star4000 Large Compressed.woff') format('woff');
}
@font-face
{
font-family: 'Star4000 Large';
src: url('../fonts/Star4000 Large.woff') format('woff');
}
@font-face
{
font-family: 'Star4000 Small';
src: url('../fonts/Star4000 Small.woff') format('woff');
}
#display
{
font-family: "Star4000";
margin: 0 0 0 0;
/* overflow: hidden; */
width: 100%;
/* height: 480px; */
/* max-width: 640px; */
}
jsgif
{
display: none;
}
#Star4000
{
font-family: 'Star4000';
}
#Star4000Extended
{
font-family: 'Star4000 Extended';
}
#Star4000LargeCompressed
{
font-family: 'Star4000 Large Compressed';
}
#Star4000Large
{
font-family: 'Star4000 Large';
}
#Star4000LargeCompressedNumbers
{
font-family: 'Star4000LCN';
}
#Star4000Small
{
font-family: 'Star4000 Small';
}
#Star4Radar
{
font-family: 'Star 4 Radar';
}
#container {
position: relative;
width: 100%;
/* max-width: 640px; */
height: 100%;
max-height: 480;
background-image: url(../images/BackGround1_1.png);
}
#divTwc:fullscreen #container {
background-image: none;
}
#loading {
width: 640px;
height: 480px;
max-width: 100%;
text-shadow: 4px 4px black;
display: flex;
align-items: center;
text-align: center;
justify-content: center;
}
#loading .title {
font-family: Star4000 Large;
font-size: 36px;
color: yellow;
margin-bottom: 40px;
}
#loading .instructions {font-size: 18pt;}
#container canvas {
/* position: absolute; */
width: 100%;
/* max-width: 640px; */
}
.heading {
font-weight: bold;
margin-top: 15px;
}
#enabledDisplays {
margin-bottom: 15px;
}
#enabledDisplays label {
display: block;
max-width: 300px;
}
#divTwcBottom img {
zoom: 150%;
}
#divTwc:fullscreen {
display:flex;
align-items: center;
justify-content: center;
align-content: center;
}
#divTwc:fullscreen #display {
position: relative;
}
#divTwc:fullscreen #divTwcBottom {
display: flex;
flex-direction: row;
background-color: rgb(0 0 0 / 0.5);
color: #ffffff;
width: 100%;
position: absolute;
bottom: 0px;
}
@media screen and (orientation: portrait) {
#divTwc:fullscreen canvas {
width: 100vw;
max-width: 100vw;
height: auto;
}
#divTwc:fullscreen #container {
width: 100vw;
height: auto;
max-height: unset;
max-width: unset;
}
}
@media screen and (orientation: landscape) {
#divTwc:fullscreen canvas {
height: 100vh;
max-height: 100vh;
width: auto;
}
#divTwc:fullscreen #container {
height: 100vh;
width: auto;
max-width: 100vw;
max-height: unset;
}
}
.navButton {
cursor: pointer;
}
.visible {
visibility: visible;
opacity: 1;
transition: opacity 1s linear;
}
.hidden {
visibility: hidden;
opacity: 0;
transition: visibility 0s 1s, opacity 1s linear
}

1
server/styles/main.css Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,85 @@
@use 'shared/_colors'as c;
@use 'shared/_utils'as u;
#almanac-html.weather-display {
background-image: url('../images/BackGround3_1.png');
}
.weather-display .main.almanac {
font-family: 'Star4000';
font-size: 24pt;
@include u.text-shadow();
.sun {
display: table;
margin-left: 50px;
height: 100px;
&>div {
display: table-row;
position: relative;
&>div {
display: table-cell;
}
}
.days {
color: c.$column-header-text;
text-align: right;
top: -5px;
.day {
padding-right: 10px;
}
}
.times {
text-align: right;
.sun-time {
width: 200px;
}
&.times-1 {
top: -10px;
}
&.times-2 {
top: -15px;
}
}
}
.moon {
position: relative;
top: -10px;
padding: 0px 60px;
.title {
color: c.$column-header-text;
}
.day {
display: inline-block;
text-align: center;
width: 130px;
.icon {
// shadow in image make it look off center
padding-left: 10px;
}
.date {
position: relative;
top: -10px;
}
}
}
}

View file

@ -0,0 +1,96 @@
@use 'shared/_colors'as c;
@use 'shared/_utils'as u;
.weather-display .main.current-weather {
&.main {
.col {
height: 50px;
width: 255px;
display: inline-block;
margin-top: 10px;
position: absolute;
@include u.text-shadow();
&.left {
font-family: 'Star4000 Extended';
font-size: 24pt;
}
&.right {
right: 0px;
font-family: 'Star4000 Large';
font-size: 16pt;
font-weight: bold;
.row {
margin-bottom: 12px;
.label,
.value {
display: inline-block;
}
.label {
margin-left: 20px;
}
.value {
float: right;
margin-right: 10px;
}
}
}
}
.center {
text-align: center;
}
.temp {
font-family: 'Star4000 Large';
font-size: 24pt;
}
.condition {}
.icon {
height: 100px;
img {
max-width: 126px;
}
}
.wind-container {
margin-bottom: 10px;
&>div {
width: 45%;
display: inline-block;
margin: 0px;
}
.wind-label {
margin-left: 5px;
}
.wind {
text-align: right;
}
}
.wind-gusts {
margin-left: 5px;
}
.location {
color: c.$title-color;
margin-bottom: 10px;
}
}
}

View file

@ -0,0 +1,73 @@
@use 'shared/_colors'as c;
@use 'shared/_utils'as u;
#extended-forecast-html.weather-display {
background-image: url('../images/BackGround2_1.png');
}
.weather-display .main.extended-forecast {
.day-container {
margin-top: 16px;
margin-left: 27px;
}
.day {
@include u.text-shadow();
padding: 5px;
height: 285px;
width: 155px;
display: inline-block;
margin: 0px 15px;
font-family: 'Star4000';
font-size: 24pt;
.date {
text-transform: uppercase;
text-align: center;
color: c.$title-color;
}
.condition {
text-align: center;
height: 74px;
margin-top: 10px;
}
.icon {
text-align: center;
height: 75px;
img {
max-height: 75px;
}
}
.temperatures {
width: 100%;
margin-top: 5px;
.temperature-block {
display: inline-block;
width: 44%;
vertical-align: top;
>div {
text-align: center;
}
.value {
font-family: 'Star4000 Large';
margin-top: 4px;
}
&.lo .label {
color: c.$extended-low;
}
&.hi .label {
color: c.$title-color;
}
}
}
}
}

View file

@ -0,0 +1,95 @@
@use 'shared/_colors'as c;
@use 'shared/_utils'as u;
.weather-display .main.hourly {
&.main {
overflow-y: hidden;
.column-headers {
background-color: c.$column-header;
height: 20px;
position: absolute;
width: 100%;
}
.column-headers {
position: sticky;
top: 0px;
z-index: 5;
div {
display: inline-block;
font-family: 'Star4000 Small';
font-size: 24pt;
color: c.$column-header-text;
position: absolute;
top: -14px;
z-index: 5;
@include u.text-shadow();
}
.temp {
left: 355px;
}
.like {
left: 435px;
}
.wind {
left: 535px;
}
}
.hourly-lines {
min-height: 338px;
padding-top: 10px;
background: repeating-linear-gradient(0deg, c.$gradient-main-background-2 0px,
c.$gradient-main-background-1 136px,
c.$gradient-main-background-1 202px,
c.$gradient-main-background-2 338px,
);
.hourly-row {
font-family: 'Star4000 Large';
font-size: 24pt;
height: 72px;
color: c.$title-color;
@include u.text-shadow();
position: relative;
>div {
position: absolute;
white-space: pre;
top: 8px;
}
.hour {
left: 25px;
}
.icon {
left: 255px;
width: 70px;
text-align: center;
top: unset;
}
.temp {
left: 355px;
}
.like {
left: 425px;
}
.wind {
left: 505px;
width: 100px;
text-align: right;
}
}
}
}
}

View file

@ -0,0 +1,72 @@
@use 'shared/_colors'as c;
@use 'shared/_utils'as u;
.weather-display .latest-observations {
&.main {
overflow-y: hidden;
.column-headers {
height: 20px;
position: absolute;
width: 100%;
}
.column-headers {
top: 0px;
div {
display: inline-block;
font-family: 'Star4000 Small';
font-size: 24pt;
position: absolute;
top: -14px;
@include u.text-shadow();
}
.temp {
// hidden initially for english/metric switching
display: none;
&.show {
display: inline-block;
}
}
}
.temp {
left: 230px;
}
.weather {
left: 280px;
}
.wind {
left: 430px;
}
.observation-lines {
min-height: 338px;
padding-top: 10px;
.observation-row {
font-family: 'Star4000';
font-size: 24pt;
@include u.text-shadow();
position: relative;
height: 40px;
>div {
position: absolute;
top: 8px;
}
.wind {
white-space: pre;
text-align: right;
}
}
}
}
}

View file

@ -0,0 +1,26 @@
@use 'shared/_colors'as c;
@use 'shared/_utils'as u;
.weather-display .local-forecast {
.container {
position: relative;
top: 15px;
margin: 0px 10px;
box-sizing: border-box;
height: 280px;
overflow: hidden;
}
.forecasts {
position: relative;
}
.forecast {
font-family: 'Star4000';
font-size: 24pt;
text-transform: uppercase;
@include u.text-shadow();
min-height: 280px;
line-height: 40px;
}
}

View file

@ -0,0 +1,331 @@
@font-face {
font-family: "Star4000";
src: url('../fonts/Star4000.woff') format('woff');
}
body {
font-family: "Star4000";
}
input,
button {
font-family: "Star4000";
}
#imgGetGps {
height: 13px;
vertical-align: middle;
}
#txtAddress {
width: 490px;
font-size: 16pt;
}
#btnGetGps,
#btnGetLatLng,
#btnClearQuery {
font-size: 16pt;
}
.autocomplete-suggestions {
background-color: #ffffff;
border: 1px solid #000000;
/*overflow: auto;*/
}
.autocomplete-suggestion {
/*padding: 2px 5px;*/
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
font-size: 16pt;
}
.autocomplete-selected {
background-color: #0000ff;
color: #ffffff;
}
#divTwc {
display: block;
background-color: #000000;
color: #ffffff;
width: 100%;
max-width: 640px;
}
#divTwcLeft {
display: none;
text-align: right;
flex-direction: column;
vertical-align: middle;
}
#divTwcLeft>div {
flex: 1;
padding-right: 12px;
display: flex;
flex-direction: column;
justify-content: center;
}
#divTwcRight {
text-align: left;
display: none;
flex-direction: column;
vertical-align: middle;
}
#divTwcRight>div {
flex: 1;
padding-left: 12px;
display: flex;
flex-direction: column;
justify-content: center;
}
#divTwcBottom {
/* visibility: hidden; */
display: flex;
flex-direction: row;
background-color: #000000;
color: #ffffff;
width: 100%;
}
#divTwcBottom>div {
padding-left: 6px;
padding-right: 6px;
}
#divTwcBottomLeft {
flex: 1;
text-align: left;
}
#divTwcBottomMiddle {
flex: 0;
text-align: center;
}
#divTwcBottomRight {
flex: 1;
text-align: right;
}
#divTwcNavContainer {
display: none;
}
#divTwcNav {
width: 100%;
display: flex;
flex-direction: row;
background-color: #000000;
color: #ffffff;
max-width: 640px;
}
#divTwcNav>div {
padding-left: 6px;
padding-right: 6px;
}
#divTwcNavLeft {
flex: 1;
text-align: left;
}
#divTwcNavMiddle {
flex: 0;
text-align: center;
}
#divTwcNavRight {
flex: 1;
text-align: right;
}
#imgPause1x,
#imgPause2x {
visibility: hidden;
position: absolute;
}
.HideCursor {
cursor: none !important;
}
#txtScrollText {
width: 475px;
}
@font-face {
font-family: "Star4000";
src: url('../fonts/Star4000.woff') format('woff');
}
@font-face {
font-family: "Star 4 Radar";
src: url('../fonts/Star 4 Radar.woff') format('woff');
}
@font-face {
font-family: 'Star4000 Extended';
src: url('../fonts/Star4000 Extended.woff') format('woff');
}
@font-face {
font-family: 'Star4000LCN';
src: url('../fonts/Star4000LCN.woff') format('woff');
}
@font-face {
font-family: 'Star4000 Large Compressed';
src: url('../fonts/Star4000 Large Compressed.woff') format('woff');
}
@font-face {
font-family: 'Star4000 Large';
src: url('../fonts/Star4000 Large.ttf') format('truetype');
}
@font-face {
font-family: 'Star4000 Small';
src: url('../fonts/Star4000 Small.woff') format('woff');
}
#display {
font-family: "Star4000";
margin: 0 0 0 0;
width: 100%;
}
jsgif {
display: none;
}
#Star4000 {
font-family: 'Star4000';
}
#Star4000Extended {
font-family: 'Star4000 Extended';
}
#Star4000LargeCompressed {
font-family: 'Star4000 Large Compressed';
}
#Star4000Large {
font-family: 'Star4000 Large';
}
#Star4000LargeCompressedNumbers {
font-family: 'Star4000LCN';
}
#Star4000Small {
font-family: 'Star4000 Small';
}
#Star4Radar {
font-family: 'Star 4 Radar';
}
#container {
position: relative;
width: 100%;
height: 100%;
background-image: url(../images/BackGround1_1.png);
}
#divTwc:fullscreen #container {
background-image: none;
width: unset;
height: unset;
}
#loading {
width: 640px;
height: 480px;
max-width: 100%;
text-shadow: 4px 4px black;
display: flex;
align-items: center;
text-align: center;
justify-content: center;
}
#loading .title {
font-family: Star4000 Large;
font-size: 36px;
color: yellow;
margin-bottom: 40px;
}
#loading .instructions {
font-size: 18pt;
}
#container canvas {
/* position: absolute; */
width: 100%;
/* max-width: 640px; */
}
.heading {
font-weight: bold;
margin-top: 15px;
}
#enabledDisplays {
margin-bottom: 15px;
}
#enabledDisplays label {
display: block;
max-width: 300px;
}
#divTwcBottom img {
zoom: 150%;
}
#divTwc:fullscreen {
display: flex;
align-items: center;
justify-content: center;
align-content: center;
}
#divTwc:fullscreen #display {
position: relative;
}
#divTwc:fullscreen #divTwcBottom {
display: flex;
flex-direction: row;
background-color: rgb(0 0 0 / 0.5);
color: #ffffff;
width: 100%;
position: absolute;
bottom: 0px;
}
.navButton {
cursor: pointer;
}
.visible {
visibility: visible;
opacity: 1;
transition: opacity 1s linear;
}
.hidden {
visibility: hidden;
opacity: 0;
transition: visibility 0s 1s, opacity 1s linear
}

View file

@ -0,0 +1,150 @@
@use 'shared/_colors'as c;
@use 'shared/_utils'as u;
.weather-display .progress {
@include u.text-shadow();
font-family: 'Star4000 Extended';
font-size: 19pt;
.container {
position: relative;
top: 15px;
margin: 0px 10px;
box-sizing: border-box;
height: 310px;
overflow: hidden;
.item {
position: relative;
.name {
white-space: nowrap;
&::after {
content: '........................................................................';
}
}
.links {
position: absolute;
text-align: right;
right: 0px;
top: 0px;
>div {
background-color: c.$blue-box;
display: none;
padding-left: 4px;
}
.loading {
color: #ffff00;
}
.press-here {
color: #00ff00;
cursor: pointer;
}
.failed {
color: #ff0000;
}
.no-data {
color: #C0C0C0;
}
.disabled {
color: #C0C0C0;
}
&.loading .loading {
display: block;
}
&.press-here .press-here {
display: block;
}
&.failed .failed {
display: block;
}
&.no-data .no-data {
display: block;
}
&.disabled .disabled {
display: block;
}
}
}
}
}
#progress-html.weather-display .scroll {
@keyframes progress-scroll {
0% {
background-position: -40px 0;
}
100% {
background-position: 40px 0;
}
}
.progress-bar-container {
border: 2px solid black;
background-color: white;
margin: 20px auto;
width: 524px;
position: relative;
display: none;
&.show {
display: block;
}
.progress-bar {
height: 20px;
margin: 2px;
width: 520px;
background: repeating-linear-gradient(90deg,
c.$gradient-loading-1 0px,
c.$gradient-loading-1 5px,
c.$gradient-loading-2 5px,
c.$gradient-loading-2 10px,
c.$gradient-loading-3 10px,
c.$gradient-loading-3 15px,
c.$gradient-loading-4 15px,
c.$gradient-loading-4 20px,
c.$gradient-loading-3 20px,
c.$gradient-loading-3 25px,
c.$gradient-loading-2 25px,
c.$gradient-loading-2 30px,
c.$gradient-loading-1 30px,
c.$gradient-loading-1 40px,
);
// animation
animation-duration: 2s;
animation-fill-mode: forwards;
animation-iteration-count: infinite;
animation-name: progress-scroll;
animation-timing-function: steps(8, end);
}
.cover {
position: absolute;
top: 0px;
right: 0px;
background-color: white;
width: 100%;
height: 24px;
transition: width 1s steps(6);
}
}
}

View file

@ -0,0 +1,114 @@
@use 'shared/_colors'as c;
@use 'shared/_utils'as u;
#radar-html.weather-display {
background-image: url('../images/BackGround4_1.png');
.header {
height: 83px;
.title.dual {
color: white;
font-family: 'Arial', sans-serif;
font-weight: bold;
font-size: 28pt;
left: 155px;
.top {
top: -4px;
}
.bottom {
top: 31px;
}
}
.right {
position: absolute;
right: 0px;
width: 360px;
margin-top: 2px;
font-family: 'Star4000';
font-size: 18pt;
font-weight: bold;
@include u.text-shadow();
text-align: center;
.scale>div {
display: inline-block;
}
.scale-table {
display: table-row;
border-collapse: collapse;
.box {
display: table-cell;
border: 2px solid black;
width: 17px;
height: 24px;
padding: 0
}
.box-1 {
background-color: rgb(49, 210, 22);
}
.box-2 {
background-color: rgb(28, 138, 18);
}
.box-3 {
background-color: rgb(20, 90, 15);
}
.box-4 {
background-color: rgb(10, 40, 10);
}
.box-5 {
background-color: rgb(196, 179, 70);
}
.box-6 {
background-color: rgb(190, 72, 19);
}
.box-7 {
background-color: rgb(171, 14, 14);
}
.box-8 {
background-color: rgb(115, 31, 4);
}
}
.scale {
.text {
position: relative;
top: -5px;
}
}
.time {
position: relative;
font-weight: normal;
top: -14px;
font-family: 'Star4000 Small';
font-size: 24pt;
}
}
}
}
.weather-display .main.radar {
overflow: hidden;
height: 367px;
.container {
.scroll-area {
position: relative;
}
}
}

View file

@ -0,0 +1,51 @@
@use 'shared/_colors'as c;
@use 'shared/_utils'as u;
#regional-forecast-html.weather-display {
background-image: url('../images/BackGround5_1.png');
}
.weather-display .main.regional-forecast {
position: relative;
.map {
position: absolute;
}
.location {
position: absolute;
width: 140px;
margin-left: -40px;
margin-top: -35px;
>div {
position: absolute;
@include u.text-shadow();
}
.icon {
top: 26px;
left: 44px;
img {
max-height: 32px;
}
}
.temp {
font-family: 'Star4000 Large';
font-size: 28px;
color: c.$title-color;
top: 28px;
text-align: right;
width: 40px;
}
.city {
font-family: Star4000;
font-size: 20px;
}
}
}

View file

@ -0,0 +1,103 @@
@use 'shared/_colors'as c;
@use 'shared/_utils'as u;
.weather-display .main.travel {
&.main {
overflow-y: hidden;
.column-headers {
background-color: c.$column-header;
height: 20px;
position: absolute;
width: 100%;
}
.column-headers {
position: sticky;
top: 0px;
z-index: 5;
div {
display: inline-block;
font-family: 'Star4000 Small';
font-size: 24pt;
color: c.$column-header-text;
position: absolute;
top: -14px;
z-index: 5;
@include u.text-shadow();
}
.temp {
width: 50px;
text-align: center;
&.low {
left: 455px;
}
&.high {
left: 510px;
width: 60px;
}
}
}
.travel-lines {
min-height: 338px;
padding-top: 10px;
background: repeating-linear-gradient(0deg, c.$gradient-main-background-2 0px,
c.$gradient-main-background-1 136px,
c.$gradient-main-background-1 202px,
c.$gradient-main-background-2 338px,
);
.travel-row {
font-family: 'Star4000 Large';
font-size: 24pt;
height: 72px;
color: c.$title-color;
@include u.text-shadow();
position: relative;
>div {
position: absolute;
white-space: pre;
top: 8px;
}
.city {
left: 80px;
}
.icon {
left: 330px;
width: 70px;
text-align: center;
top: unset;
img {
max-width: 47px;
}
}
.temp {
width: 50px;
text-align: center;
&.low {
left: 455px;
}
&.high {
left: 510px;
width: 60px;
}
}
}
}
}
}

View file

@ -0,0 +1,123 @@
@use 'shared/_colors'as c;
@use 'shared/_utils'as u;
.weather-display {
width: 640px;
height: 480px;
overflow: hidden;
position: relative;
background-image: url(../images/BackGround1_1.png);
/* this method is required to hide blocks so they can be measured while off screen */
height: 0px;
&.show {
height: 480px;
}
.template {
display: none;
}
.header {
width: 640px;
height: 60px;
padding-top: 30px;
.title {
color: c.$title-color;
@include u.text-shadow(3px, 1.5px);
font-family: 'Star4000';
font-size: 24pt;
position: absolute;
width: 250px;
&.single {
left: 170px;
top: 25px;
}
&.dual {
left: 170px;
&>div {
position: absolute;
}
.top {
top: -3px;
}
.bottom {
top: 26px;
}
}
}
.logo {
top: 30px;
left: 50px;
position: absolute;
z-index: 10;
}
.noaa-logo {
position: absolute;
top: 39px;
left: 356px;
}
.title.single {
top: 40px;
}
.date-time {
white-space: pre;
color: c.$date-time;
font-family: 'Star4000 Small';
font-size: 24pt;
@include u.text-shadow(3px, 1.5px);
left: 415px;
width: 170px;
text-align: right;
position: absolute;
&.date {
padding-top: 22px;
}
}
}
.main {
position: relative;
&.has-scroll {
width: 640px;
height: 310px;
overflow: hidden;
}
&.has-box {
margin-left: 64px;
margin-right: 64px;
width: calc(100% - 128px);
}
}
.scroll {
@include u.text-shadow(3px, 1.5px);
width: 640px;
height: 70px;
overflow: hidden;
margin-top: 10px;
.fixed {
font-family: 'Star4000';
font-size: 24pt;
margin-left: 55px;
}
}
}

View file

@ -0,0 +1,12 @@
@import 'page';
@import 'weather-display';
@import 'current-weather';
@import 'extended-forecast';
@import 'hourly';
@import 'travel';
@import 'latest-observations';
@import 'local-forecast';
@import 'progress';
@import 'radar';
@import 'regional-forecast';
@import 'almanac';

View file

@ -0,0 +1,17 @@
$title-color: yellow;
$date-time: white;
$text-shadow: black;
$column-header-text: yellow;
$column-header: rgb(32, 0, 87);
$gradient-main-background-1: #102080;
$gradient-main-background-2: #001040;
$gradient-loading-1: #09246f;
$gradient-loading-2: #364ac0;
$gradient-loading-3: #4f99f9;
$gradient-loading-4: #8ffdfa;
$extended-low: #8080FF;
$blue-box: #26235a;

View file

@ -0,0 +1,17 @@
@use 'colors'as c;
@mixin text-shadow($offset: 3px, $outline: 1.5px) {
/* eventually, when chrome supports paint-order for html elements */
/* -webkit-text-stroke: 2px black; */
/* paint-order: stroke fill; */
text-shadow:
$offset $offset 0 c.$text-shadow,
(-$outline) (-$outline) 0 c.$text-shadow,
0 (-$outline) 0 c.$text-shadow,
$outline (-$outline) 0 c.$text-shadow,
$outline 0 0 c.$text-shadow,
$outline $outline 0 c.$text-shadow,
0 $outline 0 c.$text-shadow,
(-$outline) $outline 0 c.$text-shadow,
(-$outline) 0 0 c.$text-shadow;
}

View file

@ -1,139 +1,165 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml"> <html lang="en" xmlns="http://www.w3.org/1999/xhtml">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<link rel="preload" href="fonts/Star4000.woff" as="font" crossorigin="anonymous" /> <title>WeatherStar 4000+</title>
<link rel="preload" href="fonts/Star 4 Radar.woff" as="font" crossorigin="anonymous" /> <meta name="description" content="Web based WeatherStar 4000 simulator that reports current and forecast weather conditions plus a few extras!" />
<link rel="preload" href="fonts/Star4000 Extended.woff" as="font" crossorigin="anonymous" /> <meta name="keywords" content="WeatherStar 4000+" />
<link rel="preload" href="fonts/Star4000 Large Compressed.woff" as="font" crossorigin="anonymous" /> <meta name="author" content="Matt Walsh" />
<link rel="preload" href="fonts/Star4000 Large.woff" as="font" crossorigin="anonymous" /> <meta name="application-name" content="WeatherStar 4000+" />
<link rel="preload" href="fonts/Star4000 Small.woff" as="font" crossorigin="anonymous" />
<title>WeatherStar 4000+</title> <meta name="viewport" content="width=device-width,initial-scale=1">
<meta name="description" content="Web based WeatherStar 4000 simulator that reports current and forecast weather conditions plus a few extras!" /> <meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="keywords" content="WeatherStar 4000+" /> <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="author" content="Matt Walsh" /> <link rel="manifest" href="manifest.json" />
<meta name="application-name" content="WeatherStar 4000+" /> <link rel="icon" href="images/Logo192.png" />
<meta name="viewport" content="width=device-width,initial-scale=1"> <% if (production) { %>
<meta name="apple-mobile-web-app-capable" content="yes" /> <link rel="stylesheet" type="text/css" href="resources/ws.min.css?_=<%=production%>" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" /> <script type="text/javascript" src="resources/data.min.js?_=<%=production%>"></script>
<link rel="manifest" href="manifest.json" /> <script type="text/javascript" src="resources/ws.min.js?_=<%=production%>"></script>
<link rel="icon" href="images/Logo192.png" /> <% } else { %>
<link rel="stylesheet" type="text/css" href="styles/main.css" />
<script type="text/javascript" src="scripts/vendor/auto/jquery.js"></script>
<script type="text/javascript" src="scripts/vendor/jquery.autocomplete.min.js"></script>
<script type="text/javascript" src="scripts/vendor/auto/nosleep.js"></script>
<script type="text/javascript" src="scripts/vendor/auto/swiped-events.js"></script>
<script type="text/javascript" src="scripts/index.js"></script>
<script type="text/javascript" src="scripts/data/states.js"></script>
<% if (production) { %> <script type="text/javascript" src="scripts/vendor/auto/luxon.js"></script>
<link rel="stylesheet" type="text/css" href="resources/ws.min.css?_=<%=production%>" /> <script type="text/javascript" src="scripts/vendor/auto/suncalc.js"></script>
<script type="text/javascript" src="resources/data.min.js?_=<%=production%>"></script> <script type="text/javascript" src="scripts/data/travelcities.js"></script>
<script type="text/javascript" src="resources/ws.min.js?_=<%=production%>"></script> <script type="text/javascript" src="scripts/data/regionalcities.js"></script>
<% } else { %> <script type="text/javascript" src="scripts/data/stations.js"></script>
<link rel="stylesheet" type="text/css" href="styles/index.css" /> <script type="text/javascript" src="scripts/modules/draw.js"></script>
<script type="text/javascript" src="scripts/vendor/auto/jquery.js"></script> <script type="text/javascript" src="scripts/modules/weatherdisplay.js"></script>
<script type="text/javascript" src="scripts/vendor/jquery.autocomplete.min.js"></script> <script type="text/javascript" src="scripts/modules/icons.js"></script>
<script type="text/javascript" src="scripts/vendor/auto/nosleep.js"></script> <script type="text/javascript" src="scripts/modules/utilities.js"></script>
<script type="text/javascript" src="scripts/vendor/auto/swiped-events.js"></script> <script type="text/javascript" src="scripts/modules/currentweather.js"></script>
<script type="text/javascript" src="scripts/vendor/jquery.touchswipe.min.js"></script> <script type="text/javascript" src="scripts/modules/currentweatherscroll.js"></script>
<script type="text/javascript" src="scripts/index.js"></script> <script type="text/javascript" src="scripts/modules/latestobservations.js"></script>
<script type="text/javascript" src="scripts/data/states.js"></script> <script type="text/javascript" src="scripts/modules/travelforecast.js"></script>
<script type="text/javascript" src="scripts/modules/regionalforecast.js"></script>
<script type="text/javascript" src="scripts/modules/localforecast.js"></script>
<script type="text/javascript" src="scripts/modules/extendedforecast.js"></script>
<script type="text/javascript" src="scripts/modules/almanac.js"></script>
<script type="text/javascript" src="scripts/modules/radar.js"></script>
<script type="text/javascript" src="scripts/modules/hourly.js"></script>
<script type="text/javascript" src="scripts/modules/progress.js"></script>
<script type="text/javascript" src="scripts/modules/navigation.js"></script>
<script type="text/javascript" src="scripts/libgif.js"></script> <% } %>
<script type="text/javascript" src="scripts/vendor/auto/luxon.js"></script>
<script type="text/javascript" src="scripts/vendor/auto/suncalc.js"></script>
<script type="text/javascript" src="scripts/data/travelcities.js"></script>
<script type="text/javascript" src="scripts/data/regionalcities.js"></script>
<script type="text/javascript" src="scripts/data/stations.js"></script>
<script type="text/javascript" src="scripts/modules/draw.js"></script>
<script type="text/javascript" src="scripts/modules/weatherdisplay.js"></script>
<script type="text/javascript" src="scripts/modules/icons.js"></script>
<script type="text/javascript" src="scripts/modules/utilities.js"></script>
<script type="text/javascript" src="scripts/modules/currentweather.js"></script>
<script type="text/javascript" src="scripts/modules/currentweatherscroll.js"></script>
<script type="text/javascript" src="scripts/modules/latestobservations.js"></script>
<script type="text/javascript" src="scripts/modules/travelforecast.js"></script>
<script type="text/javascript" src="scripts/modules/regionalforecast.js"></script>
<script type="text/javascript" src="scripts/modules/localforecast.js"></script>
<script type="text/javascript" src="scripts/modules/extendedforecast.js"></script>
<script type="text/javascript" src="scripts/modules/almanac.js"></script>
<script type="text/javascript" src="scripts/modules/radar.js"></script>
<script type="text/javascript" src="scripts/modules/hourly.js"></script>
<script type="text/javascript" src="scripts/modules/progress.js"></script>
<script type="text/javascript" src="scripts/modules/navigation.js"></script>
<% } %>
</head> </head>
<body> <body>
<div id="divQuery"> <div id="divQuery">
<form id="frmGetLatLng"> <form id="frmGetLatLng">
<input id="txtAddress" type="text" value="" placeholder="Zip or City, State" /><button id="btnGetGps" type="button" title="Get GPS Location"><img id="imgGetGps" src="images/nav/ic_gps_fixed_black_18dp_1x.png" /></button> <input id="txtAddress" type="text" value="" placeholder="Zip or City, State" /><button id="btnGetGps" type="button" title="Get GPS Location"><img id="imgGetGps" src="images/nav/ic_gps_fixed_black_18dp_1x.png" /></button>
<input id="btnGetLatLng" type="submit" value="GO" /> <input id="btnGetLatLng" type="submit" value="GO" />
<input id="btnClearQuery" type="reset" value="Reset" /> <input id="btnClearQuery" type="reset" value="Reset" />
</form> </form>
<div id="divLat"></div> <div id="divLat"></div>
<div id="divLng"></div> <div id="divLng"></div>
</div> </div>
<br /> <br />
<img id="imgPause1x" src="images/nav/ic_pause_white_24dp_1x.png" /> <img id="imgPause1x" src="images/nav/ic_pause_white_24dp_1x.png" />
<img id="imgPause2x" src="images/nav/ic_pause_white_24dp_2x.png" /> <img id="imgPause2x" src="images/nav/ic_pause_white_24dp_2x.png" />
<div id="version" style="display:none"><%- version %> </div> <div id="version" style="display:none">
<%- version %>
</div>
<div id="divTwc"> <div id="divTwc">
<div id="container"> <div id="container">
<div id="loading" width="640" height="480"> <div id="loading" width="640" height="480">
<div> <div>
<div class="title">WeatherStar 4000+</div> <div class="title">WeatherStar 4000+</div>
<div class="instructions">Enter your location above to continue</div> <div class="instructions">Enter your location above to continue</div>
</div>
</div>
</div>
<div id="divTwcBottom">
<div id="divTwcBottomLeft">
<img id="NavigateMenu" class="navButton" src="images/nav/ic_menu_white_24dp_1x.png" title="Menu" />
<img id="NavigatePrevious" class="navButton" src="images/nav/ic_skip_previous_white_24dp_1x.png" title="Previous" />
<img id="NavigateNext" class="navButton" src="images/nav/ic_skip_next_white_24dp_1x.png" title="Next" />
<img id="NavigatePlay" class="navButton" src="images/nav/ic_play_arrow_white_24dp_1x.png" title="Play" />
</div>
<div id="divTwcBottomMiddle">
<img id="NavigateRefresh" class="navButton" src="images/nav/ic_refresh_white_24dp_1x.png" title="Refresh" />
</div>
<div id="divTwcBottomRight">
<img id="ToggleFullScreen" class="navButton" src="images/nav/ic_fullscreen_exit_white_24dp_1x.png" title="Exit Fullscreen" />
</div>
</div> </div>
</div>
<div id="progress-html" class="weather-display">
<%- include('partials/progress.ejs') %>
</div>
<div id="hourly-html" class="weather-display">
<%- include('partials/hourly.ejs') %>
</div>
<div id="travel-html" class="weather-display">
<%- include('partials/travel.ejs') %>
</div>
<div id="current-weather-html" class="weather-display">
<%- include('partials/current-weather.ejs') %>
</div>
<div id="local-forecast-html" class="weather-display">
<%- include('partials/local-forecast.ejs') %>
</div>
<div id="latest-observations-html" class="weather-display">
<%- include('partials/latest-observations.ejs') %>
</div>
<div id="regional-forecast-html" class="weather-display">
<%- include('partials/regional-forecast.ejs') %>
</div>
<div id="almanac-html" class="weather-display">
<%- include('partials/almanac.ejs') %>
</div>
<div id="extended-forecast-html" class="weather-display">
<%- include('partials/extended-forecast.ejs') %>
</div>
<div id="radar-html" class="weather-display">
<%- include('partials/radar.ejs') %>
</div>
</div> </div>
<div id="divTwcBottom">
<br /> <div id="divTwcBottomLeft">
<img id="NavigateMenu" class="navButton" src="images/nav/ic_menu_white_24dp_1x.png" title="Menu" />
<div class="info"> <img id="NavigatePrevious" class="navButton" src="images/nav/ic_skip_previous_white_24dp_1x.png" title="Previous" />
<a href="https://github.com/netbymatt/ws4kp#weatherstar-4000">More information</a> <img id="NavigateNext" class="navButton" src="images/nav/ic_skip_next_white_24dp_1x.png" title="Next" />
</div> <img id="NavigatePlay" class="navButton" src="images/nav/ic_play_arrow_white_24dp_1x.png" title="Play" />
</div>
<div class='heading'>Selected displays</div> <div id="divTwcBottomMiddle">
<div id='enabledDisplays'> <img id="NavigateRefresh" class="navButton" src="images/nav/ic_refresh_white_24dp_1x.png" title="Refresh" />
</div>
</div> <div id="divTwcBottomRight">
<img id="ToggleFullScreen" class="navButton" src="images/nav/ic_fullscreen_exit_white_24dp_1x.png" title="Enter Fullscreen" />
<div id="divInfo"> </div>
Location: <span id="spanCity"></span> <span id="spanState"></span><br />
Station Id: <span id="spanStationId"></span><br />
Radar Id: <span id="spanRadarId"></span><br />
Zone Id: <span id="spanZoneId"></span><br />
</div> </div>
</div>
<div id="divRefresh"> <br />
Last Update: <span id="spanLastRefresh">(None)</span><br />
<input id="chkAutoRefresh" name="chkAutoRefresh" type="checkbox" /><label id="lblRefreshCountDown" for="chkAutoRefresh">Auto Refresh: <span id="spanRefreshCountDown">--:--</span></label>
</div>
<div id="divUnits"> <div class="info">
Units: <a href="https://github.com/netbymatt/ws4kp#weatherstar-4000">More information</a>
<input id="radEnglish" name="radUnits" type="radio" value="ENGLISH" /><label for="radEnglish">English</label> </div>
<input id="radMetric" name="radUnits" type="radio" value="METRIC" /><label for="radMetric">Metric</label>
</div> <div class='heading'>Selected displays</div>
<div id='enabledDisplays'>
</div>
<div id="divInfo">
Location: <span id="spanCity"></span> <span id="spanState"></span><br />
Station Id: <span id="spanStationId"></span><br />
Radar Id: <span id="spanRadarId"></span><br />
Zone Id: <span id="spanZoneId"></span><br />
</div>
<div id="divRefresh">
Last Update: <span id="spanLastRefresh">(None)</span><br />
<input id="chkAutoRefresh" name="chkAutoRefresh" type="checkbox" /><label id="lblRefreshCountDown" for="chkAutoRefresh">Auto Refresh: <span id="spanRefreshCountDown">--:--</span></label>
</div>
<div id="divUnits">
Units:
<input id="radEnglish" name="radUnits" type="radio" value="ENGLISH" /><label for="radEnglish">English</label>
<input id="radMetric" name="radUnits" type="radio" value="METRIC" /><label for="radMetric">Metric</label>
</div>
</body> </body>
</html> </html>

View file

@ -0,0 +1,31 @@
<%- include('header.ejs', {title:'Almanac', hasTime: true}) %>
<div class="main has-scroll almanac">
<div class="sun">
<div class="days">
<div class="day"></div>
<div class="day day-1">Monday</div>
<div class="day day-2">Tuesday</div>
</div>
<div class="times times-1">
<div class="name">Sunrise:</div>
<div class="sun-time rise-1">6:24 am</div>
<div class="sun-time rise-2">6:25 am</div>
</div>
<div class="times times-2">
<div class="name">Sunset:</div>
<div class="sun-time set-1">6:24 am</div>
<div class="sun-time set-2">6:25 am</div>
</div>
</div>
<div class="moon">
<div class="title">Moon Data:</div>
<div class="days">
<div class="day template">
<div class="type"></div>
<div class="icon"><img /></div>
<div class="date"></div>
</div>
</div>
</div>
</div>
<%- include('scroll.ejs') %>

View file

@ -0,0 +1,43 @@
<%- include('header.ejs', {titleDual:{ top: 'Current' , bottom: 'Conditions' }, noaaLogo: true}) %>
<div class="main has-scroll has-box current-weather">
<div class="weather template">
<div class="left col">
<div class="temp center"></div>
<div class="condition center"></div>
<div class="icon center"><img src="" /></div>
<div class="wind-container">
<div class="wind-label">Wind:</div>
<div class="wind"></div>
</div>
<div class="wind-gusts"></div>
</div>
<div class="right col">
<div class="location"></div>
<div class="row">
<div class="label">Humidity:</div>
<div class="humidity value"></div>
</div>
<div class="row">
<div class="label">Dewpoint:</div>
<div class="dewpoint value"></div>
</div>
<div class="row">
<div class="label">Ceiling:</div>
<div class="ceiling value"></div>
</div>
<div class="row">
<div class="label">Visibility:</div>
<div class="visibility value"></div>
</div>
<div class="row">
<div class="label">Pressure:</div>
<div class="pressure value"></div>
</div>
<div class="row">
<div class="heat-index-label label"></div>
<div class="heat-index value"></div>
</div>
</div>
</div>
</div>
<%- include('scroll.ejs') %>

View file

@ -0,0 +1,23 @@
<%- include('header.ejs', {titleDual:{ top: 'Extended' , bottom: 'Forecast' }, hasTime: true }) %>
<div class="main has-scroll extended-forecast">
<div class="day-container">
<div class="day template">
<div class="date"></div>
<div class="icon">
<img src="" />
</div>
<div class="condition"></div>
<div class="temperatures">
<div class="temperature-block lo">
<div class="label">Lo</div>
<div class="value value-lo"></div>
</div>
<div class="temperature-block hi">
<div class="label">Hi</div>
<div class="value value-hi"></div>
</div>
</div>
</div>
</div>
</div>
<%- include('scroll.ejs') %>

26
views/partials/header.ejs Normal file
View file

@ -0,0 +1,26 @@
<div class="header">
<div class="logo"><img src="images/Logo3.png" /></div>
<% if (locals?.titleDual) { %>
<div class="title dual">
<div class="top">
<%-titleDual.top %>
</div>
<div class="bottom">
<%-titleDual.bottom %>
</div>
</div>
<% } else { %>
<div class="title single">
<%-title %>
</div>
<% } %>
<% if (locals?.hasTime) { %>
<div class="date-time date"></div>
<div class="date-time time"></div>
<% } %>
<% if (locals?.noaaLogo) { %>
<div class="noaa-logo">
<img src="images/noaa5.gif" />
</div>
<%}%>
</div>

18
views/partials/hourly.ejs Normal file
View file

@ -0,0 +1,18 @@
<%- include('header.ejs', {title: 'Hourly Forecast' , hasTime: true }) %>
<div class="main has-scroll hourly">
<div class="column-headers">
<div class="temp">TEMP</div>
<div class="like">LIKE</div>
<div class="wind">WIND</div>
</div>
<div class="hourly-lines">
<div class="hourly-row template">
<div class="hour"></div>
<div class="icon"><img /></div>
<div class="temp"></div>
<div class="like"></div>
<div class="wind"></div>
</div>
</div>
</div>
<%- include('scroll.ejs') %>

View file

@ -0,0 +1,20 @@
<%- include('header.ejs', {titleDual:{ top: 'Latest' , bottom: 'Observations' }, noaaLogo: true }) %>
<div class="main has-scroll latest-observations has-box">
<div class="container">
<div class="column-headers">
<div class="temp english">&deg;F</div>
<div class="temp metric">&deg;C</div>
<div class="weather">Weather</div>
<div class="wind">Wind</div>
</div>
<div class="observation-lines">
<div class="observation-row template">
<div class="location"></div>
<div class="temp"></div>
<div class="weather"></div>
<div class="wind"></div>
</div>
</div>
</div>
</div>
<%- include('scroll.ejs') %>

View file

@ -0,0 +1,12 @@
<%- include('header.ejs', {titleDual:{ top: 'Local' , bottom: 'Forecast' }, hasTime: true, noaaLogo: true}) %>
<div class="main has-scroll has-box local-forecast">
<div class="container">
<div class="forecasts">
<div class="forecast template">
<div class="text">
</div>
</div>
</div>
</div>
</div>
<%- include('scroll.ejs') %>

View file

@ -0,0 +1,21 @@
<%- include('header.ejs', {titleDual:{ top: 'WeatherStar' , bottom: '4000+ v' + version }, hasTime: true}) %>
<div class="main has-box progress">
<div class="container">
<div class="item template">
<div class="name">Current Conditions</div>
<div class="links loading">
<div class="loading">Loading</div>
<div class="press-here">Press Here</div>
<div class="failed">Failed</div>
<div class="no-data">No Data</div>
<div class="disabled">Disabled</div>
</div>
</div>
</div>
</div>
<div class="scroll">
<div class="progress-bar-container show">
<div class="progress-bar"></div>
<div class="cover"></div>
</div>
</div>

43
views/partials/radar.ejs Normal file
View file

@ -0,0 +1,43 @@
<div class="header">
<div class="logo"><img src="images/Logo3.png" /></div>
<div class="title dual">
<div class="top">
Local
</div>
<div class="bottom">
Radar
</div>
</div>
<div class="right">
<div class="precip">
<div class="precip-header">PRECIP</div>
<div class="scale">
<div class="text">Light</div>
<div class="scale-table">
<div class="box box-1"></div>
<div class="box box-2"></div>
<div class="box box-3"></div>
<div class="box box-4"></div>
<div class="box box-5"></div>
<div class="box box-6"></div>
<div class="box box-7"></div>
<div class="box box-7"></div>
</div>
<div class="text">Heavy</div>
</div>
<div class="time"></div>
</div>
</div>
</div>
<div class="main radar">
<div class="container">
<div class="scroll-area">
<div class="frame template">
<div class="map">
<img src="images/4000RadarMap2.jpg" />
</div>
</div>
</div>
</div>
</div>

View file

@ -0,0 +1,14 @@
<%- include('header.ejs', {titleDual:{ top: 'Regional' , bottom: 'Observations' }, hasTime: true }) %>
<div class="main has-scroll regional-forecast">
<div class="map"><img src="images/Basemap2.png" /></div>
<div class="location-container">
<div class="location template">
<div class="icon">
<img src="" />
</div>
<div class="city"></div>
<div class="temp"></div>
</div>
</div>
</div>
<%- include('scroll.ejs') %>

View file

@ -0,0 +1,4 @@
<div class="scroll">
<div class="scrolling template"></div>
<div class="fixed"></div>
</div>

16
views/partials/travel.ejs Normal file
View file

@ -0,0 +1,16 @@
<%- include('header.ejs', {titleDual: {top: 'Travel Forecast', bottom: 'For '} , hasTime: true }) %>
<div class="main has-scroll travel">
<div class="column-headers">
<div class="temp low">LOW</div>
<div class="temp high">HIGH</div>
</div>
<div class="travel-lines">
<div class="travel-row template">
<div class="city"></div>
<div class="icon"><img /></div>
<div class="temp low"></div>
<div class="temp high"></div>
</div>
</div>
</div>
<%- include('scroll.ejs') %>

View file

@ -6,28 +6,32 @@
], ],
"settings": { "settings": {
"search.exclude": { "search.exclude": {
"**/node_modules": true,
"**/bower_components": true,
"**/*.code-search": true, "**/*.code-search": true,
"dist/**": true, "**/*.css": true,
"**/*.min.js": true, "**/*.min.js": true,
"**/bower_components": true,
"**/node_modules": true,
"**/vendor": true, "**/vendor": true,
"dist/**": true
}, },
"cSpell.enabled": true, "cSpell.enabled": true,
"cSpell.words": [ "cSpell.words": [
"'storm", "'storm",
"arcgis",
"Battaglia", "Battaglia",
"devbridge",
"gifs",
"ltrim",
"Noaa", "Noaa",
"nosleep",
"Pngs", "Pngs",
"PRECIP",
"rtrim",
"T", "T",
"T'storm", "T'storm",
"uscomp",
"Visib", "Visib",
"arcgis", "Waukegan"
"devbridge",
"ltrim",
"nosleep",
"rtrim",
"uscomp"
], ],
"cSpell.ignorePaths": [ "cSpell.ignorePaths": [
"**/package-lock.json", "**/package-lock.json",
@ -40,5 +44,11 @@
"**/twc3.js", "**/twc3.js",
], ],
"editor.tabSize": 2, "editor.tabSize": 2,
"emmet.includeLanguages": {
"ejs": "html",
},
"[html]": {
"editor.defaultFormatter": "j69.ejs-beautify"
},
}, },
} }