reorganize for build system

This commit is contained in:
Matt Walsh
2020-09-04 13:02:20 -05:00
commit 8bc7a7dd95
477 changed files with 49411 additions and 0 deletions

View File

@@ -0,0 +1,66 @@
var _TimerInfos = [];
var TimerElasped = function (Id)
{
var TimerInfo = _TimerInfos[Id];
if (!TimerInfo)
{
return;
}
this.postMessage({
Action: "ELASPED",
Id: TimerInfo.Id,
RunOnce: TimerInfo.RunOnce,
});
};
this.onmessage = function (e)
{
var Message = e.data;
switch (Message.Action)
{
case "START":
var TimerInfo = {
Id: Message.Id,
RunOnce: Message.RunOnce,
TimeOut: Message.TimeOut,
};
_TimerInfos[Message.Id] = TimerInfo;
if (Message.RunOnce == true)
{
TimerInfo.TimerId = setTimeout(TimerElasped, Message.TimeOut, Message.Id);
}
else
{
TimerInfo.TimerId = setInterval(TimerElasped, Message.TimeOut, Message.Id);
}
break;
case "STOP":
var TimerInfo = _TimerInfos[Message.Id];
if (!TimerInfo)
{
return;
}
if (TimerInfo.RunOnce == true)
{
clearTimeout(TimerInfo.TimerId);
delete _TimerInfos[Message.Id];
}
else
{
clearInterval(TimerInfo.TimerId);
delete _TimerInfos[Message.Id];
}
break;
}
}

View File

@@ -0,0 +1,584 @@
// eslint-disable-next-line no-unused-vars
const _RegionalCities = [
{
Name: 'Atlanta',
Latitude: 33.749,
Longitude: -84.388,
},
{
Name: 'Boston',
Latitude: 42.3584,
Longitude: -71.0598,
},
{
Name: 'Chicago',
Latitude: 41.9796,
Longitude: -87.9045,
},
{
Name: 'Cleveland',
Latitude: 41.4995,
Longitude: -81.6954,
},
{
Name: 'Dallas',
Latitude: 32.8959,
Longitude: -97.0372,
},
{
Name: 'Denver',
Latitude: 39.7391,
Longitude: -104.9847,
},
{
Name: 'Detroit',
Latitude: 42.3314,
Longitude: -83.0457,
},
{
Name: 'Hartford',
Latitude: 41.7637,
Longitude: -72.6851,
},
{
Name: 'Houston',
Latitude: 29.7633,
Longitude: -95.3633,
},
{
Name: 'Indianapolis',
Latitude: 39.7684,
Longitude: -86.158,
},
{
Name: 'Los Angeles',
Latitude: 34.0522,
Longitude: -118.2437,
},
{
Name: 'Miami',
Latitude: 25.7743,
Longitude: -80.1937,
},
{
Name: 'Minneapolis',
Latitude: 44.98,
Longitude: -93.2638,
},
{
Name: 'New York',
Latitude: 40.78,
Longitude: -73.88,
},
{
Name: 'Norfolk',
Latitude: 36.8468,
Longitude: -76.2852,
},
{
Name: 'Orlando',
Latitude: 28.5383,
Longitude: -81.3792,
},
{
Name: 'Philadelphia',
Latitude: 39.9523,
Longitude: -75.1638,
},
{
Name: 'Pittsburgh',
Latitude: 40.4406,
Longitude: -79.9959,
},
{
Name: 'St. Louis',
Latitude: 38.6273,
Longitude: -90.1979,
},
{
Name: 'San Francisco',
Latitude: 37.6148,
Longitude: -122.3918,
},
{
Name: 'Seattle',
Latitude: 47.6062,
Longitude: -122.3321,
},
{
Name: 'Syracuse',
Latitude: 43.0481,
Longitude: -76.1474,
},
{
Name: 'Tampa',
Latitude: 27.9756,
Longitude: -82.5329,
},
{
Name: 'Washington DC',
Latitude: 38.8951,
Longitude: -77.0364,
},
{
Name: 'Albany',
Latitude: 42.6526,
Longitude: -73.7562,
},
{
Name: 'Albuquerque',
Latitude: 35.0845,
Longitude: -106.6511,
},
{
Name: 'Amarillo',
Latitude: 35.222,
Longitude: -101.8313,
},
{
Name: 'Anchorage',
Latitude: 61.2181,
Longitude: -149.9003,
},
{
Name: 'Austin',
Latitude: 30.2671,
Longitude: -97.7431,
},
{
Name: 'Baker',
Latitude: 44.7502,
Longitude: -117.6677,
},
{
Name: 'Baltimore',
Latitude: 39.2904,
Longitude: -76.6122,
},
{
Name: 'Bangor',
Latitude: 44.8012,
Longitude: -68.7778,
},
{
Name: 'Birmingham',
Latitude: 33.5207,
Longitude: -86.8025,
},
{
Name: 'Bismarck',
Latitude: 46.8083,
Longitude: -100.7837,
},
{
Name: 'Boise',
Latitude: 43.6135,
Longitude: -116.2034,
},
{
Name: 'Buffalo',
Latitude: 42.8864,
Longitude: -78.8784,
},
{
Name: 'Carlsbad',
Latitude: 32.4207,
Longitude: -104.2288,
},
{
Name: 'Charleston',
Latitude: 32.7766,
Longitude: -79.9309,
},
{
Name: 'Charleston',
Latitude: 38.3498,
Longitude: -81.6326,
},
{
Name: 'Charlotte',
Latitude: 35.2271,
Longitude: -80.8431,
},
{
Name: 'Cheyenne',
Latitude: 41.14,
Longitude: -104.8202,
},
{
Name: 'Cincinnati',
Latitude: 39.162,
Longitude: -84.4569,
},
{
Name: 'Columbia',
Latitude: 34.0007,
Longitude: -81.0348,
},
{
Name: 'Columbus',
Latitude: 39.9612,
Longitude: -82.9988,
},
{
Name: 'Des Moines',
Latitude: 41.6005,
Longitude: -93.6091,
},
{
Name: 'Dubuque',
Latitude: 42.5006,
Longitude: -90.6646,
},
{
Name: 'Duluth',
Latitude: 46.7833,
Longitude: -92.1066,
},
{
Name: 'Eastport',
Latitude: 44.9062,
Longitude: -66.99,
},
{
Name: 'El Centro',
Latitude: 32.792,
Longitude: -115.563,
},
{
Name: 'El Paso',
Latitude: 31.7587,
Longitude: -106.4869,
},
{
Name: 'Eugene',
Latitude: 44.0521,
Longitude: -123.0867,
},
{
Name: 'Fargo',
Latitude: 46.8772,
Longitude: -96.7898,
},
{
Name: 'Flagstaff',
Latitude: 35.1981,
Longitude: -111.6513,
},
{
Name: 'Fresno',
Latitude: 36.7477,
Longitude: -119.7724,
},
{
Name: 'Grand Junction',
Latitude: 39.0639,
Longitude: -108.5506,
},
{
Name: 'Grand Rapids',
Latitude: 42.9634,
Longitude: -85.6681,
},
{
Name: 'Havre',
Latitude: 48.55,
Longitude: -109.6841,
},
{
Name: 'Helena',
Latitude: 46.5927,
Longitude: -112.0361,
},
{
Name: 'Honolulu',
Latitude: 21.3069,
Longitude: -157.8583,
},
{
Name: 'Hot Springs',
Latitude: 34.5037,
Longitude: -93.0552,
},
{
Name: 'Idaho Falls',
Latitude: 43.4666,
Longitude: -112.0341,
},
{
Name: 'Jackson',
Latitude: 32.2988,
Longitude: -90.1848,
},
{
Name: 'Jacksonville',
Latitude: 30.3322,
Longitude: -81.6556,
},
{
Name: 'Juneau',
Latitude: 58.3019,
Longitude: -134.4197,
},
{
Name: 'Kansas City',
Latitude: 39.1142,
Longitude: -94.6275,
},
{
Name: 'Key West',
Latitude: 24.5557,
Longitude: -81.7826,
},
{
Name: 'Klamath Falls',
Latitude: 42.2249,
Longitude: -121.7817,
},
{
Name: 'Knoxville',
Latitude: 35.9606,
Longitude: -83.9207,
},
{
Name: 'Las Vegas',
Latitude: 36.175,
Longitude: -115.1372,
},
{
Name: 'Lewiston',
Latitude: 46.4165,
Longitude: -117.0177,
},
{
Name: 'Lincoln',
Latitude: 40.8,
Longitude: -96.667,
},
{
Name: 'Long Beach',
Latitude: 33.767,
Longitude: -118.1892,
},
{
Name: 'Louisville',
Latitude: 38.2542,
Longitude: -85.7594,
},
{
Name: 'Manchester',
Latitude: 42.9956,
Longitude: -71.4548,
},
{
Name: 'Memphis',
Latitude: 35.1495,
Longitude: -90.049,
},
{
Name: 'Milwaukee',
Latitude: 43.0389,
Longitude: -87.9065,
},
{
Name: 'Mobile',
Latitude: 30.6944,
Longitude: -88.043,
},
{
Name: 'Montgomery',
Latitude: 32.3668,
Longitude: -86.3,
},
{
Name: 'Montpelier',
Latitude: 44.2601,
Longitude: -72.5754,
},
{
Name: 'Nashville',
Latitude: 36.1659,
Longitude: -86.7844,
},
{
Name: 'Newark',
Latitude: 40.7357,
Longitude: -74.1724,
},
{
Name: 'New Haven',
Latitude: 41.3081,
Longitude: -72.9282,
},
{
Name: 'New Orleans',
Latitude: 29.9546,
Longitude: -90.0751,
},
{
Name: 'Nome',
Latitude: 64.5011,
Longitude: -165.4064,
},
{
Name: 'Oklahoma City',
Latitude: 35.4676,
Longitude: -97.5164,
},
{
Name: 'Omaha',
Latitude: 41.2586,
Longitude: -95.9378,
},
{
Name: 'Phoenix',
Latitude: 33.4484,
Longitude: -112.074,
},
{
Name: 'Pierre',
Latitude: 44.3683,
Longitude: -100.351,
},
{
Name: 'Portland',
Latitude: 43.6615,
Longitude: -70.2553,
},
{
Name: 'Portland',
Latitude: 45.5234,
Longitude: -122.6762,
},
{
Name: 'Providence',
Latitude: 41.824,
Longitude: -71.4128,
},
{
Name: 'Raleigh',
Latitude: 35.7721,
Longitude: -78.6386,
},
{
Name: 'Reno',
Latitude: 39.4986,
Longitude: -119.7681,
},
{
Name: 'Richfield',
Latitude: 38.7725,
Longitude: -112.0841,
},
{
Name: 'Richmond',
Latitude: 37.5538,
Longitude: -77.4603,
},
{
Name: 'Roanoke',
Latitude: 37.271,
Longitude: -79.9414,
},
{
Name: 'Sacramento',
Latitude: 38.5816,
Longitude: -121.4944,
},
{
Name: 'Salt Lake City',
Latitude: 40.7608,
Longitude: -111.891,
},
{
Name: 'San Antonio',
Latitude: 29.4241,
Longitude: -98.4936,
},
{
Name: 'San Diego',
Latitude: 32.7153,
Longitude: -117.1573,
},
{
Name: 'San Jose',
Latitude: 37.3394,
Longitude: -121.895,
},
{
Name: 'Santa Fe',
Latitude: 35.687,
Longitude: -105.9378,
},
{
Name: 'Savannah',
Latitude: 32.0835,
Longitude: -81.0998,
},
{
Name: 'Shreveport',
Latitude: 32.5251,
Longitude: -93.7502,
},
{
Name: 'Sioux Falls',
Latitude: 43.55,
Longitude: -96.7003,
},
{
Name: 'Sitka',
Latitude: 57.0531,
Longitude: -135.33,
},
{
Name: 'Spokane',
Latitude: 47.6597,
Longitude: -117.4291,
},
{
Name: 'Springfield',
Latitude: 39.8017,
Longitude: -89.6437,
},
{
Name: 'Springfield',
Latitude: 42.1015,
Longitude: -72.5898,
},
{
Name: 'Springfield',
Latitude: 37.2153,
Longitude: -93.2982,
},
{
Name: 'Toledo',
Latitude: 41.6639,
Longitude: -83.5552,
},
{
Name: 'Tulsa',
Latitude: 36.154,
Longitude: -95.9928,
},
{
Name: 'Virginia Beach',
Latitude: 36.8529,
Longitude: -75.978,
},
{
Name: 'Wichita',
Latitude: 37.6922,
Longitude: -97.3375,
},
{
Name: 'Wilmington',
Latitude: 34.2257,
Longitude: -77.9447,
},
{
Name: 'Tuscan',
Latitude: 32.2216,
Longitude: -110.9698,
},
];

View File

@@ -0,0 +1,60 @@
// eslint-disable-next-line no-unused-vars
const states = (() => {
const stateList = {
'Arizona': 'AZ',
'Alabama': 'AL',
'Alaska': 'AK',
'Arkansas': 'AR',
'California': 'CA',
'Colorado': 'CO',
'Connecticut': 'CT',
'Delaware': 'DE',
'Florida': 'FL',
'Georgia': 'GA',
'Hawaii': 'HI',
'Idaho': 'ID',
'Illinois': 'IL',
'Indiana': 'IN',
'Iowa': 'IA',
'Kansas': 'KS',
'Kentucky': 'KY',
'Louisiana': 'LA',
'Maine': 'ME',
'Maryland': 'MD',
'Massachusetts': 'MA',
'Michigan': 'MI',
'Minnesota': 'MN',
'Mississippi': 'MS',
'Missouri': 'MO',
'Montana': 'MT',
'Nebraska': 'NE',
'Nevada': 'NV',
'New Hampshire': 'NH',
'New Jersey': 'NJ',
'New Mexico': 'NM',
'New York': 'NY',
'North Carolina': 'NC',
'North Dakota': 'ND',
'Ohio': 'OH',
'Oklahoma': 'OK',
'Oregon': 'OR',
'Pennsylvania': 'PA',
'Rhode Island': 'RI',
'South Carolina': 'SC',
'South Dakota': 'SD',
'Tennessee': 'TN',
'Texas': 'TX',
'Utah': 'UT',
'Vermont': 'VT',
'Virginia': 'VA',
'Washington': 'WA',
'West Virginia': 'WV',
'Wisconsin': 'WI',
'Wyoming': 'WY',
};
return {
getTwoDigitCode: (stateFullName) => stateList[stateFullName],
};
})();

View File

@@ -0,0 +1,149 @@
//Atlanta
//Boston
//Chicago
//Cleveland
//Dallas
//Denver
//Detroit
//Hartford
//Houston
//Indianapolis
//Los Angeles
//Miami
//Minneapolis
//New York
//Norfolk
//Orlando
//Philadelphia
//Pittsburgh
//St. Louis
//San Francisco
//Seattle
//Syracuse
//Tampa
//Washington DC
// eslint-disable-next-line no-unused-vars
const _TravelCities = [
{
Name: 'Atlanta',
Latitude: 33.749,
Longitude: -84.388,
},
{
Name: 'Boston',
Latitude: 42.3584,
Longitude: -71.0598,
},
{
Name: 'Chicago',
Latitude: 41.9796,
Longitude: -87.9045,
},
{
Name: 'Cleveland',
Latitude: 41.4995,
Longitude: -81.6954,
},
{
Name: 'Dallas',
Latitude: 32.8959,
Longitude: -97.0372,
},
{
Name: 'Denver',
Latitude: 39.7391,
Longitude: -104.9847,
},
{
Name: 'Detroit',
Latitude: 42.3314,
Longitude: -83.0457,
},
{
Name: 'Hartford',
Latitude: 41.7637,
Longitude: -72.6851,
},
{
Name: 'Houston',
Latitude: 29.7633,
Longitude: -95.3633,
},
{
Name: 'Indianapolis',
Latitude: 39.7684,
Longitude: -86.158,
},
{
Name: 'Los Angeles',
Latitude: 34.0522,
Longitude: -118.2437,
},
{
Name: 'Miami',
Latitude: 25.7743,
Longitude: -80.1937,
},
{
Name: 'Minneapolis',
Latitude: 44.98,
Longitude: -93.2638,
},
{
Name: 'New York',
Latitude: 40.7142,
Longitude: -74.0059,
},
{
Name: 'Norfolk',
Latitude: 36.8468,
Longitude: -76.2852,
},
{
Name: 'Orlando',
Latitude: 28.5383,
Longitude: -81.3792,
},
{
Name: 'Philadelphia',
Latitude: 39.9523,
Longitude: -75.1638,
},
{
Name: 'Pittsburgh',
Latitude: 40.4406,
Longitude: -79.9959,
},
{
Name: 'St. Louis',
Latitude: 38.6273,
Longitude: -90.1979,
},
{
Name: 'San Francisco',
Latitude: 37.7749,
Longitude: -122.4194,
},
{
Name: 'Seattle',
Latitude: 47.6062,
Longitude: -122.3321,
},
{
Name: 'Syracuse',
Latitude: 43.0481,
Longitude: -76.1474,
},
{
Name: 'Tampa',
Latitude: 27.9475,
Longitude: -82.4584,
},
{
Name: 'Washington DC',
Latitude: 38.8951,
Longitude: -77.0364,
},
];

File diff suppressed because it is too large Load Diff

956
server/scripts/index.js Normal file
View File

