mirror of
https://github.com/netbymatt/ws4kp.git
synced 2026-04-14 15:49:31 -07:00
Compare commits
58 Commits
personal-w
...
42f1f66117
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
42f1f66117 | ||
|
|
d4f648f244 | ||
|
|
71d52c0b72 | ||
|
|
d2bf8f3f99 | ||
|
|
f4289e6329 | ||
|
|
11c54391b2 | ||
|
|
0b47cf79c1 | ||
|
|
ba36904477 | ||
|
|
dae5b20bc6 | ||
|
|
ccc936d81a | ||
|
|
5dc214c6a5 | ||
|
|
ec1169e07b | ||
|
|
eee4519095 | ||
|
|
38cdb46c85 | ||
|
|
e70639d7a6 | ||
|
|
63d27d1a26 | ||
|
|
97ac0a1656 | ||
|
|
8158afd039 | ||
|
|
5fffc495ae | ||
|
|
b2a424a64f | ||
|
|
9f6b90919c | ||
|
|
778b7f4456 | ||
|
|
443114f555 | ||
|
|
2a4dc03cf7 | ||
|
|
8c13128005 | ||
|
|
942fa8b817 | ||
|
|
15b68eba2f | ||
|
|
933a289d03 | ||
|
|
e6121327ce | ||
|
|
678f04fe42 | ||
|
|
77592a08a3 | ||
|
|
dadfcb8a5c | ||
|
|
245e9daf9c | ||
|
|
177012317b | ||
|
|
7bd21bcf1d | ||
|
|
ec65025ae2 | ||
|
|
194e108037 | ||
|
|
d5b7c6630a | ||
|
|
39bafae394 | ||
|
|
ec8fffbb64 | ||
|
|
0a794eae36 | ||
|
|
8c83736aba | ||
|
|
872162080d | ||
|
|
69d2b0f40b | ||
|
|
37193112a7 | ||
|
|
0d9c445919 | ||
|
|
6c9fb4cf68 | ||
|
|
59b10ae222 | ||
|
|
d18b13821a | ||
|
|
320d3139c3 | ||
|
|
34dedb44c1 | ||
|
|
18633708f9 | ||
|
|
9b12255e0a | ||
|
|
f3360772c8 | ||
|
|
767bb8f11d | ||
|
|
7586dd7489 | ||
|
|
f37cbd66f7 | ||
|
|
d00262ebbc |
11
.github/ISSUE_TEMPLATE/screen_enhance.md
vendored
Normal file
11
.github/ISSUE_TEMPLATE/screen_enhance.md
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
---
|
||||
name: Screen Enhancement
|
||||
about: Items and tasks related to the screen enhancement project
|
||||
title: '[Project]: '
|
||||
labels: screen-enhance
|
||||
projects: ['netbymatt/5']
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
Describe the task, how it affects the overall project and what is considered complete.
|
||||
2
.github/workflows/build-docker.yaml
vendored
2
.github/workflows/build-docker.yaml
vendored
@@ -4,6 +4,8 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- '**'
|
||||
- '!screen-enhance'
|
||||
- '!screen-enhance/**'
|
||||
tags:
|
||||
- 'v*.*.*'
|
||||
- 'v*.*'
|
||||
|
||||
4
.vscode/settings.json
vendored
4
.vscode/settings.json
vendored
@@ -2,7 +2,7 @@
|
||||
"liveSassCompile.settings.formats": [
|
||||
{
|
||||
"format": "compressed",
|
||||
"extensionName": ".css",
|
||||
"extensionName": ".min.css",
|
||||
"savePath": "/server/styles",
|
||||
}
|
||||
],
|
||||
@@ -17,4 +17,4 @@
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": "explicit"
|
||||
},
|
||||
}
|
||||
}
|
||||
14
README.md
14
README.md
@@ -1,3 +1,5 @@
|
||||

