Merge lp:~martin-borho/ubuntu-weather-app/reboot-data into lp:ubuntu-weather-app

Proposed by Martin Borho
Status: Merged
Approved by: Martin Borho
Approved revision: 7
Merged at revision: 4
Proposed branch: lp:~martin-borho/ubuntu-weather-app/reboot-data
Merge into: lp:ubuntu-weather-app
Diff against target: 992 lines (+927/-0)
7 files modified
CMakeLists.txt (+3/-0)
app/CMakeLists.txt (+1/-0)
app/data/CMakeLists.txt (+5/-0)
app/data/Storage.qml (+136/-0)
app/data/WeatherApi.js (+743/-0)
app/ubuntu-weather-app.qml (+38/-0)
key.js (+1/-0)
To merge this branch: bzr merge lp:~martin-borho/ubuntu-weather-app/reboot-data
Reviewer Review Type Date Requested Status
Ubuntu Phone Apps Jenkins Bot continuous-integration Approve
Victor Thompson Approve
Review via email: mp+247764@code.launchpad.net

Commit message

Added WeatherApi.js data provider client. Also added old Storage.qml to reuse data from the old weather app until storage is reimplemented.

Description of the change

Added WeatherApi.js for data provider access. Added also old Storage.qml with LocalStorage for using locations (and settings?) from the old weather app, until storage with u1db is reimplemented!

When the app is started, locations found in LocalStorage will get updated. Except they are not older than 30mins, than the data-client will return the data which was given in for the location (cached!).

Example calls from QML:

    function responseDataHandler(messageObject) {
        if(!messageObject.error) {
            if(messageObject.action === "updateData") {
                messageObject.result.forEach(function(loc) {
                    // replace location data in cache with refreshed values
                    if(loc["save"] === true) {
                        //storage.updateLocation(loc.db.id, loc);
                    }
                });
                print("\n\n##### location data result ####")
                print(JSON.stringify(messageObject.result));
            }
        } else {
            // handle errors
            console.log(messageObject.error.msg+" / "+messageObject.error.request.url)
        }
    }

    function demoLoadLoactionWeather() {
        var payload = {
            "action": "updateData", // action called
            "params": {
                "api_key": Key.twcKey,
                "force": true, // true/false - force api call, independant of caching state
                "locations": [ // list of locations
                    {
                        "location": { // location data as from search service
                            "objectName": "",
                            "adminName1": "Hamburg",
                            "adminName2": "",
                            "adminName3": "",
                            "areaLabel": "Hamburg, Germany",
                            "coord": {
                                "lat": "53.57532",
                                "lon": "10.01534"
                            },
                            "country": "DE",
                            "countryName": "Germany",
                            "name": "Hamburg",
                            "population": 1739117,
                            "services": {
                                "geonames": 2911298
                            },
                            "timezone": {
                                "dstOffset": 2,
                                "gmtOffset": 1,
                                "timeZoneId": "Europe/Berlin"
                            }
                        },
                        "updated": 1422196837780, // used for cache check, will reload if older than 30mins
                        "db": { // storage related data
                            "id": 4,
                            "updated": "2015-01-25T14:40:37.780Z"
                        }
                    }
                ],
                "service": "weatherchannel" // service to call
            }
        }
        // request weather data from the backend with callback to call with the results,
        // cache handling in backend!
        WeatherApi.sendRequest(payload, responseDataHandler)
    }

    function demoGeoLookup() {
        WeatherApi.sendRequest({"action":"getGeoIp","params":{}}, function(result) {
            print("\n\n#### location search by geoip ####")
            print(JSON.stringify(result))
        })

        var byPointPayload = {"action":"searchByPoint", "params": {"coords": {"lat":"53.5828","lon":"9.9808"}}}
        WeatherApi.sendRequest(byPointPayload, function(result) {
            print("\n\n#### location search by lat/long ####")
            print(JSON.stringify(result))
        })

        var byNamePayload = {"action":"searchByName","params":{"name":"London","units":"metric"}}
        WeatherApi.sendRequest(byNamePayload, function(result) {
            print("\n\n##### location search by name ####")
            print(JSON.stringify(result.result))
        })
    }

    Component.onCompleted: {
        demoGeoLookup()
        demoLoadLoactionWeather()

    }

To post a comment you must log in.
Revision history for this message
Victor Thompson (vthompson) wrote :

Please change "keys.js" to "key.js", or rename the file to match the CMakeLists.txt file

Install the project...
-- Install configuration: ""
-- Installing: /tmp/tmp.o7PpsVrGpn//manifest.json
-- Installing: /tmp/tmp.o7PpsVrGpn//ubuntu-weather-app.apparmor
-- Installing: /tmp/tmp.o7PpsVrGpn/share/applications/ubuntu-weather-app.desktop
CMake Error at cmake_install.cmake:72 (FILE):
  file INSTALL cannot find "/home/victor/Development/reboot-data/keys.js".

review: Needs Fixing
6. By Martin Borho

fixed typo

Revision history for this message
Victor Thompson (vthompson) wrote :

It might also be a good idea to rename the "KEYS_FILE" variable to be "KEY_FILE", assuming it won't need to hold multiple API keys.

review: Needs Information
7. By Martin Borho

renamed KEYS_FILE to KEY_FILE

Revision history for this message
Victor Thompson (vthompson) wrote :

LGTM!