@@ -0,0 +1,956 @@
/* globals NoSleep, states */
let frmGetLatLng;
let txtAddress;
let btnClearQuery;
let btnGetGps;
let divTwc;
let divTwcTop;
let divTwcMiddle;
let divTwcBottom;
let divTwcLeft;
let divTwcRight;
let divTwcNavContainer;
let iframeTwc;
let spanLastRefresh;
let chkAutoRefresh;
let spanRefreshCountDown;
let spanCity;
let spanState;
let spanStationId;
let spanRadarId;
let spanZoneId;
let frmScrollText;
let chkScrollText;
let txtScrollText;
let _AutoSelectQuery = false;
let _TwcDataUrl = '';
let _IsPlaying = false;
let _NoSleep = new NoSleep();
let _LastUpdate = null;
let _AutoRefreshIntervalId = null;
let _AutoRefreshIntervalMs = 500;
let _AutoRefreshTotalIntervalMs = 600000; // 10 min.
let _AutoRefreshCountMs = 0;
let _FullScreenOverride = false;
let _WindowHeight = 0;
let _WindowWidth = 0;
let latLon;
let _canvasIds = [
'canvasProgress',
'canvasCurrentWeather',
'canvasLatestObservations',
'canvasTravelForecast',
'canvasRegionalForecast1',
'canvasRegionalForecast2',
'canvasRegionalObservations',
'canvasLocalForecast',
'canvasExtendedForecast1',
'canvasExtendedForecast2',
'canvasAlmanac',
'canvasAlmanacTides',
'canvasOutlook',
'canvasMarineForecast',
'canvasAirQuality',
'canvasLocalRadar',
'canvasHazards',
];
const FullScreenResize = () => {
const iframeDoc = $(iframeTwc[0].contentWindow.document);
const WindowWidth = $(window).width();
const WindowHeight = $(window).height();
const inFullScreen = InFullScreen();
let NewWidth;
let NewHeight;
let IFrameWidth;
let IFrameHeight;
let LeftWidth;
let RightWidth;
let TopHeight;
let BottomHeight;
let Offset;
if (inFullScreen) {
if ((WindowWidth / WindowHeight) >= 1.583333333333333) {
NewHeight = WindowHeight + 'px';
NewWidth = '';
divTwcTop.hide();
divTwcBottom.hide();
divTwcLeft.show();
divTwcRight.show();
divTwcMiddle.attr('style', 'width:100%; height:100%; border:none; margin:0; padding:0; overflow:hidden; z-index:999999;');
LeftWidth = ((WindowWidth - (WindowHeight * 1.33333333333333333333)) / 2);
if (LeftWidth < 60) {
LeftWidth = 60;
}
divTwcLeft.attr('style', 'width:' + LeftWidth + 'px; height:100%; border:none; margin:0; padding:0; overflow:hidden; z-index:999999;');
divTwcLeft.css('visibility', 'visible');
RightWidth = ((WindowWidth - (WindowHeight * 1.33333333333333333333)) / 2);
if (RightWidth < 60) {
RightWidth = 60;
}
divTwcRight.attr('style', 'width:' + RightWidth + 'px; height:100%; border:none; margin:0; padding:0; overflow:hidden; z-index:999999;');
divTwcRight.css('visibility', 'visible');
IFrameWidth = WindowWidth - LeftWidth - RightWidth;
NewWidth = IFrameWidth + 'px';
iframeTwc.attr('style', 'width:' + IFrameWidth + 'px; height:100%; border:none; margin:0; padding:0; overflow:hidden; z-index:999999;');
} else {
NewHeight = '';
NewWidth = WindowWidth + 'px';
divTwcTop.show();
divTwcBottom.show();
divTwcLeft.hide();
divTwcRight.hide();
Offset = 0;
TopHeight = ((WindowHeight - ((WindowWidth - Offset) * 0.75)) / 2);
if (TopHeight < 0) {
TopHeight = 0;
}
divTwcTop.attr('style', 'width:100%; height:' + TopHeight + 'px; border:none; margin:0; padding:0; overflow:hidden; z-index:999999;');
BottomHeight = ((WindowHeight - ((WindowWidth - Offset) * 0.75)) / 2);
if (BottomHeight < 30) {
BottomHeight = 30;
}
divTwcBottom.attr('style', 'width:100%; height:' + BottomHeight + 'px; border:none; margin:0; padding:0; overflow:hidden; z-index:999999;');
divTwcBottom.css('visibility', 'visible');
IFrameHeight = WindowHeight - TopHeight - BottomHeight;
NewHeight = IFrameHeight + 'px';
iframeTwc.attr('style', 'width:100%; height:' + IFrameHeight + 'px; border:none; margin:0; padding:0; overflow:hidden; z-index:999999;');
divTwcMiddle.attr('style', 'width:100%; height:' + IFrameHeight + 'px; border:none; margin:0; padding:0; overflow:hidden; z-index:999999;');
}
}
if (!inFullScreen) {
NewHeight = '';
NewWidth = '';
divTwcTop.hide();
divTwcBottom.hide();
divTwcLeft.hide();
divTwcRight.hide();
divTwc.attr('style', '');
divTwcMiddle.attr('style', '');
iframeTwc.attr('style', '');
$(window).off('resize', FullScreenResize);
}
$(_canvasIds).each(function () {
const canvas = iframeDoc.find('#' + this.toString());
canvas.css('width', NewWidth);
canvas.css('height', NewHeight);
});
if (inFullScreen) {
$('body').css('overflow', 'hidden');
$('.ToggleFullScreen').val('Exit Full Screen');
if (!GetFullScreenElement()) {
EnterFullScreen();
}
} else {
$('body').css('overflow', '');
$('.ToggleFullScreen').val('Full Screen');
}
divTwcNavContainer.show();
};
const _lockOrientation = screen.lockOrientation || screen.mozLockOrientation || screen.msLockOrientation;
const _unlockOrientation = screen.unlockOrientation || screen.mozUnlockOrientation || screen.msUnlockOrientation || (screen.orientation && screen.orientation.unlock);
const OnFullScreen = () => {
if (InFullScreen()) {
divTwc.attr('style', 'position:fixed; top:0px; left:0px; bottom:0px; right:0px; width:100%; height:100%; border:none; margin:0; padding:0; overflow:hidden; z-index:999999;');
FullScreenResize();
$(window).on('resize', FullScreenResize);
//FullScreenResize();
if (_lockOrientation) try { _lockOrientation('landscape-primary'); } catch (ex) { console.log('Unable to lock screen orientation.'); }
} else {
divTwc.attr('style', '');
divTwcMiddle.attr('style', '');
iframeTwc.attr('style', '');
$(window).off('resize', FullScreenResize);
FullScreenResize();
if (_unlockOrientation) try { _unlockOrientation(); } catch (ex) { console.log('Unable to unlock screen orientation.'); }
}
};
const InFullScreen = () => ((_FullScreenOverride) || (GetFullScreenElement()) || (window.innerHeight === screen.height) || (window.innerHeight === (screen.height - 1)));
const GetFullScreenElement = () => {
if (_FullScreenOverride) return document.body;
return (document.fullScreenElement || document.mozFullScreenElement || document.webkitFullscreenElement || document.msFullscreenElement);
};
const btnFullScreen_click = () => {
if (!InFullScreen()) {
EnterFullScreen();
} else {
ExitFullscreen();
}
if (_IsPlaying) {
_NoSleep.enable();
} else {
_NoSleep.disable();
}
UpdateFullScreenNavigate();
return false;
};
const EnterFullScreen = () => {
const element = document.body;
// Supports most browsers and their versions.
const requestMethod = element.requestFullScreen || element.webkitRequestFullScreen || element.mozRequestFullScreen || element.msRequestFullscreen;
if (requestMethod) {
// Native full screen.
requestMethod.call(element, { navigationUI: 'hide' }); // https://bugs.chromium.org/p/chromium/issues/detail?id=933436#c7
} else {
// iOS doesn't support FullScreen API.
window.scrollTo(0, 0);
_FullScreenOverride = true;
$(window).resize();
}
UpdateFullScreenNavigate();
};
const ExitFullscreen = () => {
// exit full-screen
if (_FullScreenOverride) {
_FullScreenOverride = false;
$(window).resize();
}
if (document.exitFullscreen) {
// Chrome 71 broke this if the user pressed F11 to enter full screen mode.
document.exitFullscreen();
} else if (document.webkitExitFullscreen) {
document.webkitExitFullscreen();
} else if (document.mozCancelFullScreen) {
document.mozCancelFullScreen();
} else if (document.msExitFullscreen) {
document.msExitFullscreen();
}
};
const btnNavigateMenu_click = () => {
postMessage('navButton', 'menu');
UpdateFullScreenNavigate();
return false;
};
const LoadTwcData = () => {
txtAddress.blur();
StopAutoRefreshTimer();
_LastUpdate = null;
AssignLastUpdate();
postMessage('latLon', latLon);
iframeTwc.off('load');
FullScreenResize();
if (chkScrollText.is(':checked')) {
postMessage('assignScrollText', txtScrollText.val());
}
postMessage('units', $('input[type=\'radio\'][name=\'radUnits\']:checked').val());
if (_IsPlaying) postMessage('navButton', 'playToggle');
$(iframeTwc[0].contentWindow.document).on('mousemove', document_mousemove);
$(iframeTwc[0].contentWindow.document).on('mousedown', document_mousemove);
$(iframeTwc[0].contentWindow.document).on('keydown', document_keydown);
const SwipeCallBack = (event, direction) => {
switch (direction) {
case 'left':
btnNavigateNext_click();
break;
case 'right':
default:
btnNavigatePrevious_click();
break;
}
};
$(iframeTwc[0].contentWindow.document).swipe({
//Generic swipe handler for all directions
swipeRight: SwipeCallBack,
swipeLeft: SwipeCallBack,
});
};
const AssignLastUpdate = () => {
let LastUpdate = '(None)';
if (_LastUpdate) {
switch ($('input[type=\'radio\'][name=\'radUnits\']:checked').val()) {
case 'ENGLISH':
LastUpdate = _LastUpdate.toLocaleString('en-US', { weekday: 'short', month: 'short', day: 'numeric', year: 'numeric', hour: 'numeric', minute: 'numeric', second: 'numeric', timeZoneName: 'short' });
break;
default:
LastUpdate = _LastUpdate.toLocaleString('en-GB', { weekday: 'short', month: 'short', day: 'numeric', year: 'numeric', hour: 'numeric', minute: 'numeric', second: 'numeric', timeZoneName: 'short' });
break;
}
}
spanLastRefresh.html(LastUpdate);
if (_LastUpdate && chkAutoRefresh.is(':checked')) StartAutoRefreshTimer();
};
const btnNavigateRefresh_click = () => {
LoadTwcData(_TwcDataUrl);
UpdateFullScreenNavigate();
return false;
};
const btnNavigateNext_click = () => {
postMessage('navButton', 'next');
UpdateFullScreenNavigate();
return false;
};
const btnNavigatePrevious_click = () => {
postMessage('navButton', 'previous');
UpdateFullScreenNavigate();
return false;
};
const window_resize = () => {
const $window = $(window);
if ($window.height() === _WindowHeight || $window.width() === _WindowWidth) return;
_WindowHeight = $window.height();
_WindowWidth = $window.width();
try {
postMessage('navButton', 'reset');
} catch (ex) {
console.log(ex);
}
UpdateFullScreenNavigate();
};
let _NavigateFadeIntervalId = null;
const UpdateFullScreenNavigate = () => {
$(document.activeElement).blur();
$('body').removeClass('HideCursor');
$(iframeTwc[0].contentWindow.document).find('body').removeClass('HideCursor');
divTwcLeft.fadeIn2();
divTwcRight.fadeIn2();
divTwcBottom.fadeIn2();
if (_NavigateFadeIntervalId) {
window.clearTimeout(_NavigateFadeIntervalId);
_NavigateFadeIntervalId = null;
}
_NavigateFadeIntervalId = window.setTimeout(() => {
//console.log("window_mousemove: TimeOut");
if (InFullScreen()) {
$('body').addClass('HideCursor');
$(iframeTwc[0].contentWindow.document).find('body').addClass('HideCursor');
divTwcLeft.fadeOut2();
divTwcRight.fadeOut2();
divTwcBottom.fadeOut2();
}
}, 2000);
};
const document_mousemove = (e) => {
if (InFullScreen() && (e.originalEvent.movementX === 0 && e.originalEvent.movementY === 0 && e.originalEvent.buttons === 0)) return;
UpdateFullScreenNavigate();
};
const document_keydown = (e) => {
const code = (e.keyCode || e.which);
if (InFullScreen() || document.activeElement === document.body) {
switch (code) {
case 32: // Space
btnNavigatePlay_click();
return false;
case 39: // Right Arrow
case 34: // Page Down
btnNavigateNext_click();
return false;
case 37: // Left Arrow
case 33: // Page Up
btnNavigatePrevious_click();
return false;
case 36: // Home
btnNavigateMenu_click();
return false;
case 48: // Restart
btnNavigateRefresh_click();
return false;
case 70: // F
btnFullScreen_click();
return false;
default:
}
}
};
$.fn.fadeIn2 = function () {
const _self = this;
let opacity = 0.0;
let IntervalId = null;
if (_self.css('opacity') !== '0') return;
_self.css('visibility', 'visible');
_self.css('opacity', '0.0');
IntervalId = window.setInterval(() => {
opacity += 0.1;
opacity = Math.round2(opacity, 1);
_self.css('opacity', opacity.toString());
if (opacity === 1.0) {
//_self.css("visibility", "");
_self.css('visibility', 'visible');
window.clearInterval(IntervalId);
}
}, 50);
return _self;
};
$.fn.fadeOut2 = function () {
const _self = this;
let opacity = 1.0;
let IntervalId = null;
if (_self.css('opacity') !== '1') return;
_self.css('visibility', 'visible');
_self.css('opacity', '1.0');
IntervalId = window.setInterval(() => {
opacity -= 0.2;
opacity = Math.round2(opacity, 1);
_self.css('opacity', opacity.toString());
if (opacity === 0) {
_self.css('visibility', 'hidden');
window.clearInterval(IntervalId);
}
}, 50);
return _self;
};
Math.round2 = (value, decimals) => Number(Math.round(value + 'e' + decimals) + 'e-' + decimals);
const btnNavigatePlay_click = () => {
postMessage('navButton', 'playToggle');
UpdateFullScreenNavigate();
return false;
};
$(() => {
_WindowHeight = $(window).height();
_WindowWidth = $(window).width();
frmGetLatLng = $('#frmGetLatLng');
txtAddress = $('#txtAddress');
btnGetLatLng = $('#btnGetLatLng');
btnClearQuery = $('#btnClearQuery');
btnGetGps = $('#btnGetGps');
divLat = $('#divLat');
spanLat = $('#spanLat');
divLng = $('#divLng');
spanLng = $('#spanLng');
iframeTwc = $('#iframeTwc');
btnFullScreen = $('#btnFullScreen');
divTwc = $('#divTwc');
divTwcTop = $('#divTwcTop');
divTwcMiddle = $('#divTwcMiddle');
divTwcBottom = $('#divTwcBottom');
divTwcLeft = $('#divTwcLeft');
divTwcRight = $('#divTwcRight');
divTwcNav = $('#divTwcNav');
divTwcNavContainer = $('#divTwcNavContainer');
frmScrollText = $('#frmScrollText');
chkScrollText = $('#chkScrollText');
txtScrollText = $('#txtScrollText');
btnScrollText = $('#btnScrollText');
frmScrollText.on('submit', frmScrollText_submit);
txtScrollText.on('focus', function () {
txtScrollText.select();
});
chkScrollText.on('change', chkScrollText_change);
txtAddress.on('focus', function () {
txtAddress.select();
});
txtAddress.focus();
$('.NavigateMenu').on('click', btnNavigateMenu_click);
$('.NavigateRefresh').on('click', btnNavigateRefresh_click);
$('.NavigateNext').on('click', btnNavigateNext_click);
$('.NavigatePrevious').on('click', btnNavigatePrevious_click);
$('.NavigatePlay').on('click', btnNavigatePlay_click);
$(btnGetGps).on('click', btnGetGps_click);
$(window).on('resize', OnFullScreen);
$(window).on('resize', window_resize);
$(document).on('mousemove', document_mousemove);
$(document).on('mousedown', document_mousemove);
divTwc.on('mousedown', document_mousemove);
$(document).on('keydown', document_keydown);
document.addEventListener('touchmove', e => { if (_FullScreenOverride) e.preventDefault(); });
$('.ToggleFullScreen').on('click', btnFullScreen_click);
FullScreenResize();
// listen for messages (from iframe) and handle accordingly
window.addEventListener('message', messageHandler, false);
const categories = [
'Land Features',
'Bay', 'Channel', 'Cove', 'Dam', 'Delta', 'Gulf', 'Lagoon', 'Lake', 'Ocean', 'Reef', 'Reservoir', 'Sea', 'Sound', 'Strait', 'Waterfall', 'Wharf', // Water Features
'Amusement Park', 'Historical Monument', 'Landmark', 'Tourist Attraction', 'Zoo', // POI/Arts and Entertainment
'College', // POI/Education
'Beach', 'Campground', 'Golf Course', 'Harbor', 'Nature Reserve', 'Other Parks and Outdoors', 'Park', 'Racetrack',
'Scenic Overlook', 'Ski Resort', 'Sports Center', 'Sports Field', 'Wildlife Reserve', // POI/Parks and Outdoors
'Airport', 'Ferry', 'Marina', 'Pier', 'Port', 'Resort', // POI/Travel
'Postal', 'Populated Place',
];
const cats = categories.join(',');
const overrides = {
'08736, Manasquan, New Jersey, USA': { x: -74.037, y: 40.1128 },
'32899, Orlando, Florida, USA': { x: -80.6774, y: 28.6143 },
'97003, Beaverton, Oregon, USA': { x: -122.8752489, y: 45.5050916 },
'99734, Prudhoe Bay, Alaska, USA': { x: -148.3372, y: 70.2552 },
'Guam, Oceania': { x: 144.74, y: 13.46 },
'Andover, Maine, United States': { x: -70.7525, y: 44.634167 },
'Bear Creek, Pennsylvania, United States': { x: -75.772809, y: 41.204074 },
'Bear Creek Village, Pennsylvania, United States': { x: -75.772809, y: 41.204074 },
'New York City, New York, United States': { x: -74.0059, y: 40.7142 },
'Pinnacles National Monument, San Benito County,California, United States': { x: -121.147278, y: 36.47075 },
'Pinnacles National Park, CA-146, Paicines, California': { x: -121.147278, y: 36.47075 },
'Welcome, Maryland, United States': { x: -77.081212, y: 38.4692469 },
'Tampa, Florida, United States (City)': { x: -82.5329, y: 27.9756 },
'San Francisco, California, United States': { x: -122.3758, y: 37.6188 },
};
const roundToPlaces = function (num, decimals) {
var n = Math.pow(10, decimals);
return Math.round((n * num).toFixed(decimals)) / n;
};
const doRedirectToGeometry = (geom) => {
latLon = {lat:roundToPlaces(geom.y, 4), lon:roundToPlaces(geom.x, 4)};
LoadTwcData();
// Save the query
localStorage.setItem('TwcQuery', txtAddress.val());
};
let PreviousSeggestionValue = null;
let PreviousSeggestion = null;
const OnSelect = (suggestion) => {
let request;
// Do not auto get the same city twice.
if (PreviousSeggestionValue === suggestion.value) return;
PreviousSeggestionValue = suggestion.value;
PreviousSeggestion = suggestion;
if (overrides[suggestion.value]) {
doRedirectToGeometry(overrides[suggestion.value]);
} else {
request = $.ajax({
url: location.protocol + '//geocode.arcgis.com/arcgis/rest/services/World/GeocodeServer/find',
data: {
text: suggestion.value,
magicKey: suggestion.data,
f: 'json',
},
jsonp: 'callback',
dataType: 'jsonp',
});
request.done((data) => {
const loc = data.locations[0];
if (loc) {
doRedirectToGeometry(loc.feature.geometry);
} else {
alert('An unexpected error occurred. Please try a different search string.');
}
});
}
};
$('#frmGetLatLng #txtAddress').devbridgeAutocomplete({
serviceUrl: location.protocol + '//geocode.arcgis.com/arcgis/rest/services/World/GeocodeServer/suggest',
deferRequestBy: 300,
paramName: 'text',
params: {
f: 'json',
countryCode: 'USA', //'USA,PRI,VIR,GUM,ASM',
category: cats,
maxSuggestions: 10,
},
dataType: 'jsonp',
transformResult: (response) => {
if (_AutoSelectQuery) {
_AutoSelectQuery = false;
window.setTimeout(() => {
$(ac.suggestionsContainer.children[0]).click();
}, 1);
}
return {
suggestions: $.map(response.suggestions, function (i) {
return {
value: i.text,
data: i.magicKey,
};
}),
};
},
minChars: 3,
showNoSuggestionNotice: true,
noSuggestionNotice: 'No results found. Please try a different search string.',
onSelect: OnSelect,
width: 490,
});
const ac = $('#frmGetLatLng #txtAddress').devbridgeAutocomplete();
frmGetLatLng.submit(function () {
if (ac.suggestions[0]) {
$(ac.suggestionsContainer.children[0]).click();
return false;
}
if (PreviousSeggestion) {
PreviousSeggestionValue = null;
OnSelect(PreviousSeggestion);
}
return false;
});
// Auto load the previous query
const TwcQuery = localStorage.getItem('TwcQuery');
if (TwcQuery) {
_AutoSelectQuery = true;
txtAddress.val(TwcQuery);
txtAddress.blur();
txtAddress.focus();
}
const TwcPlay = localStorage.getItem('TwcPlay');
if (!TwcPlay || TwcPlay === 'true') {
_IsPlaying = true;
}
const TwcScrollText = localStorage.getItem('TwcScrollText');
if (TwcScrollText) {
txtScrollText.val(TwcScrollText);
}
const TwcScrollTextChecked = localStorage.getItem('TwcScrollTextChecked');
if (TwcScrollTextChecked && TwcScrollTextChecked === 'true') {
chkScrollText.prop('checked', 'checked');
} else {
chkScrollText.prop('checked', '');
}
btnClearQuery.on('click', () => {
spanCity.text('');
spanState.text('');
spanStationId.text('');
spanRadarId.text('');
spanZoneId.text('');
chkScrollText.prop('checked', '');
txtScrollText.val('');
localStorage.removeItem('TwcScrollText');
localStorage.removeItem('TwcScrollTextChecked');
chkAutoRefresh.prop('checked', 'checked');
localStorage.removeItem('TwcAutoRefresh');
$('#radEnglish').prop('checked', 'checked');
localStorage.removeItem('TwcUnits');
TwcCallBack({ Status: 'ISPLAYING', Value: false });
localStorage.removeItem('TwcPlay');
_IsPlaying = true;
localStorage.removeItem('TwcQuery');
PreviousSeggestionValue = null;
PreviousSeggestion = null;
LoadTwcData('');
});
const TwcUnits = localStorage.getItem('TwcUnits');
if (!TwcUnits || TwcUnits === 'ENGLISH') {
$('#radEnglish').prop('checked', 'checked');
} else if (TwcUnits === 'METRIC') {
$('#radMetric').prop('checked', 'checked');
}
$('input[type=\'radio\'][name=\'radUnits\']').on('change', (e) => {
const Units = $(e.target).val();
e;
localStorage.setItem('TwcUnits', Units);
AssignLastUpdate();
postMessage('units', Units);
});
divRefresh = $('#divRefresh');
spanLastRefresh = $('#spanLastRefresh');
chkAutoRefresh = $('#chkAutoRefresh');
lblRefreshCountDown = $('#lblRefreshCountDown');
spanRefreshCountDown = $('#spanRefreshCountDown');
chkAutoRefresh.on('change', (e) => {
const Checked = $(e.target).is(':checked');
if (_LastUpdate) {
if (Checked) {
StartAutoRefreshTimer();
} else {
StopAutoRefreshTimer();
}
}
localStorage.setItem('TwcAutoRefresh', Checked);
});
const TwcAutoRefresh = localStorage.getItem('TwcAutoRefresh');
if (!TwcAutoRefresh || TwcAutoRefresh === 'true') {
chkAutoRefresh.prop('checked', 'checked');
} else {
chkAutoRefresh.prop('checked', '');
}
spanCity = $('#spanCity');
spanState = $('#spanState');
spanStationId = $('#spanStationId');
spanRadarId = $('#spanRadarId');
spanZoneId = $('#spanZoneId');
});
// read and dispatch an event from the iframe
const messageHandler = (event) => {
// test for trust
if (!event.isTrusted) return;
// get the data
const data = JSON.parse(event.data);
// dispatch event
if (!data.type) return;
switch (data.type) {
case 'loaded':
_LastUpdate = new Date();
AssignLastUpdate();
break;
case 'weatherParameters':
populateWeatherParameters(data.message);
break;
case 'isPlaying':
_IsPlaying = data.message;
localStorage.setItem('TwcPlay', _IsPlaying);
if (_IsPlaying) {
_NoSleep.enable();
$('img[src=\'images/nav/ic_play_arrow_white_24dp_1x.png\']').attr('title', 'Pause');
$('img[src=\'images/nav/ic_play_arrow_white_24dp_1x.png\']').attr('src', 'images/nav/ic_pause_white_24dp_1x.png');
$('img[src=\'images/nav/ic_play_arrow_white_24dp_2x.png\']').attr('title', 'Pause');
$('img[src=\'images/nav/ic_play_arrow_white_24dp_2x.png\']').attr('src', 'images/nav/ic_pause_white_24dp_2x.png');
} else {
_NoSleep.disable();
$('img[src=\'images/nav/ic_pause_white_24dp_1x.png\']').attr('title', 'Play');
$('img[src=\'images/nav/ic_pause_white_24dp_1x.png\']').attr('src', 'images/nav/ic_play_arrow_white_24dp_1x.png');
$('img[src=\'images/nav/ic_pause_white_24dp_2x.png\']').attr('title', 'Play');
$('img[src=\'images/nav/ic_pause_white_24dp_2x.png\']').attr('src', 'images/nav/ic_play_arrow_white_24dp_2x.png');
}
break;
default:
console.error(`Unknown event '${data.eventType}`);
}
};
// post a message to the iframe
const postMessage = (type, message = {}) => {
const iframeWindow = document.getElementById('iframeTwc').contentWindow;
iframeWindow.postMessage(JSON.stringify({type, message}, window.location.origin));
};
const StartAutoRefreshTimer = () => {
// Esnure that any previous timer has already stopped.
//StopAutoRefreshTimer();
if (_AutoRefreshIntervalId) {
// Timer is already running.
return;
}
// Reset the time elapsed.
_AutoRefreshCountMs = 0;
const AutoRefreshTimer = () => {
// Increment the total time elapsed.
_AutoRefreshCountMs += _AutoRefreshIntervalMs;
// Display the count down.
let RemainingMs = (_AutoRefreshTotalIntervalMs - _AutoRefreshCountMs);
if (RemainingMs < 0) {
RemainingMs = 0;
}
const dt = new Date(RemainingMs);
spanRefreshCountDown.html((dt.getMinutes() < 10 ? '0' + dt.getMinutes() : dt.getMinutes()) + ':' + (dt.getSeconds() < 10 ? '0' + dt.getSeconds() : dt.getSeconds()));
// Time has elapsed.
if (_AutoRefreshCountMs >= _AutoRefreshTotalIntervalMs) LoadTwcData(_TwcDataUrl);
};
_AutoRefreshIntervalId = window.setInterval(AutoRefreshTimer, _AutoRefreshIntervalMs);
AutoRefreshTimer();
};
const StopAutoRefreshTimer = () => {
if (_AutoRefreshIntervalId) {
window.clearInterval(_AutoRefreshIntervalId);
spanRefreshCountDown.html('--:--');
_AutoRefreshIntervalId = null;
}
};
const btnGetGps_click = () => {
if (!navigator.geolocation) return;
const CurrentPosition = (position) => {
const latitude = position.coords.latitude;
const longitude = position.coords.longitude;
console.log('Latitude: ' + latitude + '; Longitude: ' + longitude);
//http://geocode.arcgis.com/arcgis/rest/services/World/GeocodeServer/reverseGeocode?location=-72.971293%2C+40.850043&f=pjson
const request = $.ajax({
url: location.protocol + '//geocode.arcgis.com/arcgis/rest/services/World/GeocodeServer/reverseGeocode',
data: {
location: longitude + ',' + latitude,
distance: 1000, // Find location upto 1 KM.
f: 'json',
},
jsonp: 'callback',
dataType: 'jsonp',
});
request.done((data) => {
console.log(data);
const ZipCode = data.address.Postal;
const City = data.address.City;
const State = states.getStateTwoDigitCode(data.address.Region);
const Country = data.address.CountryCode;
const TwcQuery = `${ZipCode}, ${City}, ${State}, ${Country}`;
txtAddress.val(TwcQuery);
txtAddress.blur();
txtAddress.focus();
// Save the query
localStorage.setItem('TwcQuery', TwcQuery);
});
};
navigator.geolocation.getCurrentPosition(CurrentPosition);
};
const populateWeatherParameters = (weatherParameters) => {
spanCity.text(weatherParameters.city + ', ');
spanState.text(weatherParameters.state);
spanStationId.text(weatherParameters.stationId);
spanRadarId.text(weatherParameters.radarId);
spanZoneId.text(weatherParameters.zoneId);
};
const frmScrollText_submit = () => {
chkScrollText_change();
return false;
};
const chkScrollText_change = () => {
txtScrollText.blur();
let ScrollText = txtScrollText.val();
localStorage.setItem('TwcScrollText', ScrollText);
const ScrollTextChecked = chkScrollText.is(':checked');
localStorage.setItem('TwcScrollTextChecked', ScrollTextChecked);
if (chkScrollText.is(':checked') === false) {
ScrollText = '';
}
postMessage('assignScrollText', ScrollText);
};

