mirror of
https://github.com/netbymatt/ws4kp.git
synced 2026-04-17 17:19:30 -07:00
reorganize for build system
This commit is contained in:
66
server/scripts/TimerWorker.js
Normal file
66
server/scripts/TimerWorker.js
Normal 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;
|
||||
}
|
||||
}
|
||||
584
server/scripts/data/RegionalCities.js
Normal file
584
server/scripts/data/RegionalCities.js
Normal 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,
|
||||
},
|
||||
];
|
||||
|
||||
60
server/scripts/data/States.js
Normal file
60
server/scripts/data/States.js
Normal 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],
|
||||
};
|
||||
})();
|
||||
|
||||
149
server/scripts/data/TravelCities.js
Normal file
149
server/scripts/data/TravelCities.js
Normal 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,
|
||||
},
|
||||
];
|
||||
|
||||
30822
server/scripts/data/stations.js
Normal file
30822
server/scripts/data/stations.js
Normal file
File diff suppressed because it is too large
Load Diff
956
server/scripts/index.js
Normal file
956
server/scripts/index.js
Normal 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);
|
||||
};
|
||||
163
server/scripts/modules/almanac.js
Normal file
163
server/scripts/modules/almanac.js
Normal 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);
|
||||
}
|
||||
}
|
||||
202
server/scripts/modules/currentweather.js
Normal file
202
server/scripts/modules/currentweather.js
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
101
server/scripts/modules/draw.js
Normal file
101
server/scripts/modules/draw.js
Normal 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,
|
||||
};
|
||||
})();
|
||||
177
server/scripts/modules/extendedforecast.js
Normal file
177
server/scripts/modules/extendedforecast.js
Normal 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);
|
||||
}
|
||||
}
|
||||
237
server/scripts/modules/icons.js
Normal file
237
server/scripts/modules/icons.js
Normal 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,
|
||||
};
|
||||
})();
|
||||
129
server/scripts/modules/latestobservations.js
Normal file
129
server/scripts/modules/latestobservations.js
Normal 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;
|
||||
}
|
||||
}
|
||||
145
server/scripts/modules/localforecast.js
Normal file
145
server/scripts/modules/localforecast.js
Normal 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,
|
||||
}));
|
||||
}
|
||||
}
|
||||
245
server/scripts/modules/navigation.js
Normal file
245
server/scripts/modules/navigation.js
Normal 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,
|
||||
};
|
||||
})();
|
||||
104
server/scripts/modules/regionalforecast.js
Normal file
104
server/scripts/modules/regionalforecast.js
Normal 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);
|
||||
|
||||
}
|
||||
}
|
||||
319
server/scripts/modules/regionalforecastdata.js
Normal file
319
server/scripts/modules/regionalforecastdata.js
Normal 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,
|
||||
};
|
||||
})();
|
||||
183
server/scripts/modules/travelforecast.js
Normal file
183
server/scripts/modules/travelforecast.js
Normal 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;
|
||||
}
|
||||
}
|
||||
410
server/scripts/modules/utilities.js
Normal file
410
server/scripts/modules/utilities.js
Normal 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);
|
||||
};
|
||||
316
server/scripts/modules/weatherdisplay.js
Normal file
316
server/scripts/modules/weatherdisplay.js
Normal 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
94
server/scripts/timer.js
Normal 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
4942
server/scripts/twc3.js
Normal file
File diff suppressed because it is too large
Load Diff
2
server/scripts/vendor/addtohomescreen.min.js
vendored
Normal file
2
server/scripts/vendor/addtohomescreen.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
196
server/scripts/vendor/jeoquery.js
vendored
Normal file
196
server/scripts/vendor/jeoquery.js
vendored
Normal 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);
|
||||
2
server/scripts/vendor/jquery-3.5.1.min.js
vendored
Normal file
2
server/scripts/vendor/jquery-3.5.1.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
8
server/scripts/vendor/jquery.autocomplete.min.js
vendored
Normal file
8
server/scripts/vendor/jquery.autocomplete.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
14
server/scripts/vendor/jquery.touchSwipe.min.js
vendored
Normal file
14
server/scripts/vendor/jquery.touchSwipe.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1169
server/scripts/vendor/libgif.js
vendored
Normal file
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
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
2
server/scripts/vendor/nosleep.min.js
vendored
Normal 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
317
server/scripts/vendor/suncalc.js
vendored
Normal 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;
|
||||
|
||||
}());
|
||||
Reference in New Issue
Block a user