Compare commits

...

26 Commits

Author SHA1 Message Date
Matt Walsh
392b339727 5.23.7 2025-06-12 20:53:28 -05:00
Matt Walsh
852eff8de6 add basic volume control #109 2025-06-12 20:53:23 -05:00
Matt Walsh
c74a15c40c 5.23.6 2025-06-12 13:17:26 -05:00
Matt Walsh
5419425834 fix for large font linux/win differences in extended forecast 2025-06-12 13:16:55 -05:00
Matt Walsh
f3a386079b fix for file didn't load in spc outlook 2025-06-12 13:13:19 -05:00
Matt Walsh
aa43713943 5.23.5 2025-06-12 12:15:10 -05:00
Matt Walsh
1dece10679 cache-busting for radar worker 2025-06-12 12:14:56 -05:00
Matt Walsh
c4f16d786a Merge remote-tracking branch 'origin/static-host' 2025-06-12 12:07:55 -05:00
Matt Walsh
36b8adc019 5.23.4 2025-06-12 09:30:54 -05:00
Matt Walsh
bfe0b4757d Merge branch 'main' of github.com:netbymatt/ws4kp 2025-06-12 09:30:38 -05:00
Matt Walsh
2b61e55783 get current conditions direct from api 2025-06-12 09:30:31 -05:00
Matt Walsh
36c4f451b3 Merge pull request #107 from rmitchellscott/fix-push-on-tag
chore(docker): fix pushing on tag
2025-06-09 15:12:38 -05:00
Mitchell Scott
268d4ae7fa chore(docker): fix pushing on tag 2025-06-07 11:46:00 -06:00
Matt Walsh
1b49e02cd8 5.23.3 2025-06-06 16:40:14 -05:00
Matt Walsh
9a55a6ec39 fix setting boolean-style query string parsing close #106 2025-06-06 16:40:08 -05:00
Matt Walsh
faaf6f770f 5.23.2 2025-06-06 16:31:13 -05:00
Matt Walsh
79e4ed6e8b clean up star4000 large font baseline differences between linux/win 2025-06-06 16:31:05 -05:00
Matt Walsh
f956df1272 5.23.1 2025-06-05 23:34:47 -05:00
Matt Walsh
089ef56b10 add event type to hazard scroll #92 2025-06-05 23:34:25 -05:00
Matt Walsh
c4e8721a2b 5.23.0 2025-06-05 22:02:07 -05:00
Matt Walsh
a2efc2f767 Merge branch 'hazard-scroll-2' #92 2025-06-05 22:01:23 -05:00
Matt Walsh
c0e1c55453 clean up location switching 2025-06-05 21:57:06 -05:00
Matt Walsh
974a061b44 clean up paths for server and server-dist, remove cors #96 2025-06-02 20:59:35 -05:00
Matt Walsh
7c50f5f1d7 issues changing locations 2025-06-02 15:57:58 -05:00
Matt Walsh
4bf3f4d1e0 scroll triggers properly on red background 2025-06-02 14:48:53 -05:00
Matt Walsh
46da573715 hazard scroll working, needs styling #92 2025-06-01 23:25:07 -05:00
27 changed files with 246 additions and 185 deletions

View File

@@ -49,7 +49,7 @@ jobs:
with: with:
context: . context: .
pull: true pull: true
push: ${{ github.ref == 'refs/heads/main' }} push: ${{ github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/') }}
platforms: linux/amd64,linux/arm64/v8 platforms: linux/amd64,linux/arm64/v8
tags: ${{ steps.meta.outputs.tags }} tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }} labels: ${{ steps.meta.outputs.labels }}

View File

@@ -139,6 +139,11 @@ Ws4kp is muted by default, but if it was unmuted on the last visit it is coded t
Chrome seems to be more lenient on auto play and will eventually let a site auto-play music if you're visited it enough recently and manually clicked to start playing music on each visit. It also has a flag you can add to the command line when launching Chrome: `chrome.exe --autoplay-policy=no-user-gesture-required`. This is the best solution when using Kiosk-style setup. Chrome seems to be more lenient on auto play and will eventually let a site auto-play music if you're visited it enough recently and manually clicked to start playing music on each visit. It also has a flag you can add to the command line when launching Chrome: `chrome.exe --autoplay-policy=no-user-gesture-required`. This is the best solution when using Kiosk-style setup.
If you're unable to pre-set the play state before entering kiosk mode (such as with a home dashboard implemenation) you can add the query string value below to the url. The browser will still follow the auto play rules outlined above.
```
?settings-mediaPlaying-boolean=true
```
## Community Notes ## Community Notes
Thanks to the WeatherStar community for providing these discussions to further extend your retro forecasts! Thanks to the WeatherStar community for providing these discussions to further extend your retro forecasts!

