Compare commits

...

32 Commits

Author SHA1 Message Date
Matt Walsh
a11e783cde 5.24.1 2025-06-13 22:29:35 -05:00
Matt Walsh
e2e22517b6 fix webpack for new radar-worker 2025-06-13 22:27:36 -05:00
Matt Walsh
d8e0399e92 5.24.0 2025-06-13 22:01:59 -05:00
Matt Walsh
f456897520 use background and foreground tiles for fixed parts of the radar #74 #111 2025-06-13 22:01:06 -05:00
Matt Walsh
9303035bb9 tile position correction 2025-06-13 17:58:05 -05:00
Matt Walsh
3c40219003 tile background created, need to fix shifting of tile 2025-06-13 16:44:53 -05:00
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
33 changed files with 503 additions and 341 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,31 @@ 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',
'./server/scripts/modules/radar-worker-bg-fg.mjs',
];
const buildWorkers = () => {
// update the file name in the webpack options
const output = {
chunkFilename: '[id].mjs',
chunkFormat: 'module',
filename: '[name].mjs',
};
const workerWebpackOptions = {
...webpackOptions,
output,
entry: {
'radar-worker': workerSources[0],
'radar-worker-bg-fg': workerSources[1],
},
};
return src(workerSources)
.pipe(webpack(workerWebpackOptions))
.pipe(dest(RESOURCES_PATH));
};
const cssSources = [ const cssSources = [
'server/styles/main.css', 'server/styles/main.css',
]; ];
@@ -137,6 +162,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 +194,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 +214,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.24.1",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "ws4kp", "name": "ws4kp",
"version": "5.22.0", "version": "5.24.1",
"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.24.1",
"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

