Compare commits

...

26 Commits

Author SHA1 Message Date
Matt Walsh
8b076db25d 6.2.4 2025-10-17 00:51:09 +00:00
Matt Walsh
807932fe3c Merge branch 'ios-regex' close #137 2025-10-17 00:49:59 +00:00
Matt Walsh
7bb024eff5 6.2.3 2025-10-17 00:36:14 +00:00
Matt Walsh
f4a1a3a1d8 add hazards before custom scroll options close #149 2025-10-17 00:35:26 +00:00
Matt Walsh
9a5efe9d48 update dependencies 2025-10-17 00:14:59 +00:00
Matt Walsh
58e0611a46 6.2.2 2025-10-16 19:00:30 -05:00
Matt Walsh
9ed496c892 better formatting for headend info 2025-10-16 19:00:20 -05:00
Matt Walsh
31315d1ace add com.chrome.devtools.json 2025-10-16 18:36:41 -05:00
Matt Walsh
77838e1a81 use locally stored weather parameters in spc outlook close #150 2025-10-15 00:29:23 +00:00
Matt Walsh
64d6484bd8 Merge pull request #151 from bparkin1283/patch-1
Update README.md clarifying displays if you're within one of the high…
2025-10-09 11:17:13 -05:00
bparkin1283
20cab8c25e Update README.md clarifying displays if you're within one of the highlight areas 2025-10-09 10:55:33 -05:00
Matt Walsh
b4de17ccd0 update dependencies 2025-10-02 21:50:28 -05:00
Matt Walsh
0fd90feb7a update community notes 2025-10-02 21:37:36 -05:00
Matt Walsh
8c3b596b69 add build script for travel cities #146 2025-10-02 21:26:45 -05:00
Matt Walsh
e57b9bcb20 6.2.1 2025-09-24 22:33:59 -05:00
Matt Walsh
e27750e915 fix load order on scroll when compiled 2025-09-24 22:33:47 -05:00
Matt Walsh
f5431a04c7 6.2.0 2025-09-24 22:27:44 -05:00
Matt Walsh
5117a9d475 move bottom scroll to single div #144 2025-09-24 22:27:31 -05:00
Matt Walsh
28baa022a9 6.1.11 2025-09-15 08:54:20 -05:00
Matt Walsh
e8b8890260 fix full screen centering on chrome #139 2025-09-15 08:54:11 -05:00
Matt Walsh
b797a10b9e 6.1.10 2025-09-15 08:04:24 -05:00
Matt Walsh
2a64cda383 Merge branch 'fullscreen-kiosk-sizing' 2025-09-15 08:03:53 -05:00
Matt Walsh
e6e357c51b separate full screen container and scaling #139 2025-09-15 08:01:28 -05:00
Matt Walsh
24deb4dce4 additional full screen scaling calculation adjustments 2025-09-11 15:34:02 -05:00
Matt Walsh
14b1891efd direct check of regex lookbehind capability 2025-09-11 08:47:16 -05:00
Matt Walsh
cc05aafb95 patch for kiosk drawing off screen 2025-09-09 19:22:18 -05:00
34 changed files with 1239 additions and 3560 deletions

View File

@@ -11,4 +11,4 @@ Please do not report issues with api.weather.gov being down. It's a new service
Please include:
* Web browser and OS
* Forecast Information text block from the very bottom of the web page
* Headend Information text block from the very bottom of the web page

View File