View File

@@ -0,0 +1,163 @@
// display sun and moon data
/* globals WeatherDisplay, utils, STATUS, UNITS, draw, navigation, SunCalc, luxon */
// eslint-disable-next-line no-unused-vars
class Almanac extends WeatherDisplay {
constructor(navId,elemId,weatherParameters) {
super(navId,elemId);
// pre-load background image (returns promise)
this.backgroundImage = utils.image.load('images/BackGround1_1.png');
// load all images in parallel (returns promises)
this.moonImages = [
utils.image.load('images/2/Full-Moon.gif'),
utils.image.load('images/2/Last-Quarter.gif'),
utils.image.load('images/2/New-Moon.gif'),
utils.image.load('images/2/First-Quarter.gif'),
];
this.backgroundImage = utils.image.load('images/BackGround3_1.png');
// get the data
this.getData(weatherParameters);
}
getData(weatherParameters) {
super.getData();
const {DateTime} = luxon;
const sun = [
SunCalc.getTimes(new Date(), weatherParameters.latitude, weatherParameters.longitude),
SunCalc.getTimes(DateTime.local().plus({days:1}).toJSDate(), weatherParameters.latitude, weatherParameters.longitude),
];
// brute force the moon phases by scanning the next 30 days
const moon = [];
// start with yesterday
let moonDate = DateTime.local().minus({days:1});
let phase = SunCalc.getMoonIllumination(moonDate.toJSDate()).phase;
let iterations = 0;
do {
// get yesterday's moon info
const lastPhase = phase;
// calculate new values
moonDate = moonDate.plus({days:1});
phase = SunCalc.getMoonIllumination(moonDate.toJSDate()).phase;
// check for 4 cases
if (lastPhase < 0.25 && phase >= 0.25) moon.push(this.getMoonTransition(0.25, 'First', moonDate));
if (lastPhase < 0.50 && phase >= 0.50) moon.push(this.getMoonTransition(0.50, 'Full', moonDate));
if (lastPhase < 0.75 && phase >= 0.75) moon.push(this.getMoonTransition(0.75, 'Last', moonDate));
if (lastPhase > phase) moon.push(this.getMoonTransition(0.00, 'New', moonDate));
// stop after 30 days or 4 moon phases
iterations++;
} while (iterations <= 30 && moon.length < 4);
// store the data
this.data = {
sun,
moon,
};
// draw the canvas
this.drawCanvas();
}
// get moon transition from one phase to the next by drilling down by hours, minutes and seconds
getMoonTransition(threshold, phaseName, start, iteration = 0) {
let moonDate = start;
let phase = SunCalc.getMoonIllumination(moonDate.toJSDate()).phase;
let iterations = 0;
const step = {
hours: iteration === 0 ? -1:0,
minutes: iteration === 1 ? 1:0,
seconds: iteration === 2 ? -1:0,
milliseconds: iteration === 3 ? 1:0,
};
// increasing test
let test = (lastPhase,phase,threshold) => lastPhase < threshold && phase >= threshold;
// decreasing test
if (iteration%2===0) test = (lastPhase,phase,threshold) => lastPhase > threshold && phase <= threshold;
do {
// store last phase
const lastPhase = phase;
// calculate new phase after step
moonDate = moonDate.plus(step);
phase = SunCalc.getMoonIllumination(moonDate.toJSDate()).phase;
// wrap phases > 0.9 to -0.1 for ease of detection
if (phase > 0.9) phase -= 1.0;
// compare
if (test(lastPhase, phase, threshold)) {
// last iteration is three, return value
if (iteration >= 3) break;
// iterate recursively
return this.getMoonTransition(threshold, phaseName, moonDate, iteration+1);
}
iterations++;
} while (iterations < 1000);
return {phase: phaseName, date: moonDate};
}
async drawCanvas() {
super.drawCanvas();
const info = this.data;
const {DateTime} = luxon;
// extract moon images
const [FullMoonImage, LastMoonImage, NewMoonImage, FirstMoonImage] = await Promise.all(this.moonImages);
this.context.drawImage(await this.backgroundImage, 0, 0);
draw.horizontalGradientSingle(this.context, 0, 30, 500, 90, draw.topColor1, draw.topColor2);
draw.triangle(this.context, 'rgb(28, 10, 87)', 500, 30, 450, 90, 500, 90);
draw.horizontalGradientSingle(this.context, 0, 90, 640, 190, draw.sideColor1, draw.sideColor2);
draw.titleText(this.context, 'Almanac', 'Astronomical');
const Today = DateTime.local();
const Tomorrow = Today.plus({days: 1});
draw.text(this.context, 'Star4000', '24pt', '#FFFF00', 320, 120, Today.toLocaleString({weekday: 'long'}), 2, 'center');
draw.text(this.context, 'Star4000', '24pt', '#FFFF00', 500, 120, Tomorrow.toLocaleString({weekday: 'long'}), 2, 'center');
draw.text(this.context, 'Star4000', '24pt', '#FFFFFF', 70, 150, 'Sunrise:', 2);
draw.text(this.context, 'Star4000', '24pt', '#FFFFFF', 270, 150, DateTime.fromJSDate(info.sun[0].sunrise).toLocaleString(DateTime.TIME_SIMPLE).toLowerCase(), 2);
draw.text(this.context, 'Star4000', '24pt', '#FFFFFF', 450, 150, DateTime.fromJSDate(info.sun[1].sunrise).toLocaleString(DateTime.TIME_SIMPLE).toLowerCase(), 2);
draw.text(this.context, 'Star4000', '24pt', '#FFFFFF', 70, 180, ' Sunset:', 2);
draw.text(this.context, 'Star4000', '24pt', '#FFFFFF', 270, 180, DateTime.fromJSDate(info.sun[0].sunset).toLocaleString(DateTime.TIME_SIMPLE).toLowerCase(), 2);
draw.text(this.context, 'Star4000', '24pt', '#FFFFFF', 450, 180, DateTime.fromJSDate(info.sun[1].sunset).toLocaleString(DateTime.TIME_SIMPLE).toLowerCase(), 2);
draw.text(this.context, 'Star4000', '24pt', '#FFFF00', 70, 220, 'Moon Data:', 2);
info.moon.forEach((MoonPhase, Index) => {
const date = MoonPhase.date.toLocaleString({month: 'short', day: 'numeric'});
draw.text(this.context, 'Star4000', '24pt', '#FFFFFF', 120+Index*130, 260, MoonPhase.phase, 2, 'center');
draw.text(this.context, 'Star4000', '24pt', '#FFFFFF', 120+Index*130, 390, date, 2, 'center');
const image = (() => {
switch (MoonPhase.phase) {
case 'Full':
return FullMoonImage;
case 'Last':
return LastMoonImage;
case 'New':
return NewMoonImage;
case 'First':
default:
return FirstMoonImage;
}
})();
this.context.drawImage(image, 75+Index*130, 270);
});
this.finishDraw();
this.setStatus(STATUS.loaded);
}
}

View File

