Compare commits

...

10 Commits

Author SHA1 Message Date
Matt Walsh
f7505e3e6f 5.4.0 2022-12-08 16:25:24 -06:00
Matt Walsh
705fa9f582 dark mode, page only 2022-12-08 16:25:12 -06:00
Matt Walsh
5edf5cc947 cleanup 2022-12-08 15:05:51 -06:00
Matt Walsh
d0382e0de1 better error handlig of shared data 2022-12-08 14:41:15 -06:00
Matt Walsh
69d14236f1 hourly graph uses image with internal canvas for drawing 2022-12-08 09:23:26 -06:00
Matt Walsh
64fb06d7b4 capture distribution files 2022-12-07 15:39:34 -06:00
Matt Walsh
3ea6c0fd55 5.3.0 2022-12-07 15:39:05 -06:00
Matt Walsh
e13582b760 readme cleanup 2022-12-07 15:38:34 -06:00
Matt Walsh
1a7734b620 add hourly graph 2022-12-07 15:36:02 -06:00
Matt Walsh
0331de8b8a distribution 2022-12-07 11:06:51 -06:00
33 changed files with 473 additions and 79 deletions

View File

@@ -31,7 +31,7 @@ Open your web browser: http://localhost:8080/
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 better show location in the available space
* City and airport names are better parsed to fit the available space
* Remove the dependency on libgif-js
* Use browser for text wrapping where necessary
* Some new weather icons
@@ -61,14 +61,15 @@ The fork is a result of wanting a more manageable, modern code base to work with
I've made several changes to this Weather Star 4000 simulation compared to the original hardware unit and the code that this was forked from.
* Radar displays the timestamp of the image.
* A new hour-by-hour graph of the temperature, cloud cover and precipitation chances for the next 24 hours.
* A new hourly forecast display for the next 24 hours is available, and is shown in the style of the travel cities forecast. (off by default because it duplicates the hourly graph)
* The "Local Forecast" and "Extended Forecast" provide several additional days of information compared to the original format in the 90's.
* Narration was removed. In the original code narration made use of the computer's local text-to-speech engine which didn't sound great.
* Music was removed. I don't want to deal with copyright issues and hosting MP3s. If you're looking for the music that played during forecasts please visit [TWCClassics](https://twcclassics.com/audio/).
* Marine forecast (tides) is not available as it is not part of the new API.
* The nearby cities displayed on screens such as "Latest Observations" and "Regional Forecast" are likely not the same as they were in the 90's. The weather monitoring equipment at these stations move over time for one reason or another, and coming up with a simple formulaic way of finding nearby stations is sufficient to give the same look-and-feel as the original.
* The "Local Forecast" and "Extended Forecast" provide several additional days of information compared to the original format in the 90's.
* "Flavors" are not present in this simulation. Flavors refer to the order of the weather information that was shown on the original units. Instead, the order of the displays has been fixed and a checkboxes can be used to turn on and off individual displays. The travel forecast has been defaulted to off so only local information shows for new users.
* Radar displays the timestamp of the image.
* A new hourly forecast display for the next 24 hours is available, and is shown in the style of the travel cities forecast.
## Wish list

2
dist/index.html vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -77,6 +77,7 @@ const mjsSources = [
'server/scripts/modules/icons.mjs',
'server/scripts/modules/extendedforecast.mjs',
'server/scripts/modules/hourly.mjs',
'server/scripts/modules/hourly-graph.mjs',
'server/scripts/modules/latestobservations.mjs',
'server/scripts/modules/localforecast.mjs',
'server/scripts/modules/radar.mjs',

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "ws4kp",
"version": "5.2.0",
"version": "5.4.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "ws4kp",
"version": "5.2.0",
"version": "5.4.0",
"license": "MIT",
"dependencies": {
"eslint": "^8.21.0",

View File

@@ -1,6 +1,6 @@
{
"name": "ws4kp",
"version": "5.2.0",
"version": "5.4.0",
"description": "Welcome to the WeatherStar 4000+ project page!",
"main": "index.js",
"scripts": {

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

View File

@@ -23,7 +23,7 @@ const categories = [
'Airport', 'Ferry', 'Marina', 'Pier', 'Port', 'Resort', // POI/Travel
'Postal', 'Populated Place',
];
const cats = categories.join(',');
const category = categories.join(',');
const init = () => {
document.getElementById('txtAddress').addEventListener('focus', (e) => {
@@ -54,7 +54,7 @@ const init = () => {
params: {
f: 'json',
countryCode: 'USA', // 'USA,PRI,VIR,GUM,ASM',
category: cats,
category,
maxSuggestions: 10,
},
dataType: 'json',
@@ -192,8 +192,10 @@ const EnterFullScreen = () => {
resize();
UpdateFullScreenNavigate();
// change hover text
document.getElementById('ToggleFullScreen').title = 'Exit fullscreen';
// change hover text and image
const img = document.getElementById('ToggleFullScreen');
img.src = 'images/nav/ic_fullscreen_exit_white_24dp_1x.png';
img.title = 'Exit fullscreen';
};
const ExitFullscreen = () => {
@@ -214,8 +216,10 @@ const ExitFullscreen = () => {
document.msExitFullscreen();
}
resize();
// change hover text
document.getElementById('ToggleFullScreen').title = 'Enter fullscreen';
// change hover text and image
const img = document.getElementById('ToggleFullScreen');
img.src = 'images/nav/ic_fullscreen_white_24dp_1x.png';
img.title = 'Enter fullscreen';
};
const btnNavigateMenuClick = () => {

View File

@@ -171,7 +171,7 @@ class Almanac extends WeatherDisplay {
}
// register display
const display = new Almanac(7, 'almanac');
const display = new Almanac(8, 'almanac');
registerDisplay(display);
export default display.getSun.bind(display);

View File

@@ -55,7 +55,9 @@ class CurrentWeather extends WeatherDisplay {
// test for data received
if (!observations) {
console.error('All current weather stations exhausted');
this.setStatus(STATUS.failed);
if (this.enabled) this.setStatus(STATUS.failed);
// send failed to subscribers
this.getDataCallback(undefined);
return;
}
// preload the icon

View File

@@ -161,4 +161,4 @@ class ExtendedForecast extends WeatherDisplay {
}
// register display
registerDisplay(new ExtendedForecast(6, 'extended-forecast'));
registerDisplay(new ExtendedForecast(7, 'extended-forecast'));

View File

@@ -0,0 +1,149 @@
// hourly forecast list
import STATUS from './status.mjs';
import getHourlyData from './hourly.mjs';
import WeatherDisplay from './weatherdisplay.mjs';
import { registerDisplay } from './navigation.mjs';
import { DateTime } from '../vendor/auto/luxon.mjs';
class HourlyGraph extends WeatherDisplay {
constructor(navId, elemId, defaultActive) {
// special height and width for scrolling
super(navId, elemId, 'Hourly Graph', defaultActive);
// move the top right data into the correct location on load
document.addEventListener('DOMContentLoaded', () => {
this.moveHeader();
});
}
moveHeader() {
// get the header
const header = this.fillTemplate('top-right', {});
// place the header
this.elem.querySelector('.header .right').append(header);
}
async getData() {
if (!super.getData()) return;
const data = await getHourlyData();
if (data === undefined) {
this.setStatus(STATUS.failed);
return;
}
// get interesting data
const temperature = data.map((d) => d.temperature);
const probabilityOfPrecipitation = data.map((d) => d.probabilityOfPrecipitation);
const skyCover = data.map((d) => d.skyCover);
this.data = {
skyCover, temperature, probabilityOfPrecipitation,
};
this.setStatus(STATUS.loaded);
}
drawCanvas() {
if (!this.image) this.image = this.elem.querySelector('.chart img');
// get available space
const availableWidth = 532;
const availableHeight = 285;
this.image.width = availableWidth;
this.image.height = availableHeight;
// get context
const canvas = document.createElement('canvas');
canvas.width = availableWidth;
canvas.height = availableHeight;
const ctx = canvas.getContext('2d');
ctx.imageSmoothingEnabled = false;
// calculate time scale
const timeScale = calcScale(0, 5, this.data.temperature.length - 1, availableWidth);
const startTime = DateTime.now().startOf('hour');
document.querySelector('.x-axis .l-1').innerHTML = formatTime(startTime);
document.querySelector('.x-axis .l-2').innerHTML = formatTime(startTime.plus({ hour: 6 }));
document.querySelector('.x-axis .l-3').innerHTML = formatTime(startTime.plus({ hour: 12 }));
document.querySelector('.x-axis .l-4').innerHTML = formatTime(startTime.plus({ hour: 18 }));
document.querySelector('.x-axis .l-5').innerHTML = formatTime(startTime.plus({ hour: 24 }));
// order is important last line drawn is on top
// clouds
const percentScale = calcScale(0, availableHeight - 10, 100, 10);
const cloud = createPath(this.data.skyCover, timeScale, percentScale);
drawPath(cloud, ctx, {
strokeStyle: 'lightgrey',
lineWidth: 3,
});
// precip
const precip = createPath(this.data.probabilityOfPrecipitation, timeScale, percentScale);
drawPath(precip, ctx, {
strokeStyle: 'aqua',
lineWidth: 3,
});
// temperature
const minTemp = Math.min(...this.data.temperature);
const maxTemp = Math.max(...this.data.temperature);
const midTemp = Math.round((minTemp + maxTemp) / 2);
const tempScale = calcScale(minTemp, availableHeight - 10, maxTemp, 10);
const tempPath = createPath(this.data.temperature, timeScale, tempScale);
drawPath(tempPath, ctx, {
strokeStyle: 'red',
lineWidth: 3,
});
// temperature axis labels
// limited to 3 characters, sacraficing degree character
const degree = String.fromCharCode(176);
this.elem.querySelector('.y-axis .l-1').innerHTML = (maxTemp + degree).substring(0, 3);
this.elem.querySelector('.y-axis .l-2').innerHTML = (midTemp + degree).substring(0, 3);
this.elem.querySelector('.y-axis .l-3').innerHTML = (minTemp + degree).substring(0, 3);
// set the image source
this.image.src = canvas.toDataURL();
super.drawCanvas();
this.finishDraw();
}
}
// create a scaling function from two points
const calcScale = (x1, y1, x2, y2) => {
const m = (y2 - y1) / (x2 - x1);
const b = y1 - m * x1;
return (x) => m * x + b;
};
// create a path as an array of [x,y]
const createPath = (data, xScale, yScale) => data.map((d, i) => [xScale(i), yScale(d)]);
// draw a path with shadow
const drawPath = (path, ctx, options) => {
// first shadow
ctx.beginPath();
ctx.strokeStyle = 'black';
ctx.lineWidth = (options?.lineWidth ?? 2) + 2;
ctx.moveTo(path[0][0], path[0][1]);
path.slice(1).forEach((point) => ctx.lineTo(point[0], point[1] + 2));
ctx.stroke();
// then colored line
ctx.beginPath();
ctx.strokeStyle = options?.strokeStyle ?? 'red';
ctx.lineWidth = (options?.lineWidth ?? 2);
ctx.moveTo(path[0][0], path[0][1]);
path.slice(1).forEach((point) => ctx.lineTo(point[0], point[1]));
ctx.stroke();
};
// format as 1p, 12a, etc.
const formatTime = (time) => time.toFormat('ha').slice(0, -1);
// register display
registerDisplay(new HourlyGraph(3, 'hourly-graph'));

View File

@@ -29,7 +29,7 @@ class Hourly extends WeatherDisplay {
async getData(weatherParameters) {
// super checks for enabled
if (!super.getData(weatherParameters)) return;
const superResponse = super.getData(weatherParameters);
let forecast;
try {
// get the forecast
@@ -37,11 +37,15 @@ class Hourly extends WeatherDisplay {
} catch (e) {
console.error('Get hourly forecast failed');
console.error(e.status, e.responseJSON);
this.setStatus(STATUS.failed);
if (this.enabled) this.setStatus(STATUS.failed);
// return undefined to other subscribers
this.getDataCallback(undefined);
return;
}
this.data = await Hourly.parseForecast(forecast.properties);
this.getDataCallback();
if (!superResponse) return;
this.setStatus(STATUS.loaded);
this.drawLongCanvas();
@@ -66,6 +70,8 @@ class Hourly extends WeatherDisplay {
apparentTemperature: celsiusToFahrenheit(apparentTemperature[idx]),
windSpeed: kilometersToMiles(windSpeed[idx]),
windDirection: directionToNSEW(windDirection[idx]),
probabilityOfPrecipitation: probabilityOfPrecipitation[idx],
skyCover: skyCover[idx],
icon: icons[idx],
}));
}
@@ -184,7 +190,20 @@ class Hourly extends WeatherDisplay {
return dayName;
}, '');
}
// make data available outside this class
// promise allows for data to be requested before it is available
async getCurrentData() {
return new Promise((resolve) => {
if (this.data) resolve(this.data);
// data not available, put it into the data callback queue
this.getDataCallbacks.push(() => resolve(this.data));
});
}
}
// register display
registerDisplay(new Hourly(2, 'hourly'));
const display = new Hourly(2, 'hourly', false);
registerDisplay(display);
export default display.getCurrentData.bind(display);

View File

@@ -93,4 +93,4 @@ class LocalForecast extends WeatherDisplay {
}
// register display
registerDisplay(new LocalForecast(5, 'local-forecast'));
registerDisplay(new LocalForecast(6, 'local-forecast'));

View File

@@ -23,6 +23,7 @@ let AutoRefreshCountMs = 0;
const init = async () => {
// set up resize handler
window.addEventListener('resize', resize);
resize();
// auto refresh
const TwcAutoRefresh = localStorage.getItem('TwcAutoRefresh');
@@ -281,7 +282,7 @@ const generateCheckboxes = () => {
if (!availableDisplays) return;
// generate checkboxes
const checkboxes = displays.map((d) => d.generateCheckbox()).filter((d) => d);
const checkboxes = displays.map((d) => d.generateCheckbox(d.defaultEnabled)).filter((d) => d);
// write to page
availableDisplays.innerHTML = '';

View File

@@ -367,8 +367,6 @@ class Radar extends WeatherDisplay {
}
RadarContext.putImageData(RadarImageData, 0, 0);
// MapContext.drawImage(RadarContext.canvas, 0, 0);
}
static mergeDopplerRadarImage(mapContext, radarContext) {
@@ -402,4 +400,4 @@ class Radar extends WeatherDisplay {
}
// register display
registerDisplay(new Radar(8, 'radar'));
registerDisplay(new Radar(9, 'radar'));

View File

@@ -389,4 +389,4 @@ class RegionalForecast extends WeatherDisplay {
}
// register display
registerDisplay(new RegionalForecast(4, 'regional-forecast'));
registerDisplay(new RegionalForecast(5, 'regional-forecast'));

View File

@@ -160,4 +160,4 @@ class TravelForecast extends WeatherDisplay {
}
// register display, not active by default
registerDisplay(new TravelForecast(3, 'travel', false));
registerDisplay(new TravelForecast(4, 'travel', false));

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,150 @@
@use 'shared/_colors'as c;
@use 'shared/_utils'as u;
#hourly-graph-html {
background-image: url(../images/BackGround1_1_Chart.png);
.header {
.right {
position: absolute;
top: 35px;
right: 60px;
width: 360px;
font-family: 'Star4000 Small';
font-size: 32px;
@include u.text-shadow();
text-align: right;
div {
margin-top: -18px;
}
.temperature {
color: red;
}
.cloud {
color: lightgrey;
}
.rain {
color: aqua;
}
}
}
}
.weather-display .main.hourly-graph {
&.main {
>div {
position: absolute;
}
.label {
font-family: 'Star4000 Small';
font-size: 24pt;
color: c.$column-header-text;
@include u.text-shadow();
margin-top: -15px;
position: absolute;
}
.x-axis {
bottom: 0px;
left: 0px;
width: 640px;
height: 20px;
.label {
text-align: center;
width: 50px;
&.l-1 {
left: 25px;
}
&.l-2 {
left: 158px;
}
&.l-3 {
left: 291px;
}
&.l-4 {
left: 424px;
}
&.l-5 {
left: 557px;
}
}
}
.chart {
top: 0px;
left: 50px;
img {
width: 532px;
height: 285px;
}
}
.y-axis {
top: 0px;
left: 0px;
width: 50px;
height: 285px;
.label {
text-align: right;
right: 0px;
&.l-1 {
top: 0px;
}
&.l-2 {
top: 140px;
}
&.l-3 {
bottom: 0px;
}
}
}
.column-headers {
background-color: c.$column-header;
height: 20px;
position: absolute;
width: 100%;
}
.column-headers {
position: sticky;
top: 0px;
z-index: 5;
.temp {
left: 355px;
}
.like {
left: 435px;
}
.wind {
left: 535px;
}
}
}
}

View File

@@ -5,6 +5,17 @@
body {
font-family: "Star4000";
@media (prefers-color-scheme: dark) {
background-color: #000000;
color: white;
}
a {
@media (prefers-color-scheme: dark) {
color: lightblue;
}
}
}
input,
@@ -20,12 +31,42 @@ button {
#txtAddress {
width: 490px;
font-size: 16pt;
@media (prefers-color-scheme: dark) {
background-color: #000000;
color: white;
border: 1px solid darkgray;
}
}
#btnGetGps,
#btnGetLatLng,
#btnClearQuery {
font-size: 16pt;
@media (prefers-color-scheme: dark) {
background-color: #000000;
color: white;
}
border: 1px solid darkgray;
}
#btnGetGps img {
&.dark {
display: none;
@media (prefers-color-scheme: dark) {
display: inline-block;
}
}
&.light {
@media (prefers-color-scheme: dark) {
display: none;
}
}
}
.autocomplete-suggestions {
@@ -90,6 +131,11 @@ button {
display: flex;
flex-direction: row;
background-color: #000000;
@media (prefers-color-scheme: dark) {
background-color: rgb(48, 48, 48);
}
color: #ffffff;
width: 100%;
}
@@ -269,12 +315,6 @@ jsgif {
font-size: 18pt;
}
#container canvas {
/* position: absolute; */
width: 100%;
/* max-width: 640px; */
}
.heading {
font-weight: bold;
margin-top: 15px;

View File

@@ -3,6 +3,7 @@
@import 'current-weather';
@import 'extended-forecast';
@import 'hourly';
@import 'hourly-graph';
@import 'travel';
@import 'latest-observations';
@import 'local-forecast';

View File

@@ -34,6 +34,7 @@
<script type="module" src="scripts/modules/almanac.mjs"></script>
<script type="module" src="scripts/modules/icons.mjs"></script>
<script type="module" src="scripts/modules/extendedforecast.mjs"></script>
<script type="module" src="scripts/modules/hourly-graph.mjs"></script>
<script type="module" src="scripts/modules/hourly.mjs"></script>
<script type="module" src="scripts/modules/latestobservations.mjs"></script>
<script type="module" src="scripts/modules/localforecast.mjs"></script>
@@ -60,7 +61,7 @@
<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="txtAddress" type="text" value="" placeholder="Zip or City, State" /><button id="btnGetGps" type="button" title="Get GPS Location"><img src="images/nav/ic_gps_fixed_black_18dp_1x.png" class="light"/><img src="images/nav/ic_gps_fixed_white_18dp_1x.png" class="dark"/></button>
<input id="btnGetLatLng" type="submit" value="GO" />
<input id="btnClearQuery" type="reset" value="Reset" />
</form>
@@ -90,6 +91,9 @@
<div id="hourly-html" class="weather-display">
<%- include('partials/hourly.ejs') %>
</div>
<div id="hourly-graph-html" class="weather-display">
<%- include('partials/hourly-graph.ejs') %>
</div>
<div id="travel-html" class="weather-display">
<%- include('partials/travel.ejs') %>
</div>
@@ -126,7 +130,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="Enter Fullscreen" />
<img id="ToggleFullScreen" class="navButton" src="images/nav/ic_fullscreen_white_24dp_1x.png" title="Enter Fullscreen" />
</div>
</div>
</div>

View File

@@ -17,6 +17,8 @@
<% if (locals?.hasTime) { %>
<div class="date-time date"></div>
<div class="date-time time"></div>
<% } else if (!locals?.noaaLogo) { %>
<div class="right"></div>
<% } %>
<% if (locals?.noaaLogo) { %>
<div class="noaa-logo">

View File

@@ -0,0 +1,24 @@
<%- include('header.ejs', {title: 'Hourly Graph' , hasTime: false }) %>
<div class="main has-scroll hourly-graph">
<div class="top-right template ">
<div class="temperature">Temperature</div>
<div class="cloud">Cloud %</div>
<div class="rain">Precip %</div>
</div>
<div class="y-axis">
<div class="label l-1">75</div>
<div class="label l-2">65</div>
<div class="label l-3">55</div>
</div>
<div class="chart">
<img id="chart-area"></img>
</div>
<div class="x-axis">
<div class="label l-1">12a</div>
<div class="label l-2">6a</div>
<div class="label l-3">12p</div>
<div class="label l-4">6p</div>
<div class="label l-5">12a</div>
</div>
</div>
<%- include('scroll.ejs') %>

View File

@@ -1,43 +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 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 class="container">
<div class="scroll-area">
<div class="frame template">
<div class="map">
<img src="images/4000RadarMap2.jpg" />
</div>
</div>
</div>
</div>
</div>