reduce regional forecast/observations to one call
This commit is contained in:
parent
4cf146c7a2
commit
3825397c78
|
@ -38,7 +38,6 @@ const js_sources = [
|
||||||
'server/scripts/modules/currentweather.js',
|
'server/scripts/modules/currentweather.js',
|
||||||
'server/scripts/modules/latestobservations.js',
|
'server/scripts/modules/latestobservations.js',
|
||||||
'server/scripts/modules/travelforecast.js',
|
'server/scripts/modules/travelforecast.js',
|
||||||
'server/scripts/modules/regionalforecastdata.js',
|
|
||||||
'server/scripts/modules/regionalforecast.js',
|
'server/scripts/modules/regionalforecast.js',
|
||||||
'server/scripts/modules/localforecast.js',
|
'server/scripts/modules/localforecast.js',
|
||||||
'server/scripts/modules/extendedforecast.js',
|
'server/scripts/modules/extendedforecast.js',
|
||||||
|
|
|
@ -103,14 +103,11 @@ const navigation = (() => {
|
||||||
new CurrentWeather(0,'currentWeather', weatherParameters),
|
new CurrentWeather(0,'currentWeather', weatherParameters),
|
||||||
new LatestObservations(1, 'latestObservations', weatherParameters),
|
new LatestObservations(1, 'latestObservations', weatherParameters),
|
||||||
new TravelForecast(2, 'travelForecast', weatherParameters),
|
new TravelForecast(2, 'travelForecast', weatherParameters),
|
||||||
// Regional Forecast: 0 = regional conditions, 1 = today, 2 = tomorrow
|
new RegionalForecast(3, 'regionalForecast', weatherParameters),
|
||||||
new RegionalForecast(3, 'regionalForecast0', weatherParameters, 0),
|
new LocalForecast(4, 'localForecast', weatherParameters),
|
||||||
new RegionalForecast(4, 'regionalForecast1', weatherParameters, 1),
|
new ExtendedForecast(5, 'extendedForecast', weatherParameters),
|
||||||
new RegionalForecast(5, 'regionalForecast2', weatherParameters, 2),
|
new Almanac(6, 'almanac', weatherParameters),
|
||||||
new LocalForecast(6, 'localForecast', weatherParameters),
|
new Radar(7, 'radar', weatherParameters),
|
||||||
new ExtendedForecast(7, 'extendedForecast', weatherParameters),
|
|
||||||
new Almanac(8, 'almanac', weatherParameters),
|
|
||||||
new Radar(8, 'radar', weatherParameters),
|
|
||||||
];
|
];
|
||||||
} else {
|
} else {
|
||||||
// or just call for new data if the canvases already exist
|
// or just call for new data if the canvases already exist
|
||||||
|
|
|
@ -1,19 +1,19 @@
|
||||||
// regional forecast and observations
|
// regional forecast and observations
|
||||||
// type 0 = observations, 1 = first forecast, 2 = second forecast
|
// 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 */
|
/* globals WeatherDisplay, utils, STATUS, icons, UNITS, draw, navigation, luxon, _StationInfo, _RegionalCities */
|
||||||
|
|
||||||
// eslint-disable-next-line no-unused-vars
|
// eslint-disable-next-line no-unused-vars
|
||||||
class RegionalForecast extends WeatherDisplay {
|
class RegionalForecast extends WeatherDisplay {
|
||||||
constructor(navId,elemId, weatherParameters, period) {
|
constructor(navId,elemId, weatherParameters) {
|
||||||
super(navId,elemId,'Regional Forecast');
|
super(navId,elemId,'Regional Forecast');
|
||||||
// store the period, see above
|
|
||||||
this.period = period;
|
|
||||||
|
|
||||||
// pre-load background image (returns promise)
|
// pre-load background image (returns promise)
|
||||||
this.backgroundImage = utils.image.load('images/BackGround5_1.png');
|
this.backgroundImage = utils.image.load('images/BackGround5_1.png');
|
||||||
|
|
||||||
|
// timings
|
||||||
|
this.timing.totalScreens = 3;
|
||||||
|
|
||||||
// get the data and update the promise
|
// get the data and update the promise
|
||||||
this.getData(weatherParameters);
|
this.getData(weatherParameters);
|
||||||
}
|
}
|
||||||
|
@ -30,10 +30,298 @@ class RegionalForecast extends WeatherDisplay {
|
||||||
}
|
}
|
||||||
this.baseMap = utils.image.load(src);
|
this.baseMap = utils.image.load(src);
|
||||||
|
|
||||||
this.data = await RegionalForecastData.updateData(weatherParameters);
|
// map offset
|
||||||
|
const offsetXY = {
|
||||||
|
x: 240,
|
||||||
|
y: 117,
|
||||||
|
};
|
||||||
|
// get user's location in x/y
|
||||||
|
const sourceXY = this.getXYFromLatitudeLongitude(weatherParameters.latitude, weatherParameters.longitude, offsetXY.x, offsetXY.y, weatherParameters.state);
|
||||||
|
|
||||||
|
// get latitude and longitude limits
|
||||||
|
const minMaxLatLon = this.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 = this.getRegionalObservation(point, city);
|
||||||
|
|
||||||
|
const forecast = await $.ajax({
|
||||||
|
url: point.properties.forecast,
|
||||||
|
dataType: 'json',
|
||||||
|
crossDomain: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// get XY on map for city
|
||||||
|
const cityXY = this.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,
|
||||||
|
this.buildForecast(forecast.properties.periods[1], city, cityXY),
|
||||||
|
this.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
|
||||||
|
this.data = {
|
||||||
|
regionalData,
|
||||||
|
offsetXY,
|
||||||
|
sourceXY,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.setStatus(STATUS.loaded);
|
||||||
this.drawCanvas();
|
this.drawCanvas();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
buildForecast (forecast, city, cityXY) {
|
||||||
|
return {
|
||||||
|
daytime: forecast.isDaytime,
|
||||||
|
temperature: forecast.temperature||0,
|
||||||
|
name: city.Name,
|
||||||
|
icon: forecast.icon,
|
||||||
|
x: cityXY.x,
|
||||||
|
y: cityXY.y,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async getRegionalObservation (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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// utility latitude/pixel conversions
|
||||||
|
getXYFromLatitudeLongitude (Latitude, Longitude, OffsetX, OffsetY, state) {
|
||||||
|
if (state === 'AK') return this.getXYFromLatitudeLongitudeAK(...arguments);
|
||||||
|
if (state === 'HI') return this.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 };
|
||||||
|
}
|
||||||
|
|
||||||
|
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 };
|
||||||
|
}
|
||||||
|
|
||||||
|
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 };
|
||||||
|
}
|
||||||
|
|
||||||
|
getMinMaxLatitudeLongitude (X, Y, OffsetX, OffsetY, state) {
|
||||||
|
if (state === 'AK') return this.getMinMaxLatitudeLongitudeAK(...arguments);
|
||||||
|
if (state === 'HI') return this.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 };
|
||||||
|
}
|
||||||
|
|
||||||
|
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 };
|
||||||
|
}
|
||||||
|
|
||||||
|
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 };
|
||||||
|
}
|
||||||
|
|
||||||
|
getXYForCity (City, MaxLatitude, MinLongitude, state) {
|
||||||
|
if (state === 'AK') this.getXYForCityAK(...arguments);
|
||||||
|
if (state === 'HI') this.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 };
|
||||||
|
}
|
||||||
|
|
||||||
|
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 };
|
||||||
|
}
|
||||||
|
|
||||||
|
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 };
|
||||||
|
}
|
||||||
|
|
||||||
async drawCanvas() {
|
async drawCanvas() {
|
||||||
super.drawCanvas();
|
super.drawCanvas();
|
||||||
// break up data into useful values
|
// break up data into useful values
|
||||||
|
@ -49,21 +337,21 @@ class RegionalForecast extends WeatherDisplay {
|
||||||
draw.triangle(this.context, 'rgb(28, 10, 87)', 500, 30, 450, 90, 500, 90);
|
draw.triangle(this.context, 'rgb(28, 10, 87)', 500, 30, 450, 90, 500, 90);
|
||||||
|
|
||||||
// draw the appropriate title
|
// draw the appropriate title
|
||||||
if (this.period === 0) {
|
if (this.screenIndex === 0) {
|
||||||
draw.titleText(this.context, 'Regional', 'Observations');
|
draw.titleText(this.context, 'Regional', 'Observations');
|
||||||
} else {
|
} else {
|
||||||
let forecastDate = DateTime.local();
|
let forecastDate = DateTime.local();
|
||||||
// four conditions to evaluate based on whether the first forecast is for daytime and the requested period
|
// four conditions to evaluate based on whether the first forecast is for daytime and the requested period
|
||||||
const firstIsDay = data[0][1].daytime;
|
const firstIsDay = data[0][1].daytime;
|
||||||
if (firstIsDay && this.period === 1) forecastDate = forecastDate.plus({days: 1});
|
if (firstIsDay && this.screenIndex === 1) forecastDate = forecastDate.plus({days: 1});
|
||||||
if (firstIsDay && this.period === 2); // no change, shown for consistency
|
if (firstIsDay && this.screenIndex === 2); // no change, shown for consistency
|
||||||
if (!firstIsDay && this.period === 1); // no change, shown for consistency
|
if (!firstIsDay && this.screenIndex === 1); // no change, shown for consistency
|
||||||
if (!firstIsDay && this.period === 2) forecastDate = forecastDate.plus({days: 1});
|
if (!firstIsDay && this.screenIndex === 2) forecastDate = forecastDate.plus({days: 1});
|
||||||
|
|
||||||
// get the name of the day
|
// get the name of the day
|
||||||
const dayName = forecastDate.toLocaleString({weekday: 'long'});
|
const dayName = forecastDate.toLocaleString({weekday: 'long'});
|
||||||
// draw the title
|
// draw the title
|
||||||
if (data[0][this.period].daytime) {
|
if (data[0][this.screenIndex].daytime) {
|
||||||
draw.titleText(this.context, 'Forecast for', dayName);
|
draw.titleText(this.context, 'Forecast for', dayName);
|
||||||
} else {
|
} else {
|
||||||
draw.titleText(this.context, 'Forecast for', dayName + ' Night');
|
draw.titleText(this.context, 'Forecast for', dayName + ' Night');
|
||||||
|
@ -73,7 +361,7 @@ class RegionalForecast extends WeatherDisplay {
|
||||||
// draw the map
|
// draw the map
|
||||||
this.context.drawImage(await this.baseMap, sourceXY.x, sourceXY.y, (offsetXY.x * 2), (offsetXY.y * 2), 0, mapYOff, 640, 312);
|
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 => {
|
await Promise.all(data.map(async city => {
|
||||||
const period = city[this.period];
|
const period = city[this.screenIndex];
|
||||||
// draw the icon if possible
|
// draw the icon if possible
|
||||||
const icon = icons.getWeatherRegionalIconFromIconLink(period.icon, !period.daytime);
|
const icon = icons.getWeatherRegionalIconFromIconLink(period.icon, !period.daytime);
|
||||||
if (icon) {
|
if (icon) {
|
||||||
|
|
|
@ -1,319 +0,0 @@
|
||||||
// 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,
|
|
||||||
};
|
|
||||||
})();
|
|
|
@ -6,7 +6,6 @@
|
||||||
<link rel="preload" href="fonts/Star4000.woff" as="font" crossorigin="anonymous" />
|
<link rel="preload" href="fonts/Star4000.woff" as="font" crossorigin="anonymous" />
|
||||||
<link rel="preload" href="fonts/Star 4 Radar.woff" as="font" crossorigin="anonymous" />
|
<link rel="preload" href="fonts/Star 4 Radar.woff" as="font" crossorigin="anonymous" />
|
||||||
<link rel="preload" href="fonts/Star4000 Extended.woff" as="font" crossorigin="anonymous" />
|
<link rel="preload" href="fonts/Star4000 Extended.woff" as="font" crossorigin="anonymous" />
|
||||||
<link rel="preload" href="fonts/Star4000 Large Compressed Numbers.woff" as="font" crossorigin="anonymous" />
|
|
||||||
<link rel="preload" href="fonts/Star4000 Large Compressed.woff" as="font" crossorigin="anonymous" />
|
<link rel="preload" href="fonts/Star4000 Large Compressed.woff" as="font" crossorigin="anonymous" />
|
||||||
<link rel="preload" href="fonts/Star4000 Large.woff" as="font" crossorigin="anonymous" />
|
<link rel="preload" href="fonts/Star4000 Large.woff" as="font" crossorigin="anonymous" />
|
||||||
<link rel="preload" href="fonts/Star4000 Small.woff" as="font" crossorigin="anonymous" />
|
<link rel="preload" href="fonts/Star4000 Small.woff" as="font" crossorigin="anonymous" />
|
||||||
|
@ -30,7 +29,6 @@
|
||||||
<script type="text/javascript" src="scripts/modules/currentweather.js"></script>
|
<script type="text/javascript" src="scripts/modules/currentweather.js"></script>
|
||||||
<script type="text/javascript" src="scripts/modules/latestobservations.js"></script>
|
<script type="text/javascript" src="scripts/modules/latestobservations.js"></script>
|
||||||
<script type="text/javascript" src="scripts/modules/travelforecast.js"></script>
|
<script type="text/javascript" src="scripts/modules/travelforecast.js"></script>
|
||||||
<script type="text/javascript" src="scripts/modules/regionalforecastdata.js"></script>
|
|
||||||
<script type="text/javascript" src="scripts/modules/regionalforecast.js"></script>
|
<script type="text/javascript" src="scripts/modules/regionalforecast.js"></script>
|
||||||
<script type="text/javascript" src="scripts/modules/localforecast.js"></script>
|
<script type="text/javascript" src="scripts/modules/localforecast.js"></script>
|
||||||
<script type="text/javascript" src="scripts/modules/extendedforecast.js"></script>
|
<script type="text/javascript" src="scripts/modules/extendedforecast.js"></script>
|
||||||
|
|
Loading…
Reference in a new issue