From 9f13a3e391478f979591a436554dc48bc4ff4b4d Mon Sep 17 00:00:00 2001 From: Martin Donnelly Date: Thu, 1 Mar 2018 23:45:04 +0000 Subject: [PATCH] Added weather alerts --- package-lock.json | 9 +++ package.json | 1 + server.js | 20 +++++ server/weather.js | 19 ++++- src/v1/index.html | 24 +++--- src/v1/js/Location.js | 11 ++- src/v1/js/Weather.js | 4 +- src/v1/js/WeatherAlert.js | 161 ++++++++++++++++++++++++++++++++++++++ src/v1/js/app.js | 5 +- src/v1/js/reducers.js | 32 +++++++- 10 files changed, 265 insertions(+), 21 deletions(-) create mode 100644 src/v1/js/WeatherAlert.js diff --git a/package-lock.json b/package-lock.json index db4f64d..7ce74e9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9588,6 +9588,15 @@ "error": "7.0.2" } }, + "request-json": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/request-json/-/request-json-0.6.3.tgz", + "integrity": "sha512-5TVnMD3LaeK0GRCyFlsNgJf5Fjg8J8j7VEfsoJESSWZlWRgPIf7IojsBLbTHcg2798JrrRkJ6L3k1+wj4sglgw==", + "requires": { + "depd": "1.1.2", + "request": "2.83.0" + } + }, "request-promise": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/request-promise/-/request-promise-4.2.2.tgz", diff --git a/package.json b/package.json index c715a49..f573cfa 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "muicss": "^0.9.36", "node-foursquare-venues": "^1.1.0", "openweather-apis": "^3.3.5", + "request-json": "^0.6.3", "twitter": "^1.7.1", "uglifyify": "^4.0.5", "underscore": "^1.8.3", diff --git a/server.js b/server.js index d229b5b..d773a7a 100644 --- a/server.js +++ b/server.js @@ -21,6 +21,7 @@ app.use(express.static(path.join(__dirname, sitePath))); app.get('/weather', cache('45 minutes'), (req, res) => { if (req.query.hasOwnProperty('ll')) + weather.doGetOpenWeather(req.query.ll) .then((d) => { res.send(d); @@ -36,6 +37,25 @@ app.get('/weather', cache('45 minutes'), (req, res) => { } }); +app.get('/weatheralert', cache('45 minutes'), (req, res) => { + if (req.query.hasOwnProperty('ll')) + // weather.doGetOpenWeather(req.query.ll) + // doGetDarkSkyWeather + weather.doGetDarkSkyWeather(req.query.ll) + .then((d) => { + res.send(d); + }).catch((e) => { + logger.error(e); + res.status(500).send('There was an error!'); + }); + + else { + // throw new Error('Weather: LL missing'); + logger.warn('Weather: LL missing'); + res.status(500).send('LL Missing'); + } +}); + app.get('/fsexplore', cache('30 minutes'), (req, res) => { if (req.query.hasOwnProperty('ll')) foursquare.doGetFourSquareExplore(req.query.ll) diff --git a/server/weather.js b/server/weather.js index 3fa8236..557acd8 100644 --- a/server/weather.js +++ b/server/weather.js @@ -1,4 +1,4 @@ - +const Client = require('request-json'); const logger = require('log4js').getLogger('Weather'); const weather = require('openweather-apis'); @@ -6,6 +6,9 @@ logger.level = 'debug'; const openWeatherApiKey = process.env.openweatherAPI || '936a0ed9eb23b95cf08fc9f693c24264'; +const darkskyApiKey = process.env.darkskyApiKey || '9ad2a41d420f3cf4960571bb886f710c'; +const DSclient = Client.createClient(`https://api.darksky.net/forecast/${ darkskyApiKey }/`); + weather.setAPPID(openWeatherApiKey); weather.setLang('en'); // weather.setCity('Glasgow City'); @@ -38,5 +41,17 @@ function doGetOpenWeatherForecast(ll) { }); } +function doGetDarkSkyWeather(ll) { + const query = `${ll}?units=uk2&exclude=daily,flags,minutely,hourly`; + logger.debug(query); + return new Promise((resolve, reject) => { + DSclient.get(query, function(err, res, body) { + if (err || !body || !body.currently) + return reject(err); -module.exports = { doGetOpenWeather, doGetOpenWeatherForecast }; + return resolve(body); + }); + }); +} + +module.exports = { doGetOpenWeather, doGetOpenWeatherForecast, doGetDarkSkyWeather }; diff --git a/src/v1/index.html b/src/v1/index.html index a73dddb..2f2dc47 100644 --- a/src/v1/index.html +++ b/src/v1/index.html @@ -25,10 +25,16 @@
+
+
+ +
By me
+
+
+
Latest news
+
+ +
Weather
+
+ diff --git a/src/v1/js/Location.js b/src/v1/js/Location.js index 5769e6f..717aac6 100644 --- a/src/v1/js/Location.js +++ b/src/v1/js/Location.js @@ -113,7 +113,6 @@ const LocationModel = Backbone.Model.extend({ console.error(err); this.set('location', newLocation); }); - } else { newLocation.city = current.city; @@ -126,16 +125,16 @@ const LocationModel = Backbone.Model.extend({ // console.log('>> distanceFromLastGeocode', distanceFromLastGeocode, TimeFormat.fromMs(timestamp - lastGeocode.timestamp, 'hh:mm:ss')); console.log('>> distanceFromLastGeocode', distanceFromLastGeocode, TimeFormat.fromMs(currentTime - lastGeocode.timestamp, 'hh:mm:ss')); console.log(`(currentTime:${currentTime}, timestamp:${timestamp}, lastGeocode.timestamp:${lastGeocode.timestamp})`); - console.log('(currentTime - current.timestamp > 900000) ' , (currentTime - current.timestamp > 900000)); - //if ((distanceFromLast > 0.5 && distanceFromLast < 2.0) || (timestamp - current.timestamp > 900000)) { - if ((distanceFromLast > 0.5 && distanceFromLast < 2.0) || (currentTime - current.timestamp > 900000)) { + console.log('(currentTime - current.timestamp > 900000) ', (currentTime - current.timestamp > 900000)); + // if ((distanceFromLast > 0.5 && distanceFromLast < 2.0) || (timestamp - current.timestamp > 900000)) { + if ((distanceFromLast > 0.5 && distanceFromLast < 2.0) || ((currentTime - current.timestamp > 900000) && (currentTime - lastGeocode.timestamp < 1.8e+6))) { // dont bother re geocoding console.log('Slightly moved from previous'); this.set('location', newLocation); this.set('moving', moving); } - else if (distanceFromLastGeocode >= 2.0 || (timestamp - lastGeocode.timestamp > 1.8e+6) ) { - console.log('Moved from previous', (timestamp - lastGeocode.timestamp > 1.8e+6)); + else if (distanceFromLastGeocode >= 2.0 || (currentTime - lastGeocode.timestamp >= 1.8e+6) ) { + console.log('Moved from previous', (currentTime - lastGeocode.timestamp >= 1.8e+6)); console.info('>> Location:geocoder request'); geocoder.reverse(latlong) .then(function(res) { diff --git a/src/v1/js/Weather.js b/src/v1/js/Weather.js index 6f3ac72..e0c01a4 100644 --- a/src/v1/js/Weather.js +++ b/src/v1/js/Weather.js @@ -92,7 +92,7 @@ const WeatherModel = Backbone.Model.extend({ }, 'getWeather': function() { // const ll = this.get('llShort'); - console.info('>> Weather:request'); + console.log('>> Weather request'); const llFixed = this.get('llFixed'); request({ 'url': `${window.loc}/weather`, @@ -175,7 +175,7 @@ const WeatherView = Backbone.View.extend({ console.log('>> Weather No location yet'); }, 'render': function() { - console.info('>> Weather:Render'); + console.log('>> Weather:Render'); this.$el.empty(); const item = this.wCollection.first(); diff --git a/src/v1/js/WeatherAlert.js b/src/v1/js/WeatherAlert.js new file mode 100644 index 0000000..74c8b0b --- /dev/null +++ b/src/v1/js/WeatherAlert.js @@ -0,0 +1,161 @@ +const $ = require('jquery'); +const _ = require('underscore'); +const Backbone = require('backbone'); +const request = require('request'); +const fecha = require('fecha'); +const { get } = require('lodash'); +const { reduceOpenWeather } = require('./reducers'); +const { distance, toHour } = require('./utils'); + +const WeatherAlertModel = Backbone.Model.extend({ + 'defaults': function (obj) { + // return a new object + return { + 'update': new Date().getTime() + }; + }, + 'initialize': function () { + this.run = false; + this.listenTo(this, 'change:ll', this.onChange); + this.listenTo(this, 'change:update', this.onChange); + }, + 'onChange': function () { + console.log('Weather LL has changed'); + // if distance change > 10km + // if its been an hour since last update + + if (!this.has('log')) { + console.info('First run'); + this.getWeatherAlert(); + } + else { + const log = this.get('log'); + const timeDiff = new Date().getTime() - log.time; + const ll = this.get('ll').split(','); + + const dist = distance(log.lat, log.long, ll[0], ll[1]); + console.log('Weather distance:', dist); + + if ((dist > 5.0) && (timeDiff > 1.8e+6)) + this.getWeatherAlert(); + else if (timeDiff > 3.6e+6) { + console.log('WeatherAlert hourly update'); + this.getWeatherAlert(); + } + } + }, + 'getWeatherAlert': function () { + // const ll = this.get('llShort'); + console.log('>> WeatherAlert request'); + const llFixed = this.get('ll'); + request({ + 'url': `${window.loc}/weatheralert`, + 'method': 'GET', + 'qs': { + 'll': llFixed + } + }, function (err, res, body) { + if (err) + console.error(err); + else { + // console.log(body); + const fsJSON = JSON.parse(body); + + const alerts = get(fsJSON, 'alerts', []); + const item = alerts.slice(0, 1); + + this.set('alert', item); + + console.log(fsJSON); + + this.logUpdate(); + } + }.bind(this)); + }, + 'logUpdate': function () { + console.log('WeatherAlert logging:'); + + const ll = this.get('ll').split(','); + + const log = { 'lat': ll[0], 'long': ll[1], 'time': new Date().getTime() }; + + console.log('>>WeatherAlert log', log); + this.set('log', log); + + this.timerID = setTimeout( + () => this.tick(), + toHour + ); + + // console.log(this); + }, + 'tick': function () { + console.log('Set update'); + this.set('update', new Date().getTime()); + } + +} +); + +const WeatherAlertView = Backbone.View.extend({ + 'className': 'mui--align-middle', + + 'initialize': function(options) { + this.eventBus = options.eventBus; + this.location = options.location; + + this.$parent = this.$el.parent(); + this.$title = $('#weatherAlertTitle'); + + // this.model.bind('change', this.render, this); + this.location.bind('change', this.updateLocation, this); + this.model.bind('change:alert', this.render, this); + }, + 'template': _.template(` +
+
<%=description%>
+ +
+ Expires <%= readdate %> +
+ `), + 'attributes': function() { + return { + 'class': 'mui--align-middle' + }; + }, + 'events': { + 'click': 'doClick' + }, + 'updateLocation': function(l) { + console.log('>> WeatherAlert Location has changed...'); + + if (l.has('location')) { + const location = l.get('location'); + if (location.hasOwnProperty('atHome')) + this.model.set('ll', location.llFixed); + } + else + console.log('>> Weather No location yet'); + }, + 'render': function() { + console.log('>> WeatherAlert:Render'); + + this.$el.empty(); + + const alert = this.model.get('alert'); + + if (alert.length === 0) + // nothing to see, hide the alert. + this.$parent.hide(); + else { + const fts = new Date(alert[0].expires * 1000); + alert[0].readdate = fecha.format(fts, 'default'); + this.$title.html(alert[0].title); + this.$el.html(this.template(alert[0])); + this.$el.parent().show(); + } + } +}); + +module.exports = { WeatherAlertModel, WeatherAlertView }; diff --git a/src/v1/js/app.js b/src/v1/js/app.js index e7f2edb..efd0a44 100644 --- a/src/v1/js/app.js +++ b/src/v1/js/app.js @@ -7,6 +7,7 @@ const { LocationModel } = require('./Location'); const { GreetModel, GreetView } = require('./Greet'); const { NearbyModel, NearbyView } = require('./Nearby'); const { WeatherModel, WeatherView } = require('./Weather'); +const { WeatherAlertModel, WeatherAlertView } = require('./WeatherAlert'); const { NewsModel, NewsView } = require('./News'); const { ByMeModel, ByMeView } = require('./RightByMe'); @@ -56,7 +57,9 @@ else app.nearby = new NearbyView({ 'model': new NearbyModel(), 'eventBus': app.eventBus, 'location': app.locationModel, 'el':'#nearby' }); - app.weather = new WeatherView({ 'model': new WeatherModel(), 'eventBus': app.eventBus, 'location': app.locationModel, 'el':'#weather' }); + app.weather = new WeatherView({ 'model': new WeatherModel(), 'eventBus': app.eventBus, 'location': app.locationModel, 'el':'#weather', 'viewFrame':'#viewFrame' }); + + app.weatherAlert = new WeatherAlertView({ 'model': new WeatherAlertModel(), 'eventBus': app.eventBus, 'location': app.locationModel, 'el':'#weatherAlert', 'viewFrame':'#viewFrame' }); app.news = new NewsView({ 'model': new NewsModel(), 'eventBus': app.eventBus, 'el':'#news' }); diff --git a/src/v1/js/reducers.js b/src/v1/js/reducers.js index 43fee21..bdc7d4a 100644 --- a/src/v1/js/reducers.js +++ b/src/v1/js/reducers.js @@ -1,6 +1,36 @@ const fecha = require('fecha'); const { get } = require('lodash'); +function reduceDarksky(item, city) { + // Openweather returns timestamps in seconds. Moment requires them in milliseconds. + // Replaced Moment with Fecha. + // Too many issues trying to get Moment packaged into the bundle. + + if (!item || item === null) return {}; + const currently = get(item, 'currently'); + const alerts = get(item, 'alerts', []); + const fts = new Date(currently.time * 1000); + + const newItem = { 'timestamp': fts, + 'icon': `wi-wu-${currently.icon}`, + 'summary': currently.summary, + 'temp': parseInt(currently.temperature, 10), + 'feels': parseFloat(currently.apparentTemperature, 10), + 'datelong': fecha.format(fts, 'YYYY-MM-DDTHH:mm:ss.SSSZZ'), + 'readdate': fecha.format(fts, 'default'), + 'time': fts, + 'date': fecha.format(fts, 'D/M'), + 'day': fecha.format(fts, 'ddd'), + 'city': city + }; + + return { + 'item' : newItem, + 'alerts': (alerts.length === 0) ? alerts : alerts.slice(0, 1) + + }; +} + function reduceOpenWeather(item, city) { // Openweather returns timestamps in seconds. Moment requires them in milliseconds. // Replaced Moment with Fecha. @@ -88,4 +118,4 @@ const pubdateSrc = fecha.parse(item.pubdate, 'ddd, DD MMM YYYY HH:mm:SS ZZ'); `; */ -module.exports = { reduceOpenWeather, reduceNearby, reduceEuronews }; +module.exports = { reduceOpenWeather, reduceNearby, reduceEuronews, reduceDarksky };