mirror of
https://github.com/netbymatt/ws4kp.git
synced 2026-04-15 08:09:31 -07:00
Compare commits
61 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9c0d42c5c4 | ||
|
|
07ad5141a4 | ||
|
|
2c010a9a32 | ||
|
|
3b050073ed | ||
|
|
2953dc993c | ||
|
|
f481c5cfeb | ||
|
|
e4672d12d7 | ||
|
|
04ed3e0a52 | ||
|
|
58a337efbf | ||
|
|
5da4a50a96 | ||
|
|
d5cce14fc2 | ||
|
|
d850165752 | ||
|
|
2d0af7a143 | ||
|
|
146a3fda76 | ||
|
|
e1083c83ae | ||
|
|
249cbb93e6 | ||
|
|
888b35ea73 | ||
|
|
2a9e5b370e | ||
|
|
87d4155d71 | ||
|
|
3166dfad16 | ||
|
|
b6cceb5acf | ||
|
|
1303c55851 | ||
|
|
339d391110 | ||
|
|
b6e57e8a19 | ||
|
|
3743c45de6 | ||
|
|
b890b4e53d | ||
|
|
b07478f7ff | ||
|
|
8a25881d5b | ||
|
|
0743b9e2bb | ||
|
|
784c074e32 | ||
|
|
4840909098 | ||
|
|
03dfbc462b | ||
|
|
25291efff5 | ||
|
|
dc77ba835c | ||
|
|
a440990696 | ||
|
|
fc4cbc1415 | ||
|
|
20ba3ddaac | ||
|
|
4205155f96 | ||
|
|
f4101f06cc | ||
|
|
4c3fcfc358 | ||
|
|
366f527aee | ||
|
|
797b4d32fa | ||
|
|
5092076050 | ||
|
|
5d891fb38f | ||
|
|
97e0fda709 | ||
|
|
7cf9dd6466 | ||
|
|
a44bd866ed | ||
|
|
21ef7f476a | ||
|
|
c5b715d631 | ||
|
|
dfd9facc79 | ||
|
|
5b926a358e | ||
|
|
ba1fbd7088 | ||
|
|
f82980ed09 | ||
|
|
af17b3c690 | ||
|
|
2c394c2e4a | ||
|
|
97f8eda236 | ||
|
|
deb107e4ec | ||
|
|
111f077e20 | ||
|
|
806ef91000 | ||
|
|
2a577aaea7 | ||
|
|
49296e53f0 |
3
.dockerignore
Normal file
3
.dockerignore
Normal file
@@ -0,0 +1,3 @@
|
||||
.git/
|
||||
Dockerfile
|
||||
.vscode/
|
||||
27
.eslintrc.js
27
.eslintrc.js
@@ -6,7 +6,10 @@ module.exports = {
|
||||
node: true,
|
||||
jquery: true,
|
||||
},
|
||||
extends: 'airbnb-base',
|
||||
extends: [
|
||||
'airbnb-base',
|
||||
'plugin:sonarjs/recommended',
|
||||
],
|
||||
globals: {
|
||||
Atomics: 'readonly',
|
||||
SharedArrayBuffer: 'readonly',
|
||||
@@ -21,6 +24,10 @@ module.exports = {
|
||||
parserOptions: {
|
||||
ecmaVersion: 2021,
|
||||
},
|
||||
plugins: [
|
||||
'unicorn',
|
||||
'sonarjs',
|
||||
],
|
||||
rules: {
|
||||
indent: [
|
||||
'error',
|
||||
@@ -60,6 +67,24 @@ 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',
|
||||
|
||||
47
.github/workflows/build-docker.yaml
vendored
Normal file
47
.github/workflows/build-docker.yaml
vendored
Normal file
@@ -0,0 +1,47 @@
|
||||
name: build-docker
|
||||
on: push
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build Image
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
- id: short-sha
|
||||
uses: benjlevesque/short-sha@v1.2
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v4
|
||||
with:
|
||||
images: |
|
||||
ghcr.io/netbymatt/ws4kp
|
||||
tags: |
|
||||
type=raw,priority=1000,value=latest,enable=${{ github.ref == 'refs/heads/main' }}
|
||||
type=ref,event=branch
|
||||
${{ steps.short-sha.outputs.sha }}
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
- name: Set up Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Build and Push
|
||||
id: docker_build
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
context: .
|
||||
pull: true
|
||||
push: ${{ github.ref == 'refs/heads/main' }}
|
||||
platforms: linux/amd64,linux/arm/v7,linux/arm64/v8
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
12
.vscode/launch.json
vendored
12
.vscode/launch.json
vendored
@@ -63,7 +63,17 @@
|
||||
"env": {
|
||||
"DIST": "1"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Test",
|
||||
"program": "${workspaceFolder}/tests/index.js",
|
||||
"request": "launch",
|
||||
"skipFiles": [
|
||||
"<node_internals>/**"
|
||||
],
|
||||
"type": "node",
|
||||
"outputCapture": "std"
|
||||
},
|
||||
],
|
||||
"compounds": [
|
||||
{
|
||||
|
||||
14
.vscode/tasks.json
vendored
Normal file
14
.vscode/tasks.json
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"type": "npm",
|
||||
"script": "lint",
|
||||
"problemMatcher": [
|
||||
"$eslint-stylish"
|
||||
],
|
||||
"label": "npm: lint",
|
||||
"detail": "eslint ./server/scripts/**"
|
||||
}
|
||||
]
|
||||
}
|
||||
10
Dockerfile
Normal file
10
Dockerfile
Normal file
@@ -0,0 +1,10 @@
|
||||
FROM node:18-alpine
|
||||
WORKDIR /app
|
||||
|
||||
COPY package.json .
|
||||
COPY package-lock.json .
|
||||
|
||||
RUN npm ci
|
||||
|
||||
COPY . .
|
||||
CMD ["node", "index.js"]
|
||||
@@ -19,12 +19,18 @@ This project is based on the work of [Mike Battaglia](https://github.com/vbguyny
|
||||
## Run Your WeatherStar
|
||||
There are a lot of CORS considerations and issues with api.weather.gov that are easiest to deal with by running a local server to see this in action (or use the live link above). You'll need Node.js >12.0 to run the local server.
|
||||
|
||||
To run via Node locally:
|
||||
```
|
||||
git clone https://github.com/netbymatt/ws4kp.git
|
||||
cd ws4kp
|
||||
npm i
|
||||
node index.js
|
||||
```
|
||||
|
||||
To run via Docker:
|
||||
```
|
||||
docker run -p 8080:8080 ghcr.io/netbymatt/ws4kp
|
||||
```
|
||||
Open your web browser: http://localhost:8080/
|
||||
|
||||
## Updates in 5.0
|
||||
|
||||
@@ -11,7 +11,7 @@ module.exports = (req, res) => {
|
||||
// add out-going headers
|
||||
const headers = {};
|
||||
headers['user-agent'] = '(WeatherStar 4000+, ws4000@netbymatt.com)';
|
||||
headers['accept'] = req.headers.accept;
|
||||
headers.accept = req.headers.accept;
|
||||
|
||||
// get query paramaters if the exist
|
||||
const queryParams = Object.keys(req.query).reduce((acc, key) => {
|
||||
@@ -20,16 +20,16 @@ module.exports = (req, res) => {
|
||||
// add the paramter to the resulting object
|
||||
acc[key] = req.query[key];
|
||||
return acc;
|
||||
},{});
|
||||
}, {});
|
||||
let query = queryString.encode(queryParams);
|
||||
if (query.length > 0) query = '?' + query;
|
||||
if (query.length > 0) query = `?${query}`;
|
||||
|
||||
// get the page
|
||||
https.get('https://www.cpc.ncep.noaa.gov/' + req.path + query, {
|
||||
https.get(`https://www.cpc.ncep.noaa.gov/${req.path}${query}`, {
|
||||
headers,
|
||||
}, getRes => {
|
||||
}, (getRes) => {
|
||||
// pull some info
|
||||
const {statusCode} = getRes;
|
||||
const { statusCode } = getRes;
|
||||
// pass the status code through
|
||||
res.status(statusCode);
|
||||
|
||||
@@ -38,8 +38,7 @@ module.exports = (req, res) => {
|
||||
res.header('last-modified', getRes.headers['last-modified']);
|
||||
// pipe to response
|
||||
getRes.pipe(res);
|
||||
|
||||
}).on('error', e=>{
|
||||
}).on('error', (e) => {
|
||||
console.error(e);
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
@@ -11,7 +11,7 @@ module.exports = (req, res) => {
|
||||
// add out-going headers
|
||||
const headers = {};
|
||||
headers['user-agent'] = '(WeatherStar 4000+, ws4000@netbymatt.com)';
|
||||
headers['accept'] = req.headers.accept;
|
||||
headers.accept = req.headers.accept;
|
||||
|
||||
// get query paramaters if the exist
|
||||
const queryParams = Object.keys(req.query).reduce((acc, key) => {
|
||||
@@ -20,16 +20,16 @@ module.exports = (req, res) => {
|
||||
// add the paramter to the resulting object
|
||||
acc[key] = req.query[key];
|
||||
return acc;
|
||||
},{});
|
||||
}, {});
|
||||
let query = queryString.encode(queryParams);
|
||||
if (query.length > 0) query = '?' + query;
|
||||
if (query.length > 0) query = `?${query}`;
|
||||
|
||||
// get the page
|
||||
https.get('https://radar.weather.gov' + req.path + query, {
|
||||
https.get(`https://radar.weather.gov${req.path}${query}`, {
|
||||
headers,
|
||||
}, getRes => {
|
||||
}, (getRes) => {
|
||||
// pull some info
|
||||
const {statusCode} = getRes;
|
||||
const { statusCode } = getRes;
|
||||
// pass the status code through
|
||||
res.status(statusCode);
|
||||
|
||||
@@ -38,8 +38,7 @@ module.exports = (req, res) => {
|
||||
res.header('last-modified', getRes.headers['last-modified']);
|
||||
// pipe to response
|
||||
getRes.pipe(res);
|
||||
|
||||
}).on('error', e=>{
|
||||
}).on('error', (e) => {
|
||||
console.error(e);
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
2
dist/index.html
vendored
2
dist/index.html
vendored
File diff suppressed because one or more lines are too long
2
dist/resources/data.min.js
vendored
2
dist/resources/data.min.js
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
@@ -71,6 +71,7 @@ gulp.task('compress_js_vendor', () => gulp.src(jsVendorSources)
|
||||
|
||||
const mjsSources = [
|
||||
'server/scripts/modules/currentweatherscroll.mjs',
|
||||
'server/scripts/modules/hazards.mjs',
|
||||
'server/scripts/modules/currentweather.mjs',
|
||||
'server/scripts/modules/almanac.mjs',
|
||||
'server/scripts/modules/icons.mjs',
|
||||
@@ -164,4 +165,6 @@ gulp.task('invalidate', async () => cloudfront.createInvalidation({
|
||||
},
|
||||
}).promise());
|
||||
|
||||
module.exports = gulp.series(clean, gulp.parallel('build_js', 'compress_js_data', 'compress_js_vendor', 'copy_css', 'compress_html', 'copy_other_files'), gulp.parallel('upload', 'upload_images'), 'invalidate');
|
||||
// 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(clean, gulp.parallel('build_js', 'compress_js_data', 'compress_js_vendor', 'copy_css', 'compress_html', 'copy_other_files'), 'upload_images', 'upload', 'invalidate');
|
||||
|
||||
12
index.js
12
index.js
@@ -31,17 +31,17 @@ const index = (req, res) => {
|
||||
};
|
||||
|
||||
// debugging
|
||||
if (process.env?.DIST !== '1') {
|
||||
// debugging
|
||||
app.get('/index.html', index);
|
||||
app.get('/', index);
|
||||
app.get('*', express.static(path.join(__dirname, './server')));
|
||||
} else {
|
||||
if (process.env?.DIST === '1') {
|
||||
// distribution
|
||||
app.use('/images', express.static(path.join(__dirname, './server/images')));
|
||||
app.use('/fonts', express.static(path.join(__dirname, './server/fonts')));
|
||||
app.use('/scripts', express.static(path.join(__dirname, './server/scripts')));
|
||||
app.use('/', express.static(path.join(__dirname, './dist')));
|
||||
} else {
|
||||
// debugging
|
||||
app.get('/index.html', index);
|
||||
app.get('/', index);
|
||||
app.get('*', express.static(path.join(__dirname, './server')));
|
||||
}
|
||||
|
||||
const server = app.listen(port, () => {
|
||||
|
||||
8357
package-lock.json
generated
8357
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
17
package.json
17
package.json
@@ -1,13 +1,13 @@
|
||||
{
|
||||
"name": "ws4kp",
|
||||
"version": "5.8.0",
|
||||
"version": "5.9.9",
|
||||
"description": "Welcome to the WeatherStar 4000+ project page!",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"build:css": "sass ./server/styles/scss/style.scss ./server/styles/compiled.css",
|
||||
"lint": "eslint ./server/scripts/**",
|
||||
"lint:fix": "eslint --fix ./server/scripts/**"
|
||||
"lint": "eslint ./server/scripts/**/*.mjs",
|
||||
"lint:fix": "eslint --fix ./server/scripts/**/*.mjs"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -22,7 +22,11 @@
|
||||
"devDependencies": {
|
||||
"del": "^6.0.0",
|
||||
"ejs": "^3.1.5",
|
||||
"eslint": "^8.21.0",
|
||||
"eslint-config-airbnb-base": "^15.0.0",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-sonarjs": "^0.19.0",
|
||||
"eslint-plugin-unicorn": "^46.0.0",
|
||||
"express": "^4.17.1",
|
||||
"gulp": "^4.0.2",
|
||||
"gulp-concat": "^2.6.1",
|
||||
@@ -40,9 +44,6 @@
|
||||
"suncalc": "^1.8.0",
|
||||
"swiped-events": "^1.1.4",
|
||||
"terser-webpack-plugin": "^5.3.6",
|
||||
"webpack-stream": "^7.0.0",
|
||||
"eslint": "^8.21.0",
|
||||
"eslint-plugin-import": "^2.26.0"
|
||||
},
|
||||
"dependencies": {}
|
||||
"webpack-stream": "^7.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
6
server/scripts/data/.eslintrc.js
Normal file
6
server/scripts/data/.eslintrc.js
Normal file
@@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
rules: {
|
||||
// unicorn
|
||||
'unicorn/numeric-separators-style': 0,
|
||||
},
|
||||
};
|
||||
@@ -22,32 +22,38 @@ const categories = [
|
||||
'Postal', 'Populated Place',
|
||||
];
|
||||
const category = categories.join(',');
|
||||
const TXT_ADDRESS_SELECTOR = '#txtAddress';
|
||||
const TOGGLE_FULL_SCREEN_SELECTOR = '#ToggleFullScreen';
|
||||
const BNT_GET_GPS_SELECTOR = '#btnGetGps';
|
||||
|
||||
const init = () => {
|
||||
document.getElementById('txtAddress').addEventListener('focus', (e) => {
|
||||
document.querySelector(TXT_ADDRESS_SELECTOR).addEventListener('focus', (e) => {
|
||||
e.target.select();
|
||||
});
|
||||
|
||||
registerRefreshData(loadData);
|
||||
|
||||
document.getElementById('NavigateMenu').addEventListener('click', btnNavigateMenuClick);
|
||||
document.getElementById('NavigateRefresh').addEventListener('click', btnNavigateRefreshClick);
|
||||
document.getElementById('NavigateNext').addEventListener('click', btnNavigateNextClick);
|
||||
document.getElementById('NavigatePrevious').addEventListener('click', btnNavigatePreviousClick);
|
||||
document.getElementById('NavigatePlay').addEventListener('click', btnNavigatePlayClick);
|
||||
document.getElementById('ToggleFullScreen').addEventListener('click', btnFullScreenClick);
|
||||
const btnGetGps = document.getElementById('btnGetGps');
|
||||
document.querySelector('#NavigateMenu').addEventListener('click', btnNavigateMenuClick);
|
||||
document.querySelector('#NavigateRefresh').addEventListener('click', btnNavigateRefreshClick);
|
||||
document.querySelector('#NavigateNext').addEventListener('click', btnNavigateNextClick);
|
||||
document.querySelector('#NavigatePrevious').addEventListener('click', btnNavigatePreviousClick);
|
||||
document.querySelector('#NavigatePlay').addEventListener('click', btnNavigatePlayClick);
|
||||
document.querySelector(TOGGLE_FULL_SCREEN_SELECTOR).addEventListener('click', btnFullScreenClick);
|
||||
const btnGetGps = document.querySelector(BNT_GET_GPS_SELECTOR);
|
||||
btnGetGps.addEventListener('click', btnGetGpsClick);
|
||||
if (!navigator.geolocation) btnGetGps.style.display = 'none';
|
||||
|
||||
document.getElementById('divTwc').addEventListener('click', () => {
|
||||
document.querySelector('#divTwc').addEventListener('click', () => {
|
||||
if (document.fullscreenElement) updateFullScreenNavigate();
|
||||
});
|
||||
|
||||
document.querySelector(TXT_ADDRESS_SELECTOR).addEventListener('keydown', (key) => { if (key.code === 'Enter') formSubmit(); });
|
||||
document.querySelector('#btnGetLatLng').addEventListener('click', () => formSubmit());
|
||||
|
||||
document.addEventListener('keydown', documentKeydown);
|
||||
document.addEventListener('touchmove', (e) => { if (fullScreenOverride) e.preventDefault(); });
|
||||
|
||||
$('#frmGetLatLng #txtAddress').devbridgeAutocomplete({
|
||||
$(TXT_ADDRESS_SELECTOR).devbridgeAutocomplete({
|
||||
serviceUrl: 'https://geocode.arcgis.com/arcgis/rest/services/World/GeocodeServer/suggest',
|
||||
deferRequestBy: 300,
|
||||
paramName: 'text',
|
||||
@@ -71,18 +77,18 @@ const init = () => {
|
||||
width: 490,
|
||||
});
|
||||
|
||||
$('#frmGetLatLng').on('submit', () => {
|
||||
const ac = $('#frmGetLatLng #txtAddress').devbridgeAutocomplete();
|
||||
const formSubmit = () => {
|
||||
const ac = $(TXT_ADDRESS_SELECTOR).devbridgeAutocomplete();
|
||||
if (ac.suggestions[0]) $(ac.suggestionsContainer.children[0]).trigger('click');
|
||||
return false;
|
||||
});
|
||||
};
|
||||
|
||||
// Auto load the previous query
|
||||
const query = localStorage.getItem('latLonQuery');
|
||||
const latLon = localStorage.getItem('latLon');
|
||||
const fromGPS = localStorage.getItem('latLonFromGPS');
|
||||
if (query && latLon && !fromGPS) {
|
||||
const txtAddress = document.getElementById('txtAddress');
|
||||
const txtAddress = document.querySelector(TXT_ADDRESS_SELECTOR);
|
||||
txtAddress.value = query;
|
||||
loadData(JSON.parse(latLon));
|
||||
}
|
||||
@@ -90,17 +96,17 @@ const init = () => {
|
||||
btnGetGpsClick();
|
||||
}
|
||||
|
||||
const twcPlay = localStorage.getItem('play');
|
||||
if (twcPlay === null || twcPlay === 'true') postMessage('navButton', 'play');
|
||||
const play = localStorage.getItem('play');
|
||||
if (play === null || play === 'true') postMessage('navButton', 'play');
|
||||
|
||||
document.getElementById('btnClearQuery').addEventListener('click', () => {
|
||||
document.getElementById('spanCity').innerHTML = '';
|
||||
document.getElementById('spanState').innerHTML = '';
|
||||
document.getElementById('spanStationId').innerHTML = '';
|
||||
document.getElementById('spanRadarId').innerHTML = '';
|
||||
document.getElementById('spanZoneId').innerHTML = '';
|
||||
document.querySelector('#btnClearQuery').addEventListener('click', () => {
|
||||
document.querySelector('#spanCity').innerHTML = '';
|
||||
document.querySelector('#spanState').innerHTML = '';
|
||||
document.querySelector('#spanStationId').innerHTML = '';
|
||||
document.querySelector('#spanRadarId').innerHTML = '';
|
||||
document.querySelector('#spanZoneId').innerHTML = '';
|
||||
|
||||
document.getElementById('chkAutoRefresh').checked = true;
|
||||
document.querySelector('#chkAutoRefresh').checked = true;
|
||||
localStorage.removeItem('autoRefresh');
|
||||
|
||||
localStorage.removeItem('play');
|
||||
@@ -109,12 +115,12 @@ const init = () => {
|
||||
localStorage.removeItem('latLonQuery');
|
||||
localStorage.removeItem('latLon');
|
||||
localStorage.removeItem('latLonFromGPS');
|
||||
document.getElementById('btnGetGps').classList.remove('active');
|
||||
document.querySelector(BNT_GET_GPS_SELECTOR).classList.remove('active');
|
||||
});
|
||||
|
||||
// swipe functionality
|
||||
document.getElementById('container').addEventListener('swiped-left', () => swipeCallBack('left'));
|
||||
document.getElementById('container').addEventListener('swiped-right', () => swipeCallBack('right'));
|
||||
document.querySelector('#container').addEventListener('swiped-left', () => swipeCallBack('left'));
|
||||
document.querySelector('#container').addEventListener('swiped-right', () => swipeCallBack('right'));
|
||||
};
|
||||
|
||||
const autocompleteOnSelect = async (suggestion, elem) => {
|
||||
@@ -132,7 +138,7 @@ const autocompleteOnSelect = async (suggestion, elem) => {
|
||||
const loc = data.locations[0];
|
||||
if (loc) {
|
||||
localStorage.removeItem('latLonFromGPS');
|
||||
document.getElementById('btnGetGps').classList.remove('active');
|
||||
document.querySelector(BNT_GET_GPS_SELECTOR).classList.remove('active');
|
||||
doRedirectToGeometry(loc.feature.geometry);
|
||||
} else {
|
||||
console.error('An unexpected error occurred. Please try a different search string.');
|
||||
@@ -142,7 +148,7 @@ const autocompleteOnSelect = async (suggestion, elem) => {
|
||||
const doRedirectToGeometry = (geom, haveDataCallback) => {
|
||||
const latLon = { lat: round2(geom.y, 4), lon: round2(geom.x, 4) };
|
||||
// Save the query
|
||||
localStorage.setItem('latLonQuery', document.getElementById('txtAddress').value);
|
||||
localStorage.setItem('latLonQuery', document.querySelector(TXT_ADDRESS_SELECTOR).value);
|
||||
localStorage.setItem('latLon', JSON.stringify(latLon));
|
||||
|
||||
// get the data
|
||||
@@ -150,10 +156,10 @@ const doRedirectToGeometry = (geom, haveDataCallback) => {
|
||||
};
|
||||
|
||||
const btnFullScreenClick = () => {
|
||||
if (!document.fullscreenElement) {
|
||||
enterFullScreen();
|
||||
} else {
|
||||
if (document.fullscreenElement) {
|
||||
exitFullscreen();
|
||||
} else {
|
||||
enterFullScreen();
|
||||
}
|
||||
|
||||
if (isPlaying()) {
|
||||
@@ -168,7 +174,7 @@ const btnFullScreenClick = () => {
|
||||
};
|
||||
|
||||
const enterFullScreen = () => {
|
||||
const element = document.getElementById('divTwc');
|
||||
const element = document.querySelector('#divTwc');
|
||||
|
||||
// Supports most browsers and their versions.
|
||||
const requestMethod = element.requestFullScreen || element.webkitRequestFullScreen
|
||||
@@ -186,8 +192,8 @@ const enterFullScreen = () => {
|
||||
updateFullScreenNavigate();
|
||||
|
||||
// change hover text and image
|
||||
const img = document.getElementById('ToggleFullScreen');
|
||||
img.src = 'images/nav/ic_fullscreen_exit_white_24dp_1x.png';
|
||||
const img = document.querySelector(TOGGLE_FULL_SCREEN_SELECTOR);
|
||||
img.src = 'images/nav/ic_fullscreen_exit_white_24dp_2x.png';
|
||||
img.title = 'Exit fullscreen';
|
||||
};
|
||||
|
||||
@@ -210,8 +216,8 @@ const exitFullscreen = () => {
|
||||
}
|
||||
resize();
|
||||
// change hover text and image
|
||||
const img = document.getElementById('ToggleFullScreen');
|
||||
img.src = 'images/nav/ic_fullscreen_white_24dp_1x.png';
|
||||
const img = document.querySelector(TOGGLE_FULL_SCREEN_SELECTOR);
|
||||
img.src = 'images/nav/ic_fullscreen_white_24dp_2x.png';
|
||||
img.title = 'Enter fullscreen';
|
||||
};
|
||||
|
||||
@@ -229,7 +235,7 @@ const loadData = (_latLon, haveDataCallback) => {
|
||||
// if there's no data stop
|
||||
if (!latLon) return;
|
||||
|
||||
document.getElementById('txtAddress').blur();
|
||||
document.querySelector(TXT_ADDRESS_SELECTOR).blur();
|
||||
stopAutoRefreshTimer();
|
||||
latLonReceived(latLon, haveDataCallback);
|
||||
};
|
||||
@@ -273,8 +279,9 @@ let navigateFadeIntervalId = null;
|
||||
|
||||
const updateFullScreenNavigate = () => {
|
||||
document.activeElement.blur();
|
||||
document.getElementById('divTwcBottom').classList.remove('hidden');
|
||||
document.getElementById('divTwcBottom').classList.add('visible');
|
||||
const divTwcBottom = document.querySelector('#divTwcBottom');
|
||||
divTwcBottom.classList.remove('hidden');
|
||||
divTwcBottom.classList.add('visible');
|
||||
|
||||
if (navigateFadeIntervalId) {
|
||||
clearTimeout(navigateFadeIntervalId);
|
||||
@@ -283,44 +290,48 @@ const updateFullScreenNavigate = () => {
|
||||
|
||||
navigateFadeIntervalId = setTimeout(() => {
|
||||
if (document.fullscreenElement) {
|
||||
document.getElementById('divTwcBottom').classList.remove('visible');
|
||||
document.getElementById('divTwcBottom').classList.add('hidden');
|
||||
divTwcBottom.classList.remove('visible');
|
||||
divTwcBottom.classList.add('hidden');
|
||||
}
|
||||
}, 2000);
|
||||
};
|
||||
|
||||
const documentKeydown = (e) => {
|
||||
const code = (e.keyCode || e.which);
|
||||
|
||||
// 200ms repeat
|
||||
if ((Date.now() - documentKeydown.lastButton ?? 0) < 200) return false;
|
||||
documentKeydown.lastButton = Date.now();
|
||||
const { key } = e;
|
||||
|
||||
if (document.fullscreenElement || document.activeElement === document.body) {
|
||||
switch (code) {
|
||||
case 32: // Space
|
||||
switch (key) {
|
||||
case ' ': // Space
|
||||
// don't scroll
|
||||
e.preventDefault();
|
||||
btnNavigatePlayClick();
|
||||
return false;
|
||||
|
||||
case 39: // Right Arrow
|
||||
case 34: // Page Down
|
||||
case 'ArrowRight':
|
||||
case 'PageDown':
|
||||
// don't scroll
|
||||
e.preventDefault();
|
||||
btnNavigateNextClick();
|
||||
return false;
|
||||
|
||||
case 37: // Left Arrow
|
||||
case 33: // Page Up
|
||||
case 'ArrowLeft':
|
||||
case 'PageUp':
|
||||
// don't scroll
|
||||
e.preventDefault();
|
||||
btnNavigatePreviousClick();
|
||||
return false;
|
||||
|
||||
case 36: // Home
|
||||
case 'ArrowUp': // Home
|
||||
e.preventDefault();
|
||||
btnNavigateMenuClick();
|
||||
return false;
|
||||
|
||||
case 48: // Restart
|
||||
case '0': // "O" Restart
|
||||
btnNavigateRefreshClick();
|
||||
return false;
|
||||
|
||||
case 70: // F
|
||||
case 'F':
|
||||
case 'f':
|
||||
btnFullScreenClick();
|
||||
return false;
|
||||
|
||||
@@ -348,7 +359,7 @@ const getPosition = async () => new Promise((resolve) => {
|
||||
|
||||
const btnGetGpsClick = async () => {
|
||||
if (!navigator.geolocation) return;
|
||||
const btn = document.getElementById('btnGetGps');
|
||||
const btn = document.querySelector(BNT_GET_GPS_SELECTOR);
|
||||
|
||||
// toggle first
|
||||
if (btn.classList.contains('active')) {
|
||||
@@ -364,11 +375,10 @@ const btnGetGpsClick = async () => {
|
||||
const position = await getPosition();
|
||||
const { latitude, longitude } = position.coords;
|
||||
|
||||
const txtAddress = document.getElementById('txtAddress');
|
||||
const txtAddress = document.querySelector(TXT_ADDRESS_SELECTOR);
|
||||
txtAddress.value = `${round2(latitude, 4)}, ${round2(longitude, 4)}`;
|
||||
|
||||
doRedirectToGeometry({ y: latitude, x: longitude }, (point) => {
|
||||
console.log(point);
|
||||
const location = point.properties.relativeLocation.properties;
|
||||
// Save the query
|
||||
const query = `${location.city}, ${location.state}`;
|
||||
|
||||
@@ -22,7 +22,7 @@ class Almanac extends WeatherDisplay {
|
||||
}
|
||||
|
||||
async getData(_weatherParameters) {
|
||||
if (!super.getData(_weatherParameters)) return;
|
||||
const superResponse = super.getData(_weatherParameters);
|
||||
const weatherParameters = _weatherParameters ?? this.weatherParameters;
|
||||
|
||||
// get sun/moon data
|
||||
@@ -33,11 +33,13 @@ class Almanac extends WeatherDisplay {
|
||||
sun,
|
||||
moon,
|
||||
};
|
||||
// update status
|
||||
this.setStatus(STATUS.loaded);
|
||||
|
||||
// share data
|
||||
this.getDataCallback();
|
||||
|
||||
if (!superResponse) return;
|
||||
|
||||
// update status
|
||||
this.setStatus(STATUS.loaded);
|
||||
}
|
||||
|
||||
calcSunMoonData(weatherParameters) {
|
||||
@@ -171,7 +173,7 @@ const imageName = (type) => {
|
||||
};
|
||||
|
||||
// register display
|
||||
const display = new Almanac(8, 'almanac');
|
||||
const display = new Almanac(9, 'almanac');
|
||||
registerDisplay(display);
|
||||
|
||||
export default display.getSun.bind(display);
|
||||
|
||||
@@ -30,8 +30,9 @@ class CurrentWeather extends WeatherDisplay {
|
||||
const filteredStations = weatherParameters.stations.filter((station) => station?.properties?.stationIdentifier?.length === 4 && !skipStations.includes(station.properties.stationIdentifier.slice(0, 1)));
|
||||
|
||||
// Load the observations
|
||||
let observations; let
|
||||
station;
|
||||
let observations;
|
||||
let station;
|
||||
|
||||
// station number counter
|
||||
let stationNum = 0;
|
||||
while (!observations && stationNum < filteredStations.length) {
|
||||
@@ -53,25 +54,27 @@ class CurrentWeather extends WeatherDisplay {
|
||||
// test data quality
|
||||
if (observations.features[0].properties.temperature.value === null
|
||||
|| observations.features[0].properties.windSpeed.value === null
|
||||
|| observations.features[0].properties.textDescription === null) {
|
||||
|| observations.features[0].properties.textDescription === null
|
||||
|| observations.features[0].properties.textDescription === ''
|
||||
|| observations.features[0].properties.icon === null) {
|
||||
observations = undefined;
|
||||
throw new Error(`Unable to get observations: ${station.properties.stationIdentifier}, trying next station`);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
// test for data received
|
||||
if (!observations) {
|
||||
console.error('All current weather stations exhausted');
|
||||
if (this.enabled) this.setStatus(STATUS.failed);
|
||||
if (this.isEnabled) this.setStatus(STATUS.failed);
|
||||
// send failed to subscribers
|
||||
this.getDataCallback(undefined);
|
||||
return;
|
||||
}
|
||||
|
||||
// we only get here if there was no error above
|
||||
this.data = { ...observations, station };
|
||||
this.data = parseData({ ...observations, station });
|
||||
this.getDataCallback();
|
||||
|
||||
// stop here if we're disabled
|
||||
@@ -82,89 +85,37 @@ class CurrentWeather extends WeatherDisplay {
|
||||
this.setStatus(STATUS.loaded);
|
||||
}
|
||||
|
||||
// format the data for use outside this function
|
||||
parseData() {
|
||||
if (!this.data) return false;
|
||||
const data = {};
|
||||
const observations = this.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.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.Humidity = Math.round(observations.relativeHumidity.value);
|
||||
data.Icon = getWeatherIconFromIconLink(observations.icon);
|
||||
data.PressureDirection = '';
|
||||
data.TextConditions = observations.textDescription;
|
||||
data.station = this.data.station;
|
||||
|
||||
// difference since last measurement (pascals, looking for difference of more than 150)
|
||||
const pressureDiff = (observations.barometricPressure.value - this.data.features[1].properties.barometricPressure.value);
|
||||
if (pressureDiff > 150) data.PressureDirection = 'R';
|
||||
if (pressureDiff < -150) data.PressureDirection = 'F';
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
async drawCanvas() {
|
||||
super.drawCanvas();
|
||||
const fill = {};
|
||||
// parse each time to deal with a change in units if necessary
|
||||
const data = this.parseData();
|
||||
|
||||
fill.temp = data.Temperature + String.fromCharCode(176);
|
||||
|
||||
let Conditions = data.observations.textDescription;
|
||||
if (Conditions.length > 15) {
|
||||
Conditions = shortConditions(Conditions);
|
||||
let condition = this.data.observations.textDescription;
|
||||
if (condition.length > 15) {
|
||||
condition = shortConditions(condition);
|
||||
}
|
||||
fill.condition = Conditions;
|
||||
|
||||
fill.wind = data.WindDirection.padEnd(3, '') + data.WindSpeed.toString().padStart(3, ' ');
|
||||
if (data.WindGust) fill['wind-gusts'] = `Gusts to ${data.WindGust}`;
|
||||
const fill = {
|
||||
temp: this.data.Temperature + String.fromCharCode(176),
|
||||
condition,
|
||||
wind: this.data.WindDirection.padEnd(3, '') + this.data.WindSpeed.toString().padStart(3, ' '),
|
||||
location: locationCleanup(this.data.station.properties.name).substr(0, 20),
|
||||
humidity: `${this.data.Humidity}%`,
|
||||
dewpoint: this.data.DewPoint + String.fromCharCode(176),
|
||||
ceiling: (this.data.Ceiling === 0 ? 'Unlimited' : this.data.Ceiling + this.data.CeilingUnit),
|
||||
visibility: this.data.Visibility + this.data.VisibilityUnit,
|
||||
pressure: `${this.data.Pressure} ${this.data.PressureDirection}`,
|
||||
icon: { type: 'img', src: this.data.Icon },
|
||||
};
|
||||
|
||||
fill.location = locationCleanup(this.data.station.properties.name).substr(0, 20);
|
||||
if (this.data.WindGust) fill['wind-gusts'] = `Gusts to ${this.data.WindGust}`;
|
||||
|
||||
fill.humidity = `${data.Humidity}%`;
|
||||
fill.dewpoint = data.DewPoint + String.fromCharCode(176);
|
||||
fill.ceiling = (data.Ceiling === 0 ? 'Unlimited' : data.Ceiling + data.CeilingUnit);
|
||||
fill.visibility = data.Visibility + data.VisibilityUnit;
|
||||
fill.pressure = `${data.Pressure} ${data.PressureDirection}`;
|
||||
|
||||
if (data.observations.heatIndex.value && data.HeatIndex !== data.Temperature) {
|
||||
if (this.data.observations.heatIndex.value && this.data.HeatIndex !== this.data.Temperature) {
|
||||
fill['heat-index-label'] = 'Heat Index:';
|
||||
fill['heat-index'] = data.HeatIndex + String.fromCharCode(176);
|
||||
} else if (data.observations.windChill.value && data.WindChill !== '' && data.WindChill < data.Temperature) {
|
||||
fill['heat-index'] = this.data.HeatIndex + String.fromCharCode(176);
|
||||
} else if (this.data.observations.windChill.value && this.data.WindChill !== '' && this.data.WindChill < this.data.Temperature) {
|
||||
fill['heat-index-label'] = 'Wind Chill:';
|
||||
fill['heat-index'] = data.WindChill + String.fromCharCode(176);
|
||||
fill['heat-index'] = this.data.WindChill + String.fromCharCode(176);
|
||||
}
|
||||
|
||||
fill.icon = { type: 'img', src: data.Icon };
|
||||
|
||||
const area = this.elem.querySelector('.main');
|
||||
|
||||
area.innerHTML = '';
|
||||
@@ -178,9 +129,9 @@ class CurrentWeather extends WeatherDisplay {
|
||||
async getCurrentWeather(stillWaiting) {
|
||||
if (stillWaiting) this.stillWaitingCallbacks.push(stillWaiting);
|
||||
return new Promise((resolve) => {
|
||||
if (this.data) resolve(this.parseData());
|
||||
if (this.data) resolve(this.data);
|
||||
// data not available, put it into the data callback queue
|
||||
this.getDataCallbacks.push(() => resolve(this.parseData()));
|
||||
this.getDataCallbacks.push(() => resolve(this.data));
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -204,7 +155,53 @@ const shortConditions = (_condition) => {
|
||||
return condition;
|
||||
};
|
||||
|
||||
const display = new CurrentWeather(0, 'current-weather');
|
||||
// format the received data
|
||||
const parseData = (data) => {
|
||||
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.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.Humidity = Math.round(observations.relativeHumidity.value);
|
||||
data.Icon = getWeatherIconFromIconLink(observations.icon);
|
||||
data.PressureDirection = '';
|
||||
data.TextConditions = observations.textDescription;
|
||||
|
||||
// difference since last measurement (pascals, looking for difference of more than 150)
|
||||
const pressureDiff = (observations.barometricPressure.value - data.features[1].properties.barometricPressure.value);
|
||||
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;
|
||||
};
|
||||
|
||||
const display = new CurrentWeather(1, 'current-weather');
|
||||
registerDisplay(display);
|
||||
|
||||
export default display.getCurrentWeather.bind(display);
|
||||
|
||||
@@ -43,7 +43,7 @@ const incrementInterval = () => {
|
||||
|
||||
const drawScreen = async () => {
|
||||
// get the conditions
|
||||
const data = await getCurrentWeather(() => this.stillWaiting());
|
||||
const data = await getCurrentWeather();
|
||||
|
||||
// nothing to do if there's no data yet
|
||||
if (!data) return;
|
||||
@@ -75,12 +75,10 @@ const screens = [
|
||||
|
||||
// wind
|
||||
(data) => {
|
||||
let text = '';
|
||||
if (data.WindSpeed > 0) {
|
||||
text = `Wind: ${data.WindDirection} ${data.WindSpeed} ${data.WindUnit}`;
|
||||
} else {
|
||||
text = 'Wind: Calm';
|
||||
}
|
||||
let text = data.WindSpeed > 0
|
||||
? `Wind: ${data.WindDirection} ${data.WindSpeed} ${data.WindUnit}`
|
||||
: 'Wind: Calm';
|
||||
|
||||
if (data.WindGust > 0) {
|
||||
text += ` Gusts to ${data.WindGust}`;
|
||||
}
|
||||
@@ -88,7 +86,10 @@ const screens = [
|
||||
},
|
||||
|
||||
// visibility
|
||||
(data) => `Visib: ${data.Visibility} ${data.VisibilityUnit} Ceiling: ${data.Ceiling === 0 ? 'Unlimited' : `${data.Ceiling} ${data.CeilingUnit}`}`,
|
||||
(data) => {
|
||||
const distance = `${data.Ceiling} ${data.CeilingUnit}`;
|
||||
return `Visib: ${data.Visibility} ${data.VisibilityUnit} Ceiling: ${data.Ceiling === 0 ? 'Unlimited' : distance}`;
|
||||
},
|
||||
];
|
||||
|
||||
// internal draw function with preset parameters
|
||||
|
||||
@@ -31,9 +31,9 @@ class ExtendedForecast extends WeatherDisplay {
|
||||
retryCount: 3,
|
||||
stillWaiting: () => this.stillWaiting(),
|
||||
});
|
||||
} catch (e) {
|
||||
} catch (error) {
|
||||
console.error('Unable to get extended forecast');
|
||||
console.error(e.status, e.responseJSON);
|
||||
console.error(error.status, error.responseJSON);
|
||||
this.setStatus(STATUS.failed);
|
||||
return;
|
||||
}
|
||||
@@ -52,8 +52,11 @@ class ExtendedForecast extends WeatherDisplay {
|
||||
|
||||
// create each day template
|
||||
const days = forecast.map((Day) => {
|
||||
const fill = {};
|
||||
fill.date = Day.dayName;
|
||||
const fill = {
|
||||
icon: { type: 'img', src: Day.icon },
|
||||
condition: Day.text,
|
||||
date: Day.dayName,
|
||||
};
|
||||
|
||||
const { low } = Day;
|
||||
if (low !== undefined) {
|
||||
@@ -61,10 +64,6 @@ class ExtendedForecast extends WeatherDisplay {
|
||||
}
|
||||
const { high } = Day;
|
||||
fill['value-hi'] = Math.round(high);
|
||||
fill.condition = Day.text;
|
||||
|
||||
// draw the icon
|
||||
fill.icon = { type: 'img', src: Day.icon };
|
||||
|
||||
// return the filled template
|
||||
return this.fillTemplate('day', fill);
|
||||
@@ -123,13 +122,13 @@ const parse = (fullForecast) => {
|
||||
|
||||
const shortenExtendedForecastText = (long) => {
|
||||
const regexList = [
|
||||
[/ and /ig, ' '],
|
||||
[/Slight /ig, ''],
|
||||
[/Chance /ig, ''],
|
||||
[/Very /ig, ''],
|
||||
[/Patchy /ig, ''],
|
||||
[/Areas /ig, ''],
|
||||
[/Dense /ig, ''],
|
||||
[/ and /gi, ' '],
|
||||
[/slight /gi, ''],
|
||||
[/chance /gi, ''],
|
||||
[/very /gi, ''],
|
||||
[/patchy /gi, ''],
|
||||
[/areas /gi, ''],
|
||||
[/dense /gi, ''],
|
||||
[/Thunderstorm/g, 'T\'Storm'],
|
||||
];
|
||||
// run all regexes
|
||||
@@ -144,10 +143,10 @@ const shortenExtendedForecastText = (long) => {
|
||||
let short1 = conditions[0].substr(0, 10);
|
||||
let short2 = '';
|
||||
if (conditions[1]) {
|
||||
if (!short1.endsWith('.')) {
|
||||
short2 = conditions[1].substr(0, 10);
|
||||
} else {
|
||||
if (short1.endsWith('.')) {
|
||||
short1 = short1.replace(/\./, '');
|
||||
} else {
|
||||
short2 = conditions[1].substr(0, 10);
|
||||
}
|
||||
|
||||
if (short2 === 'Blowing') {
|
||||
@@ -163,4 +162,4 @@ const shortenExtendedForecastText = (long) => {
|
||||
};
|
||||
|
||||
// register display
|
||||
registerDisplay(new ExtendedForecast(7, 'extended-forecast'));
|
||||
registerDisplay(new ExtendedForecast(8, 'extended-forecast'));
|
||||
|
||||
140
server/scripts/modules/hazards.mjs
Normal file
140
server/scripts/modules/hazards.mjs
Normal file
@@ -0,0 +1,140 @@
|
||||
// hourly forecast list
|
||||
|
||||
import STATUS from './status.mjs';
|
||||
import { json } from './utils/fetch.mjs';
|
||||
import WeatherDisplay from './weatherdisplay.mjs';
|
||||
import { registerDisplay } from './navigation.mjs';
|
||||
|
||||
const hazardLevels = {
|
||||
Extreme: 10,
|
||||
Severe: 5,
|
||||
};
|
||||
|
||||
class Hazards extends WeatherDisplay {
|
||||
constructor(navId, elemId, defaultActive) {
|
||||
// special height and width for scrolling
|
||||
super(navId, elemId, 'Hazards', defaultActive);
|
||||
this.showOnProgress = false;
|
||||
|
||||
// 0 screens skips this during "play"
|
||||
this.timing.totalScreens = 0;
|
||||
}
|
||||
|
||||
async getData(weatherParameters) {
|
||||
// super checks for enabled
|
||||
const superResult = super.getData(weatherParameters);
|
||||
|
||||
const alert = this.checkbox.querySelector('.alert');
|
||||
alert.classList.remove('show');
|
||||
|
||||
try {
|
||||
// get the forecast
|
||||
const url = new URL('https://api.weather.gov/alerts/active');
|
||||
url.searchParams.append('point', `${this.weatherParameters.latitude},${this.weatherParameters.longitude}`);
|
||||
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');
|
||||
this.data = filteredAlerts;
|
||||
|
||||
// show alert indicator
|
||||
if (this.data.length > 0) alert.classList.add('show');
|
||||
} catch (error) {
|
||||
console.error('Get hourly forecast failed');
|
||||
console.error(error.status, error.responseJSON);
|
||||
if (this.isEnabled) this.setStatus(STATUS.failed);
|
||||
// return undefined to other subscribers
|
||||
this.getDataCallback(undefined);
|
||||
return;
|
||||
}
|
||||
|
||||
this.getDataCallback();
|
||||
|
||||
if (!superResult) {
|
||||
this.setStatus(STATUS.loaded);
|
||||
return;
|
||||
}
|
||||
this.drawLongCanvas();
|
||||
}
|
||||
|
||||
async drawLongCanvas() {
|
||||
// get the list element and populate
|
||||
const list = this.elem.querySelector('.hazard-lines');
|
||||
list.innerHTML = '';
|
||||
|
||||
const lines = this.data.map((data) => {
|
||||
const fillValues = {};
|
||||
// text
|
||||
fillValues['hazard-text'] = `${data.properties.event}<br/><br/>${data.properties.description.replace('\n', '<br/><br/>')}`;
|
||||
|
||||
return this.fillTemplate('hazard', fillValues);
|
||||
});
|
||||
|
||||
list.append(...lines);
|
||||
|
||||
// no alerts, skip this display by setting timing to zero
|
||||
if (lines.length === 0) {
|
||||
this.setStatus(STATUS.loaded);
|
||||
this.timing.totalScreens = 0;
|
||||
this.setStatus(STATUS.loaded);
|
||||
return;
|
||||
}
|
||||
|
||||
// update timing
|
||||
// set up the timing
|
||||
this.timing.baseDelay = 20;
|
||||
// 24 hours = 6 pages
|
||||
const pages = Math.max(Math.ceil(list.scrollHeight / 400) - 3, 1);
|
||||
const timingStep = 400;
|
||||
this.timing.delay = [150 + timingStep];
|
||||
// add additional pages
|
||||
for (let i = 0; i < pages; i += 1) this.timing.delay.push(timingStep);
|
||||
// add the final 3 second delay
|
||||
this.timing.delay.push(250);
|
||||
this.calcNavTiming();
|
||||
this.setStatus(STATUS.loaded);
|
||||
}
|
||||
|
||||
drawCanvas() {
|
||||
super.drawCanvas();
|
||||
this.finishDraw();
|
||||
}
|
||||
|
||||
showCanvas() {
|
||||
// special to hourly to draw the remainder of the canvas
|
||||
this.drawCanvas();
|
||||
super.showCanvas();
|
||||
}
|
||||
|
||||
// screen index change callback just runs the base count callback
|
||||
screenIndexChange() {
|
||||
this.baseCountChange(this.navBaseCount);
|
||||
}
|
||||
|
||||
// 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));
|
||||
|
||||
// don't let offset go negative
|
||||
if (offsetY < 0) offsetY = 0;
|
||||
|
||||
// copy the scrolled portion of the canvas
|
||||
this.elem.querySelector('.main').scrollTo(0, offsetY);
|
||||
}
|
||||
|
||||
// make data available outside this class
|
||||
// promise allows for data to be requested before it is available
|
||||
async getCurrentData(stillWaiting) {
|
||||
if (stillWaiting) this.stillWaitingCallbacks.push(stillWaiting);
|
||||
return new Promise((resolve) => {
|
||||
if (this.data) resolve(this.data);
|
||||
// data not available, put it into the data callback queue
|
||||
this.getDataCallbacks.push(() => resolve(this.data));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// register display
|
||||
registerDisplay(new Hazards(0, 'hazards', true));
|
||||
@@ -8,7 +8,6 @@ import { DateTime } from '../vendor/auto/luxon.mjs';
|
||||
|
||||
class HourlyGraph extends WeatherDisplay {
|
||||
constructor(navId, elemId, defaultActive) {
|
||||
// special height and width for scrolling
|
||||
super(navId, elemId, 'Hourly Graph', defaultActive);
|
||||
|
||||
// move the top right data into the correct location on load
|
||||
@@ -146,4 +145,4 @@ const drawPath = (path, ctx, options) => {
|
||||
const formatTime = (time) => time.toFormat('ha').slice(0, -1);
|
||||
|
||||
// register display
|
||||
registerDisplay(new HourlyGraph(3, 'hourly-graph'));
|
||||
registerDisplay(new HourlyGraph(4, 'hourly-graph'));
|
||||
|
||||
@@ -34,10 +34,10 @@ class Hourly extends WeatherDisplay {
|
||||
try {
|
||||
// get the forecast
|
||||
forecast = await json(weatherParameters.forecastGridData, { retryCount: 3, stillWaiting: () => this.stillWaiting() });
|
||||
} catch (e) {
|
||||
} catch (error) {
|
||||
console.error('Get hourly forecast failed');
|
||||
console.error(e.status, e.responseJSON);
|
||||
if (this.enabled) this.setStatus(STATUS.failed);
|
||||
console.error(error.status, error.responseJSON);
|
||||
if (this.isEnabled) this.setStatus(STATUS.failed);
|
||||
// return undefined to other subscribers
|
||||
this.getDataCallback(undefined);
|
||||
return;
|
||||
@@ -183,7 +183,7 @@ const expand = (data) => {
|
||||
result.push(item.value); // push data array
|
||||
} // timestamp is after now
|
||||
// increment start time by 1 hour
|
||||
startTime += 3600000;
|
||||
startTime += 3_600_000;
|
||||
} while (startTime < endTime && result.length < 24);
|
||||
}); // for each value
|
||||
|
||||
@@ -191,7 +191,7 @@ const expand = (data) => {
|
||||
};
|
||||
|
||||
// register display
|
||||
const display = new Hourly(2, 'hourly', false);
|
||||
const display = new Hourly(3, 'hourly', false);
|
||||
registerDisplay(display);
|
||||
|
||||
export default display.getCurrentData.bind(display);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable unicorn/consistent-function-scoping */
|
||||
/* spell-checker: disable */
|
||||
|
||||
const getWeatherRegionalIconFromIconLink = (link, _isNightTime) => {
|
||||
@@ -8,7 +9,7 @@ const getWeatherRegionalIconFromIconLink = (link, _isNightTime) => {
|
||||
|
||||
// grab everything after the last slash ending at any of these: ?&,
|
||||
const afterLastSlash = link.toLowerCase().match(/[^/]+$/)[0];
|
||||
let conditionName = afterLastSlash.match(/(.*?)[,?&.]/)[1];
|
||||
let conditionName = afterLastSlash.match(/(.*?)[&,.?]/)[1];
|
||||
// using probability as a crude heavy/light indication where possible
|
||||
const value = +(link.match(/,(\d{2,3})/) ?? [0, 100])[1];
|
||||
|
||||
@@ -87,6 +88,7 @@ const getWeatherRegionalIconFromIconLink = (link, _isNightTime) => {
|
||||
return addPath('Light-Snow.gif');
|
||||
|
||||
case 'rain_snow':
|
||||
case 'rain_snow-n':
|
||||
return addPath('Rain-Snow-1992.gif');
|
||||
|
||||
case 'snow_fzra':
|
||||
@@ -95,6 +97,8 @@ const getWeatherRegionalIconFromIconLink = (link, _isNightTime) => {
|
||||
|
||||
case 'fzra':
|
||||
case 'fzra-n':
|
||||
case 'rain_fzra':
|
||||
case 'rain_fzra-n':
|
||||
return addPath('Freezing-Rain-1992.gif');
|
||||
|
||||
case 'snow_sleet':
|
||||
@@ -119,10 +123,15 @@ const getWeatherRegionalIconFromIconLink = (link, _isNightTime) => {
|
||||
case 'tropical_storm':
|
||||
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_skc':
|
||||
@@ -133,6 +142,7 @@ const getWeatherRegionalIconFromIconLink = (link, _isNightTime) => {
|
||||
return addPath('Clear-Wind-1994.gif');
|
||||
|
||||
case 'blizzard':
|
||||
case 'blizzard-n':
|
||||
return addPath('Blowing Snow.gif');
|
||||
|
||||
case 'cold':
|
||||
@@ -154,7 +164,7 @@ const getWeatherIconFromIconLink = (link, _isNightTime) => {
|
||||
|
||||
// grab everything after the last slash ending at any of these: ?&,
|
||||
const afterLastSlash = link.toLowerCase().match(/[^/]+$/)[0];
|
||||
let conditionName = afterLastSlash.match(/(.*?)[,?&.]/)[1];
|
||||
let conditionName = afterLastSlash.match(/(.*?)[&,.?]/)[1];
|
||||
// using probability as a crude heavy/light indication where possible
|
||||
const value = +(link.match(/,(\d{2,3})/) ?? [0, 100])[1];
|
||||
|
||||
@@ -202,6 +212,9 @@ const getWeatherIconFromIconLink = (link, _isNightTime) => {
|
||||
return addPath('CC_Fog.gif');
|
||||
|
||||
case 'rain_sleet':
|
||||
case 'rain_sleet-n':
|
||||
case 'sleet':
|
||||
case 'sleet-n':
|
||||
return addPath('Sleet.gif');
|
||||
|
||||
case 'rain_showers':
|
||||
@@ -237,6 +250,8 @@ const getWeatherIconFromIconLink = (link, _isNightTime) => {
|
||||
case 'snow_fzra-n':
|
||||
case 'fzra':
|
||||
case 'fzra-n':
|
||||
case 'rain_fzra':
|
||||
case 'rain_fzra-n':
|
||||
return addPath('CC_FreezingRain.gif');
|
||||
|
||||
case 'snow_sleet':
|
||||
@@ -260,14 +275,16 @@ const getWeatherIconFromIconLink = (link, _isNightTime) => {
|
||||
case 'wind_sct':
|
||||
case 'wind_bkn':
|
||||
case 'wind_ovc':
|
||||
return addPath('CC_Windy.gif');
|
||||
|
||||
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');
|
||||
|
||||
default:
|
||||
|
||||
@@ -32,31 +32,25 @@ class LatestObservations extends WeatherDisplay {
|
||||
const regionalStations = sortedStations.slice(0, 30);
|
||||
|
||||
// get data for regional stations
|
||||
const allConditions = await Promise.all(regionalStations.map(async (station) => {
|
||||
try {
|
||||
const data = await json(`https://api.weather.gov/stations/${station.id}/observations/latest`, { retryCount: 3, 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;
|
||||
// format the return values
|
||||
return {
|
||||
...data.properties,
|
||||
StationId: station.id,
|
||||
city: station.city,
|
||||
};
|
||||
} catch (e) {
|
||||
console.log(`Unable to get latest observations for ${station.id}`);
|
||||
return false;
|
||||
}
|
||||
}));
|
||||
// remove and stations that did not return data
|
||||
const actualConditions = allConditions.filter((condition) => condition);
|
||||
// get first 7 stations
|
||||
const actualConditions = [];
|
||||
let lastStation = Math.min(regionalStations.length, 7);
|
||||
let firstStation = 0;
|
||||
while (actualConditions.length < 7 && (lastStation) <= regionalStations.length) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const someStations = await getStations(regionalStations.slice(firstStation, lastStation));
|
||||
|
||||
actualConditions.push(...someStations);
|
||||
// update counters
|
||||
firstStation += lastStation;
|
||||
lastStation = Math.min(regionalStations.length + 1, firstStation + 7 - actualConditions.length);
|
||||
}
|
||||
|
||||
// cut down to the maximum of 7
|
||||
this.data = actualConditions.slice(0, this.MaximumRegionalStations);
|
||||
|
||||
// test for at least one station
|
||||
if (this.data.length < 1) {
|
||||
if (this.data.length === 0) {
|
||||
this.setStatus(STATUS.noData);
|
||||
return;
|
||||
}
|
||||
@@ -79,10 +73,12 @@ class LatestObservations extends WeatherDisplay {
|
||||
const Temperature = Math.round(celsiusToFahrenheit(condition.temperature.value));
|
||||
const WindSpeed = Math.round(kphToMph(condition.windSpeed.value));
|
||||
|
||||
const fill = {};
|
||||
fill.location = locationCleanup(condition.city).substr(0, 14);
|
||||
fill.temp = Temperature;
|
||||
fill.weather = shortenCurrentConditions(condition.textDescription).substr(0, 9);
|
||||
const fill = {
|
||||
location: locationCleanup(condition.city).substr(0, 14),
|
||||
temp: Temperature,
|
||||
weather: shortenCurrentConditions(condition.textDescription).substr(0, 9),
|
||||
};
|
||||
|
||||
if (WindSpeed > 0) {
|
||||
fill.wind = windDirection + (Array(6 - windDirection.length - WindSpeed.toString().length).join(' ')) + WindSpeed.toString();
|
||||
} else if (WindSpeed === 'NA') {
|
||||
@@ -119,5 +115,28 @@ const shortenCurrentConditions = (_condition) => {
|
||||
condition = condition.replace(/ with /, '/');
|
||||
return condition;
|
||||
};
|
||||
|
||||
const getStations = async (stations) => {
|
||||
const stationData = await Promise.all(stations.map(async (station) => {
|
||||
try {
|
||||
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;
|
||||
// format the return values
|
||||
return {
|
||||
...data.properties,
|
||||
StationId: station.id,
|
||||
city: station.city,
|
||||
};
|
||||
} catch (error) {
|
||||
console.log(`Unable to get latest observations for ${station.id}`);
|
||||
return false;
|
||||
}
|
||||
}));
|
||||
// filter false (no data or other error)
|
||||
return stationData.filter((d) => d);
|
||||
};
|
||||
// register display
|
||||
registerDisplay(new LatestObservations(1, 'latest-observations'));
|
||||
registerDisplay(new LatestObservations(2, 'latest-observations'));
|
||||
|
||||
@@ -66,9 +66,9 @@ class LocalForecast extends WeatherDisplay {
|
||||
retryCount: 3,
|
||||
stillWaiting: () => this.stillWaiting(),
|
||||
});
|
||||
} catch (e) {
|
||||
} catch (error) {
|
||||
console.error(`GetWeatherForecast failed: ${weatherParameters.forecast}`);
|
||||
console.error(e.status, e.responseJSON);
|
||||
console.error(error.status, error.responseJSON);
|
||||
this.setStatus(STATUS.failed);
|
||||
return false;
|
||||
}
|
||||
@@ -92,4 +92,4 @@ const parse = (forecast) => forecast.properties.periods.slice(0, 6).map((text) =
|
||||
Text: text.detailedForecast,
|
||||
}));
|
||||
// register display
|
||||
registerDisplay(new LocalForecast(6, 'local-forecast'));
|
||||
registerDisplay(new LocalForecast(7, 'local-forecast'));
|
||||
|
||||
@@ -16,7 +16,8 @@ const weatherParameters = {};
|
||||
|
||||
// auto refresh
|
||||
const AUTO_REFRESH_INTERVAL_MS = 500;
|
||||
const AUTO_REFRESH_TIME_MS = 600000; // 10 min.
|
||||
const AUTO_REFRESH_TIME_MS = 600_000; // 10 min.
|
||||
const CHK_AUTO_REFRESH_SELECTOR = '#chkAutoRefresh';
|
||||
let AutoRefreshIntervalId = null;
|
||||
let AutoRefreshCountMs = 0;
|
||||
|
||||
@@ -26,27 +27,21 @@ const init = async () => {
|
||||
resize();
|
||||
|
||||
// auto refresh
|
||||
const TwcAutoRefresh = localStorage.getItem('TwcAutoRefresh');
|
||||
if (!TwcAutoRefresh || TwcAutoRefresh === 'true') {
|
||||
document.getElementById('chkAutoRefresh').checked = true;
|
||||
const autoRefresh = localStorage.getItem('autoRefresh');
|
||||
if (!autoRefresh || autoRefresh === 'true') {
|
||||
document.querySelector(CHK_AUTO_REFRESH_SELECTOR).checked = true;
|
||||
} else {
|
||||
document.getElementById('chkAutoRefresh').checked = false;
|
||||
document.querySelector(CHK_AUTO_REFRESH_SELECTOR).checked = false;
|
||||
}
|
||||
document.getElementById('chkAutoRefresh').addEventListener('change', autoRefreshChange);
|
||||
document.querySelector(CHK_AUTO_REFRESH_SELECTOR).addEventListener('change', autoRefreshChange);
|
||||
generateCheckboxes();
|
||||
};
|
||||
|
||||
const message = (data) => {
|
||||
// dispatch event
|
||||
if (!data.type) return;
|
||||
switch (data.type) {
|
||||
case 'navButton':
|
||||
handleNavButton(data.message);
|
||||
break;
|
||||
|
||||
default:
|
||||
console.error(`Unknown event ${data.type}`);
|
||||
}
|
||||
if (!data.type) return false;
|
||||
if (data.type === 'navButton') return handleNavButton(data.message);
|
||||
return console.error(`Unknown event ${data.type}`);
|
||||
};
|
||||
|
||||
const getWeather = async (latLon, haveDataCallback) => {
|
||||
@@ -61,10 +56,12 @@ const getWeather = async (latLon, haveDataCallback) => {
|
||||
const StationId = stations.features[0].properties.stationIdentifier;
|
||||
|
||||
let { city } = point.properties.relativeLocation.properties;
|
||||
const { state } = point.properties.relativeLocation.properties;
|
||||
|
||||
if (StationId in StationInfo) {
|
||||
city = StationInfo[StationId].city;
|
||||
[city] = city.split('/');
|
||||
city = city.replace(/\s+$/, '');
|
||||
}
|
||||
|
||||
// populate the weather parameters
|
||||
@@ -75,8 +72,8 @@ const getWeather = async (latLon, haveDataCallback) => {
|
||||
weatherParameters.stationId = StationId;
|
||||
weatherParameters.weatherOffice = point.properties.cwa;
|
||||
weatherParameters.city = city;
|
||||
weatherParameters.state = point.properties.relativeLocation.properties.state;
|
||||
weatherParameters.timeZone = point.properties.relativeLocation.properties.timeZone;
|
||||
weatherParameters.state = state;
|
||||
weatherParameters.timeZone = point.properties.timeZone;
|
||||
weatherParameters.forecast = point.properties.forecast;
|
||||
weatherParameters.forecastGridData = point.properties.forecastGridData;
|
||||
weatherParameters.stations = stations.features;
|
||||
@@ -86,7 +83,7 @@ const getWeather = async (latLon, haveDataCallback) => {
|
||||
|
||||
// draw the progress canvas and hide others
|
||||
hideAllCanvases();
|
||||
document.getElementById('loading').style.display = 'none';
|
||||
document.querySelector('#loading').style.display = 'none';
|
||||
if (progress) {
|
||||
await progress.drawCanvas();
|
||||
progress.showCanvas();
|
||||
@@ -102,8 +99,24 @@ const updateStatus = (value) => {
|
||||
if (!progress) return;
|
||||
progress.drawCanvas(displays, countLoadedDisplays());
|
||||
|
||||
// first display is hazards and it must load before evaluating the first display
|
||||
if (displays[0].status === STATUS.loading) return;
|
||||
|
||||
// calculate first enabled display
|
||||
const firstDisplayIndex = displays.findIndex((display) => display.enabled);
|
||||
const firstDisplayIndex = displays.findIndex((display) => display.enabled && display.timing.totalScreens > 0);
|
||||
|
||||
// value.id = 0 is hazards, if they fail to load hot-wire a new value.id to the current display to see if it needs to be loaded
|
||||
// typically this plays out as current conditions loads, then hazards fails.
|
||||
if (value.id === 0 && (value.status === STATUS.failed || value.status === STATUS.retrying)) {
|
||||
value.id = firstDisplayIndex;
|
||||
value.status = displays[firstDisplayIndex].status;
|
||||
}
|
||||
|
||||
// if hazards data arrives after the firstDisplayIndex loads, then we need to hot wire this to the first display
|
||||
if (value.id === 0 && value.status === STATUS.loaded && displays[0].timing.totalScreens === 0) {
|
||||
value.id = firstDisplayIndex;
|
||||
value.status = displays[firstDisplayIndex].status;
|
||||
}
|
||||
|
||||
// if this is the first display and we're playing, load it up so it starts playing
|
||||
if (isPlaying() && value.id === firstDisplayIndex && value.status === STATUS.loaded) {
|
||||
@@ -164,13 +177,14 @@ const navTo = (direction) => {
|
||||
let firstDisplay;
|
||||
let displayCount = 0;
|
||||
do {
|
||||
if (displays[displayCount].status === STATUS.loaded) firstDisplay = displays[displayCount];
|
||||
if (displays[displayCount].status === STATUS.loaded && displays[displayCount].timing.totalScreens > 0) firstDisplay = displays[displayCount];
|
||||
displayCount += 1;
|
||||
} while (!firstDisplay && displayCount < displays.length);
|
||||
|
||||
if (!firstDisplay) return;
|
||||
|
||||
firstDisplay.navNext(msg.command.firstFrame);
|
||||
firstDisplay.showCanvas();
|
||||
return;
|
||||
}
|
||||
if (direction === msg.command.nextFrame) currentDisplay().navNext();
|
||||
@@ -185,12 +199,10 @@ const loadDisplay = (direction) => {
|
||||
for (let i = 0; i < totalDisplays; i += 1) {
|
||||
// convert form simple 0-10 to start at current display index +/-1 and wrap
|
||||
idx = wrap(curIdx + (i + 1) * direction, totalDisplays);
|
||||
if (displays[idx].status === STATUS.loaded) break;
|
||||
if (displays[idx].status === STATUS.loaded && displays[idx].timing.totalScreens > 0) break;
|
||||
}
|
||||
// if new display index is less than current display a wrap occurred, test for reload timeout
|
||||
if (idx <= curIdx) {
|
||||
if (refreshCheck()) return;
|
||||
}
|
||||
if (idx <= curIdx && refreshCheck()) return;
|
||||
const newDisplay = displays[idx];
|
||||
// hide all displays
|
||||
hideAllCanvases();
|
||||
@@ -200,25 +212,22 @@ const loadDisplay = (direction) => {
|
||||
};
|
||||
|
||||
// get the current display index or value
|
||||
const currentDisplayIndex = () => {
|
||||
const index = displays.findIndex((display) => display.isActive());
|
||||
return index;
|
||||
};
|
||||
const currentDisplayIndex = () => displays.findIndex((display) => display.active);
|
||||
const currentDisplay = () => displays[currentDisplayIndex()];
|
||||
|
||||
const setPlaying = (newValue) => {
|
||||
playing = newValue;
|
||||
const playButton = document.getElementById('NavigatePlay');
|
||||
localStorage.setItem('TwcPlay', playing);
|
||||
const playButton = document.querySelector('#NavigatePlay');
|
||||
localStorage.setItem('play', playing);
|
||||
|
||||
if (playing) {
|
||||
noSleep(true);
|
||||
playButton.title = 'Pause';
|
||||
playButton.src = 'images/nav/ic_pause_white_24dp_1x.png';
|
||||
playButton.src = 'images/nav/ic_pause_white_24dp_2x.png';
|
||||
} else {
|
||||
noSleep(false);
|
||||
playButton.title = 'Play';
|
||||
playButton.src = 'images/nav/ic_play_arrow_white_24dp_1x.png';
|
||||
playButton.src = 'images/nav/ic_play_arrow_white_24dp_2x.png';
|
||||
}
|
||||
// if we're playing and on the progress screen jump to the next screen
|
||||
if (!progress) return;
|
||||
@@ -260,15 +269,14 @@ const getDisplay = (index) => displays[index];
|
||||
|
||||
// resize the container on a page resize
|
||||
const resize = () => {
|
||||
const marginOffset = (document.fullscreenElement) ? 0 : 16;
|
||||
const widthZoomPercent = (window.innerWidth - marginOffset) / 640;
|
||||
const heightZoomPercent = (window.innerHeight - marginOffset) / 480;
|
||||
const widthZoomPercent = (document.querySelector('#divTwcBottom').getBoundingClientRect().width) / 640;
|
||||
const heightZoomPercent = (window.innerHeight) / 480;
|
||||
|
||||
const scale = Math.min(widthZoomPercent, heightZoomPercent);
|
||||
if (scale < 1.0 || document.fullscreenElement) {
|
||||
document.getElementById('container').style.zoom = scale;
|
||||
document.querySelector('#container').style.transform = `scale(${scale})`;
|
||||
} else {
|
||||
document.getElementById('container').style.zoom = 1;
|
||||
document.querySelector('#container').style.transform = 'unset';
|
||||
}
|
||||
};
|
||||
|
||||
@@ -286,7 +294,7 @@ const registerDisplay = (display) => {
|
||||
};
|
||||
|
||||
const generateCheckboxes = () => {
|
||||
const availableDisplays = document.getElementById('enabledDisplays');
|
||||
const availableDisplays = document.querySelector('#enabledDisplays');
|
||||
|
||||
if (!availableDisplays) return;
|
||||
// generate checkboxes
|
||||
@@ -303,11 +311,11 @@ const registerProgress = (_progress) => {
|
||||
};
|
||||
|
||||
const populateWeatherParameters = (params) => {
|
||||
document.getElementById('spanCity').innerHTML = `${params.city}, `;
|
||||
document.getElementById('spanState').innerHTML = params.state;
|
||||
document.getElementById('spanStationId').innerHTML = params.stationId;
|
||||
document.getElementById('spanRadarId').innerHTML = params.radarId;
|
||||
document.getElementById('spanZoneId').innerHTML = params.zoneId;
|
||||
document.querySelector('#spanCity').innerHTML = `${params.city}, `;
|
||||
document.querySelector('#spanState').innerHTML = params.state;
|
||||
document.querySelector('#spanStationId').innerHTML = params.stationId;
|
||||
document.querySelector('#spanRadarId').innerHTML = params.radarId;
|
||||
document.querySelector('#spanZoneId').innerHTML = params.zoneId;
|
||||
};
|
||||
|
||||
const autoRefreshChange = (e) => {
|
||||
@@ -319,17 +327,17 @@ const autoRefreshChange = (e) => {
|
||||
stopAutoRefreshTimer();
|
||||
}
|
||||
|
||||
localStorage.setItem('TwcAutoRefresh', checked);
|
||||
localStorage.setItem('autoRefresh', checked);
|
||||
};
|
||||
|
||||
const AssignLastUpdate = (date) => {
|
||||
if (date) {
|
||||
document.getElementById('spanLastRefresh').innerHTML = date.toLocaleString('en-US', {
|
||||
document.querySelector('#spanLastRefresh').innerHTML = date.toLocaleString('en-US', {
|
||||
weekday: 'short', month: 'short', day: 'numeric', year: 'numeric', hour: 'numeric', minute: 'numeric', second: 'numeric', timeZoneName: 'short',
|
||||
});
|
||||
if (document.getElementById('chkAutoRefresh').checked) startAutoRefreshTimer();
|
||||
if (document.querySelector(CHK_AUTO_REFRESH_SELECTOR).checked) startAutoRefreshTimer();
|
||||
} else {
|
||||
document.getElementById('spanLastRefresh').innerHTML = '(none)';
|
||||
document.querySelector('#spanLastRefresh').innerHTML = '(none)';
|
||||
}
|
||||
};
|
||||
|
||||
@@ -356,7 +364,7 @@ const startAutoRefreshTimer = () => {
|
||||
RemainingMs = 0;
|
||||
}
|
||||
const dt = new Date(RemainingMs);
|
||||
document.getElementById('spanRefreshCountDown').innerHTML = `${dt.getMinutes() < 10 ? `0${dt.getMinutes()}` : dt.getMinutes()}:${dt.getSeconds() < 10 ? `0${dt.getSeconds()}` : dt.getSeconds()}`;
|
||||
document.querySelector('#spanRefreshCountDown').innerHTML = `${dt.getMinutes().toString().padStart(2, '0')}:${dt.getSeconds().toString().padStart(2, '0')}`;
|
||||
|
||||
// Time has elapsed.
|
||||
if (AutoRefreshCountMs >= AUTO_REFRESH_TIME_MS && !isPlaying()) loadTwcData();
|
||||
@@ -367,14 +375,14 @@ const startAutoRefreshTimer = () => {
|
||||
const stopAutoRefreshTimer = () => {
|
||||
if (AutoRefreshIntervalId) {
|
||||
window.clearInterval(AutoRefreshIntervalId);
|
||||
document.getElementById('spanRefreshCountDown').innerHTML = '--:--';
|
||||
document.querySelector('#spanRefreshCountDown').innerHTML = '--:--';
|
||||
AutoRefreshIntervalId = null;
|
||||
}
|
||||
};
|
||||
|
||||
const refreshCheck = () => {
|
||||
// Time has elapsed.
|
||||
if (AutoRefreshCountMs >= AUTO_REFRESH_TIME_MS) {
|
||||
if (AutoRefreshCountMs >= AUTO_REFRESH_TIME_MS && isPlaying()) {
|
||||
loadTwcData();
|
||||
return true;
|
||||
}
|
||||
@@ -389,6 +397,8 @@ const registerRefreshData = (callback) => {
|
||||
loadTwcData.callback = callback;
|
||||
};
|
||||
|
||||
const timeZone = () => weatherParameters.timeZone;
|
||||
|
||||
export {
|
||||
updateStatus,
|
||||
displayNavMessage,
|
||||
@@ -404,4 +414,5 @@ export {
|
||||
latLonReceived,
|
||||
stopAutoRefreshTimer,
|
||||
registerRefreshData,
|
||||
timeZone,
|
||||
};
|
||||
|
||||
@@ -18,7 +18,7 @@ class Progress extends WeatherDisplay {
|
||||
|
||||
// setup event listener for dom-required initialization
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
this.version = document.getElementById('version').innerHTML;
|
||||
this.version = document.querySelector('#version').innerHTML;
|
||||
this.elem.querySelector('.container').addEventListener('click', this.lineClick.bind(this));
|
||||
});
|
||||
|
||||
@@ -26,6 +26,7 @@ class Progress extends WeatherDisplay {
|
||||
}
|
||||
|
||||
async drawCanvas(displays, loadedCount) {
|
||||
if (!this.elem) return;
|
||||
super.drawCanvas();
|
||||
|
||||
// get the progress bar cover (makes percentage)
|
||||
@@ -34,9 +35,10 @@ class Progress extends WeatherDisplay {
|
||||
// if no displays provided just draw the backgrounds (above)
|
||||
if (!displays) return;
|
||||
const lines = displays.map((display, index) => {
|
||||
const fill = {};
|
||||
|
||||
fill.name = display.name;
|
||||
if (display.showOnProgress === false) return false;
|
||||
const fill = {
|
||||
name: display.name,
|
||||
};
|
||||
|
||||
const statusClass = calcStatusClass(display.status);
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import { loadImg } from './utils/image.mjs';
|
||||
import { text } from './utils/fetch.mjs';
|
||||
import { rewriteUrl } from './utils/cors.mjs';
|
||||
import WeatherDisplay from './weatherdisplay.mjs';
|
||||
import { registerDisplay } from './navigation.mjs';
|
||||
import { registerDisplay, timeZone } from './navigation.mjs';
|
||||
import * as utils from './radar-utils.mjs';
|
||||
|
||||
class Radar extends WeatherDisplay {
|
||||
@@ -71,36 +71,36 @@ class Radar extends WeatherDisplay {
|
||||
const lists = (await Promise.all(baseUrls.map(async (url) => {
|
||||
try {
|
||||
// get a list of available radars
|
||||
const radarHtml = await text(url, { cors: true });
|
||||
return radarHtml;
|
||||
} catch (e) {
|
||||
return text(url, { cors: true });
|
||||
} catch (error) {
|
||||
console.log('Unable to get list of radars');
|
||||
console.error(e);
|
||||
console.error(error);
|
||||
this.setStatus(STATUS.failed);
|
||||
return false;
|
||||
}
|
||||
}))).filter((d) => d);
|
||||
|
||||
// convert to an array of gif urls
|
||||
const pngs = lists.map((html, htmlIdx) => {
|
||||
const pngs = lists.flatMap((html, htmlIdx) => {
|
||||
const parser = new DOMParser();
|
||||
const xmlDoc = parser.parseFromString(html, 'text/html');
|
||||
// add the base url
|
||||
const base = xmlDoc.createElement('base');
|
||||
base.href = baseUrls[htmlIdx];
|
||||
xmlDoc.head.append(base);
|
||||
const anchors = xmlDoc.getElementsByTagName('a');
|
||||
const anchors = xmlDoc.querySelectorAll('a');
|
||||
const urls = [];
|
||||
Array.from(anchors).forEach((elem) => {
|
||||
if (elem.innerHTML?.includes('.png') && elem.innerHTML?.includes('n0r_')) {
|
||||
if (elem.innerHTML?.match(/n0r_\d{12}\.png/)) {
|
||||
urls.push(elem.href);
|
||||
}
|
||||
});
|
||||
return urls;
|
||||
}).flat();
|
||||
});
|
||||
|
||||
// get the last few images
|
||||
const sortedPngs = pngs.sort((a, b) => (Date(a) < Date(b) ? -1 : 1));
|
||||
const timestampRegex = /_(\d{12})\.png/;
|
||||
const sortedPngs = pngs.sort((a, b) => (a.match(timestampRegex)[1] < b.match(timestampRegex)[1] ? -1 : 1));
|
||||
const urls = sortedPngs.slice(-(this.dopplerRadarImageMax));
|
||||
|
||||
// calculate offsets and sizes
|
||||
@@ -159,7 +159,7 @@ class Radar extends WeatherDisplay {
|
||||
zone: 'UTC',
|
||||
}).setZone();
|
||||
} else {
|
||||
time = DateTime.fromHTTP(response.headers.get('last-modified')).setZone();
|
||||
time = DateTime.fromHTTP(response.headers.get('last-modified')).setZone(timeZone());
|
||||
}
|
||||
|
||||
// assign to an html image element
|
||||
@@ -214,12 +214,16 @@ class Radar extends WeatherDisplay {
|
||||
const timePadded = time.length >= 8 ? time : ` ${time}`;
|
||||
this.elem.querySelector('.header .right .time').innerHTML = timePadded;
|
||||
|
||||
// get image offset calculation
|
||||
// is slides slightly because of scaling so we have to take a measurement from the rendered page
|
||||
const actualFrameHeight = this.elem.querySelector('.frame').getBoundingClientRect().height;
|
||||
|
||||
// scroll to image
|
||||
this.elem.querySelector('.scroll-area').style.top = `${-this.screenIndex * 371}px`;
|
||||
this.elem.querySelector('.scroll-area').style.top = `${-this.screenIndex * actualFrameHeight}px`;
|
||||
|
||||
this.finishDraw();
|
||||
}
|
||||
}
|
||||
|
||||
// register display
|
||||
registerDisplay(new Radar(9, 'radar'));
|
||||
registerDisplay(new Radar(10, 'radar'));
|
||||
|
||||
@@ -23,12 +23,14 @@ const getRegionalObservation = async (point, city) => {
|
||||
const observation = await json(`${station}/observations/latest`);
|
||||
// preload the image
|
||||
if (!observation.properties.icon) return false;
|
||||
preloadImg(getWeatherRegionalIconFromIconLink(observation.properties.icon, !observation.properties.daytime));
|
||||
const icon = getWeatherRegionalIconFromIconLink(observation.properties.icon, !observation.properties.daytime);
|
||||
if (!icon) return false;
|
||||
preloadImg(icon);
|
||||
// return the observation
|
||||
return observation.properties;
|
||||
} catch (e) {
|
||||
} catch (error) {
|
||||
console.log(`Unable to get regional observations for ${city.Name ?? city.city}`);
|
||||
console.error(e.status, e.responseJSON);
|
||||
console.error(error.status, error.responseJSON);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
@@ -193,7 +195,7 @@ const getXYForCityHI = (City, MaxLatitude, MinLongitude) => {
|
||||
};
|
||||
|
||||
// to fit on the map, remove anything after punctuation and then limit to 15 characters
|
||||
const formatCity = (city) => city.match(/[^-;/\\,]*/)[0].substr(0, 12);
|
||||
const formatCity = (city) => city.match(/[^,/;\\-]*/)[0].substr(0, 12);
|
||||
|
||||
export {
|
||||
buildForecast,
|
||||
|
||||
@@ -87,9 +87,12 @@ class RegionalForecast extends WeatherDisplay {
|
||||
|
||||
// wait for the regional observation if it's not done yet
|
||||
const observation = await observationPromise;
|
||||
|
||||
if (!observation) return false;
|
||||
|
||||
// format the observation the same as the forecast
|
||||
const regionalObservation = {
|
||||
daytime: !!observation.icon.match(/\/day\//),
|
||||
daytime: !!/\/day\//.test(observation.icon),
|
||||
temperature: celsiusToFahrenheit(observation.temperature.value),
|
||||
name: utils.formatCity(city.city),
|
||||
icon: observation.icon,
|
||||
@@ -110,9 +113,9 @@ class RegionalForecast extends WeatherDisplay {
|
||||
utils.buildForecast(forecast.properties.periods[1], city, cityXY),
|
||||
utils.buildForecast(forecast.properties.periods[2], city, cityXY),
|
||||
];
|
||||
} catch (e) {
|
||||
} catch (error) {
|
||||
console.log(`No regional forecast data for '${city.name ?? city.city}'`);
|
||||
console.log(e);
|
||||
console.log(error);
|
||||
return false;
|
||||
}
|
||||
}));
|
||||
@@ -156,19 +159,15 @@ class RegionalForecast extends WeatherDisplay {
|
||||
const dayName = forecastDate.toLocaleString({ weekday: 'long' });
|
||||
titleTop.innerHTML = 'Forecast for';
|
||||
// draw the title
|
||||
if (data[0][this.screenIndex].daytime) {
|
||||
titleBottom.innerHTML = dayName;
|
||||
} else {
|
||||
titleBottom.innerHTML = `${dayName} Night`;
|
||||
}
|
||||
titleBottom.innerHTML = data[0][this.screenIndex].daytime
|
||||
? dayName
|
||||
: `${dayName} Night`;
|
||||
}
|
||||
|
||||
// draw the map
|
||||
const scale = 640 / (offsetXY.x * 2);
|
||||
const map = this.elem.querySelector('.map');
|
||||
map.style.zoom = scale;
|
||||
map.style.top = `-${sourceXY.y}px`;
|
||||
map.style.left = `-${sourceXY.x}px`;
|
||||
map.style.transform = `scale(${scale}) translate(-${sourceXY.x}px, -${sourceXY.y}px)`;
|
||||
|
||||
const cities = data.map((city) => {
|
||||
const fill = {};
|
||||
@@ -179,9 +178,11 @@ class RegionalForecast extends WeatherDisplay {
|
||||
const { temperature } = period;
|
||||
fill.temp = temperature;
|
||||
|
||||
const { x, y } = period;
|
||||
|
||||
const elem = this.fillTemplate('location', fill);
|
||||
elem.style.left = `${period.x}px`;
|
||||
elem.style.top = `${period.y}px`;
|
||||
elem.style.left = `${x}px`;
|
||||
elem.style.top = `${y}px`;
|
||||
|
||||
return elem;
|
||||
});
|
||||
@@ -204,4 +205,4 @@ const getAndFormatPoint = async (lat, lon) => {
|
||||
};
|
||||
|
||||
// register display
|
||||
registerDisplay(new RegionalForecast(5, 'regional-forecast'));
|
||||
registerDisplay(new RegionalForecast(6, 'regional-forecast'));
|
||||
|
||||
@@ -45,9 +45,9 @@ class TravelForecast extends WeatherDisplay {
|
||||
name: city.Name,
|
||||
icon: getWeatherRegionalIconFromIconLink(forecast.properties.periods[todayShift].icon),
|
||||
};
|
||||
} catch (e) {
|
||||
} catch (error) {
|
||||
console.error(`GetTravelWeather for ${city.Name} failed`);
|
||||
console.error(e.status, e.responseJSON);
|
||||
console.error(error.status, error.responseJSON);
|
||||
return { name: city.Name, error: true };
|
||||
}
|
||||
});
|
||||
@@ -57,7 +57,7 @@ class TravelForecast extends WeatherDisplay {
|
||||
this.data = forecasts;
|
||||
|
||||
// test for some data available in at least one forecast
|
||||
const hasData = this.data.reduce((acc, forecast) => acc || forecast.high, false);
|
||||
const hasData = this.data.some((forecast) => forecast.high);
|
||||
if (!hasData) {
|
||||
this.setStatus(STATUS.noData);
|
||||
return;
|
||||
@@ -77,10 +77,9 @@ class TravelForecast extends WeatherDisplay {
|
||||
|
||||
const lines = cities.map((city) => {
|
||||
if (city.error) return false;
|
||||
const fillValues = {};
|
||||
|
||||
// city name
|
||||
fillValues.city = city;
|
||||
const fillValues = {
|
||||
city,
|
||||
};
|
||||
|
||||
// check for forecast data
|
||||
if (city.icon) {
|
||||
@@ -94,8 +93,9 @@ class TravelForecast extends WeatherDisplay {
|
||||
|
||||
fillValues.low = lowString;
|
||||
fillValues.high = highString;
|
||||
const { icon } = city;
|
||||
|
||||
fillValues.icon = { type: 'img', src: city.icon };
|
||||
fillValues.icon = { type: 'img', src: icon };
|
||||
} else {
|
||||
fillValues.error = 'NO TRAVEL DATA AVAILABLE';
|
||||
}
|
||||
@@ -158,4 +158,4 @@ const getTravelCitiesDayName = (cities) => cities.reduce((dayName, city) => {
|
||||
}, '');
|
||||
|
||||
// register display, not active by default
|
||||
registerDisplay(new TravelForecast(4, 'travel', false));
|
||||
registerDisplay(new TravelForecast(5, 'travel', false));
|
||||
|
||||
@@ -21,7 +21,7 @@ const fetchAsync = async (_url, responseType, _params = {}) => {
|
||||
if (params.cors === true) corsUrl = rewriteUrl(_url);
|
||||
const url = new URL(corsUrl, `${window.location.origin}/`);
|
||||
// match the security protocol when not on localhost
|
||||
url.protocol = window.location.hostname !== 'localhost' ? window.location.protocol : url.protocol;
|
||||
url.protocol = window.location.hostname === 'localhost' ? url.protocol : window.location.protocol;
|
||||
// add parameters if necessary
|
||||
if (params.data) {
|
||||
Object.keys(params.data).forEach((key) => {
|
||||
@@ -73,7 +73,7 @@ const doFetch = (url, params) => new Promise((resolve, reject) => {
|
||||
// out of retries
|
||||
return resolve(response);
|
||||
})
|
||||
.catch((e) => reject(e));
|
||||
.catch((error) => reject(error));
|
||||
});
|
||||
|
||||
const delay = (time, func, ...args) => new Promise((resolve) => {
|
||||
@@ -87,8 +87,8 @@ const retryDelay = (retryNumber) => {
|
||||
case 1: return 1000;
|
||||
case 2: return 2000;
|
||||
case 3: return 5000;
|
||||
case 4: return 10000;
|
||||
default: return 30000;
|
||||
case 4: return 10_000;
|
||||
default: return 30_000;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -2,11 +2,11 @@ const locationCleanup = (input) => {
|
||||
// regexes to run
|
||||
const regexes = [
|
||||
// "Chicago / West Chicago", removes before slash
|
||||
/^[A-Za-z ]+ \/ /,
|
||||
/^[ A-Za-z]+ \/ /,
|
||||
// "Chicago/Waukegan" removes before slash
|
||||
/^[A-Za-z ]+\//,
|
||||
/^[ A-Za-z]+\//,
|
||||
// "Chicago, Chicago O'hare" removes before comma
|
||||
/^[A-Za-z ]+, /,
|
||||
/^[ A-Za-z]+, /,
|
||||
];
|
||||
|
||||
// run all regexes
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
|
||||
const round2 = (value, decimals) => Math.trunc(value * 10 ** decimals) / 10 ** decimals;
|
||||
|
||||
const kphToMph = (Kph) => Math.round(Kph / 1.60934);
|
||||
const kphToMph = (Kph) => Math.round(Kph / 1.609_34);
|
||||
const celsiusToFahrenheit = (Celsius) => Math.round((Celsius * 9) / 5 + 32);
|
||||
const kilometersToMiles = (Kilometers) => Math.round(Kilometers / 1.60934);
|
||||
const kilometersToMiles = (Kilometers) => Math.round(Kilometers / 1.609_34);
|
||||
const metersToFeet = (Meters) => Math.round(Meters / 0.3048);
|
||||
const pascalToInHg = (Pascal) => round2(Pascal * 0.0002953, 2);
|
||||
const pascalToInHg = (Pascal) => round2(Pascal * 0.000_295_3, 2);
|
||||
|
||||
export {
|
||||
kphToMph,
|
||||
|
||||
@@ -3,9 +3,9 @@ import { json } from './fetch.mjs';
|
||||
const getPoint = async (lat, lon) => {
|
||||
try {
|
||||
return await json(`https://api.weather.gov/points/${lat},${lon}`);
|
||||
} catch (e) {
|
||||
} catch (error) {
|
||||
console.log(`Unable to get point ${lat}, ${lon}`);
|
||||
console.error(e);
|
||||
console.error(error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2,9 +2,8 @@
|
||||
|
||||
import STATUS, { calcStatusClass, statusClasses } from './status.mjs';
|
||||
import { DateTime } from '../vendor/auto/luxon.mjs';
|
||||
import { elemForEach } from './utils/elem.mjs';
|
||||
import {
|
||||
msg, displayNavMessage, isPlaying, updateStatus,
|
||||
msg, displayNavMessage, isPlaying, updateStatus, timeZone,
|
||||
} from './navigation.mjs';
|
||||
|
||||
class WeatherDisplay {
|
||||
@@ -12,7 +11,6 @@ class WeatherDisplay {
|
||||
// navId is used in messaging and sort order
|
||||
this.navId = navId;
|
||||
this.elemId = undefined;
|
||||
this.gifs = [];
|
||||
this.data = undefined;
|
||||
this.loadingStatus = STATUS.loading;
|
||||
this.name = name ?? elemId;
|
||||
@@ -21,6 +19,7 @@ class WeatherDisplay {
|
||||
this.defaultEnabled = defaultEnabled;
|
||||
this.okToDrawCurrentConditions = true;
|
||||
this.okToDrawCurrentDateTime = true;
|
||||
this.showOnProgress = true;
|
||||
|
||||
// default navigation timing
|
||||
this.timing = {
|
||||
@@ -34,7 +33,7 @@ class WeatherDisplay {
|
||||
// store elemId once
|
||||
this.storeElemId(elemId);
|
||||
|
||||
if (this.enabled) {
|
||||
if (this.isEnabled) {
|
||||
this.setStatus(STATUS.loading);
|
||||
} else {
|
||||
this.setStatus(STATUS.disabled);
|
||||
@@ -54,14 +53,10 @@ class WeatherDisplay {
|
||||
// get the saved status of the checkbox
|
||||
let savedStatus = window.localStorage.getItem(`display-enabled: ${this.elemId}`);
|
||||
if (savedStatus === null) savedStatus = defaultEnabled;
|
||||
if (savedStatus === 'true' || savedStatus === true) {
|
||||
this.enabled = true;
|
||||
} else {
|
||||
this.enabled = false;
|
||||
}
|
||||
this.isEnabled = !!((savedStatus === 'true' || savedStatus === true));
|
||||
|
||||
// refresh (or initially store the state of the checkbox)
|
||||
window.localStorage.setItem(`display-enabled: ${this.elemId}`, this.enabled);
|
||||
window.localStorage.setItem(`display-enabled: ${this.elemId}`, this.isEnabled);
|
||||
|
||||
// create a checkbox in the selected displays area
|
||||
const label = document.createElement('label');
|
||||
@@ -72,12 +67,15 @@ class WeatherDisplay {
|
||||
checkbox.value = true;
|
||||
checkbox.id = `${this.elemId}-checkbox`;
|
||||
checkbox.name = `${this.elemId}-checkbox`;
|
||||
checkbox.checked = this.enabled;
|
||||
checkbox.checked = this.isEnabled;
|
||||
checkbox.addEventListener('change', (e) => this.checkboxChange(e));
|
||||
const span = document.createElement('span');
|
||||
span.innerHTML = this.name;
|
||||
const alert = document.createElement('span');
|
||||
alert.innerHTML = '!!!';
|
||||
alert.classList.add('alert');
|
||||
|
||||
label.append(checkbox, span);
|
||||
label.append(checkbox, span, alert);
|
||||
|
||||
this.checkbox = label;
|
||||
|
||||
@@ -86,9 +84,9 @@ class WeatherDisplay {
|
||||
|
||||
checkboxChange(e) {
|
||||
// update the state
|
||||
this.enabled = e.target.checked;
|
||||
this.isEnabled = e.target.checked;
|
||||
// store the value for the next load
|
||||
window.localStorage.setItem(`display-enabled: ${this.elemId}`, this.enabled);
|
||||
window.localStorage.setItem(`display-enabled: ${this.elemId}`, this.isEnabled);
|
||||
// calling get data will update the status and actually get the data if we're set to enabled
|
||||
this.getData();
|
||||
}
|
||||
@@ -130,7 +128,7 @@ class WeatherDisplay {
|
||||
if (weatherParameters) this.weatherParameters = weatherParameters;
|
||||
|
||||
// set status
|
||||
if (this.enabled) {
|
||||
if (this.isEnabled) {
|
||||
this.setStatus(STATUS.loading);
|
||||
} else {
|
||||
this.setStatus(STATUS.disabled);
|
||||
@@ -168,22 +166,24 @@ class WeatherDisplay {
|
||||
|
||||
drawCurrentDateTime() {
|
||||
// only draw if canvas is active to conserve battery
|
||||
if (!this.isActive()) return;
|
||||
if (!this.active) return;
|
||||
// Get the current date and time.
|
||||
const now = DateTime.local();
|
||||
const now = DateTime.local().setZone(timeZone());
|
||||
|
||||
// time = "11:35:08 PM";
|
||||
const time = now.toLocaleString(DateTime.TIME_WITH_SECONDS).padStart(11, ' ');
|
||||
const date = now.toFormat(' ccc LLL ') + now.day.toString().padStart(2, ' ');
|
||||
|
||||
if (this.lastTime !== time) {
|
||||
elemForEach('.date-time.time', (elem) => { elem.innerHTML = time.toUpperCase(); });
|
||||
const dateElem = this.elem.querySelector('.date-time.date');
|
||||
const timeElem = this.elem.querySelector('.date-time.time');
|
||||
|
||||
if (timeElem && this.lastTime !== time) {
|
||||
timeElem.innerHTML = time.toUpperCase();
|
||||
}
|
||||
this.lastTime = time;
|
||||
|
||||
const date = now.toFormat(' ccc LLL ') + now.day.toString().padStart(2, ' ');
|
||||
|
||||
if (this.lastDate !== date) {
|
||||
elemForEach('.date-time.date', (elem) => { elem.innerHTML = date.toUpperCase(); });
|
||||
if (dateElem && this.lastDate !== date) {
|
||||
dateElem.innerHTML = date.toUpperCase();
|
||||
}
|
||||
this.lastDate = date;
|
||||
}
|
||||
@@ -205,12 +205,12 @@ class WeatherDisplay {
|
||||
this.elem.classList.remove('show');
|
||||
}
|
||||
|
||||
isActive() {
|
||||
get active() {
|
||||
return this.elem.offsetHeight !== 0;
|
||||
}
|
||||
|
||||
isEnabled() {
|
||||
return this.enabled;
|
||||
get enabled() {
|
||||
return this.isEnabled;
|
||||
}
|
||||
|
||||
// navigation timings
|
||||
@@ -223,7 +223,7 @@ class WeatherDisplay {
|
||||
// if the array forms are used totalScreens is overwritten by the size of the array
|
||||
navBaseTime() {
|
||||
// see if play is active and screen is active
|
||||
if (!isPlaying() || !this.isActive()) return;
|
||||
if (!isPlaying() || !this.active) return;
|
||||
// increment the base count
|
||||
this.navBaseCount += 1;
|
||||
|
||||
@@ -249,19 +249,15 @@ class WeatherDisplay {
|
||||
if (nextScreenIndex === this.screenIndex) return;
|
||||
|
||||
// test for -1 (no screen displayed yet)
|
||||
if (nextScreenIndex === -1) {
|
||||
this.screenIndex = 0;
|
||||
} else {
|
||||
this.screenIndex = nextScreenIndex;
|
||||
}
|
||||
this.screenIndex = nextScreenIndex === -1 ? 0 : nextScreenIndex;
|
||||
|
||||
// call the appropriate screen index change method
|
||||
if (!this.screenIndexChange) {
|
||||
await this.drawCanvas();
|
||||
this.showCanvas();
|
||||
} else {
|
||||
if (this.screenIndexChange) {
|
||||
this.screenIndexChange(this.screenIndex);
|
||||
} else {
|
||||
await this.drawCanvas();
|
||||
}
|
||||
this.showCanvas();
|
||||
}
|
||||
|
||||
// take the three timing formats shown above and break them into arrays for consistent usage in navigation functions
|
||||
@@ -341,6 +337,7 @@ class WeatherDisplay {
|
||||
screenIndexFromBaseCount() {
|
||||
// test for timing enabled
|
||||
if (!this.timing) return 0;
|
||||
if (this.timing.totalScreens === 0) return false;
|
||||
// find the first timing in the timing array that is greater than the base count
|
||||
if (this.timing && !this.timing.fullDelay) this.calcNavTiming();
|
||||
const timingIndex = this.timing.fullDelay.findIndex((delay) => delay > this.navBaseCount);
|
||||
@@ -372,7 +369,7 @@ class WeatherDisplay {
|
||||
|
||||
loadTemplates() {
|
||||
this.templates = {};
|
||||
this.elem = document.getElementById(`${this.elemId}-html`);
|
||||
this.elem = document.querySelector(`#${this.elemId}-html`);
|
||||
if (!this.elem) return;
|
||||
const templates = this.elem.querySelectorAll('.template');
|
||||
templates.forEach((template) => {
|
||||
@@ -412,7 +409,7 @@ class WeatherDisplay {
|
||||
|
||||
// still waiting for data (retries triggered)
|
||||
stillWaiting() {
|
||||
if (this.enabled) this.setStatus(STATUS.retrying);
|
||||
if (this.isEnabled) this.setStatus(STATUS.retrying);
|
||||
// handle still waiting callbacks
|
||||
this.stillWaitingCallbacks.forEach((callback) => callback());
|
||||
this.stillWaitingCallbacks = [];
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
27
server/styles/scss/_hazards.scss
Normal file
27
server/styles/scss/_hazards.scss
Normal file
@@ -0,0 +1,27 @@
|
||||
@use 'shared/_colors'as c;
|
||||
@use 'shared/_utils'as u;
|
||||
|
||||
.weather-display .main.hazards {
|
||||
&.main {
|
||||
overflow-y: hidden;
|
||||
|
||||
.hazard-lines {
|
||||
min-height: 400px;
|
||||
padding-top: 10px;
|
||||
|
||||
background-color: rgb(112, 35, 35);
|
||||
|
||||
.hazard {
|
||||
font-family: 'Star4000';
|
||||
font-size: 24pt;
|
||||
color: white;
|
||||
@include u.text-shadow(0px);
|
||||
position: relative;
|
||||
text-transform: uppercase;
|
||||
margin-top: 110px;
|
||||
margin-left: 80px;
|
||||
margin-right: 80px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,70 +20,82 @@ body {
|
||||
}
|
||||
}
|
||||
|
||||
input,
|
||||
button {
|
||||
font-family: "Star4000";
|
||||
}
|
||||
#divQuery {
|
||||
max-width: 640px;
|
||||
|
||||
#imgGetGps {
|
||||
height: 13px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.buttons {
|
||||
display: inline-block;
|
||||
width: 150px;
|
||||
text-align: right;
|
||||
|
||||
#txtAddress {
|
||||
width: 490px;
|
||||
font-size: 16pt;
|
||||
max-width: calc(100% - 8px);
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
background-color: #000000;
|
||||
color: white;
|
||||
border: 1px solid darkgray;
|
||||
}
|
||||
}
|
||||
|
||||
#btnGetGps,
|
||||
#btnGetLatLng,
|
||||
#btnClearQuery {
|
||||
font-size: 16pt;
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
background-color: #000000;
|
||||
color: white;
|
||||
}
|
||||
|
||||
border: 1px solid darkgray;
|
||||
}
|
||||
|
||||
#btnGetGps {
|
||||
img {
|
||||
|
||||
&.dark {
|
||||
display: none;
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
display: inline-block;
|
||||
}
|
||||
#imgGetGps {
|
||||
height: 13px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
&.light {
|
||||
button {
|
||||
font-size: 16pt;
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
display: none;
|
||||
background-color: #000000;
|
||||
color: white;
|
||||
}
|
||||
|
||||
border: 1px solid darkgray;
|
||||
}
|
||||
|
||||
#btnGetGps {
|
||||
img {
|
||||
|
||||
&.dark {
|
||||
display: none;
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
||||
&.light {
|
||||
@media (prefers-color-scheme: dark) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: black;
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
img {
|
||||
filter: invert(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: black;
|
||||
input,
|
||||
button {
|
||||
font-family: "Star4000";
|
||||
}
|
||||
|
||||
#txtAddress {
|
||||
width: calc(100% - 170px);
|
||||
max-width: 490px;
|
||||
font-size: 16pt;
|
||||
min-width: 200px;
|
||||
display: inline-block;
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
img {
|
||||
filter: invert(1);
|
||||
background-color: #000000;
|
||||
color: white;
|
||||
border: 1px solid darkgray;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
.autocomplete-suggestions {
|
||||
@@ -93,19 +105,19 @@ button {
|
||||
@media (prefers-color-scheme: dark) {
|
||||
background-color: #000000;
|
||||
}
|
||||
}
|
||||
|
||||
.autocomplete-suggestion {
|
||||
/*padding: 2px 5px;*/
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-size: 16pt;
|
||||
}
|
||||
.autocomplete-suggestion {
|
||||
/*padding: 2px 5px;*/
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-size: 16pt;
|
||||
}
|
||||
|
||||
.autocomplete-selected {
|
||||
background-color: #0000ff;
|
||||
color: #ffffff;
|
||||
.autocomplete-selected {
|
||||
background-color: #0000ff;
|
||||
color: #ffffff;
|
||||
}
|
||||
}
|
||||
|
||||
#divTwc {
|
||||
@@ -213,8 +225,7 @@ button {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
#imgPause1x,
|
||||
#imgPause2x {
|
||||
#imgPause1x {
|
||||
visibility: hidden;
|
||||
position: absolute;
|
||||
}
|
||||
@@ -270,15 +281,19 @@ button {
|
||||
|
||||
#container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
width: 640px;
|
||||
height: 480px;
|
||||
overflow: hidden;
|
||||
background-image: url(../images/BackGround1_1.png);
|
||||
transform-origin: 0 0;
|
||||
|
||||
}
|
||||
|
||||
#divTwc:fullscreen #container {
|
||||
background-image: none;
|
||||
width: unset;
|
||||
height: unset;
|
||||
transform-origin: unset;
|
||||
}
|
||||
|
||||
#loading {
|
||||
@@ -320,15 +335,48 @@ button {
|
||||
color: white;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
|
||||
.loading,
|
||||
.retrying {
|
||||
color: hsl(60, 100%, 30%);
|
||||
}
|
||||
|
||||
.press-here {
|
||||
color: black;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.failed {
|
||||
color: hsl(0, 100%, 30%);
|
||||
}
|
||||
|
||||
.no-data {
|
||||
color: hsl(0, 0%, 30%);
|
||||
}
|
||||
|
||||
.disabled {
|
||||
color: hsl(0, 0%, 30%);
|
||||
}
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
max-width: 300px;
|
||||
|
||||
.alert {
|
||||
display: none;
|
||||
|
||||
&.show {
|
||||
display: inline;
|
||||
color: red;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#divTwcBottom img {
|
||||
zoom: 150%;
|
||||
transform: scale(0.75);
|
||||
}
|
||||
|
||||
#divTwc:fullscreen {
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
|
||||
.map {
|
||||
position: absolute;
|
||||
transform-origin: 0 0;
|
||||
}
|
||||
|
||||
.location {
|
||||
|
||||
@@ -96,6 +96,10 @@
|
||||
width: 640px;
|
||||
height: 310px;
|
||||
overflow: hidden;
|
||||
|
||||
&.no-header {
|
||||
height: 400px;
|
||||
}
|
||||
}
|
||||
|
||||
&.has-box {
|
||||
|
||||
@@ -10,4 +10,5 @@
|
||||
@import 'progress';
|
||||
@import 'radar';
|
||||
@import 'regional-forecast';
|
||||
@import 'almanac';
|
||||
@import 'almanac';
|
||||
@import 'hazards';
|
||||
1
tests/README.md
Normal file
1
tests/README.md
Normal file
@@ -0,0 +1 @@
|
||||
Currently, tests take a different approach from typical unit testing. The test methodology loads several forecasts for different locations and logs them all to one logger so errors can be found such as missing icons, locations that do not have all of the necessary data or other changes that may occur between geographical locations.
|
||||
42
tests/index.js
Normal file
42
tests/index.js
Normal file
@@ -0,0 +1,42 @@
|
||||
const puppeteer = require('puppeteer');
|
||||
const { setTimeout } = require('node:timers/promises');
|
||||
const { readFile } = require('fs/promises');
|
||||
const messageFormatter = require('./messageformatter');
|
||||
|
||||
(async () => {
|
||||
const browser = await puppeteer.launch({
|
||||
// headless: false,
|
||||
slowMo: 10,
|
||||
timeout: 10_000,
|
||||
dumpio: true,
|
||||
});
|
||||
|
||||
// get the list of locations
|
||||
const LOCATIONS = JSON.parse(await readFile('./tests/locations.json'));
|
||||
|
||||
// get the page
|
||||
const page = (await browser.pages())[0];
|
||||
await page.goto('http://localhost:8080');
|
||||
|
||||
page.on('console', messageFormatter);
|
||||
|
||||
// run all the locations
|
||||
for (let i = 0; i < LOCATIONS.length; i += 1) {
|
||||
const location = LOCATIONS[i];
|
||||
console.log(location);
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await tester(location, page);
|
||||
}
|
||||
|
||||
browser.close();
|
||||
})();
|
||||
|
||||
const tester = async (location, page) => {
|
||||
// Set the address
|
||||
await page.type('#txtAddress', location);
|
||||
await setTimeout(500);
|
||||
// get the page
|
||||
await page.click('#btnGetLatLng');
|
||||
// wait for errors
|
||||
await setTimeout(5000);
|
||||
};
|
||||
52
tests/locations.json
Normal file
52
tests/locations.json
Normal file
@@ -0,0 +1,52 @@
|
||||
[
|
||||
"New York, New York",
|
||||
"Los Angeles, California",
|
||||
"Chicago, Illinois",
|
||||
"Houston, Texas",
|
||||
"Phoenix, Arizona",
|
||||
"Philadelphia, Pennsylvania",
|
||||
"San Antonio, Texas",
|
||||
"San Diego, California",
|
||||
"Dallas, Texas",
|
||||
"San Jose, California",
|
||||
"Austin, Texas",
|
||||
"Jacksonville, Florida",
|
||||
"Fort Worth, Texas",
|
||||
"Columbus, Ohio",
|
||||
"Charlotte, North Carolina",
|
||||
"Indianapolis, Indiana",
|
||||
"San Francisco, California",
|
||||
"Seattle, Washington",
|
||||
"Denver, Colorado",
|
||||
"Nashville, Tennessee",
|
||||
"Washington, District of Columbia",
|
||||
"Oklahoma City, Oklahoma",
|
||||
"Boston, Massachusetts",
|
||||
"El Paso, Texas",
|
||||
"Portland, Oregon",
|
||||
"Las Vegas, Nevada",
|
||||
"Memphis, Tennessee",
|
||||
"Detroit, Michigan",
|
||||
"Baltimore, Maryland",
|
||||
"Milwaukee, Wisconsin",
|
||||
"Albuquerque, New Mexico",
|
||||
"Fresno, California",
|
||||
"Tucson, Arizona",
|
||||
"Sacramento, California",
|
||||
"Mesa, Arizona",
|
||||
"Kansas City, Missouri",
|
||||
"Atlanta, Georgia",
|
||||
"Omaha, Nebraska",
|
||||
"Colorado Springs, Colorado",
|
||||
"Raleigh, North Carolina",
|
||||
"Long Beach, California",
|
||||
"Virginia Beach, Virginia",
|
||||
"Oakland, California",
|
||||
"Miami, Florida",
|
||||
"Minneapolis, Minnesota",
|
||||
"Bakersfield, California",
|
||||
"Tulsa, Oklahoma",
|
||||
"Aurora, Colorado",
|
||||
"Arlington, Texas",
|
||||
"Wichita, Kansas"
|
||||
]
|
||||
28
tests/messageformatter.js
Normal file
28
tests/messageformatter.js
Normal file
@@ -0,0 +1,28 @@
|
||||
const chalk = require('chalk');
|
||||
|
||||
const describe = (jsHandle) => jsHandle.executionContext().evaluate(
|
||||
// serialize |obj| however you want
|
||||
(obj) => `OBJ: ${typeof obj}, ${obj}`,
|
||||
jsHandle,
|
||||
);
|
||||
|
||||
const colors = {
|
||||
LOG: chalk.grey,
|
||||
ERR: chalk.red,
|
||||
WAR: chalk.yellow,
|
||||
INF: chalk.cyan,
|
||||
};
|
||||
|
||||
const formatter = async (message) => {
|
||||
const args = await Promise.all(message.args().map((arg) => describe(arg)));
|
||||
// make ability to paint different console[types]
|
||||
const type = message.type().substr(0, 3).toUpperCase();
|
||||
const color = colors[type] || chalk.blue;
|
||||
let text = '';
|
||||
for (let i = 0; i < args.length; i += 1) {
|
||||
text += `[${i}] ${args[i]} `;
|
||||
}
|
||||
console.log(color(`CONSOLE.${type}: ${message.text()}\n${text} `));
|
||||
};
|
||||
|
||||
module.exports = formatter;
|
||||
1436
tests/package-lock.json
generated
Normal file
1436
tests/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
15
tests/package.json
Normal file
15
tests/package.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "ws4kp-tests",
|
||||
"version": "1.0.0",
|
||||
"description": "Currently, tests take a different approach from typical unit testing. The test methodology loads several forecasts for different locations and logs them all to one logger so errors can be found such as missing icons, locations that do not have all of the necessary data or other changes that may occur between geographical locations.",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"chalk": "^4.0.0",
|
||||
"puppeteer": "^19.5.2"
|
||||
}
|
||||
}
|
||||
@@ -28,6 +28,7 @@
|
||||
<script type="text/javascript" src="scripts/vendor/auto/nosleep.js"></script>
|
||||
<script type="text/javascript" src="scripts/vendor/auto/swiped-events.js"></script>
|
||||
<script type="text/javascript" src="scripts/vendor/auto/suncalc.js"></script>
|
||||
<script type="module" src="scripts/modules/hazards.mjs"></script>
|
||||
<script type="module" src="scripts/modules/currentweatherscroll.mjs"></script>
|
||||
<script type="module" src="scripts/modules/currentweather.mjs"></script>
|
||||
<script type="module" src="scripts/modules/almanac.mjs"></script>
|
||||
@@ -41,6 +42,7 @@
|
||||
<script type="module" src="scripts/modules/regionalforecast.mjs"></script>
|
||||
<script type="module" src="scripts/modules/travelforecast.mjs"></script>
|
||||
<script type="module" src="scripts/modules/progress.mjs"></script>
|
||||
<script type="module" src="scripts/modules/radar.mjs"></script>
|
||||
<script type="module" src="scripts/index.mjs"></script>
|
||||
|
||||
<!-- data -->
|
||||
@@ -58,19 +60,15 @@
|
||||
|
||||
|
||||
<div id="divQuery">
|
||||
<form id="frmGetLatLng">
|
||||
<input id="txtAddress" type="text" value="" placeholder="Zip or City, State" /><button id="btnGetGps" type="button" title="Get GPS Location"><img src="images/nav/ic_gps_fixed_black_18dp_1x.png" class="light"/><img src="images/nav/ic_gps_fixed_white_18dp_1x.png" class="dark"/></button>
|
||||
<input id="btnGetLatLng" type="submit" value="GO" />
|
||||
<input id="btnClearQuery" type="reset" value="Reset" />
|
||||
</form>
|
||||
<div id="divLat"></div>
|
||||
<div id="divLng"></div>
|
||||
<input id="txtAddress" type="text" value="" placeholder="Zip or City, State" />
|
||||
<div class="buttons">
|
||||
<button id="btnGetGps" type="button" title="Get GPS Location"><img src="images/nav/ic_gps_fixed_black_18dp_1x.png" class="light"/>
|
||||
<img src="images/nav/ic_gps_fixed_white_18dp_1x.png" class="dark"/>
|
||||
</button>
|
||||
<button id="btnGetLatLng" type="submit">GO</button>
|
||||
<button id="btnClearQuery" type="reset">Reset</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br />
|
||||
|
||||
<img id="imgPause1x" src="images/nav/ic_pause_white_24dp_1x.png" />
|
||||
<img id="imgPause2x" src="images/nav/ic_pause_white_24dp_2x.png" />
|
||||
<div id="version" style="display:none">
|
||||
<%- version %>
|
||||
</div>
|
||||
@@ -117,19 +115,22 @@
|
||||
<div id="radar-html" class="weather-display">
|
||||
<%- include('partials/radar.ejs') %>
|
||||
</div>
|
||||
<div id="hazards-html" class="weather-display show">
|
||||
<%- include('partials/hazards.ejs') %>
|
||||
</div>
|
||||
</div>
|
||||
<div id="divTwcBottom">
|
||||
<div id="divTwcBottomLeft">
|
||||
<img id="NavigateMenu" class="navButton" src="images/nav/ic_menu_white_24dp_1x.png" title="Menu" />
|
||||
<img id="NavigatePrevious" class="navButton" src="images/nav/ic_skip_previous_white_24dp_1x.png" title="Previous" />
|
||||
<img id="NavigateNext" class="navButton" src="images/nav/ic_skip_next_white_24dp_1x.png" title="Next" />
|
||||
<img id="NavigatePlay" class="navButton" src="images/nav/ic_play_arrow_white_24dp_1x.png" title="Play" />
|
||||
<img id="NavigateMenu" class="navButton" src="images/nav/ic_menu_white_24dp_2x.png" title="Menu" />
|
||||
<img id="NavigatePrevious" class="navButton" src="images/nav/ic_skip_previous_white_24dp_2x.png" title="Previous" />
|
||||
<img id="NavigateNext" class="navButton" src="images/nav/ic_skip_next_white_24dp_2x.png" title="Next" />
|
||||
<img id="NavigatePlay" class="navButton" src="images/nav/ic_play_arrow_white_24dp_2x.png" title="Play" />
|
||||
</div>
|
||||
<div id="divTwcBottomMiddle">
|
||||
<img id="NavigateRefresh" class="navButton" src="images/nav/ic_refresh_white_24dp_1x.png" title="Refresh" />
|
||||
<img id="NavigateRefresh" class="navButton" src="images/nav/ic_refresh_white_24dp_2x.png" title="Refresh" />
|
||||
</div>
|
||||
<div id="divTwcBottomRight">
|
||||
<img id="ToggleFullScreen" class="navButton" src="images/nav/ic_fullscreen_white_24dp_1x.png" title="Enter Fullscreen" />
|
||||
<img id="ToggleFullScreen" class="navButton" src="images/nav/ic_fullscreen_white_24dp_2x.png" title="Enter Fullscreen" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<%- include('header.ejs', {titleDual:{ top: 'Current' , bottom: 'Conditions' }, noaaLogo: true}) %>
|
||||
<%- include('header.ejs', {titleDual:{ top: 'Current' , bottom: 'Conditions' }, noaaLogo: true, hasTime: true}) %>
|
||||
<div class="main has-scroll has-box current-weather">
|
||||
<div class="weather template">
|
||||
<div class="left col">
|
||||
|
||||
8
views/partials/hazards.ejs
Normal file
8
views/partials/hazards.ejs
Normal file
@@ -0,0 +1,8 @@
|
||||
<div class="main has-scroll hazards no-header">
|
||||
<div class="hazard-lines">
|
||||
<div class="hazard template">
|
||||
<div class="hazard-text"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<%- include('scroll.ejs') %>
|
||||
@@ -1,4 +1,4 @@
|
||||
<%- include('header.ejs', {titleDual:{ top: 'Latest' , bottom: 'Observations' }, noaaLogo: true }) %>
|
||||
<%- include('header.ejs', {titleDual:{ top: 'Latest' , bottom: 'Observations' }, noaaLogo: true, hasTime: true }) %>
|
||||
<div class="main has-scroll latest-observations has-box">
|
||||
<div class="container">
|
||||
<div class="column-headers">
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
"Pngs",
|
||||
"PRECIP",
|
||||
"rtrim",
|
||||
"sonarjs",
|
||||
"T",
|
||||
"T'storm",
|
||||
"uscomp",
|
||||
|
||||
Reference in New Issue
Block a user