Added weather forcast card and service

This commit is contained in:
Martin Donnelly 2018-03-07 00:02:22 +00:00
parent 3ba1739c7b
commit 32d9bac574
14 changed files with 2094 additions and 10 deletions

View File

@ -37,6 +37,24 @@ app.get('/weather', cache('15 minutes'), (req, res) => {
}
});
app.get('/forecast', cache('15 minutes'), (req, res) => {
if (req.query.hasOwnProperty('ll'))
weather.doGetFullForcast(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('/weatheralert', cache('15 minutes'), (req, res) => {
if (req.query.hasOwnProperty('ll'))
// weather.doGetOpenWeather(req.query.ll)

View File

@ -6,7 +6,7 @@ const cheerio = require('cheerio');
const { reduceArticle } = require('./reducers/euronews');
const logger = require('log4js').getLogger('euronews');
const logger = require('log4js').getLogger('Euronews');
logger.level = 'debug';
module.exports = {

View File

@ -1,6 +1,6 @@
const cheerio = require('cheerio');
const logger = require('log4js').getLogger('Euronews Reducer');
const logger = require('log4js').getLogger('Euronews 🔧');
const { get, isEmpty } = require('lodash');
logger.level = 'debug';

View File

@ -1,4 +1,4 @@
const logger = require('log4js').getLogger('FS reducers');
const logger = require('log4js').getLogger('FS 🔧');
const { get, isEmpty } = require('lodash');

146
server/reducers/weather.js Normal file
View File

@ -0,0 +1,146 @@
const logger = require('log4js').getLogger('Weather 🔧');
const fecha = require('fecha');
const { get } = require('lodash');
logger.level = 'debug';
function getTodaysForcast(hourlyData) {
const data = get(hourlyData, 'data', []);
const output = [];
const now = new Date().getTime();
const nowToHour = now - (now % 3600000);
const tomorrow = nowToHour + (60 * 1000 * 60 * 24);
for (const item of data) {
const time = item.time * 1000;
if (!(time < nowToHour || time > tomorrow)) {
// logger.debug(item);
const newItem = { 'time': fecha.format(time, 'h A'), 'icon': item.icon, 'temp': item.temperature, 'precip': item.precipProbability };
output.push(newItem);
}
}
return output;
}
function getDailyForcast(dailyData) {
const data = get(dailyData, 'data', []);
const output = [];
const h24 = (60 * 1000 * 60 * 24);
const now = new Date().getTime();
const startOD = now - (now % h24);
const endOD = startOD + h24 - 1;
for (const item of data) {
const time = item.time * 1000;
if (!(time < endOD)) {
// logger.debug(item);
const newItem = {
'time': fecha.format(time, 'dddd'),
'icon': item.icon,
'tempHigh': item.temperatureHigh,
'tempLow': item.temperatureLow,
'precip': item.precipProbability,
'precipType': item.precipType
};
output.push(newItem);
}
}
return output;
}
function toCompass(degrees) {
return ['N', 'NNE', 'NE', 'ENE', 'E', 'ESE', 'SE', 'SSE', 'S', 'SSW', 'SW', 'WSW', 'W', 'WNW', 'NW', 'NNW', 'N'][Math.round(degrees / 11.25 / 2)];
}
function moonCalc(moonPhase) {
let output = '';
if (moonPhase === 0.0)
output = 'New moon';
else if (moonPhase >= 0.1 && moonPhase < 0.25)
output = 'waxing crescent';
else if (moonPhase === 0.25)
output = 'First Quarter';
else if (moonPhase > 0.25 && moonPhase < 0.5)
output = 'waxing gibbous';
else if (moonPhase === 0.5)
output = 'Full moon';
else if (moonPhase > 0.5 && moonPhase < 0.75)
output = 'Waning gibbous';
else if (moonPhase === 0.5)
output = 'Third quarter';
else if (moonPhase > 0.75)
output = 'Waning crescent';
return output;
// a value of 0 corresponds to a new moon, 0.25 to a first quarter moon, 0.5 to a full moon, and 0.75 to a last quarter moon. (The ranges
// in between these represent waxing crescent, waxing gibbous, waning gibbous, and waning crescent moons, respectively.)
}
function getDetails(dailyData) {
const data = get(dailyData, 'data', []);
const today = data[0];
const output = {};
output.summary = dailyData.summary;
output.icon = dailyData.icon;
output.humidity = today.humidity;
output.visibility = today.visibility;
output.uvIndex = today.uvIndex;
output.sunriseTime = fecha.format(today.sunriseTime * 1000, 'shortTime');
output.sunsetTime = fecha.format(today.sunsetTime * 1000, 'shortTime');
output.moonphase = moonCalc(today.moonPhase);
output.moonPhaseVal = today.moonPhase;
output.windSpeed = today.windSpeed;
output.pressure = today.pressure;
return output;
}
function reduceWeather(body = '') {
if (body === '') return {};
const obj = {};
const { currently, daily, hourly } = body;
const today = daily.data[0];
const outCurrent = {};
outCurrent.icon = get(currently, 'icon');
outCurrent.temperature = get(currently, 'temperature');
outCurrent.summary = get(currently, 'summary');
outCurrent.precip = get(currently, 'precipProbability');
outCurrent.precipType = get(currently, 'precipType');
outCurrent.tempMax = get(today, 'temperatureMax');
outCurrent.tempMin = get(today, 'temperatureMin');
outCurrent.windBearing = get(today, 'windBearing');
outCurrent.windBearingRead = toCompass(get(today, 'windBearing'));
const forcastToday = getTodaysForcast(hourly);
const dailyForecast = getDailyForcast(daily);
const details = getDetails(daily);
obj.currently = outCurrent;
obj.forcastToday = forcastToday;
obj.dailyForecast = dailyForecast;
obj.details = details;
obj.time = get(currently, 'time');
return obj;
}
module.exports = { reduceWeather };
//
/*
moonPhase optional, only on daily
The fractional part of the lunation number during the given day: a value of 0 corresponds to a new moon, 0.25 to a first quarter moon, 0.5 to a full moon, and 0.75 to a last quarter moon. (The ranges in between these represent waxing crescent, waxing gibbous, waning gibbous, and waning crescent moons, respectively.)
*/

View File

@ -1,6 +1,7 @@
const Client = require('request-json');
const logger = require('log4js').getLogger('Weather');
const weather = require('openweather-apis');
const { reduceWeather } = require('./reducers/weather');
logger.level = 'debug';
@ -43,6 +44,7 @@ function doGetOpenWeatherForecast(ll) {
function doGetDarkSkyWeather(ll) {
const query = `${ll}?units=uk2&exclude=daily,flags,minutely,hourly`;
return new Promise((resolve, reject) => {
DSclient.get(query, function(err, res, body) {
if (err || !body || !body.currently)
@ -53,4 +55,19 @@ function doGetDarkSkyWeather(ll) {
});
}
module.exports = { doGetOpenWeather, doGetOpenWeatherForecast, doGetDarkSkyWeather };
function doGetFullForcast(ll) {
const query = `${ll}?units=uk2&exclude=flags,minutely`;
return new Promise((resolve, reject) => {
DSclient.get(query, function(err, res, body) {
if (err || !body || !body.currently)
return reject(err);
const output = reduceWeather(body);
return resolve(output);
});
});
}
module.exports = { doGetOpenWeather, doGetOpenWeatherForecast, doGetDarkSkyWeather, doGetFullForcast };

View File

@ -14,6 +14,10 @@ $mui-base-font-family: 'Roboto', "Helvetica Neue", Helvetica, Arial, Verdana, "T
$mui-appbar-font-color: mui-color('black') !default;
$mui-link-font-color: mui-color('yellow', '900') !default;
// import MUI SASS
@import "./node_modules/muicss/lib/sass/mui";
@ -368,3 +372,106 @@ li {
max-height:100%;
max-width:100%;
}
.tiny {
font-size:1rem;
}
.small {
font-size:2rem;
}
.medium {
font-size:4rem;
}
.large {
font-size:6rem;
}
.cardLink {
color: mui-color('blue', '500')
}
.endbumper {
height:66px;
}
#weatherP {
background-size: cover;
}
.weatherDay {
background-image: url(../gfx/bg_morning.jpg);
}
.weatherEvening {
background-image: url(../gfx/bg_evening.jpg);
}
.cloudy_n {
background-image: url(../gfx/cloudy_n.jpg);
}
.cloudy_d {
background-image: url(../gfx/cloudy_d.jpg);
}
.clear_d {
background-image: url(../gfx/clear_d.jpg);
}
.clear_n {
background-image: url(../gfx/clear_n.jpg);
}
.foggy_d {
background-image: url(../gfx/foggy_d.jpg);
}
.foggy_n {
background-image: url(../gfx/foggy_n.jpg);
}
.rain_d {
background-image: url(../gfx/rain_d.jpg);
}
.rain_n {
background-image: url(../gfx/rain_n.jpg);
}
.snow_d {
background-image: url(../gfx/snow_d.jpg);
}
.snow_n {
background-image: url(../gfx/snow_n.jpg);
}
.storm_d {
background-image: url(../gfx/storm_d.jpg);
}
.storm_n {
background-image: url(../gfx/storm_n.jpg);
}
.glassy {
background-color: rgba(255, 255, 255, 0.4);
-webkit-backdrop-filter: blur(5px);
backdrop-filter: blur(2px);
}
.animTrans {
-webkit-transition: all 1s linear;
transition : all 1s linear;
-moz-transition : all 1s linear;
-webkit-transition: all 1s linear;
-o-transition : all 1s linear;
}

View File

@ -4,13 +4,13 @@
flex-wrap: nowrap;
overflow-x: auto;
.scrollCard, .scrollCardHalf, .imageCard {
.scrollCard, .scrollCardHalf, .imageCard, .hourlyCard {
flex: 0 0 auto;
margin-right: 11px;
}
}
.scrollCard, .scrollCardHalf {
.scrollCard, .scrollCardHalf, .hourlyCard {
width: 250px;
height: 175px;
overflow-y: hidden;
@ -24,6 +24,11 @@
height: 85px;
}
.hourlyCard {
width: 42px;
height:70px
}
.imageCard {
// width: 250px;
height: 175px;

View File

@ -11,7 +11,7 @@
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
const CACHE_VERSION = { 'version': '0.0.12' };
const CACHE_VERSION = { 'version': '0.0.151' };
const dataCacheName = 'jubileeData-v1';
const cacheName = 'jubilee-final-1';
const filesToCache = [
@ -37,7 +37,21 @@ const filesToCache = [
'/gfx/stars_40.png',
'/gfx/stars_45.png',
'/gfx/stars_50.png',
'/gfx/yssdk_yelp_logo.png'
'/gfx/yssdk_yelp_logo.png',
'/gfx/bg_evening.jpg',
'/gfx/bg_morning.jpg',
'/gfx/clear_d.jpg',
'/gfx/clear_n.jpg',
'/gfx/cloudy_d.jpg',
'/gfx/cloudy_n.jpg',
'/gfx/foggy_d.jpg',
'/gfx/foggy_n.jpg',
'/gfx/rain_d.jpg',
'/gfx/rain_n.jpg',
'/gfx/snow_d.jpg',
'/gfx/snow_n.jpg',
'/gfx/storm_d.jpg',
'/gfx/storm_n.jpg'
];
self.addEventListener('install', function(e) {

234
src/v1/js/Forecast.js Normal file
View File

@ -0,0 +1,234 @@
const $ = require('jquery');
const _ = require('underscore');
const Backbone = require('backbone');
const request = require('request');
const { get, isEmpty } = require('lodash');
const { createPanel, addPanel } = require('./libs/panel');
const ForecastModel = Backbone.Model.extend({
'initialize': function() {
this.listenTo(this, 'change:ll', this.newGuid);
},
'newGuid': function() {
console.log('>> LL changed:', this.get('ll'));
this.getForecast();
},
'getForecast': function() {
const ll = this.get('ll');
request({
'url': `${window.loc}/forecast`,
'method': 'GET',
'qs': {
'll': ll
}
}, function(err, res, body) {
if (err)
console.error(err);
else{
// console.log(body);
const fsJSON = JSON.parse(body);
// console.log(fsJSON);
this.set(fsJSON);
// console.log(body);
}
}.bind(this));
}
});
const ForecastView = Backbone.View.extend({
'initialize': function(options) {
this.eventBus = options.eventBus;
/* this.model.on('all', function(eventName) {
console.log(`${eventName } was triggered!`);
});*/
this.model.bind('change:time', this.doRender, this);
this.eventBus.on('showForecast', this.showForecastPanel, this);
},
'templateCurrently': _.template(`
<div class="forecastCurrently mui-container-fluid glassy">
<div class="mui--text-title"><i class="wi wi-forecast-io-<%= icon %>"></i> <%=summary%></div>
<div>
<i class="small material-icons mui--align-middle " >vertical_align_top</i><%=tempMax%>°
<i class="small material-icons mui--align-middle " >vertical_align_bottom</i><%=tempMin%>°
</div>
<div class="large temp<%=~~(temperature) %>"><%=temperature%>°</div>
</div>
`),
'templateForecast': _.template(`
<div class="forecastToday mui-container-fluid glassy">
<h3>Forecast</h3>
<div class='scrolling-wrapper-flexbox hourly' style="height: 100px;">
<%_.forEach(today, function(i) {%>
<div class="hourlyCard">
<div class="mui--text-center"><%=i.time %></div>
<div class="mui--text-center"><i class="small wi wi-forecast-io-<%= i.icon %>"></i></div>
<div class="mui--text-center temp<%=~~(i.temp) %>"><%=~~(i.temp)%>°</div>
</div>
<%}) %>
</div>
<div>
<div class="dailyCard">
<%_.forEach(daily, function(i) {%>
<div class="mui-row">
<div class="mui-col-xs-6 mui--text-left mui--text-title"><%=i.time %></div>
<div class="mui-col-xs-2 mui--text-center"><i class="mui--text-title wi wi-forecast-io-<%= i.icon %>"></i></div>
<div class="mui-col-xs-2 mui--text-center temp<%=~~(i.tempHigh) %> mui--text-title "><%=~~(i.tempHigh)%>°</div>
<div class="mui-col-xs-2 mui--text-center temp<%=~~(i.tempLow) %> mui--text-title"><%=~~(i.tempLow)%>°</div>
</div>
<%}) %>
</div>
</div>
</div>
`),
'templateDetails': _.template(`
<div class="forecastDetails mui-container-fluid glassy">
<h3>Details</h3>
<div class="mui-row">
<div class="mui-col-xs-6 mui--text-left mui--text-body2">Humidity</div>
<div class="mui-col-xs-6 mui--text-right mui--text-title"><%=~~(humidity % 100) %>%</div>
</div>
<div class="mui-row">
<div class="mui-col-xs-6 mui--text-left mui--text-body2">Visibility</div>
<div class="mui-col-xs-6 mui--text-right mui--text-title"><%=visibility %><span class="mui--text-body1">km</span></div>
</div>
<div class="mui-row">
<div class="mui-col-xs-6 mui--text-left mui--text-body2">UV Index</div>
<div class="mui-col-xs-6 mui--text-right mui--text-title"><%=uvIndex %></div>
</div>
<div class="mui-row">
<div class="mui-col-xs-12 mui--text-left mui--text-body2"><%=summary%></div>
</div>
<h3>Wind & Pressure</h3>
<div class="mui-row">
<div class="mui-col-xs-3 mui--text-left mui--text-body2">Wind</div>
<div class="mui-col-xs-3 mui--text-right mui--text-title"><%=windSpeed %><span class="mui--text-body1">km/h</span></div>
<div class="mui-col-xs-3 mui--text-left mui--text-body2">Barometer</div>
<div class="mui-col-xs-3 mui--text-right mui--text-title"><%=pressure %><span class="mui--text-body1">mBar</span></div>
</div>
<h3>Sun & Moon</h3>
<div class="mui-row">
<div class="mui-col-xs-3 mui--text-left mui--text-body2">Sunrise</div>
<div class="mui-col-xs-3 mui--text-right mui--text-title"><%=sunriseTime %></div>
<div class="mui-col-xs-3 mui--text-left mui--text-body2">Sunset</div>
<div class="mui-col-xs-3 mui--text-right mui--text-title"><%=sunsetTime %></div>
</div>
</div>
`),
'showForecastPanel': function(ll) {
console.log('Showing forecast', ll);
const prevll = this.model.get('ll');
const hour = (new Date()).getHours();
this.model.set('ll', ll);
this.$newPanel = createPanel({ 'title':'Weather forecast', 'divId':'weatherP' });
this.$el = addPanel(this.$newPanel);
this.$el.addClass('animTrans');
if (hour >= 6 && hour < 18 )
this.$el.addClass('weatherDay');
else
this.$el.addClass('weatherEvening');
this.$el.empty();
this.$newPanel.show();
if (prevll === ll)
this.doRender();
},
'events': {
'click .closebutton': 'doClick'
},
'doClick': function(d) {
console.log('Do click', d);
const id = get(d, 'currentTarget', '');
console.log(id);
// this.eventBus.trigger('showNews', id);
},
'doClose': function(d) {
console.log('close??');
},
'doRender': function() {
// this.$el.html(this.template(this.model.get('article')));
console.log('ForecastView::doRender');
const html = [];
const currently = this.model.get('currently');
const forcastToday = { 'today':this.model.get('forcastToday'), 'daily':this.model.get('dailyForecast') };
const details = this.model.get('details');
html.push(this.templateCurrently(currently));
html.push(this.templateForecast(forcastToday));
html.push(this.templateDetails(details));
html.push('<div class="endbumper"></div>');
this.$el.addClass(this.backgroundGen(currently.icon));
this.$el.html(html.join(''));
},
'backgroundGen': function(icon) {
const hour = (new Date()).getHours();
const suffix = (hour >= 6 && hour < 18 ) ? '_d' : '_n';
let output = '';
switch (icon) {
case 'clear':
output = `clear${suffix}`;
break;
case 'clear-day':
output = 'clear_d';
break;
case 'clear-night':
output = 'clear_n';
break;
case 'rain':
output = `rain${suffix}`;
break;
case 'snow':
case 'sleet':
case 'hail':
output = `snow${suffix}`;
break;
case 'fog':
output = `foggy${suffix}`;
break;
case 'wind':
case 'cloudy':
output = `cloudy${suffix}`;
break;
case 'partly-cloudy-day':
output = 'cloudy_d';
break;
case 'partly-cloudy-night':
output = 'cloudy_n';
break;
case 'thunderstorm':
case 'tornado':
output = `storm${suffix}`;
break;
default:
console.log(`Sorry, we are out of ${ icon }.`);
}
return output;
}
});
module.exports = { ForecastModel, ForecastView };

View File

@ -38,6 +38,9 @@ const weatherItemView = Backbone.View.extend({
<div class="mui--text-dark-secondary mui--text-caption mui--text-right">
For <%=city%> to <%= readdate %>
</div>
<div id='wForecast' class="cardLink">
<i class="small material-icons mui--align-middle " >arrow_forward</i> <span class="mui--align-middle">See forecast</span>
</div>
`),
'initialize': function() {
this.render();
@ -161,7 +164,7 @@ const WeatherView = Backbone.View.extend({
};
},
'events': {
'click': 'doClick'
'click #wForecast': 'doClick'
},
'updateLocation': function(l) {
console.log('>> Weather Location has changed...');
@ -186,6 +189,11 @@ const WeatherView = Backbone.View.extend({
this.$el.parent().show();
// console.log(wView.el);
}, 'doClick': function(d) {
console.log('Do click', this);
const llFixed = this.location.get('llFixed');
console.log(llFixed);
this.eventBus.trigger('showForecast', llFixed);
}
});

View File

@ -9,6 +9,7 @@ const { GreetModel, GreetView } = require('./Greet');
const { NearbyModel, NearbyView } = require('./Nearby');
const { WeatherModel, WeatherView } = require('./Weather');
const { WeatherAlertModel, WeatherAlertView } = require('./WeatherAlert');
const { ForecastModel, ForecastView } = require('./Forecast');
const { NewsModel, NewsView } = require('./News');
const { NewsCardModel, NewsCardView } = require('./NewsViewer');
const { ByMeModel, ByMeView } = require('./RightByMe');
@ -63,6 +64,8 @@ else
app.weatherAlert = new WeatherAlertView({ 'model': new WeatherAlertModel(), 'eventBus': app.eventBus, 'location': app.locationModel, 'el':'#weatherAlert', 'viewFrame':'#viewFrame' });
app.forecastCard = new ForecastView({ 'model': new ForecastModel(), 'eventBus': app.eventBus });
app.newsCard = new NewsCardView({ 'model': new NewsCardModel(), 'eventBus': app.eventBus });
app.news = new NewsView({ 'model': new NewsModel(), 'eventBus': app.eventBus, 'el':'#news' });

View File

@ -10,7 +10,7 @@ function createPanel(params) {
<div class="mui-appbar mui--appbar-line-height mui--z1" style="vertical-align:middle;">
<span>
<button class="mui-btn mui-btn--large mui-btn--primary mui-btn--flat closebutton" id="close_${divId}">
<i class="large material-icons mui--align-middle" style="color:black;">arrow_back</i>
<i class="medium material-icons mui--align-middle" style="color:black;">arrow_back</i>
</button>
</span>

1532
test/weather.spec.js Normal file

File diff suppressed because it is too large Load Diff