|
||||
|
||||
# WeatherStar 4000+
|
||||
|
||||
A live version of this project is available at https://weatherstar.netbymatt.com
|
||||
@@ -32,7 +34,7 @@ From a learning standpoint, this codebase make use of a lot of different methods
|
||||
* Hand written CSS made easier to mange with SASS
|
||||
* A linting library to keep code style consistent
|
||||
|
||||
## Quck Start
|
||||
## Quick Start
|
||||
|
||||
Ensure you have Node installed.
|
||||
```bash
|
||||
@@ -200,7 +202,9 @@ 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.
|
||||
Kiosk mode can be activated by a checkbox on the page. This will start Weatherstar in a fullscreen-like view without the play/volume/etc toolbar and scaled to fill the entire space. This does not activate the browser's fullscreen or kiosk mode. Those can only be activated by user interaction or by launching the browser with specific parameters such as `--start-fullscreen` or `--kiosk`.
|
||||
|
||||
When using kiosk mode (via the checkbox), there will be no way to exit the fullscreen-like view of weatherstar. Reloading the page should remove the kiosk checkbox and return you to the normal view. 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.
|
||||
|
||||
@@ -332,8 +336,10 @@ When using Docker:
|
||||
* **Static deployment**: Mount your `custom.js` file to `/usr/share/nginx/html/scripts/custom.js`
|
||||
* **Server deployment**: Mount your `custom.js` file to `/app/server/scripts/custom.js`
|
||||
|
||||
### RSS feeds and custom scroll
|
||||
If you would like your Weatherstar to have custom scrolling text in the bottom blue bar, or show headlines from an rss feed turn on the setting for `Enable RSS Feed/Text` and then enter a URL or text in the resulting text box. Then press set.
|
||||
### Custom text scroll
|
||||
If you would like your Weatherstar to have custom scrolling text in the bottom blue bar, turn on the setting for `Enable RSS Feed/Text` and then enter text in the resulting text box. Then press set.
|
||||
|
||||
Tip: You can have Weatherstar select randomly between several text strings on each pass through the current conditions. Use a pipe character to separate string. `Welcome to Weatherstar|Thanks for watching`.
|
||||
|
||||
## Issue reporting and feature requests
|
||||
|
||||
|
||||
@@ -729,6 +729,16 @@
|
||||
"wfo": "LMK"
|
||||
}
|
||||
},
|
||||
{
|
||||
"city": "Lubbock",
|
||||
"lat": 33.5836,
|
||||
"lon": -101.8549,
|
||||
"point": {
|
||||
"x": 49,
|
||||
"y": 34,
|
||||
"wfo": "LUB"
|
||||
}
|
||||
},
|
||||
{
|
||||
"city": "Manchester",
|
||||
"lat": 42.9956,
|
||||
|
||||
@@ -364,6 +364,11 @@
|
||||
"lat": 38.2542,
|
||||
"lon": -85.7594
|
||||
},
|
||||
{
|
||||
"city": "Lubbock",
|
||||
"lat": 33.5836,
|
||||
"lon": -101.8549
|
||||
},
|
||||
{
|
||||
"city": "Manchester",
|
||||
"lat": 42.9956,
|
||||
|
||||
@@ -8,13 +8,11 @@ import states from './stations-states.mjs';
|
||||
import chunk from './chunk.mjs';
|
||||
import overrides from './stations-overrides.mjs';
|
||||
import postProcessor from './stations-postprocessor.mjs';
|
||||
import { stationFilter } from '../server/scripts/modules/utils/string.mjs';
|
||||
|
||||
// check for cached flag
|
||||
const USE_CACHE = process.argv.includes('--use-cache');
|
||||
|
||||
// skip stations starting with these letters
|
||||
const skipStations = ['U', 'C', 'H', 'W', 'Y', 'T', 'S', 'M', 'O', 'L', 'A', 'F', 'B', 'N', 'V', 'R', 'D', 'E', 'I', 'G', 'J'];
|
||||
|
||||
// chunk the list of states
|
||||
const chunkStates = chunk(states, 3);
|
||||
|
||||
@@ -41,10 +39,8 @@ if (!USE_CACHE) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const stationsRaw = await https(next);
|
||||
stations = JSON.parse(stationsRaw);
|
||||
// filter stations for 4 letter identifiers
|
||||
const stationsFiltered4 = stations.features.filter((station) => station.properties.stationIdentifier.match(/^[A-Z]{4}$/));
|
||||
// filter against starting letter
|
||||
const stationsFiltered = stationsFiltered4.filter((station) => !skipStations.includes(station.properties.stationIdentifier.slice(0, 1)));
|
||||
const stationsFiltered = stations.filter(stationFilter);
|
||||
// add each resulting station to the output
|
||||
stationsFiltered.forEach((station) => {
|
||||
const id = station.properties.stationIdentifier;
|
||||
|
||||
@@ -15,11 +15,17 @@ import { readFile } from 'fs/promises';
|
||||
import file from 'gulp-file';
|
||||
import { CloudFrontClient, CreateInvalidationCommand } from '@aws-sdk/client-cloudfront';
|
||||
import log from 'fancy-log';
|
||||
import * as dartSass from 'sass';
|
||||
import gulpSass from 'gulp-sass';
|
||||
import sourceMaps from 'gulp-sourcemaps';
|
||||
import OVERRIDES from '../src/overrides.mjs';
|
||||
import { DateTime } from 'luxon';
|
||||
|
||||
// get cloudfront
|
||||
import reader from '../src/playlist-reader.mjs';
|
||||
|
||||
const sass = gulpSass(dartSass);
|
||||
|
||||
const clean = () => deleteAsync(['./dist/**/*', '!./dist/readme.txt']);
|
||||
|
||||
const cloudfront = new CloudFrontClient({ region: 'us-east-1' });
|
||||
@@ -36,6 +42,7 @@ const webpackOptions = {
|
||||
resolve: {
|
||||
roots: ['./'],
|
||||
},
|
||||
devtool: 'source-map',
|
||||
optimization: {
|
||||
minimize: true,
|
||||
minimizer: [
|
||||
@@ -80,7 +87,7 @@ const mjsSources = [
|
||||
'server/scripts/modules/travelforecast.mjs',
|
||||
'server/scripts/modules/progress.mjs',
|
||||
'server/scripts/modules/media.mjs',
|
||||
'server/scripts/modules/custom-rss-feed.mjs',
|
||||
'server/scripts/modules/custom-scroll-text.mjs',
|
||||
'server/scripts/index.mjs',
|
||||
];
|
||||
|
||||
@@ -89,10 +96,13 @@ const buildJs = () => src(mjsSources)
|
||||
.pipe(dest(RESOURCES_PATH));
|
||||
|
||||
const cssSources = [
|
||||
'server/styles/main.css',
|
||||
'server/styles/scss/**/*.scss',
|
||||
];
|
||||
const copyCss = () => src(cssSources)
|
||||
.pipe(concat('ws.min.css'))
|
||||
const buildCss = () => src(cssSources)
|
||||
.pipe(sourceMaps.init())
|
||||
.pipe(sass({ style: 'compressed' }).on('error', sass.logError))
|
||||
.pipe(rename({ suffix: '.min' }))
|
||||
.pipe(sourceMaps.write('./'))
|
||||
.pipe(dest(RESOURCES_PATH));
|
||||
|
||||
const htmlSources = [
|
||||
@@ -101,10 +111,9 @@ const htmlSources = [
|
||||
const packageJson = await readFile('package.json');
|
||||
let { version } = JSON.parse(packageJson);
|
||||
const previewVersion = async () => {
|
||||
// generate a relatively unique timestamp for cache invalidation of the preview site
|
||||
const now = new Date();
|
||||
const msNow = now.getTime() % 1_000_000;
|
||||
version = msNow.toString();
|
||||
// generate a unique timestamp for cache invalidation of the preview site
|
||||
const now = DateTime.utc();
|
||||
version = now.toFormat('yyyyLLddHHmm').substring(3);
|
||||
};
|
||||
|
||||
const compressHtml = async () => src(htmlSources)
|
||||
@@ -141,7 +150,6 @@ const s3 = s3Upload({
|
||||
});
|
||||
const uploadSources = [
|
||||
'dist/**',
|
||||
'!dist/**/*.map',
|
||||
'!dist/images/**/*',
|
||||
'!dist/fonts/**/*',
|
||||
];
|
||||
@@ -209,12 +217,12 @@ const logVersion = async () => {
|
||||
log(`Version Published: ${version}`);
|
||||
};
|
||||
|
||||
const buildDist = series(clean, parallel(buildJs, compressJsVendor, copyCss, compressHtml, copyOtherFiles, copyDataFiles, copyImageSources, buildPlaylist));
|
||||
const buildDist = series(clean, parallel(buildJs, compressJsVendor, buildCss, compressHtml, copyOtherFiles, copyDataFiles, copyImageSources, buildPlaylist));
|
||||
|
||||
// upload_images could be in parallel with upload, but _images logs a lot and has little changes
|
||||
// by running upload last the majority of the changes will be at the bottom of the log for easy viewing
|
||||
const publishFrontend = series(buildDist, uploadImages, upload, invalidate, logVersion);
|
||||
const stageFrontend = series(previewVersion, buildDist, uploadImagesPreview, uploadPreview, invalidatePreview);
|
||||
const stageFrontend = series(previewVersion, buildDist, uploadImagesPreview, uploadPreview, invalidatePreview, logVersion);
|
||||
|
||||
export default publishFrontend;
|
||||
|
||||
|
||||
@@ -158,6 +158,7 @@ if (process.env?.DIST === '1') {
|
||||
// 'npm run build' and then 'DIST=1 npm start'
|
||||
app.use('/scripts', express.static('./server/scripts', staticOptions));
|
||||
app.use('/geoip', geoip);
|
||||
app.use('/music', express.static('./server/music', staticOptions));
|
||||
|
||||
// render the EJS template in production mode (serve compressed files from dist directory)
|
||||
app.get('/', (req, res) => { renderIndex(req, res, true); });
|
||||
|
||||
2922
package-lock.json
generated
2922
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ws4kp",
|
||||
"version": "6.3.1",
|
||||
"version": "6.5.7",
|
||||
"description": "Welcome to the WeatherStar 4000+ project page!",
|
||||
"main": "index.mjs",
|
||||
"type": "module",
|
||||
@@ -31,7 +31,7 @@
|
||||
"@eslint/eslintrc": "^3.3.1",
|
||||
"ajv": "^8.17.1",
|
||||
"del": "^8.0.0",
|
||||
"eslint": "^9.0.0",
|
||||
"eslint": "^10.0.3",
|
||||
"eslint-config-airbnb-base": "15.0.0",
|
||||
"eslint-plugin-import": "^2.10.0",
|
||||
"fancy-log": "^2.0.0",
|
||||
@@ -40,10 +40,11 @@
|
||||
"gulp-concat": "^2.6.1",
|
||||
"gulp-ejs": "^5.1.0",
|
||||
"gulp-file": "^0.4.0",
|
||||
"gulp-html-minifier-terser": "^7.1.0",
|
||||
"gulp-html-minifier-terser": "^8.0.0",
|
||||
"gulp-rename": "^2.0.0",
|
||||
"gulp-s3-uploader": "^1.0.6",
|
||||
"gulp-sass": "^6.0.0",
|
||||
"gulp-sourcemaps": "^3.0.0",
|
||||
"gulp-terser": "^2.0.0",
|
||||
"luxon": "^3.0.0",
|
||||
"metar-taf-parser": "^9.0.0",
|
||||
@@ -57,7 +58,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"dotenv": "^17.0.1",
|
||||
"ejs": "^3.1.5",
|
||||
"ejs": "^5.0.1",
|
||||
"express": "^5.1.0"
|
||||
}
|
||||
}
|
||||
|
||||
BIN
server/images/backgrounds/1-chart-wide.png
Normal file
BIN
server/images/backgrounds/1-chart-wide.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.6 KiB |
BIN
server/images/backgrounds/1-wide-enhanced.png
Normal file
BIN
server/images/backgrounds/1-wide-enhanced.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.3 KiB |
BIN
server/images/backgrounds/3-wide-enhanced.png
Normal file
BIN
server/images/backgrounds/3-wide-enhanced.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.1 KiB |
BIN
server/images/gimp/1-chart-wide.xcf
Normal file
BIN
server/images/gimp/1-chart-wide.xcf
Normal file
Binary file not shown.
BIN
server/images/gimp/1-wide-enhanced.xcf
Normal file
BIN
server/images/gimp/1-wide-enhanced.xcf
Normal file
Binary file not shown.
BIN
server/images/gimp/1.xcf
Normal file
BIN
server/images/gimp/1.xcf
Normal file
Binary file not shown.
BIN
server/images/gimp/3-wide-enhanced.xcf
Normal file
BIN
server/images/gimp/3-wide-enhanced.xcf
Normal file
Binary file not shown.
@@ -47,10 +47,7 @@ class Almanac extends WeatherDisplay {
|
||||
}
|
||||
|
||||
calcSunMoonData(weatherParameters) {
|
||||
const sun = [
|
||||
SunCalc.getTimes(new Date(), weatherParameters.latitude, weatherParameters.longitude),
|
||||
SunCalc.getTimes(DateTime.local().plus({ days: 1 }).toJSDate(), weatherParameters.latitude, weatherParameters.longitude),
|
||||
];
|
||||
const sun = [0, 1, 2, 3, 4, 5, 6].map((days) => SunCalc.getTimes(DateTime.local().plus({ days }).toJSDate(), weatherParameters.latitude, weatherParameters.longitude));
|
||||
|
||||
// brute force the moon phases by scanning the next 30 days
|
||||
const moon = [];
|
||||
@@ -72,7 +69,7 @@ class Almanac extends WeatherDisplay {
|
||||
|
||||
// stop after 30 days or 4 moon phases
|
||||
iterations += 1;
|
||||
} while (iterations <= 30 && moon.length < 4);
|
||||
} while (iterations <= 45 && moon.length < 5);
|
||||
|
||||
return {
|
||||
sun,
|
||||
@@ -126,21 +123,16 @@ class Almanac extends WeatherDisplay {
|
||||
|
||||
// Set day names
|
||||
const Today = DateTime.local();
|
||||
const Tomorrow = Today.plus({ days: 1 });
|
||||
this.elem.querySelector('.day-1').textContent = Today.toLocaleString({ weekday: 'long' });
|
||||
this.elem.querySelector('.day-2').textContent = Tomorrow.toLocaleString({ weekday: 'long' });
|
||||
// fill all three days, even if some are hidden by non-enhanced
|
||||
for (let i = 0; i < 3; i += 1) {
|
||||
this.elem.querySelector(`.day-${i}`).textContent = Today.plus({ days: i }).toLocaleString({ weekday: 'long' });
|
||||
|
||||
const todaySunrise = DateTime.fromJSDate(info.sun[0].sunrise);
|
||||
const todaySunset = DateTime.fromJSDate(info.sun[0].sunset);
|
||||
const [todaySunriseFormatted, todaySunsetFormatted] = formatTimesForColumn([todaySunrise, todaySunset]);
|
||||
this.elem.querySelector('.rise-1').textContent = todaySunriseFormatted;
|
||||
this.elem.querySelector('.set-1').textContent = todaySunsetFormatted;
|
||||
|
||||
const tomorrowSunrise = DateTime.fromJSDate(info.sun[1].sunrise);
|
||||
const tomorrowSunset = DateTime.fromJSDate(info.sun[1].sunset);
|
||||
const [tomorrowSunriseFormatted, tomorrowSunsetformatted] = formatTimesForColumn([tomorrowSunrise, tomorrowSunset]);
|
||||
this.elem.querySelector('.rise-2').textContent = tomorrowSunriseFormatted;
|
||||
this.elem.querySelector('.set-2').textContent = tomorrowSunsetformatted;
|
||||
const sunrise = DateTime.fromJSDate(info.sun[i].sunrise);
|
||||
const sunset = DateTime.fromJSDate(info.sun[i].sunset);
|
||||
const [sunriseFormatted, sunsetFormatted] = formatTimesForColumn([sunrise, sunset]);
|
||||
this.elem.querySelector(`.rise-${i}`).textContent = sunriseFormatted;
|
||||
this.elem.querySelector(`.set-${i}`).textContent = sunsetFormatted;
|
||||
}
|
||||
|
||||
// Moon data
|
||||
const days = info.moon.map((MoonPhase) => {
|
||||
|
||||
@@ -14,9 +14,7 @@ import {
|
||||
import { debugFlag } from './utils/debug.mjs';
|
||||
import { isDataStale, enhanceObservationWithMapClick } from './utils/mapclick.mjs';
|
||||
import { DateTime } from '../vendor/auto/luxon.mjs';
|
||||
|
||||
// some stations prefixed do not provide all the necessary data
|
||||
const skipStations = ['U', 'C', 'H', 'W', 'Y', 'T', 'S', 'M', 'O', 'L', 'A', 'F', 'B', 'N', 'V', 'R', 'D', 'E', 'I', 'G', 'J'];
|
||||
import settings from './settings.mjs';
|
||||
|
||||
class CurrentWeather extends WeatherDisplay {
|
||||
constructor(navId, elemId) {
|
||||
@@ -29,8 +27,8 @@ class CurrentWeather extends WeatherDisplay {
|
||||
// note: current weather does not use old data on a silent refresh
|
||||
// this is deliberate because it can pull data from more than one station in sequence
|
||||
|
||||
// filter for 4-letter observation stations, only those contain sky conditions and thus an icon
|
||||
const filteredStations = this.weatherParameters.stations.filter((station) => station?.properties?.stationIdentifier?.length === 4 && !skipStations.includes(station.properties.stationIdentifier.slice(0, 1)));
|
||||
// get the available stations
|
||||
const { stations } = this.weatherParameters;
|
||||
|
||||
// Load the observations
|
||||
let observations;
|
||||
@@ -38,9 +36,9 @@ class CurrentWeather extends WeatherDisplay {
|
||||
|
||||
// station number counter
|
||||
let stationNum = 0;
|
||||
while (!observations && stationNum < filteredStations.length) {
|
||||
while (!observations && stationNum < stations.length) {
|
||||
// get the station
|
||||
station = filteredStations[stationNum];
|
||||
station = stations[stationNum];
|
||||
const stationId = station.properties.stationIdentifier;
|
||||
|
||||
stationNum += 1;
|
||||
@@ -104,7 +102,11 @@ class CurrentWeather extends WeatherDisplay {
|
||||
debugContext: 'currentweather',
|
||||
});
|
||||
|
||||
// copy enhanced data and restore the timestamp if it was overwritten by older data from mapclick
|
||||
const { timestamp } = candidateObservation.features[0].properties;
|
||||
candidateObservation.features[0].properties = enhancedResult.data;
|
||||
candidateObservation.features[0].properties.timestamp = timestamp;
|
||||
|
||||
const { missingFields } = enhancedResult;
|
||||
const missingRequired = missingFields.filter((fieldName) => {
|
||||
const field = requiredFields.find((f) => f.name === fieldName && f.required);
|
||||
@@ -192,7 +194,9 @@ class CurrentWeather extends WeatherDisplay {
|
||||
const wind = (typeof this.data.WindSpeed === 'number') ? this.data.WindDirection.padEnd(3, '') + this.data.WindSpeed.toString().padStart(3, ' ') : this.data.WindSpeed;
|
||||
|
||||
// get location (city name) from StationInfo if available (allows for overrides)
|
||||
const location = (StationInfo[this.data.station.properties.stationIdentifier]?.city ?? locationCleanup(this.data.station.properties.name)).substr(0, 20);
|
||||
// longer name allowed if in wide-enhanced
|
||||
const locationLimit = (settings.wide?.value && settings.enhancedScreens?.value) ? 25 : 20;
|
||||
const location = (StationInfo[this.data.station.properties.stationIdentifier]?.city ?? locationCleanup(this.data.station.properties.name)).substr(0, locationLimit);
|
||||
|
||||
const fill = {
|
||||
temp: this.data.Temperature + String.fromCharCode(176),
|
||||
@@ -324,7 +328,7 @@ const backfill = (data) => {
|
||||
// backfill each property
|
||||
Object.keys(sortedData[0].properties).forEach((key) => {
|
||||
// qualify the key (must have value)
|
||||
if (Object.hasOwn(sortedData[0].properties[key], 'value')) {
|
||||
if (Object.hasOwn(sortedData[0].properties?.[key] ?? {}, 'value')) {
|
||||
// backfill this property
|
||||
result[key] = backfillProperty(sortedData, key);
|
||||
} else {
|
||||
|
||||
@@ -261,6 +261,7 @@ const parseMessage = (event) => {
|
||||
if (event?.data?.type === 'current-weather-scroll') {
|
||||
if (event.data?.method === 'start') start();
|
||||
if (event.data?.method === 'reload') stop(true);
|
||||
if (event.data?.method === 'non-display') nonDisplay();
|
||||
if (event.data?.method === 'show') show();
|
||||
if (event.data?.method === 'hide') hide();
|
||||
}
|
||||
@@ -274,6 +275,20 @@ const hide = () => {
|
||||
mainScroll.style.display = 'none';
|
||||
};
|
||||
|
||||
const nonDisplay = () => {
|
||||
if (interval) {
|
||||
clearInterval(interval);
|
||||
interval = null;
|
||||
stop();
|
||||
// if greater than default update (typically long scroll) skip to the next weather screen
|
||||
if (nextUpdate > DEFAULT_UPDATE) {
|
||||
screenIndex = (screenIndex + 1) % (workingScreens.length);
|
||||
sinceLastUpdate = 0;
|
||||
nextUpdate = DEFAULT_UPDATE;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const screenCount = () => workingScreens.length;
|
||||
const atDefault = () => defaultScreensLoaded;
|
||||
|
||||
|
||||
@@ -1,135 +0,0 @@
|
||||
import Setting from './utils/setting.mjs';
|
||||
import { reset as resetScroll, addScreen as addScroll, hazards } from './currentweatherscroll.mjs';
|
||||
import { json } from './utils/fetch.mjs';
|
||||
|
||||
let firstRun = true;
|
||||
|
||||
const parser = new DOMParser();
|
||||
|
||||
// change of enable handler
|
||||
const changeEnable = (newValue) => {
|
||||
let newDisplay;
|
||||
if (newValue) {
|
||||
// add the feed to the scroll
|
||||
parseFeed(customFeed.value);
|
||||
// show the string box
|
||||
newDisplay = 'block';
|
||||
} else {
|
||||
// set scroll back to original
|
||||
resetScroll();
|
||||
// hide the string entry
|
||||
newDisplay = 'none';
|
||||
}
|
||||
const stringEntry = document.getElementById('settings-customFeed-label');
|
||||
if (stringEntry) {
|
||||
stringEntry.style.display = newDisplay;
|
||||
}
|
||||
};
|
||||
|
||||
// parse the feed/text provided
|
||||
const parseFeed = (textInput) => {
|
||||
// skip getting the feed on first run
|
||||
if (firstRun) return;
|
||||
|
||||
// test validity
|
||||
if (textInput === undefined || textInput === '') {
|
||||
resetScroll();
|
||||
}
|
||||
|
||||
// test for url
|
||||
if (textInput.match(/https?:\/\//)) {
|
||||
getFeed(textInput);
|
||||
return;
|
||||
}
|
||||
|
||||
// add single text scroll after hazards if present
|
||||
resetScroll();
|
||||
addScroll(hazards);
|
||||
addScroll(
|
||||
() => (
|
||||
{
|
||||
type: 'scroll',
|
||||
text: textInput,
|
||||
}),
|
||||
// keep the existing scroll
|
||||
true,
|
||||
);
|
||||
};
|
||||
|
||||
// get the rss feed and then swap out the current weather scroll
|
||||
const getFeed = async (url) => {
|
||||
// get the text as a string
|
||||
// it needs to be proxied, use a free service
|
||||
const rssResponse = await json(`https://api.allorigins.win/get?url=${url}`);
|
||||
|
||||
// this returns a data url
|
||||
// a few sanity checks
|
||||
if (rssResponse.status.content_type.indexOf('xml') < 0) return;
|
||||
// determine return type
|
||||
const isBase64 = rssResponse.status.content_type.substring(0, 8) !== 'text/xml';
|
||||
|
||||
// base 64 decode everything after the comma
|
||||
const rss = isBase64 ? atob(rssResponse.contents.split('base64,')[1]) : rssResponse.contents;
|
||||
|
||||
// parse the rss
|
||||
const doc = parser.parseFromString(rss, 'text/xml');
|
||||
|
||||
// get the title
|
||||
const rssTitle = doc.querySelector('channel title').textContent;
|
||||
|
||||
// get each item
|
||||
const titles = [...doc.querySelectorAll('item title')].map((t) => t.textContent);
|
||||
|
||||
// reset the scroll, then add the screens
|
||||
resetScroll();
|
||||
// add the hazards scroll first
|
||||
addScroll(hazards);
|
||||
titles.forEach((title) => {
|
||||
// data is provided to the screen handler, so we return a function
|
||||
addScroll(
|
||||
() => ({
|
||||
header: rssTitle,
|
||||
type: 'scroll',
|
||||
text: title,
|
||||
}),
|
||||
// false parameter does not include the default weather scrolls
|
||||
false,
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
// change the feed source and re-load if necessary
|
||||
const changeFeed = (newValue) => {
|
||||
// first pass through won't have custom feed enable ready
|
||||
if (firstRun) return;
|
||||
|
||||
if (customFeedEnable.value) {
|
||||
parseFeed(newValue);
|
||||
}
|
||||
};
|
||||
|
||||
const customFeed = new Setting('customFeed', {
|
||||
name: 'Custom RSS Feed',
|
||||
defaultValue: '',
|
||||
type: 'string',
|
||||
changeAction: changeFeed,
|
||||
placeholder: 'Text or URL',
|
||||
});
|
||||
|
||||
const customFeedEnable = new Setting('customFeedEnable', {
|
||||
name: 'Enable RSS Feed/Text',
|
||||
defaultValue: false,
|
||||
changeAction: changeEnable,
|
||||
});
|
||||
|
||||
// initialize the custom feed inputs on the page
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// add the controls to the page
|
||||
const settingsSection = document.querySelector('#settings');
|
||||
settingsSection.append(customFeedEnable.generate(), customFeed.generate());
|
||||
// clear the first run value
|
||||
firstRun = false;
|
||||
// call change enable with the current value to show/hide the url box
|
||||
// and make the call to get the feed if enabled
|
||||
changeEnable(customFeedEnable.value);
|
||||
});
|
||||
89
server/scripts/modules/custom-scroll-text.mjs
Normal file
89
server/scripts/modules/custom-scroll-text.mjs
Normal file
@@ -0,0 +1,89 @@
|
||||
import Setting from './utils/setting.mjs';
|
||||
import { reset as resetScroll, addScreen as addScroll, hazards } from './currentweatherscroll.mjs';
|
||||
|
||||
let firstRun = true;
|
||||
|
||||
// change of enable handler
|
||||
const changeEnable = (newValue) => {
|
||||
let newDisplay;
|
||||
if (newValue) {
|
||||
// add the text to the scroll
|
||||
parseText(customText.value);
|
||||
// show the string box
|
||||
newDisplay = 'block';
|
||||
} else {
|
||||
// set scroll back to original
|
||||
resetScroll();
|
||||
// hide the string entry
|
||||
newDisplay = 'none';
|
||||
}
|
||||
const stringEntry = document.getElementById('settings-customText-label');
|
||||
if (stringEntry) {
|
||||
stringEntry.style.display = newDisplay;
|
||||
}
|
||||
};
|
||||
|
||||
// parse the text provided
|
||||
const parseText = (textInput) => {
|
||||
// skip updating text on first run
|
||||
if (firstRun) return;
|
||||
|
||||
// test validity
|
||||
if (textInput === undefined || textInput === '') {
|
||||
resetScroll();
|
||||
}
|
||||
|
||||
// split the text at pipe characters
|
||||
const texts = textInput.split('|');
|
||||
|
||||
// add single text scroll after hazards if present
|
||||
resetScroll();
|
||||
addScroll(hazards);
|
||||
addScroll(
|
||||
() => {
|
||||
// pick a random string from the available list
|
||||
const randInt = Math.floor(Math.random() * texts.length);
|
||||
return {
|
||||
type: 'scroll',
|
||||
text: texts[randInt],
|
||||
};
|
||||
},
|
||||
// keep the existing scroll
|
||||
true,
|
||||
);
|
||||
};
|
||||
|
||||
// change the text
|
||||
const changeText = (newValue) => {
|
||||
// first pass through won't have custom text enable ready
|
||||
if (firstRun) return;
|
||||
|
||||
if (customTextEnable.value) {
|
||||
parseText(newValue);
|
||||
}
|
||||
};
|
||||
|
||||
const customText = new Setting('customText', {
|
||||
name: 'Custom Text',
|
||||
defaultValue: '',
|
||||
type: 'string',
|
||||
changeAction: changeText,
|
||||
placeholder: 'Text to scroll',
|
||||
});
|
||||
|
||||
const customTextEnable = new Setting('customTextEnable', {
|
||||
name: 'Enable Custom Text',
|
||||
defaultValue: false,
|
||||
changeAction: changeEnable,
|
||||
});
|
||||
|
||||
// initialize the custom text inputs on the page
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// add the controls to the page
|
||||
const settingsSection = document.querySelector('#settings');
|
||||
settingsSection.append(customTextEnable.generate(), customText.generate());
|
||||
// clear the first run value
|
||||
firstRun = false;
|
||||
// call change enable with the current value to show/hide the url box
|
||||
changeEnable(customTextEnable.value);
|
||||
});
|
||||
@@ -17,7 +17,13 @@ class ExtendedForecast extends WeatherDisplay {
|
||||
super(navId, elemId, 'Extended Forecast', true);
|
||||
|
||||
// set timings
|
||||
this.timing.totalScreens = 2;
|
||||
if (settings.portrait?.value) {
|
||||
this.timing.totalScreens = 1;
|
||||
this.perPage = 4;
|
||||
} else {
|
||||
this.timing.totalScreens = 2;
|
||||
this.perPage = 3;
|
||||
}
|
||||
}
|
||||
|
||||
async getData(weatherParameters, refresh) {
|
||||
@@ -54,7 +60,7 @@ class ExtendedForecast extends WeatherDisplay {
|
||||
|
||||
// determine bounds
|
||||
// grab the first three or second set of three array elements
|
||||
const forecast = parse(this.data.properties.periods, this.weatherParameters.forecast).slice(0 + 3 * this.screenIndex, 3 + this.screenIndex * 3);
|
||||
const forecast = parse(this.data.properties.periods, this.weatherParameters.forecast).slice(0 + this.perPage * this.screenIndex, this.perPage + this.screenIndex * this.perPage);
|
||||
|
||||
// create each day template
|
||||
const days = forecast.map((Day) => {
|
||||
@@ -97,11 +103,9 @@ const parse = (fullForecast, forecastUrl) => {
|
||||
// Skip the first period if it's nighttime (like "Tonight") since extended forecast
|
||||
// should focus on upcoming full days, not the end of the current day
|
||||
let startIndex = 0;
|
||||
let dateOffset = 0; // offset for date labels when we skip periods
|
||||
|
||||
if (activePeriods.length > 0 && !activePeriods[0].isDaytime) {
|
||||
startIndex = 1;
|
||||
dateOffset = 1; // start date labels from tomorrow since we're skipping tonight
|
||||
if (debugFlag('extendedforecast')) {
|
||||
console.log(`ExtendedForecast: Skipping first period "${activePeriods[0].name}" because it's nighttime`);
|
||||
}
|
||||
@@ -111,25 +115,14 @@ const parse = (fullForecast, forecastUrl) => {
|
||||
}
|
||||
}
|
||||
|
||||
// create a list of days starting with the appropriate day
|
||||
const Days = [0, 1, 2, 3, 4, 5, 6];
|
||||
const dates = Days.map((shift) => {
|
||||
const date = DateTime.local().startOf('day').plus({ days: shift + dateOffset });
|
||||
return date.toLocaleString({ weekday: 'short' });
|
||||
});
|
||||
|
||||
if (debugFlag('extendedforecast')) {
|
||||
console.log(`ExtendedForecast: Generated date labels: [${dates.join(', ')}]`);
|
||||
}
|
||||
|
||||
// track the destination forecast index
|
||||
let destIndex = 0;
|
||||
const forecast = [];
|
||||
|
||||
// if the first period is nighttime it is skipped above via startIndex
|
||||
for (let i = startIndex; i < activePeriods.length; i += 1) {
|
||||
const period = activePeriods[i];
|
||||
|
||||
// create the destination object if necessary
|
||||
if (!forecast[destIndex]) {
|
||||
forecast.push({
|
||||
dayName: '', low: undefined, high: undefined, text: undefined, icon: undefined,
|
||||
@@ -138,15 +131,14 @@ const parse = (fullForecast, forecastUrl) => {
|
||||
// get the object to modify/populate
|
||||
const fDay = forecast[destIndex];
|
||||
|
||||
// preload the icon
|
||||
preloadImg(fDay.icon);
|
||||
|
||||
if (period.isDaytime) {
|
||||
// day time is the high temperature
|
||||
fDay.high = period.temperature;
|
||||
fDay.icon = getLargeIcon(period.icon);
|
||||
fDay.text = shortenExtendedForecastText(period.shortForecast);
|
||||
fDay.dayName = dates[destIndex];
|
||||
fDay.dayName = DateTime.fromISO(period.startTime).startOf('day').toLocaleString({ weekday: 'short' });
|
||||
// preload the icon
|
||||
preloadImg(fDay.icon);
|
||||
// Wait for the corresponding night period to increment
|
||||
} else {
|
||||
// low temperature
|
||||
|
||||
@@ -5,10 +5,30 @@ import getHourlyData from './hourly.mjs';
|
||||
import WeatherDisplay from './weatherdisplay.mjs';
|
||||
import { registerDisplay, timeZone } from './navigation.mjs';
|
||||
import { DateTime } from '../vendor/auto/luxon.mjs';
|
||||
import settings from './settings.mjs';
|
||||
|
||||
// get available space
|
||||
const availableWidth = 532;
|
||||
const availableHeight = 285;
|
||||
// set up spacing and scales
|
||||
const scaling = () => {
|
||||
const available = {
|
||||
width: 532,
|
||||
height: 285,
|
||||
};
|
||||
const dataLength = {
|
||||
hours: 36,
|
||||
xTicks: 4,
|
||||
};
|
||||
|
||||
if (settings.wide?.value && settings.enhancedScreens?.value) {
|
||||
available.width = available.width + 107 + 107;
|
||||
available.height = 285;
|
||||
dataLength.hours = 48;
|
||||
dataLength.xTicks = 6;
|
||||
}
|
||||
return {
|
||||
available,
|
||||
dataLength,
|
||||
};
|
||||
};
|
||||
|
||||
class HourlyGraph extends WeatherDisplay {
|
||||
constructor(navId, elemId, defaultActive) {
|
||||
@@ -40,39 +60,59 @@ class HourlyGraph extends WeatherDisplay {
|
||||
const temperature = data.map((d) => d.temperature);
|
||||
const probabilityOfPrecipitation = data.map((d) => d.probabilityOfPrecipitation);
|
||||
const skyCover = data.map((d) => d.skyCover);
|
||||
const dewpoint = data.map((d) => d.dewpoint);
|
||||
|
||||
this.data = {
|
||||
skyCover, temperature, probabilityOfPrecipitation, temperatureUnit: data[0].temperatureUnit,
|
||||
skyCover, temperature, probabilityOfPrecipitation, temperatureUnit: data[0].temperatureUnit, dewpoint,
|
||||
};
|
||||
|
||||
// get the data length for current settings
|
||||
const { dataLength } = scaling();
|
||||
|
||||
// clamp down the data to the allowed size
|
||||
Object.entries(this.data).forEach(([key, value]) => {
|
||||
if (Array.isArray(value)) {
|
||||
this.data[key] = value.slice(0, dataLength.hours);
|
||||
}
|
||||
});
|
||||
|
||||
this.setStatus(STATUS.loaded);
|
||||
}
|
||||
|
||||
drawCanvas() {
|
||||
// get scaling parameters
|
||||
const { dataLength, available } = scaling();
|
||||
|
||||
// get the image
|
||||
if (!this.image) this.image = this.elem.querySelector('.chart img');
|
||||
|
||||
this.image.width = availableWidth;
|
||||
this.image.height = availableHeight;
|
||||
// set up image
|
||||
this.image.width = available.width;
|
||||
this.image.height = available.height;
|
||||
|
||||
// get context
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = availableWidth;
|
||||
canvas.height = availableHeight;
|
||||
canvas.width = available.width;
|
||||
canvas.height = available.height;
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.imageSmoothingEnabled = false;
|
||||
|
||||
// calculate time scale
|
||||
const timeScale = calcScale(0, 5, this.data.temperature.length - 1, availableWidth);
|
||||
const timeScale = calcScale(0, 5, this.data.temperature.length - 1, available.width);
|
||||
const timeStep = this.data.temperature.length / (dataLength.xTicks);
|
||||
const startTime = DateTime.now().startOf('hour');
|
||||
document.querySelector('.x-axis .l-1').innerHTML = formatTime(startTime);
|
||||
document.querySelector('.x-axis .l-2').innerHTML = formatTime(startTime.plus({ hour: 6 }));
|
||||
document.querySelector('.x-axis .l-3').innerHTML = formatTime(startTime.plus({ hour: 12 }));
|
||||
document.querySelector('.x-axis .l-4').innerHTML = formatTime(startTime.plus({ hour: 18 }));
|
||||
document.querySelector('.x-axis .l-5').innerHTML = formatTime(startTime.plus({ hour: 24 }));
|
||||
let prevTime = startTime;
|
||||
Array(dataLength.xTicks + 1).fill().forEach((val, idx) => {
|
||||
// track the previous label so a day of week can be added when it changes
|
||||
const label = formatTime(startTime.plus({ hour: idx * timeStep }), prevTime);
|
||||
prevTime = label.ts;
|
||||
// write to page
|
||||
document.querySelector(`.x-axis .l-${idx + 1}`).innerHTML = label.formatted;
|
||||
});
|
||||
|
||||
// order is important last line drawn is on top
|
||||
// clouds
|
||||
const percentScale = calcScale(0, availableHeight - 10, 100, 10);
|
||||
const percentScale = calcScale(0, available.height - 10, 100, 10);
|
||||
const cloud = createPath(this.data.skyCover, timeScale, percentScale);
|
||||
drawPath(cloud, ctx, {
|
||||
strokeStyle: 'lightgrey',
|
||||
@@ -86,11 +126,22 @@ class HourlyGraph extends WeatherDisplay {
|
||||
lineWidth: 3,
|
||||
});
|
||||
|
||||
// calculate temperature scale for min and max of dewpoint and temperature
|
||||
const minScale = Math.min(...this.data.dewpoint, ...this.data.temperature);
|
||||
const maxScale = Math.max(...this.data.dewpoint, ...this.data.temperature);
|
||||
const thirdScale = (maxScale - minScale) / 3;
|
||||
const midScale1 = Math.round(minScale + thirdScale);
|
||||
const midScale2 = Math.round(minScale + (thirdScale * 2));
|
||||
const tempScale = calcScale(minScale, available.height - 10, maxScale, 10);
|
||||
|
||||
// dewpoint
|
||||
const dewpointPath = createPath(this.data.dewpoint, timeScale, tempScale);
|
||||
drawPath(dewpointPath, ctx, {
|
||||
strokeStyle: 'green',
|
||||
lineWidth: 3,
|
||||
});
|
||||
|
||||
// temperature
|
||||
const minTemp = Math.min(...this.data.temperature);
|
||||
const maxTemp = Math.max(...this.data.temperature);
|
||||
const midTemp = Math.round((minTemp + maxTemp) / 2);
|
||||
const tempScale = calcScale(minTemp, availableHeight - 10, maxTemp, 10);
|
||||
const tempPath = createPath(this.data.temperature, timeScale, tempScale);
|
||||
drawPath(tempPath, ctx, {
|
||||
strokeStyle: 'red',
|
||||
@@ -100,15 +151,17 @@ class HourlyGraph extends WeatherDisplay {
|
||||
// temperature axis labels
|
||||
// limited to 3 characters, sacraficing degree character
|
||||
const degree = String.fromCharCode(176);
|
||||
this.elem.querySelector('.y-axis .l-1').innerHTML = (maxTemp + degree).substring(0, 3);
|
||||
this.elem.querySelector('.y-axis .l-2').innerHTML = (midTemp + degree).substring(0, 3);
|
||||
this.elem.querySelector('.y-axis .l-3').innerHTML = (minTemp + degree).substring(0, 3);
|
||||
this.elem.querySelector('.y-axis .l-1').innerHTML = (maxScale + degree).substring(0, 3);
|
||||
this.elem.querySelector('.y-axis .l-2').innerHTML = (midScale2 + degree).substring(0, 3);
|
||||
this.elem.querySelector('.y-axis .l-3').innerHTML = (midScale1 + degree).substring(0, 3);
|
||||
this.elem.querySelector('.y-axis .l-4').innerHTML = (minScale + degree).substring(0, 3);
|
||||
|
||||
// set the image source
|
||||
this.image.src = canvas.toDataURL();
|
||||
|
||||
// change the units in the header
|
||||
this.elem.querySelector('.temperature').innerHTML = `Temperature ${String.fromCharCode(176)}${this.data.temperatureUnit}`;
|
||||
this.elem.querySelector('.dewpoint').innerHTML = `Dewpoint ${String.fromCharCode(176)}${this.data.temperatureUnit}`;
|
||||
|
||||
super.drawCanvas();
|
||||
this.finishDraw();
|
||||
@@ -145,7 +198,18 @@ const drawPath = (path, ctx, options) => {
|
||||
};
|
||||
|
||||
// format as 1p, 12a, etc.
|
||||
const formatTime = (time) => time.setZone(timeZone()).toFormat('ha').slice(0, -1);
|
||||
const formatTime = (time, prev) => {
|
||||
// if the day of the week changes, show the day of the week in the label
|
||||
let format = 'ha';
|
||||
if (prev.weekday !== time.weekday) format = 'ccc ha';
|
||||
|
||||
const ts = time.setZone(timeZone());
|
||||
|
||||
return {
|
||||
ts,
|
||||
formatted: ts.toFormat(format).slice(0, -1),
|
||||
};
|
||||
};
|
||||
|
||||
// register display
|
||||
registerDisplay(new HourlyGraph(4, 'hourly-graph'));
|
||||
|
||||
@@ -75,7 +75,10 @@ class Hourly extends WeatherDisplay {
|
||||
|
||||
const startingHour = DateTime.local().setZone(timeZone());
|
||||
|
||||
const lines = this.data.map((data, index) => {
|
||||
// shorten to 24 hours
|
||||
const shortData = this.data.slice(0, 24);
|
||||
|
||||
const lines = shortData.map((data, index) => {
|
||||
const fillValues = {};
|
||||
// hour
|
||||
const hour = startingHour.plus({ hours: index });
|
||||
@@ -102,7 +105,7 @@ class Hourly extends WeatherDisplay {
|
||||
const filledRow = this.fillTemplate('hourly-row', fillValues);
|
||||
|
||||
// alter the color of the feels like column to reflect wind chill or heat index
|
||||
if (feelsLike < temperature) {
|
||||
if (data.apparentTemperature < data.temperature) {
|
||||
filledRow.querySelector('.like').classList.add('wind-chill');
|
||||
} else if (feelsLike > temperature) {
|
||||
filledRow.querySelector('.like').classList.add('heat-index');
|
||||
@@ -203,6 +206,7 @@ const parseForecast = async (data) => {
|
||||
const iceAccumulation = expand(data.iceAccumulation.values); // ice icon
|
||||
const probabilityOfPrecipitation = expand(data.probabilityOfPrecipitation.values); // rain icon
|
||||
const snowfallAmount = expand(data.snowfallAmount.values); // snow icon
|
||||
const dewpoint = expand(data.dewpoint.values);
|
||||
|
||||
const icons = await determineIcon(skyCover, weather, iceAccumulation, probabilityOfPrecipitation, snowfallAmount, windSpeed);
|
||||
|
||||
@@ -216,6 +220,7 @@ const parseForecast = async (data) => {
|
||||
probabilityOfPrecipitation: probabilityOfPrecipitation[idx],
|
||||
skyCover: skyCover[idx],
|
||||
icon: icons[idx],
|
||||
dewpoint: temperatureConverter(dewpoint[idx]),
|
||||
}));
|
||||
};
|
||||
|
||||
@@ -233,7 +238,7 @@ const determineIcon = async (skyCover, weather, iceAccumulation, probabilityOfPr
|
||||
};
|
||||
|
||||
// expand a set of values with durations to an hour-by-hour array
|
||||
const expand = (data, maxHours = 24) => {
|
||||
const expand = (data, maxHours = 48) => {
|
||||
const startOfHour = DateTime.utc().startOf('hour').toMillis();
|
||||
const result = []; // resulting expanded values
|
||||
data.forEach((item) => {
|
||||
|
||||
@@ -13,7 +13,7 @@ const largeIcon = (link, _isNightTime) => {
|
||||
} catch (error) {
|
||||
console.warn(`largeIcon: ${error.message}`);
|
||||
// Return a fallback icon to prevent downstream errors
|
||||
return addPath(`No-Data.gif?${conditionIcon}${isNightTime ? '-n' : ''}`);
|
||||
return addPath(`No-Data-Large.gif?${conditionIcon}${isNightTime ? '-n' : ''}`);
|
||||
}
|
||||
|
||||
// find the icon
|
||||
@@ -102,6 +102,8 @@ const largeIcon = (link, _isNightTime) => {
|
||||
|
||||
case 'snow_fzra':
|
||||
case 'snow_fzra-n':
|
||||
case 'winter_mix':
|
||||
case 'winter_mix-n':
|
||||
return addPath('Freezing-Rain-Snow.gif');
|
||||
|
||||
case 'fzra':
|
||||
@@ -141,6 +143,8 @@ const largeIcon = (link, _isNightTime) => {
|
||||
return addPath('Thunderstorm.gif');
|
||||
|
||||
case 'wind_skc':
|
||||
case 'wind_':
|
||||
case 'wind_-n':
|
||||
return addPath('Windy.gif');
|
||||
|
||||
case 'wind_skc-n':
|
||||
@@ -169,7 +173,7 @@ const largeIcon = (link, _isNightTime) => {
|
||||
default: {
|
||||
console.warn(`Unknown weather condition '${conditionIcon}' from ${link}; using fallback icon`);
|
||||
// Return a reasonable fallback instead of false to prevent downstream errors
|
||||
return addPath(`No-Data.gif?${conditionIcon}${isNightTime ? '-n' : ''}`);
|
||||
return addPath(`No-Data-Large.gif?${conditionIcon}${isNightTime ? '-n' : ''}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -133,6 +133,7 @@ const smallIcon = (link, _isNightTime) => {
|
||||
|
||||
case 'wind_few':
|
||||
case 'wind_few-n':
|
||||
case 'wind_':
|
||||
return addPath('Wind.gif');
|
||||
|
||||
case 'wind_sct':
|
||||
@@ -170,7 +171,7 @@ const smallIcon = (link, _isNightTime) => {
|
||||
|
||||
case 'blizzard':
|
||||
case 'blizzard-n':
|
||||
return addPath('Blowing Snow.gif');
|
||||
return addPath('Blowing-Snow.gif');
|
||||
|
||||
default:
|
||||
console.warn(`Unknown weather condition '${conditionIcon}' from ${link}; using fallback icon`);
|
||||
|
||||
@@ -159,12 +159,17 @@ class LatestObservations extends WeatherDisplay {
|
||||
const windDirection = directionToNSEW(condition.windDirection.value);
|
||||
|
||||
const Temperature = temperatureConverter(condition.temperature.value);
|
||||
const Like = likeTemperature(condition.heatIndex?.value, condition.windChill?.value, Temperature, temperatureConverter);
|
||||
const WindSpeed = windConverter(condition.windSpeed.value);
|
||||
|
||||
const locationLimit = (settings.wide?.value && settings.enhancedScreens?.value) ? 20 : 14;
|
||||
const weatherLimit = (settings.wide?.value && settings.enhancedScreens?.value) ? 10 : 9;
|
||||
|
||||
const fill = {
|
||||
location: locationCleanup(condition.city).substr(0, 14),
|
||||
location: locationCleanup(condition.city).substr(0, locationLimit),
|
||||
temp: Temperature,
|
||||
weather: shortenCurrentConditions(condition.textDescription).substr(0, 9),
|
||||
like: Like.value,
|
||||
weather: shortenCurrentConditions(condition.textDescription).substr(0, weatherLimit),
|
||||
};
|
||||
|
||||
if (WindSpeed > 0) {
|
||||
@@ -175,7 +180,12 @@ class LatestObservations extends WeatherDisplay {
|
||||
fill.wind = 'Calm';
|
||||
}
|
||||
|
||||
return this.fillTemplate('observation-row', fill);
|
||||
const filledRow = this.fillTemplate('observation-row', fill);
|
||||
|
||||
// add the feels like class
|
||||
filledRow.querySelector('.like').classList.add(Like.cssClass);
|
||||
|
||||
return filledRow;
|
||||
});
|
||||
|
||||
const linesContainer = this.elem.querySelector('.observation-lines');
|
||||
@@ -186,6 +196,25 @@ class LatestObservations extends WeatherDisplay {
|
||||
}
|
||||
}
|
||||
|
||||
// generate a "feels like" temperature from heat index and wind chill.
|
||||
const likeTemperature = (heat, wind, actual, converter) => {
|
||||
// figure out the feels like value
|
||||
let value = '';
|
||||
if (heat) value = converter(heat);
|
||||
if (wind) value = converter(wind);
|
||||
|
||||
// determine if there's a red/blue color class to add
|
||||
let cssClass;
|
||||
if (value !== '') {
|
||||
if (value > actual) cssClass = 'heat-index';
|
||||
if (value < actual) cssClass = 'wind-chill';
|
||||
}
|
||||
return {
|
||||
value,
|
||||
cssClass,
|
||||
};
|
||||
};
|
||||
|
||||
const shortenCurrentConditions = (_condition) => {
|
||||
let condition = _condition;
|
||||
condition = condition.replace(/Light/, 'L');
|
||||
|
||||
@@ -6,6 +6,7 @@ import { safeJson } from './utils/fetch.mjs';
|
||||
import { getPoint } from './utils/weather.mjs';
|
||||
import { debugFlag } from './utils/debug.mjs';
|
||||
import settings from './settings.mjs';
|
||||
import { stationFilter } from './utils/string.mjs';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
init();
|
||||
@@ -37,6 +38,11 @@ const init = async () => {
|
||||
resizeTimeout = setTimeout(() => resize(), 100);
|
||||
});
|
||||
|
||||
// redraw current screen (typically from enhanced setting change)
|
||||
window.addEventListener('redraw', () => {
|
||||
currentDisplay()?.drawCanvas();
|
||||
});
|
||||
|
||||
// Handle orientation changes (Mobile Safari doesn't always fire resize events on orientation change)
|
||||
window.addEventListener('orientationchange', () => {
|
||||
if (debugFlag('resize')) {
|
||||
@@ -85,7 +91,15 @@ const getWeather = async (latLon, haveDataCallback) => {
|
||||
return;
|
||||
}
|
||||
|
||||
const StationId = stations.features[0].properties.stationIdentifier;
|
||||
// filter stations for proper format
|
||||
const stationsFiltered = stations.features.filter(stationFilter);
|
||||
// check for stations available after filtering
|
||||
if (stationsFiltered.length === 0) {
|
||||
console.warn('No observation stations left for location after filtering');
|
||||
return;
|
||||
}
|
||||
|
||||
const StationId = stationsFiltered[0].properties.stationIdentifier;
|
||||
|
||||
let { city } = point.properties.relativeLocation.properties;
|
||||
const { state } = point.properties.relativeLocation.properties;
|
||||
@@ -108,7 +122,7 @@ const getWeather = async (latLon, haveDataCallback) => {
|
||||
weatherParameters.timeZone = point.properties.timeZone;
|
||||
weatherParameters.forecast = point.properties.forecast;
|
||||
weatherParameters.forecastGridData = point.properties.forecastGridData;
|
||||
weatherParameters.stations = stations.features;
|
||||
weatherParameters.stations = stationsFiltered;
|
||||
weatherParameters.relativeLocation = point.properties.relativeLocation.properties;
|
||||
|
||||
// update the main process for display purposes
|
||||
|
||||
@@ -1,5 +1,56 @@
|
||||
import settings from './settings.mjs';
|
||||
|
||||
const radarFinalSize = () => {
|
||||
const size = {
|
||||
width: 640, height: 367,
|
||||
};
|
||||
if (settings.wide?.value && settings.enhancedScreens?.value) {
|
||||
size.width = 854;
|
||||
}
|
||||
return size;
|
||||
};
|
||||
|
||||
const radarSourceSize = () => {
|
||||
const size = {
|
||||
width: 240,
|
||||
height: 163,
|
||||
};
|
||||
if (settings.wide?.value && settings.enhancedScreens?.value) {
|
||||
size.width = 240 / 640 * 854; // original size of 640 scaled up to wide at 854
|
||||
}
|
||||
return size;
|
||||
};
|
||||
|
||||
const radarOffset = () => {
|
||||
const offset = {
|
||||
x: 240,
|
||||
y: 138,
|
||||
};
|
||||
if (settings.wide?.value && settings.enhancedScreens?.value) {
|
||||
// 107 is the margins shift, 640/854 is the scaling factor normal => wide, /2 is because of the fixed 2:1 scaling between source radar and map tiles
|
||||
offset.x = 240 + (107 * 640 / 854 / 2); // original size of 640 scaled up to wide at 854;
|
||||
}
|
||||
|
||||
return offset;
|
||||
};
|
||||
|
||||
// shift the base coordinates to align with enhanced radar window sizes
|
||||
const radarShift = () => {
|
||||
const shift = {
|
||||
x: 0,
|
||||
y: 0,
|
||||
};
|
||||
if (settings.wide?.value && settings.enhancedScreens?.value) {
|
||||
shift.x = 107;
|
||||
}
|
||||
return shift;
|
||||
};
|
||||
|
||||
export const TILE_SIZE = { x: 680, y: 387 };
|
||||
export const TILE_COUNT = { x: 10, y: 11 };
|
||||
export const TILE_FULL_SIZE = { x: 6800, y: 4255 };
|
||||
export const RADAR_FULL_SIZE = { width: 2550, height: 1600 };
|
||||
export const RADAR_FINAL_SIZE = { width: 640, height: 367 };
|
||||
export const RADAR_FINAL_SIZE = radarFinalSize;
|
||||
export const RADAR_SOURCE_SIZE = radarSourceSize;
|
||||
export const RADAR_OFFSET = radarOffset;
|
||||
export const RADAR_SHIFT = radarShift;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { removeDopplerRadarImageNoise } from './radar-utils.mjs';
|
||||
import { RADAR_FULL_SIZE, RADAR_FINAL_SIZE } from './radar-constants.mjs';
|
||||
import { RADAR_FULL_SIZE, RADAR_FINAL_SIZE, RADAR_SOURCE_SIZE } from './radar-constants.mjs';
|
||||
|
||||
// process a single radar image and place it on the provided canvas
|
||||
const processRadar = async (data) => {
|
||||
@@ -13,8 +13,8 @@ const processRadar = async (data) => {
|
||||
|
||||
// calculate offsets and sizes
|
||||
const radarSource = {
|
||||
width: 240,
|
||||
height: 163,
|
||||
width: RADAR_SOURCE_SIZE().width,
|
||||
height: RADAR_SOURCE_SIZE().height,
|
||||
x: Math.round(radarSourceXY.x / 2),
|
||||
y: Math.round(radarSourceXY.y / 2),
|
||||
};
|
||||
@@ -52,11 +52,11 @@ const processRadar = async (data) => {
|
||||
|
||||
// stretch the radar image
|
||||
const stretchCanvas = document.createElement('canvas');
|
||||
stretchCanvas.width = RADAR_FINAL_SIZE.width;
|
||||
stretchCanvas.height = RADAR_FINAL_SIZE.height;
|
||||
stretchCanvas.width = RADAR_FINAL_SIZE().width;
|
||||
stretchCanvas.height = RADAR_FINAL_SIZE().height;
|
||||
const stretchContext = stretchCanvas.getContext('2d', { willReadFrequently: true });
|
||||
stretchContext.imageSmoothingEnabled = false;
|
||||
stretchContext.drawImage(croppedRadarCanvas, 0, 0, radarSource.width, radarSource.height, 0, 0, RADAR_FINAL_SIZE.width, RADAR_FINAL_SIZE.height);
|
||||
stretchContext.drawImage(croppedRadarCanvas, 0, 0, radarSource.width, radarSource.height, 0, 0, RADAR_FINAL_SIZE().width, RADAR_FINAL_SIZE().height);
|
||||
|
||||
return stretchCanvas.toDataURL();
|
||||
};
|
||||
|
||||
@@ -9,10 +9,12 @@ const pixelToFile = (xPixel, yPixel) => {
|
||||
return `${yTile}-${xTile}`;
|
||||
};
|
||||
|
||||
// convert a pixel location in the overall map to a pixel location on the tile
|
||||
// convert a pixel location in the overall map to a pixel location on the tile set
|
||||
const modTile = (xPixel, yPixel) => {
|
||||
const x = Math.round(xPixel) % TILE_SIZE.x;
|
||||
const y = Math.round(yPixel) % TILE_SIZE.y;
|
||||
// adjust for additional 1 tile when odd
|
||||
const x = (Math.floor(xPixel) % (TILE_SIZE.x));
|
||||
const y = (Math.floor(yPixel) % (TILE_SIZE.y));
|
||||
|
||||
return { x, y };
|
||||
};
|
||||
|
||||
@@ -29,28 +31,33 @@ const setTiles = (data) => {
|
||||
|
||||
// determine the basemap images needed
|
||||
const baseMapTiles = [
|
||||
pixelToFile(sourceXY.x, sourceXY.y),
|
||||
pixelToFile(sourceXY.x + TILE_SIZE.x, sourceXY.y),
|
||||
pixelToFile(sourceXY.x, sourceXY.y + TILE_SIZE.y),
|
||||
pixelToFile(sourceXY.x + TILE_SIZE.x, sourceXY.y + TILE_SIZE.y),
|
||||
pixelToFile(sourceXY.x + TILE_SIZE.x * 0, sourceXY.y),
|
||||
pixelToFile(sourceXY.x + TILE_SIZE.x * 1, sourceXY.y),
|
||||
pixelToFile(sourceXY.x + TILE_SIZE.x * 2, sourceXY.y),
|
||||
pixelToFile(sourceXY.x + TILE_SIZE.x * 0, sourceXY.y + TILE_SIZE.y),
|
||||
pixelToFile(sourceXY.x + TILE_SIZE.x * 1, sourceXY.y + TILE_SIZE.y),
|
||||
pixelToFile(sourceXY.x + TILE_SIZE.x * 2, sourceXY.y + TILE_SIZE.y),
|
||||
];
|
||||
|
||||
// do some calculations
|
||||
// the tiles are arranged as follows, with the horizontal axis as x, and correlating with the second set of digits in the image file number
|
||||
// T[0] T[1]
|
||||
// T[2] T[3]
|
||||
// T[0] T[1] T[2]
|
||||
// T[3] T[4] T[5]
|
||||
|
||||
// calculate the shift of tile 0 (upper left)
|
||||
const tileShift = modTile(sourceXY.x, sourceXY.y);
|
||||
|
||||
// determine which tiles are used
|
||||
const secondRow = tileShift.y + TILE_SIZE.y > RADAR_FINAL_SIZE().height;
|
||||
const usedTiles = [
|
||||
true,
|
||||
TILE_SIZE.x - tileShift.x < RADAR_FINAL_SIZE.width,
|
||||
TILE_SIZE.y - tileShift.y < RADAR_FINAL_SIZE.width,
|
||||
tileShift.x + TILE_SIZE.x > RADAR_FINAL_SIZE().width,
|
||||
tileShift.x + (TILE_SIZE.x * 2) > RADAR_FINAL_SIZE().width,
|
||||
secondRow,
|
||||
];
|
||||
// if we need t[1] and t[2] then we also need t[3]
|
||||
usedTiles.push(usedTiles[1] && usedTiles[2]);
|
||||
// second row is a copy of the first row when in use
|
||||
// calculate T[4] and T[5]
|
||||
usedTiles.push(secondRow && usedTiles[1], secondRow && usedTiles[2]);
|
||||
|
||||
// helper function for populating tiles
|
||||
const populateTile = (tileName) => (elem, index) => {
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { TILE_SIZE, TILE_FULL_SIZE } from './radar-constants.mjs';
|
||||
import {
|
||||
TILE_SIZE, TILE_FULL_SIZE, RADAR_OFFSET, RADAR_SHIFT,
|
||||
} from './radar-constants.mjs';
|
||||
|
||||
// limit a value to within a range
|
||||
const coerce = (low, value, high) => Math.max(Math.min(value, high), low);
|
||||
@@ -9,16 +11,16 @@ const getXYFromLatitudeLongitudeMap = (pos) => {
|
||||
// 589 466 -122.3615246 47.63177832
|
||||
// 5288 3638 -80.18297384 25.77018996
|
||||
|
||||
// map position is calculated as a regresion from the above values (=/- a manual adjustment factor)
|
||||
// map position is calculated as a regresion from the above values (+/- a manual adjustment factor) and shifting for enhanced views
|
||||
// then shifted by half of the tile size (to center the map)
|
||||
// then they are limited to values between 0 and the width or height of the map
|
||||
const y = coerce(0, (-145.095 * pos.latitude + 7377.117) - 27 - (TILE_SIZE.y / 2), TILE_FULL_SIZE.y - (TILE_SIZE.y));
|
||||
const x = coerce(0, (111.407 * pos.longitude + 14220.972) + 4 - (TILE_SIZE.x / 2), TILE_FULL_SIZE.x - (TILE_SIZE.x));
|
||||
const y = coerce(0, (-145.095 * pos.latitude + 7377.117) - 27 - (TILE_SIZE.y / 2) - RADAR_SHIFT().y, TILE_FULL_SIZE.y - (TILE_SIZE.y));
|
||||
const x = coerce(0, (111.407 * pos.longitude + 14220.972) + 4 - (TILE_SIZE.x / 2) - RADAR_SHIFT().x, TILE_FULL_SIZE.x - (TILE_SIZE.x));
|
||||
|
||||
return { x, y };
|
||||
};
|
||||
|
||||
const getXYFromLatitudeLongitudeDoppler = (pos, offsetX, offsetY) => {
|
||||
const getXYFromLatitudeLongitudeDoppler = (pos) => {
|
||||
const imgHeight = 6000;
|
||||
const imgWidth = 2800;
|
||||
|
||||
@@ -26,8 +28,8 @@ const getXYFromLatitudeLongitudeDoppler = (pos, offsetX, offsetY) => {
|
||||
// then shifted by half of the tile size (to center the map)
|
||||
// then they are limited to values between 0 and the width or height of the map
|
||||
|
||||
const y = coerce(0, (51 - pos.latitude) * 61.4481 - offsetY, imgHeight);
|
||||
const x = coerce(0, ((-129.138 - pos.longitude) * 42.1768) * -1 - offsetX, imgWidth);
|
||||
const y = coerce(0, (51 - pos.latitude) * 61.4481 - RADAR_OFFSET().y, imgHeight);
|
||||
const x = coerce(0, ((-129.138 - pos.longitude) * 42.1768) * -1 - RADAR_OFFSET().x, imgWidth);
|
||||
|
||||
return { x: x * 2, y: y * 2 };
|
||||
};
|
||||
|
||||
@@ -128,10 +128,8 @@ class Radar extends WeatherDisplay {
|
||||
const urls = sortedPngs.slice(-(this.dopplerRadarImageMax));
|
||||
|
||||
// calculate offsets and sizes
|
||||
const offsetX = 120 * 2;
|
||||
const offsetY = 69 * 2;
|
||||
const sourceXY = utils.getXYFromLatitudeLongitudeMap(this.weatherParameters);
|
||||
const radarSourceXY = utils.getXYFromLatitudeLongitudeDoppler(this.weatherParameters, offsetX, offsetY);
|
||||
const radarSourceXY = utils.getXYFromLatitudeLongitudeDoppler(this.weatherParameters);
|
||||
|
||||
// set up the base map and overlay tiles
|
||||
setTiles({
|
||||
|
||||
@@ -23,7 +23,7 @@ const buildForecast = (forecast, city, cityXY) => {
|
||||
const getRegionalObservation = async (point, city) => {
|
||||
try {
|
||||
// get stations using centralized safe handling
|
||||
const stations = await safeJson(`https://api.weather.gov/gridpoints/${point.wfo}/${point.x},${point.y}/stations?limit=1`);
|
||||
const stations = await safeJson(`https://api.weather.gov/gridpoints/${point.wfo}/${point.x},${point.y}/stations?limit=10`);
|
||||
|
||||
if (!stations || !stations.features || stations.features.length === 0) {
|
||||
if (debugFlag('verbose-failures')) {
|
||||
@@ -32,9 +32,13 @@ const getRegionalObservation = async (point, city) => {
|
||||
return false;
|
||||
}
|
||||
|
||||
// get the first station
|
||||
const station = stations.features[0].id;
|
||||
const stationId = stations.features[0].properties.stationIdentifier;
|
||||
// get the first station with a 4-letter id (generally has appropriate data)
|
||||
const station4Letter = stations.features.find((station) => {
|
||||
if (station.properties.stationIdentifier.length === 4) return station.properties;
|
||||
return false;
|
||||
});
|
||||
const station = station4Letter.id;
|
||||
const stationId = station4Letter.properties.stationIdentifier;
|
||||
// get the observation data using centralized safe handling
|
||||
const observation = await safeJson(`${station}/observations/latest`);
|
||||
|
||||
@@ -205,7 +209,7 @@ const getMinMaxLatitudeLongitudeHI = (X, Y, OffsetX, OffsetY) => {
|
||||
};
|
||||
};
|
||||
|
||||
const getXYForCity = (City, MaxLatitude, MinLongitude, state) => {
|
||||
const getXYForCity = (City, MaxLatitude, MinLongitude, state, maxX = 580) => {
|
||||
if (state === 'AK') getXYForCityAK(City, MaxLatitude, MinLongitude);
|
||||
if (state === 'HI') getXYForCityHI(City, MaxLatitude, MinLongitude);
|
||||
let x = (City.lon - MinLongitude) * 57;
|
||||
@@ -215,7 +219,7 @@ const getXYForCity = (City, MaxLatitude, MinLongitude, state) => {
|
||||
if (y > 282) y = 282;
|
||||
|
||||
if (x < 40) x = 40;
|
||||
if (x > 580) x = 580;
|
||||
if (x > maxX) x = maxX;
|
||||
|
||||
return { x, y };
|
||||
};
|
||||
|
||||
@@ -14,11 +14,29 @@ import * as utils from './regionalforecast-utils.mjs';
|
||||
import { getPoint } from './utils/weather.mjs';
|
||||
import { debugFlag } from './utils/debug.mjs';
|
||||
import filterExpiredPeriods from './utils/forecast-utils.mjs';
|
||||
import settings from './settings.mjs';
|
||||
|
||||
// map offset
|
||||
const mapOffsetXY = {
|
||||
x: 240,
|
||||
y: 117,
|
||||
// set up spacing and scales
|
||||
const scaling = () => {
|
||||
// available space
|
||||
const available = {
|
||||
x: 640,
|
||||
};
|
||||
|
||||
// map offset
|
||||
const mapOffsetXY = {
|
||||
x: 240,
|
||||
y: 117,
|
||||
};
|
||||
|
||||
if (settings.wide?.value && settings.enhancedScreens?.value) {
|
||||
mapOffsetXY.x = 320;
|
||||
available.x = 854;
|
||||
}
|
||||
return {
|
||||
mapOffsetXY,
|
||||
available,
|
||||
};
|
||||
};
|
||||
|
||||
class RegionalForecast extends WeatherDisplay {
|
||||
@@ -45,13 +63,14 @@ class RegionalForecast extends WeatherDisplay {
|
||||
this.elem.querySelector('.map img').src = baseMap;
|
||||
|
||||
// get user's location in x/y
|
||||
const { available, mapOffsetXY } = scaling();
|
||||
const sourceXY = utils.getXYFromLatitudeLongitude(this.weatherParameters.latitude, this.weatherParameters.longitude, mapOffsetXY.x, mapOffsetXY.y, weatherParameters.state);
|
||||
|
||||
// get latitude and longitude limits
|
||||
const minMaxLatLon = utils.getMinMaxLatitudeLongitude(sourceXY.x, sourceXY.y, mapOffsetXY.x, mapOffsetXY.y, this.weatherParameters.state);
|
||||
|
||||
// get a target distance
|
||||
let targetDistance = 2.5;
|
||||
let targetDistance = 2.4;
|
||||
if (this.weatherParameters.state === 'HI') targetDistance = 1;
|
||||
|
||||
// make station info into an array
|
||||
@@ -102,7 +121,7 @@ class RegionalForecast extends WeatherDisplay {
|
||||
}
|
||||
|
||||
// get XY on map for city
|
||||
const cityXY = utils.getXYForCity(city, minMaxLatLon.maxLat, minMaxLatLon.minLon, this.weatherParameters.state);
|
||||
const cityXY = utils.getXYForCity(city, minMaxLatLon.maxLat, minMaxLatLon.minLon, this.weatherParameters.state, available - 60);
|
||||
|
||||
// wait for the regional observation if it's not done yet
|
||||
const observation = await observationPromise;
|
||||
@@ -188,7 +207,8 @@ class RegionalForecast extends WeatherDisplay {
|
||||
}
|
||||
|
||||
// draw the map
|
||||
const scale = 640 / (mapOffsetXY.x * 2);
|
||||
const { available, mapOffsetXY } = scaling();
|
||||
const scale = available.x / (mapOffsetXY.x * 2);
|
||||
const map = this.elem.querySelector('.map');
|
||||
map.style.transform = `scale(${scale}) translate(-${sourceXY.x}px, -${sourceXY.y}px)`;
|
||||
|
||||
|
||||
@@ -32,6 +32,25 @@ const wideScreenChange = (value) => {
|
||||
window.dispatchEvent(new Event('resize'));
|
||||
};
|
||||
|
||||
const enhancedScreenChange = (value) => {
|
||||
const container = document.querySelector('#divTwc');
|
||||
if (!container) {
|
||||
// DOM not ready; defer enabling if set
|
||||
if (value) {
|
||||
deferredDomSettings.add('enhancedScreens');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (value) {
|
||||
container.classList.add('enhanced');
|
||||
} else {
|
||||
container.classList.remove('enhanced');
|
||||
}
|
||||
// Trigger resize to recalculate scaling for new width
|
||||
window.dispatchEvent(new Event('redraw'));
|
||||
};
|
||||
|
||||
const kioskChange = (value) => {
|
||||
const body = document.querySelector('body');
|
||||
if (!body) {
|
||||
@@ -130,6 +149,17 @@ const init = () => {
|
||||
changeAction: wideScreenChange,
|
||||
sticky: true,
|
||||
});
|
||||
settings.portrait = new Setting('portrait', {
|
||||
name: 'Allow Portrait',
|
||||
defaultValue: false,
|
||||
sticky: true,
|
||||
});
|
||||
settings.enhancedScreens = new Setting('enhancedScreens', {
|
||||
name: 'Enhanced Screens',
|
||||
defaultValue: false,
|
||||
changeAction: enhancedScreenChange,
|
||||
sticky: true,
|
||||
});
|
||||
settings.kiosk = new Setting('kiosk', {
|
||||
name: 'Kiosk',
|
||||
defaultValue: false,
|
||||
|
||||
@@ -13,7 +13,12 @@ const locationCleanup = (input) => {
|
||||
return regexes.reduce((value, regex) => value.replace(regex, ''), input);
|
||||
};
|
||||
|
||||
// stations must be 4 alpha characters and not start with the provided list
|
||||
const skipStations = ['U', 'C', 'H', 'W', 'Y', 'T', 'S', 'M', 'O', 'L', 'A', 'F', 'B', 'N', 'V', 'R', 'D', 'E', 'I', 'G', 'J'];
|
||||
const stationFilter = (station) => station.properties.stationIdentifier.match(/^[A-Z]{4}$/) && !skipStations.includes(station.properties.stationIdentifier.slice(0, 1));
|
||||
|
||||
export {
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
|
||||
locationCleanup,
|
||||
stationFilter,
|
||||
};
|
||||
|
||||
@@ -172,6 +172,7 @@ class WeatherDisplay {
|
||||
if (this.screenIndex < 0) this.screenIndex = 0;
|
||||
if (this.okToDrawCurrentDateTime) this.drawCurrentDateTime();
|
||||
if (this.okToDrawCurrentConditions) postMessage({ type: 'current-weather-scroll', method: 'start' });
|
||||
if (!this.okToDrawCurrentConditions) postMessage({ type: 'current-weather-scroll', method: 'non-display' });
|
||||
if (this.okToDrawCurrentConditions === false) postMessage({ type: 'current-weather-scroll', method: 'hide' });
|
||||
}
|
||||
|
||||
|
||||
@@ -346,7 +346,7 @@ var TimeIndicator;
|
||||
TimeIndicator["TL"] = "TL";
|
||||
})(TimeIndicator || (TimeIndicator = {}));
|
||||
/**
|
||||
* https://www.aviationweather.gov/taf/decoder
|
||||
* https://web.archive.org/web/20230318235549/https://aviationweather.gov/taf/decoder
|
||||
*/
|
||||
var WeatherChangeType;
|
||||
(function (WeatherChangeType) {
|
||||
@@ -2535,7 +2535,8 @@ class MetarParser extends AbstractParser {
|
||||
while (i < trendParts.length &&
|
||||
trendParts[i] !== this.TEMPO &&
|
||||
trendParts[i] !== this.INTER &&
|
||||
trendParts[i] !== this.BECMG) {
|
||||
trendParts[i] !== this.BECMG &&
|
||||
trendParts[i] !== this.RMK) {
|
||||
if (trendParts[i].startsWith(this.FM) ||
|
||||
trendParts[i].startsWith(this.TL) ||
|
||||
trendParts[i].startsWith(this.AT)) {
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,8 +1,13 @@
|
||||
@use 'shared/_colors' as c;
|
||||
@use 'shared/_utils' as u;
|
||||
@use 'shared/_colors'as c;
|
||||
@use 'shared/_utils'as u;
|
||||
|
||||
#almanac-html.weather-display {
|
||||
background-image: url('../images/backgrounds/3.png');
|
||||
|
||||
// repeat the background if wide-enhanced
|
||||
.wide.enhanced & {
|
||||
background-image: url('../images/backgrounds/3-wide-enhanced.png');
|
||||
}
|
||||
}
|
||||
|
||||
.weather-display .main.almanac {
|
||||
@@ -14,13 +19,17 @@
|
||||
// Use CSS Grid for cross-browser consistency
|
||||
// Grid is populated in reading order (left-to-right, top-to-bottom):
|
||||
display: grid;
|
||||
grid-template-columns: auto auto auto;
|
||||
grid-template-rows: auto auto auto;
|
||||
grid-template-columns: repeat(3, auto);
|
||||
grid-template-rows: repeat(3, auto);
|
||||
gap: 0px 90px;
|
||||
margin: 3px auto 5px auto; // align the bottom of the div with the background
|
||||
width: fit-content;
|
||||
line-height: 30px;
|
||||
|
||||
.wide.enhanced & {
|
||||
grid-template-columns: repeat(4, auto);
|
||||
}
|
||||
|
||||
.grid-item {
|
||||
// Reset inherited styles that interfere with grid layout
|
||||
width: auto;
|
||||
@@ -45,6 +54,14 @@
|
||||
&.time {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
&.wide-enhanced {
|
||||
display: none;
|
||||
|
||||
.wide.enhanced & {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,6 +75,10 @@
|
||||
padding-left: 13px;
|
||||
}
|
||||
|
||||
.days {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.day {
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
@@ -77,4 +98,4 @@
|
||||
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
@use 'shared/_colors' as c;
|
||||
@use 'shared/_utils' as u;
|
||||
@use 'shared/_colors'as c;
|
||||
@use 'shared/_utils'as u;
|
||||
@use 'shared/positions'as p;
|
||||
|
||||
.weather-display .main.current-weather {
|
||||
&.main {
|
||||
width: calc(p.$standard-width - (2 * p.$blue-box-margin));
|
||||
|
||||
.col {
|
||||
height: 50px;
|
||||
@@ -12,12 +14,17 @@
|
||||
padding-top: 10px;
|
||||
position: absolute;
|
||||
|
||||
.wide.enhanced & {
|
||||
width: 300px;
|
||||
margin-left: 25px;
|
||||
margin-right: 25px;
|
||||
}
|
||||
|
||||
@include u.text-shadow();
|
||||
|
||||
&.left {
|
||||
font-family: 'Star4000 Extended';
|
||||
font-size: 24pt;
|
||||
|
||||
}
|
||||
|
||||
&.right {
|
||||
@@ -92,4 +99,4 @@
|
||||
text-wrap: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
@use 'shared/_colors'as c;
|
||||
@use 'shared/_utils'as u;
|
||||
@use 'shared/positions'as p;
|
||||
|
||||
#hazards-html.weather-display {
|
||||
background-image: url('../images/backgrounds/7.png');
|
||||
@@ -8,7 +9,7 @@
|
||||
.weather-display .main.hazards {
|
||||
&.main {
|
||||
overflow-y: hidden;
|
||||
height: 480px;
|
||||
height: p.$standard-height;
|
||||
background-color: rgb(112, 35, 35);
|
||||
|
||||
|
||||
|
||||
@@ -4,6 +4,12 @@
|
||||
#hourly-graph-html {
|
||||
background-image: url(../images/backgrounds/1-chart.png);
|
||||
|
||||
// change background for wide-enhanced
|
||||
.wide.enhanced & {
|
||||
background-image: url(../images/backgrounds/1-chart-wide.png);
|
||||
background-position-x: 0px;
|
||||
}
|
||||
|
||||
.header {
|
||||
.right {
|
||||
position: absolute;
|
||||
@@ -11,7 +17,7 @@
|
||||
right: 60px;
|
||||
width: 360px;
|
||||
font-family: 'Star4000 Small';
|
||||
font-size: 32px;
|
||||
font-size: 28px;
|
||||
@include u.text-shadow();
|
||||
text-align: right;
|
||||
|
||||
@@ -23,6 +29,10 @@
|
||||
color: red;
|
||||
}
|
||||
|
||||
.dewpoint {
|
||||
color: green;
|
||||
}
|
||||
|
||||
.cloud {
|
||||
color: lightgrey;
|
||||
}
|
||||
@@ -52,37 +62,79 @@
|
||||
|
||||
.x-axis {
|
||||
bottom: 0px;
|
||||
left: 0px;
|
||||
width: 640px;
|
||||
left: 54px;
|
||||
width: 532px;
|
||||
height: 20px;
|
||||
|
||||
.label {
|
||||
text-align: center;
|
||||
width: 50px;
|
||||
transform: translateX(-50%);
|
||||
white-space: nowrap;
|
||||
|
||||
&.l-1 {
|
||||
left: 25px;
|
||||
left: 0px;
|
||||
}
|
||||
|
||||
&.l-2 {
|
||||
left: 158px;
|
||||
left: calc(532px / 4 * 1);
|
||||
}
|
||||
|
||||
&.l-3 {
|
||||
left: 291px;
|
||||
left: calc(532px / 4 * 2);
|
||||
}
|
||||
|
||||
&.l-4 {
|
||||
left: 424px;
|
||||
left: calc(532px / 4 * 3);
|
||||
}
|
||||
|
||||
&.l-5 {
|
||||
left: 557px;
|
||||
left: calc(532px / 4 * 4);
|
||||
}
|
||||
|
||||
// adjust when enhanced
|
||||
.wide.enhanced & {
|
||||
|
||||
&.l-1 {
|
||||
left: 0px;
|
||||
}
|
||||
|
||||
&.l-2 {
|
||||
left: calc(726px / 6 * 1);
|
||||
}
|
||||
|
||||
&.l-3 {
|
||||
left: calc(726px / 6 * 2);
|
||||
}
|
||||
|
||||
&.l-4 {
|
||||
left: calc(726px / 6 * 3);
|
||||
}
|
||||
|
||||
&.l-5 {
|
||||
left: calc(726px / 6 * 4);
|
||||
}
|
||||
|
||||
&.l-6 {
|
||||
left: calc(726px / 6 * 5);
|
||||
}
|
||||
|
||||
&.l-7 {
|
||||
left: calc(726px / 6 * 6);
|
||||
}
|
||||
}
|
||||
|
||||
// only in wide + enhanced
|
||||
&.l-6,
|
||||
&.l-7 {
|
||||
display: none;
|
||||
|
||||
.wide.enhanced & {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
.chart {
|
||||
@@ -92,6 +144,11 @@
|
||||
img {
|
||||
width: 532px;
|
||||
height: 285px;
|
||||
|
||||
// wide and enhanced
|
||||
.wide.enhanced & {
|
||||
width: 746px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,41 +167,18 @@
|
||||
}
|
||||
|
||||
&.l-2 {
|
||||
top: 140px;
|
||||
top: calc(280px / 3);
|
||||
}
|
||||
|
||||
&.l-3 {
|
||||
bottom: calc(280px / 3 - 11px);
|
||||
}
|
||||
|
||||
&.l-4 {
|
||||
bottom: 0px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.column-headers {
|
||||
background-color: c.$column-header;
|
||||
height: 20px;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.column-headers {
|
||||
position: sticky;
|
||||
top: 0px;
|
||||
z-index: 5;
|
||||
|
||||
|
||||
.temp {
|
||||
left: 355px;
|
||||
}
|
||||
|
||||
.like {
|
||||
left: 435px;
|
||||
}
|
||||
|
||||
.wind {
|
||||
left: 535px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@@ -84,11 +84,11 @@
|
||||
left: 425px;
|
||||
|
||||
&.heat-index {
|
||||
color: #e00;
|
||||
color: c.$heat-index;
|
||||
}
|
||||
|
||||
&.wind-chill {
|
||||
color: c.$extended-low;
|
||||
color: c.$wind-chill;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -46,6 +46,39 @@
|
||||
left: 430px;
|
||||
}
|
||||
|
||||
.like {
|
||||
display: none;
|
||||
}
|
||||
|
||||
// wide and enhanced moves the columns and enables the like column
|
||||
.wide.enhanced & {
|
||||
.temp {
|
||||
left: 320px;
|
||||
}
|
||||
|
||||
.like {
|
||||
left: 380px;
|
||||
display: block;
|
||||
|
||||
&.heat-index {
|
||||
color: c.$heat-index;
|
||||
}
|
||||
|
||||
&.wind-chill {
|
||||
color: c.$wind-chill;
|
||||
}
|
||||
}
|
||||
|
||||
.weather {
|
||||
left: 470px;
|
||||
}
|
||||
|
||||
.wind {
|
||||
left: 630px;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.observation-lines {
|
||||
min-height: 338px;
|
||||
padding-top: 10px;
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
@use 'shared/_colors'as c;
|
||||
@use 'shared/_utils'as u;
|
||||
@use 'shared/positions'as p;
|
||||
|
||||
.weather-display .local-forecast {
|
||||
|
||||
// clamp width to standard
|
||||
&.main {
|
||||
width: calc(p.$standard-width - (2 * p.$blue-box-margin));
|
||||
}
|
||||
|
||||
.container {
|
||||
position: relative;
|
||||
top: 15px;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
@use 'shared/_utils'as u;
|
||||
@use 'shared/_colors'as c;
|
||||
@use 'shared/positions'as p;
|
||||
|
||||
@font-face {
|
||||
font-family: "Star4000";
|
||||
@@ -33,7 +34,7 @@ body {
|
||||
}
|
||||
|
||||
#divQuery {
|
||||
max-width: 640px;
|
||||
max-width: p.$standard-width;
|
||||
padding: 8px;
|
||||
|
||||
.buttons {
|
||||
@@ -146,11 +147,11 @@ body {
|
||||
background-color: #000000;
|
||||
color: #ffffff;
|
||||
width: 100%;
|
||||
max-width: 640px;
|
||||
max-width: p.$standard-width;
|
||||
margin: 0; // Ensure edge-to-edge display
|
||||
|
||||
&.wide {
|
||||
max-width: 854px;
|
||||
max-width: p.$wide-width;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -159,12 +160,12 @@ body {
|
||||
}
|
||||
|
||||
#divTwcMain {
|
||||
width: 640px;
|
||||
height: 480px;
|
||||
width: p.$standard-width;
|
||||
height: p.$standard-height;
|
||||
position: relative;
|
||||
|
||||
.wide & {
|
||||
width: 854px;
|
||||
width: p.$wide-width;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -209,10 +210,10 @@ body {
|
||||
background-color: #000000;
|
||||
|
||||
color: #ffffff;
|
||||
width: 640px;
|
||||
width: p.$standard-width;
|
||||
|
||||
.wide & {
|
||||
width: 854px;
|
||||
width: p.$wide-width;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
@@ -274,7 +275,7 @@ body {
|
||||
flex-direction: row;
|
||||
background-color: #000000;
|
||||
color: #ffffff;
|
||||
max-width: 640px;
|
||||
max-width: p.$standard-width;
|
||||
}
|
||||
|
||||
#divTwcNav>div {
|
||||
@@ -336,8 +337,8 @@ body {
|
||||
|
||||
#container {
|
||||
position: relative;
|
||||
width: 640px;
|
||||
height: 480px;
|
||||
width: p.$standard-width;
|
||||
height: p.$standard-height;
|
||||
// overflow: hidden;
|
||||
background-image: url(../images/backgrounds/1.png);
|
||||
transform-origin: 0 0;
|
||||
@@ -345,8 +346,7 @@ body {
|
||||
}
|
||||
|
||||
.wide #container {
|
||||
padding-left: 107px;
|
||||
padding-right: 107px;
|
||||
width: p.$wide-width;
|
||||
background: url(../images/backgrounds/1-wide.png);
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
@@ -359,8 +359,8 @@ body {
|
||||
}
|
||||
|
||||
#loading {
|
||||
width: 640px;
|
||||
height: 480px;
|
||||
width: p.$standard-width;
|
||||
height: p.$standard-height;
|
||||
max-width: 100%;
|
||||
text-shadow: 4px 4px black;
|
||||
display: flex;
|
||||
@@ -368,6 +368,10 @@ body {
|
||||
text-align: center;
|
||||
justify-content: center;
|
||||
|
||||
.wide & {
|
||||
margin-left: p.$wide-margin;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-family: Star4000 Large;
|
||||
font-size: 36px;
|
||||
|
||||
@@ -1,17 +1,23 @@
|
||||
@use 'shared/_colors' as c;
|
||||
@use 'shared/_utils' as u;
|
||||
@use 'shared/_colors'as c;
|
||||
@use 'shared/_utils'as u;
|
||||
@use 'shared/positions'as p;
|
||||
|
||||
.weather-display .progress {
|
||||
@include u.text-shadow();
|
||||
font-family: 'Star4000 Extended';
|
||||
font-size: 19pt;
|
||||
|
||||
// clamp width to standard
|
||||
&.main {
|
||||
width: calc(p.$standard-width - (2 * p.$blue-box-margin));
|
||||
}
|
||||
|
||||
.container {
|
||||
position: relative;
|
||||
top: 15px;
|
||||
margin: 0px 10px;
|
||||
box-sizing: border-box;
|
||||
height: 310px;
|
||||
height: p.$standard-scroll-height;
|
||||
overflow: hidden;
|
||||
line-height: 28px;
|
||||
|
||||
@@ -118,4 +124,4 @@
|
||||
transition: width 1s steps(6);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,14 @@
|
||||
@use 'shared/_colors'as c;
|
||||
@use 'shared/_utils'as u;
|
||||
@use 'shared/positions'as p;
|
||||
|
||||
#radar-html.weather-display {
|
||||
background-image: url('../images/backgrounds/4.png');
|
||||
|
||||
.wide & {
|
||||
background: url(../images/backgrounds/4-wide.png);
|
||||
}
|
||||
|
||||
.header {
|
||||
height: 83px;
|
||||
|
||||
@@ -104,12 +109,13 @@
|
||||
.weather-display .main.radar {
|
||||
overflow: hidden;
|
||||
height: 367px;
|
||||
width: p.$standard-width;
|
||||
|
||||
.container {
|
||||
|
||||
.tiles {
|
||||
position: absolute;
|
||||
width: 1400px;
|
||||
width: 2040px;
|
||||
|
||||
img {
|
||||
vertical-align: middle;
|
||||
@@ -120,8 +126,4 @@
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.wide.radar #container {
|
||||
background: url(../images/backgrounds/4-wide.png);
|
||||
}
|
||||
@@ -1,10 +1,6 @@
|
||||
@use 'shared/_colors'as c;
|
||||
@use 'shared/_utils'as u;
|
||||
|
||||
#regional-forecast-html.weather-display {
|
||||
background-image: url('../images/backgrounds/5.png');
|
||||
}
|
||||
|
||||
.weather-display .main.regional-forecast {
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
@use 'shared/_colors'as c;
|
||||
@use 'shared/_utils'as u;
|
||||
@use 'shared/positions'as p;
|
||||
|
||||
#spc-outlook-html.weather-display {
|
||||
background-image: url('../images/backgrounds/6.png');
|
||||
|
||||
@@ -1,29 +1,50 @@
|
||||
@use 'shared/_colors'as c;
|
||||
@use 'shared/_utils'as u;
|
||||
@use 'shared/positions'as p;
|
||||
|
||||
.weather-display {
|
||||
width: 640px;
|
||||
height: 480px;
|
||||
width: p.$standard-width;
|
||||
height: p.$standard-height;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
background-image: url(../images/backgrounds/1.png);
|
||||
|
||||
// adjust for wide
|
||||
.wide & {
|
||||
width: p.$wide-width;
|
||||
background-position-x: p.$wide-margin;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.wide.enhanced & {
|
||||
&:has(.can-enhance) {
|
||||
background-image: url(../images/backgrounds/1-wide-enhanced.png);
|
||||
background-position-x: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
/* this method is required to hide blocks so they can be measured while off screen */
|
||||
height: 0px;
|
||||
|
||||
&.show {
|
||||
height: 480px;
|
||||
height: p.$standard-height;
|
||||
}
|
||||
|
||||
.template {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.header {
|
||||
width: 640px;
|
||||
>.header {
|
||||
width: p.$standard-width;
|
||||
height: 60px;
|
||||
position: relative;
|
||||
padding-top: 30px;
|
||||
|
||||
// adjust for wide
|
||||
.wide & {
|
||||
left: p.$wide-margin;
|
||||
}
|
||||
|
||||
.title {
|
||||
color: c.$title-color;
|
||||
@include u.text-shadow(3px, 1.5px);
|
||||
@@ -92,10 +113,23 @@
|
||||
.main {
|
||||
position: relative;
|
||||
|
||||
// adjust for wide
|
||||
.wide & {
|
||||
left: p.$wide-margin;
|
||||
}
|
||||
|
||||
// adjust for enhanced when possible
|
||||
.wide.enhanced & {
|
||||
&.can-enhance {
|
||||
left: 0px;
|
||||
width: p.$wide-width;
|
||||
}
|
||||
}
|
||||
|
||||
&.has-scroll {
|
||||
width: 640px;
|
||||
width: p.$standard-width;
|
||||
margin-top: 0;
|
||||
height: 310px;
|
||||
height: p.$standard-scroll-height;
|
||||
overflow: hidden;
|
||||
|
||||
&.no-header {
|
||||
@@ -105,9 +139,15 @@
|
||||
}
|
||||
|
||||
&.has-box {
|
||||
margin-left: 64px;
|
||||
margin-right: 64px;
|
||||
margin-left: p.$blue-box-margin;
|
||||
margin-right: p.$blue-box-margin;
|
||||
width: calc(100% - 128px);
|
||||
|
||||
.wide.enhanced & {
|
||||
&.can-enhance {
|
||||
width: calc(p.$wide-width - p.$blue-box-margin - p.$blue-box-margin)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -117,7 +157,7 @@
|
||||
#container>.scroll {
|
||||
display: none;
|
||||
@include u.text-shadow(3px, 1.5px);
|
||||
width: 640px;
|
||||
width: p.$standard-width;
|
||||
height: 77px;
|
||||
overflow: hidden;
|
||||
margin-top: 3px;
|
||||
@@ -125,12 +165,17 @@
|
||||
bottom: 0px;
|
||||
z-index: 1;
|
||||
|
||||
// adjust for wide
|
||||
.wide & {
|
||||
left: p.$wide-margin;
|
||||
}
|
||||
|
||||
&.hazard {
|
||||
background-color: rgb(112, 35, 35);
|
||||
}
|
||||
|
||||
.scroll-container {
|
||||
width: 640px;
|
||||
width: p.$standard-width;
|
||||
|
||||
.fixed,
|
||||
.scroll-header {
|
||||
@@ -156,7 +201,7 @@
|
||||
position: relative;
|
||||
// the following added by js code as it is dependent on the content of the element
|
||||
// transition: left (x)s;
|
||||
// left: calc((elem width) - 640px);
|
||||
// left: calc((elem width) - p.$standard-width);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -166,10 +211,10 @@
|
||||
}
|
||||
|
||||
.wide #container>.scroll {
|
||||
width: 854px;
|
||||
margin-left: -107px;
|
||||
width: p.$wide-width;
|
||||
margin-left: -1*p.$wide-margin;
|
||||
|
||||
.scroll-container {
|
||||
margin-left: 107px;
|
||||
margin-left: p.$wide-margin;
|
||||
}
|
||||
}
|
||||
@@ -13,5 +13,7 @@ $gradient-loading-3: #4f99f9;
|
||||
$gradient-loading-4: #8ffdfa;
|
||||
|
||||
$extended-low: #8080FF;
|
||||
$wind-chill: #8080FF;
|
||||
$heat-index: #e00;
|
||||
|
||||
$blue-box: #26235a;
|
||||
14
server/styles/scss/shared/_positions.scss
Normal file
14
server/styles/scss/shared/_positions.scss
Normal file
@@ -0,0 +1,14 @@
|
||||
// standard positioning
|
||||
$standard-width: 640px;
|
||||
$standard-height: 480px;
|
||||
|
||||
// height with scroll
|
||||
$standard-scroll-height: 310px;
|
||||
|
||||
// blue box size
|
||||
$blue-box-margin: 64px;
|
||||
|
||||
// wide screen positioning
|
||||
$wide-padding: 107px;
|
||||
$wide-margin: 107px;
|
||||
$wide-width: 854px;
|
||||
1
server/styles/ws.min.css
vendored
Normal file
1
server/styles/ws.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
server/styles/ws.min.css.map
Normal file
1
server/styles/ws.min.css.map
Normal file
File diff suppressed because one or more lines are too long
327
tests/package-lock.json
generated
327
tests/package-lock.json
generated
@@ -14,12 +14,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/code-frame": {
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
|
||||
"integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==",
|
||||
"version": "7.29.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz",
|
||||
"integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-validator-identifier": "^7.27.1",
|
||||
"@babel/helper-validator-identifier": "^7.28.5",
|
||||
"js-tokens": "^4.0.0",
|
||||
"picocolors": "^1.1.1"
|
||||
},
|
||||
@@ -28,26 +28,26 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-validator-identifier": {
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz",
|
||||
"integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==",
|
||||
"version": "7.28.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
|
||||
"integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@puppeteer/browsers": {
|
||||
"version": "2.10.5",
|
||||
"resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.10.5.tgz",
|
||||
"integrity": "sha512-eifa0o+i8dERnngJwKrfp3dEq7ia5XFyoqB17S4gK8GhsQE4/P8nxOfQSE0zQHxzzLo/cmF+7+ywEQ7wK7Fb+w==",
|
||||
"version": "2.13.0",
|
||||
"resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.13.0.tgz",
|
||||
"integrity": "sha512-46BZJYJjc/WwmKjsvDFykHtXrtomsCIrwYQPOP7VfMJoZY2bsDF9oROBABR3paDjDcmkUye1Pb1BqdcdiipaWA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"debug": "^4.4.1",
|
||||
"debug": "^4.4.3",
|
||||
"extract-zip": "^2.0.1",
|
||||
"progress": "^2.0.3",
|
||||
"proxy-agent": "^6.5.0",
|
||||
"semver": "^7.7.2",
|
||||
"tar-fs": "^3.0.8",
|
||||
"semver": "^7.7.4",
|
||||
"tar-fs": "^3.1.1",
|
||||
"yargs": "^17.7.2"
|
||||
},
|
||||
"bin": {
|
||||
@@ -64,13 +64,13 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "22.15.29",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.29.tgz",
|
||||
"integrity": "sha512-LNdjOkUDlU1RZb8e1kOIUpN1qQUlzGkEtbVNo53vbrwDg5om6oduhm4SiUaPW5ASTXhAiP0jInWG8Qx9fVlOeQ==",
|
||||
"version": "25.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.0.tgz",
|
||||
"integrity": "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~6.21.0"
|
||||
"undici-types": "~7.18.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/yauzl": {
|
||||
@@ -84,9 +84,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/agent-base": {
|
||||
"version": "7.1.3",
|
||||
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz",
|
||||
"integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==",
|
||||
"version": "7.1.4",
|
||||
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
|
||||
"integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 14"
|
||||
@@ -135,28 +135,44 @@
|
||||
}
|
||||
},
|
||||
"node_modules/b4a": {
|
||||
"version": "1.6.7",
|
||||
"resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.7.tgz",
|
||||
"integrity": "sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg==",
|
||||
"license": "Apache-2.0"
|
||||
"version": "1.8.0",
|
||||
"resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.0.tgz",
|
||||
"integrity": "sha512-qRuSmNSkGQaHwNbM7J78Wwy+ghLEYF1zNrSeMxj4Kgw6y33O3mXcQ6Ie9fRvfU/YnxWkOchPXbaLb73TkIsfdg==",
|
||||
"license": "Apache-2.0",
|
||||
"peerDependencies": {
|
||||
"react-native-b4a": "*"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"react-native-b4a": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/bare-events": {
|
||||
"version": "2.5.4",
|
||||
"resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.5.4.tgz",
|
||||
"integrity": "sha512-+gFfDkR8pj4/TrWCGUGWmJIkBwuxPS5F+a5yWjOHQt2hHvNZd5YLzadjmDUtFmMM4y429bnKLa8bYBMHcYdnQA==",
|
||||
"version": "2.8.2",
|
||||
"resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.2.tgz",
|
||||
"integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==",
|
||||
"license": "Apache-2.0",
|
||||
"optional": true
|
||||
"peerDependencies": {
|
||||
"bare-abort-controller": "*"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bare-abort-controller": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/bare-fs": {
|
||||
"version": "4.1.5",
|
||||
"resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.1.5.tgz",
|
||||
"integrity": "sha512-1zccWBMypln0jEE05LzZt+V/8y8AQsQQqxtklqaIyg5nu6OAYFhZxPXinJTSG+kU5qyNmeLgcn9AW7eHiCHVLA==",
|
||||
"version": "4.5.6",
|
||||
"resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.5.6.tgz",
|
||||
"integrity": "sha512-1QovqDrR80Pmt5HPAsMsXTCFcDYr+NSUKW6nd6WO5v0JBmnItc/irNRzm2KOQ5oZ69P37y+AMujNyNtG+1Rggw==",
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"bare-events": "^2.5.4",
|
||||
"bare-path": "^3.0.0",
|
||||
"bare-stream": "^2.6.4"
|
||||
"bare-stream": "^2.6.4",
|
||||
"bare-url": "^2.2.2",
|
||||
"fast-fifo": "^1.3.2"
|
||||
},
|
||||
"engines": {
|
||||
"bare": ">=1.16.0"
|
||||
@@ -171,11 +187,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/bare-os": {
|
||||
"version": "3.6.1",
|
||||
"resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.6.1.tgz",
|
||||
"integrity": "sha512-uaIjxokhFidJP+bmmvKSgiMzj2sV5GPHaZVAIktcxcpCyBFFWO+YlikVAdhmUo2vYFvFhOXIAlldqV29L8126g==",
|
||||
"version": "3.8.0",
|
||||
"resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.8.0.tgz",
|
||||
"integrity": "sha512-Dc9/SlwfxkXIGYhvMQNUtKaXCaGkZYGcd1vuNUUADVqzu4/vQfvnMkYYOUnt2VwQ2AqKr/8qAVFRtwETljgeFg==",
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"bare": ">=1.14.0"
|
||||
}
|
||||
@@ -185,25 +200,28 @@
|
||||
"resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz",
|
||||
"integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==",
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"bare-os": "^3.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/bare-stream": {
|
||||
"version": "2.6.5",
|
||||
"resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.6.5.tgz",
|
||||
"integrity": "sha512-jSmxKJNJmHySi6hC42zlZnq00rga4jjxcgNZjY9N5WlOe/iOoGRtdwGsHzQv2RlH2KOYMwGUXhf2zXd32BA9RA==",
|
||||
"version": "2.11.0",
|
||||
"resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.11.0.tgz",
|
||||
"integrity": "sha512-Y/+iQ49fL3rIn6w/AVxI/2+BRrpmzJvdWt5Jv8Za6Ngqc6V227c+pYjYYgLdpR3MwQ9ObVXD0ZrqoBztakM0rw==",
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"streamx": "^2.21.0"
|
||||
"streamx": "^2.25.0",
|
||||
"teex": "^1.0.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bare-abort-controller": "*",
|
||||
"bare-buffer": "*",
|
||||
"bare-events": "*"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bare-abort-controller": {
|
||||
"optional": true
|
||||
},
|
||||
"bare-buffer": {
|
||||
"optional": true
|
||||
},
|
||||
@@ -212,10 +230,19 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/bare-url": {
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.4.0.tgz",
|
||||
"integrity": "sha512-NSTU5WN+fy/L0DDenfE8SXQna4voXuW0FHM7wH8i3/q9khUSchfPbPezO4zSFMnDGIf9YE+mt/RWhZgNRKRIXA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"bare-path": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/basic-ftp": {
|
||||
"version": "5.0.5",
|
||||
"resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.0.5.tgz",
|
||||
"integrity": "sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg==",
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.2.0.tgz",
|
||||
"integrity": "sha512-VoMINM2rqJwJgfdHq6RiUudKt2BV+FY5ZFezP/ypmwayk68+NzzAQy4XXLlqsGD4MCzq3DrmNFD/uUmBJuGoXw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
@@ -240,9 +267,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/chalk": {
|
||||
"version": "5.4.1",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz",
|
||||
"integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==",
|
||||
"version": "5.6.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz",
|
||||
"integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^12.17.0 || ^14.13 || >=16.0.0"
|
||||
@@ -252,9 +279,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/chromium-bidi": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-5.1.0.tgz",
|
||||
"integrity": "sha512-9MSRhWRVoRPDG0TgzkHrshFSJJNZzfY5UFqUMuksg7zL1yoZIZ3jLB0YAgHclbiAxPI86pBnwDX1tbzoiV8aFw==",
|
||||
"version": "14.0.0",
|
||||
"resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-14.0.0.tgz",
|
||||
"integrity": "sha512-9gYlLtS6tStdRWzrtXaTMnqcM4dudNegMXJxkR0I/CXObHalYeYcAMPrL19eroNZHtJ8DQmu1E+ZNOYu/IXMXw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"mitt": "^3.0.1",
|
||||
@@ -297,9 +324,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cosmiconfig": {
|
||||
"version": "9.0.0",
|
||||
"resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz",
|
||||
"integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==",
|
||||
"version": "9.0.1",
|
||||
"resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.1.tgz",
|
||||
"integrity": "sha512-hr4ihw+DBqcvrsEDioRO31Z17x71pUYoNe/4h6Z0wB72p7MU7/9gH8Q3s12NFhHPfYBBOV3qyfUxmr/Yn3shnQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"env-paths": "^2.2.1",
|
||||
@@ -332,9 +359,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.4.1",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
|
||||
"integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
|
||||
"version": "4.4.3",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
@@ -363,9 +390,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/devtools-protocol": {
|
||||
"version": "0.0.1452169",
|
||||
"resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1452169.tgz",
|
||||
"integrity": "sha512-FOFDVMGrAUNp0dDKsAU1TorWJUx2JOU1k9xdgBKKJF3IBh/Uhl2yswG5r3TEAOrCiGY2QRp1e6LVDQrCsTKO4g==",
|
||||
"version": "0.0.1581282",
|
||||
"resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1581282.tgz",
|
||||
"integrity": "sha512-nv7iKtNZQshSW2hKzYNr46nM/Cfh5SEvE2oV0/SEGgc9XupIY5ggf84Cz8eJIkBce7S3bmTAauFD6aysMpnqsQ==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/emoji-regex": {
|
||||
@@ -375,9 +402,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/end-of-stream": {
|
||||
"version": "1.4.4",
|
||||
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
|
||||
"integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==",
|
||||
"version": "1.4.5",
|
||||
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
|
||||
"integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"once": "^1.4.0"
|
||||
@@ -393,9 +420,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/error-ex": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
|
||||
"integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==",
|
||||
"version": "1.3.4",
|
||||
"resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz",
|
||||
"integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-arrayish": "^0.2.1"
|
||||
@@ -462,6 +489,15 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/events-universal": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz",
|
||||
"integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"bare-events": "^2.7.0"
|
||||
}
|
||||
},
|
||||
"node_modules/extract-zip": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz",
|
||||
@@ -522,9 +558,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/get-uri": {
|
||||
"version": "6.0.4",
|
||||
"resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.4.tgz",
|
||||
"integrity": "sha512-E1b1lFFLvLgak2whF2xDBcOy6NLVGZBqqjJjsIhvopKfWWEi64pLVTWWehV8KlLerZkfNTA95sTe2OdJKm1OzQ==",
|
||||
"version": "6.0.5",
|
||||
"resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.5.tgz",
|
||||
"integrity": "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"basic-ftp": "^5.0.2",
|
||||
@@ -578,14 +614,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/ip-address": {
|
||||
"version": "9.0.5",
|
||||
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz",
|
||||
"integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==",
|
||||
"version": "10.1.0",
|
||||
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz",
|
||||
"integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"jsbn": "1.1.0",
|
||||
"sprintf-js": "^1.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 12"
|
||||
}
|
||||
@@ -612,9 +644,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/js-yaml": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
|
||||
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
|
||||
"integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"argparse": "^2.0.1"
|
||||
@@ -623,12 +655,6 @@
|
||||
"js-yaml": "bin/js-yaml.js"
|
||||
}
|
||||
},
|
||||
"node_modules/jsbn": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz",
|
||||
"integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/json-parse-even-better-errors": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
|
||||
@@ -789,9 +815,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/pump": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz",
|
||||
"integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==",
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz",
|
||||
"integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"end-of-stream": "^1.1.0",
|
||||
@@ -799,18 +825,18 @@
|
||||
}
|
||||
},
|
||||
"node_modules/puppeteer": {
|
||||
"version": "24.10.0",
|
||||
"resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-24.10.0.tgz",
|
||||
"integrity": "sha512-Oua9VkGpj0S2psYu5e6mCer6W9AU9POEQh22wRgSXnLXASGH+MwLUVWgLCLeP9QPHHcJ7tySUlg4Sa9OJmaLpw==",
|
||||
"version": "24.40.0",
|
||||
"resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-24.40.0.tgz",
|
||||
"integrity": "sha512-IxQbDq93XHVVLWHrAkFP7F7iHvb9o0mgfsSIMlhHb+JM+JjM1V4v4MNSQfcRWJopx9dsNOr9adYv0U5fm9BJBQ==",
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@puppeteer/browsers": "2.10.5",
|
||||
"chromium-bidi": "5.1.0",
|
||||
"@puppeteer/browsers": "2.13.0",
|
||||
"chromium-bidi": "14.0.0",
|
||||
"cosmiconfig": "^9.0.0",
|
||||
"devtools-protocol": "0.0.1452169",
|
||||
"puppeteer-core": "24.10.0",
|
||||
"typed-query-selector": "^2.12.0"
|
||||
"devtools-protocol": "0.0.1581282",
|
||||
"puppeteer-core": "24.40.0",
|
||||
"typed-query-selector": "^2.12.1"
|
||||
},
|
||||
"bin": {
|
||||
"puppeteer": "lib/cjs/puppeteer/node/cli.js"
|
||||
@@ -820,17 +846,18 @@
|
||||
}
|
||||
},
|
||||
"node_modules/puppeteer-core": {
|
||||
"version": "24.10.0",
|
||||
"resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.10.0.tgz",
|
||||
"integrity": "sha512-xX0QJRc8t19iAwRDsAOR38Q/Zx/W6WVzJCEhKCAwp2XMsaWqfNtQ+rBfQW9PlF+Op24d7c8Zlgq9YNmbnA7hdQ==",
|
||||
"version": "24.40.0",
|
||||
"resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.40.0.tgz",
|
||||
"integrity": "sha512-MWL3XbUCfVgGR0gRsidzT6oKJT2QydPLhMITU6HoVWiiv4gkb6gJi3pcdAa8q4HwjBTbqISOWVP4aJiiyUJvag==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@puppeteer/browsers": "2.10.5",
|
||||
"chromium-bidi": "5.1.0",
|
||||
"debug": "^4.4.1",
|
||||
"devtools-protocol": "0.0.1452169",
|
||||
"typed-query-selector": "^2.12.0",
|
||||
"ws": "^8.18.2"
|
||||
"@puppeteer/browsers": "2.13.0",
|
||||
"chromium-bidi": "14.0.0",
|
||||
"debug": "^4.4.3",
|
||||
"devtools-protocol": "0.0.1581282",
|
||||
"typed-query-selector": "^2.12.1",
|
||||
"webdriver-bidi-protocol": "0.4.1",
|
||||
"ws": "^8.19.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
@@ -855,9 +882,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/semver": {
|
||||
"version": "7.7.2",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
|
||||
"integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
|
||||
"version": "7.7.4",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
|
||||
"integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
@@ -877,12 +904,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/socks": {
|
||||
"version": "2.8.4",
|
||||
"resolved": "https://registry.npmjs.org/socks/-/socks-2.8.4.tgz",
|
||||
"integrity": "sha512-D3YaD0aRxR3mEcqnidIs7ReYJFVzWdd6fXJYUM8ixcQcJRGTka/b3saV0KflYhyVJXKhb947GndU35SxYNResQ==",
|
||||
"version": "2.8.7",
|
||||
"resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz",
|
||||
"integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ip-address": "^9.0.5",
|
||||
"ip-address": "^10.0.1",
|
||||
"smart-buffer": "^4.2.0"
|
||||
},
|
||||
"engines": {
|
||||
@@ -914,23 +941,15 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/sprintf-js": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz",
|
||||
"integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/streamx": {
|
||||
"version": "2.22.0",
|
||||
"resolved": "https://registry.npmjs.org/streamx/-/streamx-2.22.0.tgz",
|
||||
"integrity": "sha512-sLh1evHOzBy/iWRiR6d1zRcLao4gGZr3C1kzNz4fopCOKJb6xD9ub8Mpi9Mr1R6id5o43S+d93fI48UC5uM9aw==",
|
||||
"version": "2.25.0",
|
||||
"resolved": "https://registry.npmjs.org/streamx/-/streamx-2.25.0.tgz",
|
||||
"integrity": "sha512-0nQuG6jf1w+wddNEEXCF4nTg3LtufWINB5eFEN+5TNZW7KWJp6x87+JFL43vaAUPyCfH1wID+mNVyW6OHtFamg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"events-universal": "^1.0.0",
|
||||
"fast-fifo": "^1.3.2",
|
||||
"text-decoder": "^1.1.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"bare-events": "^2.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/string-width": {
|
||||
@@ -960,9 +979,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tar-fs": {
|
||||
"version": "3.0.9",
|
||||
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.9.tgz",
|
||||
"integrity": "sha512-XF4w9Xp+ZQgifKakjZYmFdkLoSWd34VGKcsTCwlNWM7QG3ZbaxnTsaBwnjFZqHRf/rROxaR8rXnbtwdvaDI+lA==",
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.2.tgz",
|
||||
"integrity": "sha512-QGxxTxxyleAdyM3kpFs14ymbYmNFrfY+pHj7Z8FgtbZ7w2//VAgLMac7sT6nRpIHjppXO2AwwEOg0bPFVRcmXw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"pump": "^3.0.0",
|
||||
@@ -974,20 +993,30 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tar-stream": {
|
||||
"version": "3.1.7",
|
||||
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz",
|
||||
"integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==",
|
||||
"version": "3.1.8",
|
||||
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.8.tgz",
|
||||
"integrity": "sha512-U6QpVRyCGHva435KoNWy9PRoi2IFYCgtEhq9nmrPPpbRacPs9IH4aJ3gbrFC8dPcXvdSZ4XXfXT5Fshbp2MtlQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"b4a": "^1.6.4",
|
||||
"bare-fs": "^4.5.5",
|
||||
"fast-fifo": "^1.2.0",
|
||||
"streamx": "^2.15.0"
|
||||
}
|
||||
},
|
||||
"node_modules/teex": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz",
|
||||
"integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"streamx": "^2.12.5"
|
||||
}
|
||||
},
|
||||
"node_modules/text-decoder": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.3.tgz",
|
||||
"integrity": "sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==",
|
||||
"version": "1.2.7",
|
||||
"resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz",
|
||||
"integrity": "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"b4a": "^1.6.4"
|
||||
@@ -1000,18 +1029,24 @@
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/typed-query-selector": {
|
||||
"version": "2.12.0",
|
||||
"resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.0.tgz",
|
||||
"integrity": "sha512-SbklCd1F0EiZOyPiW192rrHZzZ5sBijB6xM+cpmrwDqObvdtunOHHIk9fCGsoK5JVIYXoyEp4iEdE3upFH3PAg==",
|
||||
"version": "2.12.1",
|
||||
"resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.1.tgz",
|
||||
"integrity": "sha512-uzR+FzI8qrUEIu96oaeBJmd9E7CFEiQ3goA5qCVgc4s5llSubcfGHq9yUstZx/k4s9dXHVKsE35YWoFyvEqEHA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||
"version": "7.18.2",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz",
|
||||
"integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/webdriver-bidi-protocol": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.4.1.tgz",
|
||||
"integrity": "sha512-ARrjNjtWRRs2w4Tk7nqrf2gBI0QXWuOmMCx2hU+1jUt6d00MjMxURrhxhGbrsoiZKJrhTSTzbIrc554iKI10qw==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/wrap-ansi": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
|
||||
@@ -1036,9 +1071,9 @@
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "8.18.2",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.2.tgz",
|
||||
"integrity": "sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==",
|
||||
"version": "8.20.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz",
|
||||
"integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
@@ -1103,9 +1138,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/zod": {
|
||||
"version": "3.25.49",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.49.tgz",
|
||||
"integrity": "sha512-JMMPMy9ZBk3XFEdbM3iL1brx4NUSejd6xr3ELrrGEfGb355gjhiAWtG3K5o+AViV/3ZfkIrCzXsZn6SbLwTR8Q==",
|
||||
"version": "3.25.76",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
|
||||
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
const OVERRIDES = <%- JSON.stringify(OVERRIDES ?? {}) %>;
|
||||
</script>
|
||||
<% } else { %>
|
||||
<link rel="stylesheet" type="text/css" href="styles/main.css" />
|
||||
<link rel="stylesheet" type="text/css" href="styles/ws.min.css" />
|
||||
<!--<script type="text/javascript">const OVERRIDES={};</script>-->
|
||||
<script type="text/javascript">
|
||||
OVERRIDES = <%- JSON.stringify(OVERRIDES ?? {}) %>;
|
||||
@@ -62,7 +62,7 @@
|
||||
<script type="module" src="scripts/modules/radar.mjs"></script>
|
||||
<script type="module" src="scripts/modules/settings.mjs"></script>
|
||||
<script type="module" src="scripts/modules/media.mjs"></script>
|
||||
<script type="module" src="scripts/modules/custom-rss-feed.mjs"></script>
|
||||
<script type="module" src="scripts/modules/custom-scroll-text.mjs"></script>
|
||||
<script type="module" src="scripts/index.mjs"></script>
|
||||
<% } %>
|
||||
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
<%- include('header.ejs', {title:'Almanac', hasTime: true}) %>
|
||||
<div class="main has-scroll almanac">
|
||||
<div class="main has-scroll almanac can-enhance">
|
||||
<div class="sun">
|
||||
<div class="grid-item empty"></div>
|
||||
<div class="grid-item header day-0"></div>
|
||||
<div class="grid-item header day-1"></div>
|
||||
<div class="grid-item header day-2"></div>
|
||||
<div class="grid-item header day-2 wide-enhanced"></div>
|
||||
<div class="grid-item row-label">Sunrise:</div>
|
||||
<div class="grid-item time rise-0"></div>
|
||||
<div class="grid-item time rise-1"></div>
|
||||
<div class="grid-item time rise-2"></div>
|
||||
<div class="grid-item time rise-2 wide-enhanced"></div>
|
||||
<div class="grid-item row-label">Sunset:</div>
|
||||
<div class="grid-item time set-0"></div>
|
||||
<div class="grid-item time set-1"></div>
|
||||
<div class="grid-item time set-2"></div>
|
||||
<div class="grid-item time set-2 wide-enhanced"></div>
|
||||
</div>
|
||||
<div class="moon">
|
||||
<div class="title">Moon Data:</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<%- include('header.ejs', {titleDual:{ top: 'Current' , bottom: 'Conditions' }, noaaLogo: true, hasTime: true}) %>
|
||||
<div class="main has-scroll has-box current-weather">
|
||||
<div class="main has-scroll has-box current-weather can-enhance">
|
||||
<div class="weather template">
|
||||
<div class="left col">
|
||||
<div class="temp center"></div>
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
<%- include('header.ejs', {title: 'Hourly Graph' , hasTime: false }) %>
|
||||
<div class="main has-scroll hourly-graph">
|
||||
<div class="main has-scroll hourly-graph can-enhance">
|
||||
<div class="top-right template ">
|
||||
<div class="temperature">Temperature</div>
|
||||
<div class="dewpoint">Dewpoint</div>
|
||||
<div class="cloud">Cloud %</div>
|
||||
<div class="rain">Precip %</div>
|
||||
</div>
|
||||
@@ -9,6 +10,7 @@
|
||||
<div class="label l-1">75</div>
|
||||
<div class="label l-2">65</div>
|
||||
<div class="label l-3">55</div>
|
||||
<div class="label l-4">45</div>
|
||||
</div>
|
||||
<div class="chart">
|
||||
<img id="chart-area"></img>
|
||||
@@ -19,5 +21,7 @@
|
||||
<div class="label l-3">12p</div>
|
||||
<div class="label l-4">6p</div>
|
||||
<div class="label l-5">12a</div>
|
||||
<div class="label l-6">6a</div>
|
||||
<div class="label l-7">12p</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,9 +1,10 @@
|
||||
<%- include('header.ejs', {titleDual:{ top: 'Latest' , bottom: 'Observations' }, noaaLogo: true, hasTime: true }) %>
|
||||
<div class="main has-scroll latest-observations has-box">
|
||||
<div class="main has-scroll latest-observations has-box can-enhance">
|
||||
<div class="container">
|
||||
<div class="column-headers">
|
||||
<div class="temp english">°F</div>
|
||||
<div class="temp metric">°C</div>
|
||||
<div class="like">Like</div>
|
||||
<div class="weather">Weather</div>
|
||||
<div class="wind">Wind</div>
|
||||
</div>
|
||||
@@ -11,6 +12,7 @@
|
||||
<div class="observation-row template">
|
||||
<div class="location"></div>
|
||||
<div class="temp"></div>
|
||||
<div class="like"></div>
|
||||
<div class="weather"></div>
|
||||
<div class="wind"></div>
|
||||
</div>
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<%- include('header.ejs', {titleDual:{ top: 'Local' , bottom: 'Forecast' }, hasTime: true, noaaLogo: true}) %>
|
||||
<div class="main has-scroll has-box local-forecast">
|
||||
<div class="container">
|
||||
<div class="forecasts">
|
||||
<div class="forecast template">
|
||||
<div class="text">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<%- include('scroll.ejs') %>
|
||||
<div class="main has-scroll has-box local-forecast can-enhance">
|
||||
<div class="container">
|
||||
<div class="forecasts">
|
||||
<div class="forecast template">
|
||||
<div class="text">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<%- include('scroll.ejs') %>
|
||||
@@ -1,45 +1,45 @@
|
||||
<div class="header">
|
||||
<div class="logo"><img src="images/logos/logo-corner.png" /></div>
|
||||
<div class="title dual">
|
||||
<div class="top">
|
||||
Local
|
||||
</div>
|
||||
<div class="bottom">
|
||||
Radar
|
||||
</div>
|
||||
</div>
|
||||
<div class="right">
|
||||
<div class="precip">
|
||||
<div class="precip-header">PRECIP</div>
|
||||
<div class="scale">
|
||||
<div class="text">Light</div>
|
||||
<div class="scale-table">
|
||||
<div class="box box-1"></div>
|
||||
<div class="box box-2"></div>
|
||||
<div class="box box-3"></div>
|
||||
<div class="box box-4"></div>
|
||||
<div class="box box-5"></div>
|
||||
<div class="box box-6"></div>
|
||||
<div class="box box-7"></div>
|
||||
<div class="box box-7"></div>
|
||||
</div>
|
||||
<div class="text">Heavy</div>
|
||||
</div>
|
||||
<div class="time"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="logo"><img src="images/logos/logo-corner.png" /></div>
|
||||
<div class="title dual">
|
||||
<div class="top">
|
||||
Local
|
||||
</div>
|
||||
<div class="bottom">
|
||||
Radar
|
||||
</div>
|
||||
</div>
|
||||
<div class="right">
|
||||
<div class="precip">
|
||||
<div class="precip-header">PRECIP</div>
|
||||
<div class="scale">
|
||||
<div class="text">Light</div>
|
||||
<div class="scale-table">
|
||||
<div class="box box-1"></div>
|
||||
<div class="box box-2"></div>
|
||||
<div class="box box-3"></div>
|
||||
<div class="box box-4"></div>
|
||||
<div class="box box-5"></div>
|
||||
<div class="box box-6"></div>
|
||||
<div class="box box-7"></div>
|
||||
<div class="box box-7"></div>
|
||||
</div>
|
||||
<div class="text">Heavy</div>
|
||||
</div>
|
||||
<div class="time"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="main radar">
|
||||
<div class="container">
|
||||
<div class="map-tiles tiles"><img/><img/><img/><img/></div>
|
||||
<div class="scroll-area">
|
||||
<div class="frame template">
|
||||
<div class="map">
|
||||
<img/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="overlay-tiles tiles"><img/><img/><img/><img/></div>
|
||||
</div>
|
||||
<div class="main radar can-enhance">
|
||||
<div class="container">
|
||||
<div class="map-tiles tiles"><img /><img /><img /><img /><img /><img /></div>
|
||||
<div class="scroll-area">
|
||||
<div class="frame template">
|
||||
<div class="map">
|
||||
<img />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="overlay-tiles tiles"><img /><img /><img /><img /><img /></div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,5 +1,5 @@
|
||||
<%- include('header.ejs', {titleDual:{ top: 'Regional' , bottom: 'Observations' }, hasTime: true }) %>
|
||||
<div class="main has-scroll regional-forecast">
|
||||
<div class="main has-scroll regional-forecast can-enhance">
|
||||
<div class="map"><img src="images/maps/basemap.webp" /></div>
|
||||
<div class="location-container">
|
||||
<div class="location template">
|
||||
|
||||
Reference in New Issue
Block a user