@@ -0,0 +1,202 @@
// current weather conditions display
/* globals WeatherDisplay, utils, STATUS, icons, UNITS, draw, navigation */
// eslint-disable-next-line no-unused-vars
class CurrentWeather extends WeatherDisplay {
constructor(navId,elemId,weatherParameters) {
super(navId,elemId);
// pre-load background image (returns promise)
this.backgroundImage = utils.image.load('images/BackGround1_1.png');
// get the data
this.getData(weatherParameters);
}
async getData(weatherParameters) {
super.getData();
// Load the observations
let observations, station;
try {
// station observations
const observationsPromise = $.ajaxCORS({
type: 'GET',
url: `https://api.weather.gov/stations/${weatherParameters.stationId}/observations`,
data: {
limit: 2,
},
dataType: 'json',
crossDomain: true,
});
// station info
const stationPromise = $.ajax({
type: 'GET',
url: `https://api.weather.gov/stations/${weatherParameters.stationId}`,
dataType: 'json',
crossDomain: true,
});
// wait for the promises to resolve
[observations, station] = await Promise.all([observationsPromise, stationPromise]);
// TODO: add retry for further stations if observations are unavailable
} catch (e) {
console.error('Unable to get current observations');
console.error(e);
this.setStatus(STATUS.error);
return;
}
// we only get here if there was no error above
this.data = Object.assign({}, observations, {station: station});
this.drawCanvas();
}
async drawCanvas () {
super.drawCanvas();
const observations = this.data.features[0].properties;
// values from api are provided in metric
let Temperature = Math.round(observations.temperature.value);
let DewPoint = Math.round(observations.dewpoint.value);
let Ceiling = Math.round(observations.cloudLayers[0].base.value);
let CeilingUnit = 'm.';
let Visibility = Math.round(observations.visibility.value/1000);
let VisibilityUnit = ' km.';
let WindSpeed = Math.round(observations.windSpeed.value);
const WindDirection = utils.calc.directionToNSEW(observations.windDirection.value);
let Pressure = Math.round(observations.barometricPressure.value);
let HeatIndex = Math.round(observations.heatIndex.value);
let WindChill = Math.round(observations.windChill.value);
let WindGust = Math.round(observations.windGust.value);
let Humidity = Math.round(observations.relativeHumidity.value);
// TODO: switch to larger icon
const Icon = icons.getWeatherRegionalIconFromIconLink(observations.icon);
let PressureDirection = '';
const TextConditions = observations.textDescription;
// difference since last measurement (pascals, looking for difference of more than 150)
const pressureDiff = (observations.barometricPressure.value - this.data.features[1].properties.barometricPressure.value);
if (pressureDiff > 150) PressureDirection = 'R';
if (pressureDiff < -150) PressureDirection = 'F';
if (navigation.units() === UNITS.english) {
Temperature = utils.units.celsiusToFahrenheit(Temperature);
DewPoint = utils.units.celsiusToFahrenheit(DewPoint);
Ceiling = Math.round(utils.units.metersToFeet(Ceiling)/100)*100;
CeilingUnit = 'ft.';
Visibility = utils.units.kilometersToMiles(observations.visibility.value/1000);
VisibilityUnit = ' mi.';
WindSpeed = utils.units.kphToMph(WindSpeed);
Pressure = utils.units.pascalToInHg(Pressure);
HeatIndex = utils.units.celsiusToFahrenheit(HeatIndex);
WindChill = utils.units.celsiusToFahrenheit(WindChill);
WindGust = utils.units.kphToMph(WindGust);
}
// get main icon
this.gifs.push(await utils.image.superGifAsync({
src: Icon,
loop_delay: 100,
auto_play: true,
canvas: this.canvas,
x: 140,
y: 175,
max_width: 126,
}));
this.context.drawImage(await this.backgroundImage, 0, 0);
draw.horizontalGradientSingle(this.context, 0, 30, 500, 90, draw.topColor1, draw.topColor2);
draw.triangle(this.context, 'rgb(28, 10, 87)', 500, 30, 450, 90, 500, 90);
draw.horizontalGradientSingle(this.context, 0, 90, 52, 399, draw.sideColor1, draw.sideColor2);
draw.horizontalGradientSingle(this.context, 584, 90, 640, 399, draw.sideColor1, draw.sideColor2);
draw.titleText(this.context, 'Current', 'Conditions');
draw.text(this.context, 'Star4000 Large', '24pt', '#FFFFFF', 170, 135, Temperature + String.fromCharCode(176), 2);
let Conditions = observations.textDescription;
if (TextConditions.length > 15) {
Conditions = this.shortConditions(Conditions);
}
draw.text(this.context, 'Star4000 Extended', '24pt', '#FFFFFF', 195, 170, Conditions, 2, 'center');
draw.text(this.context, 'Star4000 Extended', '24pt', '#FFFFFF', 80, 330, 'Wind:', 2);
draw.text(this.context, 'Star4000 Extended', '24pt', '#FFFFFF', 300, 330, WindDirection + ' ' + WindSpeed, 2, 'right');
if (WindGust) draw.text(this.context, 'Star4000 Extended', '24pt', '#FFFFFF', 80, 375, 'Gusts to ' + WindGust, 2);
draw.text(this.context, 'Star4000 Large', 'bold 16pt', '#FFFF00', 315, 120, this.data.station.properties.name.substr(0, 20), 2);
draw.text(this.context, 'Star4000 Large', 'bold 16pt', '#FFFFFF', 340, 165, 'Humidity:', 2);
draw.text(this.context, 'Star4000 Large', 'bold 16pt', '#FFFFFF', 560, 165, Humidity + '%', 2, 'right');
draw.text(this.context, 'Star4000 Large', 'bold 16pt', '#FFFFFF', 340, 205, 'Dewpoint:', 2);
draw.text(this.context, 'Star4000 Large', 'bold 16pt', '#FFFFFF', 560, 205, DewPoint + String.fromCharCode(176), 2, 'right');
draw.text(this.context, 'Star4000 Large', 'bold 16pt', '#FFFFFF', 340, 245, 'Ceiling:', 2);
draw.text(this.context, 'Star4000 Large', 'bold 16pt', '#FFFFFF', 560, 245, (Ceiling === '' ? 'Unlimited' : Ceiling + CeilingUnit), 2, 'right');
draw.text(this.context, 'Star4000 Large', 'bold 16pt', '#FFFFFF', 340, 285, 'Visibility:', 2);
draw.text(this.context, 'Star4000 Large', 'bold 16pt', '#FFFFFF', 560, 285, Visibility + VisibilityUnit, 2, 'right');
draw.text(this.context, 'Star4000 Large', 'bold 16pt', '#FFFFFF', 340, 325, 'Pressure:', 2);
draw.text(this.context, 'Star4000 Large', 'bold 16pt', '#FFFFFF', 535, 325, Pressure, 2, 'right');
switch (PressureDirection) {
case 'R':
// Shadow
draw.triangle(this.context, '#000000', 552, 302, 542, 312, 562, 312);
draw.box(this.context, '#000000', 549, 312, 6, 15);
// Border
draw.triangle(this.context, '#000000', 550, 300, 540, 310, 560, 310);
draw.box(this.context, '#000000', 547, 310, 6, 15);
// Fill
draw.triangle(this.context, '#FFFF00', 550, 301, 541, 309, 559, 309);
draw.box(this.context, '#FFFF00', 548, 309, 4, 15);
break;
case 'F':
// Shadow
draw.triangle(this.context, '#000000', 552, 327, 542, 317, 562, 317);
draw.box(this.context, '#000000', 549, 302, 6, 15);
// Border
draw.triangle(this.context, '#000000', 550, 325, 540, 315, 560, 315);
draw.box(this.context, '#000000', 547, 300, 6, 15);
// Fill
draw.triangle(this.context, '#FFFF00', 550, 324, 541, 314, 559, 314);
draw.box(this.context, '#FFFF00', 548, 301, 4, 15);
break;
default:
}
if (observations.heatIndex.value && HeatIndex !== Temperature) {
draw.text(this.context, 'Star4000 Large', 'bold 16pt', '#FFFFFF', 340, 365, 'Heat Index:', 2);
draw.text(this.context, 'Star4000 Large', 'bold 16pt', '#FFFFFF', 560, 365, HeatIndex + String.fromCharCode(176), 2, 'right');
} else if (observations.windChill.value && WindChill !== '' && WindChill < Temperature) {
draw.text(this.context, 'Star4000 Large', 'bold 16pt', '#FFFFFF', 340, 365, 'Wind Chill:', 2);
draw.text(this.context, 'Star4000 Large', 'bold 16pt', '#FFFFFF', 560, 365, WindChill + String.fromCharCode(176), 2, 'right');
}
this.finishDraw();
this.setStatus(STATUS.loaded);
}
shortConditions(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;
}
}

View File

@@ -0,0 +1,101 @@
// drawing functionality and constants
// eslint-disable-next-line no-unused-vars
const draw = (() => {
const horizontalGradient = (context, x1, y1, x2, y2, color1, color2) => {
const linearGradient = context.createLinearGradient(0, y1, 0, y2);
linearGradient.addColorStop(0, color1);
linearGradient.addColorStop(0.4, color2);
linearGradient.addColorStop(0.6, color2);
linearGradient.addColorStop(1, color1);
context.fillStyle = linearGradient;
context.fillRect(x1, y1, x2 - x1, y2 - y1);
};
const horizontalGradientSingle = (context, x1, y1, x2, y2, color1, color2) => {
const linearGradient = context.createLinearGradient(0, y1, 0, y2);
linearGradient.addColorStop(0, color1);
linearGradient.addColorStop(1, color2);
context.fillStyle = linearGradient;
context.fillRect(x1, y1, x2 - x1, y2 - y1);
};
const triangle = (context, color, x1, y1, x2, y2, x3, y3) => {
context.fillStyle = color;
context.beginPath();
context.moveTo(x1, y1);
context.lineTo(x2, y2);
context.lineTo(x3, y3);
context.fill();
};
const titleText = (context, title1, title2) => {
const font = 'Star4000';
const size = '24pt';
const color = '#ffff00';
const shadow = 3;
const x = 170;
let y = 55;
if (title2) {
text(context, font, size, color, x, y, title1, shadow); y += 30;
text(context, font, size, color, x, y, title2, shadow); y += 30;
} else {
y += 15;
text(context, font, size, color, x, y, title1, shadow); y += 30;
}
};
const text = (context, font, size, color, x, y, text, shadow = 0, align = 'start') => {
context.textAlign = align;
context.font = size + ` '${font}'`;
context.shadowColor = '#000000';
context.shadowOffsetX = shadow;
context.shadowOffsetY = shadow;
context.strokeStyle = '#000000';
context.lineWidth = 2;
context.strokeText(text, x, y);
context.fillStyle = color;
context.fillText(text, x, y);
context.fillStyle = '';
context.strokeStyle = '';
context.shadowOffsetX = 0;
context.shadowOffsetY = 0;
};
const box = (context, color, x, y, width, height) => {
context.fillStyle = color;
context.fillRect(x, y, width, height);
};
const border = (context, color, lineWith, x, y, width, height) => {
context.strokeStyle = color;
context.lineWidth = lineWith;
context.strokeRect(x, y, width, height);
};
// TODO: implement full themes support
const theme = 1; // classic
const topColor1 = 'rgb(192, 91, 2)';
const topColor2 = 'rgb(72, 34, 64)';
const sideColor1 = 'rgb(46, 18, 80)';
const sideColor2 = 'rgb(192, 91, 2)';
return {
// methods
horizontalGradient,
horizontalGradientSingle,
triangle,
titleText,
text,
box,
border,
// constant-ish
theme,
topColor1,
topColor2,
sideColor1,
sideColor2,
};
})();

View File

@@ -0,0 +1,177 @@
// display extended forecast graphically
// technically uses the same data as the local forecast, we'll let the browser do the caching of that
/* globals WeatherDisplay, utils, STATUS, UNITS, draw, icons, navigation, luxon */
// eslint-disable-next-line no-unused-vars
class ExtendedForecast extends WeatherDisplay {
constructor(navId,elemId,weatherParameters) {
super(navId,elemId);
// set timings
this.timing.totalScreens = 2;
// pre-load background image (returns promise)
this.backgroundImage = utils.image.load('images/BackGround2_1.png');
// get the data
this.getData(weatherParameters);
}
async getData(weatherParameters) {
super.getData();
// request us or si units
let units = 'us';
if (navigation.units() === UNITS.metric) units = 'si';
let forecast;
try {
forecast = await $.ajax({
type: 'GET',
url: weatherParameters.forecast,
data: {
units,
},
dataType: 'json',
crossDomain: true,
});
} catch (e) {
console.error('Unable to get extended forecast');
console.error(e);
this.setStatus(STATUS.error);
return;
}
// we only get here if there was no error above
this.data = this.parseExtendedForecast(forecast.properties.periods);
this.screnIndex = 0;
this.drawCanvas();
}
// the api provides the forecast in 12 hour increments, flatten to day increments with high and low temperatures
parseExtendedForecast(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 = luxon.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
// TODO: change to commented line when incons are matched up
// fDay.icon = icons.GetWeatherIconFromIconLink(period.icon);
fDay.icon = icons.getWeatherRegionalIconFromIconLink(period.icon);
fDay.text = this.shortenExtendedForecastText(period.shortForecast);
fDay.dayName = dates[destIndex];
if (period.isDaytime) {
// day time is the high temperature
fDay.high = period.temperature;
destIndex++;
} else {
// low temperature
fDay.low = period.temperature;
}
});
return forecast;
}
shortenExtendedForecastText(long) {
let short = long;
short = short.replace(/ and /g, ' ');
short = short.replace(/Slight /g, '');
short = short.replace(/Chance /g, '');
short = short.replace(/Very /g, '');
short = short.replace(/Patchy /g, '');
short = short.replace(/Areas /g, '');
short = short.replace(/Dense /g, '');
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 = '';
}
}
short = short1;
if (short2 !== '') {
short += ' ' + short2;
}
return [short, short1, short2];
}
async drawCanvas() {
super.drawCanvas();
// determine bounds
// grab the first three or second set of three array elements
const forecast = this.data.slice(0+3*this.screenIndex, 3+this.screenIndex*3);
const backgroundImage = await this.backgroundImage;
this.context.drawImage(backgroundImage, 0, 0);
draw.horizontalGradientSingle(this.context, 0, 30, 500, 90, draw.topColor1, draw.topColor2);
draw.triangle(this.context, 'rgb(28, 10, 87)', 500, 30, 450, 90, 500, 90);
draw.horizontalGradientSingle(this.context, 0, 90, 640, 399, draw.sideColor1, draw.sideColor2);
this.context.drawImage(backgroundImage, 38, 100, 174, 297, 38, 100, 174, 297);
this.context.drawImage(backgroundImage, 232, 100, 174, 297, 232, 100, 174, 297);
this.context.drawImage(backgroundImage, 426, 100, 174, 297, 426, 100, 174, 297);
draw.titleText(this.context, 'Extended', 'Forecast');
await Promise.all(forecast.map(async (Day, Index) => {
const offset = Index*195;
draw.text(this.context, 'Star4000', '24pt', '#FFFF00', 100+offset, 135, Day.dayName.toUpperCase(), 2);
draw.text(this.context, 'Star4000', '24pt', '#8080FF', 85+offset, 345, 'Lo', 2, 'center');
draw.text(this.context, 'Star4000', '24pt', '#FFFF00', 165+offset, 345, 'Hi', 2, 'center');
let low = Day.low;
if (low !== undefined) {
if (navigation.units() === UNITS.metric) low = utils.units.rahrenheitToCelsius(low);
draw.text(this.context, 'Star4000 Large', '24pt', '#FFFFFF', 85+offset, 385, low, 2, 'center');
}
let high = Day.high;
if (navigation.units() === UNITS.metric) high = utils.units.rahrenheitToCelsius(high);
draw.text(this.context, 'Star4000 Large', '24pt', '#FFFFFF', 165+offset, 385, high, 2, 'center');
draw.text(this.context, 'Star4000', '24pt', '#FFFFFF', 120+offset, 270, Day.text[1], 2, 'center');
draw.text(this.context, 'Star4000', '24pt', '#FFFFFF', 120+offset, 310, Day.text[2], 2, 'center');
// draw the icon
this.gifs.push(await utils.image.superGifAsync({
src: Day.icon,
loop_delay: 100,
auto_play: true,
canvas: this.canvas,
x: 70 + Index*195,
y: 150,
max_height: 75,
}));
}));
this.finishDraw();
this.setStatus(STATUS.loaded);
}
}

View File

@@ -0,0 +1,237 @@
'use strict';
// eslint-disable-next-line no-unused-vars
const icons = (() => {
// internal function to add path to returned icon
const addPath = (icon) => `images/r/${icon}`;
const getWeatherRegionalIconFromIconLink = (link, isNightTime) => {
// extract day or night if not provided
if (isNightTime === undefined) isNightTime = link.indexOf('/night/') >=0;
// grab everything after the last slash ending at any of these: ?&,
const afterLastSlash = link.toLowerCase().match(/[^/]+$/)[0];
let conditionName = afterLastSlash.match(/(.*?)[,?&.]/)[1];
// if a 'DualImage' is captured, adjust to just the j parameter
if (conditionName === 'dualimage') {
const match = link.match(/&j=(.*)&/);
conditionName = match[1];
}
// find the icon
switch (conditionName + (isNightTime?'-n':'')) {
case 'skc':
case 'hot':
case 'haze':
return addPath('Sunny.gif');
case 'skc-n':
case 'nskc':
case 'nskc-n':
return addPath('Clear-1992.gif');
case 'bkn':
return addPath('Mostly-Cloudy-1994-2.gif');
case 'bkn-n':
case 'few-n':
case 'nfew-n':
case 'nfew':
return addPath('Partly-Clear-1994-2.gif');
case 'sct':
case 'few':
return addPath('Partly-Cloudy.gif');
case 'sct-n':
case 'nsct':
case 'nsct-n':
return addPath('Mostly-Clear.gif');
case 'ovc':
return addPath('Cloudy.gif');
case 'fog':
return addPath('Fog.gif');
case 'rain_sleet':
return addPath('Sleet.gif');
case 'rain_showers':
case 'rain_showers_high':
return addPath('Scattered-Showers-1994-2.gif');
case 'rain_showers-n':
case 'rain_showers_high-n':
return addPath('Scattered-Showers-Night-1994-2.gif');
case 'rain':
return addPath('Rain-1992.gif');
// case 'snow':
// return addPath('Light-Snow.gif');
// break;
// case 'cc_snowshowers.gif':
// //case "heavy-snow.gif":
// return addPath('AM-Snow-1994.gif');
// break;
case 'snow':
return addPath('Heavy-Snow-1994-2.gif');
case 'rain_snow':
return addPath('Rain-Snow-1992.gif');
case 'snow_fzra':
return addPath('Freezing-Rain-Snow-1992.gif');
case 'fzra':
return addPath('Freezing-Rain-1992.gif');
case 'snow_sleet':
return addPath('Wintry-Mix-1992.gif');
case 'tsra_sct':
case 'tsra':
return addPath('Scattered-Tstorms-1994-2.gif');
case 'tsra_sct-n':
case 'tsra-n':
return addPath('Scattered-Tstorms-Night-1994-2.gif');
case 'tsra_hi':
case 'tsra_hi-n':
case 'hurricane':
return addPath('Thunderstorm.gif');
case 'wind_few':
case 'wind_sct':
case 'wind_bkn':
case 'wind_ovc':
return addPath('Wind.gif');
case 'wind_skc':
return addPath('Sunny-Wind-1994.gif');
case 'wind_skc-n':
return addPath('Clear-Wind-1994.gif');
case 'blizzard':
return addPath('Blowing Snow.gif');
default:
console.log(`Unable to locate regional icon for ${link} ${isNightTime}`);
return false;
}
};
const getWeatherIconFromIconLink = function (link, OverrideIsDay = true) {
// grab everything after the last slash ending at any of these: ?&,
const afterLastSlash = link.toLowerCase().match(/[^/]+$/)[0];
let conditionName = afterLastSlash.match(/(.*?)[,?&.]/)[1];
// if a 'DualImage' is captured, adjust to just the j parameter
if (conditionName === 'dualimage') {
const match = link.match(/&j=(.*)&/);
conditionName = match[1];
}
// find the icon
switch (conditionName + (!OverrideIsDay?'-n':'')) {
case 'skc':
return addPath('Sunny.gif');
case 'skc-n':
return addPath('Clear.gif');
case 'cc_mostlycloudy1.gif':
return addPath('Mostly-Cloudy.gif');
case 'cc_mostlycloudy0.gif':
return addPath('Partly-Clear.gif');
case 'cc_partlycloudy1.gif':
return addPath('Partly-Cloudy.gif');
case 'cc_partlycloudy0.gif':
return addPath('Mostly-Clear.gif');
case 'cc_cloudy.gif':
return addPath('Cloudy.gif');
case 'cc_fog.gif':
return addPath('Fog.gif');
case 'sleet.gif':
return addPath('Sleet.gif');
case 'ef_scatshowers.gif':
return addPath('Scattered-Showers.gif');
case 'cc_showers.gif':
return addPath('Shower.gif');
case 'cc_rain.gif':
return addPath('Rain.gif');
//case "ef_scatsnowshowers.gif":
case 'light-snow.gif':
return addPath('Light-Snow.gif');
case 'cc_snowshowers.gif':
return addPath('Heavy-Snow.gif');
case 'cc_snow.gif':
case 'heavy-snow.gif':
return addPath('Heavy-Snow.gif');
case 'cc_rainsnow.gif':
//return addPath("Ice-Snow.gif");
return addPath('Rain-Snow.gif');
case 'cc_freezingrain.gif':
return addPath('Freezing-Rain.gif');
case 'cc_mix.gif':
return addPath('Wintry-Mix.gif');
case 'freezing-rain-sleet.gif':
return addPath('Freezing-Rain-Sleet.gif');
case 'snow-sleet.gif':
return addPath('Snow-Sleet.gif');
case 'ef_scattstorms.gif':
return addPath('Scattered-Tstorms.gif');
case 'ef_scatsnowshowers.gif':
return addPath('Scattered-Snow-Showers.gif');
case 'cc_tstorm.gif':
case 'ef_isolatedtstorms.gif':
return addPath('Thunderstorm.gif');
case 'cc_windy.gif':
case 'cc_windy2.gif':
return addPath('Windy.gif');
case 'blowing-snow.gif':
return addPath('Blowing-Snow.gif');
default:
console.error('Unable to locate icon for \'' + link + '\'');
return false;
}
};
return {
getWeatherIconFromIconLink,
getWeatherRegionalIconFromIconLink,
};
})();