review: Approve
Revision history for this message
Ubuntu Phone Apps Jenkins Bot (ubuntu-phone-apps-jenkins-bot) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
Ubuntu Phone Apps Jenkins Bot (ubuntu-phone-apps-jenkins-bot) :
review: Approve (continuous-integration)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'CMakeLists.txt'
2--- CMakeLists.txt 2015-01-23 23:15:52 +0000
3+++ CMakeLists.txt 2015-01-29 08:55:03 +0000
4@@ -35,6 +35,7 @@
5 set(DESKTOP_FILE "${APP_HARDCODE}.desktop")
6 set(ICON weather-app@30.png)
7 set(AUTOPILOT_DIR ubuntu_weather_app)
8+set(KEY_FILE key.js)
9
10 # Set install paths
11 if(CLICK_MODE)
12@@ -99,6 +100,8 @@
13 install(FILES ${CMAKE_CURRENT_BINARY_DIR}/${DESKTOP_FILE}
14 DESTINATION ${CMAKE_INSTALL_DATADIR}/applications)
15
16+install(FILES ${KEY_FILE} DESTINATION ${CMAKE_INSTALL_PREFIX})
17+
18 add_subdirectory(app)
19 add_subdirectory(backend)
20 add_subdirectory(po)
21
22=== modified file 'app/CMakeLists.txt'
23--- app/CMakeLists.txt 2015-01-23 23:15:52 +0000
24+++ app/CMakeLists.txt 2015-01-29 08:55:03 +0000
25@@ -12,4 +12,5 @@
26 install(FILES ${MAIN_QML} DESTINATION ${UBUNTU-WEATHER_APP_DIR})
27
28 add_subdirectory(components)
29+add_subdirectory(data)
30
31
32=== added directory 'app/data'
33=== added file 'app/data/CMakeLists.txt'
34--- app/data/CMakeLists.txt 1970-01-01 00:00:00 +0000
35+++ app/data/CMakeLists.txt 2015-01-29 08:55:03 +0000
36@@ -0,0 +1,5 @@
37+file(GLOB DATA_QML_JS_FILES *.qml *.js)
38+
39+add_custom_target(ubuntu-weather-app_data_QMlFiles ALL SOURCES ${DATA_QML_JS_FILES})
40+
41+install(FILES ${DATA_QML_JS_FILES} DESTINATION ${UBUNTU-WEATHER_APP_DIR}/data)
42
43=== added file 'app/data/Storage.qml'
44--- app/data/Storage.qml 1970-01-01 00:00:00 +0000
45+++ app/data/Storage.qml 2015-01-29 08:55:03 +0000
46@@ -0,0 +1,136 @@
47+/*
48+ * Copyright (C) 2013, 2014, 2015 Canonical Ltd
49+ *
50+ * This program is free software: you can redistribute it and/or modify
51+ * it under the terms of the GNU General Public License version 3 as
52+ * published by the Free Software Foundation.
53+ *
54+ * This program is distributed in the hope that it will be useful,
55+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
56+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
57+ * GNU General Public License for more details.
58+ *
59+ * You should have received a copy of the GNU General Public License
60+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
61+ *
62+ * Authored by: Martin Borho <martin@borho.net>
63+ */
64+import QtQuick.LocalStorage 2.0
65+import QtQuick 2.3
66+
67+Item {
68+ property var db: null
69+
70+ function openDB() {
71+ if(db !== null) return;
72+
73+ db = LocalStorage.openDatabaseSync("com.ubuntu.weather", "", "Default Ubuntu weather app", 100000);
74+
75+ if (db.version === "") {
76+ db.changeVersion("", "0.1",
77+ function(tx) {
78+ tx.executeSql('CREATE TABLE IF NOT EXISTS Locations(id INTEGER PRIMARY KEY AUTOINCREMENT, data TEXT, date TEXT)');
79+ console.log('Database created');
80+ });
81+ // reopen database with new version number
82+ db = LocalStorage.openDatabaseSync("com.ubuntu.weather", "", "Default Ubuntu weather app", 100000);
83+ }
84+
85+ if(db.version === "0.1") {
86+ db.changeVersion("0.1", "0.2",
87+ function(tx) {
88+ tx.executeSql('CREATE TABLE IF NOT EXISTS settings(key TEXT UNIQUE, value TEXT)');
89+ console.log('Settings table added, Database upgraded to v0.2');
90+ });
91+ // reopen database with new version number
92+ db = LocalStorage.openDatabaseSync("com.ubuntu.weather", "", "Default Ubuntu weather app", 100000);
93+ }
94+
95+ if(db.version === "0.2") {
96+ db.changeVersion("0.2", "0.3",
97+ function(tx) {
98+ tx.executeSql('DELETE FROM Locations WHERE 1');
99+ console.log('Removed old locations, Database upgraded to v0.3');
100+ });
101+ }
102+ }
103+
104+ function saveSetting(key, value) {
105+ openDB();
106+ db.transaction( function(tx){
107+ tx.executeSql('INSERT OR REPLACE INTO settings VALUES(?, ?)', [key, value]);
108+ });
109+ }
110+
111+ function getSettings(callback) {
112+ openDB();
113+ var settings = {};
114+ db.readTransaction(
115+ function(tx){
116+ var rs = tx.executeSql('SELECT key, value FROM Settings');
117+ for(var i = 0; i < rs.rows.length; i++) {
118+ var row = rs.rows.item(i);
119+ settings[row.key] = row.value;
120+ }
121+ callback(settings);
122+ }
123+ );
124+ }
125+
126+ function clearSetting(name) {
127+ openDB();
128+ db.transaction(function(tx){
129+ tx.executeSql('DELETE FROM Settings WHERE key = ?', [name]);
130+ });
131+ }
132+
133+ function insertLocation(data) {
134+ openDB();
135+ var res;
136+ db.transaction( function(tx){
137+ var r = tx.executeSql('INSERT INTO Locations(data, date) VALUES(?, ?)', [JSON.stringify(data), new Date().getTime()]);
138+ res = r.insertId;
139+ });
140+ return res;
141+ }
142+
143+ function updateLocation(dbId, data) {
144+ openDB();
145+ db.transaction( function(tx){
146+ var r = tx.executeSql('UPDATE Locations SET data = ?, date=? WHERE id = ?', [JSON.stringify(data), new Date().getTime(), dbId])
147+ });
148+ }
149+
150+ function getLocations(callback) {
151+ openDB();
152+ db.readTransaction(
153+ function(tx){
154+ var locations = [];
155+ var rs = tx.executeSql('SELECT * FROM Locations');
156+ for(var i = 0; i < rs.rows.length; i++) {
157+ var row = rs.rows.item(i),
158+ locData = JSON.parse(row.data);
159+ locData["updated"] = parseInt(row.date, 10);
160+ locData["db"] = {id: row.id, updated: new Date(parseInt(row.date, 10))};
161+ locations.push(locData);
162+ }
163+ callback(locations);
164+ }
165+ );
166+ }
167+
168+ function clearLocation(location_id) {
169+ openDB();
170+ db.transaction(function(tx){
171+ tx.executeSql('DELETE FROM Locations WHERE id = ?', [location_id]);
172+ });
173+ }
174+
175+ function clearDB() { // for dev purposes
176+ openDB();
177+ db.transaction(function(tx){
178+ tx.executeSql('DELETE FROM Locations WHERE 1');
179+ tx.executeSql('DELETE FROM Settings WHERE 1');
180+ });
181+ }
182+}
183
184=== added file 'app/data/WeatherApi.js'
185--- app/data/WeatherApi.js 1970-01-01 00:00:00 +0000
186+++ app/data/WeatherApi.js 2015-01-29 08:55:03 +0000
187@@ -0,0 +1,743 @@
188+.pragma library
189+/*
190+ * Copyright (C) 2013 Canonical Ltd
191+ *
192+ * This program is free software: you can redistribute it and/or modify
193+ * it under the terms of the GNU General Public License version 3 as
194+ * published by the Free Software Foundation.
195+ *
196+ * This program is distributed in the hope that it will be useful,
197+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
198+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
199+ * GNU General Public License for more details.
200+ *
201+ * You should have received a copy of the GNU General Public License
202+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
203+ *
204+ * Authored by: Raúl Yeguas <neokore@gmail.com>
205+ * Martin Borho <martin@borho.net>
206+ * Andrew Starr-Bochicchio <a.starr.b@gmail.com>
207+ */
208+
209+/**
210+* Version of the response data format.
211+* Increase this number to force a refresh.
212+*/
213+var RESPONSE_DATA_VERSION = 20140315;
214+
215+/**
216+* Helper functions
217+*/
218+function debug(obj) {
219+ print(JSON.stringify(obj))
220+}
221+//
222+function calcFahrenheit(celsius) {
223+ return celsius * 1.8 + 32;
224+}
225+//
226+function calcMph(ms) {
227+ return ms*2.24;
228+}
229+//
230+function calcInch(mm) {
231+ return mm/25.4;
232+}
233+//
234+function calcKmh(ms) {
235+ return ms*3.6;
236+}
237+//
238+function convertKmhToMph(kmh) {
239+ return kmh*0.621;
240+}
241+//
242+function calcWindDir(degrees) {
243+ var direction = "?";
244+ if(degrees >=0 && degrees <= 30){
245+ direction = "N";
246+ } else if(degrees >30 && degrees <= 60){
247+ direction = "NE";
248+ } else if(degrees >60 && degrees <= 120){
249+ direction = "E";
250+ } else if(degrees >120 && degrees <= 150){
251+ direction = "SE";
252+ } else if(degrees >150 && degrees <= 210){
253+ direction = "S";
254+ } else if(degrees >210 && degrees <= 240){
255+ direction = "SW";
256+ } else if(degrees >240 && degrees <= 300){
257+ direction = "W";
258+ } else if(degrees >300 && degrees <= 330){
259+ direction = "NW";
260+ } else if(degrees >330 && degrees <= 360){
261+ direction = "N";
262+ }
263+ return direction;
264+}
265+
266+//
267+function getLocationTime(tstamp) {
268+ var locTime = new Date(tstamp);
269+ return {
270+ year: locTime.getUTCFullYear(),
271+ month: locTime.getUTCMonth(),
272+ date: locTime.getUTCDate(),
273+ hours: locTime.getUTCHours(),
274+ minutes: locTime.getUTCMinutes()
275+ }
276+}
277+// Serialize a JavaScript object to URL parameters
278+// E.g. {param1: value1, param2: value2} to "param1=value&param2=value"
279+// TODO: it'd be nice to make it work with either passing a single object
280+// or several at once
281+function parameterize(obj) {
282+ var str = [];
283+ for(var param in obj) {
284+ str.push(encodeURIComponent(param) + "=" + encodeURIComponent(obj[param]));
285+ }
286+ return str.join("&");
287+}
288+
289+var GeoipApi = (function() {
290+ var _baseUrl = "http://geoip.ubuntu.com/lookup";
291+ return {
292+ getLatLong: function(params, apiCaller, onSuccess, onError) {
293+ var request = { type: "geolookup",url: _baseUrl},
294+ resultHandler = (function(request, xmlDoc) {
295+ var coords = {},
296+ childNodes = xmlDoc.childNodes;
297+ for(var i=0;i<childNodes.length;i++) {
298+ if(childNodes[i].nodeName === "Latitude") {
299+ coords.lat = childNodes[i].firstChild.nodeValue;
300+ } else if(childNodes[i].nodeName === "Longitude") {
301+ coords.lon = childNodes[i].firstChild.nodeValue;
302+ }
303+ }
304+ onSuccess(coords);
305+ }),
306+ retryHandler = (function(err) {
307+ console.log("geolookup retry of "+err.request.url);
308+ apiCaller(request, resultHandler, onError);
309+ });
310+ apiCaller(request, resultHandler, retryHandler);
311+ }
312+ }
313+})();
314+
315+var GeonamesApi = (function() {
316+ /**
317+ provides neccessary methods for requesting and preparing data from Geonames.org
318+ */
319+ var _baseUrl = "http://api.geonames.org/";
320+ var _username = "uweatherdev"
321+ var _addParams = "&maxRows=25&featureClass=P"
322+ //
323+ function _buildSearchResult(request, data) {
324+ var searchResult = { locations: [], request: request };
325+ if(data.geonames) {
326+ data.geonames.forEach(function(r) {
327+ searchResult.locations.push({
328+ name: r.name,
329+ coord: {lat: r.lat, lon: r.lng},
330+ country: r.countryCode,
331+ countryName: r.countryName,
332+ timezone: r.timezone,
333+ adminName1: r.adminName1,
334+ adminName2: r.adminName2,
335+ adminName3: r.adminName3,
336+ population: r.population,
337+ services: {
338+ "geonames": r.geonameId
339+ }
340+ });
341+ })
342+ }
343+ return searchResult;
344+ }
345+ //
346+ return {
347+ //
348+ search: function(mode, params, apiCaller, onSuccess, onError) {
349+ var request,
350+ retryHandler = (function(err) {
351+ console.log("search retry of "+err.request.url);
352+ apiCaller(request, searchResponseHandler, onError);
353+ }),
354+ searchResponseHandler = function(request, data) {
355+ onSuccess(_buildSearchResult(request, data));
356+ };
357+ if(mode === "point") {
358+ request = { type: "search",
359+ url: _baseUrl+ "findNearbyPlaceNameJSON?style=full&username="+encodeURIComponent(_username)
360+ +"&lat="+encodeURIComponent(params.coords.lat)+"&lng="+encodeURIComponent(params.coords.lon)
361+ +_addParams}
362+ } else {
363+ request = { type: "search",
364+ url: _baseUrl+ "searchJSON?style=full&username="+encodeURIComponent(_username)
365+ +"&name_startsWith="+encodeURIComponent(params.name)+_addParams}
366+ }
367+ apiCaller(request, searchResponseHandler, retryHandler);
368+ }
369+ }
370+
371+})();
372+
373+var OpenWeatherMapApi = (function() {
374+ /**
375+ provides neccessary methods for requesting and preparing data from OpenWeatherMap.org
376+ */
377+ var _baseUrl = "http://api.openweathermap.org/data/2.5/";
378+ //
379+ var _serviceName = "openweathermap";
380+ //
381+ var _icon_map = {
382+ "01d": "sun",
383+ "01n": "moon",
384+ "02d": "cloud_sun",
385+ "02n": "cloud_moon",
386+ "03d": "cloud_sun",
387+ "03n": "cloud_moon",
388+ "04d": "cloud",
389+ "04n": "cloud",
390+ "09d": "rain",
391+ "09n": "rain",
392+ "10d": "rain",
393+ "10n": "rain",
394+ "11d": "thunder",
395+ "11n": "thunder",
396+ "13d": "snow_shower",
397+ "13n": "snow_shower",
398+ "50d": "fog",
399+ "50n": "fog"
400+ }
401+ //
402+ function _buildDataPoint(date, data) {
403+ var result = {
404+ timestamp: data.dt,
405+ date: date,
406+ metric: {
407+ temp:data.main.temp,
408+ windSpeed: calcKmh(data.wind.speed),
409+ rain: data.main.rain || ((data.rain) ? data.rain["3h"] : false ) || 0,
410+ snow: data.main.snow || ((data.snow) ? data.snow["3h"] : false ) || 0
411+ },
412+ imperial: {
413+ temp: calcFahrenheit(data.main.temp),
414+ windSpeed: calcMph(data.wind.speed),
415+ rain: calcInch(data.main.rain || ((data.rain) ? data.rain["3h"] : false ) || 0),
416+ snow: calcInch(data.main.snow || ((data.snow) ? data.snow["3h"] : false ) ||0)
417+ },
418+ humidity: data.main.humidity,
419+ pressure: data.main.pressure,
420+ windDeg: data.wind.deg,
421+ windDir: calcWindDir(data.wind.deg),
422+ icon: _icon_map[data.weather[0].icon],
423+ condition: data.weather[0]
424+ };
425+ if(data.id !== undefined) {
426+ result["service"] = _serviceName;
427+ result["service_id"] = data.id;
428+ }
429+ return result;
430+ }
431+ //
432+ function _buildDayFormat(date, data) {
433+ var result = {
434+ date: date,
435+ timestamp: data.dt,
436+ metric: {
437+ tempMin: data.temp.min,
438+ tempMax: data.temp.max,
439+ windSpeed: calcKmh(data.speed),
440+ rain: data.rain || 0,
441+ snow: data.snow || 0
442+ },
443+ imperial: {
444+ tempMin: calcFahrenheit(data.temp.min),
445+ tempMax: calcFahrenheit(data.temp.max),
446+ windSpeed: calcMph(data.speed),
447+ rain: calcInch(data.rain || 0),
448+ snow: calcInch(data.snow || 0)
449+ },
450+ pressure: data.pressure,
451+ humidity: data.humidity,
452+ icon: _icon_map[data.weather[0].icon],
453+ condition: data.weather[0],
454+ windDeg: data.deg,
455+ windDir: calcWindDir(data.deg),
456+ hourly: []
457+ }
458+ return result;
459+ }
460+ //
461+ function formatResult(data, location) {
462+ var tmpResult = {},
463+ result = [],
464+ day=null,
465+ offset=(location.timezone && location.timezone.gmtOffset) ? location.timezone.gmtOffset*60*60*1000: 0,
466+ localNow = getLocationTime(new Date().getTime()+offset),
467+ todayDate;
468+ print("["+location.name+"] "+JSON.stringify(localNow))
469+ // add openweathermap id for faster responses
470+ if(location.services && !location.services[_serviceName] && data["current"].id) {
471+ location.services[_serviceName] = data["current"].id
472+ }
473+ //
474+ data["daily"]["list"].forEach(function(dayData) {
475+ var date = getLocationTime(((dayData.dt*1000)-1000)+offset), // minus 1 sec to handle +/-12 TZ
476+ day = date.year+"-"+date.month+"-"+date.date;
477+ if(!todayDate) {
478+ if(localNow.year+"-"+localNow.month+"-"+localNow.date > day) {
479+ // skip "yesterday"
480+ return;
481+ }
482+ todayDate = date;
483+ }
484+ tmpResult[day] = _buildDayFormat(date, dayData);
485+ })
486+ //
487+ var today = todayDate.year+"-"+todayDate.month+"-"+todayDate.date
488+ tmpResult[today]["current"] = _buildDataPoint(todayDate, data["current"]);
489+ if(data["forecast"] !== undefined) {
490+ data["forecast"]["list"].forEach(function(hourData) {
491+ var dateData = getLocationTime((hourData.dt*1000)+offset),
492+ day = dateData.year+"-"+dateData.month+"-"+dateData.date;
493+ if(tmpResult[day]) {
494+ tmpResult[day]["hourly"].push(_buildDataPoint(dateData, hourData));
495+ }
496+ })
497+ }
498+ //
499+ for(var d in tmpResult) {
500+ result.push(tmpResult[d]);
501+ }
502+ return result;
503+ }
504+ //
505+ function _getUrls(params) {
506+ var urls = {
507+ current: "",
508+ daily: "",
509+ forecast: ""
510+ },
511+ latLongParams = "&lat="+encodeURIComponent(params.location.coord.lat)
512+ + "&lon="+encodeURIComponent(params.location.coord.lon);
513+ if(params.location.services && params.location.services[_serviceName]) {
514+ urls.current = _baseUrl + "weather?units="+params.units+"&id="+params.location.services[_serviceName];
515+ urls.daily = _baseUrl + "forecast/daily?id="+params.location.services[_serviceName]+"&cnt=10&units="+params.units
516+ urls.forecast = _baseUrl + "forecast?id="+params.location.services[_serviceName]+"&units="+params.units
517+
518+ } else if (params.location.coord) {
519+ urls.current = _baseUrl + "weather?units="+params.units+latLongParams;
520+ urls.daily = _baseUrl+"forecast/daily?cnt=10&units="+params.units+latLongParams;
521+ urls.forecast = _baseUrl+"forecast?units="+params.units+latLongParams;
522+ }
523+ return urls;
524+ }
525+ //
526+ return {
527+ //
528+ getData: function(params, apiCaller, onSuccess, onError) {
529+ var urls = _getUrls(params),
530+ handlerMap = {
531+ current: { type: "current",url: urls.current},
532+ daily: { type: "daily",url: urls.daily},
533+ forecast: { type: "forecast", url: urls.forecast}},
534+ response = {
535+ location: params.location,
536+ db: (params.db) ? params.db : null,
537+ format: RESPONSE_DATA_VERSION
538+ },
539+ respData = {},
540+ addDataToResponse = (function(request, data) {
541+ var formattedResult;
542+ respData[request.type] = data;
543+ if(respData["current"] !== undefined
544+ && respData["forecast"] !== undefined
545+ && respData["daily"] !== undefined) {
546+ response["data"] = formatResult(respData, params.location)
547+ onSuccess(response);
548+ }
549+ }),
550+ onErrorHandler = (function(err) {
551+ onError(err);
552+ }),
553+ retryHandler = (function(err) {
554+ console.log("retry of "+err.request.url);
555+ var retryFunc = handlerMap[err.request.type];
556+ apiCaller(retryFunc, addDataToResponse, onErrorHandler);
557+ });
558+ //
559+ apiCaller(handlerMap.current, addDataToResponse, retryHandler);
560+ apiCaller(handlerMap.forecast, addDataToResponse, retryHandler);
561+ apiCaller(handlerMap.daily, addDataToResponse, retryHandler);
562+ }
563+ }
564+
565+})();
566+
567+var WeatherChannelApi = (function() {
568+ /**
569+ provides neccessary methods for requesting and preparing data from OpenWeatherMap.org
570+ */
571+ var _baseUrl = "http://wxdata.weather.com/wxdata/";
572+ //
573+ var _serviceName = "weatherchannel";
574+ //
575+ // see http://s.imwx.com/v.20131006.223722/img/wxicon/72/([0-9]+).png
576+ var _iconMap = {
577+ "0": "thunder", // ??
578+ "1": "thunder", // ??
579+ "2": "thunder", // ??
580+ "3": "thunder", // ??
581+ "4": "thunder", //T-Storms
582+ "5": "snow_rain", //Rain / Snow
583+ "6": "snow_rain", // ??
584+ "7": "snow_rain", //Wintry Mix
585+ "8": "scattered", //Freezing Drizzle
586+ "9": "scattered", //Drizzle
587+ "10": "rain", // ??
588+ "11": "rain", //Showers
589+ "12": "rain", //Rain
590+ "13": "snow_shower", // ??
591+ "14": "snow_shower", //Snow shower/Light snow
592+ "15": "snow_shower", //
593+ "16": "snow_shower", //Snow
594+ "17": "thunder", // Hail??
595+ "18": "snow_rain", // Rain / Snow ??
596+ "19": "fog", //Fog ??
597+ "20": "fog", //Fog
598+ "21": "fog", //Haze
599+ "22": "fog", // ??
600+ "23": "fog", // Wind ??
601+ "24": "overcast", //Partly Cloudy / Wind
602+ "25": "overcast", // ??
603+ "26": "overcast",//Cloudy
604+ "27": "cloud_moon",//Mostly Cloudy
605+ "28": "cloud_sun", //Mostly Cloudy
606+ "29": "cloud_moon", //Partly Cloudy
607+ "30": "cloud_sun", //Partly Cloudy
608+ "31": "moon", //Clear
609+ "32": "sun", //Sunny
610+ "33": "cloud_moon", //Mostly Clear
611+ "34": "cloud_sun", //Mostly Sunny
612+ "35": "snow_rain", // ??
613+ "36": "sun", //Sunny
614+ "37": "thunder", //Isolated T-Storms
615+ "38": "thunder", //Scattered T-Storms
616+ "39": "scattered", //Scattered Showers
617+ "40": "rain", // ??
618+ "41": "snow", //Scattered Snow Showers
619+ "42": "snow_shower", // ??
620+ "43": "snow_shower", // ??
621+ "44": "fog", // ??
622+ "45": "scattered", // ??
623+ "46": "snow_shower", //Snow Showers Early
624+ "47": "thunder" //Isolated T-Storms
625+ };
626+ //
627+ function _buildDataPoint(date, dataObj) {
628+ var data = dataObj["Observation"] || dataObj,
629+ result = {
630+ timestamp: data.date || data.dateTime,
631+ date: date,
632+ metric: {
633+ temp: data.temp,
634+ tempFeels: data.feelsLike,
635+ windSpeed: data.wSpeed
636+ },
637+ imperial: {
638+ temp: calcFahrenheit(data.temp),
639+ tempFeels: calcFahrenheit(data.feelsLike),
640+ windSpeed: convertKmhToMph(data.wSpeed)
641+ },
642+ precipType: (data.precip_type !== undefined) ? data.precip_type : null,
643+ propPrecip: (data.pop !== undefined) ? data.pop : null,
644+ humidity: data.humid,
645+ pressure: data.pressure,
646+ windDeg: data.wDir,
647+ windDir: data.wDirText,
648+ icon: _iconMap[(data.wxIcon||data.icon)],
649+ condition: data.text || data.wDesc,
650+ uv: data.uv
651+ };
652+ if(_iconMap[data.wxIcon||data.icon] === undefined) {
653+ print("ICON MISSING POINT: "+(data.wxIcon||data.icon)+" "+result.condition)
654+ }
655+ return result;
656+ }
657+ //
658+ function _buildDayFormat(date, data, now) {
659+ var partData = (now > data.validDate || data.day === undefined) ? data.night : data.day,
660+ result = {
661+ date: date,
662+ timestamp: data.validDate,
663+ metric: {
664+ tempMin: data.minTemp,
665+ tempMax: data.maxTemp,
666+ windSpeed: partData.wSpeed
667+ },
668+ imperial: {
669+ tempMin: calcFahrenheit(data.minTemp),
670+ tempMax: calcFahrenheit(data.maxTemp || data.minTemp),
671+ windSpeed: convertKmhToMph(partData.wSpeed)
672+ },
673+ precipType: partData.precip_type,
674+ propPrecip: partData.pop,
675+ pressure: null,
676+ humidity: partData.humid,
677+ icon: _iconMap[partData.icon],
678+ condition: partData.phrase,
679+ windDeg: partData.wDir,
680+ windDir: partData.wDirText,
681+ uv: partData.uv,
682+ hourly: []
683+ }
684+ if(_iconMap[partData.icon] === undefined) {
685+ print("ICON MISSING DAY: "+partData.icon+" "+result.condition)
686+ }
687+ return result;
688+ }
689+ //
690+ function formatResult(combinedData, location) {
691+ var tmpResult = {}, result = [],
692+ day=null, todayDate,
693+ offset=(location.timezone && location.timezone.gmtOffset) ? location.timezone.gmtOffset*60*60*1000: 0,
694+ now = new Date().getTime(),
695+ nowMs = parseInt(now/1000),
696+ localNow = getLocationTime(now+offset),
697+ data = {
698+ "location": combinedData[0]["Location"],
699+ "daily": combinedData[0]["DailyForecasts"],
700+ "forecast": combinedData[0]["HourlyForecasts"],
701+ "current": combinedData[0]["StandardObservation"],
702+ };
703+ print("["+location.name+"] "+JSON.stringify(localNow));
704+ // add openweathermap id for faster responses
705+ if(location.services && !location.services[_serviceName] && data["location"].key) {
706+ location.services[_serviceName] = data["location"].key
707+ }
708+ // only 5 days of forecast for TWC
709+ for(var x=0;x<5;x++) {
710+ var dayData = data["daily"][x],
711+ date = getLocationTime(((dayData.validDate*1000)-1000)+offset); // minus 1 sec to handle +/-12 TZ
712+ day = date.year+"-"+date.month+"-"+date.date;
713+ if(!todayDate) {
714+ if(localNow.year+"-"+localNow.month+"-"+localNow.date > day) {
715+ // skip "yesterday"
716+ continue;
717+ }
718+ todayDate = date;
719+ }
720+ tmpResult[day] = _buildDayFormat(date, dayData, nowMs);
721+ }
722+ //
723+ if(data["current"]) {
724+ var today = todayDate.year+"-"+todayDate.month+"-"+todayDate.date
725+ tmpResult[today]["current"] = _buildDataPoint(todayDate, data["current"]);
726+ }
727+ if(data["forecast"] !== undefined) {
728+ data["forecast"].forEach(function(hourData) {
729+ var dateData = getLocationTime((hourData.dateTime*1000)+offset),
730+ day = dateData.year+"-"+dateData.month+"-"+dateData.date;
731+ if(tmpResult[day]) {
732+ tmpResult[day]["hourly"].push(_buildDataPoint(dateData, hourData));
733+ }
734+ })
735+ }
736+ //
737+ for(var d in tmpResult) {
738+ result.push(tmpResult[d]);
739+ }
740+ return result;
741+ }
742+ //
743+ function _getUrl(params) {
744+ var url, serviceId,
745+ baseParams = {
746+ key: params.api_key,
747+ units: (params.units === "metric") ? "m" : "e",
748+ locale: Qt.locale().name,
749+ hours: "48",
750+ },
751+ commands = {
752+ "mobileaggregation": "mobile/mobagg/",
753+ };
754+ if(params.location.services && params.location.services[_serviceName]) {
755+ serviceId = encodeURIComponent(params.location.services[_serviceName]);
756+ url = _baseUrl+commands["mobileaggregation"]+serviceId+".js?"+parameterize(baseParams);
757+ } else if (params.location.coord) {
758+ var coord = {lat: params.location.coord.lat, lng: params.location.coord.lon};
759+ url = _baseUrl+commands["mobileaggregation"]+"get.js?"+parameterize(baseParams)+"&"+
760+ parameterize(coord);
761+ }
762+ return url;
763+ }
764+ //
765+ return {
766+ getData: function(params, apiCaller, onSuccess, onError) {
767+ var url = _getUrl(params),
768+ handlerMap = {
769+ all: { type: "all", url: url}
770+ },
771+ response = {
772+ location: params.location,
773+ db: (params.db) ? params.db : null,
774+ format: RESPONSE_DATA_VERSION
775+ },
776+ addDataToResponse = (function(request, data) {
777+ var formattedResult;
778+ response["data"] = formatResult(data, params.location);
779+ onSuccess(response);
780+ }),
781+ onErrorHandler = (function(err) {
782+ onError(err);
783+ });
784+ apiCaller(handlerMap.all, addDataToResponse, onErrorHandler);
785+ }
786+ }
787+})();
788+
789+var WeatherApi = (function(_services) {
790+ /**
791+ proxy for requesting weather apis, the passed _services are providing the respective api endpoints
792+ and formatters to build a uniform response object
793+ */
794+ function _getService(name) {
795+ if(_services[name] !== undefined) {
796+ return _services[name];
797+ }
798+ return _services["weatherchannel"];
799+ }
800+ //
801+ function _sendRequest(request, onSuccess, onError) {
802+ var xmlHttp = new XMLHttpRequest();
803+ if (xmlHttp) {
804+ console.log("Sent request URL: " + request.url);
805+ xmlHttp.open('GET', request.url, true);
806+ xmlHttp.onreadystatechange = function () {
807+ try {
808+ if (xmlHttp.readyState == 4) {
809+ if(xmlHttp.status === 200) {
810+ if(xmlHttp.responseXML) {
811+ onSuccess(request, xmlHttp.responseXML.documentElement);
812+ } else {
813+ var json = JSON.parse(xmlHttp.responseText);
814+ onSuccess(request,json);
815+ }
816+ } else {
817+ onError({
818+ msg: "wrong response http code, got "+xmlHttp.status,
819+ request: request
820+ });
821+ }
822+ }
823+ } catch (e) {
824+ print("Exception: "+e)
825+ onError({msg: "wrong response data format", request: request});
826+ }
827+ };
828+ xmlHttp.send(null);
829+ }
830+ }
831+ //
832+ return {
833+ //
834+ geoLookup: function(params, onSuccess, onError) {
835+ var service = _getService('geoip'),
836+ geoNameService = _getService('geonames'),
837+ lookupHandler = function(data) {
838+ print("Geolookup: "+JSON.stringify(data))
839+ geoNameService.search("point", {coords:data}, _sendRequest, onSuccess, onError);
840+ };
841+ service.getLatLong(params, _sendRequest, lookupHandler, onError)
842+ },
843+ //
844+ search: function(mode, params, onSuccess, onError) {
845+ var service = _getService('geonames');
846+ service.search(mode, params, _sendRequest, onSuccess, onError);
847+ },
848+ //
849+ getLocationData: function(params, onSuccess, onError) {
850+ var service = _getService(params.service);
851+ service.getData(params, _sendRequest, onSuccess, onError);
852+ },
853+ }
854+})({
855+ "openweathermap": OpenWeatherMapApi,
856+ "weatherchannel": WeatherChannelApi,
857+ "geonames": GeonamesApi,
858+ "geoip": GeoipApi
859+});
860+
861+var sendRequest = function(message, responseCallback) {
862+ // handles the response data
863+ var finished = function(result) {
864+ // print result to get data for test json files
865+ // print(JSON.stringify(result));
866+ //WorkerScript.sendMessage({
867+ responseCallback({
868+ action: message.action,
869+ result: result
870+ })
871+ }
872+ // handles errors
873+ var onError = function(err) {
874+ console.log(JSON.stringify(err, null, true));
875+ //WorkerScript.sendMessage({ 'error': err})
876+ responseCallback({ 'error': err})
877+ }
878+ // keep order of locations, sort results
879+ var sortDataResults = function(locA, locB) {
880+ return locA.db.id - locB.db.id;
881+ }
882+ // perform the api calls
883+ if(message.action === "searchByName") {
884+ WeatherApi.search("name", message.params, finished, onError);
885+ } else if(message.action === "searchByPoint") {
886+ WeatherApi.search("point", message.params, finished, onError);
887+ } else if(message.action === "getGeoIp") {
888+ WeatherApi.geoLookup(message.params, finished, onError);
889+ } else if(message.action === "updateData") {
890+ var locLength = message.params.locations.length,
891+ locUpdated = 0,
892+ result = [],
893+ now = new Date().getTime();
894+ if(locLength > 0) {
895+ message.params.locations.forEach(function(loc) {
896+ var updatedHnd = function (newData, cached) {
897+ locUpdated += 1;
898+ if(cached === true) {
899+ newData["save"] = false;
900+ } else {
901+ newData["save"] = true;
902+ newData["updated"] = new Date().getTime();
903+ }
904+ result.push(newData);
905+ if(locUpdated === locLength) {
906+ result.sort(sortDataResults);
907+ finished(result);
908+ }
909+ },
910+ params = {
911+ location:loc.location,
912+ db: loc.db,
913+ units: 'metric',
914+ service: message.params.service,
915+ api_key: message.params.api_key
916+ },
917+ secsFromLastFetch = (now-loc.updated)/1000;
918+ if( message.params.force===true || loc.format !== RESPONSE_DATA_VERSION || secsFromLastFetch > 1800){
919+ // data older than 30min, location is new or data format is deprecated
920+ WeatherApi.getLocationData(params, updatedHnd, onError);
921+ } else {
922+ console.log("["+loc.location.name+"] returning cached data, time from last fetch: "+secsFromLastFetch)
923+ updatedHnd(loc, true);
924+ }
925+ })
926+ } else {
927+ finished(result);
928+ }
929+ }
930+}
931
932=== modified file 'app/ubuntu-weather-app.qml'
933--- app/ubuntu-weather-app.qml 2015-01-24 00:41:10 +0000
934+++ app/ubuntu-weather-app.qml 2015-01-29 08:55:03 +0000
935@@ -18,6 +18,9 @@
936
937 import QtQuick 2.3
938 import Ubuntu.Components 1.1
939+import "data" as Data
940+import "data/WeatherApi.js" as WeatherApi
941+import "../key.js" as Key
942
943 MainView {
944 id: weatherApp
945@@ -36,6 +39,41 @@
946 useDeprecatedToolbar: false
947 anchorToKeyboard: true
948
949+ Component.onCompleted: {
950+ storage.getLocations(function(locations) {
951+ WeatherApi.sendRequest({
952+ action: "updateData",
953+ params: {
954+ locations:locations,
955+ force:false,
956+ service: "weatherchannel",
957+ api_key: Key.twcKey
958+ }
959+ }, responseDataHandler)
960+ })
961+ }
962+
963+ function responseDataHandler(messageObject) {
964+ if(!messageObject.error) {
965+ if(messageObject.action === "updateData") {
966+ messageObject.result.forEach(function(loc) {
967+ // replace location data in cache with refreshed values
968+ if(loc["save"] === true) {
969+ storage.updateLocation(loc.db.id, loc);
970+ }
971+ });
972+ //print(JSON.stringify(messageObject.result));
973+ //buildTabs(messageObject.result);
974+ }
975+ } else {
976+ console.log(messageObject.error.msg+" / "+messageObject.error.request.url)
977+ // TODO error handling
978+ }
979+ }
980+
981+ Data.Storage {
982+ id: storage
983+ }
984
985 Page {
986 title: "Weather Reboot"
987
988=== added file 'key.js'
989--- key.js 1970-01-01 00:00:00 +0000
990+++ key.js 2015-01-29 08:55:03 +0000
991@@ -0,0 +1,1 @@
992+var twcKey = "";

Subscribers

People subscribed via source and target branches

to all changes: