mirror of
https://github.com/netbymatt/ws4kp.git
synced 2026-04-18 09:39:30 -07:00
reorganize for build system
This commit is contained in:
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,
|
||||
};
|
||||
})();
|
||||
Reference in New Issue
Block a user