View File

@@ -0,0 +1,129 @@
// current weather conditions display
/* globals WeatherDisplay, utils, STATUS, UNITS, draw, navigation, _StationInfo */
// eslint-disable-next-line no-unused-vars
class LatestObservations extends WeatherDisplay {
constructor(navId,elemId, weatherParameters) {
super(navId,elemId);
// pre-load background image (returns promise)
this.backgroundImage = utils.image.load('images/BackGround1_1.png');
// constants
this.MaximumRegionalStations = 7;
// get the data
this.getData(weatherParameters);
}
async getData(weatherParameters) {
// calculate distance to each station
const stationsByDistance = Object.keys(_StationInfo).map(key => {
const station = _StationInfo[key];
const distance = utils.calc.distance(station.Latitude, station.Longitude, weatherParameters.latitude, weatherParameters.longitude);
return Object.assign({}, station, {distance});
});
// sort the stations by distance
const sortedStations = stationsByDistance.sort((a,b) => a.distance - b.distance);
// try up to 30 regional stations
const regionalStations = sortedStations.slice(0,30);
// get data for regional stations
const allConditions = await Promise.all(regionalStations.map(async station => {
try {
const data = await $.ajax({
type: 'GET',
url: `https://api.weather.gov/stations/${station.StationId}/observations/latest`,
dataType: 'json',
crossDomain: true,
});
// format the return values
return Object.assign({}, data.properties, {
StationId: station.StationId,
City: station.City,
});
} catch (e) {
console.log(`Unable to get latest observations for ${station.StationId}`);
return;
}
}));
// remove and stations that did not return data
const actualConditions = allConditions.filter(condition => condition);
// cut down to the maximum of 7
this.data = actualConditions.slice(0,this.MaximumRegionalStations);
this.drawCanvas();
}
async drawCanvas() {
super.drawCanvas();
const conditions = this.data;
// sort array by station name
const sortedConditions = conditions.sort((a,b) => ((a.Name < b.Name) ? -1 : ((a.Name > b.Name) ? 1 : 0)));
this.context.drawImage(await this.backgroundImage, 0, 0);
draw.horizontalGradientSingle(this.context, 0, 30, 500, 90, draw.topColor1, draw.topColor2);
draw.triangle(this.context, 'rgb(28, 10, 87)', 500, 30, 450, 90, 500, 90);
draw.horizontalGradientSingle(this.context, 0, 90, 52, 399, draw.sideColor1, draw.sideColor2);
draw.horizontalGradientSingle(this.context, 584, 90, 640, 399, draw.sideColor1, draw.sideColor2);
draw.titleText(this.context, 'Latest', 'Observations');
if (navigation.units() === UNITS.english) {
draw.text(this.context, 'Star4000 Small', '24pt', '#FFFFFF', 295, 105, String.fromCharCode(176) + 'F', 2);
} else {
draw.text(this.context, 'Star4000 Small', '24pt', '#FFFFFF', 295, 105, String.fromCharCode(176) + 'C', 2);
}
draw.text(this.context, 'Star4000 Small', '24pt', '#FFFFFF', 345, 105, 'WEATHER', 2);
draw.text(this.context, 'Star4000 Small', '24pt', '#FFFFFF', 495, 105, 'WIND', 2);
let y = 140;
sortedConditions.forEach((condition) => {
let Temperature = condition.temperature.value;
let WindSpeed = condition.windSpeed.value;
const windDirection = utils.calc.directionToNSEW(condition.windDirection.value);
if (navigation.units() === UNITS.english) {
Temperature = utils.units.celsiusToFahrenheit(Temperature);
WindSpeed = utils.units.kphToMph(WindSpeed);
}
draw.text(this.context, 'Star4000', '24pt', '#FFFFFF', 65, y, condition.City.substr(0, 14), 2);
draw.text(this.context, 'Star4000', '24pt', '#FFFFFF', 345, y, this.shortenCurrentConditions(condition.textDescription).substr(0, 9), 2);
if (WindSpeed > 0) {
draw.text(this.context, 'Star4000', '24pt', '#FFFFFF', 495, y, windDirection + (Array(6 - windDirection.length - WindSpeed.toString().length).join(' ')) + WindSpeed.toString(), 2);
} else if (WindSpeed === 'NA') {
draw.text(this.context, 'Star4000', '24pt', '#FFFFFF', 495, y, 'NA', 2);
} else {
draw.text(this.context, 'Star4000', '24pt', '#FFFFFF', 495, y, 'Calm', 2);
}
const x = (325 - (Temperature.toString().length * 15));
draw.text(this.context, 'Star4000', '24pt', '#FFFFFF', x, y, Temperature, 2);
y += 40;
});
this.finishDraw();
this.setStatus(STATUS.loaded);
}
shortenCurrentConditions(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;
}
}

View File

@@ -0,0 +1,145 @@
// display text based local forecast
/* globals WeatherDisplay, utils, STATUS, UNITS, draw, navigation*/
// eslint-disable-next-line no-unused-vars
class LocalForecast extends WeatherDisplay {
constructor(navId,elemId,weatherParameters) {
super(navId,elemId);
// set timings
this.timing.baseDelay= 3000;
// pre-load background image (returns promise)
this.backgroundImage = utils.image.load('images/BackGround1_1.png');
// get the data
this.getData(weatherParameters);
}
async getData(weatherParameters) {
super.getData();
// get raw data
const rawData = await this.getRawData(weatherParameters);
// parse raw data
const conditions = this.parseLocalForecast(rawData);
// split this forecast into the correct number of screens
const MaxRows = 7;
const MaxCols = 32;
this.screenTexts = [];
// read each text
conditions.forEach(condition => {
// process the text
let text = condition.DayName.toUpperCase() + '...';
let conditionText = condition.Text;
if (navigation.units() === UNITS.metric) {
conditionText = condition.TextC;
}
text += conditionText.toUpperCase().replace('...', ' ');
text = text.wordWrap(MaxCols, '\n');
const Lines = text.split('\n');
const LineCount = Lines.length;
let ScreenText = '';
const MaxRowCount = MaxRows;
let RowCount = 0;
// if (PrependAlert) {
// ScreenText = LocalForecastScreenTexts[LocalForecastScreenTexts.length - 1];
// RowCount = ScreenText.split('\n').length - 1;
// }
for (let i = 0; i <= LineCount - 1; i++) {
if (Lines[i] === '') continue;
if (RowCount > MaxRowCount - 1) {
// if (PrependAlert) {
// LocalForecastScreenTexts[LocalForecastScreenTexts.length - 1] = ScreenText;
// PrependAlert = false;
// } else {
this.screenTexts.push(ScreenText);
// }
ScreenText = '';
RowCount = 0;
}
ScreenText += Lines[i] + '\n';
RowCount++;
}
// if (PrependAlert) {
// this.screenTexts[this.screenTexts.length - 1] = ScreenText;
// PrependAlert = false;
// } else {
this.screenTexts.push(ScreenText);
// }
});
this.currentScreen = 0;
this.timing.totalScreens = this.screenTexts.length;
this.drawCanvas();
}
// get the unformatted data (also used by extended forecast)
async getRawData(weatherParameters) {
// request us or si units
let units = 'us';
if (navigation.units() === UNITS.metric) units = 'si';
try {
return await $.ajax({
type: 'GET',
url: weatherParameters.forecast,
data: {
units,
},
dataType: 'json',
crossDomain: true,
});
} catch (e) {
console.error(`GetWeatherForecast failed: ${weatherParameters.forecast}`);
console.error(e);
return false;
}
}
// TODO: alerts needs a cleanup
// TODO: second page of screenTexts when needed
async drawCanvas() {
super.drawCanvas();
this.context.drawImage(await this.backgroundImage, 0, 0);
draw.horizontalGradientSingle(this.context, 0, 30, 500, 90, draw.topColor1, draw.topColor2);
draw.triangle(this.context, 'rgb(28, 10, 87)', 500, 30, 450, 90, 500, 90);
draw.horizontalGradientSingle(this.context, 0, 90, 52, 399, draw.sideColor1, draw.sideColor2);
draw.horizontalGradientSingle(this.context, 584, 90, 640, 399, draw.sideColor1, draw.sideColor2);
draw.titleText(this.context, 'Local ', 'Forecast');
// clear existing text
draw.box(this.context, 'rgb(33, 40, 90)', 65, 105, 505, 280);
// Draw the text.
this.screenTexts[this.screenIndex].split('\n').forEach((text, index) => {
draw.text(this.context, 'Star4000', '24pt', '#FFFFFF', 75, 140+40*index, text, 2);
});
this.finishDraw();
this.setStatus(STATUS.loaded);
}
// format the forecast
parseLocalForecast (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,
}));
}
}

View File

@@ -0,0 +1,245 @@
'use strict';
// navigation handles progress, next/previous and initial load messages from the parent frame
/* globals utils, _StationInfo, STATUS */
/* globals CurrentWeather, LatestObservations, TravelForecast, RegionalForecast, LocalForecast, ExtendedForecast, Almanac */
document.addEventListener('DOMContentLoaded', () => {
navigation.init();
});
const UNITS = {
english: Symbol('english'),
metric: Symbol('metric'),
};
const navigation = (() => {
let weatherParameters = {};
let displays = [];
let initialLoadDone = false;
let currentUnits = UNITS.english;
let playing = false;
const init = () => {
// set up message receive and dispatch accordingly
window.addEventListener('message', (event) => {
// test for trust
if (!event.isTrusted) return;
// get the data
const data = JSON.parse(event.data);
// dispatch event
if (!data.type) return;
switch (data.type) {
case 'latLon':
getWeather(data.message);
break;
case 'units':
setUnits(data.message);
break;
case 'navButton':
handleNavButton(data.message);
break;
default:
console.error(`Unknown event ${data.type}`);
}
}, false);
};
const postMessage = (type, message = {}) => {
const parent = window.parent;
parent.postMessage(JSON.stringify({type, message}, window.location.origin));
};
const getWeather = async (latLon) => {
// reset statuses
initialLoadDone = false;
// get initial weather data
const point = await utils.weather.getPoint(latLon.lat, latLon.lon);
// get stations
const stations = await $.ajax({
type: 'GET',
url: point.properties.observationStations,
dataType: 'json',
crossDomain: true,
});
const StationId = stations.features[0].properties.stationIdentifier;
let city = point.properties.relativeLocation.properties.city;
if (StationId in _StationInfo) {
city = _StationInfo[StationId].City;
city = city.split('/')[0];
}
// populate the weather parameters
weatherParameters.latitude = latLon.lat;
weatherParameters.longitude = latLon.lon;
weatherParameters.zoneId = point.properties.forecastZone.substr(-6);
weatherParameters.radarId = point.properties.radarStation.substr(-3);
weatherParameters.stationId = StationId;
weatherParameters.weatherOffice = point.properties.cwa;
weatherParameters.city = city;
weatherParameters.state = point.properties.relativeLocation.properties.state;
weatherParameters.timeZone = point.properties.relativeLocation.properties.timeZone;
weatherParameters.forecast = point.properties.forecast;
weatherParameters.stations = stations.features;
// update the main process for display purposes
postMessage('weatherParameters', weatherParameters);
// start loading canvases if necessary
if (displays.length === 0) {
displays = [
new CurrentWeather(0,'currentWeather', weatherParameters),
new LatestObservations(1, 'latestObservations', weatherParameters),
new TravelForecast(2, 'travelForecast', weatherParameters),
// Regional Forecast: 0 = regional conditions, 1 = today, 2 = tomorrow
new RegionalForecast(3, 'regionalForecast0', weatherParameters, 0),
new RegionalForecast(4, 'regionalForecast1', weatherParameters, 1),
new RegionalForecast(5, 'regionalForecast2', weatherParameters, 2),
new LocalForecast(6, 'localForecast', weatherParameters),
new ExtendedForecast(7, 'extendedForecast', weatherParameters),
new Almanac(8, 'alamanac', weatherParameters),
];
} else {
// or just call for new data if the canvases already exist
displays.forEach(display => display.getData(weatherParameters));
}
// GetMonthPrecipitation(this.weatherParameters);
// GetAirQuality3(this.weatherParameters);
// ShowDopplerMap(this.weatherParameters);
// GetWeatherHazards3(this.weatherParameters);
};
// receive a status update from a module {id, value}
const updateStatus = (value) => {
// skip if initial load
if (initialLoadDone) return;
// test for loaded status
if (value.status !== STATUS.loaded) return;
// display the first canvas loaded on the next scan (allows display constructors to finish loading)
initialLoadDone = true;
setTimeout(() => {
hideAllCanvases();
displays[value.id].showCanvas();
}, 1);
// send loaded messaged to parent
postMessage('loaded');
// store the display number
};
const hideAllCanvases = () => {
displays.forEach(display => display.hideCanvas());
};
const units = () => currentUnits;
const setUnits = (_unit) => {
const unit = _unit.toLowerCase();
if (unit === 'english') {
currentUnits = UNITS.english;
} else {
currentUnits = UNITS.metric;
}
// TODO: refresh current screen
};
// is playing interface
const isPlaying = () => playing;
// navigation message constants
const msg = {
response: { // display to navigation
previous: Symbol('previous'), // already at first frame, calling function should switch to previous canvas
inProgress: Symbol('inProgress'), // have data to display, calling function should do nothing
next: Symbol('next'), // end of frames reached, calling function should switch to next canvas
},
command: { // navigation to display
firstFrame: Symbol('firstFrame'),
previousFrame: Symbol('previousFrame'),
nextFrame: Symbol('nextFrame'),
lastFrame: Symbol('lastFrame'), // used when navigating backwards from the begining of the next canvas
},
};
// receive naivgation messages from displays
const displayNavMessage = (message) => {
if (message.type === msg.response.previous) loadDisplay(-1);
if (message.type === msg.response.next) loadDisplay(1);
};
// navigate to next or previous
const navTo = (direction) => {
if (direction === msg.command.nextFrame) currentDisplay().navNext();
if (direction === msg.command.previousFrame) currentDisplay().navPrev();
};
// find the next or previous available display
const loadDisplay = (direction) => {
const totalDisplays = displays.length;
const curIdx = currentDisplayIndex();
let idx;
for (let i = 0; i < totalDisplays; i++) {
// convert form simple 0-10 to start at current display index +/-1 and wrap
idx = utils.calc.wrap(curIdx+(i+1)*direction,totalDisplays);
if (displays[idx].status === STATUS.loaded) break;
}
const newDisplay = displays[idx];
// hide all displays
hideAllCanvases();
// show the new display and navigate to an appropriate display
if (direction < 0) newDisplay.showCanvas(msg.command.lastFrame);
if (direction > 0) newDisplay.showCanvas(msg.command.firstFrame);
};
// get the current display index or value
const currentDisplayIndex = () => {
const index = displays.findIndex(display=>display.isActive());
if (index === undefined) console.error('No active display');
return index;
};
const currentDisplay = () => {
return displays[currentDisplayIndex()];
};
const setPlaying = (newValue) => {
playing = newValue;
postMessage('isPlaying', playing);
};
// handle all navigation buttons
const handleNavButton = (button) => {
switch (button) {
case 'playToggle':
setPlaying(!playing);
break;
case 'next':
setPlaying(false);
navTo(msg.command.nextFrame);
break;
case 'previous':
setPlaying(false);
navTo(msg.command.previousFrame);
break;
default:
console.error(`Unknown navButton ${button}`);
}
};
return {
init,
updateStatus,
units,
isPlaying,
displayNavMessage,
msg,
};
})();

View File

@@ -0,0 +1,104 @@
// regional forecast and observations
// type 0 = observations, 1 = first forecast, 2 = second forecast
// makes use of global data retrevial through RegionalForecastData
/* globals WeatherDisplay, utils, STATUS, icons, UNITS, draw, navigation, luxon, RegionalForecastData */
// eslint-disable-next-line no-unused-vars
class RegionalForecast extends WeatherDisplay {
constructor(navId,elemId, weatherParameters, period) {
super(navId,elemId);
// store the period, see above
this.period = period;
// pre-load background image (returns promise)
this.backgroundImage = utils.image.load('images/BackGround5_1.png');
// get the data and update the promise
this.getData(weatherParameters);
}
// get the data from the globally shared object
async getData(weatherParameters) {
super.getData();
// pre-load the base map (returns promise)
let src = 'images/Basemap2.png';
if (weatherParameters.State === 'HI') {
src = 'images/HawaiiRadarMap4.png';
} else if (weatherParameters.State === 'AK') {
src = 'images/AlaskaRadarMap6.png';
}
this.baseMap = utils.image.load(src);
this.data = await RegionalForecastData.updateData(weatherParameters);
this.drawCanvas();
}
async drawCanvas() {
super.drawCanvas();
// break up data into useful values
const {regionalData: data, sourceXY, offsetXY} = this.data;
// fixed offset for all y values when drawing to the map
const mapYOff = 90;
const {DateTime} = luxon;
// draw the header graphics
this.context.drawImage(await this.backgroundImage, 0, 0);
draw.horizontalGradientSingle(this.context, 0, 30, 500, 90, draw.topColor1, draw.topColor2);
draw.triangle(this.context, 'rgb(28, 10, 87)', 500, 30, 450, 90, 500, 90);
// draw the appropriate title
if (this.period === 0) {
draw.titleText(this.context, 'Regional', 'Observations');
} else {
let forecastDate = DateTime.local();
// four conditions to evaluate based on whether the first forecast is for daytime and the requested period
const firstIsDay = data[0][1].daytime;
if (firstIsDay && this.period === 1) forecastDate = forecastDate.plus({days: 1});
if (firstIsDay && this.period === 2); // no change, shown for consistency
if (!firstIsDay && this.period === 1); // no change, shown for consistency
if (!firstIsDay && this.period === 2) forecastDate = forecastDate.plus({days: 1});
// get the name of the day
const dayName = forecastDate.toLocaleString({weekday: 'long'});
// draw the title
if (data[0][this.period].daytime) {
draw.titleText(this.context, 'Forecast for', dayName);
} else {
draw.titleText(this.context, 'Forecast for', dayName + ' Night');
}
}
// draw the map
this.context.drawImage(await this.baseMap, sourceXY.x, sourceXY.y, (offsetXY.x * 2), (offsetXY.y * 2), 0, mapYOff, 640, 312);
await Promise.all(data.map(async city => {
const period = city[this.period];
// draw the icon if possible
const icon = icons.getWeatherRegionalIconFromIconLink(period.icon, !period.daytime);
if (icon) {
this.gifs.push(await utils.image.superGifAsync({
src: icon,
max_width: 42,
loop_delay: 100,
auto_play: true,
canvas: this.canvas,
x: period.x,
y: period.y - 15+mapYOff,
}));
}
// City Name
draw.text(this.context, 'Star4000', '20px', '#ffffff', period.x - 40, period.y - 15+mapYOff, period.name, 2);
// Temperature
let temperature = period.temperature;
if (navigation.units() === UNITS.metric) temperature = Math.round(utils.units.fahrenheitToCelsius(temperature));
draw.text(this.context, 'Star4000 Large Compressed', '28px', '#ffff00', period.x - (temperature.toString().length * 15), period.y + 20+mapYOff, temperature, 2);
}));
this.finishDraw();
this.setStatus(STATUS.loaded);
}
}

