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
"version": "0.2.0",
"configurations": [
{
"name": "Frontend",
"request": "launch",
"type": "pwa-chrome",
"type": "chrome",
"url": "http://localhost:8080",
"webRoot": "${workspaceFolder}/server",
"skipFiles": [
"<node_internals>/**",
"**/*.min.js",
"**/vendor/**"
]
],
},
{
"name": "Data:stations",
@ -40,7 +39,10 @@
"compounds": [
{
"name": "Compound",
"configurations": ["Frontend", "Server"]
"configurations": [
"Frontend",
"Server"
]
}
]
}

18
.vscode/settings.json vendored
View file

@ -1,5 +1,21 @@
{
"cSpell.enableFiletypes": [
"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
cd ws4kp
npm i
node index.js
```
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?
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 concat = require('gulp-concat');
const terser = require('gulp-terser');
const cleanCSS = require('gulp-clean-css');
const ejs = require('gulp-ejs');
const rename = require('gulp-rename');
const htmlmin = require('gulp-htmlmin');
@ -34,7 +33,6 @@ const jsSources = [
'server/scripts/vendor/auto/nosleep.js',
'server/scripts/vendor/auto/swiped-events.js',
'server/scripts/index.js',
'server/scripts/libgif.js',
'server/scripts/vendor/auto/luxon.js',
'server/scripts/vendor/auto/suncalc.js',
'server/scripts/modules/draw.js',
@ -60,11 +58,10 @@ gulp.task('compress_js', () => gulp.src(jsSources)
.pipe(gulp.dest('./dist/resources')));
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(cleanCSS())
.pipe(gulp.dest('./dist/resources')));
const htmlSources = [
@ -121,4 +118,4 @@ gulp.task('invalidate', async () => cloudfront.createInvalidation({
},
}).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');

1784
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,10 +1,13 @@
{
"name": "ws4kp",
"version": "4.1.5",
"version": "5.0.0",
"description": "Welcome to the WeatherStar 4000+ project page!",
"main": "index.js",
"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",
"lint": "eslint ./server/scripts/**",
"lint:fix": "eslint --fix ./server/scripts/**"
},
"repository": {
"type": "git",
@ -19,23 +22,26 @@
"devDependencies": {
"del": "^6.0.0",
"ejs": "^3.1.5",
"eslint": "^8.10.0",
"eslint-config-airbnb-base": "^15.0.0",
"eslint-plugin-import": "^2.22.1",
"express": "^4.17.1",
"gulp": "^4.0.2",
"gulp-clean-css": "^4.3.0",
"gulp-concat": "^2.6.1",
"gulp-ejs": "^5.1.0",
"gulp-htmlmin": "^5.0.1",
"gulp-rename": "^2.0.0",
"gulp-s3-upload": "^1.7.3",
"gulp-sass": "^5.1.0",
"gulp-terser": "^2.0.0",
"jquery": "^3.6.0",
"jquery-touchswipe": "^1.6.19",
"luxon": "^3.0.0",
"nosleep.js": "^0.12.0",
"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,
},
];

View file

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

File diff suppressed because it is too large Load diff

View file

@ -1,50 +1,35 @@
// 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
class Almanac extends WeatherDisplay {
constructor(navId, elemId) {
super(navId, elemId, 'Almanac');
super(navId, elemId, 'Almanac', true);
// pre-load background images (returns promises)
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)
this.moonImages = [
utils.image.load('images/2/Full-Moon.gif'),
utils.image.load('images/2/Last-Quarter.gif'),
utils.image.load('images/2/New-Moon.gif'),
utils.image.load('images/2/First-Quarter.gif'),
];
// preload the moon images
utils.image.preload('images/2/Full-Moon.gif');
utils.image.preload('images/2/Last-Quarter.gif');
utils.image.preload('images/2/New-Moon.gif');
utils.image.preload('images/2/First-Quarter.gif');
this.timing.totalScreens = 2;
this.timing.totalScreens = 1;
}
async getData(_weatherParameters) {
super.getData(_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
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
this.data = {
sun,
moon,
outlook,
};
// update status
this.setStatus(STATUS.loaded);
@ -127,115 +112,6 @@ class Almanac extends WeatherDisplay {
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() {
super.drawCanvas();
const info = this.data;
@ -243,81 +119,47 @@ class Almanac extends WeatherDisplay {
const Today = DateTime.local();
const Tomorrow = Today.plus({ days: 1 });
// extract moon images
const [FullMoonImage, LastMoonImage, NewMoonImage, FirstMoonImage] = await Promise.all(this.moonImages);
switch (this.screenIndex) {
case 1: {
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');
draw.text(this.context, 'Star4000', '24pt', '#FFFFFF', 320, 180, '30 Day Outlook', 2, 'center');
const DateRange = `MID-${info.outlook.thisMonth.toUpperCase()} TO MID-${info.outlook.nextMonth.toUpperCase()}`;
draw.text(this.context, 'Star4000', '24pt', '#FFFFFF', 320, 220, DateRange, 2, 'center');
const Temperature = info.outlook.temperature;
draw.text(this.context, 'Star4000', '24pt', '#FFFFFF', 70, 300, `Temperatures: ${Temperature}`, 2);
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);
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();
draw.titleText(this.context, 'Almanac', 'Astronomical');
const days = info.moon.map((MoonPhase) => {
const fill = {};
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');
fill.date = date;
fill.type = MoonPhase.phase;
fill.icon = { type: 'img', src: Almanac.imageName(MoonPhase.Phase) };
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);
return this.fillTemplate('day', fill);
});
break;
}
const daysContainer = this.elem.querySelector('.moon .days');
daysContainer.innerHTML = '';
daysContainer.append(...days);
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
// promise allows for data to be requested before it is available
async getSun() {

View file

@ -1,10 +1,10 @@
// 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
class CurrentWeather extends WeatherDisplay {
constructor(navId, elemId) {
super(navId, elemId, 'Current Conditions');
super(navId, elemId, 'Current Conditions', true);
// pre-load background image (returns promise)
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.TemperatureUnit = 'C';
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.Visibility = Math.round(observations.visibility.value / 1000);
data.VisibilityUnit = ' km.';
@ -111,94 +111,43 @@ class CurrentWeather extends WeatherDisplay {
async drawCanvas() {
super.drawCanvas();
const fill = {};
// parse each time to deal with a change in units if necessary
const data = this.parseData();
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, 'Current', 'Conditions');
draw.text(this.context, 'Star4000 Large', '24pt', '#FFFFFF', 170, 135, data.Temperature + String.fromCharCode(176), 2);
fill.temp = data.Temperature + String.fromCharCode(176);
let Conditions = data.observations.textDescription;
if (Conditions.length > 15) {
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);
draw.text(this.context, 'Star4000 Extended', '24pt', '#FFFFFF', 300, 330, `${data.WindDirection} ${data.WindSpeed}`, 2, 'right');
fill.wind = data.WindDirection.padEnd(3, '') + data.WindSpeed.toString().padStart(3, ' ');
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);
draw.text(this.context, 'Star4000 Large', 'bold 16pt', '#FFFFFF', 340, 165, 'Humidity:', 2);
draw.text(this.context, 'Star4000 Large', 'bold 16pt', '#FFFFFF', 560, 165, `${data.Humidity}%`, 2, 'right');
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:
}
fill.humidity = `${data.Humidity}%`;
fill.dewpoint = data.DewPoint + String.fromCharCode(176);
fill.ceiling = (data.Ceiling === 0 ? 'Unlimited' : data.Ceiling + data.CeilingUnit);
fill.visibility = data.Visibility + data.VisibilityUnit;
fill.pressure = `${data.Pressure} ${data.PressureDirection}`;
if (data.observations.heatIndex.value && data.HeatIndex !== data.Temperature) {
draw.text(this.context, 'Star4000 Large', 'bold 16pt', '#FFFFFF', 340, 365, 'Heat Index:', 2);
draw.text(this.context, 'Star4000 Large', 'bold 16pt', '#FFFFFF', 560, 365, data.HeatIndex + String.fromCharCode(176), 2, 'right');
fill['heat-index-label'] = 'Heat Index:';
fill['heat-index'] = data.HeatIndex + String.fromCharCode(176);
} 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);
draw.text(this.context, 'Star4000 Large', 'bold 16pt', '#FFFFFF', 560, 365, data.WindChill + String.fromCharCode(176), 2, 'right');
fill['heat-index-label'] = 'Wind Chill:';
fill['heat-index'] = data.WindChill + String.fromCharCode(176);
}
// get main icon
this.gifs.push(await utils.image.superGifAsync({
src: data.Icon,
auto_play: true,
canvas: this.canvas,
x: 140,
y: 175,
max_width: 126,
}));
fill.icon = { type: 'img', src: data.Icon };
const area = this.elem.querySelector('.main');
area.innerHTML = '';
area.append(this.fillTemplate('weather', fill));
this.finishDraw();
}

View file

@ -1,4 +1,4 @@
/* globals draw, navigation */
/* globals navigation, utils */
// eslint-disable-next-line no-unused-vars
const currentWeatherScroll = (() => {
@ -6,25 +6,13 @@ const currentWeatherScroll = (() => {
const degree = String.fromCharCode(176);
// local variables
let context; // currently active context
let blankDrawArea; // original state of context
let interval;
let screenIndex = 0;
// start drawing conditions
// reset starts from the first item in the text scroll list
const start = (_context) => {
// see if there is a context available
if (!_context) return;
const start = () => {
// 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
if (!interval) {
@ -36,17 +24,10 @@ const currentWeatherScroll = (() => {
};
const stop = (reset) => {
cleanLastContext();
if (interval) interval = clearInterval(interval);
if (reset) screenIndex = 0;
};
const cleanLastContext = () => {
if (blankDrawArea) context.putImageData(blankDrawArea, 0, 405);
blankDrawArea = undefined;
context = undefined;
};
// increment interval, roll over
const incrementInterval = () => {
screenIndex = (screenIndex + 1) % (screens.length);
@ -61,16 +42,13 @@ const currentWeatherScroll = (() => {
// nothing to do if there's no data yet
if (!data) return;
// clean up any old text
context.putImageData(blankDrawArea, 0, 405);
drawCondition(screens[screenIndex](data));
};
// the "screens" are stored in an array for easy addition and removal
const screens = [
// 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
(data) => {
@ -109,7 +87,10 @@ const currentWeatherScroll = (() => {
// internal draw function with preset parameters
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

View file

@ -1,18 +1,15 @@
// display extended forecast graphically
// 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
class ExtendedForecast extends WeatherDisplay {
constructor(navId, elemId) {
super(navId, elemId, 'Extended Forecast');
super(navId, elemId, 'Extended Forecast', true);
// set timings
this.timing.totalScreens = 2;
// pre-load background image (returns promise)
this.backgroundImage = utils.image.load('images/BackGround2_1.png');
}
async getData(_weatherParameters) {
@ -85,14 +82,18 @@ class ExtendedForecast extends WeatherDisplay {
}
static shortenExtendedForecastText(long) {
let short = long;
short = short.replace(/ and /g, ' ');
short = short.replace(/Slight /g, '');
short = short.replace(/Chance /g, '');
short = short.replace(/Very /g, '');
short = short.replace(/Patchy /g, '');
short = short.replace(/Areas /g, '');
short = short.replace(/Dense /g, '');
const regexList = [
[/ and /ig, ' '],
[/Slight /ig, ''],
[/Chance /ig, ''],
[/Very /ig, ''],
[/Patchy /ig, ''],
[/Areas /ig, ''],
[/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(' ');
if (short.indexOf('then') !== -1) {
@ -113,12 +114,12 @@ class ExtendedForecast extends WeatherDisplay {
short2 = '';
}
}
short = short1;
let result = short1;
if (short2 !== '') {
short += ` ${short2}`;
result += ` ${short2}`;
}
return [short, short1, short2];
return result;
}
async drawCanvas() {
@ -128,45 +129,32 @@ class ExtendedForecast extends WeatherDisplay {
// 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 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;
if (low !== undefined) {
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;
if (navigation.units() === UNITS.metric) high = utils.units.fahrenheitToCelsius(high);
draw.text(this.context, 'Star4000 Large', '24pt', '#FFFFFF', 165 + offset, 385, high, 2, 'center');
draw.text(this.context, 'Star4000', '24pt', '#FFFFFF', 120 + offset, 270, Day.text[1], 2, 'center');
draw.text(this.context, 'Star4000', '24pt', '#FFFFFF', 120 + offset, 310, Day.text[2], 2, 'center');
fill['value-hi'] = Math.round(high);
fill.condition = Day.text;
// draw the icon
this.gifs.push(await utils.image.superGifAsync({
src: Day.icon,
auto_play: true,
canvas: this.canvas,
x: 70 + Index * 195,
y: 150,
max_height: 75,
}));
}));
fill.icon = { type: 'img', src: Day.icon };
// 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();
}
}

View file

@ -1,22 +1,17 @@
// 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
class Hourly extends WeatherDisplay {
constructor(navId, elemId, defaultActive) {
// special height and width for scrolling
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
this.timing.baseDelay = 20;
// 24 hours = 6 pages
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];
// add additional pages
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(e.status, e.responseJSON);
this.setStatus(STATUS.failed);
return;
}
this.data = await Hourly.parseForecast(forecast.properties);
@ -114,52 +110,26 @@ class Hourly extends WeatherDisplay {
}
async drawLongCanvas() {
// create the "long" canvas if necessary
if (!this.longCanvas) {
this.longCanvas = document.createElement('canvas');
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');
}
// get the list element and populate
const list = this.elem.querySelector('.hourly-lines');
list.innerHTML = '';
const startingHour = luxon.DateTime.local();
await Promise.all(this.data.map(async (data, index) => {
// calculate base y value
const y = 50 + this.hourHeight * index;
const lines = this.data.map((data, index) => {
const fillValues = {};
// hour
const hour = startingHour.plus({ hours: index });
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
const temperature = Math.round(data.temperature).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
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
let wind = 'Calm';
@ -167,44 +137,25 @@ class Hourly extends WeatherDisplay {
const windSpeed = Math.round(data.windSpeed).toString();
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({
src: data.icon,
auto_play: true,
canvas: this.longCanvas,
x: 290,
y: y - 35,
max_width: 47,
}));
}));
// image
fillValues.icon = { type: 'img', src: data.icon };
return this.fillTemplate('hourly-row', fillValues);
});
list.append(...lines);
}
async 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
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();
}
async showCanvas() {
// special to travel forecast to draw the remainder of the canvas
await this.drawCanvas();
showCanvas() {
// special to hourly to draw the remainder of the canvas
this.drawCanvas();
super.showCanvas();
}
@ -215,17 +166,14 @@ class Hourly extends WeatherDisplay {
// base count change callback
baseCountChange(count) {
// get a fresh canvas
const longCanvas = this.getLongCanvas();
// 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
if (offsetY < 0) offsetY = 0;
// 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) {
@ -241,9 +189,4 @@ class Hourly extends WeatherDisplay {
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 'nskc':
case 'nskc-n':
case 'cold-n':
return addPath('Clear-1992.gif');
case 'bkn':
@ -135,6 +136,9 @@ const icons = (() => {
case 'blizzard':
return addPath('Blowing Snow.gif');
case 'cold':
return addPath('cold.gif');
default:
console.log(`Unable to locate regional icon for ${conditionName} ${link} ${isNightTime}`);
return false;
@ -142,6 +146,8 @@ const icons = (() => {
};
const getWeatherIconFromIconLink = (link, _isNightTime) => {
if (!link) return false;
// internal function to add path to returned icon
const addPath = (icon) => `images/${icon}`;
// extract day or night if not provided
@ -164,11 +170,13 @@ const icons = (() => {
case 'skc':
case 'hot':
case 'haze':
case 'cold':
return addPath('CC_Clear1.gif');
case 'skc-n':
case 'nskc':
case 'nskc-n':
case 'cold-n':
return addPath('CC_Clear0.gif');
case 'sct':

View file

@ -1,12 +1,10 @@
// 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
class LatestObservations extends WeatherDisplay {
constructor(navId, elemId) {
super(navId, elemId, 'Latest Observations');
// pre-load background image (returns promise)
this.backgroundImage = utils.image.load('images/BackGround1_1.png');
super(navId, elemId, 'Latest Observations', true);
// constants
this.MaximumRegionalStations = 7;
@ -67,25 +65,15 @@ class LatestObservations extends WeatherDisplay {
// sort array by station name
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) {
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 {
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;
sortedConditions.forEach((condition) => {
const lines = sortedConditions.map((condition) => {
let Temperature = condition.temperature.value;
let WindSpeed = condition.windSpeed.value;
const windDirection = utils.calc.directionToNSEW(condition.windDirection.value);
@ -94,23 +82,28 @@ class LatestObservations extends WeatherDisplay {
Temperature = utils.units.celsiusToFahrenheit(Temperature);
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);
draw.text(this.context, 'Star4000', '24pt', '#FFFFFF', 345, y, LatestObservations.shortenCurrentConditions(condition.textDescription).substr(0, 9), 2);
const fill = {};
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) {
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') {
draw.text(this.context, 'Star4000', '24pt', '#FFFFFF', 495, y, 'NA', 2);
fill.wind = 'NA';
} else {
draw.text(this.context, 'Star4000', '24pt', '#FFFFFF', 495, y, 'Calm', 2);
fill.wind = 'Calm';
}
const x = (325 - (Temperature.toString().length * 15));
draw.text(this.context, 'Star4000', '24pt', '#FFFFFF', x, y, Temperature, 2);
y += 40;
return this.fillTemplate('observation-row', fill);
});
const linesContainer = this.elem.querySelector('.observation-lines');
linesContainer.innerHTML = '';
linesContainer.append(...lines);
this.finishDraw();
}

View file

@ -1,17 +1,14 @@
// 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
class LocalForecast extends WeatherDisplay {
constructor(navId, elemId) {
super(navId, elemId, 'Local Forecast');
super(navId, elemId, 'Local Forecast', true);
// set timings
this.timing.baseDelay = 5000;
// pre-load background image (returns promise)
this.backgroundImage = utils.image.load('images/BackGround1_1.png');
}
async getData(_weatherParameters) {
@ -28,14 +25,8 @@ class LocalForecast extends WeatherDisplay {
// parse raw data
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
conditions.forEach((condition) => {
this.screenTexts = conditions.map((condition) => {
// process the text
let text = `${condition.DayName.toUpperCase()}...`;
let conditionText = condition.Text;
@ -44,44 +35,23 @@ class LocalForecast extends WeatherDisplay {
}
text += conditionText.toUpperCase().replace('...', ' ');
text = utils.string.wordWrap(text, maxCols, '\n');
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);
// }
return text;
});
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.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() {
super.drawCanvas();
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);
const top = -this.screenIndex * this.pageHeight;
this.elem.querySelector('.forecasts').style.top = `${top}px`;
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();
}

View file

@ -23,7 +23,8 @@ const navigation = (() => {
let almanac;
const init = async () => {
// nothing to do
// set up resize handler
window.addEventListener('resize', resize);
};
const message = (data) => {
@ -87,22 +88,22 @@ const navigation = (() => {
// draw the progress canvas and hide others
hideAllCanvases();
document.getElementById('loading').style.display = 'none';
progress = new Progress(-1, 'progress');
if (!progress) progress = new Progress(-1, 'progress');
await progress.drawCanvas();
progress.showCanvas();
// start loading canvases if necessary
if (displays.length === 0) {
currentWeather = new CurrentWeather(0, 'currentWeather');
currentWeather = new CurrentWeather(0, 'current-weather');
almanac = new Almanac(7, 'almanac');
displays = [
currentWeather,
new LatestObservations(1, 'latestObservations'),
new LatestObservations(1, 'latest-observations'),
new Hourly(2, 'hourly'),
new TravelForecast(3, 'travelForecast', false), // not active by default
new RegionalForecast(4, 'regionalForecast'),
new LocalForecast(5, 'localForecast'),
new ExtendedForecast(6, 'extendedForecast'),
new TravelForecast(3, 'travel', false), // not active by default
new RegionalForecast(4, 'regional-forecast'),
new LocalForecast(5, 'local-forecast'),
new ExtendedForecast(6, 'extended-forecast'),
almanac,
new Radar(8, 'radar'),
];
@ -177,7 +178,15 @@ const navigation = (() => {
progress.hideCanvas();
if (!current) {
// 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;
}
if (direction === msg.command.nextFrame) currentDisplay().navNext();
@ -266,6 +275,25 @@ const navigation = (() => {
return almanac.getSun();
};
// resize the container on a page resize
const resize = () => {
const widthZoomPercent = window.innerWidth / 640;
const heightZoomPercent = window.innerHeight / 480;
const scale = Math.min(widthZoomPercent, heightZoomPercent);
if (scale < 1.0 || document.fullscreenElement) {
document.getElementById('container').style.zoom = scale;
} else {
document.getElementById('container').style.zoom = 1;
}
};
// reset all statuses to loading on all displays, used to keep the progress bar accurate during refresh
const resetStatuses = () => {
displays.forEach((display) => { display.status = STATUS.loading; });
};
return {
init,
message,
@ -277,5 +305,7 @@ const navigation = (() => {
getDisplay,
getCurrentWeather,
getSun,
resize,
resetStatuses,
};
})();

View file

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

View file

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

View file

@ -1,15 +1,12 @@
// regional forecast and observations
// 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
class RegionalForecast extends WeatherDisplay {
constructor(navId, elemId) {
super(navId, elemId, 'Regional Forecast');
// pre-load background image (returns promise)
this.backgroundImage = utils.image.load('images/BackGround5_1.png');
super(navId, elemId, 'Regional Forecast', true);
// timings
this.timing.totalScreens = 3;
@ -19,14 +16,14 @@ class RegionalForecast extends WeatherDisplay {
super.getData(_weatherParameters);
const weatherParameters = _weatherParameters ?? this.weatherParameters;
// pre-load the base map (returns promise)
let src = 'images/Basemap2.png';
// pre-load the base map
let baseMap = 'images/Basemap2.png';
if (weatherParameters.state === 'HI') {
src = 'images/HawaiiRadarMap4.png';
baseMap = 'images/HawaiiRadarMap4.png';
} 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
const offsetXY = {
@ -34,10 +31,10 @@ class RegionalForecast extends WeatherDisplay {
y: 117,
};
// 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
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
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)
const regionalForecastPromises = regionalCities.map(async (city) => {
const regionalDataAll = await Promise.all(regionalCities.map(async (city) => {
try {
// get the point first, then break down into forecast and observations
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);
// 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
const observation = await observationPromise;
@ -105,14 +102,12 @@ class RegionalForecast extends WeatherDisplay {
RegionalForecast.buildForecast(forecast.properties.periods[2], city, cityXY),
];
} 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);
return false;
}
});
}));
// wait for the forecasts
const regionalDataAll = await Promise.all(regionalForecastPromises);
// filter out any false (unavailable data)
const regionalData = regionalDataAll.filter((data) => data);
@ -154,20 +149,21 @@ class RegionalForecast extends WeatherDisplay {
// get the observation data
const observation = await utils.fetch.json(`${station}/observations/latest`);
// preload the image
if (!observation.properties.icon) return false;
utils.image.preload(icons.getWeatherRegionalIconFromIconLink(observation.properties.icon, !observation.properties.daytime));
// return the observation
return observation.properties;
} 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);
return false;
}
}
// utility latitude/pixel conversions
getXYFromLatitudeLongitude(Latitude, Longitude, OffsetX, OffsetY, state) {
if (state === 'AK') return this.getXYFromLatitudeLongitudeAK(Latitude, Longitude, OffsetX, OffsetY);
if (state === 'HI') return this.getXYFromLatitudeLongitudeHI(Latitude, Longitude, OffsetX, OffsetY);
static getXYFromLatitudeLongitude(Latitude, Longitude, OffsetX, OffsetY, state) {
if (state === 'AK') return RegionalForecast.getXYFromLatitudeLongitudeAK(Latitude, Longitude, OffsetX, OffsetY);
if (state === 'HI') return RegionalForecast.getXYFromLatitudeLongitudeHI(Latitude, Longitude, OffsetX, OffsetY);
let y = 0;
let x = 0;
const ImgHeight = 1600;
@ -248,9 +244,9 @@ class RegionalForecast extends WeatherDisplay {
return { x, y };
}
getMinMaxLatitudeLongitude(X, Y, OffsetX, OffsetY, state) {
if (state === 'AK') return this.getMinMaxLatitudeLongitudeAK(X, Y, OffsetX, OffsetY);
if (state === 'HI') return this.getMinMaxLatitudeLongitudeHI(X, Y, OffsetX, OffsetY);
static getMinMaxLatitudeLongitude(X, Y, OffsetX, OffsetY, state) {
if (state === 'AK') return RegionalForecast.getMinMaxLatitudeLongitudeAK(X, Y, OffsetX, OffsetY);
if (state === 'HI') return RegionalForecast.getMinMaxLatitudeLongitudeHI(X, Y, OffsetX, OffsetY);
const maxLat = ((Y / 55.2) - 50.5) * -1;
const minLat = (((Y + (OffsetY * 2)) / 55.2) - 50.5) * -1;
const minLon = (((X * -1) / 41.775) + 127.5) * -1;
@ -283,9 +279,9 @@ class RegionalForecast extends WeatherDisplay {
};
}
getXYForCity(City, MaxLatitude, MinLongitude, state) {
if (state === 'AK') this.getXYForCityAK(City, MaxLatitude, MinLongitude);
if (state === 'HI') this.getXYForCityHI(City, MaxLatitude, MinLongitude);
static getXYForCity(City, MaxLatitude, MinLongitude, state) {
if (state === 'AK') RegionalForecast.getXYForCityAK(City, MaxLatitude, MinLongitude);
if (state === 'HI') RegionalForecast.getXYForCityHI(City, MaxLatitude, MinLongitude);
let x = (City.lon - MinLongitude) * 57;
let y = (MaxLatitude - City.lat) * 70;
@ -328,61 +324,61 @@ class RegionalForecast extends WeatherDisplay {
return city.match(/[^-;/\\,]*/)[0].substr(0, 12);
}
async drawCanvas() {
drawCanvas() {
super.drawCanvas();
// break up data into useful values
const { regionalData: data, sourceXY, offsetXY } = this.data;
// fixed offset for all y values when drawing to the map
const mapYOff = 90;
const { DateTime } = luxon;
// 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
const titleTop = this.elem.querySelector('.title.dual .top');
const titleBottom = this.elem.querySelector('.title.dual .bottom');
if (this.screenIndex === 0) {
draw.titleText(this.context, 'Regional', 'Observations');
titleTop.innerHTML = 'Regional';
titleBottom.innerHTML = 'Observations';
} else {
const forecastDate = DateTime.fromISO(data[0][this.screenIndex].time);
// get the name of the day
const dayName = forecastDate.toLocaleString({ weekday: 'long' });
titleTop.innerHTML = 'Forecast for';
// draw the title
if (data[0][this.screenIndex].daytime) {
draw.titleText(this.context, 'Forecast for', dayName);
titleBottom.innerHTML = dayName;
} else {
draw.titleText(this.context, 'Forecast for', `${dayName} Night`);
titleBottom.innerHTML = `${dayName} Night`;
}
}
// draw the map
this.context.drawImage(await this.baseMap, sourceXY.x, sourceXY.y, (offsetXY.x * 2), (offsetXY.y * 2), 0, mapYOff, 640, 312);
await Promise.all(data.map(async (city) => {
const scale = 640 / (offsetXY.x * 2);
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];
// 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
draw.text(this.context, 'Star4000', '20px', '#ffffff', period.x - 40, period.y - 15 + mapYOff, period.name, 2);
// Temperature
fill.icon = { type: 'img', src: icons.getWeatherRegionalIconFromIconLink(period.icon, !period.daytime) };
fill.city = period.name;
let { temperature } = period;
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();
}

View file

@ -1,16 +1,11 @@
// 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
class TravelForecast extends WeatherDisplay {
constructor(navId, elemId, defaultActive) {
// special height and width for scrolling
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
this.timing.baseDelay = 20;
@ -18,7 +13,7 @@ class TravelForecast extends WeatherDisplay {
const pagesFloat = TravelCities.length / 4;
const pages = Math.floor(pagesFloat) - 2; // first page is already displayed, last page doesn't happen
const extra = pages % 1;
const timingStep = this.cityHeight * 4;
const timingStep = 75 * 4;
this.timing.delay = [150 + timingStep];
// add additional pages
for (let i = 0; i < pages; i += 1) this.timing.delay.push(timingStep);
@ -49,7 +44,7 @@ class TravelForecast extends WeatherDisplay {
} catch (e) {
console.error(`GetTravelWeather for ${city.Name} failed`);
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() {
// create the "long" canvas if necessary
if (!this.longCanvas) {
this.longCanvas = document.createElement('canvas');
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;
// get the element and populate
const list = this.elem.querySelector('.travel-lines');
list.innerHTML = '';
// set up variables
const cities = this.data;
// 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, 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;
const lines = cities.map((city) => {
if (city.error) return false;
const fillValues = {};
// city name
draw.text(this.longContext, 'Star4000 Large Compressed', '24pt', '#FFFF00', 80, y, city.name, 2);
fillValues.city = city;
// check for forecast data
if (city.icon) {
fillValues.city = city.name;
// get temperatures and convert if necessary
let { low, high } = city;
@ -122,25 +93,16 @@ class TravelForecast extends WeatherDisplay {
const lowString = Math.round(low).toString();
const highString = Math.round(high).toString();
const xLow = (500 - (lowString.length * 20));
draw.text(this.longContext, 'Star4000 Large', '24pt', '#FFFF00', xLow, y, lowString, 2);
fillValues.low = lowString;
fillValues.high = highString;
const xHigh = (560 - (highString.length * 20));
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,
}));
fillValues.icon = { type: 'img', src: city.icon };
} else {
draw.text(this.longContext, 'Star4000 Small', '24pt', '#FFFFFF', 400, y - 18, 'NO TRAVEL', 2);
draw.text(this.longContext, 'Star4000 Small', '24pt', '#FFFFFF', 400, y, 'DATA AVAILABLE', 2);
fillValues.error = 'NO TRAVEL DATA AVAILABLE';
}
}));
return this.fillTemplate('travel-row', fillValues);
}).filter((d) => d);
list.append(...lines);
}
async drawCanvas() {
@ -151,18 +113,7 @@ class TravelForecast extends WeatherDisplay {
// set up variables
const cities = this.data;
// 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, '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.elem.querySelector('.header .title.dual .bottom').innerHTML = `For ${TravelForecast.getTravelCitiesDayName(cities)}`;
this.finishDraw();
}
@ -180,17 +131,14 @@ class TravelForecast extends WeatherDisplay {
// base count change callback
baseCountChange(count) {
// get a fresh canvas
const longCanvas = this.getLongCanvas();
// 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
if (offsetY < 0) offsetY = 0;
// 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) {

View file

@ -1,6 +1,5 @@
// radar utilities
/* globals SuperGif */
// eslint-disable-next-line no-unused-vars
const utils = (() => {
// ****************************** 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
// the goal is to get it in the browser's cache so it is available more quickly when the browser needs it
// a list of cached icons is used to avoid hitting the cache multiple times
const cachedImages = [];
const preload = (src) => {
if (cachedImages.includes(src)) return false;
const img = new Image();
img.scr = src;
cachedImages.push(src);
blob(src);
// cachedImages.push(src);
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 ***********************
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;
// ********************************* strings *********************************************
const wordWrap = (_str, ...rest) => {
// discuss at: https://locutus.io/php/wordwrap/
// original by: Jonas Raoni Soares Silva (https://www.jsfromhell.com)
// improved by: Nick Callen
// improved by: Kevin van Zonneveld (https://kvz.io)
// improved by: Sakimori
// revised by: Jonas Raoni Soares Silva (https://www.jsfromhell.com)
// bugfixed by: Michael Grier
// bugfixed by: Feras ALHAEK
// 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;
const locationCleanup = (input) => {
// regexes to run
const regexes = [
// "Chicago / West Chicago", removes before slash
/^[A-Za-z ]+ \/ /,
// "Chicago/Waukegan" removes before slash
/^[A-Za-z ]+\//,
// "Chicago, Chicago O'hare" removes before comma
/^[A-Za-z ]+, /,
];
let i;
let j;
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');
// run all regexes
return regexes.reduce((value, regex) => value.replace(regex, ''), input);
};
// ********************************* cors ********************************************
// rewrite some urls for local server
const rewriteUrl = (_url) => {
@ -255,9 +153,9 @@ const utils = (() => {
// build a url, including the rewrite for cors if necessary
let corsUrl = _url;
if (params.cors === true) corsUrl = rewriteUrl(_url);
const url = new URL(corsUrl);
// match the security protocol
url.protocol = window.location.protocol;
const url = new URL(corsUrl, `${window.location.origin}/`);
// match the security protocol when not on localhost
url.protocol = window.location.hostname !== 'localhost' ? window.location.protocol : url.protocol;
// add parameters if necessary
if (params.data) {
Object.keys(params.data).forEach((key) => {
@ -286,13 +184,18 @@ const utils = (() => {
}
};
const elemForEach = (selector, callback) => {
[...document.querySelectorAll(selector)].forEach(callback);
};
// return an orderly object
return {
elem: {
forEach: elemForEach,
},
image: {
load: loadImg,
superGifAsync,
preload,
drawLocalCanvas,
},
weather: {
getPoint,
@ -318,7 +221,7 @@ const utils = (() => {
wrap,
},
string: {
wordWrap,
locationCleanup,
},
cors: {
rewriteUrl,

View file

@ -1,6 +1,6 @@
// base weather display class
/* globals navigation, utils, draw, UNITS, luxon, currentWeatherScroll */
/* globals navigation, utils, luxon, currentWeatherScroll */
const STATUS = {
loading: Symbol('loading'),
@ -31,8 +31,8 @@ class WeatherDisplay {
this.navBaseCount = 0;
this.screenIndex = -1; // special starting condition
// create the canvas, also stores this.elemId
this.createCanvas(elemId);
// store elemId once
this.storeElemId(elemId);
if (elemId !== 'progress') this.addCheckbox(defaultEnabled);
if (this.enabled) {
@ -41,6 +41,9 @@ class WeatherDisplay {
this.setStatus(STATUS.disabled);
}
this.startNavCount();
// get any templates
this.loadTemplates();
}
addCheckbox(defaultEnabled = true) {
@ -92,18 +95,10 @@ class WeatherDisplay {
this.loadingStatus = state;
}
createCanvas(elemId, width = 640, height = 480) {
storeElemId(elemId) {
// only create it once
if (this.elemId) return;
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
@ -136,46 +131,27 @@ class WeatherDisplay {
}
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
if (this.screenIndex < 0) this.screenIndex = 0;
}
finishDraw() {
let OkToDrawCurrentConditions = true;
let OkToDrawNoaaImage = true;
let OkToDrawCurrentDateTime = true;
let OkToDrawLogoImage = true;
// let OkToDrawCustomScrollText = false;
let bottom;
// visibility tests
// 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') {
OkToDrawCurrentConditions = false;
OkToDrawNoaaImage = false;
}
if (this.elemId === 'radar') {
OkToDrawCurrentConditions = false;
OkToDrawCurrentDateTime = false;
OkToDrawNoaaImage = false;
// OkToDrawCustomScrollText = false;
}
if (this.elemId === 'hazards') {
OkToDrawNoaaImage = false;
bottom = true;
OkToDrawLogoImage = false;
}
// draw functions
if (OkToDrawCurrentDateTime) {
@ -185,10 +161,8 @@ class WeatherDisplay {
setInterval(() => this.drawCurrentDateTime(bottom), 100);
}
}
if (OkToDrawLogoImage) this.drawLogoImage();
if (OkToDrawNoaaImage) this.drawNoaaImage();
if (OkToDrawCurrentConditions) {
currentWeatherScroll.start(this.context);
currentWeatherScroll.start();
} else {
// cause a reset if the progress screen is displayed
currentWeatherScroll.stop(this.elemId === 'progress');
@ -197,81 +171,27 @@ class WeatherDisplay {
// if (OkToDrawCustomScrollText) DrawCustomScrollText(WeatherParameters, context);
}
drawCurrentDateTime(bottom) {
drawCurrentDateTime() {
// only draw if canvas is active to conserve battery
if (!this.isActive()) return;
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.
const now = DateTime.local();
// time = "11:35:08 PM";
const time = now.toLocaleString(DateTime.TIME_WITH_SECONDS).padStart(11, ' ');
let x; let y;
if (bottom) {
x = 400;
y = 402;
} else {
x = 410;
y = 65;
if (this.lastTime !== time) {
utils.elem.forEach('.date-time.time', (elem) => { elem.innerHTML = time.toUpperCase(); });
}
if (navigation.units() === UNITS.metric) {
x += 45;
}
draw.text(this.context, font, size, color, x, y, time.toUpperCase(), shadow); // y += 20;
this.lastTime = time;
const date = now.toFormat(' ccc LLL ') + now.day.toString().padStart(2, ' ');
if (bottom) {
x = 55;
y = 402;
} else {
x = 410;
y = 85;
if (this.lastDate !== date) {
utils.elem.forEach('.date-time.date', (elem) => { elem.innerHTML = date.toUpperCase(); });
}
draw.text(this.context, font, size, color, x, y, date.toUpperCase(), shadow);
}
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);
this.lastDate = date;
}
// 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.lastFrame) this.navPrev(navCmd);
// see if the canvas is already showing
if (this.canvas.style.display === 'block') return false;
this.startNavCount();
// show the canvas
this.canvas.style.display = 'block';
return false;
this.elem.classList.add('show');
}
hideCanvas() {
this.resetNavBaseCount();
if (!this.canvas) return;
this.canvas.style.display = 'none';
this.elem.classList.remove('show');
}
isActive() {
return document.getElementById(`${this.elemId}Canvas`).offsetParent !== null;
return this.elem.offsetHeight !== 0;
}
isEnabled() {
@ -452,7 +367,6 @@ class WeatherDisplay {
clearInterval(this.navInterval);
this.navInterval = undefined;
}
this.startNavCount();
}
sendNavDisplayMessage(message) {
@ -461,4 +375,44 @@ class WeatherDisplay {
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,15 +1,9 @@
<!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+" />
@ -27,16 +21,14 @@
<script type="text/javascript" src="resources/data.min.js?_=<%=production%>"></script>
<script type="text/javascript" src="resources/ws.min.js?_=<%=production%>"></script>
<% } else { %>
<link rel="stylesheet" type="text/css" href="styles/index.css" />
<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/vendor/jquery.touchswipe.min.js"></script>
<script type="text/javascript" src="scripts/index.js"></script>
<script type="text/javascript" src="scripts/data/states.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>
@ -62,6 +54,7 @@
<% } %>
</head>
<body>
@ -79,7 +72,9 @@
<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"><%- version %> </div>
<div id="version" style="display:none">
<%- version %>
</div>
<div id="divTwc">
<div id="container">
@ -89,6 +84,36 @@
<div class="instructions">Enter your location above to continue</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 id="divTwcBottom">
<div id="divTwcBottomLeft">
@ -101,7 +126,7 @@
<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" />
<img id="ToggleFullScreen" class="navButton" src="images/nav/ic_fullscreen_exit_white_24dp_1x.png" title="Enter Fullscreen" />
</div>
</div>
</div>
@ -136,4 +161,5 @@
</div>
</body>
</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": {
"search.exclude": {
"**/node_modules": true,
"**/bower_components": true,
"**/*.code-search": true,
"dist/**": true,
"**/*.css": true,
"**/*.min.js": true,
"**/bower_components": true,
"**/node_modules": true,
"**/vendor": true,
"dist/**": true
},
"cSpell.enabled": true,
"cSpell.words": [
"'storm",
"arcgis",
"Battaglia",
"devbridge",
"gifs",
"ltrim",
"Noaa",
"nosleep",
"Pngs",
"PRECIP",
"rtrim",
"T",
"T'storm",
"uscomp",
"Visib",
"arcgis",
"devbridge",
"ltrim",
"nosleep",
"rtrim",
"uscomp"
"Waukegan"
],
"cSpell.ignorePaths": [
"**/package-lock.json",
@ -40,5 +44,11 @@
"**/twc3.js",
],
"editor.tabSize": 2,
"emmet.includeLanguages": {
"ejs": "html",
},
"[html]": {
"editor.defaultFormatter": "j69.ejs-beautify"
},
},
}