Compare commits

...

4 Commits

Author SHA1 Message Date
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
26 changed files with 368 additions and 33 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

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.3.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "ws4kp",
"version": "5.2.0",
"version": "5.3.0",
"license": "MIT",
"dependencies": {
"eslint": "^8.21.0",

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

View File

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

@@ -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,138 @@
// 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();
// 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.canvas) this.canvas = this.elem.querySelector('.chart canvas');
// get available space
const boundingRect = this.canvas.getBoundingClientRect();
const availableWidth = boundingRect.width;
const availableHeight = boundingRect.height;
this.canvas.width = availableWidth;
this.canvas.height = availableHeight;
// get context
const ctx = this.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
this.elem.querySelector('.y-axis .l-1').innerHTML = maxTemp;
this.elem.querySelector('.y-axis .l-2').innerHTML = midTemp;
this.elem.querySelector('.y-axis .l-3').innerHTML = minTemp;
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
@@ -43,6 +43,9 @@ class Hourly extends WeatherDisplay {
this.data = await Hourly.parseForecast(forecast.properties);
this.getDataCallback();
if (!superResponse) return;
this.setStatus(STATUS.loaded);
this.drawLongCanvas();
}
@@ -66,6 +69,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 +189,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

@@ -281,7 +281,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

@@ -402,4 +402,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;
canvas {
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

@@ -269,12 +269,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>
@@ -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">
<canvas id="chart-area"></canvas>
</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') %>