2020-09-04 18:02:20 +00:00
|
|
|
// radar utilities
|
|
|
|
|
2020-09-25 00:51:29 +00:00
|
|
|
/* globals SuperGif */
|
2020-09-04 18:02:20 +00:00
|
|
|
// eslint-disable-next-line no-unused-vars
|
|
|
|
const utils = (() => {
|
|
|
|
// ****************************** weather data ********************************
|
|
|
|
const getPoint = async (lat, lon) => {
|
|
|
|
try {
|
2020-10-02 02:35:49 +00:00
|
|
|
return await json(`https://api.weather.gov/points/${lat},${lon}`);
|
2020-09-04 18:02:20 +00:00
|
|
|
} catch (e) {
|
2020-10-02 02:35:49 +00:00
|
|
|
console.log(`Unable to get point ${lat}, ${lon}`);
|
|
|
|
console.error(e);
|
2020-09-04 18:02:20 +00:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
// ****************************** load images *********************************
|
|
|
|
// load an image from a blob or url
|
2020-10-29 21:44:28 +00:00
|
|
|
const loadImg = (imgData, cors = false) => new Promise((resolve) => {
|
|
|
|
const img = new Image();
|
|
|
|
img.onload = (e) => {
|
|
|
|
resolve(e.target);
|
|
|
|
};
|
|
|
|
if (imgData instanceof Blob) {
|
|
|
|
img.src = window.URL.createObjectURL(imgData);
|
|
|
|
} else {
|
|
|
|
let url = imgData;
|
|
|
|
if (cors) url = rewriteUrl(imgData);
|
|
|
|
img.src = url;
|
|
|
|
}
|
|
|
|
});
|
2020-09-04 18:02:20 +00:00
|
|
|
|
|
|
|
// async version of SuperGif
|
2020-10-29 21:44:28 +00:00
|
|
|
const superGifAsync = (e) => new Promise((resolve) => {
|
|
|
|
const gif = new SuperGif(e);
|
|
|
|
gif.load(() => resolve(gif));
|
|
|
|
});
|
2020-09-04 18:02:20 +00:00
|
|
|
|
2020-09-23 16:49:15 +00:00
|
|
|
// preload an image
|
|
|
|
// the goal is to get it in the browser's cache so it is available more quickly when the browser needs it
|
|
|
|
// a list of cached icons is used to avoid hitting the cache multiple times
|
|
|
|
const cachedImages = [];
|
|
|
|
const preload = (src) => {
|
|
|
|
if (cachedImages.includes(src)) return false;
|
2022-08-05 03:03:59 +00:00
|
|
|
blob(src);
|
|
|
|
// cachedImages.push(src);
|
2020-09-23 16:49:15 +00:00
|
|
|
return true;
|
|
|
|
};
|
|
|
|
|
2020-09-23 19:43:49 +00:00
|
|
|
// draw an image on a local canvas and return the context
|
|
|
|
const drawLocalCanvas = (img) => {
|
|
|
|
// create a canvas
|
|
|
|
const canvas = document.createElement('canvas');
|
|
|
|
canvas.width = img.width;
|
|
|
|
canvas.height = img.height;
|
|
|
|
|
|
|
|
// get the context
|
|
|
|
const context = canvas.getContext('2d');
|
|
|
|
context.imageSmoothingEnabled = false;
|
|
|
|
|
|
|
|
// draw the image
|
2020-10-29 21:44:28 +00:00
|
|
|
context.drawImage(img, 0, 0);
|
2020-09-23 19:43:49 +00:00
|
|
|
return context;
|
|
|
|
};
|
|
|
|
|
2020-09-04 18:02:20 +00:00
|
|
|
// *********************************** unit conversions ***********************
|
|
|
|
|
2020-10-29 21:44:28 +00:00
|
|
|
Math.round2 = (value, decimals) => Number(`${Math.round(`${value}e${decimals}`)}e-${decimals}`);
|
2020-09-04 18:02:20 +00:00
|
|
|
|
|
|
|
const mphToKph = (Mph) => Math.round(Mph * 1.60934);
|
|
|
|
const kphToMph = (Kph) => Math.round(Kph / 1.60934);
|
2020-10-29 21:44:28 +00:00
|
|
|
const celsiusToFahrenheit = (Celsius) => Math.round((Celsius * 9) / 5 + 32);
|
|
|
|
const fahrenheitToCelsius = (Fahrenheit) => Math.round2((((Fahrenheit) - 32) * 5) / 9, 1);
|
2020-09-04 18:02:20 +00:00
|
|
|
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);
|
2020-10-29 21:44:28 +00:00
|
|
|
const pascalToInHg = (Pascal) => Math.round2(Pascal * 0.0002953, 2);
|
2020-09-04 18:02:20 +00:00
|
|
|
|
|
|
|
// ***************************** 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;
|
|
|
|
|
2020-10-29 21:44:28 +00:00
|
|
|
return Math.round(35.74 + (0.6215 * T) - (35.75 * (V ** 0.16)) + (0.4275 * T * (V ** 0.16)));
|
2020-09-04 18:02:20 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
// 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)];
|
|
|
|
};
|
|
|
|
|
2020-10-29 21:44:28 +00:00
|
|
|
const distance = (x1, y1, x2, y2) => Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2);
|
2020-09-04 18:02:20 +00:00
|
|
|
|
|
|
|
// wrap a number to 0-m
|
2020-10-29 21:44:28 +00:00
|
|
|
const wrap = (x, m) => ((x % m) + m) % m;
|
2020-09-04 18:02:20 +00:00
|
|
|
|
|
|
|
// ********************************* strings *********************************************
|
2022-08-04 17:49:04 +00:00
|
|
|
const locationCleanup = (input) => {
|
|
|
|
// regexes to run
|
|
|
|
const regexes = [
|
|
|
|
// "Chicago / West Chicago", removes before slash
|
|
|
|
/^[A-Za-z ]+ \/ /,
|
|
|
|
// "Chicago/Waukegan" removes before slash
|
|
|
|
/^[A-Za-z ]+\//,
|
|
|
|
// "Chicago, Chicago O'hare" removes before comma
|
|
|
|
/^[A-Za-z ]+, /,
|
|
|
|
];
|
|
|
|
|
|
|
|
// run all regexes
|
|
|
|
return regexes.reduce((value, regex) => value.replace(regex, ''), input);
|
2020-09-04 18:02:20 +00:00
|
|
|
};
|
2022-08-04 17:49:04 +00:00
|
|
|
|
2020-09-25 00:51:29 +00:00
|
|
|
// ********************************* cors ********************************************
|
|
|
|
// rewrite some urls for local server
|
2020-10-29 21:44:28 +00:00
|
|
|
const rewriteUrl = (_url) => {
|
|
|
|
let url = _url;
|
2020-10-02 02:35:49 +00:00
|
|
|
url = url.replace('https://api.weather.gov/', window.location.href);
|
|
|
|
url = url.replace('https://www.cpc.ncep.noaa.gov/', window.location.href);
|
2020-09-25 00:51:29 +00:00
|
|
|
return url;
|
|
|
|
};
|
2020-09-04 18:02:20 +00:00
|
|
|
|
2020-10-02 01:53:02 +00:00
|
|
|
// ********************************* fetch ********************************************
|
2020-10-02 02:35:49 +00:00
|
|
|
const json = (url, params) => fetchAsync(url, 'json', params);
|
|
|
|
const text = (url, params) => fetchAsync(url, 'text', params);
|
|
|
|
const raw = (url, params) => fetchAsync(url, '', params);
|
|
|
|
const blob = (url, params) => fetchAsync(url, 'blob', params);
|
|
|
|
|
2020-10-29 21:44:28 +00:00
|
|
|
const fetchAsync = async (_url, responseType, _params = {}) => {
|
|
|
|
// combine default and provided parameters
|
|
|
|
const params = {
|
2020-10-02 01:53:02 +00:00
|
|
|
method: 'GET',
|
|
|
|
mode: 'cors',
|
|
|
|
type: 'GET',
|
2020-10-29 21:44:28 +00:00
|
|
|
..._params,
|
|
|
|
};
|
2020-10-02 02:35:49 +00:00
|
|
|
// build a url, including the rewrite for cors if necessary
|
|
|
|
let corsUrl = _url;
|
|
|
|
if (params.cors === true) corsUrl = rewriteUrl(_url);
|
2022-08-05 03:03:59 +00:00
|
|
|
const url = new URL(corsUrl, `${window.location.origin}/`);
|
2022-11-14 19:40:38 +00:00
|
|
|
// match the security protocol when not on localhost
|
|
|
|
url.protocol = window.location.hostname !== 'localhost' ? window.location.protocol : url.protocol;
|
2020-10-02 02:35:49 +00:00
|
|
|
// add parameters if necessary
|
|
|
|
if (params.data) {
|
|
|
|
Object.keys(params.data).forEach((key) => {
|
|
|
|
// get the value
|
|
|
|
const value = params.data[key];
|
|
|
|
// add to the url
|
|
|
|
url.searchParams.append(key, value);
|
|
|
|
});
|
|
|
|
}
|
2020-10-02 01:53:02 +00:00
|
|
|
|
2020-10-02 02:35:49 +00:00
|
|
|
// make the request
|
|
|
|
const response = await fetch(url, params);
|
|
|
|
|
|
|
|
// check for ok response
|
|
|
|
if (!response.ok) throw new Error(`Fetch error ${response.status} ${response.statusText} while fetching ${response.url}`);
|
|
|
|
// return the requested response
|
|
|
|
switch (responseType) {
|
|
|
|
case 'json':
|
2020-10-29 21:44:28 +00:00
|
|
|
return response.json();
|
2020-10-02 02:35:49 +00:00
|
|
|
case 'text':
|
2020-10-29 21:44:28 +00:00
|
|
|
return response.text();
|
2020-10-02 02:35:49 +00:00
|
|
|
case 'blob':
|
2020-10-29 21:44:28 +00:00
|
|
|
return response.blob();
|
2020-10-02 02:35:49 +00:00
|
|
|
default:
|
|
|
|
return response;
|
|
|
|
}
|
2020-10-02 01:53:02 +00:00
|
|
|
};
|
|
|
|
|
2022-07-29 21:12:42 +00:00
|
|
|
const elemForEach = (selector, callback) => {
|
|
|
|
[...document.querySelectorAll(selector)].forEach(callback);
|
|
|
|
};
|
|
|
|
|
2020-09-04 18:02:20 +00:00
|
|
|
// return an orderly object
|
|
|
|
return {
|
2022-07-29 21:12:42 +00:00
|
|
|
elem: {
|
|
|
|
forEach: elemForEach,
|
|
|
|
},
|
2020-09-04 18:02:20 +00:00
|
|
|
image: {
|
|
|
|
load: loadImg,
|
|
|
|
superGifAsync,
|
2020-09-23 16:49:15 +00:00
|
|
|
preload,
|
2020-09-23 19:43:49 +00:00
|
|
|
drawLocalCanvas,
|
2020-09-04 18:02:20 +00:00
|
|
|
},
|
|
|
|
weather: {
|
|
|
|
getPoint,
|
|
|
|
},
|
|
|
|
units: {
|
|
|
|
mphToKph,
|
|
|
|
kphToMph,
|
|
|
|
celsiusToFahrenheit,
|
|
|
|
fahrenheitToCelsius,
|
|
|
|
milesToKilometers,
|
|
|
|
kilometersToMiles,
|
|
|
|
feetToMeters,
|
|
|
|
metersToFeet,
|
|
|
|
inchesToCentimeters,
|
|
|
|
pascalToInHg,
|
|
|
|
},
|
|
|
|
calc: {
|
|
|
|
relativeHumidity,
|
|
|
|
heatIndex,
|
|
|
|
windChill,
|
|
|
|
directionToNSEW,
|
|
|
|
distance,
|
|
|
|
wrap,
|
|
|
|
},
|
2020-09-25 00:38:53 +00:00
|
|
|
string: {
|
2022-08-04 17:49:04 +00:00
|
|
|
locationCleanup,
|
2020-09-04 18:02:20 +00:00
|
|
|
},
|
2020-09-25 00:51:29 +00:00
|
|
|
cors: {
|
|
|
|
rewriteUrl,
|
|
|
|
},
|
2020-10-02 01:53:02 +00:00
|
|
|
fetch: {
|
|
|
|
json,
|
2020-10-02 02:35:49 +00:00
|
|
|
text,
|
|
|
|
raw,
|
|
|
|
blob,
|
|
|
|
},
|
2020-09-04 18:02:20 +00:00
|
|
|
};
|
2020-10-29 21:44:28 +00:00
|
|
|
})();
|