View File

@@ -0,0 +1,319 @@
// provide regional forecast and regional observations on a map
// this is a two stage process because the data is shared between both
// and allows for three instances of RegionalForecast to use the same data
/* globals utils, _StationInfo, _RegionalCities */
// a shared global object is used to handle the data for all instances of regional weather
// eslint-disable-next-line no-unused-vars
const RegionalForecastData = (() => {
let dataPromise;
let lastWeatherParameters;
// update the data by providing weatherParamaters
const updateData = (weatherParameters) => {
// test for new data comparing weather paramaters
if (utils.object.shallowEqual(lastWeatherParameters, weatherParameters)) return dataPromise;
// update the promise by calling get data
lastWeatherParameters = weatherParameters;
dataPromise = getData(weatherParameters);
return dataPromise;
};
// return an array of cities each containing an array of 3 weather paramaters 0 = current observation, 1,2 = next forecast periods
const getData = async (weatherParameters) => {
// map offset
const offsetXY = {
x: 240,
y: 117,
};
// get user's location in x/y
const sourceXY = getXYFromLatitudeLongitude(weatherParameters.latitude, weatherParameters.longitude, offsetXY.x, offsetXY.y, weatherParameters.state);
// get latitude and longitude limits
const minMaxLatLon = getMinMaxLatitudeLongitude(sourceXY.x, sourceXY.y, offsetXY.x, offsetXY.y, weatherParameters.state);
// get a target distance
let targetDistance = 2.5;
if (weatherParameters.State === 'HI') targetDistance = 1;
// make station info into an array
const stationInfoArray = Object.keys(_StationInfo).map(key => Object.assign({}, _StationInfo[key], {Name: _StationInfo[key].City, targetDistance}));
// combine regional cities with station info for additional stations
// stations are intentionally after cities to allow cities priority when drawing the map
const combinedCities = [..._RegionalCities, ...stationInfoArray];
// Determine which cities are within the max/min latitude/longitude.
const regionalCities = [];
combinedCities.forEach(city => {
if (city.Latitude > minMaxLatLon.minLat && city.Latitude < minMaxLatLon.maxLat &&
city.Longitude > minMaxLatLon.minLon && city.Longitude < minMaxLatLon.maxLon - 1) {
// default to 1 for cities loaded from _RegionalCities, use value calculate above for remaining stations
const targetDistance = city.targetDistance || 1;
// Only add the city as long as it isn't within set distance degree of any other city already in the array.
const okToAddCity = regionalCities.reduce((acc, testCity) => {
const distance = utils.calc.distance(city.Longitude, city.Latitude, testCity.Longitude, testCity.Latitude);
return acc && distance >= targetDistance;
}, true);
if (okToAddCity) regionalCities.push(city);
}
});
// get regional forecasts and observations (the two are intertwined due to the design of api.weather.gov)
const regionalForecastPromises = regionalCities.map(async city => {
try {
// get the point first, then break down into forecast and observations
const point = await utils.weather.getPoint(city.Latitude, city.Longitude);
// start off the observation task
const observationPromise = getRegionalObservation(point, city);
const forecast = await $.ajax({
url: point.properties.forecast,
dataType: 'json',
crossDomain: true,
});
// get XY on map for city
const cityXY = getXYForCity(city, minMaxLatLon.maxLat, minMaxLatLon.minLon, weatherParameters.state);
// wait for the regional observation if it's not done yet
const observation = await observationPromise;
// format the observation the same as the forecast
const regionalObservation = {
daytime: !!observation.icon.match(/\/day\//),
temperature: utils.units.celsiusToFahrenheit(observation.temperature.value),
name: city.Name,
icon: observation.icon,
x: cityXY.x,
y: cityXY.y,
};
// return a pared-down forecast
// 0th object is the current conditions
// first object is the next period i.e. if it's daytime then it's the "tonight" forecast
// second object is the following period
// always skip the first forecast index because it's what's going on right now
return [
regionalObservation,
buildForecast(forecast.properties.periods[1], city, cityXY),
buildForecast(forecast.properties.periods[2], city, cityXY),
];
} catch (e) {
console.log(`No regional forecast data for '${city.Name}'`);
console.error(e);
return false;
}
});
// wait for the forecasts
const regionalDataAll = await Promise.all(regionalForecastPromises);
// filter out any false (unavailable data)
const regionalData = regionalDataAll.filter(data => data);
// return the weather data and offsets
return {
regionalData,
offsetXY,
sourceXY,
};
};
const buildForecast = (forecast, city, cityXY) => ({
daytime: forecast.isDaytime,
temperature: forecast.temperature||0,
name: city.Name,
icon: forecast.icon,
x: cityXY.x,
y: cityXY.y,
});
const getRegionalObservation = async (point, city) => {
try {
// get stations
const stations = await $.ajax({
type: 'GET',
url: point.properties.observationStations,
dataType: 'json',
crossDomain: true,
});
// get the first station
const station = stations.features[0].id;
// get the observation data
const observation = await $.ajax({
type: 'GET',
url: `${station}/observations/latest`,
dataType: 'json',
crossDomain: true,
});
// return the observation
return observation.properties;
} catch (e) {
console.log(`Unable to get regional observations for ${city.Name}`);
console.error(e);
return false;
}
};
// return the data promise so everyone gets the same thing at the same time
const getDataPromise = () => dataPromise;
// utility latitude/pixel conversions
const getXYFromLatitudeLongitude = (Latitude, Longitude, OffsetX, OffsetY, state) => {
if (state === 'AK') return getXYFromLatitudeLongitudeAK(...arguments);
if (state === 'HI') return getXYFromLatitudeLongitudeHI(...arguments);
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 = function (X, Y, OffsetX, OffsetY, state) {
if (state === 'AK') return getMinMaxLatitudeLongitudeAK(...arguments);
if (state === 'HI') return getMinMaxLatitudeLongitudeHI(...arguments);
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(...arguments);
if (state === 'HI') getXYForCityHI(...arguments);
let x = (City.Longitude - MinLongitude) * 57;
let y = (MaxLatitude - City.Latitude) * 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.Longitude - MinLongitude) * 37;
let y = (MaxLatitude - City.Latitude) * 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.Longitude - MinLongitude) * 57;
let y = (MaxLatitude - City.Latitude) * 70;
if (y < 30) y = 30;
if (y > 282) y = 282;
if (x < 40) x = 40;
if (x > 580) x = 580;
return { x, y };
};
return {
updateData,
getDataPromise,
};
})();

View File

@@ -0,0 +1,183 @@
// travel forecast display
/* globals WeatherDisplay, utils, STATUS, UNITS, draw, navigation, icons, luxon, _TravelCities */
// eslint-disable-next-line no-unused-vars
class TravelForecast extends WeatherDisplay {
constructor(navId, elemId, weatherParameters) {
// special height and width for scrolling
super(navId, elemId);
// pre-load background image (returns promise)
this.backgroundImage = utils.image.load('images/BackGround6_1.png');
// get the data
this.getData(weatherParameters);
// scrolling tracking
this.scrollCount = 0;
this.endDelay = 0;
}
async getData() {
const forecastPromises = _TravelCities.map(async city => {
try {
// get point then forecast
const point = await utils.weather.getPoint(city.Latitude, city.Longitude);
const forecast = await $.ajax({
url: point.properties.forecast,
dataType: 'json',
crossDomain: true,
});
// determine today or tomorrow (shift periods by 1 if tomorrow)
const todayShift = forecast.properties.periods[0].isDaytime? 0:1;
// return a pared-down forecast
return {
today: todayShift === 0,
high: forecast.properties.periods[todayShift].temperature,
low: forecast.properties.periods[todayShift+1].temperature,
name: city.Name,
icon: icons.getWeatherRegionalIconFromIconLink(forecast.properties.periods[todayShift].icon),
};
} catch (e) {
console.error(`GetTravelWeather for ${city.Name} failed`);
console.error(e);
return {name: city.Name};
}
});
// wait for all forecasts
const forecasts = await Promise.all(forecastPromises);
this.data = forecasts;
this.drawCanvas(true);
}
async drawCanvas(newData) {
// there are technically 2 canvases: the standard canvas and the extra-long canvas that contains the complete
// list of cities. The second canvas is copied into the standard canvas to create the scroll
super.drawCanvas();
// create the "long" canvas if necessary
if (!this.longCanvas) {
this.longCanvas = document.createElement('canvas');
this.longCanvas.width = 640;
this.longCanvas.height = 1728;
this.longContext = this.longCanvas.getContext('2d');
}
// set up variables
const cities = this.data;
// draw the long canvas only if there is new data
if (newData) {
this.longContext.clearRect(0,0,this.longCanvas.width,this.longCanvas.height);
// draw the "long" canvas with all cities
draw.box(this.longContext, 'rgb(35, 50, 112)', 0, 0, 640, 1728);
for (let i = 0; i <= 4; i++) {
const y = i * 346;
draw.horizontalGradient(this.longContext, 0, y, 640, y + 346, '#102080', '#001040');
}
await Promise.all(cities.map(async (city, index) => {
// calculate base y value
const y = 50+72*index;
// city name
draw.text(this.longContext, 'Star4000 Large Compressed', '24pt', '#FFFF00', 80, y, city.name, 2);
// check for forecast data
if (city.icon) {
// get temperatures and convert if necessary
let {low, high} = city;
if (navigation.units() === UNITS.metric) {
low = utils.units.fahrenheitToCelsius(low);
high = utils.units.fahrenheitToCelsius(high);
}
// convert to strings with no decimal
const lowString = Math.round(low).toString();
const highString = Math.round(high).toString();
const xLow = (500 - (lowString.length * 20));
draw.text(this.longContext, 'Star4000 Large', '24pt', '#FFFF00', xLow, y, lowString, 2);
const xHigh = (560 - (highString.length * 20));
draw.text(this.longContext, 'Star4000 Large', '24pt', '#FFFF00', xHigh, y, highString, 2);
this.gifs.push(await utils.image.superGifAsync({
src: city.icon,
loop_delay: 100,
auto_play: true,
canvas: this.longCanvas,
x: 330,
y: y - 35,
max_width: 47,
}));
} else {
draw.text(this.longContext, 'Star4000 Small', '24pt', '#FFFFFF', 400, y - 18, 'NO TRAVEL', 2);
draw.text(this.longContext, 'Star4000 Small', '24pt', '#FFFFFF', 400, y, 'DATA AVAILABLE', 2);
}
}));
}
// draw the standard context
this.context.drawImage(await this.backgroundImage, 0, 0);
draw.horizontalGradientSingle(this.context, 0, 30, 500, 90, draw.topColor1, draw.topColor2);
draw.triangle(this.context, 'rgb(28, 10, 87)', 500, 30, 450, 90, 500, 90);
draw.titleText(this.context, 'Travel Forecast', 'For ' + this.getTravelCitiesDayName(cities));
draw.text(this.context, 'Star4000 Small', '24pt', '#FFFF00', 455, 105, 'LOW', 2);
draw.text(this.context, 'Star4000 Small', '24pt', '#FFFF00', 510, 105, 'HIGH', 2);
// copy the scrolled portion of the canvas for the initial run before the scrolling starts
this.context.drawImage(this.longCanvas, 0, 0, 640, 289, 0, 110, 640, 289);
// set up scrolling one time
if (!this.scrollInterval) {
this.scrollInterval = window.setInterval(() => {
if (this.isActive()) {
// get a fresh canvas
const longCanvas = this.getLongCanvas();
// increment scrolling
this.scrollCount++;
// wait 3 seconds at begining
if (this.scrollCount < 150) return;
// calculate scroll offset and don't go past end of canvas
const offsetY = Math.min(longCanvas.height-289, (this.scrollCount-150));
// copy the scrolled portion of the canvas
this.context.drawImage(longCanvas, 0, offsetY, 640, 289, 0, 110, 640, 289);
// track end of scrolling for 3 seconds
if (offsetY >= longCanvas.height-289) this.endDelay++;
// TODO: report playback done
} else {
// reset scroll to top of image
this.scrollCount = 0;
this.endDelay = 0;
}
}, 20);
}
this.finishDraw();
this.setStatus(STATUS.loaded);
}
getTravelCitiesDayName(cities) {
const {DateTime} = luxon;
// 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;
}
}

View File

@@ -0,0 +1,410 @@
'use strict';
// radar utilities
/* globals _Units, Units, SuperGif */
// eslint-disable-next-line no-unused-vars
const utils = (() => {
// ****************************** weather data ********************************
const getPoint = async (lat, lon) => {
try {
return await $.ajax({
type: 'GET',
url: `https://api.weather.gov/points/${lat},${lon}`,
dataType: 'json',
crossDomain: true,
});
} catch (e) {
console.error('Unable to get point');
console.error(lat,lon);
console.error(e);
return false;
}
};
// ****************************** load images *********************************
// load an image from a blob or url
const loadImg = (imgData) => {
return new Promise(resolve => {
const img = new Image();
img.onload = (e) => {
resolve(e.target);
};
if (imgData instanceof Blob) {
img.src = window.URL.createObjectURL(imgData);
} else {
img.src = imgData;
}
});
};
// async version of SuperGif
const superGifAsync = (e) => {
return new Promise(resolve => {
const gif = new SuperGif(e);
gif.load(() => resolve(gif));
});
};
// *********************************** unit conversions ***********************
Math.round2 = (value, decimals) => Number(Math.round(value + 'e' + decimals) + 'e-' + decimals);
const mphToKph = (Mph) => Math.round(Mph * 1.60934);
const kphToMph = (Kph) => Math.round(Kph / 1.60934);
const celsiusToFahrenheit = (Celsius) => Math.round(Celsius * 9 / 5 + 32);
const fahrenheitToCelsius = (Fahrenheit) => Math.round2(((Fahrenheit) - 32) * 5 / 9, 1);
const milesToKilometers = (Miles) => Math.round(Miles * 1.60934);
const kilometersToMiles = (Kilometers) => Math.round(Kilometers / 1.60934);
const feetToMeters = (Feet) => Math.round(Feet * 0.3048);
const metersToFeet = (Meters) => Math.round(Meters / 0.3048);
const inchesToCentimeters = (Inches) => Math.round2(Inches * 2.54, 2);
const pascalToInHg = (Pascal) => Math.round2(Pascal*0.0002953,2);
// ***************************** calculations **********************************
const relativeHumidity = (Temperature, DewPoint) => {
const T = Temperature;
const TD = DewPoint;
return Math.round(100 * (Math.exp((17.625 * TD) / (243.04 + TD)) / Math.exp((17.625 * T) / (243.04 + T))));
};
const heatIndex = (Temperature, RelativeHumidity) => {
const T = Temperature;
const RH = RelativeHumidity;
let HI = 0.5 * (T + 61.0 + ((T - 68.0) * 1.2) + (RH * 0.094));
let ADJUSTMENT;
if (T >= 80) {
HI = -42.379 + 2.04901523 * T + 10.14333127 * RH - 0.22475541 * T * RH - 0.00683783 * T * T - 0.05481717 * RH * RH + 0.00122874 * T * T * RH + 0.00085282 * T * RH * RH - 0.00000199 * T * T * RH * RH;
if (RH < 13 && (T > 80 && T < 112)) {
ADJUSTMENT = ((13 - RH) / 4) * Math.sqrt((17 - Math.abs(T - 95)) / 17);
HI -= ADJUSTMENT;
} else if (RH > 85 && (T > 80 && T < 87)) {
ADJUSTMENT = ((RH - 85) / 10) * ((87 - T) / 5);
HI += ADJUSTMENT;
}
}
if (HI < Temperature) {
HI = Temperature;
}
return Math.round(HI);
};
const windChill = (Temperature, WindSpeed) => {
if (WindSpeed === '0' || WindSpeed === 'Calm' || WindSpeed === 'NA') {
return '';
}
const T = Temperature;
const V = WindSpeed;
return Math.round(35.74 + (0.6215 * T) - (35.75 * Math.pow(V, 0.16)) + (0.4275 * T * Math.pow(V, 0.16)));
};
// wind direction
const directionToNSEW = (Direction) => {
const val = Math.floor((Direction / 22.5) + 0.5);
const arr = ['N', 'NNE', 'NE', 'ENE', 'E', 'ESE', 'SE', 'SSE', 'S', 'SSW', 'SW', 'WSW', 'W', 'WNW', 'NW', 'NNW'];
return arr[(val % 16)];
};
const distance = (x1 ,y1, x2, y2) => Math.sqrt((x2-=x1)*x2 + (y2-=y1)*y2);
// wrap a number to 0-m
const wrap = (x,m) => (x%m + m)%m;
// ********************************* date functions ***************************
const getDateFromUTC = (date, utc) => {
const time = utc.split(':');
return new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate(), time[0], time[1], 0));
};
const getTimeZoneOffsetFromUTC = (timezone) => {
switch (timezone) {
case 'EST':
return -5;
case 'EDT':
return -4;
case 'CST':
return -6;
case 'CDT':
return -5;
case 'MST':
return -7;
case 'MDT':
return -6;
case 'PST':
return -8;
case 'PDT':
return -7;
case 'AST':
case 'AKST':
return -9;
case 'ADT':
case 'AKDT':
return -8;
case 'HST':
return -10;
case 'HDT':
return -9;
default:
return null;
}
};
Date.prototype.getTimeZone = function () {
const tz = this.toLocaleTimeString('en-us', { timeZoneName: 'short' }).split(' ')[2];
if (tz === null){
switch (this.toTimeString().split(' ')[2]) {
case '(Eastern':
return 'EST';
case '(Central':
return 'CST';
case '(Mountain':
return 'MST';
case '(Pacific':
return 'PST';
case '(Alaskan':
return 'AST';
case '(Hawaiian':
return 'HST';
default:
}
} else if (tz.length === 4) {
// Fix weird bug in Edge where it returns the timezone with a null character in the first position.
return tz.substr(1);
}
return tz;
};
Date.prototype.addHours = function (hours) {
var dat = new Date(this.valueOf());
dat.setHours(dat.getHours() + hours);
return dat;
};
Date.prototype.getDayShortName = function () {
var days = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
return days[this.getDay()];
};
Date.prototype.getMonthShortName = function () {
var months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
return months[this.getMonth()];
};
const dateToTimeZone = (date, timezone) => {
const OldOffset = getTimeZoneOffsetFromUTC(date.getTimeZone());
const NewOffset = getTimeZoneOffsetFromUTC(timezone);
let dt = new Date(date);
dt = dt.addHours(OldOffset * -1);
dt = dt.addHours(NewOffset);
return dt;
};
const getDateFromTime = (date, time, timezone) => {
const Time = time.split(':');
if (timezone) {
const Offset = getTimeZoneOffsetFromUTC(timezone) * -1;
const newDate = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate(), Time[0], Time[1], 0));
return newDate.addHours(Offset);
} else {
return new Date(date.getFullYear(), date.getMonth(), date.getDate(), Time[0], Time[1], 0);
}
};
Date.prototype.getFormattedTime = function () {
let hours;
let minutes;
let ampm;
switch (_Units) {
case Units.English:
hours = this.getHours() === 0 ? '12' : this.getHours() > 12 ? this.getHours() - 12 : this.getHours();
minutes = (this.getMinutes() < 10 ? '0' : '') + this.getMinutes();
ampm = this.getHours() < 12 ? 'am' : 'pm';
return hours + ':' + minutes + ' ' + ampm;
default:
hours = (this.getHours() < 10 ? ' ' : '') + this.getHours();
minutes = (this.getMinutes() < 10 ? '0' : '') + this.getMinutes();
return hours + ':' + minutes;
}
};
Date.prototype.toTimeAMPM = function () {
const date = this;
let hours = date.getHours();
let minutes = date.getMinutes();
let ampm = hours >= 12 ? 'pm' : 'am';
hours = hours % 12;
hours = hours ? hours : 12; // the hour '0' should be '12'
minutes = minutes < 10 ? '0' + minutes : minutes;
return hours + ':' + minutes + ' ' + ampm;
};
const xmlDateToJsDate = (XmlDate) => {
let bits = XmlDate.split(/[-T:+]/g);
if (bits[5] === undefined) {
console.log('bit[5] is undefined');
}
bits[5] = bits[5].replace('Z', '');
const d = new Date(bits[0], bits[1] - 1, bits[2]);
d.setHours(bits[3], bits[4], bits[5]);
// Case for when no time zone offset if specified
if (bits.length < 8) {
bits.push('00');
bits.push('00');
}
// Get supplied time zone offset in minutes
const sign = /\d\d-\d\d:\d\d$/.test(XmlDate) ? '-' : '+';
const offsetMinutes = (sign==='-'?-1:1)*(bits[6] * 60 + Number(bits[7]));
// Apply offset and local timezone
// d is now a local time equivalent to the supplied time
return d.setMinutes(d.getMinutes() - offsetMinutes - d.getTimezoneOffset());
};
const timeTo24Hour = (Time) => {
const AMPM = Time.substr(Time.length - 2);
const MM = Time.split(':')[1].substr(0, 2);
let HH = Time.split(':')[0];
switch (AMPM.toLowerCase()) {
case 'am':
if (HH === '12') HH = '0';
break;
case 'pm':
if (HH !== '12') HH = (parseInt(HH) + 12).toString();
break;
default:
}
return HH + ':' + MM;
};
// compare objects on shallow equality (nested objects ignored)
const shallowEqual= (obj1, obj2) => {
if (typeof obj1 !== 'object') return false;
if (typeof obj2 !== 'object') return false;
const keys1 = Object.keys(obj1);
const keys2 = Object.keys(obj2);
if (keys1.length !== keys2.length) return false;
for (const key of keys1) {
if (typeof obj1[key] !== 'object' && obj1[key] !== obj2[key]) return false;
}
return true;
};
// ********************************* strings *********************************************
if (!String.prototype.startsWith) {
String.prototype.startsWith = function (searchString, position) {
position = position || 0;
return this.substr(position, searchString.length) === searchString;
};
}
if (!String.prototype.endsWith) {
String.prototype.endsWith = function(searchString, position) {
var subjectString = this.toString();
if (typeof position !== 'number' || !isFinite(position) || Math.floor(position) !== position || position > subjectString.length) {
position = subjectString.length;
}
position -= searchString.length;
var lastIndex = subjectString.lastIndexOf(searchString, position);
return lastIndex !== -1 && lastIndex === position;
};
}
String.prototype.wordWrap = function () {
let str = this;
let m = ((arguments.length >= 1) ? arguments[0] : 75);
let b = ((arguments.length >= 2) ? arguments[1] : '\n');
let c = ((arguments.length >= 3) ? arguments[2] : false);
let i, j, l, s, r;
str += '';
if (m < 1) {
return str;
}
for (i = -1, l = (r = str.split(/\r\n|\n|\r/)).length; ++i < l; r[i] += s) {
// @todo: Split this up over many more lines and more semantic variable names
// so it becomes readable
for (s = r[i], r[i] = '';
s.length > m;
r[i] += s.slice(0, j) + ((s = s.slice(j)).length ? b : '')) {
j = c === 2 || (j = s.slice(0, m + 1).match(/\S*(\s)?$/))[1]
? m
: j.input.length - j[0].length || c === true && m ||
j.input.length + (j = s.slice(m).match(/^\S*/))[0].length;
}
}
return r.join('\n').replace(/\n /g, '\n');
};
// return an orderly object
return {
image: {
load: loadImg,
superGifAsync,
},
weather: {
getPoint,
},
units: {
mphToKph,
kphToMph,
celsiusToFahrenheit,
fahrenheitToCelsius,
milesToKilometers,
kilometersToMiles,
feetToMeters,
metersToFeet,
inchesToCentimeters,
pascalToInHg,
},
calc: {
relativeHumidity,
heatIndex,
windChill,
directionToNSEW,
distance,
wrap,
},
dateTime: {
getDateFromUTC,
getTimeZoneOffsetFromUTC,
dateToTimeZone,
getDateFromTime,
xmlDateToJsDate,
timeTo24Hour,
},
object: {
shallowEqual,
},
};
})();
// pass data through local server as CORS workaround
$.ajaxCORS = function (e) {
// modify the URL
e.url = e.url.replace('https://api.weather.gov/', '');
// call the ajax function
return $.ajax(e);
};

