Compare commits

...

14 Commits

Author SHA1 Message Date
Matt Walsh
3166dfad16 5.9.7 2023-01-10 14:12:32 -06:00
Matt Walsh
b6cceb5acf radar scroll height 2023-01-10 14:12:22 -06:00
Matt Walsh
1303c55851 freezing rain icon 2023-01-10 14:07:48 -06:00
Matt Walsh
339d391110 code cleanup 2023-01-06 16:26:19 -06:00
Matt Walsh
b6e57e8a19 reduse use of .reduce 2023-01-06 16:18:33 -06:00
Matt Walsh
3743c45de6 additional eslint rules 2023-01-06 14:39:39 -06:00
Matt Walsh
b890b4e53d update dependencies 2023-01-05 14:48:21 -06:00
Matt Walsh
b07478f7ff capture dist 2023-01-05 14:26:58 -06:00
Matt Walsh
8a25881d5b 5.9.6 2023-01-05 14:19:43 -06:00
Matt Walsh
0743b9e2bb filter current conditions for missing icon or current conditions 2023-01-05 14:19:33 -06:00
Matt Walsh
784c074e32 capture dist 2022-12-22 14:48:05 -06:00
Matt Walsh
4840909098 5.9.5 2022-12-22 14:47:08 -06:00
Matt Walsh
03dfbc462b better latest observation get data routine 2022-12-22 14:46:58 -06:00
Matt Walsh
25291efff5 capture dist 2022-12-21 16:20:54 -06:00
31 changed files with 1412 additions and 357 deletions

View File

@@ -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',

14
.vscode/tasks.json vendored Normal file
View File

@@ -0,0 +1,14 @@
{
"version": "2.0.0",
"tasks": [
{
"type": "npm",
"script": "lint",
"problemMatcher": [
"$eslint-stylish"
],
"label": "npm: lint",
"detail": "eslint ./server/scripts/**"
}
]
}

View File

@@ -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);
});
};
};

View File

@@ -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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

1254
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "ws4kp",
"version": "5.9.4",
"version": "5.9.7",
"description": "Welcome to the WeatherStar 4000+ project page!",
"main": "index.js",
"scripts": {
@@ -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.17.0",
"eslint-plugin-unicorn": "^45.0.2",
"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"
}
}

View File

@@ -0,0 +1,6 @@
module.exports = {
rules: {
// unicorn
'unicorn/numeric-separators-style': 0,
},
};

View File

