mirror of
https://github.com/netbymatt/ws4kp.git
synced 2026-04-15 08:09:31 -07:00
Compare commits
46 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9b4eed7332 | ||
|
|
ef1477f9eb | ||
|
|
e2876df177 | ||
|
|
d6335b2878 | ||
|
|
781128100e | ||
|
|
56261ded4b | ||
|
|
58540ad67b | ||
|
|
af53cca45e | ||
|
|
94470db9a7 | ||
|
|
3c7a77e200 | ||
|
|
d472df2e26 | ||
|
|
fdbf11dcd4 | ||
|
|
b7e9091320 | ||
|
|
d24284d340 | ||
|
|
4e8dc35739 | ||
|
|
779d34a0a8 | ||
|
|
efeb45d3d0 | ||
|
|
e2d7a96971 | ||
|
|
487c83f664 | ||
|
|
88b8b4a82e | ||
|
|
13b77a0070 | ||
|
|
c9307768a4 | ||
|
|
844544c364 | ||
|
|
80a68caa27 | ||
|
|
c7889eaa2c | ||
|
|
019908684b | ||
|
|
e1a58b6548 | ||
|
|
e794976f4d | ||
|
|
193d742aa3 | ||
|
|
b62339af94 | ||
|
|
913dc383f6 | ||
|
|
94249560f2 | ||
|
|
75314d92c9 | ||
|
|
168c0c5caf | ||
|
|
b6cd75ab42 | ||
|
|
934a489340 | ||
|
|
c5f5c101f9 | ||
|
|
933367974f | ||
|
|
0e67eb22dc | ||
|
|
9df6f6888f | ||
|
|
04cc5d4252 | ||
|
|
543d3f5196 | ||
|
|
78ceba9c19 | ||
|
|
763d42061e | ||
|
|
318c55b92d | ||
|
|
84d39101e5 |
26
.eslintrc.js
26
.eslintrc.js
@@ -8,7 +8,6 @@ module.exports = {
|
||||
},
|
||||
extends: [
|
||||
'airbnb-base',
|
||||
'plugin:sonarjs/recommended',
|
||||
],
|
||||
globals: {
|
||||
Atomics: 'readonly',
|
||||
@@ -22,16 +21,17 @@ module.exports = {
|
||||
|
||||
},
|
||||
parserOptions: {
|
||||
ecmaVersion: 2021,
|
||||
ecmaVersion: 2023,
|
||||
},
|
||||
plugins: [
|
||||
'unicorn',
|
||||
'sonarjs',
|
||||
],
|
||||
rules: {
|
||||
indent: [
|
||||
'error',
|
||||
'tab',
|
||||
{
|
||||
SwitchCase: 1
|
||||
},
|
||||
],
|
||||
'no-tabs': 0,
|
||||
'no-console': 0,
|
||||
@@ -67,24 +67,6 @@ module.exports = {
|
||||
json: 'always',
|
||||
},
|
||||
],
|
||||
// unicorn
|
||||
'unicorn/numeric-separators-style': 'error',
|
||||
'unicorn/prefer-query-selector': 'error',
|
||||
'unicorn/catch-error-name': 'error',
|
||||
'unicorn/no-negated-condition': 'error',
|
||||
'unicorn/better-regex': 'error',
|
||||
'unicorn/consistent-function-scoping': 'error',
|
||||
'unicorn/prefer-array-flat-map': 'error',
|
||||
'unicorn/prefer-array-find': 'error',
|
||||
'unicorn/prefer-regexp-test': 'error',
|
||||
'unicorn/consistent-destructuring': 'error',
|
||||
'unicorn/prefer-date-now': 'error',
|
||||
'unicorn/prefer-ternary': 'error',
|
||||
'unicorn/prefer-dom-node-append': 'error',
|
||||
'unicorn/explicit-length-check': 'error',
|
||||
'unicorn/prefer-at': 'error',
|
||||
// sonarjs
|
||||
'sonarjs/cognitive-complexity': 0,
|
||||
},
|
||||
ignorePatterns: [
|
||||
'*.min.js',
|
||||
|
||||
7
.vscode/settings.json
vendored
7
.vscode/settings.json
vendored
@@ -7,8 +7,6 @@
|
||||
"format": "compressed",
|
||||
"extensionName": ".css",
|
||||
"savePath": "/server/styles",
|
||||
"savePathSegmentKeys": null,
|
||||
"savePathReplaceSegmentsWith": null
|
||||
}
|
||||
],
|
||||
"search.exclude": {
|
||||
@@ -21,5 +19,8 @@
|
||||
"editor.formatOnSave": true,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": "explicit"
|
||||
}
|
||||
},
|
||||
"eslint.validate": [
|
||||
"javascript"
|
||||
]
|
||||
}
|
||||
2
LICENSE
2
LICENSE
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2016-2017 Michael Battaglia
|
||||
Copyright (c) 2020-2024 Matt Walsh
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
18
README.md
18
README.md
@@ -16,6 +16,13 @@ This project is based on the work of [Mike Battaglia](https://github.com/vbguyny
|
||||
* [Icon](https://twcclassics.com/downloads.html) sets
|
||||
* Countless photos and videos of WeatherStar 4000 forecasts used as references.
|
||||
|
||||
## Does WeatherStar 4000+ work outside of the USA?
|
||||
|
||||
This project is tightly coupled to [NOAA's Weather API](https://www.weather.gov/documentation/services-web-api), which is exclsuive to the United States. Using NOAA's Weather API is a crucial requirement to provide an authentic WeatherStar 4000+ experience.
|
||||
|
||||
If you would like to display weather information for international locations (outside of the USA), please checkout a fork of this project created by [@mwood77](https://github.com/mwood77):
|
||||
- [`ws4kp-international`](https://github.com/mwood77/ws4kp-international)
|
||||
|
||||
## Run Your WeatherStar
|
||||
There are a lot of CORS considerations and issues with api.weather.gov that are easiest to deal with by running a local server to see this in action (or use the live link above). You'll need Node.js >12.0 to run the local server.
|
||||
|
||||
@@ -78,7 +85,7 @@ I've made several changes to this Weather Star 4000 simulation compared to the o
|
||||
* "Flavors" are not present in this simulation. Flavors refer to the order of the weather information that was shown on the original units. Instead, the order of the displays has been fixed and a checkboxes can be used to turn on and off individual displays. The travel forecast has been defaulted to off so only local information shows for new users.
|
||||
|
||||
## Sharing a permalink (bookmarking)
|
||||
Selected displays, the forecast city and widescreen setting are sticky from one session to the next. However if you would like to share your exact configuration or bookmark it click the "Copy Permalink" near the bottom of the page. A URL will be copied to your clipboard with all of you selected displays and location. You can then share this link or add it to your bookmarks.
|
||||
Selected displays, the forecast city and widescreen setting are sticky from one session to the next. However if you would like to share your exact configuration or bookmark it click the "Copy Permalink" (or get "Get Parmalink") near the bottom of the page. A URL will be copied to your clipboard with all of you selected displays and location (or copy it from the page if your browser doesn't support clipboard transfers directly). You can then share this link or add it to your bookmarks.
|
||||
|
||||
## Kiosk mode
|
||||
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.
|
||||
@@ -91,20 +98,21 @@ As time allows I will be working on the following enhancements.
|
||||
|
||||
* Better error reporting when api.weather.gov is down (happens more often than you would think)
|
||||
|
||||
And the following technical fixes.
|
||||
|
||||
* Caching of the animated gifs, specifically after they are decompressed
|
||||
|
||||
## Community Notes
|
||||
|
||||
Thanks to the WeatherStar community for providing these discussions to further extend your retro forecasts!
|
||||
|
||||
* [Stream as FFMPEG](https://github.com/netbymatt/ws4kp/issues/37#issuecomment-2008491948)
|
||||
|
||||
## Customization
|
||||
A hook is provided as `/server/scripts/custom.js` to allow customizations to your own fork of this project, without accidentally pushing your customizations back upstream to the git repository. An sample file is provided at `/server/scripts/custom.sample.js` and should be renamed to `custom.js` activate it.
|
||||
|
||||
## Issue reporting and feature requests
|
||||
|
||||
Please do not report issues with api.weather.gov being down. It's a new service and not considered fully operational yet. Before reporting an issue or requesting a feature please consider that this is not intended to be a perfect recreation of the WeatherStar 4000, it's a best effort that fits within what's available from the API and within a web browser.
|
||||
|
||||
Note: not all units are converted to metric, if selected. Some text-based products such as warnings are simple text strings provided from the national weather service and thus have baked-in units such as "gusts up to 60 mph." These values will not be converted.
|
||||
|
||||
## Disclaimer
|
||||
|
||||
This web site should NOT be used in life threatening weather situations, or be relied on to inform the public of such situations. The Internet is an unreliable network subject to server and network outages and by nature is not suitable for such mission critical use. If you require such access to NWS data, please consider one of their subscription services. The authors of this web site shall not be held liable in the event of injury, death or property damage that occur as a result of disregarding this warning.
|
||||
|
||||
2
dist/index.html
vendored
2
dist/index.html
vendored
File diff suppressed because one or more lines are too long
2
dist/resources/ws.min.css
vendored
2
dist/resources/ws.min.css
vendored
File diff suppressed because one or more lines are too long
2
dist/resources/ws.min.js
vendored
2
dist/resources/ws.min.js
vendored
File diff suppressed because one or more lines are too long
@@ -1,23 +1,26 @@
|
||||
/* eslint-disable import/no-extraneous-dependencies */
|
||||
const gulp = require('gulp');
|
||||
const concat = require('gulp-concat');
|
||||
const terser = require('gulp-terser');
|
||||
const ejs = require('gulp-ejs');
|
||||
const rename = require('gulp-rename');
|
||||
const htmlmin = require('gulp-htmlmin');
|
||||
const del = require('del');
|
||||
const s3Upload = require('gulp-s3-upload');
|
||||
const webpack = require('webpack-stream');
|
||||
const TerserPlugin = require('terser-webpack-plugin');
|
||||
const path = require('path');
|
||||
|
||||
const clean = () => del(['./dist**']);
|
||||
import {
|
||||
src, dest, series, parallel,
|
||||
} from 'gulp';
|
||||
import concat from 'gulp-concat';
|
||||
import terser from 'gulp-terser';
|
||||
import ejs from 'gulp-ejs';
|
||||
import rename from 'gulp-rename';
|
||||
import htmlmin from 'gulp-htmlmin';
|
||||
import { deleteAsync } from 'del';
|
||||
import s3Upload from 'gulp-s3-upload';
|
||||
import webpack from 'webpack-stream';
|
||||
import TerserPlugin from 'terser-webpack-plugin';
|
||||
import { readFile } from 'fs/promises';
|
||||
|
||||
// get cloudfront
|
||||
const AWS = require('aws-sdk');
|
||||
import { CloudFrontClient, CreateInvalidationCommand } from '@aws-sdk/client-cloudfront';
|
||||
|
||||
AWS.config.update({ region: 'us-east-1' });
|
||||
const cloudfront = new AWS.CloudFront({ apiVersion: '2020-01-01' });
|
||||
const clean = () => deleteAsync(['./dist**']);
|
||||
|
||||
const cloudfront = new CloudFrontClient({ region: 'us-east-1' });
|
||||
|
||||
const RESOURCES_PATH = './dist/resources';
|
||||
|
||||
const jsSourcesData = [
|
||||
'server/scripts/data/travelcities.js',
|
||||
@@ -33,7 +36,7 @@ const webpackOptions = {
|
||||
filename: 'ws.min.js',
|
||||
},
|
||||
resolve: {
|
||||
roots: [path.resolve(__dirname, './')],
|
||||
roots: ['./'],
|
||||
},
|
||||
optimization: {
|
||||
minimize: true,
|
||||
@@ -51,10 +54,10 @@ const webpackOptions = {
|
||||
},
|
||||
};
|
||||
|
||||
gulp.task('compress_js_data', () => gulp.src(jsSourcesData)
|
||||
const compressJsData = () => src(jsSourcesData)
|
||||
.pipe(concat('data.min.js'))
|
||||
.pipe(terser())
|
||||
.pipe(gulp.dest('./dist/resources')));
|
||||
.pipe(dest(RESOURCES_PATH));
|
||||
|
||||
const jsVendorSources = [
|
||||
'server/scripts/vendor/auto/jquery.js',
|
||||
@@ -64,10 +67,10 @@ const jsVendorSources = [
|
||||
'server/scripts/vendor/auto/suncalc.js',
|
||||
];
|
||||
|
||||
gulp.task('compress_js_vendor', () => gulp.src(jsVendorSources)
|
||||
const compressJsVendor = () => src(jsVendorSources)
|
||||
.pipe(concat('vendor.min.js'))
|
||||
.pipe(terser())
|
||||
.pipe(gulp.dest('./dist/resources')));
|
||||
.pipe(dest(RESOURCES_PATH));
|
||||
|
||||
const mjsSources = [
|
||||
'server/scripts/modules/currentweatherscroll.mjs',
|
||||
@@ -87,39 +90,40 @@ const mjsSources = [
|
||||
'server/scripts/index.mjs',
|
||||
];
|
||||
|
||||
gulp.task('build_js', () => gulp.src(mjsSources)
|
||||
const buildJs = () => src(mjsSources)
|
||||
.pipe(webpack(webpackOptions))
|
||||
.pipe(gulp.dest('dist/resources')));
|
||||
.pipe(dest(RESOURCES_PATH));
|
||||
|
||||
const cssSources = [
|
||||
'server/styles/main.css',
|
||||
];
|
||||
gulp.task('copy_css', () => gulp.src(cssSources)
|
||||
const copyCss = () => src(cssSources)
|
||||
.pipe(concat('ws.min.css'))
|
||||
.pipe(gulp.dest('./dist/resources')));
|
||||
.pipe(dest(RESOURCES_PATH));
|
||||
|
||||
const htmlSources = [
|
||||
'views/*.ejs',
|
||||
];
|
||||
gulp.task('compress_html', () => {
|
||||
// eslint-disable-next-line global-require
|
||||
const { version } = require('../package.json');
|
||||
return gulp.src(htmlSources)
|
||||
const compressHtml = async () => {
|
||||
const packageJson = await readFile('package.json');
|
||||
const { version } = JSON.parse(packageJson);
|
||||
|
||||
return src(htmlSources)
|
||||
.pipe(ejs({
|
||||
production: version,
|
||||
version,
|
||||
}))
|
||||
.pipe(rename({ extname: '.html' }))
|
||||
.pipe(htmlmin({ collapseWhitespace: true }))
|
||||
.pipe(gulp.dest('./dist'));
|
||||
});
|
||||
.pipe(dest('./dist'));
|
||||
};
|
||||
|
||||
const otherFiles = [
|
||||
'server/robots.txt',
|
||||
'server/manifest.json',
|
||||
];
|
||||
gulp.task('copy_other_files', () => gulp.src(otherFiles, { base: 'server/' })
|
||||
.pipe(gulp.dest('./dist')));
|
||||
const copyOtherFiles = () => src(otherFiles, { base: 'server/' })
|
||||
.pipe(dest('./dist'));
|
||||
|
||||
const s3 = s3Upload({
|
||||
useIAM: true,
|
||||
@@ -130,7 +134,7 @@ const uploadSources = [
|
||||
'dist/**',
|
||||
'!dist/**/*.map',
|
||||
];
|
||||
gulp.task('upload', () => gulp.src(uploadSources, { base: './dist' })
|
||||
const upload = () => src(uploadSources, { base: './dist' })
|
||||
.pipe(s3({
|
||||
Bucket: 'weatherstar',
|
||||
StorageClass: 'STANDARD',
|
||||
@@ -140,21 +144,21 @@ gulp.task('upload', () => gulp.src(uploadSources, { base: './dist' })
|
||||
return 'max-age=2592000'; // 1 month
|
||||
},
|
||||
},
|
||||
})));
|
||||
}));
|
||||
|
||||
const imageSources = [
|
||||
'server/fonts/**',
|
||||
'server/images/**',
|
||||
];
|
||||
gulp.task('upload_images', () => gulp.src(imageSources, { base: './server' })
|
||||
const uploadImages = () => src(imageSources, { base: './server', encoding: false })
|
||||
.pipe(
|
||||
s3({
|
||||
Bucket: 'weatherstar',
|
||||
StorageClass: 'STANDARD',
|
||||
}),
|
||||
));
|
||||
);
|
||||
|
||||
gulp.task('invalidate', async () => cloudfront.createInvalidation({
|
||||
const invalidate = () => cloudfront.send(new CreateInvalidationCommand({
|
||||
DistributionId: 'E9171A4KV8KCW',
|
||||
InvalidationBatch: {
|
||||
CallerReference: (new Date()).toLocaleString(),
|
||||
@@ -163,10 +167,12 @@ gulp.task('invalidate', async () => cloudfront.createInvalidation({
|
||||
Items: ['/*'],
|
||||
},
|
||||
},
|
||||
}).promise());
|
||||
}));
|
||||
|
||||
gulp.task('build-dist', gulp.series(clean, gulp.parallel('build_js', 'compress_js_data', 'compress_js_vendor', 'copy_css', 'compress_html', 'copy_other_files')));
|
||||
const buildDist = series(clean, parallel(buildJs, compressJsData, compressJsVendor, copyCss, compressHtml, copyOtherFiles));
|
||||
|
||||
// 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
|
||||
module.exports = gulp.series('build-dist', 'upload_images', 'upload', 'invalidate');
|
||||
const publishFrontend = series(buildDist, uploadImages, upload, invalidate);
|
||||
|
||||
export default publishFrontend;
|
||||
@@ -1,12 +1,9 @@
|
||||
/* eslint-disable import/no-extraneous-dependencies */
|
||||
const gulp = require('gulp');
|
||||
const del = require('del');
|
||||
const rename = require('gulp-rename');
|
||||
import { src, series, dest } from 'gulp';
|
||||
import { deleteAsync } from 'del';
|
||||
import rename from 'gulp-rename';
|
||||
|
||||
const clean = (cb) => {
|
||||
del(['./server/scripts/vendor/auto/**']);
|
||||
cb();
|
||||
};
|
||||
const clean = () => deleteAsync(['./server/scripts/vendor/auto/**']);
|
||||
|
||||
const vendorFiles = [
|
||||
'./node_modules/luxon/build/es6/luxon.js',
|
||||
@@ -17,13 +14,15 @@ const vendorFiles = [
|
||||
'./node_modules/swiped-events/src/swiped-events.js',
|
||||
];
|
||||
|
||||
const copy = () => gulp.src(vendorFiles)
|
||||
const copy = () => src(vendorFiles)
|
||||
.pipe(rename((path) => {
|
||||
path.dirname = path.dirname.toLowerCase();
|
||||
path.basename = path.basename.toLowerCase();
|
||||
path.extname = path.extname.toLowerCase();
|
||||
if (path.basename === 'luxon') path.extname = '.mjs';
|
||||
}))
|
||||
.pipe(gulp.dest('./server/scripts/vendor/auto'));
|
||||
.pipe(dest('./server/scripts/vendor/auto'));
|
||||
|
||||
module.exports = gulp.series(clean, copy);
|
||||
const updateVendor = series(clean, copy);
|
||||
|
||||
export default updateVendor;
|
||||
@@ -1,4 +0,0 @@
|
||||
const gulp = require('gulp');
|
||||
|
||||
gulp.task('update-vendor', require('./gulp/update-vendor'));
|
||||
gulp.task('publish-frontend', require('./gulp/publish-frontend'));
|
||||
7
gulpfile.mjs
Normal file
7
gulpfile.mjs
Normal file
@@ -0,0 +1,7 @@
|
||||
import updateVendor from './gulp/update-vendor.mjs';
|
||||
import publishFrontend from './gulp/publish-frontend.mjs';
|
||||
|
||||
export {
|
||||
updateVendor,
|
||||
publishFrontend,
|
||||
};
|
||||
1
index.js
1
index.js
@@ -1,5 +1,4 @@
|
||||
// express
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
const express = require('express');
|
||||
|
||||
const app = express();
|
||||
|
||||
5998
package-lock.json
generated
5998
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
34
package.json
34
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ws4kp",
|
||||
"version": "5.11.3",
|
||||
"version": "5.14.2",
|
||||
"description": "Welcome to the WeatherStar 4000+ project page!",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
@@ -20,30 +20,32 @@
|
||||
},
|
||||
"homepage": "https://github.com/netbymatt/ws4kp#readme",
|
||||
"devDependencies": {
|
||||
"del": "^6.0.0",
|
||||
"ejs": "^3.1.5",
|
||||
"del": "^8.0.0",
|
||||
"jquery": "^3.6.0",
|
||||
"jquery-touchswipe": "^1.6.19",
|
||||
"luxon": "^3.0.0",
|
||||
"nosleep.js": "^0.12.0",
|
||||
"suncalc": "^1.8.0",
|
||||
"swiped-events": "^1.1.4",
|
||||
"@aws-sdk/client-cloudfront": "^3.609.0",
|
||||
"gulp-awspublish": "^8.0.0",
|
||||
"gulp-s3-upload": "^1.7.3",
|
||||
"eslint": "^8.2.0",
|
||||
"eslint-config-airbnb-base": "^15.0.0",
|
||||
"eslint-plugin-import": "^2.10.0",
|
||||
"eslint-plugin-sonarjs": "^0.25.1",
|
||||
"eslint-plugin-unicorn": "^52.0.0",
|
||||
"express": "^4.17.1",
|
||||
"gulp": "^5.0.0",
|
||||
"gulp-concat": "^2.6.1",
|
||||
"gulp-ejs": "^5.1.0",
|
||||
"gulp-htmlmin": "^5.0.1",
|
||||
"gulp-rename": "^2.0.0",
|
||||
"gulp-s3-upload": "^1.7.3",
|
||||
"gulp-sass": "^5.1.0",
|
||||
"gulp-sass": "^6.0.0",
|
||||
"gulp-terser": "^2.0.0",
|
||||
"jquery": "^3.6.0",
|
||||
"jquery-touchswipe": "^1.6.19",
|
||||
"luxon": "^3.0.0",
|
||||
"nosleep.js": "^0.12.0",
|
||||
"sass": "^1.54.0",
|
||||
"suncalc": "^1.8.0",
|
||||
"swiped-events": "^1.1.4",
|
||||
"terser-webpack-plugin": "^5.3.6",
|
||||
"webpack-stream": "^7.0.0"
|
||||
"webpack-stream": "^7.0.0",
|
||||
"sass": "^1.54.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"express": "^4.17.1",
|
||||
"ejs": "^3.1.5"
|
||||
}
|
||||
}
|
||||
|
||||
14
server/scripts/custom.sample.js
Normal file
14
server/scripts/custom.sample.js
Normal file
@@ -0,0 +1,14 @@
|
||||
// this file is loaded by the main html page (when renamed to custom.js)
|
||||
// it is intended to allow for customizations that do not get published back to the git repo
|
||||
// for example, changing the logo
|
||||
|
||||
// start running after all content is loaded
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// get all of the logo images
|
||||
const logos = document.querySelectorAll('.logo img');
|
||||
// loop through each logo
|
||||
logos.forEach((elem) => {
|
||||
// change the source
|
||||
elem.src = 'my-custom-logo.gif';
|
||||
});
|
||||
});
|
||||
@@ -43,9 +43,12 @@ const init = () => {
|
||||
btnGetGps.addEventListener('click', btnGetGpsClick);
|
||||
if (!navigator.geolocation) btnGetGps.style.display = 'none';
|
||||
|
||||
document.querySelector('#divTwc').addEventListener('click', () => {
|
||||
document.querySelector('#divTwc').addEventListener('mousemove', () => {
|
||||
if (document.fullscreenElement) updateFullScreenNavigate();
|
||||
});
|
||||
// local change detection when exiting full screen via ESC key (or other non button click methods)
|
||||
window.addEventListener('resize', fullScreenResizeCheck);
|
||||
fullScreenResizeCheck.wasFull = false;
|
||||
|
||||
document.querySelector(TXT_ADDRESS_SELECTOR).addEventListener('keydown', (key) => { if (key.code === 'Enter') formSubmit(); });
|
||||
document.querySelector('#btnGetLatLng').addEventListener('click', () => formSubmit());
|
||||
@@ -189,7 +192,7 @@ const enterFullScreen = () => {
|
||||
|
||||
if (requestMethod) {
|
||||
// Native full screen.
|
||||
requestMethod.call(element, { navigationUI: 'hide' }); // https://bugs.chromium.org/p/chromium/issues/detail?id=933436#c7
|
||||
requestMethod.call(element, { navigationUI: 'hide' });
|
||||
} else {
|
||||
// iOS doesn't support FullScreen API.
|
||||
window.scrollTo(0, 0);
|
||||
@@ -217,10 +220,15 @@ const exitFullscreen = () => {
|
||||
document.msExitFullscreen();
|
||||
}
|
||||
resize();
|
||||
exitFullScreenVisibilityChanges();
|
||||
};
|
||||
|
||||
const exitFullScreenVisibilityChanges = () => {
|
||||
// change hover text and image
|
||||
const img = document.querySelector(TOGGLE_FULL_SCREEN_SELECTOR);
|
||||
img.src = 'images/nav/ic_fullscreen_white_24dp_2x.png';
|
||||
img.title = 'Enter fullscreen';
|
||||
document.querySelector('#divTwc').classList.remove('no-cursor');
|
||||
const divTwcBottom = document.querySelector('#divTwcBottom');
|
||||
divTwcBottom.classList.remove('hidden');
|
||||
divTwcBottom.classList.add('visible');
|
||||
@@ -228,7 +236,6 @@ const exitFullscreen = () => {
|
||||
|
||||
const btnNavigateMenuClick = () => {
|
||||
postMessage('navButton', 'menu');
|
||||
updateFullScreenNavigate();
|
||||
return false;
|
||||
};
|
||||
|
||||
@@ -247,35 +254,32 @@ const loadData = (_latLon, haveDataCallback) => {
|
||||
|
||||
const swipeCallBack = (direction) => {
|
||||
switch (direction) {
|
||||
case 'left':
|
||||
btnNavigateNextClick();
|
||||
break;
|
||||
case 'left':
|
||||
btnNavigateNextClick();
|
||||
break;
|
||||
|
||||
case 'right':
|
||||
default:
|
||||
btnNavigatePreviousClick();
|
||||
break;
|
||||
case 'right':
|
||||
default:
|
||||
btnNavigatePreviousClick();
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
const btnNavigateRefreshClick = () => {
|
||||
resetStatuses();
|
||||
loadData();
|
||||
updateFullScreenNavigate();
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
const btnNavigateNextClick = () => {
|
||||
postMessage('navButton', 'next');
|
||||
updateFullScreenNavigate();
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
const btnNavigatePreviousClick = () => {
|
||||
postMessage('navButton', 'previous');
|
||||
updateFullScreenNavigate();
|
||||
|
||||
return false;
|
||||
};
|
||||
@@ -287,6 +291,7 @@ const updateFullScreenNavigate = () => {
|
||||
const divTwcBottom = document.querySelector('#divTwcBottom');
|
||||
divTwcBottom.classList.remove('hidden');
|
||||
divTwcBottom.classList.add('visible');
|
||||
document.querySelector('#divTwc').classList.remove('no-cursor');
|
||||
|
||||
if (navigateFadeIntervalId) {
|
||||
clearTimeout(navigateFadeIntervalId);
|
||||
@@ -297,6 +302,7 @@ const updateFullScreenNavigate = () => {
|
||||
if (document.fullscreenElement) {
|
||||
divTwcBottom.classList.remove('visible');
|
||||
divTwcBottom.classList.add('hidden');
|
||||
document.querySelector('#divTwc').classList.add('no-cursor');
|
||||
}
|
||||
}, 2000);
|
||||
};
|
||||
@@ -306,41 +312,41 @@ const documentKeydown = (e) => {
|
||||
|
||||
if (document.fullscreenElement || document.activeElement === document.body) {
|
||||
switch (key) {
|
||||
case ' ': // Space
|
||||
// don't scroll
|
||||
e.preventDefault();
|
||||
btnNavigatePlayClick();
|
||||
return false;
|
||||
case ' ': // Space
|
||||
// don't scroll
|
||||
e.preventDefault();
|
||||
btnNavigatePlayClick();
|
||||
return false;
|
||||
|
||||
case 'ArrowRight':
|
||||
case 'PageDown':
|
||||
// don't scroll
|
||||
e.preventDefault();
|
||||
btnNavigateNextClick();
|
||||
return false;
|
||||
case 'ArrowRight':
|
||||
case 'PageDown':
|
||||
// don't scroll
|
||||
e.preventDefault();
|
||||
btnNavigateNextClick();
|
||||
return false;
|
||||
|
||||
case 'ArrowLeft':
|
||||
case 'PageUp':
|
||||
// don't scroll
|
||||
e.preventDefault();
|
||||
btnNavigatePreviousClick();
|
||||
return false;
|
||||
case 'ArrowLeft':
|
||||
case 'PageUp':
|
||||
// don't scroll
|
||||
e.preventDefault();
|
||||
btnNavigatePreviousClick();
|
||||
return false;
|
||||
|
||||
case 'ArrowUp': // Home
|
||||
e.preventDefault();
|
||||
btnNavigateMenuClick();
|
||||
return false;
|
||||
case 'ArrowUp': // Home
|
||||
e.preventDefault();
|
||||
btnNavigateMenuClick();
|
||||
return false;
|
||||
|
||||
case '0': // "O" Restart
|
||||
btnNavigateRefreshClick();
|
||||
return false;
|
||||
case '0': // "O" Restart
|
||||
btnNavigateRefreshClick();
|
||||
return false;
|
||||
|
||||
case 'F':
|
||||
case 'f':
|
||||
btnFullScreenClick();
|
||||
return false;
|
||||
case 'F':
|
||||
case 'f':
|
||||
btnFullScreenClick();
|
||||
return false;
|
||||
|
||||
default:
|
||||
default:
|
||||
}
|
||||
}
|
||||
return false;
|
||||
@@ -348,7 +354,6 @@ const documentKeydown = (e) => {
|
||||
|
||||
const btnNavigatePlayClick = () => {
|
||||
postMessage('navButton', 'playToggle');
|
||||
updateFullScreenNavigate();
|
||||
|
||||
return false;
|
||||
};
|
||||
@@ -393,3 +398,18 @@ const btnGetGpsClick = async () => {
|
||||
txtAddress.value = `${location.city}, ${location.state}`;
|
||||
});
|
||||
};
|
||||
|
||||
// check for change in full screen triggered by browser and run local functions
|
||||
const fullScreenResizeCheck = () => {
|
||||
if (fullScreenResizeCheck.wasFull && !document.fullscreenElement) {
|
||||
// leaving full screen
|
||||
exitFullScreenVisibilityChanges();
|
||||
}
|
||||
if (!fullScreenResizeCheck.wasFull && document.fullscreenElement) {
|
||||
// entering full screen
|
||||
// can't do much here because a UI interaction is required to change the full screen div element
|
||||
}
|
||||
|
||||
// store state of fullscreen element for next change detection
|
||||
fullScreenResizeCheck.wasFull = !!document.fullscreenElement;
|
||||
};
|
||||
|
||||
@@ -3,7 +3,7 @@ import { loadImg, preloadImg } from './utils/image.mjs';
|
||||
import { DateTime } from '../vendor/auto/luxon.mjs';
|
||||
import STATUS from './status.mjs';
|
||||
import WeatherDisplay from './weatherdisplay.mjs';
|
||||
import { registerDisplay } from './navigation.mjs';
|
||||
import { registerDisplay, timeZone } from './navigation.mjs';
|
||||
|
||||
class Almanac extends WeatherDisplay {
|
||||
constructor(navId, elemId) {
|
||||
@@ -94,7 +94,7 @@ class Almanac extends WeatherDisplay {
|
||||
if (iteration % 2 === 0) test = (lastPhase, testPhase) => lastPhase > threshold && testPhase <= threshold;
|
||||
|
||||
do {
|
||||
// store last phase
|
||||
// store last phase
|
||||
const lastPhase = phase;
|
||||
// calculate new phase after step
|
||||
moonDate = moonDate.plus(step);
|
||||
@@ -103,7 +103,7 @@ class Almanac extends WeatherDisplay {
|
||||
if (phase > 0.9) phase -= 1.0;
|
||||
// compare
|
||||
if (test(lastPhase, phase)) {
|
||||
// last iteration is three, return value
|
||||
// last iteration is three, return value
|
||||
if (iteration >= 3) break;
|
||||
// iterate recursively
|
||||
return this.getMoonTransition(threshold, phaseName, moonDate, iteration + 1);
|
||||
@@ -123,10 +123,10 @@ class Almanac extends WeatherDisplay {
|
||||
// sun and moon data
|
||||
this.elem.querySelector('.day-1').innerHTML = Today.toLocaleString({ weekday: 'long' });
|
||||
this.elem.querySelector('.day-2').innerHTML = Tomorrow.toLocaleString({ weekday: 'long' });
|
||||
this.elem.querySelector('.rise-1').innerHTML = DateTime.fromJSDate(info.sun[0].sunrise).toLocaleString(DateTime.TIME_SIMPLE).toLowerCase();
|
||||
this.elem.querySelector('.rise-2').innerHTML = DateTime.fromJSDate(info.sun[1].sunrise).toLocaleString(DateTime.TIME_SIMPLE).toLowerCase();
|
||||
this.elem.querySelector('.set-1').innerHTML = DateTime.fromJSDate(info.sun[0].sunset).toLocaleString(DateTime.TIME_SIMPLE).toLowerCase();
|
||||
this.elem.querySelector('.set-2').innerHTML = DateTime.fromJSDate(info.sun[1].sunset).toLocaleString(DateTime.TIME_SIMPLE).toLowerCase();
|
||||
this.elem.querySelector('.rise-1').innerHTML = DateTime.fromJSDate(info.sun[0].sunrise).setZone(timeZone()).toLocaleString(DateTime.TIME_SIMPLE).toLowerCase();
|
||||
this.elem.querySelector('.rise-2').innerHTML = DateTime.fromJSDate(info.sun[1].sunrise).setZone(timeZone()).toLocaleString(DateTime.TIME_SIMPLE).toLowerCase();
|
||||
this.elem.querySelector('.set-1').innerHTML = DateTime.fromJSDate(info.sun[0].sunset).setZone(timeZone()).toLocaleString(DateTime.TIME_SIMPLE).toLowerCase();
|
||||
this.elem.querySelector('.set-2').innerHTML = DateTime.fromJSDate(info.sun[1].sunset).setZone(timeZone()).toLocaleString(DateTime.TIME_SIMPLE).toLowerCase();
|
||||
|
||||
const days = info.moon.map((MoonPhase) => {
|
||||
const fill = {};
|
||||
@@ -160,15 +160,15 @@ class Almanac extends WeatherDisplay {
|
||||
|
||||
const imageName = (type) => {
|
||||
switch (type) {
|
||||
case 'Full':
|
||||
return 'images/2/Full-Moon.gif';
|
||||
case 'Last':
|
||||
return 'images/2/Last-Quarter.gif';
|
||||
case 'New':
|
||||
return 'images/2/New-Moon.gif';
|
||||
case 'First':
|
||||
default:
|
||||
return 'images/2/First-Quarter.gif';
|
||||
case 'Full':
|
||||
return 'images/2/Full-Moon.gif';
|
||||
case 'Last':
|
||||
return 'images/2/Last-Quarter.gif';
|
||||
case 'New':
|
||||
return 'images/2/New-Moon.gif';
|
||||
case 'First':
|
||||
default:
|
||||
return 'images/2/First-Quarter.gif';
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import { getWeatherIconFromIconLink } from './icons.mjs';
|
||||
import WeatherDisplay from './weatherdisplay.mjs';
|
||||
import { registerDisplay } from './navigation.mjs';
|
||||
import {
|
||||
celsiusToFahrenheit, kphToMph, pascalToInHg, metersToFeet, kilometersToMiles,
|
||||
temperature, windSpeed, pressure, distanceMeters, distanceKilometers,
|
||||
} from './utils/units.mjs';
|
||||
|
||||
// some stations prefixed do not provide all the necessary data
|
||||
@@ -56,7 +56,9 @@ class CurrentWeather extends WeatherDisplay {
|
||||
|| observations.features[0].properties.windSpeed.value === null
|
||||
|| observations.features[0].properties.textDescription === null
|
||||
|| observations.features[0].properties.textDescription === ''
|
||||
|| observations.features[0].properties.icon === null) {
|
||||
|| observations.features[0].properties.icon === null
|
||||
|| observations.features[0].properties.dewpoint.value === null
|
||||
|| observations.features[0].properties.barometricPressure.value === null) {
|
||||
observations = undefined;
|
||||
throw new Error(`Unable to get observations: ${station.properties.stationIdentifier}, trying next station`);
|
||||
}
|
||||
@@ -157,23 +159,32 @@ const shortConditions = (_condition) => {
|
||||
|
||||
// format the received data
|
||||
const parseData = (data) => {
|
||||
// get the unit converter
|
||||
const windConverter = windSpeed();
|
||||
const temperatureConverter = temperature();
|
||||
const metersConverter = distanceMeters();
|
||||
const kilometersConverter = distanceKilometers();
|
||||
const pressureConverter = pressure();
|
||||
|
||||
const observations = data.features[0].properties;
|
||||
// values from api are provided in metric
|
||||
data.observations = observations;
|
||||
data.Temperature = Math.round(observations.temperature.value);
|
||||
data.TemperatureUnit = 'C';
|
||||
data.DewPoint = Math.round(observations.dewpoint.value);
|
||||
data.Ceiling = Math.round(observations.cloudLayers[0]?.base?.value ?? 0);
|
||||
data.CeilingUnit = 'm.';
|
||||
data.Visibility = Math.round(observations.visibility.value / 1000);
|
||||
data.VisibilityUnit = ' km.';
|
||||
data.WindSpeed = Math.round(observations.windSpeed.value);
|
||||
data.Temperature = temperatureConverter(observations.temperature.value);
|
||||
data.TemperatureUnit = temperatureConverter.units;
|
||||
data.DewPoint = temperatureConverter(observations.dewpoint.value);
|
||||
data.Ceiling = metersConverter(observations.cloudLayers[0]?.base?.value ?? 0);
|
||||
data.CeilingUnit = metersConverter.units;
|
||||
data.Visibility = kilometersConverter(observations.visibility.value);
|
||||
data.VisibilityUnit = kilometersConverter.units;
|
||||
data.Pressure = pressureConverter(observations.barometricPressure.value);
|
||||
data.PressureUnit = pressureConverter.units;
|
||||
data.HeatIndex = temperatureConverter(observations.heatIndex.value);
|
||||
data.WindChill = temperatureConverter(observations.windChill.value);
|
||||
data.WindSpeed = windConverter(observations.windSpeed.value);
|
||||
data.WindDirection = directionToNSEW(observations.windDirection.value);
|
||||
data.Pressure = Math.round(observations.barometricPressure.value);
|
||||
data.HeatIndex = Math.round(observations.heatIndex.value);
|
||||
data.WindChill = Math.round(observations.windChill.value);
|
||||
data.WindGust = Math.round(observations.windGust.value);
|
||||
data.WindUnit = 'KPH';
|
||||
data.WindGust = windConverter(observations.windGust.value);
|
||||
data.WindSpeed = windConverter(data.WindSpeed);
|
||||
data.WindUnit = windConverter.units;
|
||||
data.Humidity = Math.round(observations.relativeHumidity.value);
|
||||
data.Icon = getWeatherIconFromIconLink(observations.icon);
|
||||
data.PressureDirection = '';
|
||||
@@ -184,20 +195,6 @@ const parseData = (data) => {
|
||||
if (pressureDiff > 150) data.PressureDirection = 'R';
|
||||
if (pressureDiff < -150) data.PressureDirection = 'F';
|
||||
|
||||
// convert to us units
|
||||
data.Temperature = celsiusToFahrenheit(data.Temperature);
|
||||
data.TemperatureUnit = 'F';
|
||||
data.DewPoint = celsiusToFahrenheit(data.DewPoint);
|
||||
data.Ceiling = Math.round(metersToFeet(data.Ceiling) / 100) * 100;
|
||||
data.CeilingUnit = 'ft.';
|
||||
data.Visibility = kilometersToMiles(observations.visibility.value / 1000);
|
||||
data.VisibilityUnit = ' mi.';
|
||||
data.WindSpeed = kphToMph(data.WindSpeed);
|
||||
data.WindUnit = 'MPH';
|
||||
data.Pressure = pascalToInHg(data.Pressure).toFixed(2);
|
||||
data.HeatIndex = celsiusToFahrenheit(data.HeatIndex);
|
||||
data.WindChill = celsiusToFahrenheit(data.WindChill);
|
||||
data.WindGust = kphToMph(data.WindGust);
|
||||
return data;
|
||||
};
|
||||
|
||||
|
||||
@@ -71,7 +71,7 @@ const screens = [
|
||||
(data) => `Humidity: ${data.Humidity}% Dewpoint: ${data.DewPoint}${degree}${data.TemperatureUnit}`,
|
||||
|
||||
// barometric pressure
|
||||
(data) => `Barometric Pressure: ${data.Pressure} ${data.PressureDirection}`,
|
||||
(data) => `Barometric Pressure: ${data.Pressure} ${data.PressureUnit} ${data.PressureDirection}`,
|
||||
|
||||
// wind
|
||||
(data) => {
|
||||
|
||||
@@ -8,6 +8,7 @@ import { getWeatherIconFromIconLink } from './icons.mjs';
|
||||
import { preloadImg } from './utils/image.mjs';
|
||||
import WeatherDisplay from './weatherdisplay.mjs';
|
||||
import { registerDisplay } from './navigation.mjs';
|
||||
import settings from './settings.mjs';
|
||||
|
||||
class ExtendedForecast extends WeatherDisplay {
|
||||
constructor(navId, elemId) {
|
||||
@@ -26,7 +27,7 @@ class ExtendedForecast extends WeatherDisplay {
|
||||
try {
|
||||
forecast = await json(weatherParameters.forecast, {
|
||||
data: {
|
||||
units: 'us',
|
||||
units: settings.units.value,
|
||||
},
|
||||
retryCount: 3,
|
||||
stillWaiting: () => this.stillWaiting(),
|
||||
@@ -131,7 +132,7 @@ const shortenExtendedForecastText = (long) => {
|
||||
[/dense /gi, ''],
|
||||
[/Thunderstorm/g, 'T\'Storm'],
|
||||
];
|
||||
// run all regexes
|
||||
// run all regexes
|
||||
const short = regexList.reduce((working, [regex, replace]) => working.replace(regex, replace), long);
|
||||
|
||||
let conditions = short.split(' ');
|
||||
|
||||
@@ -10,6 +10,12 @@ const hazardLevels = {
|
||||
Severe: 5,
|
||||
};
|
||||
|
||||
const hazardModifiers = {
|
||||
'Hurricane Warning': 2,
|
||||
'Tornado Warning': 3,
|
||||
'Severe Thunderstorm Warning': 1,
|
||||
};
|
||||
|
||||
class Hazards extends WeatherDisplay {
|
||||
constructor(navId, elemId, defaultActive) {
|
||||
// special height and width for scrolling
|
||||
@@ -34,8 +40,9 @@ class Hazards extends WeatherDisplay {
|
||||
url.searchParams.append('limit', 5);
|
||||
const alerts = await json(url, { retryCount: 3, stillWaiting: () => this.stillWaiting() });
|
||||
const unsortedAlerts = alerts.features ?? [];
|
||||
const sortedAlerts = unsortedAlerts.sort((a, b) => (hazardLevels[b.properties.severity] ?? 0) - (hazardLevels[a.properties.severity] ?? 0));
|
||||
const filteredAlerts = sortedAlerts.filter((hazard) => hazard.properties.severity !== 'Unknown');
|
||||
const hasImmediate = unsortedAlerts.reduce((acc, hazard) => acc || hazard.properties.urgency === 'Immediate', false);
|
||||
const sortedAlerts = unsortedAlerts.sort((a, b) => (calcSeverity(b.properties.severity, b.properties.event)) - (calcSeverity(a.properties.severity, a.properties.event)));
|
||||
const filteredAlerts = sortedAlerts.filter((hazard) => hazard.properties.severity !== 'Unknown' && (!hasImmediate || (hazard.properties.urgency === 'Immediate')));
|
||||
this.data = filteredAlerts;
|
||||
|
||||
// show alert indicator
|
||||
@@ -115,7 +122,7 @@ class Hazards extends WeatherDisplay {
|
||||
// base count change callback
|
||||
baseCountChange(count) {
|
||||
// calculate scroll offset and don't go past end
|
||||
let offsetY = Math.min(this.elem.querySelector('.hazard-lines').getBoundingClientRect().height - 390, (count - 150));
|
||||
let offsetY = Math.min(this.elem.querySelector('.hazard-lines').offsetHeight - 390, (count - 150));
|
||||
|
||||
// don't let offset go negative
|
||||
if (offsetY < 0) offsetY = 0;
|
||||
@@ -134,7 +141,26 @@ class Hazards extends WeatherDisplay {
|
||||
this.getDataCallbacks.push(() => resolve(this.data));
|
||||
});
|
||||
}
|
||||
|
||||
// after we roll through the hazards once, don't display again until the next refresh (10 minutes)
|
||||
screenIndexFromBaseCount() {
|
||||
const superValue = super.screenIndexFromBaseCount();
|
||||
// false is returned when we reach the end of the scroll
|
||||
if (superValue === false) {
|
||||
// set total screens to zero to take this out of the rotation
|
||||
this.timing.totalScreens = 0;
|
||||
}
|
||||
// return the value as expected
|
||||
return superValue;
|
||||
}
|
||||
}
|
||||
|
||||
const calcSeverity = (severity, event) => {
|
||||
// base severity plus some modifiers for specific types of warnings
|
||||
const baseSeverity = hazardLevels[severity] ?? 0;
|
||||
const modifiedSeverity = hazardModifiers[event] ?? 0;
|
||||
return baseSeverity + modifiedSeverity;
|
||||
};
|
||||
|
||||
// register display
|
||||
registerDisplay(new Hazards(0, 'hazards', true));
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import STATUS from './status.mjs';
|
||||
import getHourlyData from './hourly.mjs';
|
||||
import WeatherDisplay from './weatherdisplay.mjs';
|
||||
import { registerDisplay } from './navigation.mjs';
|
||||
import { registerDisplay, timeZone } from './navigation.mjs';
|
||||
import { DateTime } from '../vendor/auto/luxon.mjs';
|
||||
|
||||
class HourlyGraph extends WeatherDisplay {
|
||||
@@ -38,7 +38,7 @@ class HourlyGraph extends WeatherDisplay {
|
||||
const skyCover = data.map((d) => d.skyCover);
|
||||
|
||||
this.data = {
|
||||
skyCover, temperature, probabilityOfPrecipitation,
|
||||
skyCover, temperature, probabilityOfPrecipitation, temperatureUnit: data[0].temperatureUnit,
|
||||
};
|
||||
|
||||
this.setStatus(STATUS.loaded);
|
||||
@@ -107,6 +107,9 @@ class HourlyGraph extends WeatherDisplay {
|
||||
// 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}`;
|
||||
|
||||
super.drawCanvas();
|
||||
this.finishDraw();
|
||||
}
|
||||
@@ -142,7 +145,7 @@ const drawPath = (path, ctx, options) => {
|
||||
};
|
||||
|
||||
// format as 1p, 12a, etc.
|
||||
const formatTime = (time) => time.toFormat('ha').slice(0, -1);
|
||||
const formatTime = (time) => time.setZone(timeZone()).toFormat('ha').slice(0, -1);
|
||||
|
||||
// register display
|
||||
registerDisplay(new HourlyGraph(4, 'hourly-graph'));
|
||||
|
||||
@@ -3,11 +3,11 @@
|
||||
import STATUS from './status.mjs';
|
||||
import { DateTime, Interval, Duration } from '../vendor/auto/luxon.mjs';
|
||||
import { json } from './utils/fetch.mjs';
|
||||
import { celsiusToFahrenheit, kilometersToMiles } from './utils/units.mjs';
|
||||
import { temperature as temperatureUnit, distanceKilometers } from './utils/units.mjs';
|
||||
import { getHourlyIcon } from './icons.mjs';
|
||||
import { directionToNSEW } from './utils/calc.mjs';
|
||||
import WeatherDisplay from './weatherdisplay.mjs';
|
||||
import { registerDisplay } from './navigation.mjs';
|
||||
import { registerDisplay, timeZone } from './navigation.mjs';
|
||||
import getSun from './almanac.mjs';
|
||||
|
||||
class Hourly extends WeatherDisplay {
|
||||
@@ -56,7 +56,7 @@ class Hourly extends WeatherDisplay {
|
||||
const list = this.elem.querySelector('.hourly-lines');
|
||||
list.innerHTML = '';
|
||||
|
||||
const startingHour = DateTime.local();
|
||||
const startingHour = DateTime.local().setZone(timeZone());
|
||||
|
||||
const lines = this.data.map((data, index) => {
|
||||
const fillValues = {};
|
||||
@@ -66,8 +66,8 @@ class Hourly extends WeatherDisplay {
|
||||
fillValues.hour = formattedHour;
|
||||
|
||||
// temperatures, convert to strings with no decimal
|
||||
const temperature = Math.round(data.temperature).toString().padStart(3);
|
||||
const feelsLike = Math.round(data.apparentTemperature).toString().padStart(3);
|
||||
const temperature = data.temperature.toString().padStart(3);
|
||||
const feelsLike = data.apparentTemperature.toString().padStart(3);
|
||||
fillValues.temp = temperature;
|
||||
// only plot apparent temperature if there is a difference
|
||||
// if (temperature !== feelsLike) line.querySelector('.like').innerHTML = feelsLike;
|
||||
@@ -109,7 +109,7 @@ class Hourly extends WeatherDisplay {
|
||||
// base count change callback
|
||||
baseCountChange(count) {
|
||||
// calculate scroll offset and don't go past end
|
||||
let offsetY = Math.min(this.elem.querySelector('.hourly-lines').getBoundingClientRect().height - 289, (count - 150));
|
||||
let offsetY = Math.min(this.elem.querySelector('.hourly-lines').offsetHeight - 289, (count - 150));
|
||||
|
||||
// don't let offset go negative
|
||||
if (offsetY < 0) offsetY = 0;
|
||||
@@ -132,6 +132,11 @@ class Hourly extends WeatherDisplay {
|
||||
|
||||
// extract specific values from forecast and format as an array
|
||||
const parseForecast = async (data) => {
|
||||
// get unit converters
|
||||
const temperatureConverter = temperatureUnit();
|
||||
const distanceConverter = distanceKilometers();
|
||||
|
||||
// parse data
|
||||
const temperature = expand(data.temperature.values);
|
||||
const apparentTemperature = expand(data.apparentTemperature.values);
|
||||
const windSpeed = expand(data.windSpeed.values);
|
||||
@@ -145,9 +150,11 @@ const parseForecast = async (data) => {
|
||||
const icons = await determineIcon(skyCover, weather, iceAccumulation, probabilityOfPrecipitation, snowfallAmount, windSpeed);
|
||||
|
||||
return temperature.map((val, idx) => ({
|
||||
temperature: celsiusToFahrenheit(temperature[idx]),
|
||||
apparentTemperature: celsiusToFahrenheit(apparentTemperature[idx]),
|
||||
windSpeed: kilometersToMiles(windSpeed[idx]),
|
||||
temperature: temperatureConverter(temperature[idx]),
|
||||
temperatureUnit: temperatureConverter.units,
|
||||
apparentTemperature: temperatureConverter(apparentTemperature[idx]),
|
||||
windSpeed: distanceConverter(windSpeed[idx]),
|
||||
windUnit: distanceConverter.units,
|
||||
windDirection: directionToNSEW(windDirection[idx]),
|
||||
probabilityOfPrecipitation: probabilityOfPrecipitation[idx],
|
||||
skyCover: skyCover[idx],
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
/* eslint-disable unicorn/consistent-function-scoping */
|
||||
/* spell-checker: disable */
|
||||
|
||||
const getWeatherRegionalIconFromIconLink = (link, _isNightTime) => {
|
||||
@@ -21,136 +20,138 @@ const getWeatherRegionalIconFromIconLink = (link, _isNightTime) => {
|
||||
|
||||
// find the icon
|
||||
switch (conditionName + (isNightTime ? '-n' : '')) {
|
||||
case 'skc':
|
||||
case 'hot':
|
||||
case 'haze':
|
||||
return addPath('Sunny.gif');
|
||||
case 'skc':
|
||||
case 'hot':
|
||||
case 'haze':
|
||||
return addPath('Sunny.gif');
|
||||
|
||||
case 'skc-n':
|
||||
case 'nskc':
|
||||
case 'nskc-n':
|
||||
case 'cold-n':
|
||||
return addPath('Clear-1992.gif');
|
||||
case 'skc-n':
|
||||
case 'nskc':
|
||||
case 'nskc-n':
|
||||
case 'cold-n':
|
||||
return addPath('Clear-1992.gif');
|
||||
|
||||
case 'bkn':
|
||||
return addPath('Mostly-Cloudy-1994-2.gif');
|
||||
case 'bkn':
|
||||
return addPath('Mostly-Cloudy-1994-2.gif');
|
||||
|
||||
case 'bkn-n':
|
||||
case 'few-n':
|
||||
case 'nfew-n':
|
||||
case 'nfew':
|
||||
return addPath('Partly-Clear-1994-2.gif');
|
||||
case 'bkn-n':
|
||||
case 'few-n':
|
||||
case 'nfew-n':
|
||||
case 'nfew':
|
||||
return addPath('Partly-Clear-1994-2.gif');
|
||||
|
||||
case 'sct':
|
||||
case 'few':
|
||||
return addPath('Partly-Cloudy.gif');
|
||||
case 'sct':
|
||||
case 'few':
|
||||
return addPath('Partly-Cloudy.gif');
|
||||
|
||||
case 'sct-n':
|
||||
case 'nsct':
|
||||
case 'nsct-n':
|
||||
return addPath('Mostly-Clear.gif');
|
||||
case 'sct-n':
|
||||
case 'nsct':
|
||||
case 'nsct-n':
|
||||
return addPath('Mostly-Clear.gif');
|
||||
|
||||
case 'ovc':
|
||||
case 'ovc-n':
|
||||
return addPath('Cloudy.gif');
|
||||
case 'ovc':
|
||||
case 'ovc-n':
|
||||
return addPath('Cloudy.gif');
|
||||
|
||||
case 'fog':
|
||||
case 'fog-n':
|
||||
return addPath('Fog.gif');
|
||||
case 'fog':
|
||||
case 'fog-n':
|
||||
return addPath('Fog.gif');
|
||||
|
||||
case 'rain_sleet':
|
||||
return addPath('Sleet.gif');
|
||||
case 'rain_sleet':
|
||||
return addPath('Sleet.gif');
|
||||
|
||||
case 'rain_showers':
|
||||
case 'rain_showers_high':
|
||||
return addPath('Scattered-Showers-1994-2.gif');
|
||||
case 'rain_showers':
|
||||
case 'rain_showers_high':
|
||||
return addPath('Scattered-Showers-1994-2.gif');
|
||||
|
||||
case 'rain_showers-n':
|
||||
case 'rain_showers_high-n':
|
||||
return addPath('Scattered-Showers-Night-1994-2.gif');
|
||||
case 'rain_showers-n':
|
||||
case 'rain_showers_high-n':
|
||||
return addPath('Scattered-Showers-Night-1994-2.gif');
|
||||
|
||||
case 'rain':
|
||||
case 'rain-n':
|
||||
return addPath('Rain-1992.gif');
|
||||
case 'rain':
|
||||
case 'rain-n':
|
||||
return addPath('Rain-1992.gif');
|
||||
|
||||
// case 'snow':
|
||||
// return addPath('Light-Snow.gif');
|
||||
// break;
|
||||
// case 'snow':
|
||||
// return addPath('Light-Snow.gif');
|
||||
// break;
|
||||
|
||||
// case 'cc_snowshowers.gif':
|
||||
// //case "heavy-snow.gif":
|
||||
// return addPath('AM-Snow-1994.gif');
|
||||
// break;
|
||||
// case 'cc_snowshowers.gif':
|
||||
// //case "heavy-snow.gif":
|
||||
// return addPath('AM-Snow-1994.gif');
|
||||
// break;
|
||||
|
||||
case 'snow':
|
||||
case 'snow-n':
|
||||
if (value > 50) return addPath('Heavy-Snow-1994-2.gif');
|
||||
return addPath('Light-Snow.gif');
|
||||
case 'snow':
|
||||
case 'snow-n':
|
||||
if (value > 50) return addPath('Heavy-Snow-1994-2.gif');
|
||||
return addPath('Light-Snow.gif');
|
||||
|
||||
case 'rain_snow':
|
||||
case 'rain_snow-n':
|
||||
return addPath('Rain-Snow-1992.gif');
|
||||
case 'rain_snow':
|
||||
case 'rain_snow-n':
|
||||
return addPath('Rain-Snow-1992.gif');
|
||||
|
||||
case 'snow_fzra':
|
||||
case 'snow_fzra-n':
|
||||
return addPath('Freezing-Rain-Snow-1992.gif');
|
||||
case 'snow_fzra':
|
||||
case 'snow_fzra-n':
|
||||
return addPath('Freezing-Rain-Snow-1992.gif');
|
||||
|
||||
case 'fzra':
|
||||
case 'fzra-n':
|
||||
case 'rain_fzra':
|
||||
case 'rain_fzra-n':
|
||||
return addPath('Freezing-Rain-1992.gif');
|
||||
case 'fzra':
|
||||
case 'fzra-n':
|
||||
case 'rain_fzra':
|
||||
case 'rain_fzra-n':
|
||||
return addPath('Freezing-Rain-1992.gif');
|
||||
|
||||
case 'snow_sleet':
|
||||
case 'snow_sleet-n':
|
||||
return addPath('Snow and Sleet.gif');
|
||||
case 'snow_sleet':
|
||||
case 'snow_sleet-n':
|
||||
return addPath('Snow and Sleet.gif');
|
||||
|
||||
case 'sleet':
|
||||
case 'sleet-n':
|
||||
return addPath('Sleet.gif');
|
||||
case 'sleet':
|
||||
case 'sleet-n':
|
||||
return addPath('Sleet.gif');
|
||||
|
||||
case 'tsra_sct':
|
||||
case 'tsra':
|
||||
return addPath('Scattered-Tstorms-1994-2.gif');
|
||||
case 'tsra_sct':
|
||||
case 'tsra':
|
||||
return addPath('Scattered-Tstorms-1994-2.gif');
|
||||
|
||||
case 'tsra_sct-n':
|
||||
case 'tsra-n':
|
||||
return addPath('Scattered-Tstorms-Night-1994-2.gif');
|
||||
case 'tsra_sct-n':
|
||||
case 'tsra-n':
|
||||
return addPath('Scattered-Tstorms-Night-1994-2.gif');
|
||||
|
||||
case 'tsra_hi':
|
||||
case 'tsra_hi-n':
|
||||
case 'hurricane':
|
||||
case 'tropical_storm':
|
||||
return addPath('Thunderstorm.gif');
|
||||
case 'tsra_hi':
|
||||
case 'tsra_hi-n':
|
||||
case 'hurricane':
|
||||
case 'tropical_storm':
|
||||
case 'hurricane-n':
|
||||
case 'tropical_storm-n':
|
||||
return addPath('Thunderstorm.gif');
|
||||
|
||||
case 'wind':
|
||||
case 'wind_few':
|
||||
case 'wind_sct':
|
||||
case 'wind_bkn':
|
||||
case 'wind_ovc':
|
||||
case 'wind-n':
|
||||
case 'wind_few-n':
|
||||
case 'wind_bkn-n':
|
||||
case 'wind_ovc-n':
|
||||
return addPath('Wind.gif');
|
||||
case 'wind':
|
||||
case 'wind_few':
|
||||
case 'wind_sct':
|
||||
case 'wind_bkn':
|
||||
case 'wind_ovc':
|
||||
case 'wind-n':
|
||||
case 'wind_few-n':
|
||||
case 'wind_bkn-n':
|
||||
case 'wind_ovc-n':
|
||||
return addPath('Wind.gif');
|
||||
|
||||
case 'wind_skc':
|
||||
return addPath('Sunny-Wind-1994.gif');
|
||||
case 'wind_skc':
|
||||
return addPath('Sunny-Wind-1994.gif');
|
||||
|
||||
case 'wind_skc-n':
|
||||
case 'wind_sct-n':
|
||||
return addPath('Clear-Wind-1994.gif');
|
||||
case 'wind_skc-n':
|
||||
case 'wind_sct-n':
|
||||
return addPath('Clear-Wind-1994.gif');
|
||||
|
||||
case 'blizzard':
|
||||
case 'blizzard-n':
|
||||
return addPath('Blowing Snow.gif');
|
||||
case 'blizzard':
|
||||
case 'blizzard-n':
|
||||
return addPath('Blowing Snow.gif');
|
||||
|
||||
case 'cold':
|
||||
return addPath('cold.gif');
|
||||
case 'cold':
|
||||
return addPath('cold.gif');
|
||||
|
||||
default:
|
||||
console.log(`Unable to locate regional icon for ${conditionName} ${link} ${isNightTime}`);
|
||||
return false;
|
||||
default:
|
||||
console.log(`Unable to locate regional icon for ${conditionName} ${link} ${isNightTime}`);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -176,120 +177,122 @@ const getWeatherIconFromIconLink = (link, _isNightTime) => {
|
||||
|
||||
// find the icon
|
||||
switch (conditionName + (isNightTime ? '-n' : '')) {
|
||||
case 'skc':
|
||||
case 'hot':
|
||||
case 'haze':
|
||||
case 'cold':
|
||||
return addPath('CC_Clear1.gif');
|
||||
case 'skc':
|
||||
case 'hot':
|
||||
case 'haze':
|
||||
case 'cold':
|
||||
return addPath('CC_Clear1.gif');
|
||||
|
||||
case 'skc-n':
|
||||
case 'nskc':
|
||||
case 'nskc-n':
|
||||
case 'cold-n':
|
||||
return addPath('CC_Clear0.gif');
|
||||
case 'skc-n':
|
||||
case 'nskc':
|
||||
case 'nskc-n':
|
||||
case 'cold-n':
|
||||
return addPath('CC_Clear0.gif');
|
||||
|
||||
case 'sct':
|
||||
case 'few':
|
||||
case 'bkn':
|
||||
return addPath('CC_PartlyCloudy1.gif');
|
||||
case 'sct':
|
||||
case 'few':
|
||||
case 'bkn':
|
||||
return addPath('CC_PartlyCloudy1.gif');
|
||||
|
||||
case 'bkn-n':
|
||||
case 'few-n':
|
||||
case 'nfew-n':
|
||||
case 'nfew':
|
||||
case 'sct-n':
|
||||
case 'nsct':
|
||||
case 'nsct-n':
|
||||
return addPath('CC_PartlyCloudy0.gif');
|
||||
case 'bkn-n':
|
||||
case 'few-n':
|
||||
case 'nfew-n':
|
||||
case 'nfew':
|
||||
case 'sct-n':
|
||||
case 'nsct':
|
||||
case 'nsct-n':
|
||||
return addPath('CC_PartlyCloudy0.gif');
|
||||
|
||||
case 'ovc':
|
||||
case 'novc':
|
||||
case 'ovc-n':
|
||||
return addPath('CC_Cloudy.gif');
|
||||
case 'ovc':
|
||||
case 'novc':
|
||||
case 'ovc-n':
|
||||
return addPath('CC_Cloudy.gif');
|
||||
|
||||
case 'fog':
|
||||
case 'fog-n':
|
||||
return addPath('CC_Fog.gif');
|
||||
case 'fog':
|
||||
case 'fog-n':
|
||||
return addPath('CC_Fog.gif');
|
||||
|
||||
case 'rain_sleet':
|
||||
case 'rain_sleet-n':
|
||||
case 'sleet':
|
||||
case 'sleet-n':
|
||||
return addPath('Sleet.gif');
|
||||
case 'rain_sleet':
|
||||
case 'rain_sleet-n':
|
||||
case 'sleet':
|
||||
case 'sleet-n':
|
||||
return addPath('Sleet.gif');
|
||||
|
||||
case 'rain_showers':
|
||||
case 'rain_showers_high':
|
||||
return addPath('CC_Showers.gif');
|
||||
case 'rain_showers':
|
||||
case 'rain_showers_high':
|
||||
return addPath('CC_Showers.gif');
|
||||
|
||||
case 'rain_showers-n':
|
||||
case 'rain_showers_high-n':
|
||||
return addPath('CC_Showers.gif');
|
||||
case 'rain_showers-n':
|
||||
case 'rain_showers_high-n':
|
||||
return addPath('CC_Showers.gif');
|
||||
|
||||
case 'rain':
|
||||
case 'rain-n':
|
||||
return addPath('CC_Rain.gif');
|
||||
case 'rain':
|
||||
case 'rain-n':
|
||||
return addPath('CC_Rain.gif');
|
||||
|
||||
// case 'snow':
|
||||
// return addPath('Light-Snow.gif');
|
||||
// break;
|
||||
// case 'snow':
|
||||
// return addPath('Light-Snow.gif');
|
||||
// break;
|
||||
|
||||
// case 'cc_snowshowers.gif':
|
||||
// //case "heavy-snow.gif":
|
||||
// return addPath('AM-Snow-1994.gif');
|
||||
// break;
|
||||
// case 'cc_snowshowers.gif':
|
||||
// //case "heavy-snow.gif":
|
||||
// return addPath('AM-Snow-1994.gif');
|
||||
// break;
|
||||
|
||||
case 'snow':
|
||||
case 'snow-n':
|
||||
if (value > 50) return addPath('CC_Snow.gif');
|
||||
return addPath('CC_SnowShowers.gif');
|
||||
case 'snow':
|
||||
case 'snow-n':
|
||||
if (value > 50) return addPath('CC_Snow.gif');
|
||||
return addPath('CC_SnowShowers.gif');
|
||||
|
||||
case 'rain_snow':
|
||||
return addPath('CC_RainSnow.gif');
|
||||
case 'rain_snow':
|
||||
return addPath('CC_RainSnow.gif');
|
||||
|
||||
case 'snow_fzra':
|
||||
case 'snow_fzra-n':
|
||||
case 'fzra':
|
||||
case 'fzra-n':
|
||||
case 'rain_fzra':
|
||||
case 'rain_fzra-n':
|
||||
return addPath('CC_FreezingRain.gif');
|
||||
case 'snow_fzra':
|
||||
case 'snow_fzra-n':
|
||||
case 'fzra':
|
||||
case 'fzra-n':
|
||||
case 'rain_fzra':
|
||||
case 'rain_fzra-n':
|
||||
return addPath('CC_FreezingRain.gif');
|
||||
|
||||
case 'snow_sleet':
|
||||
return addPath('Snow-Sleet.gif');
|
||||
case 'snow_sleet':
|
||||
return addPath('Snow-Sleet.gif');
|
||||
|
||||
case 'tsra_sct':
|
||||
case 'tsra':
|
||||
return addPath('EF_ScatTstorms.gif');
|
||||
case 'tsra_sct':
|
||||
case 'tsra':
|
||||
return addPath('EF_ScatTstorms.gif');
|
||||
|
||||
case 'tsra_sct-n':
|
||||
case 'tsra-n':
|
||||
return addPath('CC_TStorm.gif');
|
||||
case 'tsra_sct-n':
|
||||
case 'tsra-n':
|
||||
return addPath('CC_TStorm.gif');
|
||||
|
||||
case 'tsra_hi':
|
||||
case 'tsra_hi-n':
|
||||
case 'hurricane':
|
||||
case 'tropical_storm':
|
||||
return addPath('CC_TStorm.gif');
|
||||
case 'tsra_hi':
|
||||
case 'tsra_hi-n':
|
||||
case 'hurricane':
|
||||
case 'tropical_storm':
|
||||
case 'hurricane-n':
|
||||
case 'tropical_storm-n':
|
||||
return addPath('CC_TStorm.gif');
|
||||
|
||||
case 'wind_few':
|
||||
case 'wind_sct':
|
||||
case 'wind_bkn':
|
||||
case 'wind_ovc':
|
||||
case 'wind_skc':
|
||||
case 'wind_few-n':
|
||||
case 'wind_bkn-n':
|
||||
case 'wind_ovc-n':
|
||||
case 'wind_skc-n':
|
||||
case 'wind_sct-n':
|
||||
return addPath('CC_Windy.gif');
|
||||
case 'wind_few':
|
||||
case 'wind_sct':
|
||||
case 'wind_bkn':
|
||||
case 'wind_ovc':
|
||||
case 'wind_skc':
|
||||
case 'wind_few-n':
|
||||
case 'wind_bkn-n':
|
||||
case 'wind_ovc-n':
|
||||
case 'wind_skc-n':
|
||||
case 'wind_sct-n':
|
||||
return addPath('CC_Windy.gif');
|
||||
|
||||
case 'blizzard':
|
||||
case 'blizzard-n':
|
||||
return addPath('Blowing-Snow.gif');
|
||||
case 'blizzard':
|
||||
case 'blizzard-n':
|
||||
return addPath('Blowing-Snow.gif');
|
||||
|
||||
default:
|
||||
console.log(`Unable to locate icon for ${conditionName} ${link} ${isNightTime}`);
|
||||
return false;
|
||||
default:
|
||||
console.log(`Unable to locate icon for ${conditionName} ${link} ${isNightTime}`);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -3,9 +3,10 @@ import { distance as calcDistance, directionToNSEW } from './utils/calc.mjs';
|
||||
import { json } from './utils/fetch.mjs';
|
||||
import STATUS from './status.mjs';
|
||||
import { locationCleanup } from './utils/string.mjs';
|
||||
import { celsiusToFahrenheit, kphToMph } from './utils/units.mjs';
|
||||
import { temperature, windSpeed } from './utils/units.mjs';
|
||||
import WeatherDisplay from './weatherdisplay.mjs';
|
||||
import { registerDisplay } from './navigation.mjs';
|
||||
import settings from './settings.mjs';
|
||||
|
||||
class LatestObservations extends WeatherDisplay {
|
||||
constructor(navId, elemId) {
|
||||
@@ -64,14 +65,22 @@ class LatestObservations extends WeatherDisplay {
|
||||
// sort array by station name
|
||||
const sortedConditions = conditions.sort((a, b) => ((a.Name < b.Name) ? -1 : 1));
|
||||
|
||||
this.elem.querySelector('.column-headers .temp.english').classList.add('show');
|
||||
this.elem.querySelector('.column-headers .temp.metric').classList.remove('show');
|
||||
if (settings.units.value === 'us') {
|
||||
this.elem.querySelector('.column-headers .temp.english').classList.add('show');
|
||||
this.elem.querySelector('.column-headers .temp.metric').classList.remove('show');
|
||||
} else {
|
||||
this.elem.querySelector('.column-headers .temp.english').classList.remove('show');
|
||||
this.elem.querySelector('.column-headers .temp.metric').classList.add('show');
|
||||
}
|
||||
// get unit converters
|
||||
const windConverter = windSpeed();
|
||||
const temperatureConverter = temperature();
|
||||
|
||||
const lines = sortedConditions.map((condition) => {
|
||||
const windDirection = directionToNSEW(condition.windDirection.value);
|
||||
|
||||
const Temperature = Math.round(celsiusToFahrenheit(condition.temperature.value));
|
||||
const WindSpeed = Math.round(kphToMph(condition.windSpeed.value));
|
||||
const Temperature = temperatureConverter(condition.temperature.value);
|
||||
const WindSpeed = windConverter(condition.windSpeed.value);
|
||||
|
||||
const fill = {
|
||||
location: locationCleanup(condition.city).substr(0, 14),
|
||||
@@ -94,6 +103,8 @@ class LatestObservations extends WeatherDisplay {
|
||||
linesContainer.innerHTML = '';
|
||||
linesContainer.append(...lines);
|
||||
|
||||
// update temperature unit header
|
||||
|
||||
this.finishDraw();
|
||||
}
|
||||
}
|
||||
@@ -122,8 +133,8 @@ const getStations = async (stations) => {
|
||||
const data = await json(`https://api.weather.gov/stations/${station.id}/observations/latest`, { retryCount: 1, stillWaiting: () => this.stillWaiting() });
|
||||
// test for temperature, weather and wind values present
|
||||
if (data.properties.temperature.value === null
|
||||
|| data.properties.textDescription === ''
|
||||
|| data.properties.windSpeed.value === null) return false;
|
||||
|| data.properties.textDescription === ''
|
||||
|| data.properties.windSpeed.value === null) return false;
|
||||
// format the return values
|
||||
return {
|
||||
...data.properties,
|
||||
|
||||
@@ -4,6 +4,7 @@ import STATUS from './status.mjs';
|
||||
import { json } from './utils/fetch.mjs';
|
||||
import WeatherDisplay from './weatherdisplay.mjs';
|
||||
import { registerDisplay } from './navigation.mjs';
|
||||
import settings from './settings.mjs';
|
||||
|
||||
class LocalForecast extends WeatherDisplay {
|
||||
constructor(navId, elemId) {
|
||||
@@ -61,7 +62,7 @@ class LocalForecast extends WeatherDisplay {
|
||||
try {
|
||||
return await json(weatherParameters.forecast, {
|
||||
data: {
|
||||
units: 'us',
|
||||
units: settings.units.value,
|
||||
},
|
||||
retryCount: 3,
|
||||
stillWaiting: () => this.stillWaiting(),
|
||||
|
||||
@@ -238,30 +238,30 @@ const setPlaying = (newValue) => {
|
||||
// handle all navigation buttons
|
||||
const handleNavButton = (button) => {
|
||||
switch (button) {
|
||||
case 'play':
|
||||
setPlaying(true);
|
||||
break;
|
||||
case 'playToggle':
|
||||
setPlaying(!playing);
|
||||
break;
|
||||
case 'stop':
|
||||
setPlaying(false);
|
||||
break;
|
||||
case 'next':
|
||||
setPlaying(false);
|
||||
navTo(msg.command.nextFrame);
|
||||
break;
|
||||
case 'previous':
|
||||
setPlaying(false);
|
||||
navTo(msg.command.previousFrame);
|
||||
break;
|
||||
case 'menu':
|
||||
setPlaying(false);
|
||||
progress.showCanvas();
|
||||
hideAllCanvases();
|
||||
break;
|
||||
default:
|
||||
console.error(`Unknown navButton ${button}`);
|
||||
case 'play':
|
||||
setPlaying(true);
|
||||
break;
|
||||
case 'playToggle':
|
||||
setPlaying(!playing);
|
||||
break;
|
||||
case 'stop':
|
||||
setPlaying(false);
|
||||
break;
|
||||
case 'next':
|
||||
setPlaying(false);
|
||||
navTo(msg.command.nextFrame);
|
||||
break;
|
||||
case 'previous':
|
||||
setPlaying(false);
|
||||
navTo(msg.command.previousFrame);
|
||||
break;
|
||||
case 'menu':
|
||||
setPlaying(false);
|
||||
progress.showCanvas();
|
||||
hideAllCanvases();
|
||||
break;
|
||||
default:
|
||||
console.error(`Unknown navButton ${button}`);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -157,7 +157,7 @@ class Radar extends WeatherDisplay {
|
||||
minute,
|
||||
}, {
|
||||
zone: 'UTC',
|
||||
}).setZone();
|
||||
}).setZone(timeZone());
|
||||
} else {
|
||||
time = DateTime.fromHTTP(response.headers.get('last-modified')).setZone(timeZone());
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import STATUS from './status.mjs';
|
||||
import { distance as calcDistance } from './utils/calc.mjs';
|
||||
import { json } from './utils/fetch.mjs';
|
||||
import { celsiusToFahrenheit } from './utils/units.mjs';
|
||||
import { temperature as temperatureUnit } from './utils/units.mjs';
|
||||
import { getWeatherRegionalIconFromIconLink } from './icons.mjs';
|
||||
import { preloadImg } from './utils/image.mjs';
|
||||
import { DateTime } from '../vendor/auto/luxon.mjs';
|
||||
@@ -59,7 +59,7 @@ class RegionalForecast extends WeatherDisplay {
|
||||
const regionalCities = [];
|
||||
combinedCities.forEach((city) => {
|
||||
if (city.lat > minMaxLatLon.minLat && city.lat < minMaxLatLon.maxLat
|
||||
&& city.lon > minMaxLatLon.minLon && city.lon < minMaxLatLon.maxLon - 1) {
|
||||
&& city.lon > minMaxLatLon.minLon && city.lon < minMaxLatLon.maxLon - 1) {
|
||||
// default to 1 for cities loaded from RegionalCities, use value calculate above for remaining stations
|
||||
const targetDist = city.targetDistance || 1;
|
||||
// Only add the city as long as it isn't within set distance degree of any other city already in the array.
|
||||
@@ -71,6 +71,9 @@ class RegionalForecast extends WeatherDisplay {
|
||||
}
|
||||
});
|
||||
|
||||
// get a unit converter
|
||||
const temperatureConverter = temperatureUnit();
|
||||
|
||||
// get regional forecasts and observations (the two are intertwined due to the design of api.weather.gov)
|
||||
const regionalDataAll = await Promise.all(regionalCities.map(async (city) => {
|
||||
try {
|
||||
@@ -93,7 +96,7 @@ class RegionalForecast extends WeatherDisplay {
|
||||
// format the observation the same as the forecast
|
||||
const regionalObservation = {
|
||||
daytime: !!/\/day\//.test(observation.icon),
|
||||
temperature: celsiusToFahrenheit(observation.temperature.value),
|
||||
temperature: temperatureConverter(observation.temperature.value),
|
||||
name: utils.formatCity(city.city),
|
||||
icon: observation.icon,
|
||||
x: cityXY.x,
|
||||
|
||||
@@ -4,20 +4,32 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
init();
|
||||
});
|
||||
|
||||
const settings = {};
|
||||
// default speed
|
||||
const settings = { speed: { value: 1.0 } };
|
||||
|
||||
const init = () => {
|
||||
// create settings
|
||||
settings.wide = new Setting('wide', 'Widescreen', 'boolean', false, wideScreenChange, true);
|
||||
settings.wide = new Setting('wide', 'Widescreen', 'checkbox', false, wideScreenChange, true);
|
||||
settings.kiosk = new Setting('kiosk', 'Kiosk', 'boolean', false, kioskChange, false);
|
||||
settings.speed = new Setting('speed', 'Speed', 'select', 1.0, null, true, [
|
||||
[0.5, 'Very Fast'],
|
||||
[0.75, 'Fast'],
|
||||
[1.0, 'Normal'],
|
||||
[1.25, 'Slow'],
|
||||
[1.5, 'Very Slow'],
|
||||
]);
|
||||
settings.units = new Setting('units', 'Units', 'select', 'us', unitChange, true, [
|
||||
['us', 'US'],
|
||||
['si', 'Metric'],
|
||||
]);
|
||||
|
||||
// generate checkboxes
|
||||
const checkboxes = Object.values(settings).map((d) => d.generateCheckbox());
|
||||
// generate html objects
|
||||
const settingHtml = Object.values(settings).map((d) => d.generate());
|
||||
|
||||
// write to page
|
||||
const settingsSection = document.querySelector('#settings');
|
||||
settingsSection.innerHTML = '';
|
||||
settingsSection.append(...checkboxes);
|
||||
settingsSection.append(...settingHtml);
|
||||
};
|
||||
|
||||
const wideScreenChange = (value) => {
|
||||
@@ -39,4 +51,13 @@ const kioskChange = (value) => {
|
||||
}
|
||||
};
|
||||
|
||||
const unitChange = () => {
|
||||
// reload the data at the top level to refresh units
|
||||
// after the initial load
|
||||
if (unitChange.firstRunDone) {
|
||||
window.location.reload();
|
||||
}
|
||||
unitChange.firstRunDone = true;
|
||||
};
|
||||
|
||||
export default settings;
|
||||
|
||||
@@ -7,7 +7,13 @@ const specialMappings = {
|
||||
|
||||
const init = () => {
|
||||
// add action to existing link
|
||||
document.querySelector('#share-link').addEventListener('click', createLink);
|
||||
const shareLink = document.querySelector('#share-link');
|
||||
shareLink.addEventListener('click', createLink);
|
||||
|
||||
// if navigator.clipboard does not exist, change text
|
||||
if (!navigator?.clipboard) {
|
||||
shareLink.textContent = 'Get Permalink';
|
||||
}
|
||||
};
|
||||
|
||||
const createLink = async (e) => {
|
||||
@@ -25,6 +31,14 @@ const createLink = async (e) => {
|
||||
}
|
||||
});
|
||||
|
||||
// get all select boxes
|
||||
const selects = document.querySelectorAll('select');
|
||||
[...selects].forEach((elem) => {
|
||||
if (elem?.id) {
|
||||
queryStringElements[elem.id] = elem?.value ?? 0;
|
||||
}
|
||||
});
|
||||
|
||||
// add the location string
|
||||
queryStringElements.latLonQuery = localStorage.getItem('latLonQuery');
|
||||
queryStringElements.latLon = localStorage.getItem('latLon');
|
||||
@@ -33,10 +47,23 @@ const createLink = async (e) => {
|
||||
|
||||
const url = new URL(`?${queryString}`, document.location.href);
|
||||
|
||||
// send to proper function based on availability of clipboard
|
||||
if (navigator?.clipboard) {
|
||||
copyToClipboard(url);
|
||||
} else {
|
||||
writeLinkToPage(url);
|
||||
}
|
||||
};
|
||||
|
||||
const copyToClipboard = async (url) => {
|
||||
try {
|
||||
// write to clipboard
|
||||
await navigator.clipboard.writeText(url.toString());
|
||||
// alert user
|
||||
const confirmSpan = document.querySelector('#share-link-copied');
|
||||
confirmSpan.style.display = 'inline';
|
||||
|
||||
// hide confirm text after 5 seconds
|
||||
setTimeout(() => {
|
||||
confirmSpan.style.display = 'none';
|
||||
}, 5000);
|
||||
@@ -45,6 +72,18 @@ const createLink = async (e) => {
|
||||
}
|
||||
};
|
||||
|
||||
const writeLinkToPage = (url) => {
|
||||
// get elements
|
||||
const shareLinkInstructions = document.querySelector('#share-link-instructions');
|
||||
const shareLinkUrl = shareLinkInstructions.querySelector('#share-link-url');
|
||||
// populate url and display
|
||||
shareLinkUrl.value = url;
|
||||
shareLinkInstructions.style.display = 'inline';
|
||||
// highlight for convenience
|
||||
shareLinkUrl.focus();
|
||||
shareLinkUrl.select();
|
||||
};
|
||||
|
||||
const parseQueryString = () => {
|
||||
// return memoized result
|
||||
if (parseQueryString.params) return parseQueryString.params;
|
||||
|
||||
@@ -9,20 +9,20 @@ const STATUS = {
|
||||
|
||||
const calcStatusClass = (statusCode) => {
|
||||
switch (statusCode) {
|
||||
case STATUS.loading:
|
||||
return 'loading';
|
||||
case STATUS.loaded:
|
||||
return 'press-here';
|
||||
case STATUS.failed:
|
||||
return 'failed';
|
||||
case STATUS.noData:
|
||||
return 'no-data';
|
||||
case STATUS.disabled:
|
||||
return 'disabled';
|
||||
case STATUS.retrying:
|
||||
return 'retrying';
|
||||
default:
|
||||
return '';
|
||||
case STATUS.loading:
|
||||
return 'loading';
|
||||
case STATUS.loaded:
|
||||
return 'press-here';
|
||||
case STATUS.failed:
|
||||
return 'failed';
|
||||
case STATUS.noData:
|
||||
return 'no-data';
|
||||
case STATUS.disabled:
|
||||
return 'disabled';
|
||||
case STATUS.retrying:
|
||||
return 'retrying';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import { getWeatherRegionalIconFromIconLink } from './icons.mjs';
|
||||
import { DateTime } from '../vendor/auto/luxon.mjs';
|
||||
import WeatherDisplay from './weatherdisplay.mjs';
|
||||
import { registerDisplay } from './navigation.mjs';
|
||||
import settings from './settings.mjs';
|
||||
|
||||
class TravelForecast extends WeatherDisplay {
|
||||
constructor(navId, elemId, defaultActive) {
|
||||
@@ -34,7 +35,11 @@ class TravelForecast extends WeatherDisplay {
|
||||
try {
|
||||
// get point then forecast
|
||||
if (!city.point) throw new Error('No pre-loaded point');
|
||||
const forecast = await json(`https://api.weather.gov/gridpoints/${city.point.wfo}/${city.point.x},${city.point.y}/forecast`);
|
||||
const forecast = await json(`https://api.weather.gov/gridpoints/${city.point.wfo}/${city.point.x},${city.point.y}/forecast`, {
|
||||
data: {
|
||||
units: settings.units.value,
|
||||
},
|
||||
});
|
||||
// determine today or tomorrow (shift periods by 1 if tomorrow)
|
||||
const todayShift = forecast.properties.periods[0].isDaytime ? 0 : 1;
|
||||
// return a pared-down forecast
|
||||
@@ -131,7 +136,7 @@ class TravelForecast extends WeatherDisplay {
|
||||
// base count change callback
|
||||
baseCountChange(count) {
|
||||
// calculate scroll offset and don't go past end
|
||||
let offsetY = Math.min(this.elem.querySelector('.travel-lines').getBoundingClientRect().height - 289, (count - 150));
|
||||
let offsetY = Math.min(this.elem.querySelector('.travel-lines').offsetHeight - 289, (count - 150));
|
||||
|
||||
// don't let offset go negative
|
||||
if (offsetY < 0) offsetY = 0;
|
||||
|
||||
@@ -39,14 +39,14 @@ const fetchAsync = async (_url, responseType, _params = {}) => {
|
||||
if (!response.ok) throw new Error(`Fetch error ${response.status} ${response.statusText} while fetching ${response.url}`);
|
||||
// return the requested response
|
||||
switch (responseType) {
|
||||
case 'json':
|
||||
return response.json();
|
||||
case 'text':
|
||||
return response.text();
|
||||
case 'blob':
|
||||
return response.blob();
|
||||
default:
|
||||
return response;
|
||||
case 'json':
|
||||
return response.json();
|
||||
case 'text':
|
||||
return response.text();
|
||||
case 'blob':
|
||||
return response.blob();
|
||||
default:
|
||||
return response;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -84,11 +84,11 @@ const delay = (time, func, ...args) => new Promise((resolve) => {
|
||||
|
||||
const retryDelay = (retryNumber) => {
|
||||
switch (retryNumber) {
|
||||
case 1: return 1000;
|
||||
case 2: return 2000;
|
||||
case 3: return 5000;
|
||||
case 4: return 10_000;
|
||||
default: return 30_000;
|
||||
case 1: return 1000;
|
||||
case 2: return 2000;
|
||||
case 3: return 5000;
|
||||
case 4: return 10_000;
|
||||
default: return 30_000;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -3,23 +3,31 @@ import { parseQueryString } from '../share.mjs';
|
||||
const SETTINGS_KEY = 'Settings';
|
||||
|
||||
class Setting {
|
||||
constructor(shortName, name, type, defaultValue, changeAction, sticky) {
|
||||
constructor(shortName, name, type, defaultValue, changeAction, sticky, values) {
|
||||
// store values
|
||||
this.shortName = shortName;
|
||||
this.name = name;
|
||||
this.defaultValue = defaultValue;
|
||||
this.myValue = defaultValue;
|
||||
this.type = type;
|
||||
this.type = type ?? 'checkbox';
|
||||
this.sticky = sticky;
|
||||
this.values = values;
|
||||
// a default blank change function is provided
|
||||
this.changeAction = changeAction ?? (() => { });
|
||||
|
||||
// get value from url
|
||||
const urlValue = parseQueryString()?.[`settings-${shortName}-checkbox`];
|
||||
const urlValue = parseQueryString()?.[`settings-${shortName}-${type}`];
|
||||
let urlState;
|
||||
if (urlValue !== undefined) {
|
||||
if (type === 'checkbox' && urlValue !== undefined) {
|
||||
urlState = urlValue === 'true';
|
||||
}
|
||||
if (type === 'select' && urlValue !== undefined) {
|
||||
urlState = parseFloat(urlValue);
|
||||
}
|
||||
if (type === 'select' && urlValue !== undefined && Number.isNaN(urlState)) {
|
||||
// couldn't parse as a float, store as a string
|
||||
urlState = urlValue;
|
||||
}
|
||||
|
||||
// get existing value if present
|
||||
const storedValue = urlState ?? this.getFromLocalStorage();
|
||||
@@ -28,7 +36,50 @@ class Setting {
|
||||
}
|
||||
|
||||
// call the change function on startup
|
||||
this.checkboxChange({ target: { checked: this.myValue } });
|
||||
switch (type) {
|
||||
case 'select':
|
||||
this.selectChange({ target: { value: this.myValue } });
|
||||
break;
|
||||
case 'checkbox':
|
||||
default:
|
||||
this.checkboxChange({ target: { checked: this.myValue } });
|
||||
}
|
||||
}
|
||||
|
||||
generateSelect() {
|
||||
// create a radio button set in the selected displays area
|
||||
const label = document.createElement('label');
|
||||
label.for = `settings-${this.shortName}-select`;
|
||||
label.id = `settings-${this.shortName}-label`;
|
||||
|
||||
const span = document.createElement('span');
|
||||
span.innerHTML = `${this.name} `;
|
||||
label.append(span);
|
||||
|
||||
const select = document.createElement('select');
|
||||
select.id = `settings-${this.shortName}-select`;
|
||||
select.name = `settings-${this.shortName}-select`;
|
||||
select.addEventListener('change', (e) => this.selectChange(e));
|
||||
|
||||
this.values.forEach(([value, text]) => {
|
||||
const option = document.createElement('option');
|
||||
if (typeof value === 'number') {
|
||||
option.value = value.toFixed(2);
|
||||
} else {
|
||||
option.value = value;
|
||||
}
|
||||
|
||||
option.innerHTML = text;
|
||||
select.append(option);
|
||||
});
|
||||
label.append(select);
|
||||
|
||||
this.element = label;
|
||||
|
||||
// set the initial value
|
||||
this.selectHighlight(this.myValue);
|
||||
|
||||
return label;
|
||||
}
|
||||
|
||||
generateCheckbox() {
|
||||
@@ -48,7 +99,7 @@ class Setting {
|
||||
|
||||
label.append(checkbox, span);
|
||||
|
||||
this.checkbox = label;
|
||||
this.element = label;
|
||||
|
||||
return label;
|
||||
}
|
||||
@@ -62,6 +113,19 @@ class Setting {
|
||||
this.changeAction(this.myValue);
|
||||
}
|
||||
|
||||
selectChange(e) {
|
||||
// update the value
|
||||
this.myValue = parseFloat(e.target.value);
|
||||
if (Number.isNaN(this.myValue)) {
|
||||
// was a string, store as such
|
||||
this.myValue = e.target.value;
|
||||
}
|
||||
this.storeToLocalStorage(this.myValue);
|
||||
|
||||
// call the change action
|
||||
this.changeAction(this.myValue);
|
||||
}
|
||||
|
||||
storeToLocalStorage(value) {
|
||||
if (!this.sticky) return;
|
||||
const allSettingsString = localStorage?.getItem(SETTINGS_KEY) ?? '{}';
|
||||
@@ -77,12 +141,12 @@ class Setting {
|
||||
const storedValue = JSON.parse(allSettings)?.[this.shortName];
|
||||
if (storedValue !== undefined) {
|
||||
switch (this.type) {
|
||||
case 'boolean':
|
||||
return storedValue;
|
||||
case 'int':
|
||||
return storedValue;
|
||||
default:
|
||||
return null;
|
||||
case 'boolean':
|
||||
return storedValue;
|
||||
case 'select':
|
||||
return storedValue;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -99,12 +163,36 @@ class Setting {
|
||||
set value(newValue) {
|
||||
// update the state
|
||||
this.myValue = newValue;
|
||||
this.checkbox.checked = newValue;
|
||||
switch (this.type) {
|
||||
case 'select':
|
||||
this.selectHighlight(newValue);
|
||||
break;
|
||||
case 'checkbox':
|
||||
default:
|
||||
this.element.checked = newValue;
|
||||
}
|
||||
this.storeToLocalStorage(this.myValue);
|
||||
|
||||
// call change action
|
||||
this.changeAction(this.myValue);
|
||||
}
|
||||
|
||||
selectHighlight(newValue) {
|
||||
// set the dropdown to the provided value
|
||||
this.element.querySelectorAll('option').forEach((elem) => {
|
||||
elem.selected = (newValue?.toFixed?.(2) === elem.value) || (newValue === elem.value);
|
||||
});
|
||||
}
|
||||
|
||||
generate() {
|
||||
switch (this.type) {
|
||||
case 'select':
|
||||
return this.generateSelect();
|
||||
case 'checkbox':
|
||||
default:
|
||||
return this.generateCheckbox();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default Setting;
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
// get the settings for units
|
||||
import settings from '../settings.mjs';
|
||||
// *********************************** unit conversions ***********************
|
||||
|
||||
// round 2 provided for lat/lon formatting
|
||||
const round2 = (value, decimals) => Math.trunc(value * 10 ** decimals) / 10 ** decimals;
|
||||
|
||||
const kphToMph = (Kph) => Math.round(Kph / 1.609_34);
|
||||
@@ -8,11 +11,98 @@ const kilometersToMiles = (Kilometers) => Math.round(Kilometers / 1.609_34);
|
||||
const metersToFeet = (Meters) => Math.round(Meters / 0.3048);
|
||||
const pascalToInHg = (Pascal) => round2(Pascal * 0.000_295_3, 2);
|
||||
|
||||
// each module/page/slide creates it's own unit converter as needed by providing the base units available
|
||||
// the factory function then returns an appropriate converter or pass-thru function for use on the page
|
||||
|
||||
const windSpeed = (defaultUnit = 'si') => {
|
||||
// default to passthru
|
||||
let converter = (passthru) => Math.round(passthru);
|
||||
// change the converter if there is a mismatch
|
||||
if (defaultUnit !== settings.units.value) {
|
||||
converter = kphToMph;
|
||||
}
|
||||
// append units
|
||||
if (settings.units.value === 'si') {
|
||||
converter.units = 'kph';
|
||||
} else {
|
||||
converter.units = 'MPH';
|
||||
}
|
||||
return converter;
|
||||
};
|
||||
|
||||
const temperature = (defaultUnit = 'si') => {
|
||||
// default to passthru
|
||||
let converter = (passthru) => Math.round(passthru);
|
||||
// change the converter if there is a mismatch
|
||||
if (defaultUnit !== settings.units.value) {
|
||||
converter = celsiusToFahrenheit;
|
||||
}
|
||||
// append units
|
||||
if (settings.units.value === 'si') {
|
||||
converter.units = 'C';
|
||||
} else {
|
||||
converter.units = 'F';
|
||||
}
|
||||
return converter;
|
||||
};
|
||||
|
||||
const distanceMeters = (defaultUnit = 'si') => {
|
||||
// default to passthru
|
||||
let converter = (passthru) => Math.round(passthru);
|
||||
// change the converter if there is a mismatch
|
||||
if (defaultUnit !== settings.units.value) {
|
||||
// rounded to the nearest 100 (ceiling)
|
||||
converter = (value) => Math.round(metersToFeet(value) / 100) * 100;
|
||||
}
|
||||
// append units
|
||||
if (settings.units.value === 'si') {
|
||||
converter.units = 'm.';
|
||||
} else {
|
||||
converter.units = 'ft.';
|
||||
}
|
||||
return converter;
|
||||
};
|
||||
|
||||
const distanceKilometers = (defaultUnit = 'si') => {
|
||||
// default to passthru
|
||||
let converter = (passthru) => Math.round(passthru / 1000);
|
||||
// change the converter if there is a mismatch
|
||||
if (defaultUnit !== settings.units.value) {
|
||||
converter = (value) => Math.round(kilometersToMiles(value) / 1000);
|
||||
}
|
||||
// append units
|
||||
if (settings.units.value === 'si') {
|
||||
converter.units = ' km.';
|
||||
} else {
|
||||
converter.units = ' mi.';
|
||||
}
|
||||
return converter;
|
||||
};
|
||||
|
||||
const pressure = (defaultUnit = 'si') => {
|
||||
// default to passthru (millibar)
|
||||
let converter = (passthru) => Math.round(passthru / 100);
|
||||
// change the converter if there is a mismatch
|
||||
if (defaultUnit !== settings.units.value) {
|
||||
converter = (value) => pascalToInHg(value).toFixed(2);
|
||||
}
|
||||
// append units
|
||||
if (settings.units.value === 'si') {
|
||||
converter.units = ' mbar';
|
||||
} else {
|
||||
converter.units = ' in.hg';
|
||||
}
|
||||
return converter;
|
||||
};
|
||||
|
||||
export {
|
||||
kphToMph,
|
||||
celsiusToFahrenheit,
|
||||
kilometersToMiles,
|
||||
metersToFeet,
|
||||
pascalToInHg,
|
||||
// unit conversions
|
||||
windSpeed,
|
||||
temperature,
|
||||
distanceMeters,
|
||||
distanceKilometers,
|
||||
pressure,
|
||||
|
||||
// formatter
|
||||
round2,
|
||||
};
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
msg, displayNavMessage, isPlaying, updateStatus, timeZone,
|
||||
} from './navigation.mjs';
|
||||
import { parseQueryString } from './share.mjs';
|
||||
import settings from './settings.mjs';
|
||||
|
||||
class WeatherDisplay {
|
||||
constructor(navId, elemId, name, defaultEnabled) {
|
||||
@@ -169,7 +170,7 @@ class WeatherDisplay {
|
||||
// auto clock refresh
|
||||
if (!this.dateTimeInterval) {
|
||||
// only draw if canvas is active to conserve battery
|
||||
setInterval(() => this.active && this.drawCurrentDateTime(), 100);
|
||||
this.dateTimeInterval = setInterval(() => this.active && this.drawCurrentDateTime(), 100);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -358,7 +359,7 @@ class WeatherDisplay {
|
||||
|
||||
// start and stop base counter
|
||||
startNavCount() {
|
||||
if (!this.navInterval) this.navInterval = setInterval(() => this.navBaseTime(), this.timing.baseDelay);
|
||||
if (!this.navInterval) this.navInterval = setInterval(() => this.navBaseTime(), this.timing.baseDelay * settings.speed.value);
|
||||
}
|
||||
|
||||
resetNavBaseCount() {
|
||||
|
||||
2
server/scripts/vendor/auto/luxon.js.map
vendored
2
server/scripts/vendor/auto/luxon.js.map
vendored
File diff suppressed because one or more lines are too long
540
server/scripts/vendor/auto/luxon.mjs
vendored
540
server/scripts/vendor/auto/luxon.mjs
vendored
@@ -259,6 +259,12 @@ class Zone {
|
||||
throw new ZoneIsAbstractError();
|
||||
}
|
||||
|
||||
/**
|
||||
* The IANA name of this zone.
|
||||
* Defaults to `name` if not overwritten by a subclass.
|
||||
* @abstract
|
||||
* @type {string}
|
||||
*/
|
||||
get ianaName() {
|
||||
return this.name;
|
||||
}
|
||||
@@ -468,7 +474,7 @@ class IANAZone extends Zone {
|
||||
* @param {string} s - The string to check validity on
|
||||
* @example IANAZone.isValidSpecifier("America/New_York") //=> true
|
||||
* @example IANAZone.isValidSpecifier("Sport~~blorp") //=> false
|
||||
* @deprecated This method returns false for some valid IANA names. Use isValidZone instead.
|
||||
* @deprecated For backward compatibility, this forwards to isValidZone, better use `isValidZone()` directly instead.
|
||||
* @return {boolean}
|
||||
*/
|
||||
static isValidSpecifier(s) {
|
||||
@@ -503,32 +509,65 @@ class IANAZone extends Zone {
|
||||
this.valid = IANAZone.isValidZone(name);
|
||||
}
|
||||
|
||||
/** @override **/
|
||||
/**
|
||||
* The type of zone. `iana` for all instances of `IANAZone`.
|
||||
* @override
|
||||
* @type {string}
|
||||
*/
|
||||
get type() {
|
||||
return "iana";
|
||||
}
|
||||
|
||||
/** @override **/
|
||||
/**
|
||||
* The name of this zone (i.e. the IANA zone name).
|
||||
* @override
|
||||
* @type {string}
|
||||
*/
|
||||
get name() {
|
||||
return this.zoneName;
|
||||
}
|
||||
|
||||
/** @override **/
|
||||
/**
|
||||
* Returns whether the offset is known to be fixed for the whole year:
|
||||
* Always returns false for all IANA zones.
|
||||
* @override
|
||||
* @type {boolean}
|
||||
*/
|
||||
get isUniversal() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/** @override **/
|
||||
/**
|
||||
* Returns the offset's common name (such as EST) at the specified timestamp
|
||||
* @override
|
||||
* @param {number} ts - Epoch milliseconds for which to get the name
|
||||
* @param {Object} opts - Options to affect the format
|
||||
* @param {string} opts.format - What style of offset to return. Accepts 'long' or 'short'.
|
||||
* @param {string} opts.locale - What locale to return the offset name in.
|
||||
* @return {string}
|
||||
*/
|
||||
offsetName(ts, { format, locale }) {
|
||||
return parseZoneInfo(ts, format, locale, this.name);
|
||||
}
|
||||
|
||||
/** @override **/
|
||||
/**
|
||||
* Returns the offset's value as a string
|
||||
* @override
|
||||
* @param {number} ts - Epoch milliseconds for which to get the offset
|
||||
* @param {string} format - What style of offset to return.
|
||||
* Accepts 'narrow', 'short', or 'techie'. Returning '+6', '+06:00', or '+0600' respectively
|
||||
* @return {string}
|
||||
*/
|
||||
formatOffset(ts, format) {
|
||||
return formatOffset(this.offset(ts), format);
|
||||
}
|
||||
|
||||
/** @override **/
|
||||
/**
|
||||
* Return the offset in minutes for this zone at the specified timestamp.
|
||||
* @override
|
||||
* @param {number} ts - Epoch milliseconds for which to compute the offset
|
||||
* @return {number}
|
||||
*/
|
||||
offset(ts) {
|
||||
const date = new Date(ts);
|
||||
|
||||
@@ -562,12 +601,21 @@ class IANAZone extends Zone {
|
||||
return (asUTC - asTS) / (60 * 1000);
|
||||
}
|
||||
|
||||
/** @override **/
|
||||
/**
|
||||
* Return whether this Zone is equal to another zone
|
||||
* @override
|
||||
* @param {Zone} otherZone - the zone to compare
|
||||
* @return {boolean}
|
||||
*/
|
||||
equals(otherZone) {
|
||||
return otherZone.type === "iana" && otherZone.name === this.name;
|
||||
}
|
||||
|
||||
/** @override **/
|
||||
/**
|
||||
* Return whether this Zone is valid.
|
||||
* @override
|
||||
* @type {boolean}
|
||||
*/
|
||||
get isValid() {
|
||||
return this.valid;
|
||||
}
|
||||
@@ -909,7 +957,7 @@ class Locale {
|
||||
|
||||
static create(locale, numberingSystem, outputCalendar, weekSettings, defaultToEN = false) {
|
||||
const specifiedLocale = locale || Settings.defaultLocale;
|
||||
// the system locale is useful for human readable strings but annoying for parsing/formatting known formats
|
||||
// the system locale is useful for human-readable strings but annoying for parsing/formatting known formats
|
||||
const localeR = specifiedLocale || (defaultToEN ? "en-US" : systemLocale());
|
||||
const numberingSystemR = numberingSystem || Settings.defaultNumberingSystem;
|
||||
const outputCalendarR = outputCalendar || Settings.defaultOutputCalendar;
|
||||
@@ -1108,6 +1156,10 @@ class Locale {
|
||||
this.outputCalendar === other.outputCalendar
|
||||
);
|
||||
}
|
||||
|
||||
toString() {
|
||||
return `Locale(${this.locale}, ${this.numberingSystem}, ${this.outputCalendar})`;
|
||||
}
|
||||
}
|
||||
|
||||
let singleton = null;
|
||||
@@ -1161,16 +1213,31 @@ class FixedOffsetZone extends Zone {
|
||||
this.fixed = offset;
|
||||
}
|
||||
|
||||
/** @override **/
|
||||
/**
|
||||
* The type of zone. `fixed` for all instances of `FixedOffsetZone`.
|
||||
* @override
|
||||
* @type {string}
|
||||
*/
|
||||
get type() {
|
||||
return "fixed";
|
||||
}
|
||||
|
||||
/** @override **/
|
||||
/**
|
||||
* The name of this zone.
|
||||
* All fixed zones' names always start with "UTC" (plus optional offset)
|
||||
* @override
|
||||
* @type {string}
|
||||
*/
|
||||
get name() {
|
||||
return this.fixed === 0 ? "UTC" : `UTC${formatOffset(this.fixed, "narrow")}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* The IANA name of this zone, i.e. `Etc/UTC` or `Etc/GMT+/-nn`
|
||||
*
|
||||
* @override
|
||||
* @type {string}
|
||||
*/
|
||||
get ianaName() {
|
||||
if (this.fixed === 0) {
|
||||
return "Etc/UTC";
|
||||
@@ -1179,32 +1246,65 @@ class FixedOffsetZone extends Zone {
|
||||
}
|
||||
}
|
||||
|
||||
/** @override **/
|
||||
/**
|
||||
* Returns the offset's common name at the specified timestamp.
|
||||
*
|
||||
* For fixed offset zones this equals to the zone name.
|
||||
* @override
|
||||
*/
|
||||
offsetName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
/** @override **/
|
||||
/**
|
||||
* Returns the offset's value as a string
|
||||
* @override
|
||||
* @param {number} ts - Epoch milliseconds for which to get the offset
|
||||
* @param {string} format - What style of offset to return.
|
||||
* Accepts 'narrow', 'short', or 'techie'. Returning '+6', '+06:00', or '+0600' respectively
|
||||
* @return {string}
|
||||
*/
|
||||
formatOffset(ts, format) {
|
||||
return formatOffset(this.fixed, format);
|
||||
}
|
||||
|
||||
/** @override **/
|
||||
/**
|
||||
* Returns whether the offset is known to be fixed for the whole year:
|
||||
* Always returns true for all fixed offset zones.
|
||||
* @override
|
||||
* @type {boolean}
|
||||
*/
|
||||
get isUniversal() {
|
||||
return true;
|
||||
}
|
||||
|
||||
/** @override **/
|
||||
/**
|
||||
* Return the offset in minutes for this zone at the specified timestamp.
|
||||
*
|
||||
* For fixed offset zones, this is constant and does not depend on a timestamp.
|
||||
* @override
|
||||
* @return {number}
|
||||
*/
|
||||
offset() {
|
||||
return this.fixed;
|
||||
}
|
||||
|
||||
/** @override **/
|
||||
/**
|
||||
* Return whether this Zone is equal to another zone (i.e. also fixed and same offset)
|
||||
* @override
|
||||
* @param {Zone} otherZone - the zone to compare
|
||||
* @return {boolean}
|
||||
*/
|
||||
equals(otherZone) {
|
||||
return otherZone.type === "fixed" && otherZone.fixed === this.fixed;
|
||||
}
|
||||
|
||||
/** @override **/
|
||||
/**
|
||||
* Return whether this Zone is valid:
|
||||
* All fixed offset zones are valid.
|
||||
* @override
|
||||
* @type {boolean}
|
||||
*/
|
||||
get isValid() {
|
||||
return true;
|
||||
}
|
||||
@@ -1288,6 +1388,97 @@ function normalizeZone(input, defaultZone) {
|
||||
}
|
||||
}
|
||||
|
||||
const numberingSystems = {
|
||||
arab: "[\u0660-\u0669]",
|
||||
arabext: "[\u06F0-\u06F9]",
|
||||
bali: "[\u1B50-\u1B59]",
|
||||
beng: "[\u09E6-\u09EF]",
|
||||
deva: "[\u0966-\u096F]",
|
||||
fullwide: "[\uFF10-\uFF19]",
|
||||
gujr: "[\u0AE6-\u0AEF]",
|
||||
hanidec: "[〇|一|二|三|四|五|六|七|八|九]",
|
||||
khmr: "[\u17E0-\u17E9]",
|
||||
knda: "[\u0CE6-\u0CEF]",
|
||||
laoo: "[\u0ED0-\u0ED9]",
|
||||
limb: "[\u1946-\u194F]",
|
||||
mlym: "[\u0D66-\u0D6F]",
|
||||
mong: "[\u1810-\u1819]",
|
||||
mymr: "[\u1040-\u1049]",
|
||||
orya: "[\u0B66-\u0B6F]",
|
||||
tamldec: "[\u0BE6-\u0BEF]",
|
||||
telu: "[\u0C66-\u0C6F]",
|
||||
thai: "[\u0E50-\u0E59]",
|
||||
tibt: "[\u0F20-\u0F29]",
|
||||
latn: "\\d",
|
||||
};
|
||||
|
||||
const numberingSystemsUTF16 = {
|
||||
arab: [1632, 1641],
|
||||
arabext: [1776, 1785],
|
||||
bali: [6992, 7001],
|
||||
beng: [2534, 2543],
|
||||
deva: [2406, 2415],
|
||||
fullwide: [65296, 65303],
|
||||
gujr: [2790, 2799],
|
||||
khmr: [6112, 6121],
|
||||
knda: [3302, 3311],
|
||||
laoo: [3792, 3801],
|
||||
limb: [6470, 6479],
|
||||
mlym: [3430, 3439],
|
||||
mong: [6160, 6169],
|
||||
mymr: [4160, 4169],
|
||||
orya: [2918, 2927],
|
||||
tamldec: [3046, 3055],
|
||||
telu: [3174, 3183],
|
||||
thai: [3664, 3673],
|
||||
tibt: [3872, 3881],
|
||||
};
|
||||
|
||||
const hanidecChars = numberingSystems.hanidec.replace(/[\[|\]]/g, "").split("");
|
||||
|
||||
function parseDigits(str) {
|
||||
let value = parseInt(str, 10);
|
||||
if (isNaN(value)) {
|
||||
value = "";
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
const code = str.charCodeAt(i);
|
||||
|
||||
if (str[i].search(numberingSystems.hanidec) !== -1) {
|
||||
value += hanidecChars.indexOf(str[i]);
|
||||
} else {
|
||||
for (const key in numberingSystemsUTF16) {
|
||||
const [min, max] = numberingSystemsUTF16[key];
|
||||
if (code >= min && code <= max) {
|
||||
value += code - min;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return parseInt(value, 10);
|
||||
} else {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
// cache of {numberingSystem: {append: regex}}
|
||||
let digitRegexCache = {};
|
||||
function resetDigitRegexCache() {
|
||||
digitRegexCache = {};
|
||||
}
|
||||
|
||||
function digitRegex({ numberingSystem }, append = "") {
|
||||
const ns = numberingSystem || "latn";
|
||||
|
||||
if (!digitRegexCache[ns]) {
|
||||
digitRegexCache[ns] = {};
|
||||
}
|
||||
if (!digitRegexCache[ns][append]) {
|
||||
digitRegexCache[ns][append] = new RegExp(`${numberingSystems[ns]}${append}`);
|
||||
}
|
||||
|
||||
return digitRegexCache[ns][append];
|
||||
}
|
||||
|
||||
let now = () => Date.now(),
|
||||
defaultZone = "system",
|
||||
defaultLocale = null,
|
||||
@@ -1412,7 +1603,7 @@ class Settings {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the cutoff year after which a string encoding a year as two digits is interpreted to occur in the current century.
|
||||
* Get the cutoff year for whether a 2-digit year string is interpreted in the current or previous century. Numbers higher than the cutoff will be considered to mean 19xx and numbers lower or equal to the cutoff will be considered 20xx.
|
||||
* @type {number}
|
||||
*/
|
||||
static get twoDigitCutoffYear() {
|
||||
@@ -1420,10 +1611,11 @@ class Settings {
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the cutoff year after which a string encoding a year as two digits is interpreted to occur in the current century.
|
||||
* Set the cutoff year for whether a 2-digit year string is interpreted in the current or previous century. Numbers higher than the cutoff will be considered to mean 19xx and numbers lower or equal to the cutoff will be considered 20xx.
|
||||
* @type {number}
|
||||
* @example Settings.twoDigitCutoffYear = 0 // cut-off year is 0, so all 'yy' are interpreted as current century
|
||||
* @example Settings.twoDigitCutoffYear = 50 // '49' -> 1949; '50' -> 2050
|
||||
* @example Settings.twoDigitCutoffYear = 0 // all 'yy' are interpreted as 20th century
|
||||
* @example Settings.twoDigitCutoffYear = 99 // all 'yy' are interpreted as 21st century
|
||||
* @example Settings.twoDigitCutoffYear = 50 // '49' -> 2049; '50' -> 1950
|
||||
* @example Settings.twoDigitCutoffYear = 1950 // interpreted as 50
|
||||
* @example Settings.twoDigitCutoffYear = 2050 // ALSO interpreted as 50
|
||||
*/
|
||||
@@ -1454,6 +1646,8 @@ class Settings {
|
||||
static resetCaches() {
|
||||
Locale.resetCache();
|
||||
IANAZone.resetCache();
|
||||
DateTime.resetCache();
|
||||
resetDigitRegexCache();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1951,6 +2145,13 @@ function normalizeObject(obj, normalizer) {
|
||||
return normalized;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the offset's value as a string
|
||||
* @param {number} ts - Epoch milliseconds for which to get the offset
|
||||
* @param {string} format - What style of offset to return.
|
||||
* Accepts 'narrow', 'short', or 'techie'. Returning '+6', '+06:00', or '+0600' respectively
|
||||
* @return {string}
|
||||
*/
|
||||
function formatOffset(offset, format) {
|
||||
const hours = Math.trunc(Math.abs(offset / 60)),
|
||||
minutes = Math.trunc(Math.abs(offset % 60)),
|
||||
@@ -4235,7 +4436,7 @@ class Interval {
|
||||
}
|
||||
|
||||
/**
|
||||
* Return whether this Interval engulfs the start and end of the specified Interval.
|
||||
* Returns true if this Interval fully contains the specified Interval, specifically if the intersect (of this Interval and the other Interval) is equal to the other Interval; false otherwise.
|
||||
* @param {Interval} other
|
||||
* @return {boolean}
|
||||
*/
|
||||
@@ -4778,82 +4979,6 @@ function diff (earlier, later, units, opts) {
|
||||
}
|
||||
}
|
||||
|
||||
const numberingSystems = {
|
||||
arab: "[\u0660-\u0669]",
|
||||
arabext: "[\u06F0-\u06F9]",
|
||||
bali: "[\u1B50-\u1B59]",
|
||||
beng: "[\u09E6-\u09EF]",
|
||||
deva: "[\u0966-\u096F]",
|
||||
fullwide: "[\uFF10-\uFF19]",
|
||||
gujr: "[\u0AE6-\u0AEF]",
|
||||
hanidec: "[〇|一|二|三|四|五|六|七|八|九]",
|
||||
khmr: "[\u17E0-\u17E9]",
|
||||
knda: "[\u0CE6-\u0CEF]",
|
||||
laoo: "[\u0ED0-\u0ED9]",
|
||||
limb: "[\u1946-\u194F]",
|
||||
mlym: "[\u0D66-\u0D6F]",
|
||||
mong: "[\u1810-\u1819]",
|
||||
mymr: "[\u1040-\u1049]",
|
||||
orya: "[\u0B66-\u0B6F]",
|
||||
tamldec: "[\u0BE6-\u0BEF]",
|
||||
telu: "[\u0C66-\u0C6F]",
|
||||
thai: "[\u0E50-\u0E59]",
|
||||
tibt: "[\u0F20-\u0F29]",
|
||||
latn: "\\d",
|
||||
};
|
||||
|
||||
const numberingSystemsUTF16 = {
|
||||
arab: [1632, 1641],
|
||||
arabext: [1776, 1785],
|
||||
bali: [6992, 7001],
|
||||
beng: [2534, 2543],
|
||||
deva: [2406, 2415],
|
||||
fullwide: [65296, 65303],
|
||||
gujr: [2790, 2799],
|
||||
khmr: [6112, 6121],
|
||||
knda: [3302, 3311],
|
||||
laoo: [3792, 3801],
|
||||
limb: [6470, 6479],
|
||||
mlym: [3430, 3439],
|
||||
mong: [6160, 6169],
|
||||
mymr: [4160, 4169],
|
||||
orya: [2918, 2927],
|
||||
tamldec: [3046, 3055],
|
||||
telu: [3174, 3183],
|
||||
thai: [3664, 3673],
|
||||
tibt: [3872, 3881],
|
||||
};
|
||||
|
||||
const hanidecChars = numberingSystems.hanidec.replace(/[\[|\]]/g, "").split("");
|
||||
|
||||
function parseDigits(str) {
|
||||
let value = parseInt(str, 10);
|
||||
if (isNaN(value)) {
|
||||
value = "";
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
const code = str.charCodeAt(i);
|
||||
|
||||
if (str[i].search(numberingSystems.hanidec) !== -1) {
|
||||
value += hanidecChars.indexOf(str[i]);
|
||||
} else {
|
||||
for (const key in numberingSystemsUTF16) {
|
||||
const [min, max] = numberingSystemsUTF16[key];
|
||||
if (code >= min && code <= max) {
|
||||
value += code - min;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return parseInt(value, 10);
|
||||
} else {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
function digitRegex({ numberingSystem }, append = "") {
|
||||
return new RegExp(`${numberingSystems[numberingSystem || "latn"]}${append}`);
|
||||
}
|
||||
|
||||
const MISSING_FTP = "missing Intl.DateTimeFormat.formatToParts support";
|
||||
|
||||
function intUnit(regex, post = (i) => i) {
|
||||
@@ -5280,27 +5405,59 @@ function expandMacroTokens(tokens, locale) {
|
||||
* @private
|
||||
*/
|
||||
|
||||
function explainFromTokens(locale, input, format) {
|
||||
const tokens = expandMacroTokens(Formatter.parseFormat(format), locale),
|
||||
units = tokens.map((t) => unitForToken(t, locale)),
|
||||
disqualifyingUnit = units.find((t) => t.invalidReason);
|
||||
class TokenParser {
|
||||
constructor(locale, format) {
|
||||
this.locale = locale;
|
||||
this.format = format;
|
||||
this.tokens = expandMacroTokens(Formatter.parseFormat(format), locale);
|
||||
this.units = this.tokens.map((t) => unitForToken(t, locale));
|
||||
this.disqualifyingUnit = this.units.find((t) => t.invalidReason);
|
||||
|
||||
if (disqualifyingUnit) {
|
||||
return { input, tokens, invalidReason: disqualifyingUnit.invalidReason };
|
||||
} else {
|
||||
const [regexString, handlers] = buildRegex(units),
|
||||
regex = RegExp(regexString, "i"),
|
||||
[rawMatches, matches] = match(input, regex, handlers),
|
||||
[result, zone, specificOffset] = matches
|
||||
? dateTimeFromMatches(matches)
|
||||
: [null, null, undefined];
|
||||
if (hasOwnProperty(matches, "a") && hasOwnProperty(matches, "H")) {
|
||||
throw new ConflictingSpecificationError(
|
||||
"Can't include meridiem when specifying 24-hour format"
|
||||
);
|
||||
if (!this.disqualifyingUnit) {
|
||||
const [regexString, handlers] = buildRegex(this.units);
|
||||
this.regex = RegExp(regexString, "i");
|
||||
this.handlers = handlers;
|
||||
}
|
||||
return { input, tokens, regex, rawMatches, matches, result, zone, specificOffset };
|
||||
}
|
||||
|
||||
explainFromTokens(input) {
|
||||
if (!this.isValid) {
|
||||
return { input, tokens: this.tokens, invalidReason: this.invalidReason };
|
||||
} else {
|
||||
const [rawMatches, matches] = match(input, this.regex, this.handlers),
|
||||
[result, zone, specificOffset] = matches
|
||||
? dateTimeFromMatches(matches)
|
||||
: [null, null, undefined];
|
||||
if (hasOwnProperty(matches, "a") && hasOwnProperty(matches, "H")) {
|
||||
throw new ConflictingSpecificationError(
|
||||
"Can't include meridiem when specifying 24-hour format"
|
||||
);
|
||||
}
|
||||
return {
|
||||
input,
|
||||
tokens: this.tokens,
|
||||
regex: this.regex,
|
||||
rawMatches,
|
||||
matches,
|
||||
result,
|
||||
zone,
|
||||
specificOffset,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
get isValid() {
|
||||
return !this.disqualifyingUnit;
|
||||
}
|
||||
|
||||
get invalidReason() {
|
||||
return this.disqualifyingUnit ? this.disqualifyingUnit.invalidReason : null;
|
||||
}
|
||||
}
|
||||
|
||||
function explainFromTokens(locale, input, format) {
|
||||
const parser = new TokenParser(locale, format);
|
||||
return parser.explainFromTokens(input);
|
||||
}
|
||||
|
||||
function parseFromTokens(locale, input, format) {
|
||||
@@ -5639,13 +5796,46 @@ function normalizeUnitWithLocalWeeks(unit) {
|
||||
}
|
||||
}
|
||||
|
||||
// cache offsets for zones based on the current timestamp when this function is
|
||||
// first called. When we are handling a datetime from components like (year,
|
||||
// month, day, hour) in a time zone, we need a guess about what the timezone
|
||||
// offset is so that we can convert into a UTC timestamp. One way is to find the
|
||||
// offset of now in the zone. The actual date may have a different offset (for
|
||||
// example, if we handle a date in June while we're in December in a zone that
|
||||
// observes DST), but we can check and adjust that.
|
||||
//
|
||||
// When handling many dates, calculating the offset for now every time is
|
||||
// expensive. It's just a guess, so we can cache the offset to use even if we
|
||||
// are right on a time change boundary (we'll just correct in the other
|
||||
// direction). Using a timestamp from first read is a slight optimization for
|
||||
// handling dates close to the current date, since those dates will usually be
|
||||
// in the same offset (we could set the timestamp statically, instead). We use a
|
||||
// single timestamp for all zones to make things a bit more predictable.
|
||||
//
|
||||
// This is safe for quickDT (used by local() and utc()) because we don't fill in
|
||||
// higher-order units from tsNow (as we do in fromObject, this requires that
|
||||
// offset is calculated from tsNow).
|
||||
function guessOffsetForZone(zone) {
|
||||
if (!zoneOffsetGuessCache[zone]) {
|
||||
if (zoneOffsetTs === undefined) {
|
||||
zoneOffsetTs = Settings.now();
|
||||
}
|
||||
|
||||
zoneOffsetGuessCache[zone] = zone.offset(zoneOffsetTs);
|
||||
}
|
||||
return zoneOffsetGuessCache[zone];
|
||||
}
|
||||
|
||||
// this is a dumbed down version of fromObject() that runs about 60% faster
|
||||
// but doesn't do any validation, makes a bunch of assumptions about what units
|
||||
// are present, and so on.
|
||||
function quickDT(obj, opts) {
|
||||
const zone = normalizeZone(opts.zone, Settings.defaultZone),
|
||||
loc = Locale.fromObject(opts),
|
||||
tsNow = Settings.now();
|
||||
const zone = normalizeZone(opts.zone, Settings.defaultZone);
|
||||
if (!zone.isValid) {
|
||||
return DateTime.invalid(unsupportedZone(zone));
|
||||
}
|
||||
|
||||
const loc = Locale.fromObject(opts);
|
||||
|
||||
let ts, o;
|
||||
|
||||
@@ -5662,10 +5852,10 @@ function quickDT(obj, opts) {
|
||||
return DateTime.invalid(invalid);
|
||||
}
|
||||
|
||||
const offsetProvis = zone.offset(tsNow);
|
||||
const offsetProvis = guessOffsetForZone(zone);
|
||||
[ts, o] = objToTS(obj, offsetProvis, zone);
|
||||
} else {
|
||||
ts = tsNow;
|
||||
ts = Settings.now();
|
||||
}
|
||||
|
||||
return new DateTime({ ts, zone, loc, o });
|
||||
@@ -5713,6 +5903,18 @@ function lastOpts(argList) {
|
||||
return [opts, args];
|
||||
}
|
||||
|
||||
/**
|
||||
* Timestamp to use for cached zone offset guesses (exposed for test)
|
||||
*/
|
||||
let zoneOffsetTs;
|
||||
/**
|
||||
* Cache for zone offset guesses (exposed for test).
|
||||
*
|
||||
* This optimizes quickDT via guessOffsetForZone to avoid repeated calls of
|
||||
* zone.offset().
|
||||
*/
|
||||
let zoneOffsetGuessCache = {};
|
||||
|
||||
/**
|
||||
* A DateTime is an immutable data structure representing a specific date and time and accompanying methods. It contains class and instance methods for creating, parsing, interrogating, transforming, and formatting them.
|
||||
*
|
||||
@@ -5757,7 +5959,9 @@ class DateTime {
|
||||
if (unchanged) {
|
||||
[c, o] = [config.old.c, config.old.o];
|
||||
} else {
|
||||
const ot = zone.offset(this.ts);
|
||||
// If an offset has been passed and we have not been called from
|
||||
// clone(), we can trust it and avoid the offset calculation.
|
||||
const ot = isNumber(config.o) && !config.old ? config.o : zone.offset(this.ts);
|
||||
c = tsToObj(this.ts, ot);
|
||||
invalid = Number.isNaN(c.year) ? new Invalid("invalid input") : null;
|
||||
c = invalid ? null : c;
|
||||
@@ -5852,6 +6056,7 @@ class DateTime {
|
||||
* @param {string} [options.locale] - a locale to set on the resulting DateTime instance
|
||||
* @param {string} [options.outputCalendar] - the output calendar to set on the resulting DateTime instance
|
||||
* @param {string} [options.numberingSystem] - the numbering system to set on the resulting DateTime instance
|
||||
* @param {string} [options.weekSettings] - the week settings to set on the resulting DateTime instance
|
||||
* @example DateTime.utc() //~> now
|
||||
* @example DateTime.utc(2017) //~> 2017-01-01T00:00:00Z
|
||||
* @example DateTime.utc(2017, 3) //~> 2017-03-01T00:00:00Z
|
||||
@@ -5904,6 +6109,7 @@ class DateTime {
|
||||
* @param {string} [options.locale] - a locale to set on the resulting DateTime instance
|
||||
* @param {string} options.outputCalendar - the output calendar to set on the resulting DateTime instance
|
||||
* @param {string} options.numberingSystem - the numbering system to set on the resulting DateTime instance
|
||||
* @param {string} options.weekSettings - the week settings to set on the resulting DateTime instance
|
||||
* @return {DateTime}
|
||||
*/
|
||||
static fromMillis(milliseconds, options = {}) {
|
||||
@@ -5912,7 +6118,7 @@ class DateTime {
|
||||
`fromMillis requires a numerical input, but received a ${typeof milliseconds} with value ${milliseconds}`
|
||||
);
|
||||
} else if (milliseconds < -MAX_DATE || milliseconds > MAX_DATE) {
|
||||
// this isn't perfect because because we can still end up out of range because of additional shifting, but it's a start
|
||||
// this isn't perfect because we can still end up out of range because of additional shifting, but it's a start
|
||||
return DateTime.invalid("Timestamp out of range");
|
||||
} else {
|
||||
return new DateTime({
|
||||
@@ -5931,6 +6137,7 @@ class DateTime {
|
||||
* @param {string} [options.locale] - a locale to set on the resulting DateTime instance
|
||||
* @param {string} options.outputCalendar - the output calendar to set on the resulting DateTime instance
|
||||
* @param {string} options.numberingSystem - the numbering system to set on the resulting DateTime instance
|
||||
* @param {string} options.weekSettings - the week settings to set on the resulting DateTime instance
|
||||
* @return {DateTime}
|
||||
*/
|
||||
static fromSeconds(seconds, options = {}) {
|
||||
@@ -5967,6 +6174,7 @@ class DateTime {
|
||||
* @param {string} [opts.locale='system\'s locale'] - a locale to set on the resulting DateTime instance
|
||||
* @param {string} opts.outputCalendar - the output calendar to set on the resulting DateTime instance
|
||||
* @param {string} opts.numberingSystem - the numbering system to set on the resulting DateTime instance
|
||||
* @param {string} opts.weekSettings - the week settings to set on the resulting DateTime instance
|
||||
* @example DateTime.fromObject({ year: 1982, month: 5, day: 25}).toISODate() //=> '1982-05-25'
|
||||
* @example DateTime.fromObject({ year: 1982 }).toISODate() //=> '1982-01-01'
|
||||
* @example DateTime.fromObject({ hour: 10, minute: 26, second: 6 }) //~> today at 10:26:06
|
||||
@@ -6080,6 +6288,10 @@ class DateTime {
|
||||
);
|
||||
}
|
||||
|
||||
if (!inst.isValid) {
|
||||
return DateTime.invalid(inst.invalid);
|
||||
}
|
||||
|
||||
return inst;
|
||||
}
|
||||
|
||||
@@ -6092,6 +6304,7 @@ class DateTime {
|
||||
* @param {string} [opts.locale='system's locale'] - a locale to set on the resulting DateTime instance
|
||||
* @param {string} [opts.outputCalendar] - the output calendar to set on the resulting DateTime instance
|
||||
* @param {string} [opts.numberingSystem] - the numbering system to set on the resulting DateTime instance
|
||||
* @param {string} [opts.weekSettings] - the week settings to set on the resulting DateTime instance
|
||||
* @example DateTime.fromISO('2016-05-25T09:08:34.123')
|
||||
* @example DateTime.fromISO('2016-05-25T09:08:34.123+06:00')
|
||||
* @example DateTime.fromISO('2016-05-25T09:08:34.123+06:00', {setZone: true})
|
||||
@@ -6113,6 +6326,7 @@ class DateTime {
|
||||
* @param {string} [opts.locale='system's locale'] - a locale to set on the resulting DateTime instance
|
||||
* @param {string} opts.outputCalendar - the output calendar to set on the resulting DateTime instance
|
||||
* @param {string} opts.numberingSystem - the numbering system to set on the resulting DateTime instance
|
||||
* @param {string} opts.weekSettings - the week settings to set on the resulting DateTime instance
|
||||
* @example DateTime.fromRFC2822('25 Nov 2016 13:23:12 GMT')
|
||||
* @example DateTime.fromRFC2822('Fri, 25 Nov 2016 13:23:12 +0600')
|
||||
* @example DateTime.fromRFC2822('25 Nov 2016 13:23 Z')
|
||||
@@ -6133,6 +6347,7 @@ class DateTime {
|
||||
* @param {string} [opts.locale='system's locale'] - a locale to set on the resulting DateTime instance
|
||||
* @param {string} opts.outputCalendar - the output calendar to set on the resulting DateTime instance
|
||||
* @param {string} opts.numberingSystem - the numbering system to set on the resulting DateTime instance
|
||||
* @param {string} opts.weekSettings - the week settings to set on the resulting DateTime instance
|
||||
* @example DateTime.fromHTTP('Sun, 06 Nov 1994 08:49:37 GMT')
|
||||
* @example DateTime.fromHTTP('Sunday, 06-Nov-94 08:49:37 GMT')
|
||||
* @example DateTime.fromHTTP('Sun Nov 6 08:49:37 1994')
|
||||
@@ -6153,6 +6368,7 @@ class DateTime {
|
||||
* @param {boolean} [opts.setZone=false] - override the zone with a zone specified in the string itself, if it specifies one
|
||||
* @param {string} [opts.locale='en-US'] - a locale string to use when parsing. Will also set the DateTime to this locale
|
||||
* @param {string} opts.numberingSystem - the numbering system to use when parsing. Will also set the resulting DateTime to this numbering system
|
||||
* @param {string} opts.weekSettings - the week settings to set on the resulting DateTime instance
|
||||
* @param {string} opts.outputCalendar - the output calendar to set on the resulting DateTime instance
|
||||
* @return {DateTime}
|
||||
*/
|
||||
@@ -6191,6 +6407,7 @@ class DateTime {
|
||||
* @param {boolean} [opts.setZone=false] - override the zone with a zone specified in the string itself, if it specifies one
|
||||
* @param {string} [opts.locale='en-US'] - a locale string to use when parsing. Will also set the DateTime to this locale
|
||||
* @param {string} opts.numberingSystem - the numbering system to use when parsing. Will also set the resulting DateTime to this numbering system
|
||||
* @param {string} opts.weekSettings - the week settings to set on the resulting DateTime instance
|
||||
* @param {string} opts.outputCalendar - the output calendar to set on the resulting DateTime instance
|
||||
* @example DateTime.fromSQL('2017-05-15')
|
||||
* @example DateTime.fromSQL('2017-05-15 09:12:34')
|
||||
@@ -6259,6 +6476,11 @@ class DateTime {
|
||||
return expanded.map((t) => t.val).join("");
|
||||
}
|
||||
|
||||
static resetCache() {
|
||||
zoneOffsetTs = undefined;
|
||||
zoneOffsetGuessCache = {};
|
||||
}
|
||||
|
||||
// INFO
|
||||
|
||||
/**
|
||||
@@ -7493,6 +7715,74 @@ class DateTime {
|
||||
return DateTime.fromFormatExplain(text, fmt, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a parser for `fmt` using the given locale. This parser can be passed
|
||||
* to {@link DateTime.fromFormatParser} to a parse a date in this format. This
|
||||
* can be used to optimize cases where many dates need to be parsed in a
|
||||
* specific format.
|
||||
*
|
||||
* @param {String} fmt - the format the string is expected to be in (see
|
||||
* description)
|
||||
* @param {Object} options - options used to set locale and numberingSystem
|
||||
* for parser
|
||||
* @returns {TokenParser} - opaque object to be used
|
||||
*/
|
||||
static buildFormatParser(fmt, options = {}) {
|
||||
const { locale = null, numberingSystem = null } = options,
|
||||
localeToUse = Locale.fromOpts({
|
||||
locale,
|
||||
numberingSystem,
|
||||
defaultToEN: true,
|
||||
});
|
||||
return new TokenParser(localeToUse, fmt);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a DateTime from an input string and format parser.
|
||||
*
|
||||
* The format parser must have been created with the same locale as this call.
|
||||
*
|
||||
* @param {String} text - the string to parse
|
||||
* @param {TokenParser} formatParser - parser from {@link DateTime.buildFormatParser}
|
||||
* @param {Object} opts - options taken by fromFormat()
|
||||
* @returns {DateTime}
|
||||
*/
|
||||
static fromFormatParser(text, formatParser, opts = {}) {
|
||||
if (isUndefined(text) || isUndefined(formatParser)) {
|
||||
throw new InvalidArgumentError(
|
||||
"fromFormatParser requires an input string and a format parser"
|
||||
);
|
||||
}
|
||||
const { locale = null, numberingSystem = null } = opts,
|
||||
localeToUse = Locale.fromOpts({
|
||||
locale,
|
||||
numberingSystem,
|
||||
defaultToEN: true,
|
||||
});
|
||||
|
||||
if (!localeToUse.equals(formatParser.locale)) {
|
||||
throw new InvalidArgumentError(
|
||||
`fromFormatParser called with a locale of ${localeToUse}, ` +
|
||||
`but the format parser was created for ${formatParser.locale}`
|
||||
);
|
||||
}
|
||||
|
||||
const { result, zone, specificOffset, invalidReason } = formatParser.explainFromTokens(text);
|
||||
|
||||
if (invalidReason) {
|
||||
return DateTime.invalid(invalidReason);
|
||||
} else {
|
||||
return parseDataToDateTime(
|
||||
result,
|
||||
zone,
|
||||
opts,
|
||||
`format ${formatParser.format}`,
|
||||
text,
|
||||
specificOffset
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// FORMAT PRESETS
|
||||
|
||||
/**
|
||||
@@ -7689,7 +7979,7 @@ function friendlyDateTime(dateTimeish) {
|
||||
}
|
||||
}
|
||||
|
||||
const VERSION = "3.4.4";
|
||||
const VERSION = "3.5.0";
|
||||
|
||||
export { DateTime, Duration, FixedOffsetZone, IANAZone, Info, Interval, InvalidZone, Settings, SystemZone, VERSION, Zone };
|
||||
//# sourceMappingURL=luxon.js.map
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -353,7 +353,8 @@ body {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
#enabledDisplays, #settings {
|
||||
#enabledDisplays,
|
||||
#settings {
|
||||
margin-bottom: 15px;
|
||||
@include u.status-colors();
|
||||
|
||||
@@ -411,7 +412,12 @@ body {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
align-content: center;
|
||||
|
||||
&.no-cursor {
|
||||
cursor: none;
|
||||
}
|
||||
}
|
||||
|
||||
.kiosk #divTwc {
|
||||
justify-content: unset;
|
||||
}
|
||||
@@ -447,10 +453,10 @@ body {
|
||||
.visible {
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
transition: opacity 1s linear;
|
||||
transition: opacity 0.1s linear;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
#divTwc:fullscreen .hidden {
|
||||
visibility: hidden;
|
||||
opacity: 0;
|
||||
transition: visibility 0s 1s, opacity 1s linear
|
||||
@@ -728,7 +734,12 @@ body {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#share-link-instructions {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.kiosk {
|
||||
|
||||
#divQuery,
|
||||
>.info,
|
||||
>.heading,
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<meta name="author" content="Matt Walsh" />
|
||||
<meta name="application-name" content="WeatherStar 4000+" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,minimum-scale=1">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||
<link rel="manifest" href="manifest.json" />
|
||||
<link rel="icon" href="images/Logo192.png" />
|
||||
@@ -45,7 +45,7 @@
|
||||
<script type="module" src="scripts/modules/radar.mjs"></script>
|
||||
<script type="module" src="scripts/modules/settings.mjs"></script>
|
||||
<script type="module" src="scripts/index.mjs"></script>
|
||||
|
||||
<script type="text/javascript" src="scripts/custom.js"></script>
|
||||
<!-- data -->
|
||||
<script type="text/javascript" src="scripts/data/travelcities.js"></script>
|
||||
<script type="text/javascript" src="scripts/data/regionalcities.js"></script>
|
||||
@@ -151,10 +151,16 @@
|
||||
<div id='settings'>
|
||||
</div>
|
||||
|
||||
<div class='heading'>Sharing</div>
|
||||
<div class='info'>
|
||||
<a href='' id='share-link'>Copy Permalink</a> <span id="share-link-copied">Link copied to clipboard!</span>
|
||||
<div id="share-link-instructions">
|
||||
Copy this long URL:
|
||||
<input type='text' id="share-link-url"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class='heading'>Forecast Information</div>
|
||||
<div id="divInfo">
|
||||
Location: <span id="spanCity"></span> <span id="spanState"></span><br />
|
||||
Station Id: <span id="spanStationId"></span><br />
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
"devbridge",
|
||||
"gifs",
|
||||
"ltrim",
|
||||
"mbar",
|
||||
"Noaa",
|
||||
"nosleep",
|
||||
"Pngs",
|
||||
@@ -51,12 +52,15 @@
|
||||
"[html]": {
|
||||
"editor.defaultFormatter": "j69.ejs-beautify"
|
||||
},
|
||||
"files.exclude": {},
|
||||
"files.exclude": {
|
||||
"**/node_modules": true,
|
||||
"**/debug.log": true,
|
||||
"server/scripts/custom.js": true
|
||||
},
|
||||
"files.eol": "\n",
|
||||
"editor.formatOnSave": true,
|
||||
"editor.codeActionsOnSave": {
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": "explicit"
|
||||
}
|
||||
|
||||
},
|
||||
}
|
||||
Reference in New Issue
Block a user