View File

@@ -0,0 +1,316 @@
// base weather display class
/* globals navigation, utils, draw, UNITS, luxon */
const STATUS = {
loading: Symbol('loading'),
loaded: Symbol('loaded'),
failed: Symbol('failed'),
noData: Symbol('noData'),
};
// eslint-disable-next-line no-unused-vars
class WeatherDisplay {
constructor(navId, elemId, canvasWidth, canvasHeight) {
// navId is used in messaging
this.navId = navId;
this.elemId = undefined;
this.gifs = [];
this.data = undefined;
this.loadingStatus = STATUS.loading;
// default navigation timing
this.timing = {
totalScreens: 1,
baseDelay: 5000, // 5 seconds
delay: 1, // 1*1second = 1 second total display time
};
this.navBaseCount = 0;
this.screenIndex = 0;
this.setStatus(STATUS.loading);
this.createCanvas(elemId, canvasWidth, canvasHeight);
}
// set data status and send update to navigation module
setStatus(value) {
this.status = value;
navigation.updateStatus({
id: this.navId,
status: this.status,
});
}
get status() {
return this.loadingStatus;
}
set status(state) {
this.loadingStatus = state;
}
createCanvas(elemId, width = 640, height = 480) {
// only create it once
if (this.elemId) return;
this.elemId = elemId;
const container = document.getElementById('container');
container.innerHTML += `<canvas id='${elemId+'Canvas'}' width='${width}' height='${height}'/ style='display: none;'>`;
}
// get necessary data for this display
getData() {
// clear current data
this.data = undefined;
// set status
this.setStatus(STATUS.loading);
}
drawCanvas() {
// stop all gifs
this.gifs.forEach(gif => gif.pause());
// delete the gifs
this.gifs.length = 0;
// refresh the canvas
this.canvas = document.getElementById(this.elemId+'Canvas');
this.context = this.canvas.getContext('2d');
// clear the canvas
this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);
}
finishDraw() {
let OkToDrawCurrentConditions = true;
let OkToDrawNoaaImage = true;
let OkToDrawCurrentDateTime = true;
let OkToDrawLogoImage = true;
// let OkToDrawCustomScrollText = false;
let bottom = undefined;
// visibility tests
// if (_ScrollText !== '') OkToDrawCustomScrollText = true;
if (this.elemId === 'almanac') OkToDrawNoaaImage = false;
if (this.elemId === 'almanacTides') OkToDrawNoaaImage = false;
if (this.elemId === 'outlook') OkToDrawNoaaImage = false;
if (this.elemId === 'marineForecast')OkToDrawNoaaImage = false;
if (this.elemId === 'airQuailty') OkToDrawNoaaImage = false;
if (this.elemId === 'travelForecast') OkToDrawNoaaImage = false;
if (this.elemId === 'regionalForecast1')OkToDrawNoaaImage = false;
if (this.elemId === 'regionalForecast2') OkToDrawNoaaImage = false;
if (this.elemId === 'regionalObservations') OkToDrawNoaaImage = false;
if (this.elemId === 'localRadar') {
OkToDrawCurrentConditions = false;
OkToDrawCurrentDateTime = false;
OkToDrawNoaaImage = false;
// OkToDrawCustomScrollText = false;
}
if (this.elemId === 'hazards') {
OkToDrawNoaaImage = false;
bottom = true;
OkToDrawLogoImage = false;
}
// draw functions
if (OkToDrawCurrentDateTime) {
this.drawCurrentDateTime(bottom);
// auto clock refresh
if (!this.dateTimeInterval) {
setInterval(() => this.drawCurrentDateTime(bottom), 100);
}
}
if (OkToDrawLogoImage) this.drawLogoImage();
if (OkToDrawNoaaImage) this.drawNoaaImage();
// TODO: fix current conditions scroll
// if (OkToDrawCurrentConditions) DrawCurrentConditions(WeatherParameters, this.context);
// TODO: add custom scroll text
// if (OkToDrawCustomScrollText) DrawCustomScrollText(WeatherParameters, context);
}
// TODO: update clock automatically
drawCurrentDateTime(bottom) {
// only draw if canvas is active to conserve battery
if (!this.isActive()) return;
const {DateTime} = luxon;
const font = 'Star4000 Small';
const size = '24pt';
const color = '#ffffff';
const shadow = 2;
// on the first pass store the background for the date and time
if (!this.dateTimeBackground) {
this.dateTimeBackground = this.context.getImageData(410, 30, 175, 60);
}
// Clear the date and time area.
if (bottom) {
draw.box(this.context, 'rgb(25, 50, 112)', 0, 389, 640, 16);
} else {
this.context.putImageData(this.dateTimeBackground, 410, 30);
}
// Get the current date and time.
const now = DateTime.local();
//time = "11:35:08 PM";
const time = now.toLocaleString(DateTime.TIME_WITH_SECONDS).padStart(11,' ');
let x,y;
if (bottom) {
x = 400;
y = 402;
} else {
x = 410;
y = 65;
}
if (navigation.units() === UNITS.metric) {
x += 45;
}
draw.text(this.context, font, size, color, x, y, time.toUpperCase(), shadow); //y += 20;
const date = now.toFormat(' ccc LLL ') + now.day.toString().padStart(2,' ');
if (bottom) {
x = 55;
y = 402;
} else {
x = 410;
y = 85;
}
draw.text(this.context, font, size, color, x, y, date.toUpperCase(), shadow);
}
async drawNoaaImage () {
// load the image and store locally
if (!this.drawNoaaImage.image) {
this.drawNoaaImage.image = utils.image.load('images/noaa5.gif');
}
// wait for the image to load completely
const img = await this.drawNoaaImage.image;
this.context.drawImage(img, 356, 39);
}
async drawLogoImage () {
// load the image and store locally
if (!this.drawLogoImage.image) {
this.drawLogoImage.image = utils.image.load('images/Logo3.png');
}
// wait for the image load completely
const img = await this.drawLogoImage.image;
this.context.drawImage(img, 50, 30, 85, 67);
}
// show/hide the canvas and start/stop the navigation timer
showCanvas(navCmd) {
// if a nav command is present call it to set the screen index
if (navCmd === navigation.msg.command.firstFrame) this.navNext(navCmd);
if (navCmd === navigation.msg.command.lastFrame) this.navPrev(navCmd);
// see if the canvas is already showing
if (this.canvas.style.display === 'block') return false;
// show the canvas
this.canvas.style.display = 'block';
// reset timing
this.startNavCount(navigation.isPlaying());
// refresh the canvas (incase the screen index chagned)
if (navCmd) this.drawCanvas();
}
hideCanvas() {
this.stopNavBaseCount(true);
if (!this.canvas) return;
this.canvas.style.display = 'none';
}
isActive() {
return document.getElementById(this.elemId+'Canvas').offsetParent !== null;
}
// navigation timings
// totalScreens = total number of screens that are available
// baseDelay = ms to delay before re-evaluating screenIndex
// delay: three options
// integer = each screen will display for this number of baseDelays
// [integer, integer, ...] = screenIndex 0 displays for integer[0]*baseDelay, etc.
// [{time, si}, ...] = time as above, si is specific screen index to display during this interval
// if the array forms are used totalScreens is overwritten by the size of the array
navBaseTime() {
// see if play is active and screen is active
if (!navigation.isPlaying() || !this.isActive()) return;
// increment the base count
this.navBaseCount++;
// update total screens
if (Array.isArray(this.timing.delay)) this.timing.totalScreens = this.timing.delay.length;
// determine type of timing
// simple delay
if (typeof this.timing.delay === 'number') {
this.navNext();
return;
}
}
// navigate to next screen
navNext(command) {
// check for special 'first frame' command
if (command === navigation.msg.command.firstFrame) {
this.resetNavBaseCount();
this.drawCanvas();
return;
}
// increment screen index
this.screenIndex++;
// test for end reached
if (this.screenIndex >= this.timing.totalScreens) {
this.sendNavDisplayMessage(navigation.msg.response.next);
this.stopNavBaseCount();
return;
}
// if the end was not reached, update the canvas
this.drawCanvas();
}
// navigate to previous screen
navPrev(command) {
// check for special 'last frame' command
if (command === navigation.msg.command.lastFrame) {
this.screenIndex = this.timing.totalScreens-1;
this.drawCanvas();
return;
}
// decrement screen index
this.screenIndex--;
// test for end reached
if (this.screenIndex < 0) {
this.sendNavDisplayMessage(navigation.msg.response.previous);
return;
}
// if the end was not reached, update the canvas
this.drawCanvas();
}
// start and stop base counter
startNavCount(reset) {
if (reset) this.resetNavBaseCount();
if (!this.navInterval) this.navInterval = setInterval(()=>this.navBaseTime(), this.timing.baseDelay);
}
stopNavBaseCount(reset) {
clearInterval(this.navInterval);
this.navInterval = undefined;
if (reset) this.resetNavBaseCount();
}
resetNavBaseCount() {
this.navBaseCount = 0;
this.screenIndex = 0;
}
sendNavDisplayMessage(message) {
navigation.displayNavMessage({
id: this.navId,
type: message,
});
}
}

94
server/scripts/timer.js Normal file
View File

@@ -0,0 +1,94 @@
if (window.Worker)
{
var _TimerWorkCallBacks = [];
var _TimerIds = 0;
var _TimerWorker = new window.Worker("scripts/TimerWorker.js");
_TimerWorker.onmessage = function (e)
{
var Message = e.data;
switch (Message.Action)
{
case "ELASPED":
var TimerWorkCallBack = _TimerWorkCallBacks[Message.Id];
if (!TimerWorkCallBack)
{
return;
}
var Window = TimerWorkCallBack.Window;
var CallBack = TimerWorkCallBack.CallBack;
var Arguments = TimerWorkCallBack.Arguments;
if (typeof (CallBack) === 'string')
{
CallBack = new Function(CallBack);
}
if (typeof (CallBack) === 'function')
{
CallBack.apply(Window, Arguments);
}
if (Message.RunOnce == true)
{
_clearIntervalWorker(Message.Id);
}
break;
}
};
window.setIntervalWorker = function (CallBack, TimeOut, Arguments)
{
Arguments = Array.prototype.slice.call(arguments).slice(2);
return _setIntervalWorker(false, CallBack, TimeOut, Arguments, window);
};
window.setTimeoutWorker = function (CallBack, TimeOut, Arguments)
{
Arguments = Array.prototype.slice.call(arguments).slice(2);
return _setIntervalWorker(true, CallBack, TimeOut, Arguments, window);
};
var _setIntervalWorker = function (RunOnce, CallBack, TimeOut, Arguments, Window)
{
var Id = ++_TimerIds;
_TimerWorkCallBacks[Id] = {
CallBack: CallBack,
Arguments: Arguments,
Window: Window,
};
_TimerWorker.postMessage({
Action: "START",
RunOnce: RunOnce,
Id: Id,
TimeOut: TimeOut,
});
return Id;
};
window.clearIntervalWorker = function (Id)
{
_clearIntervalWorker(Id);
};
window.clearTimeoutWorker = function (Id)
{
_clearIntervalWorker(Id);
};
var _clearIntervalWorker = function (Id)
{
if (!Id)
{
return;
}
_TimerWorker.postMessage({
Action: "STOP",
Id: Id,
});
delete _TimerWorkCallBacks[Id];
};
}

4942
server/scripts/twc3.js Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

196
server/scripts/vendor/jeoquery.js vendored Normal file
View File