@@ -22,35 +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.getElementById('txtAddress').addEventListener('keydown', (key) => { if (key.code === 'Enter') formSubmit(); });
document.getElementById('btnGetLatLng').addEventListener('click', () => formSubmit());
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(); });
$('#txtAddress').devbridgeAutocomplete({
$(TXT_ADDRESS_SELECTOR).devbridgeAutocomplete({
serviceUrl: 'https://geocode.arcgis.com/arcgis/rest/services/World/GeocodeServer/suggest',
deferRequestBy: 300,
paramName: 'text',
@@ -75,7 +78,7 @@ const init = () => {
});
const formSubmit = () => {
const ac = $('#txtAddress').devbridgeAutocomplete();
const ac = $(TXT_ADDRESS_SELECTOR).devbridgeAutocomplete();
if (ac.suggestions[0]) $(ac.suggestionsContainer.children[0]).trigger('click');
return false;
};
@@ -85,7 +88,7 @@ const init = () => {
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));
}
@@ -96,14 +99,14 @@ const init = () => {
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');
@@ -112,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) => {
@@ -135,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.');
@@ -145,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
@@ -153,10 +156,10 @@ const doRedirectToGeometry = (geom, haveDataCallback) => {
};
const btnFullScreenClick = () => {
if (!document.fullscreenElement) {
enterFullScreen();
} else {
if (document.fullscreenElement) {
exitFullscreen();
} else {
enterFullScreen();
}
if (isPlaying()) {
@@ -171,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
@@ -189,7 +192,7 @@ const enterFullScreen = () => {
updateFullScreenNavigate();
// change hover text and image
const img = document.getElementById('ToggleFullScreen');
const img = document.querySelector(TOGGLE_FULL_SCREEN_SELECTOR);
img.src = 'images/nav/ic_fullscreen_exit_white_24dp_2x.png';
img.title = 'Exit fullscreen';
};
@@ -213,7 +216,7 @@ const exitFullscreen = () => {
}
resize();
// change hover text and image
const img = document.getElementById('ToggleFullScreen');
const img = document.querySelector(TOGGLE_FULL_SCREEN_SELECTOR);
img.src = 'images/nav/ic_fullscreen_white_24dp_2x.png';
img.title = 'Enter fullscreen';
};
@@ -232,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);
};
@@ -276,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);
@@ -286,8 +290,8 @@ 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);
};
@@ -355,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')) {
@@ -371,7 +375,7 @@ 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) => {

View File

@@ -53,12 +53,14 @@ 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

View File

@@ -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

View File

@@ -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,7 +52,10 @@ class ExtendedForecast extends WeatherDisplay {
// create each day template
const days = forecast.map((Day) => {
const fill = {};
const fill = {
icon: { type: 'img', src: Day.icon },
condition: Day.text,
};
fill.date = Day.dayName;
const { low } = Day;
@@ -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') {

View File

@@ -40,9 +40,9 @@ class Hazards extends WeatherDisplay {
// show alert indicator
if (this.data.length > 0) alert.classList.add('show');
} catch (e) {
} catch (error) {
console.error('Get hourly forecast failed');
console.error(e.status, e.responseJSON);
console.error(error.status, error.responseJSON);
if (this.isEnabled) this.setStatus(STATUS.failed);
// return undefined to other subscribers
this.getDataCallback(undefined);

View File

@@ -34,9 +34,9 @@ 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);
console.error(error.status, error.responseJSON);
if (this.isEnabled) this.setStatus(STATUS.failed);
// return undefined to other subscribers
this.getDataCallback(undefined);
@@ -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

View File

@@ -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':
@@ -155,7 +159,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];

View File

@@ -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(2, 'latest-observations'));

View File

@@ -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;
}

View File

@@ -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;
@@ -28,25 +29,19 @@ const init = async () => {
// auto refresh
const autoRefresh = localStorage.getItem('autoRefresh');
if (!autoRefresh || autoRefresh === 'true') {
document.getElementById('chkAutoRefresh').checked = 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,6 +56,7 @@ 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;
@@ -76,7 +72,7 @@ const getWeather = async (latLon, haveDataCallback) => {
weatherParameters.stationId = StationId;
weatherParameters.weatherOffice = point.properties.cwa;
weatherParameters.city = city;
weatherParameters.state = point.properties.relativeLocation.properties.state;
weatherParameters.state = state;
weatherParameters.timeZone = point.properties.timeZone;
weatherParameters.forecast = point.properties.forecast;
weatherParameters.forecastGridData = point.properties.forecastGridData;
@@ -87,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();
@@ -200,9 +196,7 @@ const loadDisplay = (direction) => {
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();
@@ -212,15 +206,12 @@ const loadDisplay = (direction) => {
};
// get the current display index or value
const currentDisplayIndex = () => {
const index = displays.findIndex((display) => display.active);
return index;
};
const currentDisplayIndex = () => displays.findIndex((display) => display.active);
const currentDisplay = () => displays[currentDisplayIndex()];
const setPlaying = (newValue) => {
playing = newValue;
const playButton = document.getElementById('NavigatePlay');
const playButton = document.querySelector('#NavigatePlay');
localStorage.setItem('play', playing);
if (playing) {
@@ -272,14 +263,14 @@ const getDisplay = (index) => displays[index];
// resize the container on a page resize
const resize = () => {
const widthZoomPercent = (document.getElementById('divTwcBottom').getBoundingClientRect().width) / 640;
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.zoom = scale;
} else {
document.getElementById('container').style.zoom = 1;
document.querySelector('#container').style.zoom = 1;
}
};
@@ -297,7 +288,7 @@ const registerDisplay = (display) => {
};
const generateCheckboxes = () => {
const availableDisplays = document.getElementById('enabledDisplays');
const availableDisplays = document.querySelector('#enabledDisplays');
if (!availableDisplays) return;
// generate checkboxes
@@ -314,11 +305,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) => {
@@ -335,12 +326,12 @@ const autoRefreshChange = (e) => {
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)';
}
};
@@ -367,7 +358,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();
@@ -378,7 +369,7 @@ const startAutoRefreshTimer = () => {
const stopAutoRefreshTimer = () => {
if (AutoRefreshIntervalId) {
window.clearInterval(AutoRefreshIntervalId);
document.getElementById('spanRefreshCountDown').innerHTML = '--:--';
document.querySelector('#spanRefreshCountDown').innerHTML = '--:--';
AutoRefreshIntervalId = null;
}
};

View File

@@ -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));
});
@@ -36,9 +36,9 @@ class Progress extends WeatherDisplay {
if (!displays) return;
const lines = displays.map((display, index) => {
if (display.showOnProgress === false) return false;
const fill = {};
fill.name = display.name;
const fill = {
name: display.name,
};
const statusClass = calcStatusClass(display.status);

View File

@@ -71,25 +71,24 @@ 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_')) {
@@ -97,7 +96,7 @@ class Radar extends WeatherDisplay {
}
});
return urls;
}).flat();
});
// get the last few images
const sortedPngs = pngs.sort((a, b) => (Date(a) < Date(b) ? -1 : 1));
@@ -214,8 +213,12 @@ class Radar extends WeatherDisplay {
const timePadded = time.length >= 8 ? time : `&nbsp;${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();
}

View File

@@ -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,

View File

@@ -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,11 +159,9 @@ 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
@@ -179,9 +180,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;
});

View File

@@ -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';
}

View File

@@ -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;
}
};

View File

@@ -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

View File

@@ -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,

View File

@@ -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;
}
};

View File

@@ -53,11 +53,7 @@ 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.isEnabled = true;
} else {
this.isEnabled = false;
}
this.isEnabled = !!((savedStatus === 'true' || savedStatus === true));
// refresh (or initially store the state of the checkbox)
window.localStorage.setItem(`display-enabled: ${this.elemId}`, this.isEnabled);
@@ -253,17 +249,13 @@ 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();
} else {
if (this.screenIndexChange) {
this.screenIndexChange(this.screenIndex);
} else {
await this.drawCanvas();
}
this.showCanvas();
}
@@ -377,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) => {

View File

@@ -27,6 +27,7 @@
"Pngs",
"PRECIP",
"rtrim",
"sonarjs",
"T",
"T'storm",
"uscomp",