ws4kp/server/scripts/modules/regionalforecast.mjs

209 lines
7.1 KiB
JavaScript
Raw Normal View History

2020-09-04 18:02:20 +00:00
// regional forecast and observations
// type 0 = observations, 1 = first forecast, 2 = second forecast
2022-11-22 22:19:10 +00:00
import STATUS from './status.mjs';
import { distance as calcDistance } from './utils/calc.mjs';
import { json } from './utils/fetch.mjs';
2022-12-06 22:25:28 +00:00
import { celsiusToFahrenheit } from './utils/units.mjs';
2022-11-22 22:19:10 +00:00
import { getWeatherRegionalIconFromIconLink } from './icons.mjs';
import { preloadImg } from './utils/image.mjs';
import { DateTime } from '../vendor/auto/luxon.mjs';
2022-11-22 22:29:10 +00:00
import WeatherDisplay from './weatherdisplay.mjs';
2022-12-06 22:14:56 +00:00
import { registerDisplay } from './navigation.mjs';
2022-12-09 19:51:51 +00:00
import * as utils from './regionalforecast-utils.mjs';
2022-12-12 21:41:28 +00:00
import { getPoint } from './utils/weather.mjs';
2020-09-04 18:02:20 +00:00
class RegionalForecast extends WeatherDisplay {
2020-10-29 21:44:28 +00:00
constructor(navId, elemId) {
2022-11-22 03:50:22 +00:00
super(navId, elemId, 'Regional Forecast', true);
2020-09-04 18:02:20 +00:00
// timings
this.timing.totalScreens = 3;
2020-09-04 18:02:20 +00:00
}
2020-10-29 21:44:28 +00:00
async getData(_weatherParameters) {
2022-12-07 17:02:51 +00:00
if (!super.getData(_weatherParameters)) return;
2020-10-29 21:44:28 +00:00
const weatherParameters = _weatherParameters ?? this.weatherParameters;
2020-09-25 14:55:29 +00:00
2022-08-05 03:12:08 +00:00
// pre-load the base map
2022-08-05 03:26:09 +00:00
let baseMap = 'images/Basemap2.png';
2020-10-16 20:30:27 +00:00
if (weatherParameters.state === 'HI') {
2022-08-05 03:26:09 +00:00
baseMap = 'images/HawaiiRadarMap4.png';
2020-10-16 20:30:27 +00:00
} else if (weatherParameters.state === 'AK') {
2022-08-05 03:26:09 +00:00
baseMap = 'images/AlaskaRadarMap6.png';
2020-09-04 18:02:20 +00:00
}
2022-08-05 03:26:09 +00:00
this.elem.querySelector('.map img').src = baseMap;
2020-09-04 18:02:20 +00:00
// map offset
const offsetXY = {
x: 240,
y: 117,
};
// get user's location in x/y
2022-12-09 19:51:51 +00:00
const sourceXY = utils.getXYFromLatitudeLongitude(weatherParameters.latitude, weatherParameters.longitude, offsetXY.x, offsetXY.y, weatherParameters.state);
// get latitude and longitude limits
2022-12-09 19:51:51 +00:00
const minMaxLatLon = utils.getMinMaxLatitudeLongitude(sourceXY.x, sourceXY.y, offsetXY.x, offsetXY.y, weatherParameters.state);
// get a target distance
let targetDistance = 2.5;
2020-10-16 20:30:27 +00:00
if (weatherParameters.state === 'HI') targetDistance = 1;
// make station info into an array
2020-10-29 21:44:28 +00:00
const stationInfoArray = Object.values(StationInfo).map((value) => ({ ...value, targetDistance }));
// combine regional cities with station info for additional stations
// stations are intentionally after cities to allow cities priority when drawing the map
2020-10-29 21:44:28 +00:00
const combinedCities = [...RegionalCities, ...stationInfoArray];
// Determine which cities are within the max/min latitude/longitude.
const regionalCities = [];
2020-10-29 21:44:28 +00:00
combinedCities.forEach((city) => {
if (city.lat > minMaxLatLon.minLat && city.lat < minMaxLatLon.maxLat
&& city.lon > minMaxLatLon.minLon && city.lon < minMaxLatLon.maxLon - 1) {
// default to 1 for cities loaded from RegionalCities, use value calculate above for remaining stations
const targetDist = 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) => {
2022-11-22 22:19:10 +00:00
const distance = calcDistance(city.lon, city.lat, testCity.lon, testCity.lat);
2020-10-29 21:44:28 +00:00
return acc && distance >= targetDist;
}, true);
if (okToAddCity) regionalCities.push(city);
}
});
// get regional forecasts and observations (the two are intertwined due to the design of api.weather.gov)
2022-08-05 03:26:09 +00:00
const regionalDataAll = await Promise.all(regionalCities.map(async (city) => {
try {
2022-12-12 21:41:28 +00:00
const point = city?.point ?? (await getAndFormatPoint(city.lat, city.lon));
if (!point) throw new Error('No pre-loaded point');
// start off the observation task
2022-12-12 21:41:28 +00:00
const observationPromise = utils.getRegionalObservation(point, city);
2023-11-12 03:54:58 +00:00
const forecast = await json(`https://api.weather.gov/gridpoints/${point.wfo}/${point.x},${point.y}/forecast?units=si`);
// get XY on map for city
2022-12-09 19:51:51 +00:00
const cityXY = utils.getXYForCity(city, minMaxLatLon.maxLat, minMaxLatLon.minLon, weatherParameters.state);
// wait for the regional observation if it's not done yet
const observation = await observationPromise;
if (!observation) return false;
// format the observation the same as the forecast
const regionalObservation = {
2023-01-06 20:39:39 +00:00
daytime: !!/\/day\//.test(observation.icon),
2022-12-06 22:25:28 +00:00
temperature: celsiusToFahrenheit(observation.temperature.value),
2022-12-09 19:51:51 +00:00
name: utils.formatCity(city.city),
icon: observation.icon,
x: cityXY.x,
y: cityXY.y,
};
2020-09-23 16:49:15 +00:00
// preload the icon
2022-11-22 22:19:10 +00:00
preloadImg(getWeatherRegionalIconFromIconLink(regionalObservation.icon, !regionalObservation.daytime));
2020-09-23 16:49:15 +00:00
// 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,
2022-12-09 19:51:51 +00:00
utils.buildForecast(forecast.properties.periods[1], city, cityXY),
utils.buildForecast(forecast.properties.periods[2], city, cityXY),
];
2023-01-06 20:39:39 +00:00
} catch (error) {
2022-11-22 03:50:22 +00:00
console.log(`No regional forecast data for '${city.name ?? city.city}'`);
2023-01-06 20:39:39 +00:00
console.log(error);
return false;
}
2022-08-05 03:26:09 +00:00
}));
// filter out any false (unavailable data)
2020-10-29 21:44:28 +00:00
const regionalData = regionalDataAll.filter((data) => data);
// test for data present
if (regionalData.length === 0) {
this.setStatus(STATUS.noData);
return;
}
// return the weather data and offsets
this.data = {
regionalData,
offsetXY,
sourceXY,
};
this.setStatus(STATUS.loaded);
2020-09-04 18:02:20 +00:00
}
2022-08-05 03:26:09 +00:00
drawCanvas() {
2020-09-04 18:02:20 +00:00
super.drawCanvas();
// break up data into useful values
2020-10-29 21:44:28 +00:00
const { regionalData: data, sourceXY, offsetXY } = this.data;
2020-09-04 18:02:20 +00:00
// draw the header graphics
// draw the appropriate title
const titleTop = this.elem.querySelector('.title.dual .top');
const titleBottom = this.elem.querySelector('.title.dual .bottom');
if (this.screenIndex === 0) {
titleTop.innerHTML = 'Regional';
titleBottom.innerHTML = 'Observations';
2020-09-04 18:02:20 +00:00
} else {
2020-10-29 21:44:28 +00:00
const forecastDate = DateTime.fromISO(data[0][this.screenIndex].time);
2020-09-04 18:02:20 +00:00
// get the name of the day
2020-10-29 21:44:28 +00:00
const dayName = forecastDate.toLocaleString({ weekday: 'long' });
titleTop.innerHTML = 'Forecast for';
2020-09-04 18:02:20 +00:00
// draw the title
2023-01-06 20:39:39 +00:00
titleBottom.innerHTML = data[0][this.screenIndex].daytime
? dayName
: `${dayName} Night`;
2020-09-04 18:02:20 +00:00
}
// draw the map
2022-08-04 21:30:13 +00:00
const scale = 640 / (offsetXY.x * 2);
const map = this.elem.querySelector('.map');
map.style.transform = `scale(${scale}) translate(-${sourceXY.x}px, -${sourceXY.y}px)`;
2022-08-04 21:30:13 +00:00
const cities = data.map((city) => {
const fill = {};
const period = city[this.screenIndex];
2022-11-22 22:19:10 +00:00
fill.icon = { type: 'img', src: getWeatherRegionalIconFromIconLink(period.icon, !period.daytime) };
2022-08-04 21:30:13 +00:00
fill.city = period.name;
2022-12-06 22:25:28 +00:00
const { temperature } = period;
2022-08-04 21:30:13 +00:00
fill.temp = temperature;
2023-01-06 20:39:39 +00:00
const { x, y } = period;
2022-08-04 21:30:13 +00:00
const elem = this.fillTemplate('location', fill);
2023-01-06 20:39:39 +00:00
elem.style.left = `${x}px`;
elem.style.top = `${y}px`;
2022-08-04 21:30:13 +00:00
return elem;
});
const locationContainer = this.elem.querySelector('.location-container');
locationContainer.innerHTML = '';
locationContainer.append(...cities);
2020-09-04 18:02:20 +00:00
this.finishDraw();
}
2020-10-29 21:44:28 +00:00
}
2022-11-22 22:19:10 +00:00
2022-12-12 21:41:28 +00:00
const getAndFormatPoint = async (lat, lon) => {
const point = await getPoint(lat, lon);
return {
x: point.properties.gridX,
y: point.properties.gridY,
wfo: point.properties.gridId,
};
};
2022-12-06 22:14:56 +00:00
// register display
2022-12-14 22:28:33 +00:00
registerDisplay(new RegionalForecast(6, 'regional-forecast'));