@@ -179,7 +179,7 @@ I've made several changes to this Weather Star 4000 simulation compared to the o
* Radar displays the timestamp of the image.
* A new hour-by-hour graph of the temperature, cloud cover and precipitation chances for the next 24 hours.
* A new hourly forecast display for the next 24 hours is available, and is shown in the style of the travel cities forecast. (off by default because it duplicates the hourly graph)
* The SPC Outlook is shown in the style of the old air quality screen. This shows the probability of severe weather over the next 3 days at your location.
* The SPC Outlook is shown in the style of the old air quality screen. This shows the probability of severe weather over the next 3 days at your location. SPC outlook only displays if you're within one of the highlight areas over the next 3 day. You can view the [maps](https://www.weather.gov/crh/outlooks) and pick a location within one of the risk categories to see if the screen is working for you.
* The "Local Forecast" and "Extended Forecast" provide several additional days of information compared to the original format in the 90s.
* The original music has been replaced. More info in [Music](#music).
* Marine forecast (tides) is not available as it is not reliably part of the new API.
@@ -321,6 +321,7 @@ Thanks to the WeatherStar+ community for providing these discussions to further
* [ws4channels](https://github.com/rice9797/ws4channels) A Dockerized Node.js application to stream WeatherStar 4000 data into Channels DVR using Puppeteer and FFmpeg.
* [SSL Certificates](https://github.com/netbymatt/ws4kp/issues/135) Discussion about how to host with an SSL certificate (enables geolocation).
* [Changing playlists](https://github.com/netbymatt/ws4kp/issues/138) Possible ways to automatically change the playlist on a schedule.
* [Customize Travel Forecast Cities](https://github.com/netbymatt/ws4kp/issues/146#issuecomment-3363940202)
## Customization

View File

@@ -84,8 +84,8 @@
"Latitude": 29.7633,
"Longitude": -95.3633,
"point": {
"x": 65,
"y": 97,
"x": 63,
"y": 95,
"wfo": "HGX"
}
},

View File

@@ -5,8 +5,8 @@ import rename from 'gulp-rename';
const clean = () => deleteAsync(['./server/scripts/vendor/auto/**']);
const vendorFiles = [
'./node_modules/luxon/build/es6/luxon.js',
'./node_modules/luxon/build/es6/luxon.js.map',
'./node_modules/luxon/build/es6/luxon.mjs',
'./node_modules/luxon/build/es6/luxon.mjs.map',
'./node_modules/nosleep.js/dist/NoSleep.js',
'./node_modules/suncalc/suncalc.js',
'./node_modules/swiped-events/src/swiped-events.js',
@@ -23,7 +23,6 @@ const copy = () => src(vendorFiles)
path.dirname = path.dirname.toLowerCase();
path.basename = path.basename.toLowerCase();
path.extname = path.extname.toLowerCase();
if (path.basename === 'luxon') path.extname = '.mjs';
}))
.pipe(dest('./server/scripts/vendor/auto'));

View File

@@ -8,6 +8,7 @@ import {
import playlist from './src/playlist.mjs';
import OVERRIDES from './src/overrides.mjs';
import cache from './proxy/cache.mjs';
import devTools from './src/com.chrome.devtools.mjs';
const travelCities = JSON.parse(await readFile('./datagenerators/output/travelcities.json'));
const regionalCities = JSON.parse(await readFile('./datagenerators/output/regionalcities.json'));
@@ -168,6 +169,7 @@ if (process.env?.DIST === '1') {
app.use('/geoip', geoip);
app.use('/resources', express.static('./server/scripts/modules'));
app.get('/', index);
app.get('/.well-known/appspecific/com.chrome.devtools.json', devTools);
app.get('*name', express.static('./server', staticOptions));
}

4150
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "ws4kp",
"version": "6.1.9",
"version": "6.2.4",
"description": "Welcome to the WeatherStar 4000+ project page!",
"main": "index.mjs",
"type": "module",
@@ -8,6 +8,7 @@
"start": "node index.mjs",
"stop": "pkill -f 'node index.mjs' || echo 'No process found'",
"test": "echo \"Error: no test specified\" && exit 1",
"build:travelcities": "node datagenerators/travelcities.mjs",
"build:css": "sass --style=compressed ./server/styles/scss/main.scss ./server/styles/main.css",
"build": "gulp buildDist",
"lint": "eslint ./server/scripts/**/*.mjs ./proxy/**/*.mjs ./src/**/*.mjs *.mjs",
@@ -50,13 +51,12 @@
"swiped-events": "^1.1.4",
"terser-webpack-plugin": "^5.3.6",
"webpack": "^5.99.9",
"webpack-stream": "^7.0.0"
"webpack-stream": "^7.0.0",
"metar-taf-parser": "^9.0.0"
},
"dependencies": {
"dotenv": "^17.0.1",
"ejs": "^3.1.5",
"express": "^5.1.0",
"metar-taf-parser": "^9.0.0",
"npm": "^11.6.0"
"express": "^5.1.0"
}
}

View File

@@ -1,5 +1,4 @@
import { locationCleanup } from './utils/string.mjs';
import { elemForEach } from './utils/elem.mjs';
import getCurrentWeather from './currentweather.mjs';
import { currentDisplay } from './navigation.mjs';
import getHazards from './hazards.mjs';
@@ -12,6 +11,16 @@ const TICK_INTERVAL_MS = 500; // milliseconds per tick
const secondsToTicks = (seconds) => Math.ceil((seconds * 1000) / TICK_INTERVAL_MS);
const DEFAULT_UPDATE = secondsToTicks(4.0); // 4 second default for each current conditions
// items on page
let mainScroll;
let fixedScroll;
let header;
document.addEventListener('DOMContentLoaded', () => {
mainScroll = document.querySelector('#container>.scroll');
fixedScroll = document.querySelector('#container>.scroll .fixed');
header = document.querySelector('#container>.scroll .scroll-header');
});
// local variables
let interval;
let screenIndex = 0;
@@ -23,6 +32,8 @@ let defaultScreensLoaded = true;
// start drawing conditions
// reset starts from the first item in the text scroll list
const start = () => {
// show the block
show();
// if already started, draw the screen on a reset flag and return
if (interval) {
if (resetFlag) drawScreen();
@@ -62,6 +73,7 @@ const incrementInterval = (force) => {
const display = currentDisplay();
if (!display?.okToDrawCurrentConditions) {
stop(display?.elemId === 'progress');
hide();
return;
}
screenIndex = (screenIndex + 1) % (workingScreens.length);
@@ -91,18 +103,8 @@ const drawScreen = async () => {
const thisScreen = workingScreens[screenIndex](scrollData);
// 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));
});
// special case for red background on hazard scroll
const mainScrollBg = document.getElementById('scroll-bg');
mainScrollBg.className = '';
if (thisScreen?.classes?.includes('hazard')) {
mainScrollBg.classList.add('hazard');
}
mainScroll.classList.forEach((cls) => { if (cls !== 'scroll') mainScroll.classList.remove(cls); });
thisScreen?.classes?.forEach((cls) => mainScroll.classList.add(cls));
if (typeof thisScreen === 'string') {
// only a string
@@ -131,9 +133,7 @@ const hazards = (data) => {
// test for data
if (!data.hazards || data.hazards.length === 0) return false;
// since the hazard scroll element has no left/right margins, pad the beginning and end with non-breaking spaces
const padding = ' '.repeat(4);
const hazard = `${padding}${data.hazards[0].properties.event} ${data.hazards[0].properties.description}${padding}`;
const hazard = `${data.hazards[0].properties.event} ${data.hazards[0].properties.description}`;
return {
text: hazard,
@@ -196,17 +196,12 @@ let workingScreens = [...baseScreens, ...additionalScreens];
// internal draw function with preset parameters
const drawCondition = (text) => {
// update all html scroll elements
elemForEach('.weather-display .scroll .fixed', (elem) => {
elem.innerHTML = text;
});
fixedScroll.innerHTML = text;
setHeader('');
};
const setHeader = (text) => {
elemForEach('.weather-display .scroll .scroll-header', (elem) => {
elem.innerHTML = text ?? '';
});
header.innerHTML = text ?? '';
};
// reset the screens back to the original set
@@ -229,14 +224,14 @@ const drawScrollCondition = (screen) => {
scrollElement.classList.add('scroll-area');
scrollElement.innerHTML = screen.text;
// add it to the page to get the width
document.querySelector('.weather-display .scroll .fixed').innerHTML = scrollElement.outerHTML;
fixedScroll.innerHTML = scrollElement.outerHTML;
// grab the width
const { scrollWidth, clientWidth } = document.querySelector('.weather-display .scroll .fixed .scroll-area');
const { scrollWidth, clientWidth } = document.querySelector('#container>.scroll .fixed .scroll-area');
// calculate the scroll distance and set a minimum scroll
const scrollDistance = Math.max(scrollWidth - clientWidth, 0);
// calculate the scroll time (scaled by global speed setting)
const scrollTime = scrollDistance / SCROLL_SPEED * settings.speed.value;
// calculate the scroll time (scaled by global speed setting), minimum 2s (4s when added to start and end delays)
const scrollTime = Math.max(scrollDistance / SCROLL_SPEED * settings.speed.value, 2);
// add 1 second pause at the end of the scroll animation
const endPauseTime = 1.0;
const totalAnimationTime = scrollTime + endPauseTime;
@@ -252,17 +247,13 @@ const drawScrollCondition = (screen) => {
scrollElement.style.backfaceVisibility = 'hidden'; // Force hardware acceleration
scrollElement.style.perspective = '1000px'; // Enable 3D rendering context
elemForEach('.weather-display .scroll .fixed', (elem) => {
elem.innerHTML = '';
elem.append(scrollElement.cloneNode(true));
});
fixedScroll.innerHTML = '';
fixedScroll.append(scrollElement.cloneNode(true));
// start the scroll after the specified delay
setTimeout(() => {
// change the transform to trigger the scroll
elemForEach('.weather-display .scroll .fixed .scroll-area', (elem) => {
elem.style.transform = `translateX(-${scrollDistance.toFixed(0)}px)`;
});
document.querySelector('#container>.scroll .fixed .scroll-area').style.transform = `translateX(-${scrollDistance.toFixed(0)}px)`;
}, startDelayTime * 1000);
};
@@ -270,9 +261,19 @@ const parseMessage = (event) => {
if (event?.data?.type === 'current-weather-scroll') {
if (event.data?.method === 'start') start();
if (event.data?.method === 'reload') stop(true);
if (event.data?.method === 'show') show();
if (event.data?.method === 'hide') hide();
}
};
const show = () => {
mainScroll.style.display = 'block';
};
const hide = () => {
mainScroll.style.display = 'none';
};
const screenCount = () => workingScreens.length;
const atDefault = () => defaultScreensLoaded;
@@ -283,6 +284,8 @@ window.CurrentWeatherScroll = {
addScreen,
reset,
start,
show,
hide,
screenCount,
atDefault,
};
@@ -291,6 +294,9 @@ export {
addScreen,
reset,
start,
show,
hide,
screenCount,
atDefault,
hazards,
};

View File

@@ -1,5 +1,5 @@
import Setting from './utils/setting.mjs';
import { reset as resetScroll, addScreen as addScroll } from './currentweatherscroll.mjs';
import { reset as resetScroll, addScreen as addScroll, hazards } from './currentweatherscroll.mjs';
import { json } from './utils/fetch.mjs';
let firstRun = true;
@@ -42,8 +42,9 @@ const parseFeed = (textInput) => {
return;
}
// add single text scroll
// add single text scroll after hazards if present
resetScroll();
addScroll(hazards);
addScroll(
() => (
{
@@ -81,6 +82,8 @@ const getFeed = async (url) => {
// reset the scroll, then add the screens
resetScroll();
// add the hazards scroll first
addScroll(hazards);
titles.forEach((title) => {
// data is provided to the screen handler, so we return a function
addScroll(

View File

@@ -330,6 +330,7 @@ const handleNavButton = (button) => {
break;
case 'menu':
setPlaying(false);
postMessage({ type: 'current-weather-scroll', method: 'hide' });
if (progress) {
progress.showCanvas();
} else if (settings?.kiosk?.value) {
@@ -357,6 +358,17 @@ const isIOS = () => {
let lastAppliedScale = null;
let lastAppliedKioskMode = null;
// Helper function to clear CSS properties from elements
const clearElementStyles = (element, properties) => {
properties.forEach((prop) => element.style.removeProperty(prop));
};
// Define property groups for different scaling modes
const SCALING_PROPERTIES = {
wrapper: ['width', 'height', 'transform', 'transform-origin'],
positioning: ['transform', 'transform-origin', 'width', 'height', 'position', 'left', 'top', 'margin-left', 'margin-top'],
};
// resize the container on a page resize
const resize = (force = false) => {
// Ignore resize events caused by pinch-to-zoom on mobile
@@ -376,9 +388,8 @@ const resize = (force = false) => {
// Standard scaling: fit within both dimensions
const scale = Math.min(widthZoomPercent, heightZoomPercent);
// For Mobile Safari in kiosk mode, always use centering behavior regardless of scale
// For other platforms, only use fullscreen/centering behavior for actual fullscreen or kiosk mode where content fits naturally
const isKioskLike = isFullscreen || (isKioskMode && scale >= 1.0) || isMobileSafariKiosk;
// Use centering behavior for fullscreen, kiosk mode, or Mobile Safari kiosk mode
const isKioskLike = isFullscreen || isKioskMode || isMobileSafariKiosk;
if (debugFlag('resize') || debugFlag('fullscreen')) {
console.log(`🖥️ Resize: force=${force} isKioskLike=${isKioskLike} window=${window.innerWidth}x${window.innerHeight} targetWidth=${targetWidth} widthZoom=${widthZoomPercent.toFixed(3)} heightZoom=${heightZoomPercent.toFixed(3)} finalScale=${scale.toFixed(3)} fullscreenElement=${!!document.fullscreenElement} isIOS=${isIOS()} standalone=${window.navigator.standalone} isMobileSafariKiosk=${isMobileSafariKiosk} kioskMode=${settings.kiosk?.value} wideMode=${settings.wide.value}`);
@@ -412,40 +423,35 @@ const resize = (force = false) => {
console.log('🖥️ Resetting fullscreen/kiosk styles to normal');
}
// Reset wrapper styles (only properties that are actually set in fullscreen/scaling modes)
wrapper.style.removeProperty('width');
wrapper.style.removeProperty('height');
wrapper.style.removeProperty('overflow');
wrapper.style.removeProperty('transform');
wrapper.style.removeProperty('transform-origin');
// Reset container styles that might have been applied during fullscreen
mainContainer.style.removeProperty('transform');
mainContainer.style.removeProperty('transform-origin');
mainContainer.style.removeProperty('width');
mainContainer.style.removeProperty('height');
mainContainer.style.removeProperty('position');
mainContainer.style.removeProperty('left');
mainContainer.style.removeProperty('top');
mainContainer.style.removeProperty('margin-left');
mainContainer.style.removeProperty('margin-top');
// Reset all scaling-related styles
const container = document.querySelector('#container');
clearElementStyles(wrapper, SCALING_PROPERTIES.wrapper);
clearElementStyles(container, SCALING_PROPERTIES.positioning);
clearElementStyles(mainContainer, SCALING_PROPERTIES.positioning);
applyScanlineScaling(1.0);
return;
}
// MOBILE SCALING: Use wrapper scaling for mobile devices (but not Mobile Safari kiosk mode)
if ((scale < 1.0 || (isKioskMode && !isKioskLike)) && !isMobileSafariKiosk) {
// MOBILE SCALING: Use wrapper scaling for mobile devices (but not when in fullscreen/kiosk mode)
if ((scale < 1.0 || (isKioskMode && !isKioskLike)) && !isMobileSafariKiosk && !isKioskLike) {
/*
* MOBILE SCALING (Wrapper Scaling)
*
* This path is used for regular mobile browsing (NOT fullscreen/kiosk modes).
* Why scale the wrapper instead of mainContainer?
* - For mobile devices where content is larger than viewport, we need to scale the entire layout
* - The wrapper (#divTwc) contains both the main content AND the bottom navigation bar
* - Scaling the wrapper ensures both elements are scaled together as a unit
* - No centering is applied - content aligns to top-left for typical mobile behavior
* - Content aligns to top-left for typical mobile web browsing behavior (no centering)
* - Uses explicit dimensions to prevent layout issues and eliminate gaps after scaling
*/
// Reset any container/mainContainer styles that might have been set during fullscreen/kiosk mode
const container = document.querySelector('#container');
clearElementStyles(container, SCALING_PROPERTIES.positioning);
clearElementStyles(mainContainer, SCALING_PROPERTIES.positioning);
wrapper.style.setProperty('transform', `scale(${scale})`);
wrapper.style.setProperty('transform-origin', 'top left'); // Scale from top-left corner
@@ -458,7 +464,7 @@ const resize = (force = false) => {
const scaledHeight = totalHeight * scale; // Height after scaling
wrapper.style.setProperty('width', `${wrapperWidth}px`);
wrapper.style.setProperty('height', `${scaledHeight}px`); // Use scaled height to eliminate gap
wrapper.style.setProperty('height', `${scaledHeight}px`); // Use scaled height to eliminate gap under #divTwc on index page
applyScanlineScaling(scale);
return;
}
@@ -468,10 +474,7 @@ const resize = (force = false) => {
const wrapperHeight = 480;
// Reset wrapper styles to avoid double scaling (wrapper remains unstyled)
wrapper.style.removeProperty('width');
wrapper.style.removeProperty('height');
wrapper.style.removeProperty('transform');
wrapper.style.removeProperty('transform-origin');
clearElementStyles(wrapper, SCALING_PROPERTIES.wrapper);
// Platform-specific positioning logic
let transformOrigin;
@@ -529,7 +532,7 @@ const resize = (force = false) => {
const offsetY = (window.innerHeight - scaledHeight) / 2;
if (debugFlag('fullscreen')) {
console.log(`🖥️ Applying fullscreen/kiosk scaling: wrapper=${wrapperWidth}x${wrapperHeight} scale=${scale.toFixed(3)} offset=${offsetX.toFixed(1)},${offsetY.toFixed(1)} transform: scale(${scale}) translate(${offsetX / scale}px, ${offsetY / scale}px)`);
console.log(`🖥️ Applying fullscreen/kiosk scaling: wrapper=${wrapperWidth}x${wrapperHeight} scale=${scale.toFixed(3)} offset=${offsetX.toFixed(1)},${offsetY.toFixed(1)} target=${isFullscreen ? '#container' : '#divTwcMain'}`);
}
// Set positioning values for CSS-based centering
@@ -540,25 +543,41 @@ const resize = (force = false) => {
marginTop = `-${wrapperHeight / 2}px`; // Pull back by half height
}
// Apply shared mainContainer properties (same for both kiosk modes)
mainContainer.style.setProperty('transform', `scale(${scale})`, 'important');
mainContainer.style.setProperty('transform-origin', transformOrigin, 'important');
mainContainer.style.setProperty('width', `${wrapperWidth}px`, 'important');
mainContainer.style.setProperty('height', `${wrapperHeight}px`, 'important');
mainContainer.style.setProperty('position', 'absolute', 'important');
mainContainer.style.setProperty('left', leftPosition, 'important');
mainContainer.style.setProperty('top', topPosition, 'important');
// Chrome fullscreen compatibility: apply transform to #container instead of #divTwcMain
// This works around Chrome's restriction on styling fullscreen elements directly
const container = document.querySelector('#container');
const targetElement = isFullscreen ? container : mainContainer;
// Reset the other element's styles to avoid conflicts
if (isFullscreen) {
// Reset mainContainer styles when using container for fullscreen
clearElementStyles(mainContainer, SCALING_PROPERTIES.positioning);
} else {
// Reset container styles when using mainContainer for kiosk mode
clearElementStyles(container, SCALING_PROPERTIES.positioning);
}
// Apply shared properties to the target element
targetElement.style.setProperty('transform', `scale(${scale})`, 'important');
targetElement.style.setProperty('transform-origin', transformOrigin, 'important');
// the width of the target element does not change it is the fixed width of the 4:3 display which is then scaled
// the wrapper adds margins and padding to achieve widescreen
// targetElement.style.setProperty('width', `${wrapperWidth}px`, 'important');
targetElement.style.setProperty('height', `${wrapperHeight}px`, 'important');
targetElement.style.setProperty('position', 'absolute', 'important');
targetElement.style.setProperty('left', leftPosition, 'important');
targetElement.style.setProperty('top', topPosition, 'important');
// Apply or clear margin properties based on positioning method
if (marginLeft !== null) {
mainContainer.style.setProperty('margin-left', marginLeft, 'important');
targetElement.style.setProperty('margin-left', marginLeft, 'important');
} else {
mainContainer.style.removeProperty('margin-left');
targetElement.style.removeProperty('margin-left');
}
if (marginTop !== null) {
mainContainer.style.setProperty('margin-top', marginTop, 'important');
targetElement.style.setProperty('margin-top', marginTop, 'important');
} else {
mainContainer.style.removeProperty('margin-top');
targetElement.style.removeProperty('margin-top');
}
applyScanlineScaling(scale);

View File

@@ -22,7 +22,9 @@ class Progress extends WeatherDisplay {
}
async drawCanvas(displays, loadedCount) {
// skip drawing if not displayed, or not yet available
if (!this.elem) return;
if (this.elem.classList.contains('show') === false) return;
super.drawCanvas();
// get the progress bar cover (makes percentage)

View File

@@ -57,6 +57,7 @@ class SpcOutlook extends WeatherDisplay {
}
async getData(weatherParameters, refresh) {
if (weatherParameters) this.weatherParameters = weatherParameters;
if (!super.getData(weatherParameters, refresh)) return;
// SPC outlook data does not need to be reloaded on a location change, only during silent refresh
@@ -93,7 +94,7 @@ class SpcOutlook extends WeatherDisplay {
}
}
// parse the data
this.data = testAllPoints([weatherParameters.longitude, weatherParameters.latitude], this.rawOutlookData);
this.data = testAllPoints([this.weatherParameters.longitude, this.weatherParameters.latitude], this.rawOutlookData);
// check if there's a "risk" for any of the three days, otherwise skip the SPC Outlook screen
if (this.data.reduce((prev, cur) => prev || !!cur, false)) {

View File

@@ -5,21 +5,22 @@ import en from '../../vendor/auto/locale/en.js';
// metar-taf-parser requires regex lookbehind
// this does not work in iOS < 16.4
// this is a detection algorithm for iOS versions
const isIos = /iP(ad|od|hone)/i.test(window.navigator.userAgent);
let iosVersionOk = false;
if (isIos) {
// regex match the version string
const iosVersionRaw = /OS (\d+)_(\d+)/.exec(window.navigator.userAgent);
// check for match
if (iosVersionRaw) {
// break into parts
const iosVersionMajor = parseInt(iosVersionRaw[1], 10);
const iosVersionMinor = parseInt(iosVersionRaw[2], 10);
if (iosVersionMajor > 16) iosVersionOk = true;
if (iosVersionMajor === 16 && iosVersionMinor >= 4) iosVersionOk = true;
// this is a detection algorithm for missing lookbehind support
const supportsRegexLookAheadLookBehindCheck = () => {
try {
return (
// deliberately using RegExp for broader browser support during check
/* eslint-disable prefer-regex-literals */
'hibyehihi'
.replace(new RegExp('(?<=hi)hi', 'g'), 'hello')
.replace(new RegExp('hi(?!bye)', 'g'), 'hey') === 'hibyeheyhello'
/* eslint-enable prefer-regex-literals */
);
} catch {
return false;
}
}
};
const supportsRegexLookAheadLookBehind = supportsRegexLookAheadLookBehindCheck();
/**
* Augment observation data by parsing METAR when API fields are missing
@@ -27,8 +28,8 @@ if (isIos) {
* @returns {Object} - Augmented observation with parsed METAR data filled in
*/
const augmentObservationWithMetar = (observation) => {
// check for a metar message and for unusable ios versions
if (!observation?.rawMessage || (isIos && !iosVersionOk)) {
// check for a metar message and for regex lookbehind support
if (!observation?.rawMessage || (!supportsRegexLookAheadLookBehind)) {
return observation;
}

View File

@@ -172,6 +172,7 @@ class WeatherDisplay {
if (this.screenIndex < 0) this.screenIndex = 0;
if (this.okToDrawCurrentDateTime) this.drawCurrentDateTime();
if (this.okToDrawCurrentConditions) postMessage({ type: 'current-weather-scroll', method: 'start' });
if (this.okToDrawCurrentConditions === false) postMessage({ type: 'current-weather-scroll', method: 'hide' });
}
finishDraw() {

File diff suppressed because one or more lines are too long

View File

@@ -8127,7 +8127,7 @@ function friendlyDateTime(dateTimeish) {
}
}
const VERSION = "3.7.1";
const VERSION = "3.7.2";
export { DateTime, Duration, FixedOffsetZone, IANAZone, Info, Interval, InvalidZone, Settings, SystemZone, VERSION, Zone };
//# sourceMappingURL=luxon.js.map
//# sourceMappingURL=luxon.mjs.map

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -341,13 +341,14 @@ body {
// overflow: hidden;
background-image: url(../images/backgrounds/1.png);
transform-origin: 0 0;
background-repeat: no-repeat;
}
.wide #container {
padding-left: 107px;
padding-right: 107px;
background: url(../images/backgrounds/1-wide.png);
background-repeat: no-repeat;
background: url(../images/backgrounds/1-wide.png)
}
#divTwc:fullscreen #container,

View File

@@ -112,31 +112,32 @@
}
}
.scroll {
@include u.text-shadow(3px, 1.5px);
#container>.scroll {
display: none;
@include u.text-shadow(3px, 1.5px);
width: 640px;
height: 77px;
overflow: hidden;
margin-top: 3px;
position: absolute;
bottom: 0px;
z-index: 1;
&.hazard {
background-color: rgb(112, 35, 35);
}
.scroll-container {
width: 640px;
height: 77px;
overflow: hidden;
margin-top: 3px;
position: relative;
z-index: 1;
&.hazard {
background-color: rgb(112, 35, 35);
}
.fixed,
.scroll-header {
margin-left: 55px;
margin-right: 55px;
overflow: hidden;
}
// Remove margins for hazard scrolls to maximize text space
&.hazard .fixed {
margin-left: 0;
margin-right: 0;
white-space: nowrap;
}
.scroll-header {
@@ -158,21 +159,17 @@
// left: calc((elem width) - 640px);
}
}
}
}
#scroll-bg {
position: absolute;
bottom: 0px;
height: 77px;
width: 640px;
&.hazard {
background-color: rgb(112, 35, 35);
}
}
.wide #scroll-bg {
.wide #container>.scroll {
width: 854px;
margin-left: -107px;
.scroll-container {
margin-left: 107px;
}
}

View File

@@ -0,0 +1,25 @@
import path from 'path';
// get values for devtools json
const uuid = 'd2bd1130-560f-4c8e-b2c5-e91073784964';
const root = path.resolve('server');
const DEVTOOLS_CONFIG = {
workspace: {
uuid,
root,
},
};
const devTools = (req, res) => {
// test for localhost
if (['127.0.0.1', '::1', '::ffff:127.0.0.1'].includes(req.ip)) {
console.log(DEVTOOLS_CONFIG);
res.json(DEVTOOLS_CONFIG);
} else {
// not localhost
res.status(404).send('File not found');
}
};
export default devTools;

View File

@@ -133,8 +133,8 @@
<div id="hazards-html" class="weather-display">
<%- include('partials/hazards.ejs') %>
</div>
<%- include('partials/scroll.ejs') %>
</div>
<div id="scroll-bg"></div>
</div>
<div id="divTwcBottom">
<div id="divTwcBottomLeft">
@@ -186,16 +186,24 @@
</div>
</div>
<div class='heading'>Forecast Information</div>
<div class='heading'>Headend Information</div>
<div id="divInfo">
Location: <span id="spanCity"></span> <span id="spanState"></span><br />
Station Id: <span id="spanStationId"></span><br />
Radar Id: <span id="spanRadarId"></span><br />
Zone Id: <span id="spanZoneId"></span><br />
Office Id: <span id="spanOfficeId"></span><br />
Grid X,Y: <span id="spanGridPoint"></span><br />
Music: <span id="musicTrack">Not playing</span><br />
Ws4kp Version: <span><%- version %></span>
<div class="header">Location:</div>
<div class="header"><span id="spanCity"></span> <span id="spanState"></span></div>
<div class="header">Station Id:</div>
<div class="header"><span id="spanStationId"></span></div>
<div class="header">Radar Id:</div>
<div class="header"><span id="spanRadarId"></span></div>
<div class="header">Zone Id:</div>
<div class="header"><span id="spanZoneId"></span></div>
<div class="header">Office Id:</div>
<div class="header"><span id="spanOfficeId"></span></div>
<div class="header">Grid X,Y:</div>
<div class="header"><span id="spanGridPoint"></span></div>
<div class="header">Music:</div>
<div class="header"><span id="musicTrack">Not playing</span></div>
<div class="header">Ws4kp Version:</div>
<div class="header"><span><%- version %></span></div>
</div>
</div>

View File

@@ -21,5 +21,4 @@
</div>
</div>
</div>
</div>
<%- include('scroll.ejs') %>
</div>

View File

@@ -1,43 +1,42 @@
<%- include('header.ejs', {titleDual:{ top: 'Current' , bottom: 'Conditions' }, noaaLogo: true, hasTime: true}) %>
<div class="main has-scroll has-box current-weather">
<div class="weather template">
<div class="left col">
<div class="temp center"></div>
<div class="condition center"></div>
<div class="icon center"><img src="" /></div>
<div class="wind-container">
<div class="wind-label">Wind:</div>
<div class="wind"></div>
</div>
<div class="wind-gusts"></div>
</div>
<div class="right col">
<div class="location"></div>
<div class="row">
<div class="label">Humidity:</div>
<div class="humidity value"></div>
</div>
<div class="row">
<div class="label">Dewpoint:</div>
<div class="dewpoint value"></div>
</div>
<div class="row">
<div class="label">Ceiling:</div>
<div class="ceiling value"></div>
</div>
<div class="row">
<div class="label">Visibility:</div>
<div class="visibility value"></div>
</div>
<div class="row">
<div class="label">Pressure:</div>
<div class="pressure value"></div>
</div>
<div class="row">
<div class="heat-index-label label"></div>
<div class="heat-index value"></div>
</div>
</div>
</div>
</div>
<%- include('scroll.ejs') %>
<div class="main has-scroll has-box current-weather">
<div class="weather template">
<div class="left col">
<div class="temp center"></div>
<div class="condition center"></div>
<div class="icon center"><img src="" /></div>
<div class="wind-container">
<div class="wind-label">Wind:</div>
<div class="wind"></div>
</div>
<div class="wind-gusts"></div>
</div>
<div class="right col">
<div class="location"></div>
<div class="row">
<div class="label">Humidity:</div>
<div class="humidity value"></div>
</div>
<div class="row">
<div class="label">Dewpoint:</div>
<div class="dewpoint value"></div>
</div>
<div class="row">
<div class="label">Ceiling:</div>
<div class="ceiling value"></div>
</div>
<div class="row">
<div class="label">Visibility:</div>
<div class="visibility value"></div>
</div>
<div class="row">
<div class="label">Pressure:</div>
<div class="pressure value"></div>
</div>
<div class="row">
<div class="heat-index-label label"></div>
<div class="heat-index value"></div>
</div>
</div>
</div>
</div>

View File

@@ -19,5 +19,4 @@
</div>
</div>
</div>
</div>
<%- include('scroll.ejs') %>
</div>

View File

@@ -1,24 +1,23 @@
<%- include('header.ejs', {title: 'Hourly Graph' , hasTime: false }) %>
<div class="main has-scroll hourly-graph">
<div class="top-right template ">
<div class="temperature">Temperature</div>
<div class="cloud">Cloud %</div>
<div class="rain">Precip %</div>
</div>
<div class="y-axis">
<div class="label l-1">75</div>
<div class="label l-2">65</div>
<div class="label l-3">55</div>
</div>
<div class="chart">
<img id="chart-area"></img>
</div>
<div class="x-axis">
<div class="label l-1">12a</div>
<div class="label l-2">6a</div>
<div class="label l-3">12p</div>
<div class="label l-4">6p</div>
<div class="label l-5">12a</div>
</div>
</div>
<%- include('scroll.ejs') %>
<div class="top-right template ">
<div class="temperature">Temperature</div>
<div class="cloud">Cloud %</div>
<div class="rain">Precip %</div>
</div>
<div class="y-axis">
<div class="label l-1">75</div>
<div class="label l-2">65</div>
<div class="label l-3">55</div>
</div>
<div class="chart">
<img id="chart-area"></img>
</div>
<div class="x-axis">
<div class="label l-1">12a</div>
<div class="label l-2">6a</div>
<div class="label l-3">12p</div>
<div class="label l-4">6p</div>
<div class="label l-5">12a</div>
</div>
</div>

View File

@@ -1,18 +1,17 @@
<%- include('header.ejs', {title: 'Hourly Forecast' , hasTime: true }) %>
<div class="main has-scroll hourly">
<div class="column-headers">
<div class="temp">TEMP</div>
<div class="like">LIKE</div>
<div class="wind">WIND</div>
</div>
<div class="hourly-lines">
<div class="hourly-row template">
<div class="hour"></div>
<div class="icon"><img /></div>
<div class="temp"></div>
<div class="like"></div>
<div class="wind"></div>
</div>
</div>
</div>
<%- include('scroll.ejs') %>
<div class="main has-scroll hourly">
<div class="column-headers">
<div class="temp">TEMP</div>
<div class="like">LIKE</div>
<div class="wind">WIND</div>
</div>
<div class="hourly-lines">
<div class="hourly-row template">
<div class="hour"></div>
<div class="icon"><img /></div>
<div class="temp"></div>
<div class="like"></div>
<div class="wind"></div>
</div>
</div>
</div>

View File

@@ -1,20 +1,19 @@
<%- include('header.ejs', {titleDual:{ top: 'Latest' , bottom: 'Observations' }, noaaLogo: true, hasTime: true }) %>
<div class="main has-scroll latest-observations has-box">
<div class="container">
<div class="column-headers">
<div class="temp english">&deg;F</div>
<div class="temp metric">&deg;C</div>
<div class="weather">Weather</div>
<div class="wind">Wind</div>
</div>
<div class="observation-lines">
<div class="observation-row template">
<div class="location"></div>
<div class="temp"></div>
<div class="weather"></div>
<div class="wind"></div>
</div>
</div>
</div>
</div>
<%- include('scroll.ejs') %>
<div class="main has-scroll latest-observations has-box">
<div class="container">
<div class="column-headers">
<div class="temp english">&deg;F</div>
<div class="temp metric">&deg;C</div>
<div class="weather">Weather</div>
<div class="wind">Wind</div>
</div>
<div class="observation-lines">
<div class="observation-row template">
<div class="location"></div>
<div class="temp"></div>
<div class="weather"></div>
<div class="wind"></div>
</div>
</div>
</div>
</div>

View File

@@ -1,14 +1,13 @@
<%- include('header.ejs', {titleDual:{ top: 'Regional' , bottom: 'Observations' }, hasTime: true }) %>
<div class="main has-scroll regional-forecast">
<div class="map"><img src="images/maps/basemap.webp" /></div>
<div class="location-container">
<div class="location template">
<div class="icon">
<img src="" />
</div>
<div class="city"></div>
<div class="temp"></div>
</div>
</div>
</div>
<%- include('scroll.ejs') %>
<div class="main has-scroll regional-forecast">
<div class="map"><img src="images/maps/basemap.webp" /></div>
<div class="location-container">
<div class="location template">
<div class="icon">
<img src="" />
</div>
<div class="city"></div>
<div class="temp"></div>
</div>
</div>
</div>

View File

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

View File

@@ -1,20 +1,19 @@
<%- include('header.ejs', {titleDual:{ top: 'Storm Prediction' , bottom: 'Center Outlook' }, hasTime: 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') %>
<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>

View File

@@ -12,5 +12,4 @@
<div class="temp high"></div>
</div>
</div>
</div>
<%- include('scroll.ejs') %>
</div>

View File

@@ -73,7 +73,10 @@
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
}
},
"cSpell.words": [
"hibyehihi"
]
},
"extensions": {
"recommendations": [