mirror of
https://github.com/netbymatt/ws4kp.git
synced 2026-04-18 17:49:31 -07:00
Compare commits
50 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
39e8879697 | ||
|
|
5e3b917023 | ||
|
|
a813ee19a7 | ||
|
|
e01edc6972 | ||
|
|
ab0249e6eb | ||
|
|
c4c85b3b7b | ||
|
|
e954033979 | ||
|
|
ba39af9126 | ||
|
|
a814fde5b5 | ||
|
|
3f5f78eddf | ||
|
|
dc64e4bd7f | ||
|
|
776148fa6b | ||
|
|
69c050eb8f | ||
|
|
a3e142dade | ||
|
|
28917489bb | ||
|
|
2365a4c0f7 | ||
|
|
8afef77ea5 | ||
|
|
8f70ee87c5 | ||
|
|
4e7429bfba | ||
|
|
c5ffe1542a | ||
|
|
5364855c58 | ||
|
|
18efd810bd | ||
|
|
68a6bae3a7 | ||
|
|
5f0f0d9000 | ||
|
|
9d9cf4b0f3 | ||
|
|
9e500143c0 | ||
|
|
71da682660 | ||
|
|
1b9a1dcb22 | ||
|
|
095761ee81 | ||
|
|
21e528aaa3 | ||
|
|
a92c632937 | ||
|
|
6073fd1733 | ||
|
|
5da8185633 | ||
|
|
cf5c818ee3 | ||
|
|
97cec114f6 | ||
|
|
7efd2e8db7 | ||
|
|
8c28f41d54 | ||
|
|
e9d603fbfc | ||
|
|
32aa43c5b1 | ||
|
|
dbc56f014a | ||
|
|
3161a03797 | ||
|
|
205fa77f51 | ||
|
|
28bb8f2e2a | ||
|
|
cf9a99a6ca | ||
|
|
a83afa71cd | ||
|
|
74f1abd6f8 | ||
|
|
1bd45bdeeb | ||
|
|
232061b4d8 | ||
|
|
10d10ffbfb | ||
|
|
25ac2059a6 |
2
LICENSE
2
LICENSE
@@ -1,6 +1,6 @@
|
|||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) 2020-2024 Matt Walsh
|
Copyright (c) 2020-2025 Matt Walsh
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
|||||||
140
README.md
140
README.md
@@ -6,15 +6,31 @@ A live version of this project is available at https://weatherstar.netbymatt.com
|
|||||||
|
|
||||||
This project aims to bring back the feel of the 90's with a weather forecast that has the look and feel of The Weather Channel at that time but available in a modern way. This is by no means intended to be a perfect emulation of the WeatherStar 4000, the hardware that produced those wonderful blue and orange graphics you saw during the local forecast on The Weather Channel. If you would like a much more accurate project please see the [WS4000 Simulator](http://www.taiganet.com/). Instead, this project intends to create a simple to use interface with minimal configuration fuss. Some changes have been made to the screens available because either more or less forecast information is available today than was in the 90's. Most of these changes are captured in sections below.
|
This project aims to bring back the feel of the 90's with a weather forecast that has the look and feel of The Weather Channel at that time but available in a modern way. This is by no means intended to be a perfect emulation of the WeatherStar 4000, the hardware that produced those wonderful blue and orange graphics you saw during the local forecast on The Weather Channel. If you would like a much more accurate project please see the [WS4000 Simulator](http://www.taiganet.com/). Instead, this project intends to create a simple to use interface with minimal configuration fuss. Some changes have been made to the screens available because either more or less forecast information is available today than was in the 90's. Most of these changes are captured in sections below.
|
||||||
|
|
||||||
## Acknowledgements
|
## What's your motivation
|
||||||
|
|
||||||
This project is based on the work of [Mike Battaglia](https://github.com/vbguyny/ws4kp). It was forked from his work in August 2020.
|
Nostalgia. And I enjoy following the weather, especially severe storms.
|
||||||
|
|
||||||
* Mike Battaglia for the original project and all of the code which draws the weather displays. This code remains largely intact and was a huge amount of work to get exactly right. He's also responsible for all of the background graphics including the maps used in the application.
|
It's also a creative outlet for me and keeps my programming skills honed for when I need them for my day job.
|
||||||
* The team at [TWCClassics](https://twcclassics.com/) for several resources.
|
|
||||||
* A [font](https://twcclassics.com/downloads.html) set used on the original WeatherStar 4000
|
### Included technology
|
||||||
* [Icon](https://twcclassics.com/downloads.html) sets
|
I've kept this open source, well commented, and made it as library-free as possible to help others interested in programming be able to jump right in and start working with the code.
|
||||||
* Countless photos and videos of WeatherStar 4000 forecasts used as references.
|
|
||||||
|
From a learning standpoint, this codebase make use of a lot of different methods and technologies common on the internet including:
|
||||||
|
|
||||||
|
* The https://api.weather.gov REST API. ([documentation](https://www.weather.gov/documentation/services-web-api)).
|
||||||
|
* ES 6 functionality
|
||||||
|
* Arrow functions
|
||||||
|
* Promises
|
||||||
|
* Async/await and parallel loading of all forecast resources
|
||||||
|
* Classes and extensions
|
||||||
|
* Javascript modules
|
||||||
|
* Separation between API code and user interface code
|
||||||
|
* Use of a modern date parsing library [luxon](https://moment.github.io/luxon/)
|
||||||
|
* Practical API rates and static asset caching
|
||||||
|
* Very straight-forward hand written HTML
|
||||||
|
* Build system integration (Gulp, Webpack) to reduce the number of scripts that need to be loaded
|
||||||
|
* Hand written CSS made easier to mange with SASS
|
||||||
|
* A linting library to keep code style consistent
|
||||||
|
|
||||||
## Does WeatherStar 4000+ work outside of the USA?
|
## Does WeatherStar 4000+ work outside of the USA?
|
||||||
|
|
||||||
@@ -24,14 +40,12 @@ If you would like to display weather information for international locations (ou
|
|||||||
- [`ws4kp-international`](https://github.com/mwood77/ws4kp-international)
|
- [`ws4kp-international`](https://github.com/mwood77/ws4kp-international)
|
||||||
|
|
||||||
## Run Your WeatherStar
|
## Run Your WeatherStar
|
||||||
There are a lot of CORS considerations and issues with api.weather.gov that are easiest to deal with by running a local server to see this in action (or use the live link above). You'll need Node.js >12.0 to run the local server.
|
|
||||||
|
|
||||||
To run via Node locally:
|
To run via Node locally:
|
||||||
```
|
```
|
||||||
git clone https://github.com/netbymatt/ws4kp.git
|
git clone https://github.com/netbymatt/ws4kp.git
|
||||||
cd ws4kp
|
cd ws4kp
|
||||||
npm i
|
npm i
|
||||||
node index.js
|
node index.mjs
|
||||||
```
|
```
|
||||||
|
|
||||||
To run via Docker:
|
To run via Docker:
|
||||||
@@ -40,35 +54,31 @@ docker run -p 8080:8080 ghcr.io/netbymatt/ws4kp
|
|||||||
```
|
```
|
||||||
Open your web browser: http://localhost:8080/
|
Open your web browser: http://localhost:8080/
|
||||||
|
|
||||||
## Updates in 5.0
|
To run via Docker Compose (docker-compose.yaml):
|
||||||
The change to 5.0 changes from drawing the weather graphics on canvas elements and instead uses HTML and CSS to style all of the weather graphics. A lot of other changes and fixes were implemented at the same time.
|
```
|
||||||
|
---
|
||||||
|
services:
|
||||||
|
ws4kp:
|
||||||
|
image: ghcr.io/netbymatt/ws4kp
|
||||||
|
container_name: ws4kp
|
||||||
|
environment:
|
||||||
|
# Each argument in the permalink URL can become an environment variable on the Docker host by adding WSQS_
|
||||||
|
# Following the "Sharing a Permalink" example below, here are a few environment variables defined. Visit that section for a
|
||||||
|
# more complete list of configuration options.
|
||||||
|
- WSQS_latLonQuery="Orlando International Airport Orlando FL USA"
|
||||||
|
- WSQS_hazards_checkbox=false
|
||||||
|
- WSQS_current_weather_checkbox=true
|
||||||
|
ports:
|
||||||
|
- 8080:8080 # change the first 8080 to meet your local network needs
|
||||||
|
restart: unless-stopped
|
||||||
|
```
|
||||||
|
|
||||||
* Replace all canvas elements with HTML and CSS
|
### Serving static files
|
||||||
* City and airport names are better parsed to fit the available space
|
The app can be served as a static set of files on any web server. Run the provided gulp task to create a set of static distribution files:
|
||||||
* Remove the dependency on libgif-js
|
```
|
||||||
* Use browser for text wrapping where necessary
|
npm run buildDist
|
||||||
* Some new weather icons
|
```
|
||||||
* Refresh only on slideshow repeat
|
The resulting files will be in the /dist folder in the root of the project. These can then be uploaded to a web server for hosting, no server-side scripting is required.
|
||||||
* Removed Almanac 30-day outlook
|
|
||||||
* Fixed startup issue when current conditions are unavailable
|
|
||||||
|
|
||||||
## Why the fork?
|
|
||||||
|
|
||||||
The fork is a result of wanting a more manageable, modern code base to work with. Part of it is an exercise in my education in JavaScript. There are several technical changes that were made behind the scenes.
|
|
||||||
|
|
||||||
* Make use of the new API available at https://api.weather.gov ([documentation](https://www.weather.gov/documentation/services-web-api)). This caused the removal of some of the original WeatherStar 4000 displays, and allowed for new displays to be created.
|
|
||||||
* Changed code to make extensive use of ES6 functionality including:
|
|
||||||
* Arrow functions
|
|
||||||
* Promises
|
|
||||||
* Async/await and parallel loading of all forecast resources
|
|
||||||
* Classes
|
|
||||||
* Common code base for each display through use of classes
|
|
||||||
* Separation between weather display code and user interface
|
|
||||||
* Use of a modern date parsing library [luxon](https://moment.github.io/luxon/)
|
|
||||||
* Attempt to remove the need for a local server to bypass [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) issues with the various APIs used. This is almost workable but there are still some minor CORS issues with https://api.weather.gov.
|
|
||||||
* The necessary CORS pass through URLs have been rewritten so they can be deployed on Node.js using the included server or through S3/Cloudfront in a serverless environment.
|
|
||||||
* Proper settings for static resource caching
|
|
||||||
* Build system integration to reduce the number of scripts that need to be loaded
|
|
||||||
|
|
||||||
## What's different
|
## What's different
|
||||||
|
|
||||||
@@ -77,40 +87,45 @@ I've made several changes to this Weather Star 4000 simulation compared to the o
|
|||||||
* Radar displays the timestamp of the image.
|
* 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 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)
|
* 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 "Local Forecast" and "Extended Forecast" provide several additional days of information compared to the original format in the 90's.
|
* The "Local Forecast" and "Extended Forecast" provide several additional days of information compared to the original format in the 90's.
|
||||||
* Narration was removed. In the original code narration made use of the computer's local text-to-speech engine which didn't sound great.
|
* The original music has been replaced. More info in [Music](#music).
|
||||||
* Music was removed. I don't want to deal with copyright issues and hosting MP3s. If you're looking for the music that played during forecasts please visit [TWCClassics](https://twcclassics.com/audio/).
|
* Marine forecast (tides) is not available as it is not reliably part of the new API.
|
||||||
* Marine forecast (tides) is not available as it is not part of the new API.
|
|
||||||
* The nearby cities displayed on screens such as "Latest Observations" and "Regional Forecast" are likely not the same as they were in the 90's. The weather monitoring equipment at these stations move over time for one reason or another, and coming up with a simple formulaic way of finding nearby stations is sufficient to give the same look-and-feel as the original.
|
|
||||||
* "Flavors" are not present in this simulation. Flavors refer to the order of the weather information that was shown on the original units. Instead, the order of the displays has been fixed and a checkboxes can be used to turn on and off individual displays. The travel forecast has been defaulted to off so only local information shows for new users.
|
* "Flavors" are not present in this simulation. Flavors refer to the order of the weather information that was shown on the original units. Instead, the order of the displays has been fixed and a checkboxes can be used to turn on and off individual displays. The travel forecast has been defaulted to off so only local information shows for new users.
|
||||||
|
|
||||||
## Sharing a permalink (bookmarking)
|
## Sharing a permalink (bookmarking)
|
||||||
Selected displays, the forecast city and widescreen setting are sticky from one session to the next. However if you would like to share your exact configuration or bookmark it click the "Copy Permalink" (or get "Get Parmalink") near the bottom of the page. A URL will be copied to your clipboard with all of you selected displays and location (or copy it from the page if your browser doesn't support clipboard transfers directly). You can then share this link or add it to your bookmarks.
|
Selected displays, the forecast city and widescreen setting are sticky from one session to the next. However if you would like to share your exact configuration or bookmark it, click the "Copy Permalink" (or get "Get Parmalink") near the bottom of the page. A URL will be copied to your clipboard with all of you selected displays and location (or copy it from the page if your browser doesn't support clipboard transfers directly). You can then share this link or add it to your bookmarks.
|
||||||
|
|
||||||
## Kiosk mode
|
Your permalink will be very long. Here is an example for the Orlando International Airport:
|
||||||
Kiosk mode can be activated by a checkbox on the page. Note that there is no way out of kiosk mode (except refresh or closing the browser), and the play/pause and other controls will not be available. This is deliberate as a browser's kiosk mode it intended not to be exited or significantly modified.
|
```
|
||||||
|
https://weatherstar.netbymatt.com/?hazards-checkbox=false¤t-weather-checkbox=true&latest-observations-checkbox=true&hourly-checkbox=false&hourly-graph-checkbox=true&travel-checkbox=false®ional-forecast-checkbox=true&local-forecast-checkbox=true&extended-forecast-checkbox=true&almanac-checkbox=false&spc-outlook-checkbox=true&radar-checkbox=true&settings-wide-checkbox=false&settings-kiosk-checkbox=false&settings-scanLines-checkbox=false&settings-speed-select=1.00&settings-units-select=us&latLonQuery=Orlando+International+Airport%2C+Orlando%2C+FL%2C+USA&latLon=%7B%22lat%22%3A28.431%2C%22lon%22%3A-81.3076%7D
|
||||||
|
```
|
||||||
|
You can also build your own permalink. Any omitted settings will be filled with defaults. Here are a few examples:
|
||||||
|
```
|
||||||
|
https://weatherstar.netbymatt.com/?latLonQuery=Orlando+International+Airport
|
||||||
|
https://weatherstar.netbymatt.com/?kiosk=true
|
||||||
|
https://weatherstar.netbymatt.com/?settings-units-select=metric
|
||||||
|
```
|
||||||
|
|
||||||
|
### Kiosk mode
|
||||||
|
Kiosk mode can be activated by a checkbox on the page. Note that there is no way out of kiosk mode (except refresh or closing the browser), and the play/pause and other controls will not be available. This is deliberate as a browser's kiosk mode it intended not to be exited or significantly modified. A separate full-screen icon is available in the tool bar to go full-screen on a laptop or mobile browser.
|
||||||
|
|
||||||
It's also possible to enter kiosk mode using a permalink. First generate a [Permalink](#sharing-a-permalink-bookmarking), then to the end of it add `&kiosk=true`. Opening this link will load all of the selected displays included in the Permalink, enter kiosk mode immediately upon loading and start playing the forecast.
|
It's also possible to enter kiosk mode using a permalink. First generate a [Permalink](#sharing-a-permalink-bookmarking), then to the end of it add `&kiosk=true`. Opening this link will load all of the selected displays included in the Permalink, enter kiosk mode immediately upon loading and start playing the forecast.
|
||||||
|
|
||||||
## Default query string paramaters (environment variables)
|
### Default query string paramaters (environment variables)
|
||||||
When serving this via the built-in Express server, it's possible to define environment variables that direct the user to a default set of paramaters (like the [Permalink](#sharing-a-permalink-bookmarking) above). If a user requests the root page at `http://localhost:8080/` the query string provided by environment variables will be appended to the url thus providing a default configuration.
|
When serving this via the built-in Express server, it's possible to define environment variables that direct the user to a default set of paramaters (like the [Permalink](#sharing-a-permalink-bookmarking) above). If a user requests the root page at `http://localhost:8080/` the query string provided by environment variables will be appended to the url thus providing a default configuration.
|
||||||
|
|
||||||
Environment variables can be added to the command line as usual, or via a .env file which is parsed with [dotenv](https://github.com/motdotla/dotenv). Both methods have the same effect.
|
Environment variables can be added to the command line as usual, or via a .env file which is parsed with [dotenv](https://github.com/motdotla/dotenv). Both methods have the same effect.
|
||||||
|
|
||||||
Environment variables that are to be added to the default query string are prefixed with `WSQS_` and then use the same key/value pairs generated by the [Permalink](#sharing-a-permalink-bookmarking) above, with the `- (dash)` character replaced by an `_ (underscore)`. For example, if you wanted to turn the travel forecast on, you would find `travel-checkbox=true` in the permalink, it's matching environment variable becomes `WSQS_travel_checkbox=true`.
|
Environment variables that are to be added to the default query string are prefixed with `WSQS_` and then use the same key/value pairs generated by the [Permalink](#sharing-a-permalink-bookmarking) above, with the `- (dash)` character replaced by an `_ (underscore)`. For example, if you wanted to turn the travel forecast on, you would find `travel-checkbox=true` in the permalink, it's matching environment variable becomes `WSQS_travel_checkbox=true`.
|
||||||
|
|
||||||
## Serving static files
|
|
||||||
The app can be served as a static set of files on any web server. Run the provided gulp task to create a set of static distribution files:
|
|
||||||
```
|
|
||||||
npm run buildDist
|
|
||||||
```
|
|
||||||
The resulting files will be in the /dist folder in the root of the project. These can then be uploaded to a web server for hosting, no server-side scripting is required.
|
|
||||||
|
|
||||||
## Music
|
## Music
|
||||||
The WeatherStar had wonderful background music from the smooth jazz and new age genres by artists of the time. Lists of the music that played are available by searching online, but it's all copyrighted music and would be difficult to provide as part of this repository.
|
The WeatherStar had wonderful background music from the smooth jazz and new age genres by artists of the time. Lists of the music that played are available by searching online, but it's all copyrighted music and would be difficult to provide as part of this repository.
|
||||||
|
|
||||||
I've used AI tools to create WeatherStar-inspired music tracks that are unencumbered by copyright and are included in this repo. Too keep the size down, I've only included 4 tracks. Additional tracks are in a companion repository [ws4kp-music](https://github.com/netbymatt/ws4kp-music).
|
I've used AI tools to create WeatherStar-inspired music tracks that are unencumbered by copyright and are included in this repo. Too keep the size down, I've only included 4 tracks. Additional tracks are in a companion repository [ws4kp-music](https://github.com/netbymatt/ws4kp-music).
|
||||||
|
|
||||||
|
If you're looking for the original music that played during forecasts [TWCClassics](https://twcclassics.com/audio/) has thurough documentation of playlists.
|
||||||
|
|
||||||
### Customizing the music
|
### Customizing the music
|
||||||
Placing .mp3 files in the `/server/music` folder will override the default music included in the repo. Subdirectories will not be scanned. When weatherstar loads in the browser it will load a list if available files and randomize the order when it starts playing. On each loop through the available tracks the order will again be shuffled. If you're using the static files method to host your WeatherStar music is located in `/music`.
|
Placing .mp3 files in the `/server/music` folder will override the default music included in the repo. Subdirectories will not be scanned. When weatherstar loads in the browser it will load a list if available files and randomize the order when it starts playing. On each loop through the available tracks the order will again be shuffled. If you're using the static files method to host your WeatherStar music is located in `/music`.
|
||||||
|
|
||||||
@@ -137,7 +152,9 @@ A hook is provided as `/server/scripts/custom.js` to allow customizations to you
|
|||||||
|
|
||||||
## Issue reporting and feature requests
|
## Issue reporting and feature requests
|
||||||
|
|
||||||
Please do not report issues with api.weather.gov being down. It's a new service and not considered fully operational yet. Before reporting an issue or requesting a feature please consider that this is not intended to be a perfect recreation of the WeatherStar 4000, it's a best effort that fits within what's available from the API and within a web browser.
|
Please do not report issues with api.weather.gov being down. It's a new service and not considered fully operational yet. I've also observed that the API can go down on a regional basis (based on NWS office locations). This means that you may have problems getting data for, say, Chicago right now, but Dallas and others are working just fine.
|
||||||
|
|
||||||
|
Before reporting an issue or requesting a feature please consider that this is not intended to be a perfect recreation of the WeatherStar 4000, it's a best effort that fits within what's available from the API and within a web browser.
|
||||||
|
|
||||||
Note: not all units are converted to metric, if selected. Some text-based products such as warnings are simple text strings provided from the national weather service and thus have baked-in units such as "gusts up to 60 mph." These values will not be converted.
|
Note: not all units are converted to metric, if selected. Some text-based products such as warnings are simple text strings provided from the national weather service and thus have baked-in units such as "gusts up to 60 mph." These values will not be converted.
|
||||||
|
|
||||||
@@ -145,6 +162,21 @@ Note: not all units are converted to metric, if selected. Some text-based produc
|
|||||||
|
|
||||||
Not retro enough? Try the [Weatherstar 3000+](https://github.com/netbymatt/ws3kp)
|
Not retro enough? Try the [Weatherstar 3000+](https://github.com/netbymatt/ws3kp)
|
||||||
|
|
||||||
|
## Use
|
||||||
|
|
||||||
|
Linking directly to the live web site at https://weatherstar.netbymatt.com is encouraged. As is using the live site for digital signage, home dashboards, streaming and public display. Please note the disclaimer below.
|
||||||
|
|
||||||
|
## Acknowledgements
|
||||||
|
|
||||||
|
This project is based on the work of [Mike Battaglia](https://github.com/vbguyny/ws4kp). It was forked from his work in August 2020.
|
||||||
|
|
||||||
|
* Mike Battaglia for the original project and all of the code which draws the weather displays. This code remains largely intact and was a huge amount of work to get exactly right. He's also responsible for all of the background graphics including the maps used in the application.
|
||||||
|
* The team at [TWCClassics](https://twcclassics.com/) for several resources.
|
||||||
|
* A [font](https://twcclassics.com/downloads.html) set used on the original WeatherStar 4000
|
||||||
|
* [Icon](https://twcclassics.com/downloads.html) sets
|
||||||
|
* Countless photos and videos of WeatherStar 4000 forecasts used as references.
|
||||||
|
* The growing list of contributors to this repository
|
||||||
|
|
||||||
## Disclaimer
|
## Disclaimer
|
||||||
|
|
||||||
This web site should NOT be used in life threatening weather situations, or be relied on to inform the public of such situations. The Internet is an unreliable network subject to server and network outages and by nature is not suitable for such mission critical use. If you require such access to NWS data, please consider one of their subscription services. The authors of this web site shall not be held liable in the event of injury, death or property damage that occur as a result of disregarding this warning.
|
This web site should NOT be used in life threatening weather situations, or be relied on to inform the public of such situations. The Internet is an unreliable network subject to server and network outages and by nature is not suitable for such mission critical use. If you require such access to NWS data, please consider one of their subscription services. The authors of this web site shall not be held liable in the event of injury, death or property damage that occur as a result of disregarding this warning.
|
||||||
|
|||||||
@@ -99,8 +99,11 @@ const server = app.listen(port, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// graceful shutdown
|
// graceful shutdown
|
||||||
process.on('SIGINT', () => {
|
const gracefulShutdown = () => {
|
||||||
server.close(() => {
|
server.close(() => {
|
||||||
console.log('Server closed');
|
console.log('Server closed');
|
||||||
});
|
});
|
||||||
});
|
};
|
||||||
|
|
||||||
|
process.on('SIGINT', gracefulShutdown);
|
||||||
|
process.on('SIGTERM', gracefulShutdown);
|
||||||
|
|||||||
1349
package-lock.json
generated
1349
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "ws4kp",
|
"name": "ws4kp",
|
||||||
"version": "5.21.0",
|
"version": "5.21.14",
|
||||||
"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",
|
||||||
|
|||||||
BIN
server/fonts/Star4000 Large.ttf
Normal file
BIN
server/fonts/Star4000 Large.ttf
Normal file
Binary file not shown.
BIN
server/images/icons/current-conditions/Smoke.gif
Normal file
BIN
server/images/icons/current-conditions/Smoke.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.8 KiB |
BIN
server/images/nav/ic_scanlines_off_white_24dp_2x.png
Normal file
BIN
server/images/nav/ic_scanlines_off_white_24dp_2x.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 893 B |
BIN
server/images/nav/ic_scanlines_on_white_24dp_2x.png
Normal file
BIN
server/images/nav/ic_scanlines_on_white_24dp_2x.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 367 B |
@@ -38,6 +38,7 @@ const init = () => {
|
|||||||
document.querySelector('#NavigateNext').addEventListener('click', btnNavigateNextClick);
|
document.querySelector('#NavigateNext').addEventListener('click', btnNavigateNextClick);
|
||||||
document.querySelector('#NavigatePrevious').addEventListener('click', btnNavigatePreviousClick);
|
document.querySelector('#NavigatePrevious').addEventListener('click', btnNavigatePreviousClick);
|
||||||
document.querySelector('#NavigatePlay').addEventListener('click', btnNavigatePlayClick);
|
document.querySelector('#NavigatePlay').addEventListener('click', btnNavigatePlayClick);
|
||||||
|
document.querySelector('#ToggleScanlines').addEventListener('click', btnNavigateToggleScanlines);
|
||||||
document.querySelector(TOGGLE_FULL_SCREEN_SELECTOR).addEventListener('click', btnFullScreenClick);
|
document.querySelector(TOGGLE_FULL_SCREEN_SELECTOR).addEventListener('click', btnFullScreenClick);
|
||||||
const btnGetGps = document.querySelector(BNT_GET_GPS_SELECTOR);
|
const btnGetGps = document.querySelector(BNT_GET_GPS_SELECTOR);
|
||||||
btnGetGps.addEventListener('click', btnGetGpsClick);
|
btnGetGps.addEventListener('click', btnGetGpsClick);
|
||||||
@@ -61,7 +62,7 @@ const init = () => {
|
|||||||
paramName: 'text',
|
paramName: 'text',
|
||||||
params: {
|
params: {
|
||||||
f: 'json',
|
f: 'json',
|
||||||
countryCode: 'USA', // 'USA,PRI,VIR,GUM,ASM',
|
countryCode: 'USA',
|
||||||
category,
|
category,
|
||||||
maxSuggestions: 10,
|
maxSuggestions: 10,
|
||||||
},
|
},
|
||||||
@@ -82,7 +83,6 @@ const init = () => {
|
|||||||
|
|
||||||
// attempt to parse the url parameters
|
// attempt to parse the url parameters
|
||||||
const parsedParameters = parseQueryString();
|
const parsedParameters = parseQueryString();
|
||||||
|
|
||||||
const loadFromParsed = parsedParameters.latLonQuery && parsedParameters.latLon;
|
const loadFromParsed = parsedParameters.latLonQuery && parsedParameters.latLon;
|
||||||
|
|
||||||
// Auto load the parsed parameters and fall back to the previous query
|
// Auto load the parsed parameters and fall back to the previous query
|
||||||
@@ -295,6 +295,8 @@ const updateFullScreenNavigate = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const documentKeydown = (e) => {
|
const documentKeydown = (e) => {
|
||||||
|
// don't trigger on ctrl/alt/shift modified key
|
||||||
|
if (e.altKey || e.ctrlKey || e.shiftKey) return false;
|
||||||
const { key } = e;
|
const { key } = e;
|
||||||
|
|
||||||
if (document.fullscreenElement || document.activeElement === document.body) {
|
if (document.fullscreenElement || document.activeElement === document.body) {
|
||||||
@@ -345,6 +347,11 @@ const btnNavigatePlayClick = () => {
|
|||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const btnNavigateToggleScanlines = () => {
|
||||||
|
settings.scanLines.value = !settings.scanLines.value;
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
// post a message to the iframe
|
// post a message to the iframe
|
||||||
const postMessage = (type, myMessage = {}) => {
|
const postMessage = (type, myMessage = {}) => {
|
||||||
navMessage({ type, message: myMessage });
|
navMessage({ type, message: myMessage });
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// display sun and moon data
|
// display sun and moon data
|
||||||
import { loadImg, preloadImg } from './utils/image.mjs';
|
import { preloadImg } from './utils/image.mjs';
|
||||||
import { DateTime } from '../vendor/auto/luxon.mjs';
|
import { DateTime } from '../vendor/auto/luxon.mjs';
|
||||||
import STATUS from './status.mjs';
|
import STATUS from './status.mjs';
|
||||||
import WeatherDisplay from './weatherdisplay.mjs';
|
import WeatherDisplay from './weatherdisplay.mjs';
|
||||||
@@ -9,9 +9,6 @@ class Almanac extends WeatherDisplay {
|
|||||||
constructor(navId, elemId) {
|
constructor(navId, elemId) {
|
||||||
super(navId, elemId, 'Almanac', true);
|
super(navId, elemId, 'Almanac', true);
|
||||||
|
|
||||||
// pre-load background images (returns promises)
|
|
||||||
this.backgroundImage0 = loadImg('images/backgrounds/1.png');
|
|
||||||
|
|
||||||
// preload the moon images
|
// preload the moon images
|
||||||
preloadImg(imageName('Full'));
|
preloadImg(imageName('Full'));
|
||||||
preloadImg(imageName('Last'));
|
preloadImg(imageName('Last'));
|
||||||
@@ -122,10 +119,10 @@ class Almanac extends WeatherDisplay {
|
|||||||
// sun and moon data
|
// sun and moon data
|
||||||
this.elem.querySelector('.day-1').innerHTML = Today.toLocaleString({ weekday: 'long' });
|
this.elem.querySelector('.day-1').innerHTML = Today.toLocaleString({ weekday: 'long' });
|
||||||
this.elem.querySelector('.day-2').innerHTML = Tomorrow.toLocaleString({ weekday: 'long' });
|
this.elem.querySelector('.day-2').innerHTML = Tomorrow.toLocaleString({ weekday: 'long' });
|
||||||
this.elem.querySelector('.rise-1').innerHTML = DateTime.fromJSDate(info.sun[0].sunrise).setZone(timeZone()).toLocaleString(DateTime.TIME_SIMPLE).toLowerCase();
|
this.elem.querySelector('.rise-1').innerHTML = timeFormat(DateTime.fromJSDate(info.sun[0].sunrise));
|
||||||
this.elem.querySelector('.rise-2').innerHTML = DateTime.fromJSDate(info.sun[1].sunrise).setZone(timeZone()).toLocaleString(DateTime.TIME_SIMPLE).toLowerCase();
|
this.elem.querySelector('.rise-2').innerHTML = timeFormat(DateTime.fromJSDate(info.sun[1].sunrise));
|
||||||
this.elem.querySelector('.set-1').innerHTML = DateTime.fromJSDate(info.sun[0].sunset).setZone(timeZone()).toLocaleString(DateTime.TIME_SIMPLE).toLowerCase();
|
this.elem.querySelector('.set-1').innerHTML = timeFormat(DateTime.fromJSDate(info.sun[0].sunset));
|
||||||
this.elem.querySelector('.set-2').innerHTML = DateTime.fromJSDate(info.sun[1].sunset).setZone(timeZone()).toLocaleString(DateTime.TIME_SIMPLE).toLowerCase();
|
this.elem.querySelector('.set-2').innerHTML = timeFormat(DateTime.fromJSDate(info.sun[1].sunset));
|
||||||
|
|
||||||
const days = info.moon.map((MoonPhase) => {
|
const days = info.moon.map((MoonPhase) => {
|
||||||
const fill = {};
|
const fill = {};
|
||||||
@@ -171,6 +168,8 @@ const imageName = (type) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const timeFormat = (dt) => dt.setZone(timeZone()).toLocaleString(DateTime.TIME_SIMPLE).toLowerCase();
|
||||||
|
|
||||||
// register display
|
// register display
|
||||||
const display = new Almanac(9, 'almanac');
|
const display = new Almanac(9, 'almanac');
|
||||||
registerDisplay(display);
|
registerDisplay(display);
|
||||||
|
|||||||
@@ -3,43 +3,24 @@ import { json } from './utils/fetch.mjs';
|
|||||||
|
|
||||||
const KEYS = {
|
const KEYS = {
|
||||||
ESC: 27,
|
ESC: 27,
|
||||||
TAB: 9,
|
|
||||||
RETURN: 13,
|
|
||||||
LEFT: 37,
|
|
||||||
UP: 38,
|
UP: 38,
|
||||||
RIGHT: 39,
|
|
||||||
DOWN: 40,
|
DOWN: 40,
|
||||||
ENTER: 13,
|
ENTER: 13,
|
||||||
};
|
};
|
||||||
|
|
||||||
const DEFAULT_OPTIONS = {
|
const DEFAULT_OPTIONS = {
|
||||||
autoSelectFirst: false,
|
|
||||||
serviceUrl: null,
|
serviceUrl: null,
|
||||||
lookup: null,
|
|
||||||
onSelect: () => { },
|
|
||||||
onHint: null,
|
|
||||||
width: 'auto',
|
|
||||||
minChars: 3,
|
minChars: 3,
|
||||||
maxHeight: 300,
|
maxHeight: 300,
|
||||||
deferRequestBy: 0,
|
deferRequestBy: 0,
|
||||||
params: {},
|
params: {},
|
||||||
delimiter: null,
|
|
||||||
zIndex: 9999,
|
zIndex: 9999,
|
||||||
type: 'GET',
|
type: 'GET',
|
||||||
noCache: false,
|
|
||||||
preserveInput: false,
|
|
||||||
containerClass: 'autocomplete-suggestions',
|
containerClass: 'autocomplete-suggestions',
|
||||||
tabDisabled: false,
|
|
||||||
dataType: 'text',
|
|
||||||
currentRequest: null,
|
|
||||||
triggerSelectOnValidInput: true,
|
|
||||||
preventBadQueries: true,
|
|
||||||
paramName: 'query',
|
paramName: 'query',
|
||||||
transformResult: (a) => a,
|
transformResult: (a) => a,
|
||||||
showNoSuggestionNotice: false,
|
showNoSuggestionNotice: false,
|
||||||
noSuggestionNotice: 'No results',
|
noSuggestionNotice: 'No results',
|
||||||
orientation: 'bottom',
|
|
||||||
forceFixPosition: false,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const escapeRegExChars = (string) => string.replace(/[/\-\\^$*+?.()|[\]{}]/g, '\\$&');
|
const escapeRegExChars = (string) => string.replace(/[/\-\\^$*+?.()|[\]{}]/g, '\\$&');
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// current weather conditions display
|
// current weather conditions display
|
||||||
import STATUS from './status.mjs';
|
import STATUS from './status.mjs';
|
||||||
import { loadImg, preloadImg } from './utils/image.mjs';
|
import { preloadImg } from './utils/image.mjs';
|
||||||
import { json } from './utils/fetch.mjs';
|
import { json } from './utils/fetch.mjs';
|
||||||
import { directionToNSEW } from './utils/calc.mjs';
|
import { directionToNSEW } from './utils/calc.mjs';
|
||||||
import { locationCleanup } from './utils/string.mjs';
|
import { locationCleanup } from './utils/string.mjs';
|
||||||
@@ -17,8 +17,6 @@ const skipStations = ['U', 'C', 'H', 'W', 'Y', 'T', 'S', 'M', 'O', 'L', 'A', 'F'
|
|||||||
class CurrentWeather extends WeatherDisplay {
|
class CurrentWeather extends WeatherDisplay {
|
||||||
constructor(navId, elemId) {
|
constructor(navId, elemId) {
|
||||||
super(navId, elemId, 'Current Conditions', true);
|
super(navId, elemId, 'Current Conditions', true);
|
||||||
// pre-load background image (returns promise)
|
|
||||||
this.backgroundImage = loadImg('images/backgrounds/1.png');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async getData(weatherParameters, refresh) {
|
async getData(weatherParameters, refresh) {
|
||||||
@@ -52,6 +50,8 @@ class CurrentWeather extends WeatherDisplay {
|
|||||||
stillWaiting: () => this.stillWaiting(),
|
stillWaiting: () => this.stillWaiting(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (observations.features.length === 0) throw new Error(`No features returned for station: ${station.properties.stationIdentifier}, trying next station`);
|
||||||
|
|
||||||
// test data quality
|
// test data quality
|
||||||
if (observations.features[0].properties.temperature.value === null
|
if (observations.features[0].properties.temperature.value === null
|
||||||
|| observations.features[0].properties.windSpeed.value === null
|
|| observations.features[0].properties.windSpeed.value === null
|
||||||
@@ -61,10 +61,11 @@ class CurrentWeather extends WeatherDisplay {
|
|||||||
|| observations.features[0].properties.dewpoint.value === null
|
|| observations.features[0].properties.dewpoint.value === null
|
||||||
|| observations.features[0].properties.barometricPressure.value === null) {
|
|| observations.features[0].properties.barometricPressure.value === null) {
|
||||||
observations = undefined;
|
observations = undefined;
|
||||||
throw new Error(`Unable to get observations: ${station.properties.stationIdentifier}, trying next station`);
|
throw new Error(`Incomplete data set for: ${station.properties.stationIdentifier}, trying next station`);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
|
observations = undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// test for data received
|
// test for data received
|
||||||
|
|||||||
@@ -59,11 +59,10 @@ class ExtendedForecast extends WeatherDisplay {
|
|||||||
date: Day.dayName,
|
date: Day.dayName,
|
||||||
};
|
};
|
||||||
|
|
||||||
const { low } = Day;
|
const { low, high } = Day;
|
||||||
if (low !== undefined) {
|
if (low !== undefined) {
|
||||||
fill['value-lo'] = Math.round(low);
|
fill['value-lo'] = Math.round(low);
|
||||||
}
|
}
|
||||||
const { high } = Day;
|
|
||||||
fill['value-hi'] = Math.round(high);
|
fill['value-hi'] = Math.round(high);
|
||||||
|
|
||||||
// return the filled template
|
// return the filled template
|
||||||
@@ -121,17 +120,18 @@ const parse = (fullForecast) => {
|
|||||||
return forecast;
|
return forecast;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const regexList = [
|
||||||
|
[/ and /gi, ' '],
|
||||||
|
[/slight /gi, ''],
|
||||||
|
[/chance /gi, ''],
|
||||||
|
[/very /gi, ''],
|
||||||
|
[/patchy /gi, ''],
|
||||||
|
[/Areas Of /gi, ''],
|
||||||
|
[/areas /gi, ''],
|
||||||
|
[/dense /gi, ''],
|
||||||
|
[/Thunderstorm/g, 'T\'Storm'],
|
||||||
|
];
|
||||||
const shortenExtendedForecastText = (long) => {
|
const shortenExtendedForecastText = (long) => {
|
||||||
const regexList = [
|
|
||||||
[/ and /gi, ' '],
|
|
||||||
[/slight /gi, ''],
|
|
||||||
[/chance /gi, ''],
|
|
||||||
[/very /gi, ''],
|
|
||||||
[/patchy /gi, ''],
|
|
||||||
[/areas /gi, ''],
|
|
||||||
[/dense /gi, ''],
|
|
||||||
[/Thunderstorm/g, 'T\'Storm'],
|
|
||||||
];
|
|
||||||
// run all regexes
|
// run all regexes
|
||||||
const short = regexList.reduce((working, [regex, replace]) => working.replace(regex, replace), long);
|
const short = regexList.reduce((working, [regex, replace]) => working.replace(regex, replace), long);
|
||||||
|
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ class Hazards extends WeatherDisplay {
|
|||||||
// show alert indicator
|
// show alert indicator
|
||||||
if (this.data.length > 0) alert.classList.add('show');
|
if (this.data.length > 0) alert.classList.add('show');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Get hourly forecast failed');
|
console.error('Get hazards failed');
|
||||||
console.error(error.status, error.responseJSON);
|
console.error(error.status, error.responseJSON);
|
||||||
if (this.isEnabled) this.setStatus(STATUS.failed);
|
if (this.isEnabled) this.setStatus(STATUS.failed);
|
||||||
// return undefined to other subscribers
|
// return undefined to other subscribers
|
||||||
@@ -129,7 +129,7 @@ class Hazards extends WeatherDisplay {
|
|||||||
// don't let offset go negative
|
// don't let offset go negative
|
||||||
if (offsetY < 0) offsetY = 0;
|
if (offsetY < 0) offsetY = 0;
|
||||||
|
|
||||||
// copy the scrolled portion of the canvas
|
// move the element
|
||||||
this.elem.querySelector('.main').scrollTo(0, offsetY);
|
this.elem.querySelector('.main').scrollTo(0, offsetY);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,10 @@ import WeatherDisplay from './weatherdisplay.mjs';
|
|||||||
import { registerDisplay, timeZone } from './navigation.mjs';
|
import { registerDisplay, timeZone } from './navigation.mjs';
|
||||||
import { DateTime } from '../vendor/auto/luxon.mjs';
|
import { DateTime } from '../vendor/auto/luxon.mjs';
|
||||||
|
|
||||||
|
// get available space
|
||||||
|
const availableWidth = 532;
|
||||||
|
const availableHeight = 285;
|
||||||
|
|
||||||
class HourlyGraph extends WeatherDisplay {
|
class HourlyGraph extends WeatherDisplay {
|
||||||
constructor(navId, elemId, defaultActive) {
|
constructor(navId, elemId, defaultActive) {
|
||||||
super(navId, elemId, 'Hourly Graph', defaultActive);
|
super(navId, elemId, 'Hourly Graph', defaultActive);
|
||||||
@@ -47,10 +51,6 @@ class HourlyGraph extends WeatherDisplay {
|
|||||||
drawCanvas() {
|
drawCanvas() {
|
||||||
if (!this.image) this.image = this.elem.querySelector('.chart img');
|
if (!this.image) this.image = this.elem.querySelector('.chart img');
|
||||||
|
|
||||||
// get available space
|
|
||||||
const availableWidth = 532;
|
|
||||||
const availableHeight = 285;
|
|
||||||
|
|
||||||
this.image.width = availableWidth;
|
this.image.width = availableWidth;
|
||||||
this.image.height = availableHeight;
|
this.image.height = availableHeight;
|
||||||
|
|
||||||
|
|||||||
@@ -69,8 +69,7 @@ class Hourly extends WeatherDisplay {
|
|||||||
const fillValues = {};
|
const fillValues = {};
|
||||||
// hour
|
// hour
|
||||||
const hour = startingHour.plus({ hours: index });
|
const hour = startingHour.plus({ hours: index });
|
||||||
const formattedHour = hour.toLocaleString({ weekday: 'short', hour: 'numeric' });
|
fillValues.hour = hour.toLocaleString({ weekday: 'short', hour: 'numeric' });
|
||||||
fillValues.hour = formattedHour;
|
|
||||||
|
|
||||||
// temperatures, convert to strings with no decimal
|
// temperatures, convert to strings with no decimal
|
||||||
const temperature = data.temperature.toString().padStart(3);
|
const temperature = data.temperature.toString().padStart(3);
|
||||||
@@ -81,12 +80,11 @@ class Hourly extends WeatherDisplay {
|
|||||||
fillValues.like = feelsLike;
|
fillValues.like = feelsLike;
|
||||||
|
|
||||||
// wind
|
// wind
|
||||||
let wind = 'Calm';
|
fillValues.wind = 'Calm';
|
||||||
if (data.windSpeed > 0) {
|
if (data.windSpeed > 0) {
|
||||||
const windSpeed = Math.round(data.windSpeed).toString();
|
const windSpeed = Math.round(data.windSpeed).toString();
|
||||||
wind = data.windDirection + (Array(6 - data.windDirection.length - windSpeed.length).join(' ')) + windSpeed;
|
fillValues.wind = data.windDirection + (Array(6 - data.windDirection.length - windSpeed.length).join(' ')) + windSpeed;
|
||||||
}
|
}
|
||||||
fillValues.wind = wind;
|
|
||||||
|
|
||||||
// image
|
// image
|
||||||
fillValues.icon = { type: 'img', src: data.icon };
|
fillValues.icon = { type: 'img', src: data.icon };
|
||||||
@@ -96,8 +94,7 @@ class Hourly extends WeatherDisplay {
|
|||||||
// alter the color of the feels like column to reflect wind chill or heat index
|
// alter the color of the feels like column to reflect wind chill or heat index
|
||||||
if (feelsLike < temperature) {
|
if (feelsLike < temperature) {
|
||||||
filledRow.querySelector('.like').classList.add('wind-chill');
|
filledRow.querySelector('.like').classList.add('wind-chill');
|
||||||
}
|
} else if (feelsLike > temperature) {
|
||||||
if (feelsLike > temperature) {
|
|
||||||
filledRow.querySelector('.like').classList.add('heat-index');
|
filledRow.querySelector('.like').classList.add('heat-index');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
const hourlyIcon = (skyCover, weather, iceAccumulation, probabilityOfPrecipitation, snowfallAmount, windSpeed, isNight = false) => {
|
// internal function to add path to returned icon
|
||||||
// internal function to add path to returned icon
|
const addPath = (icon) => `images/icons/regional-maps/${icon}`;
|
||||||
const addPath = (icon) => `images/icons/regional-maps/${icon}`;
|
|
||||||
|
|
||||||
|
const hourlyIcon = (skyCover, weather, iceAccumulation, probabilityOfPrecipitation, snowfallAmount, windSpeed, isNight = false) => {
|
||||||
// possible phenomenon
|
// possible phenomenon
|
||||||
let thunder = false;
|
let thunder = false;
|
||||||
let snow = false;
|
let snow = false;
|
||||||
|
|||||||
@@ -65,6 +65,10 @@ const largeIcon = (link, _isNightTime) => {
|
|||||||
case 'sleet-n':
|
case 'sleet-n':
|
||||||
return addPath('Sleet.gif');
|
return addPath('Sleet.gif');
|
||||||
|
|
||||||
|
case 'smoke':
|
||||||
|
case 'smoke-n':
|
||||||
|
return addPath('Smoke.gif');
|
||||||
|
|
||||||
case 'rain_showers':
|
case 'rain_showers':
|
||||||
case 'rain_showers_high':
|
case 'rain_showers_high':
|
||||||
case 'rain_showers-n':
|
case 'rain_showers-n':
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ const smallIcon = (link, _isNightTime) => {
|
|||||||
case 'sct-n':
|
case 'sct-n':
|
||||||
case 'nsct':
|
case 'nsct':
|
||||||
case 'nsct-n':
|
case 'nsct-n':
|
||||||
|
case 'haze-n':
|
||||||
return addPath('Partly-Cloudy-Night.gif');
|
return addPath('Partly-Cloudy-Night.gif');
|
||||||
|
|
||||||
case 'ovc':
|
case 'ovc':
|
||||||
|
|||||||
@@ -22,8 +22,7 @@ class LatestObservations extends WeatherDisplay {
|
|||||||
// this is intentional because up to 30 stations are available to pull data from
|
// this is intentional because up to 30 stations are available to pull data from
|
||||||
|
|
||||||
// calculate distance to each station
|
// calculate distance to each station
|
||||||
const stationsByDistance = Object.keys(StationInfo).map((key) => {
|
const stationsByDistance = Object.values(StationInfo).map((station) => {
|
||||||
const station = StationInfo[key];
|
|
||||||
const distance = calcDistance(station.lat, station.lon, this.weatherParameters.latitude, this.weatherParameters.longitude);
|
const distance = calcDistance(station.lat, station.lon, this.weatherParameters.latitude, this.weatherParameters.longitude);
|
||||||
return { ...station, distance };
|
return { ...station, distance };
|
||||||
});
|
});
|
||||||
@@ -104,8 +103,6 @@ class LatestObservations extends WeatherDisplay {
|
|||||||
linesContainer.innerHTML = '';
|
linesContainer.innerHTML = '';
|
||||||
linesContainer.append(...lines);
|
linesContainer.append(...lines);
|
||||||
|
|
||||||
// update temperature unit header
|
|
||||||
|
|
||||||
this.finishDraw();
|
this.finishDraw();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -79,6 +79,7 @@ const startMedia = async () => {
|
|||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
await player.play();
|
await player.play();
|
||||||
|
setTrackName(playlist.availableFiles[currentTrack]);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// report the error
|
// report the error
|
||||||
console.error('Couldn\'t play music');
|
console.error('Couldn\'t play music');
|
||||||
@@ -86,6 +87,7 @@ const startMedia = async () => {
|
|||||||
// set state back to not playing for good UI experience
|
// set state back to not playing for good UI experience
|
||||||
mediaPlaying.value = false;
|
mediaPlaying.value = false;
|
||||||
stateChanged();
|
stateChanged();
|
||||||
|
setTrackName('Not playing');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -93,6 +95,7 @@ const startMedia = async () => {
|
|||||||
const stopMedia = () => {
|
const stopMedia = () => {
|
||||||
if (!player) return;
|
if (!player) return;
|
||||||
player.pause();
|
player.pause();
|
||||||
|
setTrackName('Not playing');
|
||||||
};
|
};
|
||||||
|
|
||||||
const stateChanged = () => {
|
const stateChanged = () => {
|
||||||
@@ -140,6 +143,7 @@ const initializePlayer = () => {
|
|||||||
|
|
||||||
// get the first file
|
// get the first file
|
||||||
player.src = `music/${playlist.availableFiles[currentTrack]}`;
|
player.src = `music/${playlist.availableFiles[currentTrack]}`;
|
||||||
|
setTrackName(playlist.availableFiles[currentTrack]);
|
||||||
player.type = 'audio/mpeg';
|
player.type = 'audio/mpeg';
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -160,6 +164,12 @@ const playerEnded = () => {
|
|||||||
}
|
}
|
||||||
// update the player source
|
// update the player source
|
||||||
player.src = `music/${playlist.availableFiles[currentTrack]}`;
|
player.src = `music/${playlist.availableFiles[currentTrack]}`;
|
||||||
|
setTrackName(playlist.availableFiles[currentTrack]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const setTrackName = (fileName) => {
|
||||||
|
const trackName = fileName.replace(/\.mp3/gi, '').replace(/(_-)/gi, '');
|
||||||
|
document.getElementById('musicTrack').innerHTML = trackName;
|
||||||
};
|
};
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
|||||||
@@ -253,9 +253,9 @@ const resize = () => {
|
|||||||
|
|
||||||
const scale = Math.min(widthZoomPercent, heightZoomPercent);
|
const scale = Math.min(widthZoomPercent, heightZoomPercent);
|
||||||
if (scale < 1.0 || document.fullscreenElement || settings.kiosk) {
|
if (scale < 1.0 || document.fullscreenElement || settings.kiosk) {
|
||||||
document.querySelector('#container').style.transform = `scale(${scale})`;
|
document.querySelector('#container').style.zoom = scale;
|
||||||
} else {
|
} else {
|
||||||
document.querySelector('#container').style.transform = 'unset';
|
document.querySelector('#container').style.zoom = 'unset';
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -266,6 +266,7 @@ const resetStatuses = () => {
|
|||||||
|
|
||||||
// allow displays to register themselves
|
// allow displays to register themselves
|
||||||
const registerDisplay = (display) => {
|
const registerDisplay = (display) => {
|
||||||
|
if (displays[display.navId]) console.warn(`Display nav ID ${display.navId} already in use`);
|
||||||
displays[display.navId] = display;
|
displays[display.navId] = display;
|
||||||
|
|
||||||
// generate checkboxes
|
// generate checkboxes
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
// regional forecast and observations
|
// regional forecast and observations
|
||||||
import { loadImg } from './utils/image.mjs';
|
|
||||||
import STATUS, { calcStatusClass, statusClasses } from './status.mjs';
|
import STATUS, { calcStatusClass, statusClasses } from './status.mjs';
|
||||||
import WeatherDisplay from './weatherdisplay.mjs';
|
import WeatherDisplay from './weatherdisplay.mjs';
|
||||||
import {
|
import {
|
||||||
@@ -10,9 +9,6 @@ class Progress extends WeatherDisplay {
|
|||||||
constructor(navId, elemId) {
|
constructor(navId, elemId) {
|
||||||
super(navId, elemId, '', false);
|
super(navId, elemId, '', false);
|
||||||
|
|
||||||
// pre-load background image (returns promise)
|
|
||||||
this.backgroundImage = loadImg('images/backgrounds/1.png');
|
|
||||||
|
|
||||||
// disable any navigation timing
|
// disable any navigation timing
|
||||||
this.timing = false;
|
this.timing = false;
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ const baseMapImages = new Promise((resolve) => {
|
|||||||
fetchAsBlob('/images/maps/radar.webp').then((blob) => {
|
fetchAsBlob('/images/maps/radar.webp').then((blob) => {
|
||||||
createImageBitmap(blob).then((imageBitmap) => {
|
createImageBitmap(blob).then((imageBitmap) => {
|
||||||
// extract the black pixels to overlay on to the final image (boundaries)
|
// extract the black pixels to overlay on to the final image (boundaries)
|
||||||
console.time('radar-overlay');
|
|
||||||
const canvas = new OffscreenCanvas(imageBitmap.width, imageBitmap.height);
|
const canvas = new OffscreenCanvas(imageBitmap.width, imageBitmap.height);
|
||||||
const context = canvas.getContext('2d');
|
const context = canvas.getContext('2d');
|
||||||
context.drawImage(imageBitmap, 0, 0);
|
context.drawImage(imageBitmap, 0, 0);
|
||||||
@@ -28,7 +27,6 @@ const baseMapImages = new Promise((resolve) => {
|
|||||||
// write the image data back
|
// write the image data back
|
||||||
context.putImageData(imageData, 0, 0);
|
context.putImageData(imageData, 0, 0);
|
||||||
|
|
||||||
console.timeEnd('radar-overlay');
|
|
||||||
resolve({
|
resolve({
|
||||||
fullMap: imageBitmap,
|
fullMap: imageBitmap,
|
||||||
overlay: canvas,
|
overlay: canvas,
|
||||||
|
|||||||
@@ -6,10 +6,27 @@ 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';
|
||||||
|
|
||||||
|
// 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
|
||||||
|
// any subsequent narrowing of the user-agent.
|
||||||
|
const isIos = /iP(ad|od|hone)/i.test(window.navigator.userAgent);
|
||||||
|
// NOTE: iMessages/Messages preview is provided by an Apple scraper that uses a
|
||||||
|
// user-agent similar to: `Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_1)
|
||||||
|
// AppleWebKit/601.2.4 (KHTML, like Gecko) Version/9.0.1 Safari/601.2.4
|
||||||
|
// facebookexternalhit/1.1 Facebot Twitterbot/1.0`. There is currently a bug in
|
||||||
|
// Messages macos/ios where a constantly crashing website seems to cause an
|
||||||
|
// entire Messages thread to permanently lockup until the individual website
|
||||||
|
// preview is deleted! Messages ios will judder but allows the message to be
|
||||||
|
// deleted eventually. Messages macos beachballs forever and prevents the
|
||||||
|
// successful deletion. See
|
||||||
|
// https://github.com/netbymatt/ws4kp/issues/74#issuecomment-2921154962 for more
|
||||||
|
// context.
|
||||||
|
const isBot = /twitterbot|Facebot/i.test(window.navigator.userAgent);
|
||||||
|
|
||||||
const RADAR_HOST = 'mesonet.agron.iastate.edu';
|
const RADAR_HOST = 'mesonet.agron.iastate.edu';
|
||||||
class Radar extends WeatherDisplay {
|
class Radar extends WeatherDisplay {
|
||||||
constructor(navId, elemId) {
|
constructor(navId, elemId) {
|
||||||
super(navId, elemId, 'Local Radar', true);
|
super(navId, elemId, 'Local Radar', !isIos && !isBot);
|
||||||
|
|
||||||
this.okToDrawCurrentConditions = false;
|
this.okToDrawCurrentConditions = false;
|
||||||
this.okToDrawCurrentDateTime = false;
|
this.okToDrawCurrentDateTime = false;
|
||||||
@@ -39,9 +56,6 @@ class Radar extends WeatherDisplay {
|
|||||||
{ time: 1, si: 4 },
|
{ time: 1, si: 4 },
|
||||||
{ time: 12, si: 5 },
|
{ time: 12, si: 5 },
|
||||||
];
|
];
|
||||||
|
|
||||||
// get some web workers started
|
|
||||||
this.workers = (new Array(this.dopplerRadarImageMax)).fill(null).map(() => radarWorker());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async getData(weatherParameters, refresh) {
|
async getData(weatherParameters, refresh) {
|
||||||
@@ -53,6 +67,12 @@ class Radar extends WeatherDisplay {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// get the workers started
|
||||||
|
if (!this.workers) {
|
||||||
|
// get some web workers started
|
||||||
|
this.workers = (new Array(this.dopplerRadarImageMax)).fill(null).map(() => radarWorker());
|
||||||
|
}
|
||||||
|
|
||||||
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';
|
||||||
const baseUrls = [];
|
const baseUrls = [];
|
||||||
@@ -100,10 +120,8 @@ class Radar extends WeatherDisplay {
|
|||||||
const urls = sortedPngs.slice(-(this.dopplerRadarImageMax));
|
const urls = sortedPngs.slice(-(this.dopplerRadarImageMax));
|
||||||
|
|
||||||
// calculate offsets and sizes
|
// calculate offsets and sizes
|
||||||
let offsetX = 120;
|
const offsetX = 120 * 2;
|
||||||
let offsetY = 69;
|
const offsetY = 69 * 2;
|
||||||
offsetX *= 2;
|
|
||||||
offsetY *= 2;
|
|
||||||
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);
|
||||||
|
|
||||||
@@ -201,4 +219,7 @@ const radarWorker = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// register display
|
// register display
|
||||||
registerDisplay(new Radar(11, 'radar'));
|
// TEMPORARY: except on IOS and bots
|
||||||
|
if (!isIos && !isBot) {
|
||||||
|
registerDisplay(new Radar(11, 'radar'));
|
||||||
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ const buildForecast = (forecast, city, cityXY) => {
|
|||||||
const getRegionalObservation = async (point, city) => {
|
const getRegionalObservation = async (point, city) => {
|
||||||
try {
|
try {
|
||||||
// get stations
|
// get stations
|
||||||
const stations = await json(`https://api.weather.gov/gridpoints/${point.wfo}/${point.x},${point.y}/stations`);
|
const stations = await json(`https://api.weather.gov/gridpoints/${point.wfo}/${point.x},${point.y}/stations?limit=1`);
|
||||||
|
|
||||||
// get the first station
|
// get the first station
|
||||||
const station = stations.features[0].id;
|
const station = stations.features[0].id;
|
||||||
|
|||||||
@@ -7,12 +7,18 @@ import { json } from './utils/fetch.mjs';
|
|||||||
import { temperature as temperatureUnit } from './utils/units.mjs';
|
import { temperature as temperatureUnit } from './utils/units.mjs';
|
||||||
import { getSmallIcon } from './icons.mjs';
|
import { getSmallIcon } from './icons.mjs';
|
||||||
import { preloadImg } from './utils/image.mjs';
|
import { preloadImg } from './utils/image.mjs';
|
||||||
import { DateTime } from '../vendor/auto/luxon.mjs';
|
import { DateTime, Interval } from '../vendor/auto/luxon.mjs';
|
||||||
import WeatherDisplay from './weatherdisplay.mjs';
|
import WeatherDisplay from './weatherdisplay.mjs';
|
||||||
import { registerDisplay } from './navigation.mjs';
|
import { registerDisplay } from './navigation.mjs';
|
||||||
import * as utils from './regionalforecast-utils.mjs';
|
import * as utils from './regionalforecast-utils.mjs';
|
||||||
import { getPoint } from './utils/weather.mjs';
|
import { getPoint } from './utils/weather.mjs';
|
||||||
|
|
||||||
|
// map offset
|
||||||
|
const mapOffsetXY = {
|
||||||
|
x: 240,
|
||||||
|
y: 117,
|
||||||
|
};
|
||||||
|
|
||||||
class RegionalForecast extends WeatherDisplay {
|
class RegionalForecast extends WeatherDisplay {
|
||||||
constructor(navId, elemId) {
|
constructor(navId, elemId) {
|
||||||
super(navId, elemId, 'Regional Forecast', true);
|
super(navId, elemId, 'Regional Forecast', true);
|
||||||
@@ -36,23 +42,18 @@ class RegionalForecast extends WeatherDisplay {
|
|||||||
}
|
}
|
||||||
this.elem.querySelector('.map img').src = baseMap;
|
this.elem.querySelector('.map img').src = baseMap;
|
||||||
|
|
||||||
// map offset
|
|
||||||
const offsetXY = {
|
|
||||||
x: 240,
|
|
||||||
y: 117,
|
|
||||||
};
|
|
||||||
// get user's location in x/y
|
// get user's location in x/y
|
||||||
const sourceXY = utils.getXYFromLatitudeLongitude(this.weatherParameters.latitude, this.weatherParameters.longitude, offsetXY.x, offsetXY.y, weatherParameters.state);
|
const sourceXY = utils.getXYFromLatitudeLongitude(this.weatherParameters.latitude, this.weatherParameters.longitude, mapOffsetXY.x, mapOffsetXY.y, weatherParameters.state);
|
||||||
|
|
||||||
// get latitude and longitude limits
|
// get latitude and longitude limits
|
||||||
const minMaxLatLon = utils.getMinMaxLatitudeLongitude(sourceXY.x, sourceXY.y, offsetXY.x, offsetXY.y, this.weatherParameters.state);
|
const minMaxLatLon = utils.getMinMaxLatitudeLongitude(sourceXY.x, sourceXY.y, mapOffsetXY.x, mapOffsetXY.y, this.weatherParameters.state);
|
||||||
|
|
||||||
// get a target distance
|
// get a target distance
|
||||||
let targetDistance = 2.5;
|
let targetDistance = 2.5;
|
||||||
if (this.weatherParameters.state === 'HI') targetDistance = 1;
|
if (this.weatherParameters.state === 'HI') targetDistance = 1;
|
||||||
|
|
||||||
// make station info into an array
|
// make station info into an array
|
||||||
const stationInfoArray = Object.values(StationInfo).map((value) => ({ ...value, targetDistance }));
|
const stationInfoArray = Object.values(StationInfo).map((station) => ({ ...station, targetDistance }));
|
||||||
// combine regional cities with station info for additional stations
|
// combine regional cities with station info for additional stations
|
||||||
// stations are intentionally after cities to allow cities priority when drawing the map
|
// stations are intentionally after cities to allow cities priority when drawing the map
|
||||||
const combinedCities = [...RegionalCities, ...stationInfoArray];
|
const combinedCities = [...RegionalCities, ...stationInfoArray];
|
||||||
@@ -76,6 +77,9 @@ class RegionalForecast extends WeatherDisplay {
|
|||||||
// get a unit converter
|
// get a unit converter
|
||||||
const temperatureConverter = temperatureUnit();
|
const temperatureConverter = temperatureUnit();
|
||||||
|
|
||||||
|
// get now as DateTime for calculations below
|
||||||
|
const now = DateTime.now();
|
||||||
|
|
||||||
// get regional forecasts and observations (the two are intertwined due to the design of api.weather.gov)
|
// get regional forecasts and observations (the two are intertwined due to the design of api.weather.gov)
|
||||||
const regionalDataAll = await Promise.all(regionalCities.map(async (city) => {
|
const regionalDataAll = await Promise.all(regionalCities.map(async (city) => {
|
||||||
try {
|
try {
|
||||||
@@ -109,14 +113,24 @@ class RegionalForecast extends WeatherDisplay {
|
|||||||
preloadImg(getSmallIcon(regionalObservation.icon, !regionalObservation.daytime));
|
preloadImg(getSmallIcon(regionalObservation.icon, !regionalObservation.daytime));
|
||||||
|
|
||||||
// return a pared-down forecast
|
// return a pared-down forecast
|
||||||
// 0th object is the current conditions
|
// 0th object should contain the current conditions, but when WFOs go offline or otherwise don't post
|
||||||
// first object is the next period i.e. if it's daytime then it's the "tonight" forecast
|
// an updated forecast it's possible that the 0th object is in the past.
|
||||||
// second object is the following period
|
// so we go on a search for the current time in the start/end times provided in the forecast periods
|
||||||
// always skip the first forecast index because it's what's going on right now
|
const { periods } = forecast.properties;
|
||||||
|
const currentPeriod = periods.reduce((prev, period, index) => {
|
||||||
|
const start = DateTime.fromISO(period.startTime);
|
||||||
|
const end = DateTime.fromISO(period.endTime);
|
||||||
|
const interval = Interval.fromDateTimes(start, end);
|
||||||
|
if (interval.contains(now)) {
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
return prev;
|
||||||
|
}, 0);
|
||||||
|
// group together the current observation and next two periods
|
||||||
return [
|
return [
|
||||||
regionalObservation,
|
regionalObservation,
|
||||||
utils.buildForecast(forecast.properties.periods[1], city, cityXY),
|
utils.buildForecast(forecast.properties.periods[currentPeriod + 1], city, cityXY),
|
||||||
utils.buildForecast(forecast.properties.periods[2], city, cityXY),
|
utils.buildForecast(forecast.properties.periods[currentPeriod + 2], city, cityXY),
|
||||||
];
|
];
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(`No regional forecast data for '${city.name ?? city.city}'`);
|
console.log(`No regional forecast data for '${city.name ?? city.city}'`);
|
||||||
@@ -137,7 +151,7 @@ class RegionalForecast extends WeatherDisplay {
|
|||||||
// return the weather data and offsets
|
// return the weather data and offsets
|
||||||
this.data = {
|
this.data = {
|
||||||
regionalData,
|
regionalData,
|
||||||
offsetXY,
|
mapOffsetXY,
|
||||||
sourceXY,
|
sourceXY,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -147,7 +161,7 @@ class RegionalForecast extends WeatherDisplay {
|
|||||||
drawCanvas() {
|
drawCanvas() {
|
||||||
super.drawCanvas();
|
super.drawCanvas();
|
||||||
// break up data into useful values
|
// break up data into useful values
|
||||||
const { regionalData: data, sourceXY, offsetXY } = this.data;
|
const { regionalData: data, sourceXY } = this.data;
|
||||||
|
|
||||||
// draw the header graphics
|
// draw the header graphics
|
||||||
|
|
||||||
@@ -170,7 +184,7 @@ class RegionalForecast extends WeatherDisplay {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// draw the map
|
// draw the map
|
||||||
const scale = 640 / (offsetXY.x * 2);
|
const scale = 640 / (mapOffsetXY.x * 2);
|
||||||
const map = this.elem.querySelector('.map');
|
const map = this.elem.querySelector('.map');
|
||||||
map.style.transform = `scale(${scale}) translate(-${sourceXY.x}px, -${sourceXY.y}px)`;
|
map.style.transform = `scale(${scale}) translate(-${sourceXY.x}px, -${sourceXY.y}px)`;
|
||||||
|
|
||||||
|
|||||||
@@ -33,6 +33,12 @@ const init = () => {
|
|||||||
[1.5, 'Very Slow'],
|
[1.5, 'Very Slow'],
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
settings.scanLines = new Setting('scanLines', {
|
||||||
|
name: 'Scan Lines',
|
||||||
|
defaultValue: false,
|
||||||
|
changeAction: scanLineChange,
|
||||||
|
sticky: true,
|
||||||
|
});
|
||||||
settings.units = new Setting('units', {
|
settings.units = new Setting('units', {
|
||||||
name: 'Units',
|
name: 'Units',
|
||||||
type: 'select',
|
type: 'select',
|
||||||
@@ -85,6 +91,18 @@ const kioskChange = (value) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const scanLineChange = (value) => {
|
||||||
|
const container = document.getElementById('container');
|
||||||
|
const navIcons = document.getElementById('ToggleScanlines');
|
||||||
|
if (value) {
|
||||||
|
container.classList.add('scanlines');
|
||||||
|
navIcons.classList.add('on');
|
||||||
|
} else {
|
||||||
|
container.classList.remove('scanlines');
|
||||||
|
navIcons.classList.remove('on');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const unitChange = () => {
|
const unitChange = () => {
|
||||||
// reload the data at the top level to refresh units
|
// reload the data at the top level to refresh units
|
||||||
// after the initial load
|
// after the initial load
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { elemForEach } from './utils/elem.mjs';
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', () => init());
|
document.addEventListener('DOMContentLoaded', () => init());
|
||||||
|
|
||||||
// shorthand mappings for frequently used values
|
// shorthand mappings for frequently used values
|
||||||
@@ -19,21 +21,18 @@ const init = () => {
|
|||||||
const createLink = async (e) => {
|
const createLink = async (e) => {
|
||||||
// cancel default event (click on hyperlink)
|
// cancel default event (click on hyperlink)
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
// get all checkboxes on page
|
|
||||||
const checkboxes = document.querySelectorAll('input[type=checkbox]');
|
|
||||||
|
|
||||||
// list to receive checkbox statuses
|
// list to receive checkbox statuses
|
||||||
const queryStringElements = {};
|
const queryStringElements = {};
|
||||||
|
|
||||||
[...checkboxes].forEach((elem) => {
|
elemForEach('input[type=checkbox]', (elem) => {
|
||||||
if (elem?.id) {
|
if (elem?.id) {
|
||||||
queryStringElements[elem.id] = elem?.checked ?? false;
|
queryStringElements[elem.id] = elem?.checked ?? false;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// get all select boxes
|
// get all select boxes
|
||||||
const selects = document.querySelectorAll('select');
|
elemForEach('select', (elem) => {
|
||||||
[...selects].forEach((elem) => {
|
|
||||||
if (elem?.id) {
|
if (elem?.id) {
|
||||||
queryStringElements[elem.id] = elem?.value ?? 0;
|
queryStringElements[elem.id] = elem?.value ?? 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,11 @@ const text = (url, params) => fetchAsync(url, 'text', params);
|
|||||||
const blob = (url, params) => fetchAsync(url, 'blob', params);
|
const blob = (url, params) => fetchAsync(url, 'blob', params);
|
||||||
|
|
||||||
const fetchAsync = async (_url, responseType, _params = {}) => {
|
const fetchAsync = async (_url, responseType, _params = {}) => {
|
||||||
|
// add user agent header to json request at api.weather.gov
|
||||||
|
const headers = {};
|
||||||
|
if (_url.toString().match(/api\.weather\.gov/)) {
|
||||||
|
headers['user-agent'] = 'Weatherstar 4000+; weatherstar@netbymatt.com';
|
||||||
|
}
|
||||||
// combine default and provided parameters
|
// combine default and provided parameters
|
||||||
const params = {
|
const params = {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
@@ -12,6 +17,7 @@ const fetchAsync = async (_url, responseType, _params = {}) => {
|
|||||||
type: 'GET',
|
type: 'GET',
|
||||||
retryCount: 0,
|
retryCount: 0,
|
||||||
..._params,
|
..._params,
|
||||||
|
headers,
|
||||||
};
|
};
|
||||||
// store original number of retries
|
// store original number of retries
|
||||||
params.originalRetries = params.retryCount;
|
params.originalRetries = params.retryCount;
|
||||||
|
|||||||
@@ -1,21 +1,4 @@
|
|||||||
import { blob } from './fetch.mjs';
|
import { blob } from './fetch.mjs';
|
||||||
import { rewriteUrl } from './cors.mjs';
|
|
||||||
|
|
||||||
// ****************************** load images *********************************
|
|
||||||
// load an image from a blob or url
|
|
||||||
const loadImg = (imgData, cors = false) => new Promise((resolve) => {
|
|
||||||
const img = new Image();
|
|
||||||
img.onload = (e) => {
|
|
||||||
resolve(e.target);
|
|
||||||
};
|
|
||||||
if (imgData instanceof Blob) {
|
|
||||||
img.src = window.URL.createObjectURL(imgData);
|
|
||||||
} else {
|
|
||||||
let url = imgData;
|
|
||||||
if (cors) url = rewriteUrl(imgData);
|
|
||||||
img.src = url;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// preload an image
|
// preload an image
|
||||||
// the goal is to get it in the browser's cache so it is available more quickly when the browser needs it
|
// the goal is to get it in the browser's cache so it is available more quickly when the browser needs it
|
||||||
@@ -28,15 +11,7 @@ const preloadImg = (src) => {
|
|||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
const loadImgElement = (url) => new Promise((resolve, reject) => {
|
|
||||||
const image = new Image();
|
|
||||||
image.onload = () => resolve(image);
|
|
||||||
image.onerror = reject;
|
|
||||||
image.src = url;
|
|
||||||
});
|
|
||||||
|
|
||||||
export {
|
export {
|
||||||
loadImg,
|
// eslint-disable-next-line import/prefer-default-export
|
||||||
preloadImg,
|
preloadImg,
|
||||||
loadImgElement,
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -189,7 +189,7 @@ class Setting {
|
|||||||
break;
|
break;
|
||||||
case 'checkbox':
|
case 'checkbox':
|
||||||
default:
|
default:
|
||||||
this.element.checked = newValue;
|
this.element.querySelector('input').checked = newValue;
|
||||||
}
|
}
|
||||||
this.storeToLocalStorage(this.myValue);
|
this.storeToLocalStorage(this.myValue);
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { json } from './fetch.mjs';
|
|||||||
|
|
||||||
const getPoint = async (lat, lon) => {
|
const getPoint = async (lat, lon) => {
|
||||||
try {
|
try {
|
||||||
return await json(`https://api.weather.gov/points/${lat},${lon}`);
|
return await json(`https://api.weather.gov/points/${lat.toFixed(4)},${lon.toFixed(4)}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(`Unable to get point ${lat}, ${lon}`);
|
console.log(`Unable to get point ${lat}, ${lon}`);
|
||||||
console.error(error);
|
console.error(error);
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
} from './navigation.mjs';
|
} from './navigation.mjs';
|
||||||
import { parseQueryString } from './share.mjs';
|
import { parseQueryString } from './share.mjs';
|
||||||
import settings from './settings.mjs';
|
import settings from './settings.mjs';
|
||||||
|
import { elemForEach } from './utils/elem.mjs';
|
||||||
|
|
||||||
class WeatherDisplay {
|
class WeatherDisplay {
|
||||||
constructor(navId, elemId, name, defaultEnabled) {
|
constructor(navId, elemId, name, defaultEnabled) {
|
||||||
@@ -391,8 +392,7 @@ class WeatherDisplay {
|
|||||||
this.templates = {};
|
this.templates = {};
|
||||||
this.elem = document.querySelector(`#${this.elemId}-html`);
|
this.elem = document.querySelector(`#${this.elemId}-html`);
|
||||||
if (!this.elem) return;
|
if (!this.elem) return;
|
||||||
const templates = this.elem.querySelectorAll('.template');
|
elemForEach(`#${this.elemId}-html .template`, (template) => {
|
||||||
templates.forEach((template) => {
|
|
||||||
const className = template.classList[0];
|
const className = template.classList[0];
|
||||||
const node = template.cloneNode(true);
|
const node = template.cloneNode(true);
|
||||||
node.classList.remove('template');
|
node.classList.remove('template');
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -23,6 +23,8 @@ body {
|
|||||||
|
|
||||||
&.kiosk {
|
&.kiosk {
|
||||||
margin: 0px;
|
margin: 0px;
|
||||||
|
overflow: hidden;
|
||||||
|
width: 100vw;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -141,6 +143,10 @@ body {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.kiosk #divTwc {
|
||||||
|
max-width: unset;
|
||||||
|
}
|
||||||
|
|
||||||
#divTwcLeft {
|
#divTwcLeft {
|
||||||
display: none;
|
display: none;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
@@ -189,11 +195,33 @@ body {
|
|||||||
#divTwcBottom>div {
|
#divTwcBottom>div {
|
||||||
padding-left: 6px;
|
padding-left: 6px;
|
||||||
padding-right: 6px;
|
padding-right: 6px;
|
||||||
|
|
||||||
|
// scale down the buttons on narrower screens
|
||||||
|
@media (max-width: 550px) {
|
||||||
|
zoom: 0.90;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 500px) {
|
||||||
|
zoom: 0.80;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 450px) {
|
||||||
|
zoom: 0.70;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 400px) {
|
||||||
|
zoom: 0.60;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 350px) {
|
||||||
|
zoom: 0.50;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#divTwcBottomLeft {
|
#divTwcBottomLeft {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#divTwcBottomMiddle {
|
#divTwcBottomMiddle {
|
||||||
@@ -260,7 +288,7 @@ body {
|
|||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Star4000 Large';
|
font-family: 'Star4000 Large';
|
||||||
src: url('../fonts/Star4000 Large.woff') format('woff');
|
src: url('../fonts/Star4000 Large.ttf') format('truetype');
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -300,10 +328,6 @@ body {
|
|||||||
transform-origin: unset;
|
transform-origin: unset;
|
||||||
}
|
}
|
||||||
|
|
||||||
.kiosk #divTwc #container {
|
|
||||||
transform-origin: 0 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#loading {
|
#loading {
|
||||||
width: 640px;
|
width: 640px;
|
||||||
height: 480px;
|
height: 480px;
|
||||||
@@ -404,10 +428,6 @@ body {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.kiosk #divTwc {
|
|
||||||
justify-content: unset;
|
|
||||||
}
|
|
||||||
|
|
||||||
#divTwc:fullscreen #display,
|
#divTwc:fullscreen #display,
|
||||||
.kiosk #divTwc #display {
|
.kiosk #divTwc #display {
|
||||||
position: relative;
|
position: relative;
|
||||||
@@ -436,6 +456,30 @@ body {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#ToggleScanlines {
|
||||||
|
display: inline-block;
|
||||||
|
|
||||||
|
.on {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.off {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
&.on {
|
||||||
|
.on {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.off {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.visible {
|
.visible {
|
||||||
visibility: visible;
|
visibility: visible;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
@@ -716,7 +760,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#share-link-copied {
|
#share-link-copied {
|
||||||
color: c.$title-color;
|
color: hsl(60, 100%, 30%);
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -728,6 +772,7 @@ body {
|
|||||||
|
|
||||||
#divQuery,
|
#divQuery,
|
||||||
>.info,
|
>.info,
|
||||||
|
>.related-links,
|
||||||
>.heading,
|
>.heading,
|
||||||
#enabledDisplays,
|
#enabledDisplays,
|
||||||
#settings,
|
#settings,
|
||||||
|
|||||||
@@ -13,4 +13,5 @@
|
|||||||
@use 'almanac';
|
@use 'almanac';
|
||||||
@use 'hazards';
|
@use 'hazards';
|
||||||
@use 'media';
|
@use 'media';
|
||||||
@use 'spc-outlook';
|
@use 'spc-outlook';
|
||||||
|
@use 'shared/scanlines';
|
||||||
106
server/styles/scss/shared/_scanlines.scss
Normal file
106
server/styles/scss/shared/_scanlines.scss
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
/* REGULAR SCANLINES SETTINGS */
|
||||||
|
|
||||||
|
// width of 1 scanline (min.: 1px)
|
||||||
|
$scan-width: 1px;
|
||||||
|
|
||||||
|
// emulates a damage-your-eyes bad pre-2000 CRT screen ♥ (true, false)
|
||||||
|
$scan-crt: false;
|
||||||
|
|
||||||
|
// frames-per-second (should be > 1), only applies if $scan-crt: true;
|
||||||
|
$scan-fps: 20;
|
||||||
|
|
||||||
|
// scanline-color (rgba)
|
||||||
|
$scan-color: rgba(#000, .3);
|
||||||
|
|
||||||
|
// set z-index on 8, like in ♥ 8-bits ♥, or…
|
||||||
|
// set z-index on 2147483648 or more to enable scanlines on Chrome fullscreen (doesn't work in Firefox or IE);
|
||||||
|
$scan-z-index: 2147483648;
|
||||||
|
|
||||||
|
/* MOVING SCANLINE SETTINGS */
|
||||||
|
|
||||||
|
// moving scanline (true, false)
|
||||||
|
$scan-moving-line: true;
|
||||||
|
|
||||||
|
// opacity of the moving scanline
|
||||||
|
$scan-opacity: .75;
|
||||||
|
|
||||||
|
/* MIXINS */
|
||||||
|
|
||||||
|
// apply CRT animation: @include scan-crt($scan-crt);
|
||||||
|
@mixin scan-crt($scan-crt) {
|
||||||
|
@if $scan-crt==true {
|
||||||
|
animation: scanlines 1s steps($scan-fps) infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@else {
|
||||||
|
animation: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// apply CRT animation: @include scan-crt($scan-crt);
|
||||||
|
@mixin scan-moving($scan-moving-line) {
|
||||||
|
@if $scan-moving-line==true {
|
||||||
|
animation: scanline 6s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@else {
|
||||||
|
animation: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* CSS .scanlines CLASS */
|
||||||
|
|
||||||
|
.scanlines {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden; // only to animate the unique scanline
|
||||||
|
|
||||||
|
&:before,
|
||||||
|
&:after {
|
||||||
|
display: block;
|
||||||
|
pointer-events: none;
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
// unique scanline travelling on the screen
|
||||||
|
&:before {
|
||||||
|
// position: absolute;
|
||||||
|
// bottom: 100%;
|
||||||
|
width: 100%;
|
||||||
|
height: $scan-width * 1;
|
||||||
|
z-index: $scan-z-index + 1;
|
||||||
|
background: $scan-color;
|
||||||
|
opacity: $scan-opacity;
|
||||||
|
// animation: scanline 6s linear infinite;
|
||||||
|
@include scan-moving($scan-moving-line);
|
||||||
|
}
|
||||||
|
|
||||||
|
// the scanlines, so!
|
||||||
|
&:after {
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
z-index: $scan-z-index;
|
||||||
|
background: linear-gradient(to bottom,
|
||||||
|
transparent 50%,
|
||||||
|
$scan-color 51%);
|
||||||
|
background-size: 100% $scan-width*2;
|
||||||
|
@include scan-crt($scan-crt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ANIMATE UNIQUE SCANLINE */
|
||||||
|
@keyframes scanline {
|
||||||
|
0% {
|
||||||
|
transform: translate3d(0, 200000%, 0);
|
||||||
|
// bottom: 0%; // to have a continuous scanline move, use this line (here in 0% step) instead of transform and write, in &:before, { position: absolute; bottom: 100%; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes scanlines {
|
||||||
|
0% {
|
||||||
|
background-position: 0 50%;
|
||||||
|
// bottom: 0%; // to have a continuous scanline move, use this line (here in 0% step) instead of transform and write, in &:before, { position: absolute; bottom: 100%; }
|
||||||
|
}
|
||||||
|
}
|
||||||
58
tests/package-lock.json
generated
58
tests/package-lock.json
generated
@@ -37,16 +37,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@puppeteer/browsers": {
|
"node_modules/@puppeteer/browsers": {
|
||||||
"version": "2.10.4",
|
"version": "2.10.5",
|
||||||
"resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.10.4.tgz",
|
"resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.10.5.tgz",
|
||||||
"integrity": "sha512-9DxbZx+XGMNdjBynIs4BRSz+M3iRDeB7qRcAr6UORFLphCIM2x3DXgOucvADiifcqCE4XePFUKcnaAMyGbrDlQ==",
|
"integrity": "sha512-eifa0o+i8dERnngJwKrfp3dEq7ia5XFyoqB17S4gK8GhsQE4/P8nxOfQSE0zQHxzzLo/cmF+7+ywEQ7wK7Fb+w==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"debug": "^4.4.0",
|
"debug": "^4.4.1",
|
||||||
"extract-zip": "^2.0.1",
|
"extract-zip": "^2.0.1",
|
||||||
"progress": "^2.0.3",
|
"progress": "^2.0.3",
|
||||||
"proxy-agent": "^6.5.0",
|
"proxy-agent": "^6.5.0",
|
||||||
"semver": "^7.7.1",
|
"semver": "^7.7.2",
|
||||||
"tar-fs": "^3.0.8",
|
"tar-fs": "^3.0.8",
|
||||||
"yargs": "^17.7.2"
|
"yargs": "^17.7.2"
|
||||||
},
|
},
|
||||||
@@ -64,9 +64,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
"version": "22.15.18",
|
"version": "22.15.29",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.18.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.29.tgz",
|
||||||
"integrity": "sha512-v1DKRfUdyW+jJhZNEI1PYy29S2YRxMV5AOO/x/SjKmW0acCIOqmbj6Haf9eHAhsPmrhlHSxEhv/1WszcLWV4cg==",
|
"integrity": "sha512-LNdjOkUDlU1RZb8e1kOIUpN1qQUlzGkEtbVNo53vbrwDg5om6oduhm4SiUaPW5ASTXhAiP0jInWG8Qx9fVlOeQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -363,9 +363,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/devtools-protocol": {
|
"node_modules/devtools-protocol": {
|
||||||
"version": "0.0.1439962",
|
"version": "0.0.1452169",
|
||||||
"resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1439962.tgz",
|
"resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1452169.tgz",
|
||||||
"integrity": "sha512-jJF48UdryzKiWhJ1bLKr7BFWUQCEIT5uCNbDLqkQJBtkFxYzILJH44WN0PDKMIlGDN7Utb8vyUY85C3w4R/t2g==",
|
"integrity": "sha512-FOFDVMGrAUNp0dDKsAU1TorWJUx2JOU1k9xdgBKKJF3IBh/Uhl2yswG5r3TEAOrCiGY2QRp1e6LVDQrCsTKO4g==",
|
||||||
"license": "BSD-3-Clause"
|
"license": "BSD-3-Clause"
|
||||||
},
|
},
|
||||||
"node_modules/emoji-regex": {
|
"node_modules/emoji-regex": {
|
||||||
@@ -799,17 +799,17 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/puppeteer": {
|
"node_modules/puppeteer": {
|
||||||
"version": "24.8.2",
|
"version": "24.10.0",
|
||||||
"resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-24.8.2.tgz",
|
"resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-24.10.0.tgz",
|
||||||
"integrity": "sha512-Sn6SBPwJ6ASFvQ7knQkR+yG7pcmr4LfXzmoVp3NR0xXyBbPhJa8a8ybtb6fnw1g/DD/2t34//yirubVczko37w==",
|
"integrity": "sha512-Oua9VkGpj0S2psYu5e6mCer6W9AU9POEQh22wRgSXnLXASGH+MwLUVWgLCLeP9QPHHcJ7tySUlg4Sa9OJmaLpw==",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@puppeteer/browsers": "2.10.4",
|
"@puppeteer/browsers": "2.10.5",
|
||||||
"chromium-bidi": "5.1.0",
|
"chromium-bidi": "5.1.0",
|
||||||
"cosmiconfig": "^9.0.0",
|
"cosmiconfig": "^9.0.0",
|
||||||
"devtools-protocol": "0.0.1439962",
|
"devtools-protocol": "0.0.1452169",
|
||||||
"puppeteer-core": "24.8.2",
|
"puppeteer-core": "24.10.0",
|
||||||
"typed-query-selector": "^2.12.0"
|
"typed-query-selector": "^2.12.0"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
@@ -820,15 +820,15 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/puppeteer-core": {
|
"node_modules/puppeteer-core": {
|
||||||
"version": "24.8.2",
|
"version": "24.10.0",
|
||||||
"resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.8.2.tgz",
|
"resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.10.0.tgz",
|
||||||
"integrity": "sha512-wNw5cRZOHiFibWc0vdYCYO92QuKTbJ8frXiUfOq/UGJWMqhPoBThTKkV+dJ99YyWfzJ2CfQQ4T1nhhR0h8FlVw==",
|
"integrity": "sha512-xX0QJRc8t19iAwRDsAOR38Q/Zx/W6WVzJCEhKCAwp2XMsaWqfNtQ+rBfQW9PlF+Op24d7c8Zlgq9YNmbnA7hdQ==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@puppeteer/browsers": "2.10.4",
|
"@puppeteer/browsers": "2.10.5",
|
||||||
"chromium-bidi": "5.1.0",
|
"chromium-bidi": "5.1.0",
|
||||||
"debug": "^4.4.0",
|
"debug": "^4.4.1",
|
||||||
"devtools-protocol": "0.0.1439962",
|
"devtools-protocol": "0.0.1452169",
|
||||||
"typed-query-selector": "^2.12.0",
|
"typed-query-selector": "^2.12.0",
|
||||||
"ws": "^8.18.2"
|
"ws": "^8.18.2"
|
||||||
},
|
},
|
||||||
@@ -960,9 +960,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/tar-fs": {
|
"node_modules/tar-fs": {
|
||||||
"version": "3.0.8",
|
"version": "3.0.9",
|
||||||
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.9.tgz",
|
||||||
"integrity": "sha512-ZoROL70jptorGAlgAYiLoBLItEKw/fUxg9BSYK/dF/GAGYFJOJJJMvjPAKDJraCXFwadD456FCuvLWgfhMsPwg==",
|
"integrity": "sha512-XF4w9Xp+ZQgifKakjZYmFdkLoSWd34VGKcsTCwlNWM7QG3ZbaxnTsaBwnjFZqHRf/rROxaR8rXnbtwdvaDI+lA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"pump": "^3.0.0",
|
"pump": "^3.0.0",
|
||||||
@@ -1103,9 +1103,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/zod": {
|
"node_modules/zod": {
|
||||||
"version": "3.24.4",
|
"version": "3.25.49",
|
||||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.24.4.tgz",
|
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.49.tgz",
|
||||||
"integrity": "sha512-OdqJE9UDRPwWsrHjLN2F8bPxvwJBK22EHLWtanu0LSYr5YqzsaaW3RMgmjwr8Rypg5k+meEJdSPXJZXE/yqOMg==",
|
"integrity": "sha512-JMMPMy9ZBk3XFEdbM3iL1brx4NUSejd6xr3ELrrGEfGb355gjhiAWtG3K5o+AViV/3ZfkIrCzXsZn6SbLwTR8Q==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/colinhacks"
|
"url": "https://github.com/sponsors/colinhacks"
|
||||||
|
|||||||
@@ -18,10 +18,10 @@
|
|||||||
<meta property="og:image" content="https://weatherstar.netbymatt.com/images/social/1200x600.png">
|
<meta property="og:image" content="https://weatherstar.netbymatt.com/images/social/1200x600.png">
|
||||||
<meta property="og:image:width" content="1200">
|
<meta property="og:image:width" content="1200">
|
||||||
<meta property="og:image:height" content="627">
|
<meta property="og:image:height" content="627">
|
||||||
<link rel="preload" href="fonts/Star4000.woff" as="font" type="font/woff" crossorigin>
|
<link rel="prefetch" href="fonts/Star4000.woff" as="font" type="font/woff" crossorigin>
|
||||||
<link rel="preload" href="fonts/Star4000 Extended.woff" as="font" type="font/woff" crossorigin>
|
<link rel="prefetch" href="fonts/Star4000 Extended.woff" as="font" type="font/woff" crossorigin>
|
||||||
<link rel="preload" href="fonts/Star4000 Large.woff" as="font" type="font/woff" crossorigin>
|
<link rel="prefetch" href="fonts/Star4000 Large.ttf" as="font" type="font/ttf" crossorigin>
|
||||||
<link rel="preload" href="fonts/Star4000 Small.woff" as="font" type="font/woff" crossorigin>
|
<link rel="prefetch" href="fonts/Star4000 Small.woff" as="font" type="font/woff" crossorigin>
|
||||||
|
|
||||||
<% if (production) { %>
|
<% if (production) { %>
|
||||||
<link rel="stylesheet" type="text/css" href="resources/ws.min.css?_=<%=production%>" />
|
<link rel="stylesheet" type="text/css" href="resources/ws.min.css?_=<%=production%>" />
|
||||||
@@ -145,6 +145,10 @@
|
|||||||
<img class="navButton off" src="images/nav/ic_volume_off_white_24dp_2x.png" title="Unmute" />
|
<img class="navButton off" src="images/nav/ic_volume_off_white_24dp_2x.png" title="Unmute" />
|
||||||
<img class="navButton on" src="images/nav/ic_volume_on_white_24dp_2x.png" title="Mute" />
|
<img class="navButton on" src="images/nav/ic_volume_on_white_24dp_2x.png" title="Mute" />
|
||||||
</div>
|
</div>
|
||||||
|
<div id="ToggleScanlines">
|
||||||
|
<img class="navButton off" src="images/nav/ic_scanlines_off_white_24dp_2x.png" title="Scan lines on" />
|
||||||
|
<img class="navButton on" src="images/nav/ic_scanlines_on_white_24dp_2x.png" title="Scan lines off" />
|
||||||
|
</div>
|
||||||
<img id="ToggleFullScreen" class="navButton" src="images/nav/ic_fullscreen_white_24dp_2x.png" title="Enter Fullscreen" />
|
<img id="ToggleFullScreen" class="navButton" src="images/nav/ic_fullscreen_white_24dp_2x.png" title="Enter Fullscreen" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -182,6 +186,7 @@
|
|||||||
Station Id: <span id="spanStationId"></span><br />
|
Station Id: <span id="spanStationId"></span><br />
|
||||||
Radar Id: <span id="spanRadarId"></span><br />
|
Radar Id: <span id="spanRadarId"></span><br />
|
||||||
Zone Id: <span id="spanZoneId"></span><br />
|
Zone Id: <span id="spanZoneId"></span><br />
|
||||||
|
Music: <span id="musicTrack">Not playing</span><br />
|
||||||
Ws4kp Version: <span><%- version %></span>
|
Ws4kp Version: <span><%- version %></span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -35,7 +35,6 @@
|
|||||||
<div class="scroll-area">
|
<div class="scroll-area">
|
||||||
<div class="frame template">
|
<div class="frame template">
|
||||||
<div class="map">
|
<div class="map">
|
||||||
<!-- <img src="images/maps/radar.webp" /> -->
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user