View File

@@ -1,45 +0,0 @@
// pass through api requests
// http(s) modules
import https from 'https';
// url parsing
import queryString from 'querystring';
// return an express router
const cors = (req, res) => {
// add out-going headers
const headers = {};
headers['user-agent'] = '(WeatherStar 4000+, ws4000@netbymatt.com)';
headers.accept = req.headers.accept;
// get query paramaters if the exist
const queryParams = Object.keys(req.query).reduce((acc, key) => {
// skip the paramater 'u'
if (key === 'u') return acc;
// add the paramter to the resulting object
acc[key] = req.query[key];
return acc;
}, {});
let query = queryString.encode(queryParams);
if (query.length > 0) query = `?${query}`;
// get the page
https.get(`https://api.weather.gov${req.path}${query}`, {
headers,
}, (getRes) => {
// pull some info
const { statusCode } = getRes;
// pass the status code through
res.status(statusCode);
// set headers
res.header('content-type', getRes.headers['content-type']);
// pipe to response
getRes.pipe(res);
}).on('error', (e) => {
console.error(e);
});
};
export default cors;

View File

@@ -1,46 +0,0 @@
// pass through api requests
// http(s) modules
import https from 'https';
// url parsing
import queryString from 'querystring';
// return an express router
const outlook = (req, res) => {
// add out-going headers
const headers = {};
headers['user-agent'] = '(WeatherStar 4000+, ws4000@netbymatt.com)';
headers.accept = req.headers.accept;
// get query paramaters if the exist
const queryParams = Object.keys(req.query).reduce((acc, key) => {
// skip the paramater 'u'
if (key === 'u') return acc;
// add the paramter to the resulting object
acc[key] = req.query[key];
return acc;
}, {});
let query = queryString.encode(queryParams);
if (query.length > 0) query = `?${query}`;
// get the page
https.get(`https://www.cpc.ncep.noaa.gov/${req.path}${query}`, {
headers,
}, (getRes) => {
// pull some info
const { statusCode } = getRes;
// pass the status code through
res.status(statusCode);
// set headers
res.header('content-type', getRes.headers['content-type']);
res.header('last-modified', getRes.headers['last-modified']);
// pipe to response
getRes.pipe(res);
}).on('error', (e) => {
console.error(e);
});
};
export default outlook;

View File

@@ -1,46 +0,0 @@
// pass through api requests
// http(s) modules
import https from 'https';
// url parsing
import queryString from 'querystring';
// return an express router
const radar = (req, res) => {
// add out-going headers
const headers = {};
headers['user-agent'] = '(WeatherStar 4000+, ws4000@netbymatt.com)';
headers.accept = req.headers.accept;
// get query paramaters if the exist
const queryParams = Object.keys(req.query).reduce((acc, key) => {
// skip the paramater 'u'
if (key === 'u') return acc;
// add the paramter to the resulting object
acc[key] = req.query[key];
return acc;
}, {});
let query = queryString.encode(queryParams);
if (query.length > 0) query = `?${query}`;
// get the page
https.get(`https://radar.weather.gov${req.path}${query}`, {
headers,
}, (getRes) => {
// pull some info
const { statusCode } = getRes;
// pass the status code through
res.status(statusCode);
// set headers
res.header('content-type', getRes.headers['content-type']);
res.header('last-modified', getRes.headers['last-modified']);
// pipe to response
getRes.pipe(res);
}).on('error', (e) => {
console.error(e);
});
};
export default radar;

View File

@@ -96,6 +96,19 @@ const buildJs = () => src(mjsSources)
.pipe(webpack(webpackOptions)) .pipe(webpack(webpackOptions))
.pipe(dest(RESOURCES_PATH)); .pipe(dest(RESOURCES_PATH));
const workerSources = [
'server/scripts/modules/radar-worker.mjs',
];
const buildWorkers = () => {
// update the file name in the webpack options
const output = { filename: 'radar-worker.mjs' };
const workerWebpackOptions = { ...webpackOptions, output };
return src(workerSources)
.pipe(webpack(workerWebpackOptions))
.pipe(dest(RESOURCES_PATH));
};
const cssSources = [ const cssSources = [
'server/styles/main.css', 'server/styles/main.css',
]; ];
@@ -137,6 +150,8 @@ const s3 = s3Upload({
const uploadSources = [ const uploadSources = [
'dist/**', 'dist/**',
'!dist/**/*.map', '!dist/**/*.map',
'!dist/images/**/*',
'!dist/fonts/**/*',
]; ];
const upload = () => src(uploadSources, { base: './dist', encoding: false }) const upload = () => src(uploadSources, { base: './dist', encoding: false })
.pipe(s3({ .pipe(s3({
@@ -167,6 +182,9 @@ const uploadImages = () => src(imageSources, { base: './server', encoding: false
}), }),
); );
const copyImageSources = () => src(imageSources, { base: './server', encoding: false })
.pipe(dest('./dist'));
const invalidate = () => cloudfront.send(new CreateInvalidationCommand({ const invalidate = () => cloudfront.send(new CreateInvalidationCommand({
DistributionId: process.env.DISTRIBUTION_ID, DistributionId: process.env.DISTRIBUTION_ID,
InvalidationBatch: { InvalidationBatch: {
@@ -184,7 +202,7 @@ const buildPlaylist = async () => {
return file('playlist.json', JSON.stringify(playlist)).pipe(dest('./dist')); return file('playlist.json', JSON.stringify(playlist)).pipe(dest('./dist'));
}; };
const buildDist = series(clean, parallel(buildJs, compressJsData, compressJsVendor, copyCss, compressHtml, copyOtherFiles, buildPlaylist)); const buildDist = series(clean, parallel(buildJs, buildWorkers, compressJsData, compressJsVendor, copyCss, compressHtml, copyOtherFiles, copyImageSources, buildPlaylist));
// upload_images could be in parallel with upload, but _images logs a lot and has little changes // upload_images could be in parallel with upload, but _images logs a lot and has little changes
// by running upload last the majority of the changes will be at the bottom of the log for easy viewing // by running upload last the majority of the changes will be at the bottom of the log for easy viewing

View File

@@ -1,9 +1,6 @@
import 'dotenv/config'; import 'dotenv/config';
import express from 'express'; import express from 'express';
import fs from 'fs'; import fs from 'fs';
import corsPassThru from './cors/index.mjs';
import radarPassThru from './cors/radar.mjs';
import outlookPassThru from './cors/outlook.mjs';
import playlist from './src/playlist.mjs'; import playlist from './src/playlist.mjs';
import OVERRIDES from './src/overrides.mjs'; import OVERRIDES from './src/overrides.mjs';
@@ -13,12 +10,6 @@ const port = process.env.WS4KP_PORT ?? 8080;
// template engine // template engine
app.set('view engine', 'ejs'); app.set('view engine', 'ejs');
// cors pass-thru to api.weather.gov
app.get('/stations/*station', corsPassThru);
app.get('/Conus/*radar', radarPassThru);
app.get('/products/*product', outlookPassThru);
app.get('/playlist.json', playlist);
// version // version
const { version } = JSON.parse(fs.readFileSync('package.json')); const { version } = JSON.parse(fs.readFileSync('package.json'));
@@ -81,8 +72,6 @@ const geoip = (req, res) => {
// debugging // debugging
if (process.env?.DIST === '1') { if (process.env?.DIST === '1') {
// distribution // distribution
app.use('/images', express.static('./server/images'));
app.use('/fonts', express.static('./server/fonts'));
app.use('/scripts', express.static('./server/scripts')); app.use('/scripts', express.static('./server/scripts'));
app.use('/geoip', geoip); app.use('/geoip', geoip);
app.use('/', express.static('./dist')); app.use('/', express.static('./dist'));
@@ -90,8 +79,11 @@ if (process.env?.DIST === '1') {
// debugging // debugging
app.get('/index.html', index); app.get('/index.html', index);
app.use('/geoip', geoip); app.use('/geoip', geoip);
app.use('/resources', express.static('./server/scripts/modules'));
app.get('/', index); app.get('/', index);
app.get('*name', express.static('./server')); app.get('*name', express.static('./server'));
// cors pass-thru to api.weather.gov
app.get('/playlist.json', playlist);
} }
const server = app.listen(port, () => { const server = app.listen(port, () => {

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "ws4kp", "name": "ws4kp",
"version": "5.22.0", "version": "5.23.7",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "ws4kp", "name": "ws4kp",
"version": "5.22.0", "version": "5.23.7",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"dotenv": "^16.5.0", "dotenv": "^16.5.0",

View File

@@ -1,6 +1,6 @@
{ {
"name": "ws4kp", "name": "ws4kp",
"version": "5.22.0", "version": "5.23.7",
"description": "Welcome to the WeatherStar 4000+ project page!", "description": "Welcome to the WeatherStar 4000+ project page!",
"main": "index.mjs", "main": "index.mjs",
"type": "module", "type": "module",

View File

@@ -42,7 +42,6 @@ class CurrentWeather extends WeatherDisplay {
// station observations // station observations
// eslint-disable-next-line no-await-in-loop // eslint-disable-next-line no-await-in-loop
observations = await json(`${station.id}/observations`, { observations = await json(`${station.id}/observations`, {
cors: true,
data: { data: {
limit: 2, limit: 2,
}, },

View File

@@ -2,6 +2,7 @@ import { locationCleanup } from './utils/string.mjs';
import { elemForEach } from './utils/elem.mjs'; import { elemForEach } from './utils/elem.mjs';
import getCurrentWeather from './currentweather.mjs'; import getCurrentWeather from './currentweather.mjs';
import { currentDisplay } from './navigation.mjs'; import { currentDisplay } from './navigation.mjs';
import getHazards from './hazards.mjs';
// constants // constants
const degree = String.fromCharCode(176); const degree = String.fromCharCode(176);
@@ -13,12 +14,18 @@ let interval;
let screenIndex = 0; let screenIndex = 0;
let sinceLastUpdate = 0; let sinceLastUpdate = 0;
let nextUpdate = DEFAULT_UPDATE; let nextUpdate = DEFAULT_UPDATE;
let resetFlag;
// start drawing conditions // start drawing conditions
// reset starts from the first item in the text scroll list // reset starts from the first item in the text scroll list
const start = () => { const start = () => {
// store see if the context is new // if already started, draw the screen on a reset flag and return
if (interval) {
if (resetFlag) drawScreen();
resetFlag = false;
return;
}
resetFlag = false;
// set up the interval if needed // set up the interval if needed
if (!interval) { if (!interval) {
interval = setInterval(incrementInterval, 500); interval = setInterval(incrementInterval, 500);
@@ -29,7 +36,10 @@ const start = () => {
}; };
const stop = (reset) => { const stop = (reset) => {
if (reset) screenIndex = 0; if (reset) {
screenIndex = 0;
resetFlag = true;
}
}; };
// increment interval, roll over // increment interval, roll over
@@ -51,6 +61,7 @@ const incrementInterval = (force) => {
return; return;
} }
screenIndex = (screenIndex + 1) % (lastScreen); screenIndex = (screenIndex + 1) % (lastScreen);
// draw new text // draw new text
drawScreen(); drawScreen();
}; };
@@ -59,10 +70,24 @@ const drawScreen = async () => {
// get the conditions // get the conditions
const data = await getCurrentWeather(); const data = await getCurrentWeather();
// add the hazards if on screen 0
if (screenIndex === 0) {
data.hazards = await getHazards(() => this.stillWaiting());
}
// nothing to do if there's no data yet // nothing to do if there's no data yet
if (!data) return; if (!data) return;
const thisScreen = screens[screenIndex](data); const thisScreen = screens[screenIndex](data);
// update classes on the scroll area
elemForEach('.weather-display .scroll', (elem) => {
elem.classList.forEach((cls) => { if (cls !== 'scroll') elem.classList.remove(cls); });
// no scroll on progress
if (elem.parentElement.id === 'progress-html') return;
thisScreen?.classes?.forEach((cls) => elem.classList.add(cls));
});
if (typeof thisScreen === 'string') { if (typeof thisScreen === 'string') {
// only a string // only a string
drawCondition(thisScreen); drawCondition(thisScreen);
@@ -74,14 +99,36 @@ const drawScreen = async () => {
break; break;
default: drawCondition(thisScreen); default: drawCondition(thisScreen);
} }
// add the header if available
if (thisScreen.header) {
setHeader(thisScreen.header);
} else {
setHeader('');
}
} else { } else {
// can't identify screen, get another one // can't identify screen, get another one
incrementInterval(true); incrementInterval(true);
} }
}; };
const hazards = (data) => {
// test for data
if (!data.hazards || data.hazards.length === 0) return false;
const hazard = `${data.hazards[0].properties.event} ${data.hazards[0].properties.description}`;
return {
text: hazard,
type: 'scroll',
classes: ['hazard'],
header: data.hazards[0].properties.event,
};
};
// the "screens" are stored in an array for easy addition and removal // the "screens" are stored in an array for easy addition and removal
const screens = [ const screens = [
// hazards
hazards,
// station name // station name
(data) => `Conditions at ${locationCleanup(data.station.properties.name).substr(0, 20)}`, (data) => `Conditions at ${locationCleanup(data.station.properties.name).substr(0, 20)}`,
@@ -127,10 +174,14 @@ const drawCondition = (text) => {
elemForEach('.weather-display .scroll .fixed', (elem) => { elemForEach('.weather-display .scroll .fixed', (elem) => {
elem.innerHTML = text; elem.innerHTML = text;
}); });
setHeader('');
};
const setHeader = (text) => {
elemForEach('.weather-display .scroll .scroll-header', (elem) => {
elem.innerHTML = text ?? '';
});
}; };
document.addEventListener('DOMContentLoaded', () => {
start();
});
// store the original number of screens // store the original number of screens
const originalScreens = screens.length; const originalScreens = screens.length;
@@ -180,7 +231,24 @@ const drawScrollCondition = (screen) => {
}, 1000); }, 1000);
}; };
const parseMessage = (event) => {
if (event?.data?.type === 'current-weather-scroll') {
if (event.data?.method === 'start') start();
if (event.data?.method === 'reload') stop(true);
}
};
// add event listener for start message
window.addEventListener('message', parseMessage);
window.CurrentWeatherScroll = { window.CurrentWeatherScroll = {
addScreen, addScreen,
reset, reset,
start,
};
export {
addScreen,
reset,
start,
}; };

View File

@@ -21,9 +21,17 @@ class Hazards extends WeatherDisplay {
// special height and width for scrolling // special height and width for scrolling
super(navId, elemId, 'Hazards', defaultActive); super(navId, elemId, 'Hazards', defaultActive);
this.showOnProgress = false; this.showOnProgress = false;
this.okToDrawCurrentConditions = false;
// force a 1-minute refresh time for the most up-to-date hazards
this.refreshTime = 60_000;
// 0 screens skips this during "play" // 0 screens skips this during "play"
this.timing.totalScreens = 0; this.timing.totalScreens = 0;
// take note of the already-shown alert ids
this.viewedAlerts = new Set();
this.viewedGetCount = 0;
} }
async getData(weatherParameters, refresh) { async getData(weatherParameters, refresh) {
@@ -32,9 +40,18 @@ class Hazards extends WeatherDisplay {
// hazards performs a silent refresh, but does not fall back to a previous fetch if no data is available // hazards performs a silent refresh, but does not fall back to a previous fetch if no data is available
// this is intentional to ensure the latest alerts only are displayed. // this is intentional to ensure the latest alerts only are displayed.
// auto reload must be set up specifically for hazards in case it is disabled via checkbox (for the bottom line scroll)
if (this.autoRefreshHandle === null) this.setAutoReload();
const alert = this.checkbox.querySelector('.alert'); const alert = this.checkbox.querySelector('.alert');
alert.classList.remove('show'); alert.classList.remove('show');
// if not a refresh (new site), all alerts are new
if (!refresh) {
this.viewedGetCount = 0;
this.viewedAlerts.clear();
}
try { try {
// get the forecast // get the forecast
const url = new URL('https://api.weather.gov/alerts/active'); const url = new URL('https://api.weather.gov/alerts/active');
@@ -47,8 +64,23 @@ class Hazards extends WeatherDisplay {
const filteredAlerts = sortedAlerts.filter((hazard) => hazard.properties.severity !== 'Unknown' && (!hasImmediate || (hazard.properties.urgency === 'Immediate'))); const filteredAlerts = sortedAlerts.filter((hazard) => hazard.properties.severity !== 'Unknown' && (!hasImmediate || (hazard.properties.urgency === 'Immediate')));
this.data = filteredAlerts; this.data = filteredAlerts;
// every 10 times through the get process (10 minutes), reset the viewed messages
if (this.viewedGetCount >= 10) {
this.viewedGetCount = 0;
this.viewedAlerts.clear();
}
this.viewedGetCount += 1;
// count up un-viewed alerts
const unViewed = this.data.reduce((count, hazard) => {
if (!this.viewedAlerts.has(hazard.id)) return count + 1;
return count;
}, 0);
// show alert indicator // show alert indicator
if (this.data.length > 0) alert.classList.add('show'); if (unViewed > 0) alert.classList.add('show');
// draw the canvas to calculate the new timings and activate hazards in the slide deck again
this.drawLongCanvas();
} catch (error) { } catch (error) {
console.error('Get hazards failed'); console.error('Get hazards failed');
console.error(error.status, error.responseJSON); console.error(error.status, error.responseJSON);
@@ -72,7 +104,10 @@ class Hazards extends WeatherDisplay {
const list = this.elem.querySelector('.hazard-lines'); const list = this.elem.querySelector('.hazard-lines');
list.innerHTML = ''; list.innerHTML = '';
const lines = this.data.map((data) => { // filter viewed alerts
const unViewed = this.data.filter((data) => !this.viewedAlerts.has(data.id));
const lines = unViewed.map((data) => {
const fillValues = {}; const fillValues = {};
// text // text
fillValues['hazard-text'] = `${data.properties.event}<br/><br/>${data.properties.description.replaceAll('\n\n', '<br/><br/>').replaceAll('\n', ' ')}`; fillValues['hazard-text'] = `${data.properties.event}<br/><br/>${data.properties.description.replaceAll('\n\n', '<br/><br/>').replaceAll('\n', ' ')}`;
@@ -91,18 +126,22 @@ class Hazards extends WeatherDisplay {
} }
// update timing // update timing
this.setTiming(list);
this.setStatus(STATUS.loaded);
}
setTiming(list) {
// set up the timing // set up the timing
this.timing.baseDelay = 20; this.timing.baseDelay = 20;
// 24 hours = 6 pages // 24 hours = 6 pages
const pages = Math.max(Math.ceil(list.scrollHeight / 400) - 3, 1); const pages = Math.max(Math.ceil(list.scrollHeight / 480) - 4);
const timingStep = 400; const timingStep = 480;
this.timing.delay = [150 + timingStep]; this.timing.delay = [150 + timingStep];
// add additional pages // add additional pages
for (let i = 0; i < pages; i += 1) this.timing.delay.push(timingStep); for (let i = 0; i < pages; i += 1) this.timing.delay.push(timingStep);
// add the final 3 second delay // add the final 3 second delay
this.timing.delay.push(250); this.timing.delay.push(250);
this.calcNavTiming(); this.calcNavTiming();
this.setStatus(STATUS.loaded);
} }
drawCanvas() { drawCanvas() {
@@ -151,10 +190,23 @@ class Hazards extends WeatherDisplay {
if (superValue === false) { if (superValue === false) {
// set total screens to zero to take this out of the rotation // set total screens to zero to take this out of the rotation
this.timing.totalScreens = 0; this.timing.totalScreens = 0;
// note the ids shown
this?.data?.forEach((alert) => this.viewedAlerts.add(alert.id));
} }
// return the value as expected // return the value as expected
return superValue; return superValue;
} }
// make data available outside this class
// promise allows for data to be requested before it is available
async getHazards(stillWaiting) {
if (stillWaiting) this.stillWaitingCallbacks.push(stillWaiting);
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));
});
}
} }
const calcSeverity = (severity, event) => { const calcSeverity = (severity, event) => {
@@ -165,4 +217,7 @@ const calcSeverity = (severity, event) => {
}; };
// register display // register display
registerDisplay(new Hazards(0, 'hazards', true)); const display = new Hazards(0, 'hazards', true);
registerDisplay(display);
export default display.getHazards.bind(display);

View File

@@ -47,6 +47,9 @@ const enableMediaPlayer = () => {
if (mediaPlaying.value === true) { if (mediaPlaying.value === true) {
startMedia(); startMedia();
} }
// add the volume control to the page
const settingsSection = document.querySelector('#settings');
settingsSection.append(mediaVolume.generate());
} }
}; };
@@ -123,6 +126,25 @@ const randomizePlaylist = () => {
playlist.availableFiles = randomPlaylist; playlist.availableFiles = randomPlaylist;
}; };
const setVolume = (newVolume) => {
if (player) {
player.volume = newVolume;
}
};
const mediaVolume = new Setting('mediaVolume', {
name: 'Volume',
type: 'select',
defaultValue: 0.75,
values: [
[1, '100%'],
[0.75, '75%'],
[0.50, '50%'],
[0.25, '25%'],
],
changeAction: setVolume,
});
const initializePlayer = () => { const initializePlayer = () => {
// basic sanity checks // basic sanity checks
if (!playlist.availableFiles || playlist?.availableFiles.length === 0) { if (!playlist.availableFiles || playlist?.availableFiles.length === 0) {
@@ -145,6 +167,7 @@ const initializePlayer = () => {
player.src = `music/${playlist.availableFiles[currentTrack]}`; player.src = `music/${playlist.availableFiles[currentTrack]}`;
setTrackName(playlist.availableFiles[currentTrack]); setTrackName(playlist.availableFiles[currentTrack]);
player.type = 'audio/mpeg'; player.type = 'audio/mpeg';
setVolume(mediaVolume.value);
}; };
const playerCanPlay = async () => { const playerCanPlay = async () => {

View File

@@ -67,6 +67,9 @@ const getWeather = async (latLon, haveDataCallback) => {
// update the main process for display purposes // update the main process for display purposes
populateWeatherParameters(weatherParameters); populateWeatherParameters(weatherParameters);
// reset the scroll
postMessage({ type: 'current-weather-scroll', method: 'reload' });
// draw the progress canvas and hide others // draw the progress canvas and hide others
hideAllCanvases(); hideAllCanvases();
document.querySelector('#loading').style.display = 'none'; document.querySelector('#loading').style.display = 'none';
@@ -89,7 +92,7 @@ const updateStatus = (value) => {
if (displays[0].status === STATUS.loading) return; if (displays[0].status === STATUS.loading) return;
// calculate first enabled display // calculate first enabled display
const firstDisplayIndex = displays.findIndex((display) => display.enabled && display.timing.totalScreens > 0); const firstDisplayIndex = displays.findIndex((display) => display?.enabled && display?.timing?.totalScreens > 0);
// value.id = 0 is hazards, if they fail to load hot-wire a new value.id to the current display to see if it needs to be loaded // value.id = 0 is hazards, if they fail to load hot-wire a new value.id to the current display to see if it needs to be loaded
// typically this plays out as current conditions loads, then hazards fails. // typically this plays out as current conditions loads, then hazards fails.

View File

@@ -14,7 +14,7 @@ class Progress extends WeatherDisplay {
// setup event listener for dom-required initialization // setup event listener for dom-required initialization
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
this.version = document.querySelector('#version').innerHTML; this.version = document.querySelector('#version').innerHTML.replace(/\s/g, '');
this.elem.querySelector('.container').addEventListener('click', this.lineClick.bind(this)); this.elem.querySelector('.container').addEventListener('click', this.lineClick.bind(this));
}); });
@@ -91,3 +91,10 @@ class Progress extends WeatherDisplay {
// register our own display // register our own display
const progress = new Progress(-1, 'progress'); const progress = new Progress(-1, 'progress');
registerProgress(progress); registerProgress(progress);
const version = () => progress.version;
export default progress;
export {
version,
};

View File

@@ -5,6 +5,7 @@ import { text } from './utils/fetch.mjs';
import WeatherDisplay from './weatherdisplay.mjs'; import WeatherDisplay from './weatherdisplay.mjs';
import { registerDisplay, timeZone } from './navigation.mjs'; import { registerDisplay, timeZone } from './navigation.mjs';
import * as utils from './radar-utils.mjs'; import * as utils from './radar-utils.mjs';
import { version } from './progress.mjs';
// TEMPORARY fix to disable radar on ios safari. The same engine (webkit) is // TEMPORARY fix to disable radar on ios safari. The same engine (webkit) is
// used for all ios browers (chrome, brave, firefox, etc) so it's safe to skip // used for all ios browers (chrome, brave, firefox, etc) so it's safe to skip
@@ -87,7 +88,7 @@ class Radar extends WeatherDisplay {
const lists = (await Promise.all(baseUrls.map(async (url) => { const lists = (await Promise.all(baseUrls.map(async (url) => {
try { try {
// get a list of available radars // get a list of available radars
return text(url, { cors: true }); return text(url);
} catch (error) { } catch (error) {
console.log('Unable to get list of radars'); console.log('Unable to get list of radars');
console.error(error); console.error(error);
@@ -196,7 +197,7 @@ class Radar extends WeatherDisplay {
// create a radar worker with helper functions // create a radar worker with helper functions
const radarWorker = () => { const radarWorker = () => {
// create the worker // create the worker
const worker = new Worker(new URL('./radar-worker.mjs', import.meta.url), { type: 'module' }); const worker = new Worker(`/resources/radar-worker.mjs?_=${version()}`, { type: 'module' });
const processRadar = (url) => new Promise((resolve, reject) => { const processRadar = (url) => new Promise((resolve, reject) => {
// prepare for done message // prepare for done message

View File

@@ -18,6 +18,8 @@ const testAllPoints = (point, data) => {
data.forEach((day, index) => { data.forEach((day, index) => {
// initialize the result // initialize the result
result[index] = false; result[index] = false;
// if there's no data (file didn't load), exit early
if (day === undefined) return;
// loop through each category // loop through each category
day.features.forEach((feature) => { day.features.forEach((feature) => {
if (!feature.geometry.coordinates) return; if (!feature.geometry.coordinates) return;

View File

@@ -38,6 +38,9 @@ class Setting {
if (this.type === 'checkbox' && urlValue !== undefined) { if (this.type === 'checkbox' && urlValue !== undefined) {
urlState = urlValue === 'true'; urlState = urlValue === 'true';
} }
if (this.type === 'boolean' && urlValue !== undefined) {
urlState = urlValue === 'true';
}
if (this.type === 'select' && urlValue !== undefined) { if (this.type === 'select' && urlValue !== undefined) {
urlState = parseFloat(urlValue); urlState = parseFloat(urlValue);
} }

View File

@@ -170,6 +170,7 @@ class WeatherDisplay {
// clean up the first-run flag in screen index // clean up the first-run flag in screen index
if (this.screenIndex < 0) this.screenIndex = 0; if (this.screenIndex < 0) this.screenIndex = 0;
if (this.okToDrawCurrentDateTime) this.drawCurrentDateTime(); if (this.okToDrawCurrentDateTime) this.drawCurrentDateTime();
if (this.okToDrawCurrentConditions) postMessage({ type: 'current-weather-scroll', method: 'start' });
} }
finishDraw() { finishDraw() {
@@ -443,7 +444,9 @@ class WeatherDisplay {
} }
setAutoReload() { setAutoReload() {
this.autoRefreshHandle = this.autoRefreshHandle ?? setInterval(() => this.getData(false, true), settings.refreshTime.value); // refresh time can be forced by the user (for hazards)
const refreshTime = this.refreshTime ?? settings.refreshTime.value;
this.autoRefreshHandle = this.autoRefreshHandle ?? setInterval(() => this.getData(false, true), refreshTime);
} }
} }

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -22,9 +22,10 @@
&.right { &.right {
right: 0px; right: 0px;
font-family: 'Star4000 Large'; font-family: "Star4000 Large";
font-size: 16pt; font-size: 20px;
font-weight: bold; font-weight: bold;
line-height: 24px;
.row { .row {
margin-bottom: 12px; margin-bottom: 12px;

View File

@@ -30,7 +30,7 @@
.condition { .condition {
text-align: center; text-align: center;
height: 74px; height: 74px;
margin-top: 10px; margin-top: 5px;
} }
.icon { .icon {
@@ -44,7 +44,6 @@
.temperatures { .temperatures {
width: 100%; width: 100%;
margin-top: 5px;
.temperature-block { .temperature-block {
display: inline-block; display: inline-block;

View File

@@ -4,6 +4,7 @@
.weather-display .main.hazards { .weather-display .main.hazards {
&.main { &.main {
overflow-y: hidden; overflow-y: hidden;
height: 480px;
.hazard-lines { .hazard-lines {
min-height: 400px; min-height: 400px;
@@ -18,9 +19,10 @@
@include u.text-shadow(0px); @include u.text-shadow(0px);
position: relative; position: relative;
text-transform: uppercase; text-transform: uppercase;
margin-top: 110px; margin-top: 10px;
margin-left: 80px; margin-left: 80px;
margin-right: 80px; margin-right: 80px;
padding-bottom: 10px;
} }
} }
} }

View File

@@ -113,16 +113,32 @@
.scroll { .scroll {
@include u.text-shadow(3px, 1.5px); @include u.text-shadow(3px, 1.5px);
width: calc(640px - 2 * 30px); width: 640px;
height: 70px; height: 70px;
overflow: hidden; overflow: hidden;
margin-top: 10px; margin-top: 3px;
&.hazard {
background-color: rgb(112, 35, 35);
}
.fixed,
.scroll-header {
margin-left: 55px;
margin-right: 55px;
overflow: hidden;
}
.scroll-header {
height: 26px;
font-family: "Star4000 Small";
font-size: 20pt;
margin-top: -10px;
}
.fixed { .fixed {
font-family: 'Star4000'; font-family: 'Star4000';
font-size: 24pt; font-size: 24pt;
margin-left: 55px;
overflow: hidden;
.scroll-area { .scroll-area {
text-wrap: nowrap; text-wrap: nowrap;
@@ -132,5 +148,6 @@
// left: calc((elem width) - 640px); // left: calc((elem width) - 640px);
} }
} }
} }
} }

View File

@@ -1,8 +1,7 @@
<div class="main has-scroll hazards no-header"> <div class="main hazards no-header">
<div class="hazard-lines"> <div class="hazard-lines">
<div class="hazard template"> <div class="hazard template">
<div class="hazard-text"></div> <div class="hazard-text"></div>
</div> </div>
</div> </div>
</div> </div>
<%- include('scroll.ejs') %>

View File

@@ -1,4 +1,5 @@
<div class="scroll"> <div class="scroll">
<div class="scrolling template"></div> <div class="scrolling template"></div>
<div class="scroll-header"></div>
<div class="fixed"></div> <div class="fixed"></div>
</div> </div>