Compare commits

...

19 Commits

Author SHA1 Message Date
Matt Walsh
703d64f6b2 5.5.3 2022-12-12 14:47:59 -06:00
Matt Walsh
e5a18ea073 index.ejs cleanup 2022-12-12 14:47:53 -06:00
Matt Walsh
e326c3464b 5.5.2 2022-12-12 14:13:48 -06:00
Matt Walsh
c3e38b4077 remove geoquery when there's a saved location 2022-12-12 14:13:39 -06:00
Matt Walsh
eb11feb964 5.5.1 2022-12-12 14:01:24 -06:00
Matt Walsh
b2aca1ee8d fix auto-start when first display is disabled 2022-12-12 14:01:10 -06:00
Matt Walsh
5b257ace55 5.5.0 2022-12-12 13:53:48 -06:00
Matt Walsh
5dd8f4bd62 auto-retry for some forecast data 2022-12-12 13:53:33 -06:00
Matt Walsh
76fd93e6e1 distribution 2022-12-12 10:53:12 -06:00
Matt Walsh
2368a30980 5.4.3 2022-12-12 10:52:31 -06:00
Matt Walsh
8efff1a057 fix alamanc graphics 2022-12-12 10:48:54 -06:00
Matt Walsh
ae0d0ef9ec distribution 2022-12-09 14:15:16 -06:00
Matt Walsh
f633631532 class static code cleanup 2022-12-09 13:51:51 -06:00
Matt Walsh
3f5cd4ca70 class static code cleanup 2022-12-09 13:50:17 -06:00
Matt Walsh
0c8db4f38e full screen scaling 2022-12-09 13:26:13 -06:00
Matt Walsh
dc4db67b96 capture dist 2022-12-09 13:12:53 -06:00
Matt Walsh
52487319fa 5.4.1 2022-12-09 13:12:10 -06:00
Matt Walsh
6a1e2da11e mobile scaling and rotation 2022-12-09 13:11:53 -06:00
Matt Walsh
1cf9f41ca0 gulp upload_images 2022-12-09 11:58:47 -06:00
33 changed files with 839 additions and 772 deletions

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

View File

