diff --git a/cors/index.js b/cors/index.js index a3dd9b3..ca12788 100644 --- a/cors/index.js +++ b/cors/index.js @@ -10,8 +10,6 @@ const queryString = require('querystring'); // return an express router module.exports = (req, res) => { - if (!req.query.u) res.status(404); - // add out-going headers const headers = {}; headers['user-agent'] = '(WeatherStar 4000+, ws4000@netbymatt.com)'; diff --git a/cors/radar.js b/cors/radar.js new file mode 100644 index 0000000..14313eb --- /dev/null +++ b/cors/radar.js @@ -0,0 +1,44 @@ +// 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://radar.weather.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']); + // pipe to response + getRes.pipe(res); + + }).on('error', e=>{ + console.error(e); + }); +}; \ No newline at end of file diff --git a/gulp/publish-frontend.js b/gulp/publish-frontend.js index d63d1b1..5f9e6ce 100644 --- a/gulp/publish-frontend.js +++ b/gulp/publish-frontend.js @@ -34,6 +34,7 @@ const js_sources = [ 'server/scripts/modules/localforecast.js', 'server/scripts/modules/extendedforecast.js', 'server/scripts/modules/almanac.js', + 'server/scripts/modules/radar.js', 'server/scripts/modules/navigation.js', ]; gulp.task('compress_js', () => diff --git a/index.js b/index.js index 0b1faef..5d49e64 100644 --- a/index.js +++ b/index.js @@ -9,9 +9,11 @@ app.set('view engine', 'ejs'); // cors pass through const corsPassThru = require('./cors'); +const radarPassThru = require('./cors/radar'); // cors pass-thru to api.weather.gov app.get('/stations/*', corsPassThru); +app.get('/Conus/*', radarPassThru); const index = (req, res) => { diff --git a/server/scripts/modules/navigation.js b/server/scripts/modules/navigation.js index 0d87aaf..851b3e4 100644 --- a/server/scripts/modules/navigation.js +++ b/server/scripts/modules/navigation.js @@ -1,7 +1,7 @@ 'use strict'; // navigation handles progress, next/previous and initial load messages from the parent frame /* globals utils, _StationInfo, STATUS */ -/* globals CurrentWeather, LatestObservations, TravelForecast, RegionalForecast, LocalForecast, ExtendedForecast, Almanac */ +/* globals CurrentWeather, LatestObservations, TravelForecast, RegionalForecast, LocalForecast, ExtendedForecast, Almanac, Radar */ document.addEventListener('DOMContentLoaded', () => { navigation.init(); @@ -107,6 +107,7 @@ const navigation = (() => { new LocalForecast(6, 'localForecast', weatherParameters), new ExtendedForecast(7, 'extendedForecast', weatherParameters), new Almanac(8, 'alamanac', weatherParameters), + new Radar(8, 'radar', weatherParameters), ]; } else { // or just call for new data if the canvases already exist diff --git a/server/scripts/modules/radar.js b/server/scripts/modules/radar.js new file mode 100644 index 0000000..c2dbbba --- /dev/null +++ b/server/scripts/modules/radar.js @@ -0,0 +1,322 @@ +// current weather conditions display +/* globals WeatherDisplay, utils, STATUS, icons, UNITS, draw, navigation */ + +// eslint-disable-next-line no-unused-vars +class Radar extends WeatherDisplay { + constructor(navId,elemId,weatherParameters) { + super(navId,elemId); + + // set max images + this.dopplerRadarImageMax = 6; + + // pre-load background image (returns promise) + this.backgroundImage = utils.image.load('images/BackGround4_1.png'); + + // get the data + this.getData(weatherParameters); + } + + async getData(weatherParameters) { + super.getData(); + + // ALASKA ISN'T SUPPORTED! + if (weatherParameters.state === 'AK') { + this.setStatus(STATUS.noData); + return; + } + + // get the base map + let src = 'images/4000RadarMap2.jpg'; + if (weatherParameters.State === 'HI') src = 'images/HawaiiRadarMap2.png'; + this.baseMap = await utils.image.load(src); + + const baseUrl = 'Conus/RadarImg/'; + + let radarHtml; + try { + // get a list of available radars + radarHtml = await $.ajax({ + type: 'GET', + url: baseUrl, + dataType: 'text', + crossDomain: true, + }); + } catch (e) { + console.error('Unable to get list of radars'); + console.error(e); + this.setStatus(STATUS.error); + return; + } + + // convert to an array of gif urls + const $list = $(radarHtml); + const gifs = $list.find('a[href]').map((i,elem) => elem.innerHTML).get(); + + // filter for selected urls + let filter = /^Conus_\d/; + if (weatherParameters.State === 'HI') filter = /hawaii_\d/; + + // get the last few images + const urlsFull = gifs.filter(gif => gif.match(filter)); + const urls = urlsFull.slice(-this.dopplerRadarImageMax); + + // calculate offsets and sizes + let offsetX = 120; + let offsetY = 69; + let sourceXY; + let width; + let height; + if (weatherParameters.State === 'HI') { + width = 600; + height = 571; + sourceXY = this.getXYFromLatitudeLongitudeHI(weatherParameters.latitude, weatherParameters.longitude, offsetX, offsetY); + } else { + width = 2550; + height = 1600; + offsetX *= 2; + offsetY *= 2; + sourceXY = this.getXYFromLatitudeLongitudeDoppler(weatherParameters.latitude, weatherParameters.longitude, offsetX, offsetY); + } + + // create working context for manipulation + const workingCanvas = document.createElement('canvas'); + workingCanvas.width = width; + workingCanvas.height = height; + const workingContext = workingCanvas.getContext('2d'); + workingContext.imageSmoothingEnabled = false; + + // calculate radar offsets + let radarOffsetX = 117; + let radarOffsetY = 60; + let radarSourceXY = this.getXYFromLatitudeLongitudeDoppler(weatherParameters.latitude, weatherParameters.longitude, offsetX, offsetY); + let radarSourceX = radarSourceXY.x / 2; + let radarSourceY = radarSourceXY.y / 2; + + if (weatherParameters.State === 'HI') { + radarOffsetX = 120; + radarOffsetY = 69; + radarSourceXY = this.getXYFromLatitudeLongitudeHI(weatherParameters.latitude, weatherParameters.longitude, offsetX, offsetY); + radarSourceX = radarSourceXY.x; + radarSourceY = radarSourceXY.y; + } + + // Load the most recent doppler radar images. + const radarCanvases = await Promise.all(urls.map(async (url) => { + // create destination context + const canvas = document.createElement('canvas'); + canvas.width = 640; + canvas.height = 367; + const context = canvas.getContext('2d'); + context.imageSmoothingEnabled = false; + + // get the image + const blob = await $.ajaxCORS({ + type: 'GET', + url: baseUrl + url, + xhrFields: { + responseType: 'blob', + }, + crossDomain: true, + }); + + // assign to an html image element + const imgBlob = await utils.image.load(blob); + + // draw the entire image + if (weatherParameters.State === 'HI') { + workingContext.drawImage(imgBlob, 0, 0, 571, 600); + } else { + workingContext.drawImage(imgBlob, 0, 0, 2550, 1600); + } + + // clean the image + this.removeDopplerRadarImageNoise(workingContext); + + // get the base map + context.drawImage(await this.baseMap, sourceXY.x, sourceXY.y, offsetX*2, offsetY*2, 0, 0, 640, 367); + + // put the radar on top + context.drawImage(workingCanvas, radarSourceX, radarSourceY, (radarOffsetX * 2), (radarOffsetY * 2.33), 0, 0, 640, 367); + + return canvas; + })); + // set max length + this.timing.totalScreens = radarCanvases.length; + + console.log(radarCanvases); + } + + drawCanvas() { + super.drawCanvas(); + + + this.finishDraw(); + this.setStatus(STATUS.loaded); + } + + // utility latitude/pixel conversions + getXYFromLatitudeLongitude (Latitude, Longitude, OffsetX, OffsetY, state) { + if (state === 'HI') return this.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 }; + } + + 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 }; + } + + getXYFromLatitudeLongitudeDoppler (Latitude, Longitude, OffsetX, OffsetY) { + let y = 0; + let x = 0; + const ImgHeight = 3200; + const ImgWidth = 5100; + + y = (51.75 - 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 = ((-130.37 - 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: x * 2, y: y * 2 }; + } + + removeDopplerRadarImageNoise (RadarContext) { + const RadarImageData = RadarContext.getImageData(0, 0, RadarContext.canvas.width, RadarContext.canvas.height); + + // examine every pixel, + // change any old rgb to the new-rgb + for (let i = 0; i < RadarImageData.data.length; i += 4) { + // i + 0 = red + // i + 1 = green + // i + 2 = blue + // i + 3 = alpha (0 = transparent, 255 = opaque) + let [R, G, B, A] = RadarImageData.data.slice(i,i+4); + + // is this pixel the old rgb? + if ((R === 1 && G === 159 && B === 244) + || (R >= 200 && G >= 200 && B >= 200) + || (R === 4 && G === 233 && B === 231) + || (R === 3 && G === 0 && B === 244)) { + // Transparent + R = 0; + G = 0; + B = 0; + A = 0; + } else if (R === 2 && G === 253 && B === 2) { + // Light Green 1 + R = 49; + G = 210; + B = 22; + A = 255; + } else if (R === 1 && G === 197 && B === 1) { + // Light Green 2 + R = 0; + G = 142; + B = 0; + A = 255; + } else if (R === 0 && G === 142 && B === 0) { + // Dark Green 1 + R = 20; + G = 90; + B = 15; + A = 255; + } else if (R === 253 && G === 248 && B === 2) { + // Dark Green 2 + R = 10; + G = 40; + B = 10; + A = 255; + } else if (R === 229 && G === 188 && B === 0) { + // Yellow + R = 196; + G = 179; + B = 70; + A = 255; + } else if (R === 253 && G === 139 && B === 0) { + // Orange + R = 190; + G = 72; + B = 19; + A = 255; + } else if (R === 212 && G === 0 && B === 0) { + // Red + R = 171; + G = 14; + B = 14; + A = 255; + } else if (R === 188 && G === 0 && B === 0) { + // Brown + R = 115; + G = 31; + B = 4; + A = 255; + } + + // store new values + RadarImageData.data[i] = R; + RadarImageData.data[i + 1] = G; + RadarImageData.data[i + 2] = B; + RadarImageData.data[i + 3] = A; + } + + // rewrite the image + RadarContext.putImageData(RadarImageData, 0, 0); + } +} \ No newline at end of file diff --git a/server/scripts/twc3.js b/server/scripts/twc3.js index cfc3ef6..bbe3afc 100644 --- a/server/scripts/twc3.js +++ b/server/scripts/twc3.js @@ -2882,51 +2882,9 @@ const PopulateAlmanacInfo = async (WeatherParameters) => { -const ShowRegionalMap = async (WeatherParameters, TomorrowForecast1, TomorrowForecast2) => { -}; - - - - - -const GetXYFromLatitudeLongitudeDoppler = (Latitude, Longitude, OffsetX, OffsetY) => { - let y = 0; - let x = 0; - const ImgHeight = 3200; - const ImgWidth = 5100; - - y = (51.75 - 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 = ((-130.37 - 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: x * 2, y: y * 2 }; -}; - - - - - const ShowDopplerMap = async (WeatherParameters) => { - // ALASKA ISN'T SUPPORTED! - if (WeatherParameters.State === 'AK') { - WeatherParameters.Progress.DopplerRadar = LoadStatuses.NoData; - return; - } + let OffsetY; let OffsetX; @@ -2939,11 +2897,6 @@ const ShowDopplerMap = async (WeatherParameters) => { // Clear the current image. divDopplerRadarMap.empty(); - if (_DopplerRadarInterval !== null) { - window.clearTimeout(_DopplerRadarInterval); - _DopplerRadarInterval = null; - } - let src = 'images/4000RadarMap2.jpg'; if (WeatherParameters.State === 'HI') src = 'images/HawaiiRadarMap2.png'; const img = await utils.loadImg(src); diff --git a/views/twc3.ejs b/views/twc3.ejs index 758e687..66a08a2 100644 --- a/views/twc3.ejs +++ b/views/twc3.ejs @@ -27,6 +27,7 @@ + <% } %>