mirror of
https://github.com/netbymatt/ws4kp.git
synced 2026-04-15 08:09:31 -07:00
Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1a5548d135 | ||
|
|
11c826a2af | ||
|
|
7a129c1cd3 | ||
|
|
867657a965 | ||
|
|
e89dc52541 | ||
|
|
317883fc04 | ||
|
|
a4a601a387 | ||
|
|
375812c024 | ||
|
|
6af8b58f14 | ||
|
|
6287db7483 | ||
|
|
46a8fa470c | ||
|
|
8489b7e2be | ||
|
|
7a196ac64a | ||
|
|
5946ee495a | ||
|
|
93ac03acd4 |
@@ -109,7 +109,7 @@ The resulting files will be in the /dist folder in the root of the project. Thes
|
||||
## Music
|
||||
The WeatherStar had wonderful background music from the smooth jazz and new age genres by artists of the time. Lists of the music that played are available by searching online, but it's all copyrighted music and would be difficult to provide as part of this repository.
|
||||
|
||||
I've used AI tools to create WeatherStar-inspired music tracks that are unencumbered by copyright and are included in this repo. Too keep the size down, I've only included 4 tracks. Additional tracks will be posted in a companion repository [ws4kp-music](https://github.com/netbymatt/ws4kp-music).
|
||||
I've used AI tools to create WeatherStar-inspired music tracks that are unencumbered by copyright and are included in this repo. Too keep the size down, I've only included 4 tracks. Additional tracks are in a companion repository [ws4kp-music](https://github.com/netbymatt/ws4kp-music).
|
||||
|
||||
### Customizing the music
|
||||
Placing .mp3 files in the `/server/music` folder will override the default music included in the repo. Subdirectories will not be scanned. When weatherstar loads in the browser it will load a list if available files and randomize the order when it starts playing. On each loop through the available tracks the order will again be shuffled. If you're using the static files method to host your WeatherStar music is located in `/music`.
|
||||
|
||||
@@ -76,6 +76,7 @@ const mjsSources = [
|
||||
'server/scripts/modules/hazards.mjs',
|
||||
'server/scripts/modules/currentweather.mjs',
|
||||
'server/scripts/modules/almanac.mjs',
|
||||
'server/scripts/modules/spc-outlook.mjs',
|
||||
'server/scripts/modules/icons.mjs',
|
||||
'server/scripts/modules/extendedforecast.mjs',
|
||||
'server/scripts/modules/hourly.mjs',
|
||||
@@ -151,6 +152,7 @@ const upload = () => src(uploadSources, { base: './dist', encoding: false })
|
||||
const imageSources = [
|
||||
'server/fonts/**',
|
||||
'server/images/**',
|
||||
'!server/images/gimp/**',
|
||||
];
|
||||
const uploadImages = () => src(imageSources, { base: './server', encoding: false })
|
||||
.pipe(
|
||||
|
||||
541
package-lock.json
generated
541
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ws4kp",
|
||||
"version": "5.18.0",
|
||||
"version": "5.19.3",
|
||||
"description": "Welcome to the WeatherStar 4000+ project page!",
|
||||
"main": "index.mjs",
|
||||
"type": "module",
|
||||
@@ -42,12 +42,12 @@
|
||||
"suncalc": "^1.8.0",
|
||||
"swiped-events": "^1.1.4",
|
||||
"terser-webpack-plugin": "^5.3.6",
|
||||
"webpack-stream": "^7.0.0"
|
||||
"webpack-stream": "^7.0.0",
|
||||
"gulp-html-minifier-terser": "^7.1.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"dotenv": "^16.5.0",
|
||||
"ejs": "^3.1.5",
|
||||
"express": "^5.1.0",
|
||||
"gulp-html-minifier-terser": "^7.1.0"
|
||||
"express": "^5.1.0"
|
||||
}
|
||||
}
|
||||
|
||||
BIN
server/images/backgrounds/6.png
Normal file
BIN
server/images/backgrounds/6.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.8 KiB |
BIN
server/images/gimp/Background 6.xcf
Normal file
BIN
server/images/gimp/Background 6.xcf
Normal file
Binary file not shown.
@@ -109,9 +109,6 @@ const init = () => {
|
||||
document.querySelector('#spanRadarId').innerHTML = '';
|
||||
document.querySelector('#spanZoneId').innerHTML = '';
|
||||
|
||||
document.querySelector('#chkAutoRefresh').checked = true;
|
||||
localStorage.removeItem('autoRefresh');
|
||||
|
||||
localStorage.removeItem('play');
|
||||
postMessage('navButton', 'play');
|
||||
|
||||
|
||||
@@ -90,7 +90,9 @@ class AutoComplete {
|
||||
this.elem.addEventListener('keyup', (e) => this.keyUp(e));
|
||||
this.elem.closest('form')?.addEventListener('submit', (e) => this.directFormSubmit(e));
|
||||
this.elem.addEventListener('click', () => this.deselectAll());
|
||||
this.elem.addEventListener('focusout', () => this.hideSuggestions());
|
||||
|
||||
// clicking outside the suggestion box requires a bit of work to determine if suggestions should be hidden
|
||||
document.addEventListener('click', (e) => this.checkOutsideClick(e));
|
||||
}
|
||||
|
||||
mouseOver(e) {
|
||||
@@ -138,6 +140,9 @@ class AutoComplete {
|
||||
|
||||
// up/down direction
|
||||
switch (e.which) {
|
||||
case KEYS.ESC:
|
||||
this.hideSuggestions();
|
||||
return;
|
||||
case KEYS.UP:
|
||||
case KEYS.DOWN:
|
||||
// move up or down the selection list
|
||||
@@ -302,6 +307,13 @@ class AutoComplete {
|
||||
[...this.results.querySelectorAll('.suggestion.selected')].forEach((elem) => elem.classList.remove('selected'));
|
||||
this.selectedItem = 0;
|
||||
}
|
||||
|
||||
// if a click is detected on the page, generally we hide the suggestions, unless the click was within the autocomplete elements
|
||||
checkOutsideClick(e) {
|
||||
if (e.target.id === 'txtAddress') return;
|
||||
if (e.target?.parentNode?.classList.contains(this.options.containerClass)) return;
|
||||
this.hideSuggestions();
|
||||
}
|
||||
}
|
||||
|
||||
export default AutoComplete;
|
||||
|
||||
@@ -113,9 +113,11 @@ const smallIcon = (link, _isNightTime) => {
|
||||
return addPath('Thunderstorm.gif');
|
||||
|
||||
case 'wind':
|
||||
case 'wind_':
|
||||
case 'wind_few':
|
||||
case 'wind_sct':
|
||||
case 'wind-n':
|
||||
case 'wind_-n':
|
||||
case 'wind_few-n':
|
||||
return addPath('Wind.gif');
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ class LocalForecast extends WeatherDisplay {
|
||||
forecastsElem.append(...templates);
|
||||
|
||||
// increase each forecast height to a multiple of container height
|
||||
this.pageHeight = forecastsElem.parentNode.scrollHeight;
|
||||
this.pageHeight = forecastsElem.parentNode.offsetHeight;
|
||||
templates.forEach((forecast) => {
|
||||
const newHeight = Math.ceil(forecast.scrollHeight / this.pageHeight) * this.pageHeight;
|
||||
forecast.style.height = `${newHeight}px`;
|
||||
|
||||
@@ -224,4 +224,4 @@ class Radar extends WeatherDisplay {
|
||||
}
|
||||
|
||||
// register display
|
||||
registerDisplay(new Radar(10, 'radar'));
|
||||
registerDisplay(new Radar(11, 'radar'));
|
||||
|
||||
127
server/scripts/modules/spc-outlook.mjs
Normal file
127
server/scripts/modules/spc-outlook.mjs
Normal file
@@ -0,0 +1,127 @@
|
||||
// display spc outlook in a bar graph
|
||||
|
||||
import STATUS from './status.mjs';
|
||||
import { json } from './utils/fetch.mjs';
|
||||
import { DateTime } from '../vendor/auto/luxon.mjs';
|
||||
import WeatherDisplay from './weatherdisplay.mjs';
|
||||
import { registerDisplay } from './navigation.mjs';
|
||||
import testPolygon from './utils/polygon.mjs';
|
||||
|
||||
// list of interesting files ordered [0] = today, [1] = tomorrow...
|
||||
const urlPattern = (day) => `https://www.spc.noaa.gov/products/outlook/day${day}otlk_cat.nolyr.geojson`;
|
||||
|
||||
const testAllPoints = (point, data) => {
|
||||
// returns all points where the data matches as an array of days and then matches of the properties of the data
|
||||
|
||||
const result = [];
|
||||
// start with a loop of days
|
||||
data.forEach((day, index) => {
|
||||
// initialize the result
|
||||
result[index] = false;
|
||||
// loop through each category
|
||||
day.features.forEach((feature) => {
|
||||
if (!feature.geometry.coordinates) return;
|
||||
const inPolygon = testPolygon(point, feature.geometry);
|
||||
if (inPolygon) result[index] = feature.properties;
|
||||
});
|
||||
});
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
const barSizes = {
|
||||
TSTM: 60,
|
||||
MRGL: 150,
|
||||
SLGT: 210,
|
||||
ENH: 270,
|
||||
MDT: 330,
|
||||
HIGH: 390,
|
||||
};
|
||||
|
||||
class SpcOutlook extends WeatherDisplay {
|
||||
constructor(navId, elemId) {
|
||||
super(navId, elemId, 'SPC Outlook', true);
|
||||
// don't display on progress/navigation screen
|
||||
this.showOnProgress = false;
|
||||
|
||||
// calculate file names
|
||||
this.files = [null, null, null].map((v, i) => urlPattern(i + 1));
|
||||
|
||||
// set timings
|
||||
this.timing.totalScreens = 1;
|
||||
}
|
||||
|
||||
async getData(weatherParameters, refresh) {
|
||||
if (!super.getData(weatherParameters, refresh)) return;
|
||||
|
||||
// initial data does not need to be reloaded on a location change, only during silent refresh
|
||||
if (!this.initialData || refresh) {
|
||||
try {
|
||||
// get the three categorical files to get started
|
||||
const filePromises = await Promise.allSettled(this.files.map((file) => json(file)));
|
||||
// store the data, promise will always be fulfilled
|
||||
this.initialData = filePromises.map((outlookDay) => outlookDay.value);
|
||||
} catch (error) {
|
||||
console.error('Unable to get spc outlook');
|
||||
console.error(error.status, error.responseJSON);
|
||||
// if there's no previous data, fail
|
||||
if (!this.initialData) {
|
||||
this.setStatus(STATUS.failed);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
// do the initial parsing of the data
|
||||
this.data = testAllPoints([weatherParameters.longitude, weatherParameters.latitude], this.initialData);
|
||||
|
||||
// if all the data returns false the there's nothing to do, skip this screen
|
||||
if (this.data.reduce((prev, cur) => prev || !!cur, false)) {
|
||||
this.timing.totalScreens = 1;
|
||||
} else {
|
||||
this.timing.totalScreens = 0;
|
||||
}
|
||||
this.calcNavTiming();
|
||||
|
||||
// we only get here if there was no error above
|
||||
this.screenIndex = 0;
|
||||
this.setStatus(STATUS.loaded);
|
||||
}
|
||||
|
||||
async drawCanvas() {
|
||||
super.drawCanvas();
|
||||
|
||||
// analyze each day
|
||||
const days = this.data.map((day, index) => {
|
||||
// get the day name
|
||||
const dayName = DateTime.now().plus({ days: index }).toLocaleString({ weekday: 'long' });
|
||||
|
||||
// fill the name
|
||||
const fill = {};
|
||||
fill['day-name'] = dayName;
|
||||
|
||||
// create the element
|
||||
const elem = this.fillTemplate('day', fill);
|
||||
|
||||
// update the bar length
|
||||
const bar = elem.querySelector('.risk-bar');
|
||||
if (day.LABEL) {
|
||||
bar.style.width = `${barSizes[day.LABEL]}px`;
|
||||
} else {
|
||||
bar.style.display = 'none';
|
||||
}
|
||||
|
||||
return elem;
|
||||
});
|
||||
|
||||
// add the days to the display
|
||||
const dayContainer = this.elem.querySelector('.days');
|
||||
dayContainer.innerHTML = '';
|
||||
dayContainer.append(...days);
|
||||
|
||||
// finish drawing
|
||||
this.finishDraw();
|
||||
}
|
||||
}
|
||||
|
||||
// register display
|
||||
registerDisplay(new SpcOutlook(10, 'spc-outlook'));
|
||||
51
server/scripts/modules/utils/polygon.mjs
Normal file
51
server/scripts/modules/utils/polygon.mjs
Normal file
@@ -0,0 +1,51 @@
|
||||
// handle multi-polygon and holes
|
||||
const testPolygon = (point, _polygons) => {
|
||||
// turn everything into a multi polygon for ease of processing
|
||||
let polygons = [[..._polygons.coordinates]];
|
||||
if (_polygons.type === 'MultiPolygon') polygons = [..._polygons.coordinates];
|
||||
|
||||
let inArea = false;
|
||||
|
||||
polygons.forEach((_polygon) => {
|
||||
// copy the polygon
|
||||
const polygon = [..._polygon];
|
||||
// if a match has been found don't do anything more
|
||||
if (inArea) return;
|
||||
|
||||
// polygons are defined as [[area], [optional hole 1], [optional hole 2], ...]
|
||||
const area = polygon.shift();
|
||||
// test if inside the initial area
|
||||
inArea = pointInPolygon(point, area);
|
||||
|
||||
// if not in the area return false
|
||||
if (!inArea) return;
|
||||
|
||||
// test the holes, if in any hole return false
|
||||
polygon.forEach((hole) => {
|
||||
if (pointInPolygon(point, hole)) {
|
||||
inArea = false;
|
||||
}
|
||||
});
|
||||
});
|
||||
return inArea;
|
||||
};
|
||||
|
||||
const pointInPolygon = (point, polygon) => {
|
||||
// ray casting method from https://github.com/substack/point-in-polygon
|
||||
const x = point[0];
|
||||
const y = point[1];
|
||||
let inside = false;
|
||||
// eslint-disable-next-line no-plusplus
|
||||
for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
|
||||
const xi = polygon[i][0];
|
||||
const yi = polygon[i][1];
|
||||
const xj = polygon[j][0];
|
||||
const yj = polygon[j][1];
|
||||
const intersect = ((yi > y) !== (yj > y))
|
||||
&& (x < ((xj - xi) * (y - yi)) / (yj - yi) + xi);
|
||||
if (intersect) inside = !inside;
|
||||
}
|
||||
return inside;
|
||||
};
|
||||
|
||||
export default testPolygon;
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
86
server/styles/scss/_spc-outlook.scss
Normal file
86
server/styles/scss/_spc-outlook.scss
Normal file
@@ -0,0 +1,86 @@
|
||||
@use 'shared/_colors'as c;
|
||||
@use 'shared/_utils'as u;
|
||||
|
||||
#spc-outlook-html.weather-display {
|
||||
background-image: url('../images/backgrounds/6.png');
|
||||
}
|
||||
|
||||
.weather-display .spc-outlook {
|
||||
|
||||
.container {
|
||||
position: relative;
|
||||
top: 0px;
|
||||
margin: 0px 10px;
|
||||
box-sizing: border-box;
|
||||
height: 300px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.risk-levels {
|
||||
position: absolute;
|
||||
left: 206px;
|
||||
font-family: 'Star4000 Small';
|
||||
font-size: 32px;
|
||||
@include u.text-shadow();
|
||||
|
||||
|
||||
.risk-level {
|
||||
position: relative;
|
||||
top: -14px;
|
||||
height: 20px;
|
||||
|
||||
&:nth-child(1) {
|
||||
left: calc(20px * 5);
|
||||
}
|
||||
|
||||
&:nth-child(2) {
|
||||
left: calc(20px * 4);
|
||||
}
|
||||
|
||||
&:nth-child(3) {
|
||||
left: calc(20px * 3);
|
||||
}
|
||||
|
||||
&:nth-child(4) {
|
||||
left: calc(20px * 2);
|
||||
}
|
||||
|
||||
&:nth-child(5) {
|
||||
left: calc(20px * 1);
|
||||
}
|
||||
|
||||
&:nth-child(6) {
|
||||
left: calc(20px * 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.days {
|
||||
position: absolute;
|
||||
top: 120px;
|
||||
|
||||
.day {
|
||||
height: 60px;
|
||||
|
||||
.day-name {
|
||||
position: absolute;
|
||||
font-family: 'Star4000';
|
||||
font-size: 24pt;
|
||||
width: 200px;
|
||||
text-align: right;
|
||||
@include u.text-shadow();
|
||||
padding-top: 20px;
|
||||
}
|
||||
|
||||
.risk-bar {
|
||||
position: absolute;
|
||||
width: 150px;
|
||||
height: 40px;
|
||||
left: 210px;
|
||||
margin-top: 20px;
|
||||
border: 3px outset hsl(0, 0%, 70%);
|
||||
background: linear-gradient(0deg, hsl(0, 0%, 40%) 0%, hsl(0, 0%, 60%) 50%, hsl(0, 0%, 40%) 100%);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,4 +12,5 @@
|
||||
@use 'regional-forecast';
|
||||
@use 'almanac';
|
||||
@use 'hazards';
|
||||
@use 'media';
|
||||
@use 'media';
|
||||
@use 'spc-outlook';
|
||||
@@ -33,6 +33,7 @@
|
||||
<script type="module" src="scripts/modules/currentweatherscroll.mjs"></script>
|
||||
<script type="module" src="scripts/modules/currentweather.mjs"></script>
|
||||
<script type="module" src="scripts/modules/almanac.mjs"></script>
|
||||
<script type="module" src="scripts/modules/spc-outlook.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>
|
||||
@@ -109,6 +110,9 @@
|
||||
<div id="almanac-html" class="weather-display">
|
||||
<%- include('partials/almanac.ejs') %>
|
||||
</div>
|
||||
<div id="spc-outlook-html" class="weather-display">
|
||||
<%- include('partials/spc-outlook.ejs') %>
|
||||
</div>
|
||||
<div id="extended-forecast-html" class="weather-display">
|
||||
<%- include('partials/extended-forecast.ejs') %>
|
||||
</div>
|
||||
@@ -171,6 +175,7 @@
|
||||
Station Id: <span id="spanStationId"></span><br />
|
||||
Radar Id: <span id="spanRadarId"></span><br />
|
||||
Zone Id: <span id="spanZoneId"></span><br />
|
||||
Ws4kp Version: <span><%- version %></span>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
|
||||
20
views/partials/spc-outlook.ejs
Normal file
20
views/partials/spc-outlook.ejs
Normal file
@@ -0,0 +1,20 @@
|
||||
<%- include('header.ejs', {title: 'SPC Outlook', hasTime: true, noaaLogo: true}) %>
|
||||
<div class="main has-scroll spc-outlook">
|
||||
<div class="container">
|
||||
<div class="risk-levels">
|
||||
<div class="risk-level">High</div>
|
||||
<div class="risk-level">Moderate</div>
|
||||
<div class="risk-level">Enhanced</div>
|
||||
<div class="risk-level">Slight</div>
|
||||
<div class="risk-level">Marginal</div>
|
||||
<div class="risk-level">T'Storm</div>
|
||||
</div>
|
||||
<div class="days">
|
||||
<div class="day template">
|
||||
<div class="day-name">Monday</div>
|
||||
<div class="risk-bar"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<%- include('scroll.ejs') %>
|
||||
Reference in New Issue
Block a user