forked from SkiFilmReviews/snow-forecast-sfr
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathsnow-request.js
250 lines (227 loc) · 9.18 KB
/
snow-request.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
/**
* Core part of npm. Deals directly with making the requests to snow-forecast as
* well as parsing the returned JSON and creating the relevant object
**/
var SnowRequest = function() {
var snowRequest = {};
var request = require('request');
var cheerio = require('cheerio');
var TimeUtil = require('./time-util.js');
var UnitUtil = require('./unit-util.js');
var Elevation = require('./elevation.js');
var coreURL = 'http://www.snow-forecast.com/resorts/';
var unitsInMetric;
var MAX_CELLS = 18;
/**
* Fires off a specific request to snow-forecast and passes response to callback
* url: The generated url
* cb: Callback to pass response to
*/
function pMakeRequest(url, cb, opts){
request(url, function(error, response, html){
if(!error){
var $;
try{
if(html.length < 1500 && html.indexOf('The page you were looking for doesn\'t exist') > -1){
cb(['Invalid page(404)', 'Unable to find relevant resort forecast, ' +
'please check the spelling and try again', url]);
return;
}
$ = cheerio.load(html); //Load html using cheerio for parsing
cb($);
} catch(ex){
cb(['Parse error', ex, url]);
}
} else {
cb(['Remote server error', 'Unable to get response from snow-forecast' + error, url]);
}
});
}
/**
* Helper method to build error JSON object in case something goes wrong
* error: Type of error
* message: Additional information regarding the error
* url: URL used to make request
*/
function pBuildErrorJSON(error, message, url){
return {
error: error,
message: message,
url: url
};
}
/**
* Helper method to build forecast JSON object
* $: The cheerio object which we use to extract the forecast data
* forecastOpt: Hash object with following information:
* resort: Resort name
* url: URL of request
* elevation: Elevation requested
* issuedDate: Date that forecast was issued
* startDay: First day of forecast
* isMetric: Whether response is in metric units or not
* cb: Callback function to pass completed JSON object to.
*/
function pBuildForecast($, forecastOpt, cb) {
//Get various forecast information containers
var firstTime = $($('.forecast-table-time__period')[0]).text();
var snowForecast = $('span.snow');
var rainForecast = $('span.rain');
var freezingLevel = $('span.heightfl');
var winds = $('table tr[data-row="wind"] .forecast-table-wind__container svg text');
var windChillTempContainer = $('table tr[data-row="temperature-chill"]');
var maxTempContainer = $('table tr[data-row="temperature-max"]');
var minTempContainer = $('table tr[data-row="temperature-min"]');
var maxTemp = $(maxTempContainer).find('span.temp');
var minTemp = $(minTempContainer).find('span.temp');
var windChill = $(windChillTempContainer).find('span.temp');
var summary = $('table tr[data-row="phrases"] span');
//Create forecast object, and init forecast array for later
var forecastObj = {
name: forecastOpt.resort,
url: forecastOpt.url,
issuedDate: forecastOpt.issuedDate,
elevation: forecastOpt.elevation,
units: unitsInMetric ? 'metric' : 'imperial',
forecast: []
};
var forecastArr = [];
//Loop over forecasts, get relevant information for each and push to temp array
for(var i = 0; i < MAX_CELLS; i++){
var cellObj = {
date: TimeUtil.getDay(forecastOpt.lastUpdateDate, forecastOpt.startDay, TimeUtil.getTimeOffset(firstTime), i),
time: TimeUtil.getTime(TimeUtil.getTimeOffset(firstTime), forecastOpt.startDay, i), //issued[1] is startDay
summary: $(summary[i]).text(),
wind: parseInt($(winds[i]).text(), 10),
windDirection: pGetWindDirection($, winds[i]),
snow: parseInt($(snowForecast[i]).text(), 10) || 0,
rain: parseInt($(rainForecast[i]).text(), 10) || 0,
freezingLevel: parseInt($(freezingLevel[i]).text(),10),
minTemp: parseInt($(minTemp[i]).text(), 10),
maxTemp: parseInt($(maxTemp[i]).text(), 10),
windChill: parseInt($(windChill[i]).text(), 10)
};
//If units requested isn't what's returned, convert
if(forecastOpt.isMetric !== unitsInMetric){
cellObj = pConvertUnits(cellObj, unitsInMetric);
}
forecastArr.push(cellObj);
}
forecastObj.forecast = forecastArr;
cb(forecastObj);
}
/*
* Helper method that finds if a wind cell has an image with an alternate text
* value. This is used as it is the only place where we can get the wind direction
* from.
*
* If it fails to find one it will return an empty string.
* cell
* The HTMLElement holding for the wind.
*/
function pGetWindDirection($, cell){
var img = $(cell).find('img');
var windDirectionString = '';
var maxDescriptionParts = 2;
if(img && img.length > 0) {
var imageCellAlt = $(img[0]).attr('alt');
//We don't want the speed just direction, so split string and get last part
if(imageCellAlt && imageCellAlt.split(' ').length == maxDescriptionParts) {
windDirectionString = imageCellAlt.split(' ')[1];
}
}
return windDirectionString;
}
/*
* Simple method that converts all relevant fields to either metric or imperial.
* obj
* The existing forecast object that contains all the fields
* toMetric
* a boolean on whether to convert to metric or not.
*/
function pConvertUnits(obj, toMetric){
if(toMetric){
obj.wind = UnitUtil.speedToMetric(obj.wind);
obj.snow = UnitUtil.volumeToMetric(obj.snow) || 0;
obj.rain = UnitUtil.volumeToMetric(obj.rain/10) || 0;
obj.freezingLevel = UnitUtil.distanceToMetric(obj.freezingLevel);
obj.minTemp = UnitUtil.temperatureToMetric(obj.minTemp);
obj.maxTemp = UnitUtil.temperatureToMetric(obj.maxTemp);
} else {
obj.wind = UnitUtil.speedToImperial(obj.wind);
obj.snow = UnitUtil.volumeToImperial(obj.snow) || 0;
obj.rain = UnitUtil.volumeToImperial(obj.rain/10) || 0;
obj.freezingLevel = UnitUtil.distanceToImperial(obj.freezingLevel);
obj.minTemp = UnitUtil.temperatureToImperial(obj.minTemp);
obj.maxTemp = UnitUtil.temperatureToImperial(obj.maxTemp);
}
return obj;
}
/*
* Applies the options to the class if there are any, otherwise defaults them.
* Currently only option is whether to display units in metric or imperial.
* opts
* A hash of options. Currently only 'inMetric'
*/
function pApplyOptions(opts){
if(!opts){
return;
}
unitsInMetric = opts.inMetric === undefined ? true : opts.inMetric;
}
/*
* PUBLIC
* Method used to set the parsing in motion.
* resortName: Name of resort
* elevation: The elevation we should use for the forecast
* cb: Callback to fire with a response.
* opts: Hash of available runtime option parameters
*/
snowRequest.parseResort = function(resortName, elevation, cb, opts){
if(arguments.length < 3){
return pBuildErrorJSON('Insufficient parameters', 'Please pass the resort, elevation, callback and if you wish the options object into the method', '');
}
pApplyOptions(opts);
var url = coreURL + resortName + '/6day/' + elevation; //Build the url
if(!Elevation.validate(elevation)){
cb(pBuildErrorJSON('Invalid Elevation',
'Elevation is incorrect, please use either: low, mid or top', url));
return;
}
//Validation passed, let's make the request son.
pMakeRequest(url, function($){
if($ instanceof Array){ //An error has occurred, feedback info to user.
cb(pBuildErrorJSON($[0], $[1], $[2]));
return;
}
var forecastOptObj = { resort: resortName, elevation: elevation, url: url};
//Find out if response is in metric or not.
forecastOptObj.isMetric = $('.deg-c input').attr('checked') === 'checked';
//Extrapolate time-relevant information needed to build forecast, and build object.
var issued = TimeUtil.fixIssueDateFormat($($('.location-issued__no-wrap')[5]).text() + $($('.location-issued__no-wrap')[6]).text());
forecastOptObj.issuedDate = issued;
let firstTime = $($('.forecast-table-time__period')[0]).text();
forecastOptObj.startDay = $($('.forecast-table-days__name')[0]).text();
forecastOptObj.lastUpdateDate = $($('.location-issued__no-wrap')[6]).text();
//if the first column starts from 'night', first column would not display day.
if(firstTime === 'night'){
forecastOptObj.startDay = TimeUtil.getPrevDay(forecastOptObj.startDay);
}
var match = issued.match(/^\d+/);
var time = [];
var timeIndex = issued.indexOf(match[0]) + match[0].length;
time.push(issued.substr(issued.indexOf(match[0]), match[0].length));
time.push(issued.substr(timeIndex, timeIndex+2));
time.push(issued.substr(timeIndex+3).split(/[\s]+/));
pBuildForecast($, forecastOptObj, function(obj){
if(!obj){
cb(pBuildErrorJSON("JSON Construction Error", "Internal error occurred, please try again", url));
return;
}
cb(obj);
});
});
};
return snowRequest;
};
module.exports = SnowRequest;