@@ -142,6 +142,18 @@ gulp.task('upload', () => gulp.src(uploadSources, { base: './dist' })
},
})));
const imageSources = [
'server/fonts/**',
'server/images/**',
];
gulp.task('upload_images', () => gulp.src(imageSources, { base: './server' })
.pipe(
s3({
Bucket: 'weatherstar',
StorageClass: 'STANDARD',
}),
));
gulp.task('invalidate', async () => cloudfront.createInvalidation({
DistributionId: 'E9171A4KV8KCW',
InvalidationBatch: {
@@ -153,4 +165,4 @@ 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'), 'upload', 'invalidate');
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');

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "ws4kp",
"version": "5.4.0",
"version": "5.5.3",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "ws4kp",
"version": "5.4.0",
"version": "5.5.3",
"license": "MIT",
"dependencies": {
"eslint": "^8.21.0",

View File

@@ -1,6 +1,6 @@
{
"name": "ws4kp",
"version": "5.4.0",
"version": "5.5.3",
"description": "Welcome to the WeatherStar 4000+ project page!",
"main": "index.js",
"scripts": {
@@ -40,10 +40,9 @@
"suncalc": "^1.8.0",
"swiped-events": "^1.1.4",
"terser-webpack-plugin": "^5.3.6",
"webpack-stream": "^7.0.0"
},
"dependencies": {
"webpack-stream": "^7.0.0",
"eslint": "^8.21.0",
"eslint-plugin-import": "^2.26.0"
}
},
"dependencies": {}
}

View File

@@ -3,15 +3,13 @@ import noSleep from './modules/utils/nosleep.mjs';
import {
message as navMessage, isPlaying, resize, resetStatuses, latLonReceived, stopAutoRefreshTimer, registerRefreshData,
} from './modules/navigation.mjs';
import { round2 } from './modules/utils/units.mjs';
document.addEventListener('DOMContentLoaded', () => {
init();
});
const overrides = {};
let AutoSelectQuery = false;
let FullScreenOverride = false;
let fullScreenOverride = false;
const categories = [
'Land Features',
@@ -30,7 +28,7 @@ const init = () => {
e.target.select();
});
registerRefreshData(LoadTwcData);
registerRefreshData(loadData);
document.getElementById('NavigateMenu').addEventListener('click', btnNavigateMenuClick);
document.getElementById('NavigateRefresh').addEventListener('click', btnNavigateRefreshClick);
@@ -41,11 +39,11 @@ const init = () => {
document.getElementById('btnGetGps').addEventListener('click', btnGetGpsClick);
document.getElementById('divTwc').addEventListener('click', () => {
if (document.fullscreenElement) UpdateFullScreenNavigate();
if (document.fullscreenElement) updateFullScreenNavigate();
});
document.addEventListener('keydown', documentKeydown);
document.addEventListener('touchmove', (e) => { if (FullScreenOverride) e.preventDefault(); });
document.addEventListener('touchmove', (e) => { if (fullScreenOverride) e.preventDefault(); });
$('#frmGetLatLng #txtAddress').devbridgeAutocomplete({
serviceUrl: 'https://geocode.arcgis.com/arcgis/rest/services/World/GeocodeServer/suggest',
@@ -58,21 +56,12 @@ const init = () => {
maxSuggestions: 10,
},
dataType: 'json',
transformResult: (response) => {
if (AutoSelectQuery) {
AutoSelectQuery = false;
window.setTimeout(() => {
$(ac.suggestionsContainer.children[0]).click();
}, 1);
}
return {
suggestions: $.map(response.suggestions, (i) => ({
value: i.text,
data: i.magicKey,
})),
};
},
transformResult: (response) => ({
suggestions: response.suggestions.map((i) => ({
value: i.text,
data: i.magicKey,
})),
}),
minChars: 3,
showNoSuggestionNotice: true,
noSuggestionNotice: 'No results found. Please try a different search string.',
@@ -80,24 +69,23 @@ const init = () => {
width: 490,
});
const ac = $('#frmGetLatLng #txtAddress').devbridgeAutocomplete();
$('#frmGetLatLng').on('submit', () => {
if (ac.suggestions[0]) $(ac.suggestionsContainer.children[0]).click();
const ac = $('#frmGetLatLng #txtAddress').devbridgeAutocomplete();
if (ac.suggestions[0]) $(ac.suggestionsContainer.children[0]).trigger('click');
return false;
});
// Auto load the previous query
const TwcQuery = localStorage.getItem('TwcQuery');
if (TwcQuery) {
AutoSelectQuery = true;
const query = localStorage.getItem('latLonQuery');
const latLon = localStorage.getItem('latLonLon');
if (query && latLon) {
const txtAddress = document.getElementById('txtAddress');
txtAddress.value = TwcQuery;
txtAddress.blur();
txtAddress.focus();
txtAddress.value = query;
loadData(JSON.parse(latLon));
}
const TwcPlay = localStorage.getItem('TwcPlay');
if (TwcPlay === null || TwcPlay === 'true') postMessage('navButton', 'play');
const twcPlay = localStorage.getItem('play');
if (twcPlay === null || twcPlay === 'true') postMessage('navButton', 'play');
document.getElementById('btnClearQuery').addEventListener('click', () => {
document.getElementById('spanCity').innerHTML = '';
@@ -106,18 +94,14 @@ const init = () => {
document.getElementById('spanRadarId').innerHTML = '';
document.getElementById('spanZoneId').innerHTML = '';
localStorage.removeItem('TwcScrollText');
localStorage.removeItem('TwcScrollTextChecked');
document.getElementById('chkAutoRefresh').checked = true;
localStorage.removeItem('TwcAutoRefresh');
localStorage.removeItem('autoRefresh');
document.getElementById('radEnglish').checked = true;
localStorage.removeItem('TwcPlay');
localStorage.removeItem('play');
postMessage('navButton', 'play');
localStorage.removeItem('TwcQuery');
localStorage.removeItem('latLonQuery');
localStorage.removeItem('latLonLon');
});
// swipe functionality
@@ -129,38 +113,37 @@ const autocompleteOnSelect = async (suggestion, elem) => {
// Do not auto get the same city twice.
if (elem.previousSuggestionValue === suggestion.value) return;
if (overrides[suggestion.value]) {
doRedirectToGeometry(overrides[suggestion.value]);
} else {
const data = await json('https://geocode.arcgis.com/arcgis/rest/services/World/GeocodeServer/find', {
data: {
text: suggestion.value,
magicKey: suggestion.data,
f: 'json',
},
});
const data = await json('https://geocode.arcgis.com/arcgis/rest/services/World/GeocodeServer/find', {
data: {
text: suggestion.value,
magicKey: suggestion.data,
f: 'json',
},
});
const loc = data.locations[0];
if (loc) {
doRedirectToGeometry(loc.feature.geometry);
} else {
console.error('An unexpected error occurred. Please try a different search string.');
}
const loc = data.locations[0];
if (loc) {
doRedirectToGeometry(loc.feature.geometry);
} else {
console.error('An unexpected error occurred. Please try a different search string.');
}
};
const doRedirectToGeometry = (geom) => {
const latLon = { lat: Math.round2(geom.y, 4), lon: Math.round2(geom.x, 4) };
LoadTwcData(latLon);
const latLon = { lat: round2(geom.y, 4), lon: round2(geom.x, 4) };
// Save the query
localStorage.setItem('TwcQuery', document.getElementById('txtAddress').value);
localStorage.setItem('latLonQuery', document.getElementById('txtAddress').value);
localStorage.setItem('latLonLon', JSON.stringify(latLon));
// get the data
loadData(latLon);
};
const btnFullScreenClick = () => {
if (!document.fullscreenElement) {
EnterFullScreen();
enterFullScreen();
} else {
ExitFullscreen();
exitFullscreen();
}
if (isPlaying()) {
@@ -169,12 +152,12 @@ const btnFullScreenClick = () => {
noSleep(false);
}
UpdateFullScreenNavigate();
updateFullScreenNavigate();
return false;
};
const EnterFullScreen = () => {
const enterFullScreen = () => {
const element = document.getElementById('divTwc');
// Supports most browsers and their versions.
@@ -187,10 +170,10 @@ const EnterFullScreen = () => {
} else {
// iOS doesn't support FullScreen API.
window.scrollTo(0, 0);
FullScreenOverride = true;
fullScreenOverride = true;
}
resize();
UpdateFullScreenNavigate();
updateFullScreenNavigate();
// change hover text and image
const img = document.getElementById('ToggleFullScreen');
@@ -198,11 +181,11 @@ const EnterFullScreen = () => {
img.title = 'Exit fullscreen';
};
const ExitFullscreen = () => {
const exitFullscreen = () => {
// exit full-screen
if (FullScreenOverride) {
FullScreenOverride = false;
if (fullScreenOverride) {
fullScreenOverride = false;
}
if (document.exitFullscreen) {
@@ -224,15 +207,15 @@ const ExitFullscreen = () => {
const btnNavigateMenuClick = () => {
postMessage('navButton', 'menu');
UpdateFullScreenNavigate();
updateFullScreenNavigate();
return false;
};
const LoadTwcData = (_latLon) => {
const loadData = (_latLon) => {
// if latlon is provided store it locally
if (_latLon) LoadTwcData.latLon = _latLon;
if (_latLon) loadData.latLon = _latLon;
// get the data
const { latLon } = LoadTwcData;
const { latLon } = loadData;
// if there's no data stop
if (!latLon) return;
@@ -256,39 +239,39 @@ const swipeCallBack = (direction) => {
const btnNavigateRefreshClick = () => {
resetStatuses();
LoadTwcData();
UpdateFullScreenNavigate();
loadData();
updateFullScreenNavigate();
return false;
};
const btnNavigateNextClick = () => {
postMessage('navButton', 'next');
UpdateFullScreenNavigate();
updateFullScreenNavigate();
return false;
};
const btnNavigatePreviousClick = () => {
postMessage('navButton', 'previous');
UpdateFullScreenNavigate();
updateFullScreenNavigate();
return false;
};
let NavigateFadeIntervalId = null;
let navigateFadeIntervalId = null;
const UpdateFullScreenNavigate = () => {
const updateFullScreenNavigate = () => {
document.activeElement.blur();
document.getElementById('divTwcBottom').classList.remove('hidden');
document.getElementById('divTwcBottom').classList.add('visible');
if (NavigateFadeIntervalId) {
clearTimeout(NavigateFadeIntervalId);
NavigateFadeIntervalId = null;
if (navigateFadeIntervalId) {
clearTimeout(navigateFadeIntervalId);
navigateFadeIntervalId = null;
}
NavigateFadeIntervalId = setTimeout(() => {
navigateFadeIntervalId = setTimeout(() => {
if (document.fullscreenElement) {
document.getElementById('divTwcBottom').classList.remove('visible');
document.getElementById('divTwcBottom').classList.add('hidden');
@@ -337,11 +320,9 @@ const documentKeydown = (e) => {
return false;
};
Math.round2 = (value, decimals) => Number(`${Math.round(`${value}e${decimals}`)}e-${decimals}`);
const btnNavigatePlayClick = () => {
postMessage('navButton', 'playToggle');
UpdateFullScreenNavigate();
updateFullScreenNavigate();
return false;
};
@@ -376,13 +357,13 @@ const btnGetGpsClick = async () => {
const { City } = data.address;
const State = states.getTwoDigitCode(data.address.Region);
const Country = data.address.CountryCode;
const TwcQuery = `${ZipCode}, ${City}, ${State}, ${Country}`;
const query = `${ZipCode}, ${City}, ${State}, ${Country}`;
const txtAddress = document.getElementById('txtAddress');
txtAddress.value = TwcQuery;
txtAddress.value = query;
txtAddress.blur();
txtAddress.focus();
// Save the query
localStorage.setItem('TwcQuery', TwcQuery);
localStorage.setItem('latLonQuery', query);
};

View File

@@ -133,7 +133,7 @@ class Almanac extends WeatherDisplay {
fill.date = date;
fill.type = MoonPhase.phase;
fill.icon = { type: 'img', src: Almanac.imageName(MoonPhase.Phase) };
fill.icon = { type: 'img', src: imageName(MoonPhase.phase) };
return this.fillTemplate('day', fill);
});
@@ -145,20 +145,6 @@ class Almanac extends WeatherDisplay {
this.finishDraw();
}
static imageName(type) {
switch (type) {
case 'Full':
return 'images/2/Full-Moon.gif';
case 'Last':
return 'images/2/Last-Quarter.gif';
case 'New':
return 'images/2/New-Moon.gif';
case 'First':
default:
return 'images/2/First-Quarter.gif';
}
}
// make sun and moon data available outside this class
// promise allows for data to be requested before it is available
async getSun() {
@@ -170,6 +156,20 @@ class Almanac extends WeatherDisplay {
}
}
const imageName = (type) => {
switch (type) {
case 'Full':
return 'images/2/Full-Moon.gif';
case 'Last':
return 'images/2/Last-Quarter.gif';
case 'New':
return 'images/2/New-Moon.gif';
case 'First':
default:
return 'images/2/First-Quarter.gif';
}
};
// register display
const display = new Almanac(8, 'almanac');
registerDisplay(display);

View File

@@ -19,7 +19,8 @@ class CurrentWeather extends WeatherDisplay {
}
async getData(_weatherParameters) {
if (!super.getData(_weatherParameters)) return;
// always load the data for use in the lower scroll
const superResult = super.getData(_weatherParameters);
const weatherParameters = _weatherParameters ?? this.weatherParameters;
// Load the observations
@@ -39,6 +40,8 @@ class CurrentWeather extends WeatherDisplay {
data: {
limit: 2,
},
retryCount: 3,
stillWaiting: () => this.stillWaiting(),
});
// test data quality
@@ -60,14 +63,17 @@ class CurrentWeather extends WeatherDisplay {
this.getDataCallback(undefined);
return;
}
// preload the icon
preloadImg(getWeatherIconFromIconLink(observations.features[0].properties.icon));
// we only get here if there was no error above
this.data = { ...observations, station };
this.setStatus(STATUS.loaded);
this.getDataCallback();
// stop here if we're disabled
if (!superResult) return;
// preload the icon
preloadImg(getWeatherIconFromIconLink(observations.features[0].properties.icon));
this.setStatus(STATUS.loaded);
}
// format the data for use outside this function
@@ -128,7 +134,7 @@ class CurrentWeather extends WeatherDisplay {
let Conditions = data.observations.textDescription;
if (Conditions.length > 15) {
Conditions = this.shortConditions(Conditions);
Conditions = shortConditions(Conditions);
}
fill.condition = Conditions;
@@ -163,33 +169,35 @@ class CurrentWeather extends WeatherDisplay {
// make data available outside this class
// promise allows for data to be requested before it is available
async getCurrentWeather() {
async getCurrentWeather(stillWaiting) {
if (stillWaiting) this.stillWaitingCallbacks.push(stillWaiting);
return new Promise((resolve) => {
if (this.data) resolve(this.parseData());
// data not available, put it into the data callback queue
this.getDataCallbacks.push(() => resolve(this.parseData()));
});
}
static shortConditions(_condition) {
let condition = _condition;
condition = condition.replace(/Light/g, 'L');
condition = condition.replace(/Heavy/g, 'H');
condition = condition.replace(/Partly/g, 'P');
condition = condition.replace(/Mostly/g, 'M');
condition = condition.replace(/Few/g, 'F');
condition = condition.replace(/Thunderstorm/g, 'T\'storm');
condition = condition.replace(/ in /g, '');
condition = condition.replace(/Vicinity/g, '');
condition = condition.replace(/ and /g, ' ');
condition = condition.replace(/Freezing Rain/g, 'Frz Rn');
condition = condition.replace(/Freezing/g, 'Frz');
condition = condition.replace(/Unknown Precip/g, '');
condition = condition.replace(/L Snow Fog/g, 'L Snw/Fog');
condition = condition.replace(/ with /g, '/');
return condition;
}
}
const shortConditions = (_condition) => {
let condition = _condition;
condition = condition.replace(/Light/g, 'L');
condition = condition.replace(/Heavy/g, 'H');
condition = condition.replace(/Partly/g, 'P');
condition = condition.replace(/Mostly/g, 'M');
condition = condition.replace(/Few/g, 'F');
condition = condition.replace(/Thunderstorm/g, 'T\'storm');
condition = condition.replace(/ in /g, '');
condition = condition.replace(/Vicinity/g, '');
condition = condition.replace(/ and /g, ' ');
condition = condition.replace(/Freezing Rain/g, 'Frz Rn');
condition = condition.replace(/Freezing/g, 'Frz');
condition = condition.replace(/Unknown Precip/g, '');
condition = condition.replace(/L Snow Fog/g, 'L Snw/Fog');
condition = condition.replace(/ with /g, '/');
return condition;
};
const display = new CurrentWeather(0, 'current-weather');
registerDisplay(display);

View File

@@ -43,7 +43,7 @@ const incrementInterval = () => {
const drawScreen = async () => {
// get the conditions
const data = await getCurrentWeather();
const data = await getCurrentWeather(() => this.stillWaiting());
// nothing to do if there's no data yet
if (!data) return;

View File

@@ -28,6 +28,8 @@ class ExtendedForecast extends WeatherDisplay {
data: {
units: 'us',
},
retryCount: 3,
stillWaiting: () => this.stillWaiting(),
});
} catch (e) {
console.error('Unable to get extended forecast');
@@ -36,95 +38,11 @@ class ExtendedForecast extends WeatherDisplay {
return;
}
// we only get here if there was no error above
this.data = ExtendedForecast.parse(forecast.properties.periods);
this.data = parse(forecast.properties.periods);
this.screenIndex = 0;
this.setStatus(STATUS.loaded);
}
// the api provides the forecast in 12 hour increments, flatten to day increments with high and low temperatures
static parse(fullForecast) {
// create a list of days starting with today
const Days = [0, 1, 2, 3, 4, 5, 6];
const dates = Days.map((shift) => {
const date = DateTime.local().startOf('day').plus({ days: shift });
return date.toLocaleString({ weekday: 'short' });
});
// track the destination forecast index
let destIndex = 0;
const forecast = [];
fullForecast.forEach((period) => {
// create the destination object if necessary
if (!forecast[destIndex]) {
forecast.push({
dayName: '', low: undefined, high: undefined, text: undefined, icon: undefined,
});
}
// get the object to modify/populate
const fDay = forecast[destIndex];
// high temperature will always be last in the source array so it will overwrite the low values assigned below
fDay.icon = getWeatherIconFromIconLink(period.icon);
fDay.text = ExtendedForecast.shortenExtendedForecastText(period.shortForecast);
fDay.dayName = dates[destIndex];
// preload the icon
preloadImg(fDay.icon);
if (period.isDaytime) {
// day time is the high temperature
fDay.high = period.temperature;
destIndex += 1;
} else {
// low temperature
fDay.low = period.temperature;
}
});
return forecast;
}
static shortenExtendedForecastText(long) {
const regexList = [
[/ and /ig, ' '],
[/Slight /ig, ''],
[/Chance /ig, ''],
[/Very /ig, ''],
[/Patchy /ig, ''],
[/Areas /ig, ''],
[/Dense /ig, ''],
[/Thunderstorm/g, 'T\'Storm'],
];
// run all regexes
const short = regexList.reduce((working, [regex, replace]) => working.replace(regex, replace), long);
let conditions = short.split(' ');
if (short.indexOf('then') !== -1) {
conditions = short.split(' then ');
conditions = conditions[1].split(' ');
}
let short1 = conditions[0].substr(0, 10);
let short2 = '';
if (conditions[1]) {
if (!short1.endsWith('.')) {
short2 = conditions[1].substr(0, 10);
} else {
short1 = short1.replace(/\./, '');
}
if (short2 === 'Blowing') {
short2 = '';
}
}
let result = short1;
if (short2 !== '') {
result += ` ${short2}`;
}
return result;
}
async drawCanvas() {
super.drawCanvas();
@@ -160,5 +78,89 @@ class ExtendedForecast extends WeatherDisplay {
}
}
// the api provides the forecast in 12 hour increments, flatten to day increments with high and low temperatures
const parse = (fullForecast) => {
// create a list of days starting with today
const Days = [0, 1, 2, 3, 4, 5, 6];
const dates = Days.map((shift) => {
const date = DateTime.local().startOf('day').plus({ days: shift });
return date.toLocaleString({ weekday: 'short' });
});
// track the destination forecast index
let destIndex = 0;
const forecast = [];
fullForecast.forEach((period) => {
// create the destination object if necessary
if (!forecast[destIndex]) {
forecast.push({
dayName: '', low: undefined, high: undefined, text: undefined, icon: undefined,
});
}
// get the object to modify/populate
const fDay = forecast[destIndex];
// high temperature will always be last in the source array so it will overwrite the low values assigned below
fDay.icon = getWeatherIconFromIconLink(period.icon);
fDay.text = shortenExtendedForecastText(period.shortForecast);
fDay.dayName = dates[destIndex];
// preload the icon
preloadImg(fDay.icon);
if (period.isDaytime) {
// day time is the high temperature
fDay.high = period.temperature;
destIndex += 1;
} else {
// low temperature
fDay.low = period.temperature;
}
});
return forecast;
};
const shortenExtendedForecastText = (long) => {
const regexList = [
[/ and /ig, ' '],
[/Slight /ig, ''],
[/Chance /ig, ''],
[/Very /ig, ''],
[/Patchy /ig, ''],
[/Areas /ig, ''],
[/Dense /ig, ''],
[/Thunderstorm/g, 'T\'Storm'],
];
// run all regexes
const short = regexList.reduce((working, [regex, replace]) => working.replace(regex, replace), long);
let conditions = short.split(' ');
if (short.indexOf('then') !== -1) {
conditions = short.split(' then ');
conditions = conditions[1].split(' ');
}
let short1 = conditions[0].substr(0, 10);
let short2 = '';
if (conditions[1]) {
if (!short1.endsWith('.')) {
short2 = conditions[1].substr(0, 10);
} else {
short1 = short1.replace(/\./, '');
}
if (short2 === 'Blowing') {
short2 = '';
}
}
let result = short1;
if (short2 !== '') {
result += ` ${short2}`;
}
return result;
};
// register display
registerDisplay(new ExtendedForecast(7, 'extended-forecast'));

View File

@@ -27,7 +27,7 @@ class HourlyGraph extends WeatherDisplay {
async getData() {
if (!super.getData()) return;
const data = await getHourlyData();
const data = await getHourlyData(() => this.stillWaiting());
if (data === undefined) {
this.setStatus(STATUS.failed);
return;

View File

@@ -33,7 +33,7 @@ class Hourly extends WeatherDisplay {
let forecast;
try {
// get the forecast
forecast = await json(weatherParameters.forecastGridData);
forecast = await json(weatherParameters.forecastGridData, { retryCount: 3, stillWaiting: () => this.stillWaiting() });
} catch (e) {
console.error('Get hourly forecast failed');
console.error(e.status, e.responseJSON);
@@ -43,7 +43,7 @@ class Hourly extends WeatherDisplay {
return;
}
this.data = await Hourly.parseForecast(forecast.properties);
this.data = await parseForecast(forecast.properties);
this.getDataCallback();
if (!superResponse) return;
@@ -51,66 +51,6 @@ class Hourly extends WeatherDisplay {
this.drawLongCanvas();
}
// extract specific values from forecast and format as an array
static async parseForecast(data) {
const temperature = Hourly.expand(data.temperature.values);
const apparentTemperature = Hourly.expand(data.apparentTemperature.values);
const windSpeed = Hourly.expand(data.windSpeed.values);
const windDirection = Hourly.expand(data.windDirection.values);
const skyCover = Hourly.expand(data.skyCover.values); // cloud icon
const weather = Hourly.expand(data.weather.values); // fog icon
const iceAccumulation = Hourly.expand(data.iceAccumulation.values); // ice icon
const probabilityOfPrecipitation = Hourly.expand(data.probabilityOfPrecipitation.values); // rain icon
const snowfallAmount = Hourly.expand(data.snowfallAmount.values); // snow icon
const icons = await Hourly.determineIcon(skyCover, weather, iceAccumulation, probabilityOfPrecipitation, snowfallAmount, windSpeed);
return temperature.map((val, idx) => ({
temperature: celsiusToFahrenheit(temperature[idx]),
apparentTemperature: celsiusToFahrenheit(apparentTemperature[idx]),
windSpeed: kilometersToMiles(windSpeed[idx]),
windDirection: directionToNSEW(windDirection[idx]),
probabilityOfPrecipitation: probabilityOfPrecipitation[idx],
skyCover: skyCover[idx],
icon: icons[idx],
}));
}
// given forecast paramaters determine a suitable icon
static async determineIcon(skyCover, weather, iceAccumulation, probabilityOfPrecipitation, snowfallAmount, windSpeed) {
const startOfHour = DateTime.local().startOf('hour');
const sunTimes = (await getSun()).sun;
const overnight = Interval.fromDateTimes(DateTime.fromJSDate(sunTimes[0].sunset), DateTime.fromJSDate(sunTimes[1].sunrise));
const tomorrowOvernight = DateTime.fromJSDate(sunTimes[1].sunset);
return skyCover.map((val, idx) => {
const hour = startOfHour.plus({ hours: idx });
const isNight = overnight.contains(hour) || (hour > tomorrowOvernight);
return getHourlyIcon(skyCover[idx], weather[idx], iceAccumulation[idx], probabilityOfPrecipitation[idx], snowfallAmount[idx], windSpeed[idx], isNight);
});
}
// expand a set of values with durations to an hour-by-hour array
static expand(data) {
const startOfHour = DateTime.utc().startOf('hour').toMillis();
const result = []; // resulting expanded values
data.forEach((item) => {
let startTime = Date.parse(item.validTime.substr(0, item.validTime.indexOf('/')));
const duration = Duration.fromISO(item.validTime.substr(item.validTime.indexOf('/') + 1)).shiftTo('milliseconds').values.milliseconds;
const endTime = startTime + duration;
// loop through duration at one hour intervals
do {
// test for timestamp greater than now
if (startTime >= startOfHour && result.length < 24) {
result.push(item.value); // push data array
} // timestamp is after now
// increment start time by 1 hour
startTime += 3600000;
} while (startTime < endTime && result.length < 24);
}); // for each value
return result;
}
async drawLongCanvas() {
// get the list element and populate
const list = this.elem.querySelector('.hourly-lines');
@@ -178,22 +118,10 @@ class Hourly extends WeatherDisplay {
this.elem.querySelector('.main').scrollTo(0, offsetY);
}
static getTravelCitiesDayName(cities) {
// effectively returns early on the first found date
return cities.reduce((dayName, city) => {
if (city && dayName === '') {
// today or tomorrow
const day = DateTime.local().plus({ days: (city.today) ? 0 : 1 });
// return the day
return day.toLocaleString({ weekday: 'long' });
}
return dayName;
}, '');
}
// make data available outside this class
// promise allows for data to be requested before it is available
async getCurrentData() {
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
@@ -202,6 +130,66 @@ class Hourly extends WeatherDisplay {
}
}
// extract specific values from forecast and format as an array
const parseForecast = async (data) => {
const temperature = expand(data.temperature.values);
const apparentTemperature = expand(data.apparentTemperature.values);
const windSpeed = expand(data.windSpeed.values);
const windDirection = expand(data.windDirection.values);
const skyCover = expand(data.skyCover.values); // cloud icon
const weather = expand(data.weather.values); // fog icon
const iceAccumulation = expand(data.iceAccumulation.values); // ice icon
const probabilityOfPrecipitation = expand(data.probabilityOfPrecipitation.values); // rain icon
const snowfallAmount = expand(data.snowfallAmount.values); // snow icon
const icons = await determineIcon(skyCover, weather, iceAccumulation, probabilityOfPrecipitation, snowfallAmount, windSpeed);
return temperature.map((val, idx) => ({
temperature: celsiusToFahrenheit(temperature[idx]),
apparentTemperature: celsiusToFahrenheit(apparentTemperature[idx]),
windSpeed: kilometersToMiles(windSpeed[idx]),
windDirection: directionToNSEW(windDirection[idx]),
probabilityOfPrecipitation: probabilityOfPrecipitation[idx],
skyCover: skyCover[idx],
icon: icons[idx],
}));
};
// given forecast paramaters determine a suitable icon
const determineIcon = async (skyCover, weather, iceAccumulation, probabilityOfPrecipitation, snowfallAmount, windSpeed) => {
const startOfHour = DateTime.local().startOf('hour');
const sunTimes = (await getSun()).sun;
const overnight = Interval.fromDateTimes(DateTime.fromJSDate(sunTimes[0].sunset), DateTime.fromJSDate(sunTimes[1].sunrise));
const tomorrowOvernight = DateTime.fromJSDate(sunTimes[1].sunset);
return skyCover.map((val, idx) => {
const hour = startOfHour.plus({ hours: idx });
const isNight = overnight.contains(hour) || (hour > tomorrowOvernight);
return getHourlyIcon(skyCover[idx], weather[idx], iceAccumulation[idx], probabilityOfPrecipitation[idx], snowfallAmount[idx], windSpeed[idx], isNight);
});
};
// expand a set of values with durations to an hour-by-hour array
const expand = (data) => {
const startOfHour = DateTime.utc().startOf('hour').toMillis();
const result = []; // resulting expanded values
data.forEach((item) => {
let startTime = Date.parse(item.validTime.substr(0, item.validTime.indexOf('/')));
const duration = Duration.fromISO(item.validTime.substr(item.validTime.indexOf('/') + 1)).shiftTo('milliseconds').values.milliseconds;
const endTime = startTime + duration;
// loop through duration at one hour intervals
do {
// test for timestamp greater than now
if (startTime >= startOfHour && result.length < 24) {
result.push(item.value); // push data array
} // timestamp is after now
// increment start time by 1 hour
startTime += 3600000;
} while (startTime < endTime && result.length < 24);
}); // for each value
return result;
};
// register display
const display = new Hourly(2, 'hourly', false);
registerDisplay(display);

View File

@@ -34,7 +34,7 @@ class LatestObservations extends WeatherDisplay {
// 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`);
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 === ''
@@ -82,7 +82,7 @@ class LatestObservations extends WeatherDisplay {
const fill = {};
fill.location = locationCleanup(condition.city).substr(0, 14);
fill.temp = Temperature;
fill.weather = LatestObservations.shortenCurrentConditions(condition.textDescription).substr(0, 9);
fill.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') {
@@ -100,25 +100,24 @@ class LatestObservations extends WeatherDisplay {
this.finishDraw();
}
static shortenCurrentConditions(_condition) {
let condition = _condition;
condition = condition.replace(/Light/, 'L');
condition = condition.replace(/Heavy/, 'H');
condition = condition.replace(/Partly/, 'P');
condition = condition.replace(/Mostly/, 'M');
condition = condition.replace(/Few/, 'F');
condition = condition.replace(/Thunderstorm/, 'T\'storm');
condition = condition.replace(/ in /, '');
condition = condition.replace(/Vicinity/, '');
condition = condition.replace(/ and /, ' ');
condition = condition.replace(/Freezing Rain/, 'Frz Rn');
condition = condition.replace(/Freezing/, 'Frz');
condition = condition.replace(/Unknown Precip/, '');
condition = condition.replace(/L Snow Fog/, 'L Snw/Fog');
condition = condition.replace(/ with /, '/');
return condition;
}
}
const shortenCurrentConditions = (_condition) => {
let condition = _condition;
condition = condition.replace(/Light/, 'L');
condition = condition.replace(/Heavy/, 'H');
condition = condition.replace(/Partly/, 'P');
condition = condition.replace(/Mostly/, 'M');
condition = condition.replace(/Few/, 'F');
condition = condition.replace(/Thunderstorm/, 'T\'storm');
condition = condition.replace(/ in /, '');
condition = condition.replace(/Vicinity/, '');
condition = condition.replace(/ and /, ' ');
condition = condition.replace(/Freezing Rain/, 'Frz Rn');
condition = condition.replace(/Freezing/, 'Frz');
condition = condition.replace(/Unknown Precip/, '');
condition = condition.replace(/L Snow Fog/, 'L Snw/Fog');
condition = condition.replace(/ with /, '/');
return condition;
};
// register display
registerDisplay(new LatestObservations(1, 'latest-observations'));

View File

@@ -25,7 +25,7 @@ class LocalForecast extends WeatherDisplay {
return;
}
// parse raw data
const conditions = LocalForecast.parse(rawData);
const conditions = parse(rawData);
// read each text
this.screenTexts = conditions.map((condition) => {
@@ -63,6 +63,8 @@ class LocalForecast extends WeatherDisplay {
data: {
units: 'us',
},
retryCount: 3,
stillWaiting: () => this.stillWaiting(),
});
} catch (e) {
console.error(`GetWeatherForecast failed: ${weatherParameters.forecast}`);
@@ -80,17 +82,14 @@ class LocalForecast extends WeatherDisplay {
this.finishDraw();
}
// format the forecast
static parse(forecast) {
// only use the first 6 lines
return forecast.properties.periods.slice(0, 6).map((text) => ({
// format day and text
DayName: text.name.toUpperCase(),
Text: text.detailedForecast,
}));
}
}
// format the forecast
// only use the first 6 lines
const parse = (forecast) => forecast.properties.periods.slice(0, 6).map((text) => ({
// format day and text
DayName: text.name.toUpperCase(),
Text: text.detailedForecast,
}));
// register display
registerDisplay(new LocalForecast(6, 'local-forecast'));

View File

@@ -98,21 +98,27 @@ const getWeather = async (latLon) => {
const updateStatus = (value) => {
if (value.id < 0) return;
if (!progress) return;
progress.drawCanvas(displays, countLoadedCanvases());
progress.drawCanvas(displays, countLoadedDisplays());
// calculate first enabled display
const firstDisplayIndex = displays.findIndex((display) => display.enabled);
// if this is the first display and we're playing, load it up so it starts playing
if (isPlaying() && value.id === 0 && value.status === STATUS.loaded) {
if (isPlaying() && value.id === firstDisplayIndex && value.status === STATUS.loaded) {
navTo(msg.command.firstFrame);
}
// send loaded messaged to parent
if (countLoadedCanvases() < displays.length) return;
if (countLoadedDisplays() < displays.length) return;
// everything loaded, set timestamps
AssignLastUpdate(new Date());
};
const countLoadedCanvases = () => displays.reduce((acc, display) => {
// note: a display that is "still waiting"/"retrying" is considered loaded intentionally
// the weather.gov api has long load times for some products when you are the first
// requester for the product after the cache expires
const countLoadedDisplays = () => displays.reduce((acc, display) => {
if (display.status !== STATUS.loading) return acc + 1;
return acc;
}, 0);
@@ -252,11 +258,11 @@ const getDisplay = (index) => displays[index];
// resize the container on a page resize
const resize = () => {
const widthZoomPercent = window.innerWidth / 640;
const heightZoomPercent = window.innerHeight / 480;
const marginOffset = (document.fullscreenElement) ? 0 : 16;
const widthZoomPercent = (window.innerWidth - marginOffset) / 640;
const heightZoomPercent = (window.innerHeight - marginOffset) / 480;
const scale = Math.min(widthZoomPercent, heightZoomPercent);
if (scale < 1.0 || document.fullscreenElement) {
document.getElementById('container').style.zoom = scale;
} else {

View File

@@ -55,6 +55,9 @@ class Progress extends WeatherDisplay {
case STATUS.disabled:
statusClass = 'disabled';
break;
case STATUS.retrying:
statusClass = 'retrying';
break;
default:
}

View File

@@ -0,0 +1,185 @@
const getXYFromLatitudeLongitudeMap = (pos, offsetX, offsetY) => {
let y = 0;
let x = 0;
const imgHeight = 3200;
const imgWidth = 5100;
y = (51.75 - pos.latitude) * 55.2;
// center map
y -= offsetY;
// Do not allow the map to exceed the max/min coordinates.
if (y > (imgHeight - (offsetY * 2))) {
y = imgHeight - (offsetY * 2);
} else if (y < 0) {
y = 0;
}
x = ((-130.37 - pos.longitude) * 41.775) * -1;
// center map
x -= offsetX;
// Do not allow the map to exceed the max/min coordinates.
if (x > (imgWidth - (offsetX * 2))) {
x = imgWidth - (offsetX * 2);
} else if (x < 0) {
x = 0;
}
return { x: x * 2, y: y * 2 };
};
const getXYFromLatitudeLongitudeDoppler = (pos, offsetX, offsetY) => {
let y = 0;
let x = 0;
const imgHeight = 6000;
const imgWidth = 2800;
y = (51 - pos.latitude) * 61.4481;
// center map
y -= offsetY;
// Do not allow the map to exceed the max/min coordinates.
if (y > (imgHeight - (offsetY * 2))) {
y = imgHeight - (offsetY * 2);
} else if (y < 0) {
y = 0;
}
x = ((-129.138 - pos.longitude) * 42.1768) * -1;
// center map
x -= offsetX;
// Do not allow the map to exceed the max/min coordinates.
if (x > (imgWidth - (offsetX * 2))) {
x = imgWidth - (offsetX * 2);
} else if (x < 0) {
x = 0;
}
return { x: x * 2, y: y * 2 };
};
const removeDopplerRadarImageNoise = (RadarContext) => {
const RadarImageData = RadarContext.getImageData(0, 0, RadarContext.canvas.width, RadarContext.canvas.height);
// examine every pixel,
// change any old rgb to the new-rgb
for (let i = 0; i < RadarImageData.data.length; i += 4) {
// i + 0 = red
// i + 1 = green
// i + 2 = blue
// i + 3 = alpha (0 = transparent, 255 = opaque)
let R = RadarImageData.data[i];
let G = RadarImageData.data[i + 1];
let B = RadarImageData.data[i + 2];
let A = RadarImageData.data[i + 3];
// is this pixel the old rgb?
if ((R === 0 && G === 0 && B === 0)
|| (R === 0 && G === 236 && B === 236)
|| (R === 1 && G === 160 && B === 246)
|| (R === 0 && G === 0 && B === 246)) {
// change to your new rgb
// Transparent
R = 0;
G = 0;
B = 0;
A = 0;
} else if ((R === 0 && G === 255 && B === 0)) {
// Light Green 1
R = 49;
G = 210;
B = 22;
A = 255;
} else if ((R === 0 && G === 200 && B === 0)) {
// Light Green 2
R = 0;
G = 142;
B = 0;
A = 255;
} else if ((R === 0 && G === 144 && B === 0)) {
// Dark Green 1
R = 20;
G = 90;
B = 15;
A = 255;
} else if ((R === 255 && G === 255 && B === 0)) {
// Dark Green 2
R = 10;
G = 40;
B = 10;
A = 255;
} else if ((R === 231 && G === 192 && B === 0)) {
// Yellow
R = 196;
G = 179;
B = 70;
A = 255;
} else if ((R === 255 && G === 144 && B === 0)) {
// Orange
R = 190;
G = 72;
B = 19;
A = 255;
} else if ((R === 214 && G === 0 && B === 0)
|| (R === 255 && G === 0 && B === 0)) {
// Red
R = 171;
G = 14;
B = 14;
A = 255;
} else if ((R === 192 && G === 0 && B === 0)
|| (R === 255 && G === 0 && B === 255)) {
// Brown
R = 115;
G = 31;
B = 4;
A = 255;
}
RadarImageData.data[i] = R;
RadarImageData.data[i + 1] = G;
RadarImageData.data[i + 2] = B;
RadarImageData.data[i + 3] = A;
}
RadarContext.putImageData(RadarImageData, 0, 0);
};
const mergeDopplerRadarImage = (mapContext, radarContext) => {
const mapImageData = mapContext.getImageData(0, 0, mapContext.canvas.width, mapContext.canvas.height);
const radarImageData = radarContext.getImageData(0, 0, radarContext.canvas.width, radarContext.canvas.height);
// examine every pixel,
// change any old rgb to the new-rgb
for (let i = 0; i < radarImageData.data.length; i += 4) {
// i + 0 = red
// i + 1 = green
// i + 2 = blue
// i + 3 = alpha (0 = transparent, 255 = opaque)
// is this pixel the old rgb?
if ((mapImageData.data[i] < 116 && mapImageData.data[i + 1] < 116 && mapImageData.data[i + 2] < 116)) {
// change to your new rgb
// Transparent
radarImageData.data[i] = 0;
radarImageData.data[i + 1] = 0;
radarImageData.data[i + 2] = 0;
radarImageData.data[i + 3] = 0;
}
}
radarContext.putImageData(radarImageData, 0, 0);
mapContext.drawImage(radarContext.canvas, 0, 0);
};
export {
getXYFromLatitudeLongitudeDoppler,
getXYFromLatitudeLongitudeMap,
removeDopplerRadarImageNoise,
mergeDopplerRadarImage,
};

View File

@@ -6,6 +6,7 @@ import { text } from './utils/fetch.mjs';
import { rewriteUrl } from './utils/cors.mjs';
import WeatherDisplay from './weatherdisplay.mjs';
import { registerDisplay } from './navigation.mjs';
import * as utils from './radar-utils.mjs';
class Radar extends WeatherDisplay {
constructor(navId, elemId) {
@@ -109,7 +110,7 @@ class Radar extends WeatherDisplay {
const height = 1600;
offsetX *= 2;
offsetY *= 2;
const sourceXY = Radar.getXYFromLatitudeLongitudeMap(weatherParameters, offsetX, offsetY);
const sourceXY = utils.getXYFromLatitudeLongitudeMap(weatherParameters, offsetX, offsetY);
// create working context for manipulation
const workingCanvas = document.createElement('canvas');
@@ -121,7 +122,7 @@ class Radar extends WeatherDisplay {
// calculate radar offsets
const radarOffsetX = 120;
const radarOffsetY = 70;
const radarSourceXY = Radar.getXYFromLatitudeLongitudeDoppler(weatherParameters, offsetX, offsetY);
const radarSourceXY = utils.getXYFromLatitudeLongitudeDoppler(weatherParameters, offsetX, offsetY);
const radarSourceX = radarSourceXY.x / 2;
const radarSourceY = radarSourceXY.y / 2;
@@ -179,10 +180,10 @@ class Radar extends WeatherDisplay {
cropContext.imageSmoothingEnabled = false;
cropContext.drawImage(workingCanvas, radarSourceX, radarSourceY, (radarOffsetX * 2), (radarOffsetY * 2.33), 0, 0, 640, 367);
// clean the image
Radar.removeDopplerRadarImageNoise(cropContext);
utils.removeDopplerRadarImageNoise(cropContext);
// merge the radar and map
Radar.mergeDopplerRadarImage(context, cropContext);
utils.mergeDopplerRadarImage(context, cropContext);
const elem = this.fillTemplate('frame', { map: { type: 'img', src: canvas.toDataURL() } });
@@ -218,185 +219,6 @@ class Radar extends WeatherDisplay {
this.finishDraw();
}
static getXYFromLatitudeLongitudeMap(pos, offsetX, offsetY) {
let y = 0;
let x = 0;
const imgHeight = 3200;
const imgWidth = 5100;
y = (51.75 - pos.latitude) * 55.2;
// center map
y -= offsetY;
// Do not allow the map to exceed the max/min coordinates.
if (y > (imgHeight - (offsetY * 2))) {
y = imgHeight - (offsetY * 2);
} else if (y < 0) {
y = 0;
}
x = ((-130.37 - pos.longitude) * 41.775) * -1;
// center map
x -= offsetX;
// Do not allow the map to exceed the max/min coordinates.
if (x > (imgWidth - (offsetX * 2))) {
x = imgWidth - (offsetX * 2);
} else if (x < 0) {
x = 0;
}
return { x: x * 2, y: y * 2 };
}
static getXYFromLatitudeLongitudeDoppler(pos, offsetX, offsetY) {
let y = 0;
let x = 0;
const imgHeight = 6000;
const imgWidth = 2800;
y = (51 - pos.latitude) * 61.4481;
// center map
y -= offsetY;
// Do not allow the map to exceed the max/min coordinates.
if (y > (imgHeight - (offsetY * 2))) {
y = imgHeight - (offsetY * 2);
} else if (y < 0) {
y = 0;
}
x = ((-129.138 - pos.longitude) * 42.1768) * -1;
// center map
x -= offsetX;
// Do not allow the map to exceed the max/min coordinates.
if (x > (imgWidth - (offsetX * 2))) {
x = imgWidth - (offsetX * 2);
} else if (x < 0) {
x = 0;
}
return { x: x * 2, y: y * 2 };
}
static removeDopplerRadarImageNoise(RadarContext) {
const RadarImageData = RadarContext.getImageData(0, 0, RadarContext.canvas.width, RadarContext.canvas.height);
// examine every pixel,
// change any old rgb to the new-rgb
for (let i = 0; i < RadarImageData.data.length; i += 4) {
// i + 0 = red
// i + 1 = green
// i + 2 = blue
// i + 3 = alpha (0 = transparent, 255 = opaque)
let R = RadarImageData.data[i];
let G = RadarImageData.data[i + 1];
let B = RadarImageData.data[i + 2];
let A = RadarImageData.data[i + 3];
// is this pixel the old rgb?
if ((R === 0 && G === 0 && B === 0)
|| (R === 0 && G === 236 && B === 236)
|| (R === 1 && G === 160 && B === 246)
|| (R === 0 && G === 0 && B === 246)) {
// change to your new rgb
// Transparent
R = 0;
G = 0;
B = 0;
A = 0;
} else if ((R === 0 && G === 255 && B === 0)) {
// Light Green 1
R = 49;
G = 210;
B = 22;
A = 255;
} else if ((R === 0 && G === 200 && B === 0)) {
// Light Green 2
R = 0;
G = 142;
B = 0;
A = 255;
} else if ((R === 0 && G === 144 && B === 0)) {
// Dark Green 1
R = 20;
G = 90;
B = 15;
A = 255;
} else if ((R === 255 && G === 255 && B === 0)) {
// Dark Green 2
R = 10;
G = 40;
B = 10;
A = 255;
} else if ((R === 231 && G === 192 && B === 0)) {
// Yellow
R = 196;
G = 179;
B = 70;
A = 255;
} else if ((R === 255 && G === 144 && B === 0)) {
// Orange
R = 190;
G = 72;
B = 19;
A = 255;
} else if ((R === 214 && G === 0 && B === 0)
|| (R === 255 && G === 0 && B === 0)) {
// Red
R = 171;
G = 14;
B = 14;
A = 255;
} else if ((R === 192 && G === 0 && B === 0)
|| (R === 255 && G === 0 && B === 255)) {
// Brown
R = 115;
G = 31;
B = 4;
A = 255;
}
RadarImageData.data[i] = R;
RadarImageData.data[i + 1] = G;
RadarImageData.data[i + 2] = B;
RadarImageData.data[i + 3] = A;
}
RadarContext.putImageData(RadarImageData, 0, 0);
}
static mergeDopplerRadarImage(mapContext, radarContext) {
const mapImageData = mapContext.getImageData(0, 0, mapContext.canvas.width, mapContext.canvas.height);
const radarImageData = radarContext.getImageData(0, 0, radarContext.canvas.width, radarContext.canvas.height);
// examine every pixel,
// change any old rgb to the new-rgb
for (let i = 0; i < radarImageData.data.length; i += 4) {
// i + 0 = red
// i + 1 = green
// i + 2 = blue
// i + 3 = alpha (0 = transparent, 255 = opaque)
// is this pixel the old rgb?
if ((mapImageData.data[i] < 116 && mapImageData.data[i + 1] < 116 && mapImageData.data[i + 2] < 116)) {
// change to your new rgb
// Transparent
radarImageData.data[i] = 0;
radarImageData.data[i + 1] = 0;
radarImageData.data[i + 2] = 0;
radarImageData.data[i + 3] = 0;
}
}
radarContext.putImageData(radarImageData, 0, 0);
mapContext.drawImage(radarContext.canvas, 0, 0);
}
}
// register display

View File

@@ -0,0 +1,205 @@
import { getWeatherRegionalIconFromIconLink } from './icons.mjs';
import { preloadImg } from './utils/image.mjs';
import { json } from './utils/fetch.mjs';
const buildForecast = (forecast, city, cityXY) => ({
daytime: forecast.isDaytime,
temperature: forecast.temperature || 0,
name: formatCity(city.city),
icon: forecast.icon,
x: cityXY.x,
y: cityXY.y,
time: forecast.startTime,
});
const getRegionalObservation = async (point, city) => {
try {
// get stations
const stations = await json(`https://api.weather.gov/gridpoints/${city.point.wfo}/${city.point.x},${city.point.y}/stations`);
// get the first station
const station = stations.features[0].id;
// get the observation data
const observation = await json(`${station}/observations/latest`);
// preload the image
if (!observation.properties.icon) return false;
preloadImg(getWeatherRegionalIconFromIconLink(observation.properties.icon, !observation.properties.daytime));
// return the observation
return observation.properties;
} catch (e) {
console.log(`Unable to get regional observations for ${city.Name ?? city.city}`);
console.error(e.status, e.responseJSON);
return false;
}
};
// utility latitude/pixel conversions
const getXYFromLatitudeLongitude = (Latitude, Longitude, OffsetX, OffsetY, state) => {
if (state === 'AK') return getXYFromLatitudeLongitudeAK(Latitude, Longitude, OffsetX, OffsetY);
if (state === 'HI') return getXYFromLatitudeLongitudeHI(Latitude, Longitude, OffsetX, OffsetY);
let y = 0;
let x = 0;
const ImgHeight = 1600;
const ImgWidth = 2550;
y = (50.5 - Latitude) * 55.2;
y -= OffsetY; // Centers map.
// Do not allow the map to exceed the max/min coordinates.
if (y > (ImgHeight - (OffsetY * 2))) {
y = ImgHeight - (OffsetY * 2);
} else if (y < 0) {
y = 0;
}
x = ((-127.5 - Longitude) * 41.775) * -1;
x -= OffsetX; // Centers map.
// Do not allow the map to exceed the max/min coordinates.
if (x > (ImgWidth - (OffsetX * 2))) {
x = ImgWidth - (OffsetX * 2);
} else if (x < 0) {
x = 0;
}
return { x, y };
};
const getXYFromLatitudeLongitudeAK = (Latitude, Longitude, OffsetX, OffsetY) => {
let y = 0;
let x = 0;
const ImgHeight = 1142;
const ImgWidth = 1200;
y = (73.0 - Latitude) * 56;
y -= OffsetY; // Centers map.
// Do not allow the map to exceed the max/min coordinates.
if (y > (ImgHeight - (OffsetY * 2))) {
y = ImgHeight - (OffsetY * 2);
} else if (y < 0) {
y = 0;
}
x = ((-175.0 - Longitude) * 25.0) * -1;
x -= OffsetX; // Centers map.
// Do not allow the map to exceed the max/min coordinates.
if (x > (ImgWidth - (OffsetX * 2))) {
x = ImgWidth - (OffsetX * 2);
} else if (x < 0) {
x = 0;
}
return { x, y };
};
const getXYFromLatitudeLongitudeHI = (Latitude, Longitude, OffsetX, OffsetY) => {
let y = 0;
let x = 0;
const ImgHeight = 571;
const ImgWidth = 600;
y = (25 - Latitude) * 55.2;
y -= OffsetY; // Centers map.
// Do not allow the map to exceed the max/min coordinates.
if (y > (ImgHeight - (OffsetY * 2))) {
y = ImgHeight - (OffsetY * 2);
} else if (y < 0) {
y = 0;
}
x = ((-164.5 - Longitude) * 41.775) * -1;
x -= OffsetX; // Centers map.
// Do not allow the map to exceed the max/min coordinates.
if (x > (ImgWidth - (OffsetX * 2))) {
x = ImgWidth - (OffsetX * 2);
} else if (x < 0) {
x = 0;
}
return { x, y };
};
const getMinMaxLatitudeLongitude = (X, Y, OffsetX, OffsetY, state) => {
if (state === 'AK') return getMinMaxLatitudeLongitudeAK(X, Y, OffsetX, OffsetY);
if (state === 'HI') return getMinMaxLatitudeLongitudeHI(X, Y, OffsetX, OffsetY);
const maxLat = ((Y / 55.2) - 50.5) * -1;
const minLat = (((Y + (OffsetY * 2)) / 55.2) - 50.5) * -1;
const minLon = (((X * -1) / 41.775) + 127.5) * -1;
const maxLon = ((((X + (OffsetX * 2)) * -1) / 41.775) + 127.5) * -1;
return {
minLat, maxLat, minLon, maxLon,
};
};
const getMinMaxLatitudeLongitudeAK = (X, Y, OffsetX, OffsetY) => {
const maxLat = ((Y / 56) - 73.0) * -1;
const minLat = (((Y + (OffsetY * 2)) / 56) - 73.0) * -1;
const minLon = (((X * -1) / 25) + 175.0) * -1;
const maxLon = ((((X + (OffsetX * 2)) * -1) / 25) + 175.0) * -1;
return {
minLat, maxLat, minLon, maxLon,
};
};
const getMinMaxLatitudeLongitudeHI = (X, Y, OffsetX, OffsetY) => {
const maxLat = ((Y / 55.2) - 25) * -1;
const minLat = (((Y + (OffsetY * 2)) / 55.2) - 25) * -1;
const minLon = (((X * -1) / 41.775) + 164.5) * -1;
const maxLon = ((((X + (OffsetX * 2)) * -1) / 41.775) + 164.5) * -1;
return {
minLat, maxLat, minLon, maxLon,
};
};
const getXYForCity = (City, MaxLatitude, MinLongitude, state) => {
if (state === 'AK') getXYForCityAK(City, MaxLatitude, MinLongitude);
if (state === 'HI') getXYForCityHI(City, MaxLatitude, MinLongitude);
let x = (City.lon - MinLongitude) * 57;
let y = (MaxLatitude - City.lat) * 70;
if (y < 30) y = 30;
if (y > 282) y = 282;
if (x < 40) x = 40;
if (x > 580) x = 580;
return { x, y };
};
const getXYForCityAK = (City, MaxLatitude, MinLongitude) => {
let x = (City.lon - MinLongitude) * 37;
let y = (MaxLatitude - City.lat) * 70;
if (y < 30) y = 30;
if (y > 282) y = 282;
if (x < 40) x = 40;
if (x > 580) x = 580;
return { x, y };
};
const getXYForCityHI = (City, MaxLatitude, MinLongitude) => {
let x = (City.lon - MinLongitude) * 57;
let y = (MaxLatitude - City.lat) * 70;
if (y < 30) y = 30;
if (y > 282) y = 282;
if (x < 40) x = 40;
if (x > 580) x = 580;
return { x, y };
};
// to fit on the map, remove anything after punctuation and then limit to 15 characters
const formatCity = (city) => city.match(/[^-;/\\,]*/)[0].substr(0, 12);
export {
buildForecast,
getRegionalObservation,
getXYFromLatitudeLongitude,
getMinMaxLatitudeLongitude,
getXYForCity,
formatCity,
};

View File

@@ -10,6 +10,7 @@ import { preloadImg } from './utils/image.mjs';
import { DateTime } from '../vendor/auto/luxon.mjs';
import WeatherDisplay from './weatherdisplay.mjs';
import { registerDisplay } from './navigation.mjs';
import * as utils from './regionalforecast-utils.mjs';
class RegionalForecast extends WeatherDisplay {
constructor(navId, elemId) {
@@ -38,10 +39,10 @@ class RegionalForecast extends WeatherDisplay {
y: 117,
};
// get user's location in x/y
const sourceXY = RegionalForecast.getXYFromLatitudeLongitude(weatherParameters.latitude, weatherParameters.longitude, offsetXY.x, offsetXY.y, weatherParameters.state);
const sourceXY = utils.getXYFromLatitudeLongitude(weatherParameters.latitude, weatherParameters.longitude, offsetXY.x, offsetXY.y, weatherParameters.state);
// get latitude and longitude limits
const minMaxLatLon = RegionalForecast.getMinMaxLatitudeLongitude(sourceXY.x, sourceXY.y, offsetXY.x, offsetXY.y, weatherParameters.state);
const minMaxLatLon = utils.getMinMaxLatitudeLongitude(sourceXY.x, sourceXY.y, offsetXY.x, offsetXY.y, weatherParameters.state);
// get a target distance
let targetDistance = 2.5;
@@ -75,12 +76,12 @@ class RegionalForecast extends WeatherDisplay {
if (!city.point) throw new Error('No pre-loaded point');
// start off the observation task
const observationPromise = RegionalForecast.getRegionalObservation(city.point, city);
const observationPromise = utils.getRegionalObservation(city.point, city);
const forecast = await json(`https://api.weather.gov/gridpoints/${city.point.wfo}/${city.point.x},${city.point.y}/forecast`);
// get XY on map for city
const cityXY = RegionalForecast.getXYForCity(city, minMaxLatLon.maxLat, minMaxLatLon.minLon, weatherParameters.state);
const cityXY = utils.getXYForCity(city, minMaxLatLon.maxLat, minMaxLatLon.minLon, weatherParameters.state);
// wait for the regional observation if it's not done yet
const observation = await observationPromise;
@@ -88,7 +89,7 @@ class RegionalForecast extends WeatherDisplay {
const regionalObservation = {
daytime: !!observation.icon.match(/\/day\//),
temperature: celsiusToFahrenheit(observation.temperature.value),
name: RegionalForecast.formatCity(city.city),
name: utils.formatCity(city.city),
icon: observation.icon,
x: cityXY.x,
y: cityXY.y,
@@ -104,8 +105,8 @@ class RegionalForecast extends WeatherDisplay {
// always skip the first forecast index because it's what's going on right now
return [
regionalObservation,
RegionalForecast.buildForecast(forecast.properties.periods[1], city, cityXY),
RegionalForecast.buildForecast(forecast.properties.periods[2], city, cityXY),
utils.buildForecast(forecast.properties.periods[1], city, cityXY),
utils.buildForecast(forecast.properties.periods[2], city, cityXY),
];
} catch (e) {
console.log(`No regional forecast data for '${city.name ?? city.city}'`);
@@ -133,203 +134,6 @@ class RegionalForecast extends WeatherDisplay {
this.setStatus(STATUS.loaded);
}
static buildForecast(forecast, city, cityXY) {
return {
daytime: forecast.isDaytime,
temperature: forecast.temperature || 0,
name: RegionalForecast.formatCity(city.city),
icon: forecast.icon,
x: cityXY.x,
y: cityXY.y,
time: forecast.startTime,
};
}
static async getRegionalObservation(point, city) {
try {
// get stations
const stations = await json(`https://api.weather.gov/gridpoints/${city.point.wfo}/${city.point.x},${city.point.y}/stations`);
// get the first station
const station = stations.features[0].id;
// get the observation data
const observation = await json(`${station}/observations/latest`);
// preload the image
if (!observation.properties.icon) return false;
preloadImg(getWeatherRegionalIconFromIconLink(observation.properties.icon, !observation.properties.daytime));
// return the observation
return observation.properties;
} catch (e) {
console.log(`Unable to get regional observations for ${city.Name ?? city.city}`);
console.error(e.status, e.responseJSON);
return false;
}
}
// utility latitude/pixel conversions
static getXYFromLatitudeLongitude(Latitude, Longitude, OffsetX, OffsetY, state) {
if (state === 'AK') return RegionalForecast.getXYFromLatitudeLongitudeAK(Latitude, Longitude, OffsetX, OffsetY);
if (state === 'HI') return RegionalForecast.getXYFromLatitudeLongitudeHI(Latitude, Longitude, OffsetX, OffsetY);
let y = 0;
let x = 0;
const ImgHeight = 1600;
const ImgWidth = 2550;
y = (50.5 - Latitude) * 55.2;
y -= OffsetY; // Centers map.
// Do not allow the map to exceed the max/min coordinates.
if (y > (ImgHeight - (OffsetY * 2))) {
y = ImgHeight - (OffsetY * 2);
} else if (y < 0) {
y = 0;
}
x = ((-127.5 - Longitude) * 41.775) * -1;
x -= OffsetX; // Centers map.
// Do not allow the map to exceed the max/min coordinates.
if (x > (ImgWidth - (OffsetX * 2))) {
x = ImgWidth - (OffsetX * 2);
} else if (x < 0) {
x = 0;
}
return { x, y };
}
static getXYFromLatitudeLongitudeAK(Latitude, Longitude, OffsetX, OffsetY) {
let y = 0;
let x = 0;
const ImgHeight = 1142;
const ImgWidth = 1200;
y = (73.0 - Latitude) * 56;
y -= OffsetY; // Centers map.
// Do not allow the map to exceed the max/min coordinates.
if (y > (ImgHeight - (OffsetY * 2))) {
y = ImgHeight - (OffsetY * 2);
} else if (y < 0) {
y = 0;
}
x = ((-175.0 - Longitude) * 25.0) * -1;
x -= OffsetX; // Centers map.
// Do not allow the map to exceed the max/min coordinates.
if (x > (ImgWidth - (OffsetX * 2))) {
x = ImgWidth - (OffsetX * 2);
} else if (x < 0) {
x = 0;
}
return { x, y };
}
static getXYFromLatitudeLongitudeHI(Latitude, Longitude, OffsetX, OffsetY) {
let y = 0;
let x = 0;
const ImgHeight = 571;
const ImgWidth = 600;
y = (25 - Latitude) * 55.2;
y -= OffsetY; // Centers map.
// Do not allow the map to exceed the max/min coordinates.
if (y > (ImgHeight - (OffsetY * 2))) {
y = ImgHeight - (OffsetY * 2);
} else if (y < 0) {
y = 0;
}
x = ((-164.5 - Longitude) * 41.775) * -1;
x -= OffsetX; // Centers map.
// Do not allow the map to exceed the max/min coordinates.
if (x > (ImgWidth - (OffsetX * 2))) {
x = ImgWidth - (OffsetX * 2);
} else if (x < 0) {
x = 0;
}
return { x, y };
}
static getMinMaxLatitudeLongitude(X, Y, OffsetX, OffsetY, state) {
if (state === 'AK') return RegionalForecast.getMinMaxLatitudeLongitudeAK(X, Y, OffsetX, OffsetY);
if (state === 'HI') return RegionalForecast.getMinMaxLatitudeLongitudeHI(X, Y, OffsetX, OffsetY);
const maxLat = ((Y / 55.2) - 50.5) * -1;
const minLat = (((Y + (OffsetY * 2)) / 55.2) - 50.5) * -1;
const minLon = (((X * -1) / 41.775) + 127.5) * -1;
const maxLon = ((((X + (OffsetX * 2)) * -1) / 41.775) + 127.5) * -1;
return {
minLat, maxLat, minLon, maxLon,
};
}
static getMinMaxLatitudeLongitudeAK(X, Y, OffsetX, OffsetY) {
const maxLat = ((Y / 56) - 73.0) * -1;
const minLat = (((Y + (OffsetY * 2)) / 56) - 73.0) * -1;
const minLon = (((X * -1) / 25) + 175.0) * -1;
const maxLon = ((((X + (OffsetX * 2)) * -1) / 25) + 175.0) * -1;
return {
minLat, maxLat, minLon, maxLon,
};
}
static getMinMaxLatitudeLongitudeHI(X, Y, OffsetX, OffsetY) {
const maxLat = ((Y / 55.2) - 25) * -1;
const minLat = (((Y + (OffsetY * 2)) / 55.2) - 25) * -1;
const minLon = (((X * -1) / 41.775) + 164.5) * -1;
const maxLon = ((((X + (OffsetX * 2)) * -1) / 41.775) + 164.5) * -1;
return {
minLat, maxLat, minLon, maxLon,
};
}
static getXYForCity(City, MaxLatitude, MinLongitude, state) {
if (state === 'AK') RegionalForecast.getXYForCityAK(City, MaxLatitude, MinLongitude);
if (state === 'HI') RegionalForecast.getXYForCityHI(City, MaxLatitude, MinLongitude);
let x = (City.lon - MinLongitude) * 57;
let y = (MaxLatitude - City.lat) * 70;
if (y < 30) y = 30;
if (y > 282) y = 282;
if (x < 40) x = 40;
if (x > 580) x = 580;
return { x, y };
}
static getXYForCityAK(City, MaxLatitude, MinLongitude) {
let x = (City.lon - MinLongitude) * 37;
let y = (MaxLatitude - City.lat) * 70;
if (y < 30) y = 30;
if (y > 282) y = 282;
if (x < 40) x = 40;
if (x > 580) x = 580;
return { x, y };
}
static getXYForCityHI(City, MaxLatitude, MinLongitude) {
let x = (City.lon - MinLongitude) * 57;
let y = (MaxLatitude - City.lat) * 70;
if (y < 30) y = 30;
if (y > 282) y = 282;
if (x < 40) x = 40;
if (x > 580) x = 580;
return { x, y };
}
// to fit on the map, remove anything after punctuation and then limit to 15 characters
static formatCity(city) {
return city.match(/[^-;/\\,]*/)[0].substr(0, 12);
}
drawCanvas() {
super.drawCanvas();
// break up data into useful values

View File

@@ -4,6 +4,7 @@ const STATUS = {
failed: Symbol('failed'),
noData: Symbol('noData'),
disabled: Symbol('disabled'),
retrying: Symbol('retyring'),
};
export default STATUS;

View File

@@ -112,7 +112,7 @@ class TravelForecast extends WeatherDisplay {
// set up variables
const cities = this.data;
this.elem.querySelector('.header .title.dual .bottom').innerHTML = `For ${TravelForecast.getTravelCitiesDayName(cities)}`;
this.elem.querySelector('.header .title.dual .bottom').innerHTML = `For ${getTravelCitiesDayName(cities)}`;
this.finishDraw();
}
@@ -140,24 +140,22 @@ class TravelForecast extends WeatherDisplay {
this.elem.querySelector('.main').scrollTo(0, offsetY);
}
static getTravelCitiesDayName(cities) {
// effectively returns early on the first found date
return cities.reduce((dayName, city) => {
if (city && dayName === '') {
// today or tomorrow
const day = DateTime.local().plus({ days: (city.today) ? 0 : 1 });
// return the day
return day.toLocaleString({ weekday: 'long' });
}
return dayName;
}, '');
}
// necessary to get the lastest long canvas when scrolling
getLongCanvas() {
return this.longCanvas;
}
}
// effectively returns early on the first found date
const getTravelCitiesDayName = (cities) => cities.reduce((dayName, city) => {
if (city && dayName === '') {
// today or tomorrow
const day = DateTime.local().plus({ days: (city.today) ? 0 : 1 });
// return the day
return day.toLocaleString({ weekday: 'long' });
}
return dayName;
}, '');
// register display, not active by default
registerDisplay(new TravelForecast(4, 'travel', false));

View File

@@ -11,9 +11,13 @@ const fetchAsync = async (_url, responseType, _params = {}) => {
method: 'GET',
mode: 'cors',
type: 'GET',
retryCount: 0,
..._params,
};
// build a url, including the rewrite for cors if necessary
// store original number of retries
params.originalRetries = params.retryCount;
// build a url, including the rewrite for cors if necessary
let corsUrl = _url;
if (params.cors === true) corsUrl = rewriteUrl(_url);
const url = new URL(corsUrl, `${window.location.origin}/`);
@@ -30,7 +34,7 @@ const fetchAsync = async (_url, responseType, _params = {}) => {
}
// make the request
const response = await fetch(url, params);
const response = await doFetch(url, params);
// check for ok response
if (!response.ok) throw new Error(`Fetch error ${response.status} ${response.statusText} while fetching ${response.url}`);
@@ -47,6 +51,48 @@ const fetchAsync = async (_url, responseType, _params = {}) => {
}
};
// fetch with retry and back-off
const doFetch = (url, params) => new Promise((resolve, reject) => {
fetch(url, params).then((response) => {
if (params.retryCount > 0) {
// 500 status codes should be retried after a short backoff
if (response.status >= 500 && response.status <= 599 && params.retryCount > 0) {
// call the "still waiting" function
if (typeof params.stillWaiting === 'function' && params.retryCount === params.originalRetries) {
params.stillWaiting();
}
// decrement and retry
const newParams = {
...params,
retryCount: params.retryCount - 1,
};
return resolve(delay(retryDelay(params.originalRetries - newParams.retryCount), doFetch, url, newParams));
}
// not 500 status
return resolve(response);
}
// out of retries
return resolve(response);
})
.catch((e) => reject(e));
});
const delay = (time, func, ...args) => new Promise((resolve) => {
setTimeout(() => {
resolve(func(...args));
}, time);
});
const retryDelay = (retryNumber) => {
switch (retryNumber) {
case 1: return 1000;
case 2: return 2000;
case 3: return 5000;
case 4: return 10000;
default: return 30000;
}
};
export {
json,
text,

View File

@@ -1,6 +1,6 @@
// *********************************** unit conversions ***********************
const round2 = (value, decimals) => Number(`${Math.round(`${value}e${decimals}`)}e-${decimals}`);
const round2 = (value, decimals) => Math.trunc(value * 10 ** decimals) / 10 ** decimals;
const kphToMph = (Kph) => Math.round(Kph / 1.60934);
const celsiusToFahrenheit = (Celsius) => Math.round((Celsius * 9) / 5 + 32);
@@ -14,4 +14,5 @@ export {
kilometersToMiles,
metersToFeet,
pascalToInHg,
round2,
};

View File

@@ -17,6 +17,7 @@ class WeatherDisplay {
this.loadingStatus = STATUS.loading;
this.name = name ?? elemId;
this.getDataCallbacks = [];
this.stillWaitingCallbacks = [];
this.defaultEnabled = defaultEnabled;
this.okToDrawCurrentConditions = true;
this.okToDrawCurrentDateTime = true;
@@ -51,7 +52,7 @@ class WeatherDisplay {
if (this.elemId === 'progress') return false;
// get the saved status of the checkbox
let savedStatus = window.localStorage.getItem(`${this.elemId}Enabled`);
let savedStatus = window.localStorage.getItem(`display-enabled: ${this.elemId}`);
if (savedStatus === null) savedStatus = defaultEnabled;
if (savedStatus === 'true' || savedStatus === true) {
this.enabled = true;
@@ -60,7 +61,7 @@ class WeatherDisplay {
}
// refresh (or initially store the state of the checkbox)
window.localStorage.setItem(`${this.elemId}Enabled`, this.enabled);
window.localStorage.setItem(`display-enabled: ${this.elemId}`, this.enabled);
// create a checkbox in the selected displays area
const checkbox = document.createElement('template');
@@ -76,7 +77,7 @@ class WeatherDisplay {
// update the state
this.enabled = e.target.checked;
// store the value for the next load
window.localStorage.setItem(`${this.elemId}Enabled`, this.enabled);
window.localStorage.setItem(`display-enabled: ${this.elemId}`, this.enabled);
// calling get data will update the status and actually get the data if we're set to enabled
this.getData();
}
@@ -392,6 +393,14 @@ class WeatherDisplay {
return template;
}
// still waiting for data (retries triggered)
stillWaiting() {
if (this.enabled) this.setStatus(STATUS.retrying);
// handle still waiting callbacks
this.stillWaitingCallbacks.forEach((callback) => callback());
this.stillWaitingCallbacks = [];
}
}
export default WeatherDisplay;

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -31,6 +31,7 @@ button {
#txtAddress {
width: 490px;
font-size: 16pt;
max-width: calc(100% - 8px);
@media (prefers-color-scheme: dark) {
background-color: #000000;
@@ -72,7 +73,10 @@ button {
.autocomplete-suggestions {
background-color: #ffffff;
border: 1px solid #000000;
/*overflow: auto;*/
@media (prefers-color-scheme: dark) {
background-color: #000000;
}
}
.autocomplete-suggestion {

View File

@@ -37,7 +37,8 @@
padding-left: 4px;
}
.loading {
.loading,
.retrying {
color: #ffff00;
}
@@ -58,23 +59,12 @@
color: #C0C0C0;
}
&.loading .loading {
display: block;
}
&.press-here .press-here {
display: block;
}
&.failed .failed {
display: block;
}
&.no-data .no-data {
display: block;
}
&.disabled .disabled {
&.loading .loading,
&.press-here .press-here,
&.failed .failed,
&.no-data .no-data,
&.disabled .disabled,
&.retrying .retrying {
display: block;
}

View File

@@ -9,8 +9,7 @@
<meta name="keywords" content="WeatherStar 4000+" />
<meta name="author" content="Matt Walsh" />
<meta name="application-name" content="WeatherStar 4000+" />
<meta name="viewport" content="width=device-width,initial-scale=1">
<meta name="viewport" content="width=device-width,initial-scale=1;maximum-scale=1;minimum-scale=1">
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<link rel="manifest" href="manifest.json" />

View File

@@ -9,6 +9,7 @@
<div class="failed">Failed</div>
<div class="no-data">No Data</div>
<div class="disabled">Disabled</div>
<div class="retrying">Retrying</div>
</div>
</div>
</div>

View File

@@ -50,5 +50,10 @@
"[html]": {
"editor.defaultFormatter": "j69.ejs-beautify"
},
"files.exclude": {
"**/node_modules": true,
"**/debug.log": true,
"server/scripts/custom.js": true
},
},
}