@@ -206,6 +206,11 @@ const mapSizeToFinalSize = (x, y) => ({
y: Math.round(y * scaling.width), y: Math.round(y * scaling.width),
}); });
const fetchAsBlob = async (url) => {
const response = await fetch(url);
return response.blob();
};
export { export {
getXYFromLatitudeLongitudeDoppler, getXYFromLatitudeLongitudeDoppler,
getXYFromLatitudeLongitudeMap, getXYFromLatitudeLongitudeMap,
@@ -217,4 +222,5 @@ export {
tileSize, tileSize,
radarFinalSize, radarFinalSize,
radarFullSize, radarFullSize,
fetchAsBlob,
}; };

View File

@@ -0,0 +1,139 @@
import {
radarFinalSize, pixelToFile, modTile, tileSize, mapSizeToFinalSize, fetchAsBlob,
} from './radar-utils.mjs';
// creates the radar background map image and overlay transparency
// which remain fixed on the page as the radar image changes in layered divs
// it returns 4 ImageBitmaps that represent the base map, and 4 ImageBitmaps that are the overlay
// the main thread pushes these ImageBitmaps into the image placeholders on the page
const baseMapImages = (tile) => new Promise((resolve) => {
if (tile === false) resolve(false);
fetchAsBlob(`/images/maps/radar-tiles/${tile}.webp`).then((blob) => {
createImageBitmap(blob).then((imageBitmap) => {
// extract the black pixels to overlay on to the final image (boundaries)
const canvas = new OffscreenCanvas(imageBitmap.width, imageBitmap.height);
const context = canvas.getContext('2d');
context.drawImage(imageBitmap, 0, 0);
const imageData = context.getImageData(0, 0, imageBitmap.width, imageBitmap.height);
// go through the image data and preserve the black pixels, making the rest transparent
for (let i = 0; i < imageData.data.length; i += 4) {
if (imageData.data[i + 0] >= 116 || imageData.data[i + 1] >= 116 || imageData.data[i + 2] >= 116) {
// make it transparent
imageData.data[i + 3] = 0;
}
}
// write the image data back
context.putImageData(imageData, 0, 0);
resolve({
base: imageBitmap,
overlay: canvas.transferToImageBitmap(),
});
});
});
});
onmessage = async (e) => {
const {
sourceXY, offsetX, offsetY,
} = e.data;
// determine the basemap images needed
const baseMapTiles = [
pixelToFile(sourceXY.x, sourceXY.y),
pixelToFile(sourceXY.x + offsetX * 2, sourceXY.y),
pixelToFile(sourceXY.x, sourceXY.y + offsetY * 2),
pixelToFile(sourceXY.x + offsetX * 2, sourceXY.y + offsetY * 2),
];
// get the base maps
const baseMapsPromise = Promise.allSettled(baseMapTiles.map(baseMapImages));
// do some more calculations for assembling the tiles
// the tiles are arranged as follows, with the horizontal axis as x, and correlating with the second set of digits in the image file number
// T[0] T[1]
// T[2] T[3]
// tile 0 gets special treatment, it's placement is the basis for all downstream calculations
const t0Source = modTile(sourceXY.x, sourceXY.y);
const t0Width = tileSize.x - t0Source.x;
const t0Height = tileSize.y - t0Source.y;
const t0FinalSize = mapSizeToFinalSize(t0Width, t0Height);
// these will all be used again for the overlay, calculate them once here
const mapCoordinates = [];
// t[0]
mapCoordinates.push({
sx: t0Source.x,
sw: t0Width,
dx: 0,
dw: t0FinalSize.x,
sy: t0Source.y,
sh: t0Height,
dy: 0,
dh: t0FinalSize.y,
});
// t[1]
mapCoordinates.push({
sx: 0,
sw: tileSize.x - t0Width,
dx: t0FinalSize.x,
dw: mapSizeToFinalSize(tileSize.x - t0Width, 0).x,
sy: t0Source.y,
sh: t0Height,
dy: 0,
dh: t0FinalSize.y,
});
// t[2]
mapCoordinates.push({
sx: t0Source.x,
sw: t0Width,
dx: 0,
dw: t0FinalSize.x,
sy: 0,
sh: tileSize.y - t0Height,
dy: t0FinalSize.y,
dh: mapSizeToFinalSize(0, tileSize.y - t0Height).y,
});
// t[3]
mapCoordinates.push({
sx: 0,
sw: tileSize.x - t0Width,
dx: t0FinalSize.x,
dw: mapSizeToFinalSize(tileSize.x - t0Width, 0).x,
sy: 0,
sh: tileSize.y - t0Height,
dy: t0FinalSize.y,
dh: mapSizeToFinalSize(0, tileSize.y - t0Height).y,
});
// wait for the basemaps to arrive
const baseMaps = (await baseMapsPromise).map((map) => map.value ?? false);
// build the response
const t0Base = baseMaps[0].base;
const t0Overlay = baseMaps[0].overlay;
let t1Base; let t1Overlay; let t2Base; let t2Overlay; let t3Base; let t3Overlay;
if (mapCoordinates[1].dx < radarFinalSize.width && baseMaps[1]) {
t1Base = baseMaps[1].base;
t1Overlay = baseMaps[1].overlay;
}
if (mapCoordinates[2].dy < radarFinalSize.height && baseMaps[2]) {
t2Base = baseMaps[2].base;
t2Overlay = baseMaps[2].overlay;
if (mapCoordinates[1].dx < radarFinalSize.width && baseMaps[3]) {
t3Base = baseMaps[3].base;
t3Overlay = baseMaps[3].overlay;
}
}
// baseContext.drawImage(baseMaps.fullMap, sourceXY.x, sourceXY.y, offsetX * 2, offsetY * 2, 0, 0, radarFinalSize.width, radarFinalSize.height);
postMessage({
t0Base, t0Overlay, t1Base, t1Overlay, t2Base, t2Overlay, t3Base, t3Overlay,
}, [t0Base, t0Overlay, t1Base, t1Overlay, t2Base, t2Overlay, t3Base, t3Overlay]);
};

View File

@@ -1,47 +1,10 @@
import { import {
radarFinalSize, radarFullSize, pixelToFile, modTile, tileSize, removeDopplerRadarImageNoise, mapSizeToFinalSize, radarFinalSize, radarFullSize, removeDopplerRadarImageNoise,
} from './radar-utils.mjs'; } from './radar-utils.mjs';
const fetchAsBlob = async (url) => {
const response = await fetch(url);
return response.blob();
};
const baseMapImages = (tile) => new Promise((resolve) => {
if (tile === false) resolve(false);
fetchAsBlob(`/images/maps/radar-tiles/${tile}.webp`).then((blob) => {
createImageBitmap(blob).then((imageBitmap) => {
// extract the black pixels to overlay on to the final image (boundaries)
const canvas = new OffscreenCanvas(imageBitmap.width, imageBitmap.height);
const context = canvas.getContext('2d');
context.drawImage(imageBitmap, 0, 0);
const imageData = context.getImageData(0, 0, imageBitmap.width, imageBitmap.height);
// go through the image data and preserve the black pixels, making the rest transparent
for (let i = 0; i < imageData.data.length; i += 4) {
if (imageData.data[i + 0] >= 116 || imageData.data[i + 1] >= 116 || imageData.data[i + 2] >= 116) {
// make it transparent
imageData.data[i + 3] = 0;
}
}
// write the image data back
context.putImageData(imageData, 0, 0);
resolve({
base: imageBitmap,
overlay: canvas,
});
});
});
});
const drawOnBasemap = (baseContext, drawImage, positions) => {
baseContext.drawImage(drawImage, positions.sx, positions.sy, positions.sw, positions.sh, positions.dx, positions.dy, positions.dw, positions.dh);
};
onmessage = async (e) => { onmessage = async (e) => {
const { const {
url, RADAR_HOST, OVERRIDES, radarSourceXY, sourceXY, offsetX, offsetY, url, RADAR_HOST, OVERRIDES, radarSourceXY,
} = e.data; } = e.data;
// get the image // get the image
@@ -49,7 +12,6 @@ onmessage = async (e) => {
const radarResponsePromise = fetch(modifiedRadarUrl); const radarResponsePromise = fetch(modifiedRadarUrl);
// calculate offsets and sizes // calculate offsets and sizes
const radarSource = { const radarSource = {
width: 240, width: 240,
height: 163, height: 163,
@@ -57,104 +19,11 @@ onmessage = async (e) => {
y: Math.round(radarSourceXY.y / 2), y: Math.round(radarSourceXY.y / 2),
}; };
// create destination context // create radar context for manipulation
const baseCanvas = new OffscreenCanvas(radarFinalSize.width, radarFinalSize.height);
const baseContext = baseCanvas.getContext('2d');
baseContext.imageSmoothingEnabled = false;
// create working context for manipulation
const radarCanvas = new OffscreenCanvas(radarFullSize.width, radarFullSize.height); const radarCanvas = new OffscreenCanvas(radarFullSize.width, radarFullSize.height);
const radarContext = radarCanvas.getContext('2d'); const radarContext = radarCanvas.getContext('2d');
radarContext.imageSmoothingEnabled = false; radarContext.imageSmoothingEnabled = false;
// determine the basemap images needed
const baseMapTiles = [
pixelToFile(sourceXY.x, sourceXY.y),
pixelToFile(sourceXY.x + offsetX * 2, sourceXY.y),
pixelToFile(sourceXY.x, sourceXY.y + offsetY * 2),
pixelToFile(sourceXY.x + offsetX * 2, sourceXY.y + offsetY * 2),
];
// get the base maps
const baseMapsPromise = Promise.allSettled(baseMapTiles.map(baseMapImages));
// do some more calculations for assembling the tiles
// the tiles are arranged as follows, with the horizontal axis as x, and correlating with the second set of digits in the image file number
// T[0] T[1]
// T[2] T[3]
// tile 0 gets special treatment, it's placement is the basis for all downstream calculations
const t0Source = modTile(sourceXY.x, sourceXY.y);
const t0Width = tileSize.x - t0Source.x;
const t0Height = tileSize.y - t0Source.y;
const t0FinalSize = mapSizeToFinalSize(t0Width, t0Height);
// these will all be used again for the overlay, calculate them once here
const mapCoordinates = [];
// t[0]
mapCoordinates.push({
sx: t0Source.x,
sw: t0Width,
dx: 0,
dw: t0FinalSize.x,
sy: t0Source.y,
sh: t0Height,
dy: 0,
dh: t0FinalSize.y,
});
// t[1]
mapCoordinates.push({
sx: 0,
sw: tileSize.x - t0Width,
dx: t0FinalSize.x,
dw: mapSizeToFinalSize(tileSize.x - t0Width, 0).x,
sy: t0Source.y,
sh: t0Height,
dy: 0,
dh: t0FinalSize.y,
});
// t[2]
mapCoordinates.push({
sx: t0Source.x,
sw: t0Width,
dx: 0,
dw: t0FinalSize.x,
sy: 0,
sh: tileSize.y - t0Height,
dy: t0FinalSize.y,
dh: mapSizeToFinalSize(0, tileSize.y - t0Height).y,
});
// t[3]
mapCoordinates.push({
sx: 0,
sw: tileSize.x - t0Width,
dx: t0FinalSize.x,
dw: mapSizeToFinalSize(tileSize.x - t0Width, 0).x,
sy: 0,
sh: tileSize.y - t0Height,
dy: t0FinalSize.y,
dh: mapSizeToFinalSize(0, tileSize.y - t0Height).y,
});
// wait for the basemaps to arrive
const baseMaps = (await baseMapsPromise).map((map) => map.value ?? false);
// draw each tile if needed
drawOnBasemap(baseContext, baseMaps[0].base, mapCoordinates[0]);
if (mapCoordinates[1].dx < radarFinalSize.width && baseMaps[1]) {
drawOnBasemap(baseContext, baseMaps[1].base, mapCoordinates[1]);
}
if (mapCoordinates[2].dy < radarFinalSize.height && baseMaps[2]) {
drawOnBasemap(baseContext, baseMaps[2].base, mapCoordinates[2]);
if (mapCoordinates[1].dx < radarFinalSize.width && baseMaps[3]) {
drawOnBasemap(baseContext, baseMaps[3].base, mapCoordinates[3]);
}
}
// baseContext.drawImage(baseMaps.fullMap, sourceXY.x, sourceXY.y, offsetX * 2, offsetY * 2, 0, 0, radarFinalSize.width, radarFinalSize.height);
// test response // test response
const radarResponse = await radarResponsePromise; const radarResponse = await radarResponsePromise;
if (!radarResponse.ok) throw new Error(`Unable to fetch radar error ${radarResponse.status} ${radarResponse.statusText} from ${radarResponse.url}`); if (!radarResponse.ok) throw new Error(`Unable to fetch radar error ${radarResponse.status} ${radarResponse.statusText} from ${radarResponse.url}`);
@@ -183,21 +52,7 @@ onmessage = async (e) => {
stretchContext.imageSmoothingEnabled = false; stretchContext.imageSmoothingEnabled = false;
stretchContext.drawImage(croppedRadarCanvas, 0, 0, radarSource.width, radarSource.height, 0, 0, radarFinalSize.width, radarFinalSize.height); stretchContext.drawImage(croppedRadarCanvas, 0, 0, radarSource.width, radarSource.height, 0, 0, radarFinalSize.width, radarFinalSize.height);
// put the radar on the base map const stretchedRadar = stretchCanvas.transferToImageBitmap();
baseContext.drawImage(stretchCanvas, 0, 0);
// put the road/boundaries overlay on the map as needed
drawOnBasemap(baseContext, baseMaps[0].overlay, mapCoordinates[0]);
if (mapCoordinates[1].dx < radarFinalSize.width && baseMaps[1]) {
drawOnBasemap(baseContext, baseMaps[1].overlay, mapCoordinates[1]);
}
if (mapCoordinates[2].dy < radarFinalSize.height && baseMaps[2]) {
drawOnBasemap(baseContext, baseMaps[2].overlay, mapCoordinates[2]);
if (mapCoordinates[1].dx < radarFinalSize.width && baseMaps[3]) {
drawOnBasemap(baseContext, baseMaps[3].overlay, mapCoordinates[3]);
}
}
const processedRadar = baseCanvas.transferToImageBitmap(); postMessage(stretchedRadar, [stretchedRadar]);
postMessage(processedRadar, [processedRadar]);
}; };

View File

@@ -5,6 +5,8 @@ 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';
import { elemForEach } from './utils/elem.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
@@ -72,6 +74,10 @@ class Radar extends WeatherDisplay {
// get some web workers started // get some web workers started
this.workers = (new Array(this.dopplerRadarImageMax)).fill(null).map(() => radarWorker()); this.workers = (new Array(this.dopplerRadarImageMax)).fill(null).map(() => radarWorker());
} }
if (!this.fixedWorker) {
// get the fixed background, overlay worker started
this.fixedWorker = fixedRadarWorker();
}
const baseUrl = `https://${RADAR_HOST}/archive/data/`; const baseUrl = `https://${RADAR_HOST}/archive/data/`;
const baseUrlEnd = '/GIS/uscomp/?F=0&P=n0r*.png'; const baseUrlEnd = '/GIS/uscomp/?F=0&P=n0r*.png';
@@ -87,7 +93,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);
@@ -125,16 +131,19 @@ class Radar extends WeatherDisplay {
const sourceXY = utils.getXYFromLatitudeLongitudeMap(this.weatherParameters, offsetX, offsetY); const sourceXY = utils.getXYFromLatitudeLongitudeMap(this.weatherParameters, offsetX, offsetY);
const radarSourceXY = utils.getXYFromLatitudeLongitudeDoppler(this.weatherParameters, offsetX, offsetY); const radarSourceXY = utils.getXYFromLatitudeLongitudeDoppler(this.weatherParameters, offsetX, offsetY);
const baseAndOverlayPromise = this.fixedWorker.processAssets({
sourceXY,
offsetX,
offsetY,
});
// Load the most recent doppler radar images. // Load the most recent doppler radar images.
const radarInfo = await Promise.all(urls.map(async (url, index) => { const radarInfo = await Promise.all(urls.map(async (url, index) => {
const processedRadar = await this.workers[index].processRadar({ const processedRadar = await this.workers[index].processRadar({
url, url,
RADAR_HOST, RADAR_HOST,
OVERRIDES, OVERRIDES,
sourceXY,
radarSourceXY, radarSourceXY,
offsetX,
offsetY,
}); });
// store the time // store the time
@@ -163,6 +172,50 @@ class Radar extends WeatherDisplay {
elem, elem,
}; };
})); }));
// wait for the base and overlay
const baseAndOverlay = await baseAndOverlayPromise;
// calculate final tile size
const finalTileSize = utils.mapSizeToFinalSize(utils.tileSize.x, utils.tileSize.y);
// fill the tiles with the map
elemForEach('.map-tiles img', (elem, index) => {
// get the base image
const base = baseAndOverlay[`t${index}Base`];
// put it on a canvas
const canvas = document.createElement('canvas');
const context = canvas.getContext('bitmaprenderer');
context.transferFromImageBitmap(base);
// if it's not there, return (tile not needed)
if (!base) return;
// assign the bitmap to the image
elem.width = finalTileSize.x;
elem.height = finalTileSize.y;
elem.src = canvas.toDataURL();
});
elemForEach('.overlay-tiles img', (elem, index) => {
// get the base image
const base = baseAndOverlay[`t${index}Overlay`];
// put it on a canvas
const canvas = document.createElement('canvas');
const context = canvas.getContext('bitmaprenderer');
context.transferFromImageBitmap(base);
// if it's not there, return (tile not needed)
if (!base) return;
// assign the bitmap to the image
elem.width = finalTileSize.x;
elem.height = finalTileSize.y;
elem.src = canvas.toDataURL();
});
// fill the tiles with the overlay
// shift the map tile containers
const tileShift = utils.modTile(sourceXY.x, sourceXY.y);
const tileShiftStretched = utils.mapSizeToFinalSize(tileShift.x, tileShift.y);
const mapTileContainer = this.elem.querySelector('.map-tiles');
mapTileContainer.style.top = `${-tileShiftStretched.y}px`;
mapTileContainer.style.left = `${-tileShiftStretched.x}px`;
const overlayTileContainer = this.elem.querySelector('.overlay-tiles');
overlayTileContainer.style.top = `${-tileShiftStretched.y}px`;
overlayTileContainer.style.left = `${-tileShiftStretched.x}px`;
// put the elements in the container // put the elements in the container
const scrollArea = this.elem.querySelector('.scroll-area'); const scrollArea = this.elem.querySelector('.scroll-area');
@@ -196,9 +249,9 @@ 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 = (data) => new Promise((resolve, reject) => {
// prepare for done message // prepare for done message
worker.onmessage = (e) => { worker.onmessage = (e) => {
if (e?.data instanceof Error) { if (e?.data instanceof Error) {
@@ -209,7 +262,7 @@ const radarWorker = () => {
}; };
// start up the worker // start up the worker
worker.postMessage(url); worker.postMessage(data);
}); });
// return the object // return the object
@@ -218,6 +271,31 @@ const radarWorker = () => {
}; };
}; };
// create a radar worker for the fixed background images
const fixedRadarWorker = () => {
// create the worker
const worker = new Worker(`/resources/radar-worker-bg-fg.mjs?_=${version()}`, { type: 'module' });
const processAssets = (data) => new Promise((resolve, reject) => {
// prepare for done message
worker.onmessage = (e) => {
if (e?.data instanceof Error) {
reject(e.data);
} else if (e?.data?.t0Base instanceof ImageBitmap) {
resolve(e.data);
}
};
// start up the worker
worker.postMessage(data);
});
// return the object
return {
processAssets,
};
};
// register display // register display
// TEMPORARY: except on IOS and bots // TEMPORARY: except on IOS and bots
if (!isIos && !isBot) { if (!isIos && !isBot) {

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

@@ -107,6 +107,15 @@
.container { .container {
.tiles {
position: absolute;
width: 1400px;
img {
vertical-align: middle;
}
}
.scroll-area { .scroll-area {
position: relative; position: relative;
} }

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

@@ -32,11 +32,13 @@
<div class="main radar"> <div class="main radar">
<div class="container"> <div class="container">
<div class="map-tiles tiles"><img/><img/><img/><img/></div>
<div class="scroll-area"> <div class="scroll-area">
<div class="frame template"> <div class="frame template">
<div class="map"> <div class="map">
</div> </div>
</div> </div>
</div> </div>
<div class="overlay-tiles tiles"><img/><img/><img/><img/></div>
</div> </div>
</div> </div>

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>

View File

@@ -43,7 +43,8 @@
"Malek", "Malek",
"mwood", "mwood",
"unmuted", "unmuted",
"dumpio" "dumpio",
"mesonet"
], ],
"cSpell.ignorePaths": [ "cSpell.ignorePaths": [
"**/package-lock.json", "**/package-lock.json",