// radar utilities /* globals SuperGif */ // eslint-disable-next-line no-unused-vars const utils = (() => { // ****************************** weather data ******************************** const getPoint = async (lat, lon) => { try { return await json(`https://api.weather.gov/points/${lat},${lon}`); } catch (e) { console.log(`Unable to get point ${lat}, ${lon}`); console.error(e); return false; } }; // ****************************** load images ********************************* // load an image from a blob or url 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; } }); // async version of SuperGif const superGifAsync = (e) => new Promise((resolve) => { const gif = new SuperGif(e); gif.load(() => resolve(gif)); }); // 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; const img = new Image(); img.scr = src; cachedImages.push(src); return true; }; // 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 context.drawImage(img, 0, 0); return context; }; // *********************************** 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 * (V ** 0.16)) + (0.4275 * T * (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) ** 2 + (y2 - y1) ** 2); // wrap a number to 0-m const wrap = (x, m) => ((x % m) + m) % m; // ********************************* strings ********************************************* 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); }; // ********************************* cors ******************************************** // rewrite some urls for local server const rewriteUrl = (_url) => { let url = _url; url = url.replace('https://api.weather.gov/', window.location.href); url = url.replace('https://www.cpc.ncep.noaa.gov/', window.location.href); return url; }; // ********************************* fetch ******************************************** 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); const fetchAsync = async (_url, responseType, _params = {}) => { // combine default and provided parameters const params = { method: 'GET', mode: 'cors', type: 'GET', ..._params, }; // build a url, including the rewrite for cors if necessary let corsUrl = _url; if (params.cors === true) corsUrl = rewriteUrl(_url); const url = new URL(corsUrl); // match the security protocol url.protocol = window.location.protocol; // 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); }); } // 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': return response.json(); case 'text': return response.text(); case 'blob': return response.blob(); default: return response; } }; const elemForEach = (selector, callback) => { [...document.querySelectorAll(selector)].forEach(callback); }; // return an orderly object return { elem: { forEach: elemForEach, }, image: { load: loadImg, superGifAsync, preload, drawLocalCanvas, }, weather: { getPoint, }, units: { mphToKph, kphToMph, celsiusToFahrenheit, fahrenheitToCelsius, milesToKilometers, kilometersToMiles, feetToMeters, metersToFeet, inchesToCentimeters, pascalToInHg, }, calc: { relativeHumidity, heatIndex, windChill, directionToNSEW, distance, wrap, }, string: { locationCleanup, }, cors: { rewriteUrl, }, fetch: { json, text, raw, blob, }, }; })();