Compare commits

...

15 Commits

Author SHA1 Message Date
Matt Walsh
1a5548d135 5.19.3 2025-05-16 14:42:19 -05:00
Matt Walsh
11c826a2af fix local forecast paging 2025-05-16 14:42:11 -05:00
Matt Walsh
7a129c1cd3 autocomplete cleanup 2025-05-16 11:17:35 -05:00
Matt Walsh
867657a965 5.19.2 2025-05-16 09:39:54 -05:00
Matt Walsh
e89dc52541 fix spc changing locations close #80 2025-05-16 09:39:48 -05:00
Matt Walsh
317883fc04 Add version number to bottom of page 2025-05-16 09:21:04 -05:00
Matt Walsh
a4a601a387 5.19.1 2025-05-15 22:48:44 -05:00
Matt Walsh
375812c024 better spc labeling 2025-05-15 22:48:37 -05:00
Matt Walsh
6af8b58f14 5.19.0 2025-05-15 22:29:10 -05:00
Matt Walsh
6287db7483 Merge branch 'spc-outlook' 2025-05-15 22:28:22 -05:00
Matt Walsh
46a8fa470c 5.18.1 2025-05-15 22:25:26 -05:00
Matt Walsh
8489b7e2be fix autocomplete click-away 2025-05-15 22:25:18 -05:00
Matt Walsh
7a196ac64a skip spc outlook if not in the next 3 days 2025-05-15 22:07:18 -05:00
Matt Walsh
5946ee495a initial data and graph 2025-05-15 16:04:57 -05:00
Matt Walsh
93ac03acd4 optimize gulp image uploads 2025-05-15 09:02:49 -05:00
19 changed files with 597 additions and 275 deletions

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

View File

@@ -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');

View File

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

View File

@@ -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');

View File

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

View File

@@ -224,4 +224,4 @@ class Radar extends WeatherDisplay {
}
// register display
registerDisplay(new Radar(10, 'radar'));
registerDisplay(new Radar(11, 'radar'));

View 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'));

View 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

View 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%);
}
}
}
}

View File

@@ -12,4 +12,5 @@
@use 'regional-forecast';
@use 'almanac';
@use 'hazards';
@use 'media';
@use 'media';
@use 'spc-outlook';

View File

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

View 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') %>