@@ -0,0 +1,196 @@
/*
* jeoQuery v0.5.1
*
* Copyright 2012-2038, Thomas Haukland
* MIT license.
*
*/
var jeoquery = (function ($) {
var my = {};
my.defaultData = {
userName: 'vbguyny',
lang: 'en'
};
my.defaultCountryCode = 'US';
my.defaultLanguage = 'en';
my.geoNamesApiServer = 'api.geonames.org';
my.geoNamesProtocol = 'http';
my.featureClass = {
AdministrativeBoundary: 'A',
Hydrographic: 'H',
Area: 'L',
PopulatedPlace: 'P',
RoadRailroad: 'R',
Spot: 'S',
Hypsographic: 'T',
Undersea: 'U',
Vegetation: 'V'
};
my.getGeoNames = function(method, data, callback, errorcallback) {
var deferred = $.Deferred();
if (!method || !methods[method]) {
throw 'Invalid geonames method "' + method + '".';
}
$.ajax({
url: my.geoNamesProtocol + '://' + my.geoNamesApiServer + '/' + method + 'JSON',
dataType: 'jsonp',
data: $.extend({}, my.defaultData, data),
// GeoNames expects "traditional" param serializing
traditional: true,
success: function(data) {
deferred.resolve(data);
if (!!callback) callback(data);
},
error: function (xhr, textStatus) {
deferred.reject(xhr, textStatus);
//alert('Ooops, geonames server returned: ' + textStatus);
if (!!errorcallback) errorcallback(textStatus);
}
});
return deferred.promise();
};
function formatDate(date) {
var dateQs = '';
if (date) {
dateQs = date.getFullYear() + '-' + (date.getMonth() + 1) + '-' + date.getDate();
}
return dateQs;
}
var methods = {
astergdem: {params: ['lat', 'lng'] },
children: {params: ['geonameId', 'maxRows'] },
cities: {params: ['north', 'south', 'east', 'west', 'lang'] },
countryCode: {params: ['lat', 'lng', 'type', 'lang', 'radius'] },
countryInfo: {params: ['country', 'lang'] },
countrySubdivision: {params: ['lat', 'lng', 'level', 'lang', 'radius'] },
earthquakes: {params: ['north', 'south', 'east', 'west', 'date', 'maxRows', 'minMagnitude'] },
findNearby: {params: ['lat', 'lng', 'featureClass', 'featureCode', 'radius', 'style', 'maxRows'] },
findNearbyPlacename: {params: ['lat', 'lng', 'radius', 'style'] },
findNearbyPostalCodes: {params: ['lat', 'lng', 'radius', 'style', 'maxRows', 'country', 'localCountry', 'postalCode'] },
findNearbyStreets: {params: ['lat', 'lng', 'radius', 'maxRows'] },
findNearbyStreetsOSM: {params: ['lat', 'lng'] },
findNearbyWeather: {params: ['lat', 'lng'] },
findNearbyWikipedia: {params: ['lat', 'lng', 'radius', 'maxRows', 'country', 'postalCode'] },
findNearestAddress: {params: ['lat', 'lng'] },
findNearestIntersection: {params: ['lat', 'lng'] },
findNearestIntersectionOSM: {params: ['lat', 'lng', 'radius', 'maxRows'] },
findNearbyPOIsOSM: {params: ['lat', 'lng'] },
get: {params: ['geonameId', 'lang', 'style'] },
gtopo30: {params: ['lat', 'lng'] },
hierarchy: {params: ['geonameId'] },
neighbourhood: {params: ['lat', 'lng'] },
neighbours: {params: ['geonameId', 'country'] },
ocean: {params: ['lat', 'lng', 'radius'] },
postalCodeCountryInfo: {params: [] },
postalCodeLookup: {params: ['postalcode', 'country', 'maxRows', 'charset'] },
postalCodeSearch: {params: ['postalcode', 'postalcode_startsWith', 'placename_startsWith', 'country', 'countryBias', 'maxRows', 'style', 'operator', 'charset', 'isReduced'] },
search: {params: [ 'q', 'name', 'name_equals', 'name_startsWith', 'maxRows', 'startRow', 'country', 'countryBias', 'continentCode', 'adminCode1', 'adminCode2', 'adminCode3', 'featureClass', 'featureCode', 'lang', 'type', 'style', 'isNameRequired', 'tag', 'operator', 'charset', 'fuzzy'] },
siblings: {params: ['geonameId'] },
srtm3: {params: ['lat', 'lng'] },
timezone: {params: ['lat', 'lng', 'radius', 'date'] },
weather: {params: ['north', 'south', 'east', 'west', 'maxRows'] },
weatherIcao: {params: ['ICAO'] },
wikipediaBoundingBox: {params: ['north', 'south', 'east', 'west', 'lang', 'maxRows'] },
wikipediaSearch: {params: ['q', 'title', 'lang', 'maxRows'] }
};
return my;
}(jQuery));
(function ($) {
$.fn.jeoCountrySelect = function (options) {
var el = this;
$.when(jeoquery.getGeoNames('countryInfo'))
.then(function (data) {
var sortedNames = data.geonames;
if (data.geonames.sort) {
sortedNames = data.geonames.sort(function (a, b) {
return a.countryName.localeCompare(b.countryName);
});
}
// Insert blank choice
sortedNames.unshift({countryCode:'', countryName:''});
var html = $.map(sortedNames, function(c) {
return '<option value="' + c.countryCode + '">' + c.countryName + '</option>';
});
el.html(html);
if (options && options.callback) options.callback(sortedNames);
});
};
$.fn.jeoPostalCodeLookup = function (options) {
this.on("change", function () {
var code = $(this).val();
var country = options.country || jeoquery.defaultCountryCode;
if (options.countryInput) {
country = options.countryInput.val() || jeoquery.defaultCountry;
}
if (code) {
jeoquery.getGeoNames('postalCodeLookup', {postalcode: code, country: country}, function (data) {
if (data && data.postalcodes && data.postalcodes.length > 0) {
if (options) {
if (options.target) {
options.target.val(data.postalcodes[0].placeName);
}
if (options.callback) {
options.callback(data.postalcodes[0]);
}
}
}
});
}
});
};
$.fn.jeoCityAutoComplete = function (options) {
this.autocomplete({
source: function (request, response) {
jeoquery.getGeoNames('search', {
featureClass: jeoquery.featureClass.PopulatedPlace,
style: ((options && options.style) ? options.style : "medium"),
maxRows: 12,
name_startsWith: request.term
}, function (data) {
response(function() {
data.geonames = $.map(data.geonames, function (item) {
var displayName = item.name + (item.adminName1 ? ", " + item.adminName1 : "") + ", " + item.countryName;
if (options && options.displayNameFunc) {
displayName = options.displayNameFunc(item);
if (displayName === null)
return null;
}
return {
label: displayName,
value: displayName,
details: item
};
});
if (options && options.preProcessResults) {
options.preProcessResults(data.geonames);
}
return data.geonames;
}());
});
},
minLength: 2,
select: function( event, ui ) {
if (ui && ui.item && options && options.callback) {
options.callback(ui.item.details);
}
},
open: function () {
$(this).removeClass("ui-corner-all").addClass("ui-corner-top");
},
close: function () {
$(this).removeClass("ui-corner-top").addClass("ui-corner-all");
}
});
};
})(jQuery);

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

1169
server/scripts/vendor/libgif.js vendored Normal file

File diff suppressed because it is too large Load Diff

1
server/scripts/vendor/luxon.js vendored Normal file

File diff suppressed because one or more lines are too long

2
server/scripts/vendor/nosleep.min.js vendored Normal file
View File

@@ -0,0 +1,2 @@
// NoSleep.min.js v0.5.0 - git.io/vfn01 - Rich Tibbett - MIT license
!function(A){function e(A,e,o){var t=document.createElement("source");t.src=o,t.type="video/"+e,A.appendChild(t)}var o={Android:/Android/gi.test(navigator.userAgent),iOS:/AppleWebKit/.test(navigator.userAgent)&&/Mobile\/\w+/.test(navigator.userAgent)},t={WebM:"data:video/webm;base64,GkXfo0AgQoaBAUL3gQFC8oEEQvOBCEKCQAR3ZWJtQoeBAkKFgQIYU4BnQI0VSalmQCgq17FAAw9CQE2AQAZ3aGFtbXlXQUAGd2hhbW15RIlACECPQAAAAAAAFlSua0AxrkAu14EBY8WBAZyBACK1nEADdW5khkAFVl9WUDglhohAA1ZQOIOBAeBABrCBCLqBCB9DtnVAIueBAKNAHIEAAIAwAQCdASoIAAgAAUAmJaQAA3AA/vz0AAA=",MP4:"data:video/mp4;base64,AAAAHGZ0eXBpc29tAAACAGlzb21pc28ybXA0MQAAAAhmcmVlAAAAG21kYXQAAAGzABAHAAABthADAowdbb9/AAAC6W1vb3YAAABsbXZoZAAAAAB8JbCAfCWwgAAAA+gAAAAAAAEAAAEAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAIVdHJhawAAAFx0a2hkAAAAD3wlsIB8JbCAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAQAAAAAAIAAAACAAAAAABsW1kaWEAAAAgbWRoZAAAAAB8JbCAfCWwgAAAA+gAAAAAVcQAAAAAAC1oZGxyAAAAAAAAAAB2aWRlAAAAAAAAAAAAAAAAVmlkZW9IYW5kbGVyAAAAAVxtaW5mAAAAFHZtaGQAAAABAAAAAAAAAAAAAAAkZGluZgAAABxkcmVmAAAAAAAAAAEAAAAMdXJsIAAAAAEAAAEcc3RibAAAALhzdHNkAAAAAAAAAAEAAACobXA0dgAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAIAAgASAAAAEgAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABj//wAAAFJlc2RzAAAAAANEAAEABDwgEQAAAAADDUAAAAAABS0AAAGwAQAAAbWJEwAAAQAAAAEgAMSNiB9FAEQBFGMAAAGyTGF2YzUyLjg3LjQGAQIAAAAYc3R0cwAAAAAAAAABAAAAAQAAAAAAAAAcc3RzYwAAAAAAAAABAAAAAQAAAAEAAAABAAAAFHN0c3oAAAAAAAAAEwAAAAEAAAAUc3RjbwAAAAAAAAABAAAALAAAAGB1ZHRhAAAAWG1ldGEAAAAAAAAAIWhkbHIAAAAAAAAAAG1kaXJhcHBsAAAAAAAAAAAAAAAAK2lsc3QAAAAjqXRvbwAAABtkYXRhAAAAAQAAAABMYXZmNTIuNzguMw=="},i=function(){return o.iOS?this.noSleepTimer=null:o.Android&&(this.noSleepVideo=document.createElement("video"),this.noSleepVideo.setAttribute("loop",""),e(this.noSleepVideo,"webm",t.WebM),e(this.noSleepVideo,"mp4",t.MP4)),this};i.prototype.enable=function(A){o.iOS?(this.disable(),this.noSleepTimer=window.setInterval(function(){window.location.href='/',window.setTimeout(window.stop,0)},A||15e3)):o.Android&&this.noSleepVideo.play()},i.prototype.disable=function(){o.iOS?this.noSleepTimer&&(window.clearInterval(this.noSleepTimer),this.noSleepTimer=null):o.Android&&this.noSleepVideo.pause()},A.NoSleep=i}(this);

317
server/scripts/vendor/suncalc.js vendored Normal file
View File

@@ -0,0 +1,317 @@
/*
(c) 2011-2015, Vladimir Agafonkin
SunCalc is a JavaScript library for calculating sun/moon position and light phases.
https://github.com/mourner/suncalc
*/
(function () { 'use strict';
// shortcuts for easier to read formulas
var PI = Math.PI,
sin = Math.sin,
cos = Math.cos,
tan = Math.tan,
asin = Math.asin,
atan = Math.atan2,
acos = Math.acos,
rad = PI / 180;
// sun calculations are based on http://aa.quae.nl/en/reken/zonpositie.html formulas
// date/time constants and conversions
var dayMs = 1000 * 60 * 60 * 24,
J1970 = 2440588,
J2000 = 2451545;
function toJulian(date) { return date.valueOf() / dayMs - 0.5 + J1970; }
function fromJulian(j) { return new Date((j + 0.5 - J1970) * dayMs); }
function toDays(date) { return toJulian(date) - J2000; }
// general calculations for position
var e = rad * 23.4397; // obliquity of the Earth
function rightAscension(l, b) { return atan(sin(l) * cos(e) - tan(b) * sin(e), cos(l)); }
function declination(l, b) { return asin(sin(b) * cos(e) + cos(b) * sin(e) * sin(l)); }
function azimuth(H, phi, dec) { return atan(sin(H), cos(H) * sin(phi) - tan(dec) * cos(phi)); }
function altitude(H, phi, dec) { return asin(sin(phi) * sin(dec) + cos(phi) * cos(dec) * cos(H)); }
function siderealTime(d, lw) { return rad * (280.16 + 360.9856235 * d) - lw; }
function astroRefraction(h) {
if (h < 0) // the following formula works for positive altitudes only.
h = 0; // if h = -0.08901179 a div/0 would occur.
// formula 16.4 of "Astronomical Algorithms" 2nd edition by Jean Meeus (Willmann-Bell, Richmond) 1998.
// 1.02 / tan(h + 10.26 / (h + 5.10)) h in degrees, result in arc minutes -> converted to rad:
return 0.0002967 / Math.tan(h + 0.00312536 / (h + 0.08901179));
}
// general sun calculations
function solarMeanAnomaly(d) { return rad * (357.5291 + 0.98560028 * d); }
function eclipticLongitude(M) {
var C = rad * (1.9148 * sin(M) + 0.02 * sin(2 * M) + 0.0003 * sin(3 * M)), // equation of center
P = rad * 102.9372; // perihelion of the Earth
return M + C + P + PI;
}
function sunCoords(d) {
var M = solarMeanAnomaly(d),
L = eclipticLongitude(M);
return {
dec: declination(L, 0),
ra: rightAscension(L, 0)
};
}
var SunCalc = {};
// calculates sun position for a given date and latitude/longitude
SunCalc.getPosition = function (date, lat, lng) {
var lw = rad * -lng,
phi = rad * lat,
d = toDays(date),
c = sunCoords(d),
H = siderealTime(d, lw) - c.ra;
return {
azimuth: azimuth(H, phi, c.dec),
altitude: altitude(H, phi, c.dec)
};
};
// sun times configuration (angle, morning name, evening name)
var times = SunCalc.times = [
[-0.833, 'sunrise', 'sunset' ],
[ -0.3, 'sunriseEnd', 'sunsetStart' ],
[ -6, 'dawn', 'dusk' ],
[ -12, 'nauticalDawn', 'nauticalDusk'],
[ -18, 'nightEnd', 'night' ],
[ 6, 'goldenHourEnd', 'goldenHour' ]
];
// adds a custom time to the times config
SunCalc.addTime = function (angle, riseName, setName) {
times.push([angle, riseName, setName]);
};
// calculations for sun times
var J0 = 0.0009;
function julianCycle(d, lw) { return Math.round(d - J0 - lw / (2 * PI)); }
function approxTransit(Ht, lw, n) { return J0 + (Ht + lw) / (2 * PI) + n; }
function solarTransitJ(ds, M, L) { return J2000 + ds + 0.0053 * sin(M) - 0.0069 * sin(2 * L); }
function hourAngle(h, phi, d) { return acos((sin(h) - sin(phi) * sin(d)) / (cos(phi) * cos(d))); }
function observerAngle(height) { return -2.076 * Math.sqrt(height) / 60; }
// returns set time for the given sun altitude
function getSetJ(h, lw, phi, dec, n, M, L) {
var w = hourAngle(h, phi, dec),
a = approxTransit(w, lw, n);
return solarTransitJ(a, M, L);
}
// calculates sun times for a given date, latitude/longitude, and, optionally,
// the observer height (in meters) relative to the horizon
SunCalc.getTimes = function (date, lat, lng, height) {
height = height || 0;
var lw = rad * -lng,
phi = rad * lat,
dh = observerAngle(height),
d = toDays(date),
n = julianCycle(d, lw),
ds = approxTransit(0, lw, n),
M = solarMeanAnomaly(ds),
L = eclipticLongitude(M),
dec = declination(L, 0),
Jnoon = solarTransitJ(ds, M, L),
i, len, time, h0, Jset, Jrise;
var result = {
solarNoon: fromJulian(Jnoon),
nadir: fromJulian(Jnoon - 0.5)
};
for (i = 0, len = times.length; i < len; i += 1) {
time = times[i];
h0 = (time[0] + dh) * rad;
Jset = getSetJ(h0, lw, phi, dec, n, M, L);
Jrise = Jnoon - (Jset - Jnoon);
result[time[1]] = fromJulian(Jrise);
result[time[2]] = fromJulian(Jset);
}
return result;
};
// moon calculations, based on http://aa.quae.nl/en/reken/hemelpositie.html formulas
function moonCoords(d) { // geocentric ecliptic coordinates of the moon
var L = rad * (218.316 + 13.176396 * d), // ecliptic longitude
M = rad * (134.963 + 13.064993 * d), // mean anomaly
F = rad * (93.272 + 13.229350 * d), // mean distance
l = L + rad * 6.289 * sin(M), // longitude
b = rad * 5.128 * sin(F), // latitude
dt = 385001 - 20905 * cos(M); // distance to the moon in km
return {
ra: rightAscension(l, b),
dec: declination(l, b),
dist: dt
};
}
SunCalc.getMoonPosition = function (date, lat, lng) {
var lw = rad * -lng,
phi = rad * lat,
d = toDays(date),
c = moonCoords(d),
H = siderealTime(d, lw) - c.ra,
h = altitude(H, phi, c.dec),
// formula 14.1 of "Astronomical Algorithms" 2nd edition by Jean Meeus (Willmann-Bell, Richmond) 1998.
pa = atan(sin(H), tan(phi) * cos(c.dec) - sin(c.dec) * cos(H));
h = h + astroRefraction(h); // altitude correction for refraction
return {
azimuth: azimuth(H, phi, c.dec),
altitude: h,
distance: c.dist,
parallacticAngle: pa
};
};
// calculations for illumination parameters of the moon,
// based on http://idlastro.gsfc.nasa.gov/ftp/pro/astro/mphase.pro formulas and
// Chapter 48 of "Astronomical Algorithms" 2nd edition by Jean Meeus (Willmann-Bell, Richmond) 1998.
SunCalc.getMoonIllumination = function (date) {
var d = toDays(date || new Date()),
s = sunCoords(d),
m = moonCoords(d),
sdist = 149598000, // distance from Earth to Sun in km
phi = acos(sin(s.dec) * sin(m.dec) + cos(s.dec) * cos(m.dec) * cos(s.ra - m.ra)),
inc = atan(sdist * sin(phi), m.dist - sdist * cos(phi)),
angle = atan(cos(s.dec) * sin(s.ra - m.ra), sin(s.dec) * cos(m.dec) -
cos(s.dec) * sin(m.dec) * cos(s.ra - m.ra));
return {
fraction: (1 + cos(inc)) / 2,
phase: 0.5 + 0.5 * inc * (angle < 0 ? -1 : 1) / Math.PI,
angle: angle
};
};
function hoursLater(date, h) {
return new Date(date.valueOf() + h * dayMs / 24);
}
// calculations for moon rise/set times are based on http://www.stargazing.net/kepler/moonrise.html article
SunCalc.getMoonTimes = function (date, lat, lng, inUTC) {
var t = new Date(date);
if (inUTC) t.setUTCHours(0, 0, 0, 0);
else t.setHours(0, 0, 0, 0);
var hc = 0.133 * rad,
h0 = SunCalc.getMoonPosition(t, lat, lng).altitude - hc,
h1, h2, rise, set, a, b, xe, ye, d, roots, x1, x2, dx;
// go in 2-hour chunks, each time seeing if a 3-point quadratic curve crosses zero (which means rise or set)
for (var i = 1; i <= 24; i += 2) {
h1 = SunCalc.getMoonPosition(hoursLater(t, i), lat, lng).altitude - hc;
h2 = SunCalc.getMoonPosition(hoursLater(t, i + 1), lat, lng).altitude - hc;
a = (h0 + h2) / 2 - h1;
b = (h2 - h0) / 2;
xe = -b / (2 * a);
ye = (a * xe + b) * xe + h1;
d = b * b - 4 * a * h1;
roots = 0;
if (d >= 0) {
dx = Math.sqrt(d) / (Math.abs(a) * 2);
x1 = xe - dx;
x2 = xe + dx;
if (Math.abs(x1) <= 1) roots++;
if (Math.abs(x2) <= 1) roots++;
if (x1 < -1) x1 = x2;
}
if (roots === 1) {
if (h0 < 0) rise = i + x1;
else set = i + x1;
} else if (roots === 2) {
rise = i + (ye < 0 ? x2 : x1);
set = i + (ye < 0 ? x1 : x2);
}
if (rise && set) break;
h0 = h2;
}
var result = {};
if (rise) result.rise = hoursLater(t, rise);
if (set) result.set = hoursLater(t, set);
if (!rise && !set) result[ye > 0 ? 'alwaysUp' : 'alwaysDown'] = true;
return result;
};
// export as Node module / AMD module / browser variable
if (typeof exports === 'object' && typeof module !== 'undefined') module.exports = SunCalc;
else if (typeof define === 'function' && define.amd) define(SunCalc);
else window.SunCalc = SunCalc;
}());