From 24855fd9596acc9ebd3998c38ea08b46bf31646a Mon Sep 17 00:00:00 2001 From: Matt Walsh Date: Wed, 23 Sep 2020 14:43:49 -0500 Subject: [PATCH] get outlook data --- cors/outlook.js | 45 +++++++++++ index.js | 2 + server/scripts/modules/almanac.js | 115 +++++++++++++++++++++++++++- server/scripts/modules/utilities.js | 17 ++++ 4 files changed, 175 insertions(+), 4 deletions(-) create mode 100644 cors/outlook.js diff --git a/cors/outlook.js b/cors/outlook.js new file mode 100644 index 0000000..40bf4eb --- /dev/null +++ b/cors/outlook.js @@ -0,0 +1,45 @@ +// pass through api requests + +// http(s) modules +const https = require('https'); + +// url parsing +const queryString = require('querystring'); + +// return an express router +module.exports = (req, res) => { + // add out-going headers + const headers = {}; + headers['user-agent'] = '(WeatherStar 4000+, ws4000@netbymatt.com)'; + headers['accept'] = req.headers.accept; + + // get query paramaters if the exist + const queryParams = Object.keys(req.query).reduce((acc, key) => { + // skip the paramater 'u' + if (key === 'u') return acc; + // add the paramter to the resulting object + acc[key] = req.query[key]; + return acc; + },{}); + let query = queryString.encode(queryParams); + if (query.length > 0) query = '?' + query; + + // get the page + https.get('https://www.cpc.ncep.noaa.gov/' + req.path + query, { + headers, + }, getRes => { + // pull some info + const {statusCode} = getRes; + // pass the status code through + res.status(statusCode); + + // set headers + res.header('content-type', getRes.headers['content-type']); + res.header('last-modified', getRes.headers['last-modified']); + // pipe to response + getRes.pipe(res); + + }).on('error', e=>{ + console.error(e); + }); +}; \ No newline at end of file diff --git a/index.js b/index.js index a603a50..b7212e1 100644 --- a/index.js +++ b/index.js @@ -10,10 +10,12 @@ app.set('view engine', 'ejs'); // cors pass through const corsPassThru = require('./cors'); const radarPassThru = require('./cors/radar'); +const outlookPassThru = require('./cors/outlook'); // cors pass-thru to api.weather.gov app.get('/stations/*', corsPassThru); app.get('/Conus/*', radarPassThru); +app.get('/products/*', outlookPassThru); // version const version = require('./version'); diff --git a/server/scripts/modules/almanac.js b/server/scripts/modules/almanac.js index 0dc714a..0621d62 100644 --- a/server/scripts/modules/almanac.js +++ b/server/scripts/modules/almanac.js @@ -26,11 +26,10 @@ class Almanac extends WeatherDisplay { // get images for outlook const imagePromises = [ - utils.image.load('https://www.cpc.ncep.noaa.gov/products/predictions/30day/off14_temp.gif'), - utils.image.load('https://www.cpc.ncep.noaa.gov/products/predictions/30day/off14_prcp.gif'), + utils.image.load('products/predictions/30day/off14_temp.gif'), + utils.image.load('products/predictions/30day/off14_prcp.gif'), ]; - // get sun/moon data const {sun, moon} = this.calcSunMoonData(weatherParameters); @@ -38,7 +37,7 @@ class Almanac extends WeatherDisplay { const [outlookTemp, outlookPrecip] = await Promise.all(imagePromises); console.log(outlookTemp,outlookPrecip); - const outlook = 1; + const outlook = this.parseOutlooks(weatherParameters.latitude, weatherParameters.longitude, outlookTemp, outlookPrecip); // store the data this.data = { @@ -125,6 +124,114 @@ class Almanac extends WeatherDisplay { return {phase: phaseName, date: moonDate}; } + // use the color of the pixel to determine the outlook + parseOutlooks(lat, lon, temp, precip) { + const {DateTime} = luxon; + const month = DateTime.local().toLocaleString({month: 'long'}); + + // draw the images on the canvases + const tempContext = utils.image.drawLocalCanvas(temp); + const precipContext = utils.image.drawLocalCanvas(precip); + + // get the color from each canvas + const tempColor = this.getOutlookColor(lat, lon, tempContext); + const precipColor = this.getOutlookColor(lat, lon, precipContext); + + return { + month, + temperature: this.getOutlookTemperatureIndicator(tempColor), + precipitation: this.getOutlookPrecipitationIndicator(precipColor), + }; + } + + getOutlookColor (lat, lon, context) { + let x = 0; + let y = 0; + + // The height is in the range of latitude 75'N (top) - 15'N (bottom) + y = ((75 - lat) / 53) * 707; + + if (lat < 48.83) { + y -= Math.abs(48.83 - lat) * 2.9; + } + if (lon < -100.46) { + y -= Math.abs(-100.46 - lon) * 1.7; + } else { + y -= Math.abs(-100.46 - lon) * 1.7; + } + + // The width is in the range of the longitude ??? + x = ((-155 - lon) / -110) * 719; // -155 - -40 + + if (lon < -100.46) { + x -= Math.abs(-100.46 - lon) * 1; + + if (lat > 40) { + x += Math.abs(40 - lat) * 4; + } else { + x -= Math.abs(40 - lat) * 4; + } + } else { + x += Math.abs(-100.46 - lon) * 2; + + if (lat < 36 && lon > -90) { + x += Math.abs(36 - lat) * 8; + } else { + x -= Math.abs(36 - lat) * 6; + } + } + + // The further left and right from lat 45 and lon -97 the y increases + x = Math.round(x); + y = Math.round(y); + + // Determine if there is any "non-white" colors around the area. + // Search a 16x16 region. + for (let colorX = x - 8; colorX <= x + 8; colorX++) { + for (let colorY = y - 8; colorY <= y + 8; colorY++) { + const pixelColor = this.getPixelColor(context, colorX, colorY); + if ((pixelColor.r !== 0 && pixelColor.g !== 0 && pixelColor.b !== 0) || + (pixelColor.r !== 255 && pixelColor.g !== 255 && pixelColor.b !== 255)) { + return pixelColor; + } + } + } + + return false; + } + + // get rgb values of a pixel + getPixelColor (context, x, y) { + const pixelData = context.getImageData(x, y, 1, 1).data; + return { + r: pixelData[0], + g: pixelData[1], + b: pixelData[2], + }; + } + + // get temperature outlook from color + getOutlookTemperatureIndicator(pixelColor) { + if (pixelColor.b > pixelColor.r) { + return 'B'; + } else if (pixelColor.r > pixelColor.b) { + return 'A'; + } else { + return 'N'; + } + } + + // get precipitation outlook from color + getOutlookPrecipitationIndicator (pixelColor) { + if (pixelColor.g > pixelColor.r) { + return 'A'; + } else if (pixelColor.r > pixelColor.g) { + return 'B'; + } else { + return 'N'; + } + } + async drawCanvas() { super.drawCanvas(); const info = this.data; diff --git a/server/scripts/modules/utilities.js b/server/scripts/modules/utilities.js index f9cf29a..54d1288 100644 --- a/server/scripts/modules/utilities.js +++ b/server/scripts/modules/utilities.js @@ -58,6 +58,22 @@ const utils = (() => { 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); @@ -374,6 +390,7 @@ const utils = (() => { load: loadImg, superGifAsync, preload, + drawLocalCanvas, }, weather: { getPoint,