1 // ==========================================================================
  2 // Project:   The M-Project - Mobile HTML5 Application Framework
  3 // Copyright: (c) 2010 M-Way Solutions GmbH. All rights reserved.
  4 //            (c) 2011 panacoda GmbH. All rights reserved.
  5 // Creator:   Dominik
  6 // Date:      24.01.2011
  7 // License:   Dual licensed under the MIT or GPL Version 2 licenses.
  8 //            http://github.com/mwaylabs/The-M-Project/blob/master/MIT-LICENSE
  9 //            http://github.com/mwaylabs/The-M-Project/blob/master/GPL-LICENSE
 10 // ==========================================================================
 11 
 12 m_require('core/utility/location.js');
 13 
 14 /**
 15  * A constant value for permisson denied error.
 16  *
 17  * @type String
 18  */
 19 M.LOCATION_PERMISSION_DENIED = 'PERMISSION_DENIED';
 20 
 21 /**
 22  * A constant value for position unavailable error.
 23  *
 24  * @type String
 25  */
 26 M.LOCATION_POSITION_UNAVAILABLE = 'POSITION_UNAVAILABLE';
 27 
 28 /**
 29  * A constant value for timeout error.
 30  *
 31  * @type String
 32  */
 33 M.LOCATION_TIMEOUT = 'TIMEOUT';
 34 
 35 /**
 36  * A constant value for unknown error.
 37  *
 38  * @type String
 39  */
 40 M.LOCATION_UNKNOWN_ERROR = 'UNKNOWN_ERROR';
 41 
 42 /**
 43  * A constant value for not supported error.
 44  *
 45  * @type String
 46  */
 47 M.LOCATION_NOT_SUPPORTED = 'NOT_SUPPORTED';
 48 
 49 /**
 50  * A constant value for already receiving error.
 51  *
 52  * @type String
 53  */
 54 M.LOCATION_ALREADY_RECEIVING = 'ALREADY_RECEIVING';
 55 
 56 /**
 57  * A constant value for location type: approximate.
 58  *
 59  * @type Number
 60  */
 61 M.LOCATION_TYPE_APPROXIMATE = 0;
 62 
 63 /**
 64  * A constant value for location type: geometric center.
 65  *
 66  * @type Number
 67  */
 68 M.LOCATION_TYPE_GEOMETRIC_CENTER = 1;
 69 
 70 /**
 71  * A constant value for location type: range interpolated.
 72  *
 73  * @type Number
 74  */
 75 M.LOCATION_TYPE_RANGE_INTERPOLATED = 2;
 76 
 77 /**
 78  * A constant value for location type: rooftop.
 79  *
 80  * @type Number
 81  */
 82 M.LOCATION_TYPE_ROOFTOP = 3;
 83 
 84 /**
 85  * A constant value for connection error of the google geocoder.
 86  *
 87  * @type String
 88  */
 89 M.LOCATION_GEOCODER_ERROR = 'ERROR';
 90 
 91 /**
 92  * A constant value for an invalid request of the google geocoder.
 93  *
 94  * @type String
 95  */
 96 M.LOCATION_GEOCODER_INVALID_REQUEST = 'INVALID_REQUEST';
 97 
 98 /**
 99  * A constant value for an error due to too many requests to the google geocoder service.
100  *
101  * @type String
102  */
103 M.LOCATION_GEOCODER_OVER_QUERY_LIMIT = 'OVER_QUERY_LIMIT';
104 
105 /**
106  * A constant value for a denied request of the google geocoder.
107  *
108  * @type String
109  */
110 M.LOCATION_GEOCODER_REQUEST_DENIED = 'REQUEST_DENIED';
111 
112 /**
113  * A constant value for an unknown error of the google geocoder.
114  *
115  * @type String
116  */
117 M.LOCATION_GEOCODER_UNKNOWN_ERROR = 'UNKNOWN_ERROR';
118 
119 /**
120  * A constant value for no results of the google geocoder.
121  *
122  * @type String
123  */
124 M.LOCATION_GEOCODER_ZERO_RESULTS = 'ZERO_RESULTS';
125 
126 /**
127  * @class
128  *
129  * M.LocationManager defines a prototype for managing the user's respectively the
130  * device's location, based on the HTML 5 Geolocation API. The M.LocationManager
131  * provides machanism to retrieve, manage and update a location.
132   *
133  * @extends M.Object
134  */
135 M.LocationManager = M.Object.extend(
136 /** @scope M.LocationManager.prototype */ {
137 
138     /**
139      * The type of this object.
140      *
141      * @type String
142      */
143     type: 'M.LocationManager',
144 
145     /**
146      * This property contains the date, as an M.Date object, of the last geolocation
147      * call. It is needed internally to interpret the timeout.
148      *
149      * @type M.Date
150      */
151     lastLocationUpdate: null,
152 
153     /**
154      * This property contains a reference to google maps geocoder class. It is used
155      * in combination with the getLocationByAddress() method. 
156      *
157      * @type Object
158      */
159     geoCoder: null,
160 
161     /**
162      * This property specifies whether the M.LocationManager is currently trying to
163      * get a position or not.
164      *
165      * @type Boolean
166      */
167     isGettingLocation: NO,
168 
169     /**
170      * This method is used for retrieving the current location.
171      *
172      * The first two parameters define the success and error callbacks. They are
173      * called once the location was retrieved successfully (success callback) or
174      * if it failed (error callback).
175      *
176      * The success callback will be called with an M.Location object containing
177      * all the information about the location that was retrieved.
178      *
179      * The error callback will be called with one of the following constant
180      * string values:
181      *   - PERMISSION_DENIED
182      *   - POSITION_UNAVAILABLE
183      *   - TIMEOUT
184      *   - UNKNOWN_ERROR
185      *   - NOT_SUPPORTED
186      *
187      * The third parameter, options, can be used to define some parameters for
188      * retrieving the location. These are based on the HTML5 Geolocation API but
189      * extend it:
190      *
191      *   http://dev.w3.org/geo/api/spec-source.html#position_options_interface
192      *
193      * A valid options parameter could look like:
194      * 
195      *   {
196      *     enableHighAccuracy: YES,
197      *     maximumAge: 600000,
198      *     timeout: 0,
199      *     accuracy: 100
200      *   }
201      *
202      * If you do not specify any options, the following default values are taken:
203      *
204      *   enableHighAccuracy = NO --> turned off, due to better performance
205      *   maximumAge = 0 --> always retrieve a new location
206      *   timeout = 5000 --> 5 seconds until timeout error
207      *   accuracy = 50 --> 50 meters accuracy
208      *
209      * @param {Object} caller The object, calling this function. 
210      * @param {Object} onSuccess The success callback.
211      * @param {Object} onError The error callback.
212      * @param {Object} options The options for retrieving a location.
213      */
214     getLocation: function(caller, onSuccess, onError, options) {
215         if(this.isGettingLocation) {
216             M.Logger.log('M.LocationManager is currently already trying to retrieve a location.', M.WARN);
217             this.bindToCaller(caller, onError, M.LOCATION_ALREADY_RECEIVING)();
218         } else {
219             this.isGettingLocation = YES; 
220         }
221 
222         var that = this;
223 
224         this.lastLocationUpdate = M.Date.now();
225 
226         options = options ? options : {};
227         options.enableHighAccuracy = options.enableHighAccuracy ? options.enableHighAccuracy : NO;
228         options.maximumAge = options.maximumAge ? options.maximumAge : 0;
229         options.timeout = options.timeout ? options.timeout : 3000;
230 
231         if(navigator && navigator.geolocation) {
232             navigator.geolocation.getCurrentPosition(
233                 function(position) {
234                     that.isGettingLocation = NO;
235                     if(!options.accuracy || (options.accuracy && position.coords.accuracy <= options.accuracy)) {
236                         var location = M.Location.extend({
237                             latitude: position.coords.latitude,
238                             longitude: position.coords.longitude,
239                             accuracy: position.coords.accuracy,
240                             date: M.Date.now()
241                         });
242                         that.bindToCaller(caller, onSuccess, location)();
243                     } else {
244                         M.Logger.log('Location retrieved, but accuracy too low: ' + position.coords.accuracy, M.INFO);
245                         
246                         var now = M.Date.now();
247                         options.timeout = options.timeout - that.lastLocationUpdate.timeBetween(now);
248                         that.getLocation(caller, onSuccess, onError, options);
249                     }
250                 },
251                 function(error) {
252                     that.isGettingLocation = NO;
253                     switch (error.code) {
254                         case 1:
255                             that.bindToCaller(caller, onError, M.LOCATION_PERMISSION_DENIED)();
256                             break;
257                         case 2:
258                             that.bindToCaller(caller, onError, M.LOCATION_POSITION_UNAVAILABLE)();
259                             break;
260                         case 3:
261                             that.bindToCaller(caller, onError, M.LOCATION_TIMEOUT)();
262                             break;
263                         default:
264                             that.bindToCaller(caller, onError, M.LOCATION_UNKNOWN_ERROR)();
265                             break;
266                     }
267                 },
268                 options
269             );
270         } else {
271             that.bindToCaller(that, onError, M.LOCATION_NOT_SUPPORTED)();
272         }
273     },
274 
275     /**
276      * This method tries to transform a given address into an M.Location object.
277      * This method is based on the google maps api, respectively on its geocoder
278      * class.
279      *
280      * If a valid location could be found matching the given address parameter,
281      * the success callback is called with a valid M.Location object as its
282      * only parameter, containing the information about this location.
283      *
284      * If no location could be retrieved, the error callback is called, with the
285      * error message as its only parameter. Possible values for this error message
286      * are the following:
287      *
288      *   - M.LOCATION_GEOCODER_ERROR
289      *
290      *   - M.LOCATION_GEOCODER_INVALID_REQUEST
291      *
292      *   - M.LOCATION_GEOCODER_OVER_QUERY_LIMIT
293      *
294      *   - M.LOCATION_GEOCODER_REQUEST_DENIED
295      *
296      *   - M.LOCATION_GEOCODER_UNKNOWN_ERROR
297      *
298      *   - M.LOCATION_GEOCODER_ZERO_RESULTS
299      *
300      * @param {Object} caller The object, calling this function.
301      * @param {Function} onSuccess The method to be called after retrieving the location.
302      * @param {Function} onError The method to be called if retrieving the location went wrong.
303      * @param {String} address The address to be transformed into an M.Location object.
304      */
305     getLocationByAddress: function(caller, onSuccess, onError, address) {
306         if(address && typeof(address) === 'string') {
307             if(!this.geoCoder) {
308                 this.geoCoder = new google.maps.Geocoder();
309             }
310 
311             var that = this;
312 
313             this.geoCoder.geocode({
314                 address: address,
315                 language: M.I18N.getLanguage().substr(0, 2),
316                 region: M.I18N.getLanguage().substr(3, 2)
317             }, function(results, status) {
318                 if (status == google.maps.GeocoderStatus.OK) {
319                     var bestResult = null;
320                     _.each(results, function(result) {
321                         if(!bestResult || M['LOCATION_TYPE_' + bestResult.geometry.location_type] < M['LOCATION_TYPE_' + result.geometry.location_type]) {
322                             bestResult = result;
323                         }
324                     })
325                     if(bestResult) {
326                         that.bindToCaller(caller, onSuccess, M.Location.init(bestResult.geometry.location.lat(), bestResult.geometry.location.lng()))();
327                     }
328                 } else {
329                     switch (status) {
330                         case 'ERROR':
331                             that.bindToCaller(caller, onError, M.LOCATION_GEOCODER_ERROR)();
332                             break;
333                         case 'INVALID_REQUEST':
334                             that.bindToCaller(caller, onError, M.LOCATION_GEOCODER_INVALID_REQUEST)();
335                             break;
336                         case 'OVER_QUERY_LIMIT':
337                             that.bindToCaller(caller, onError, M.LOCATION_GEOCODER_OVER_QUERY_LIMIT)();
338                             break;
339                         case 'REQUEST_DENIED':
340                             that.bindToCaller(caller, onError, M.LOCATION_GEOCODER_REQUEST_DENIED)();
341                             break;
342                         case 'ZERO_RESULTS':
343                             that.bindToCaller(caller, onError, M.LOCATION_GEOCODER_ZERO_RESULTS)();
344                             break;
345                         default:
346                             that.bindToCaller(caller, onError, M.LOCATION_GEOCODER_UNKNOWN_ERROR)();
347                             break;
348                     }
349                 }
350             });
351         }
352     },
353 
354     /**
355      * This method tries to transform a given location as an M.Location object into
356      * a valid address. This method is based on the google maps api, respectively
357      * on its geocoder class.
358      *
359      * If a valid address could be found matching the given location parameter,
360      * the success callback is called with a valid address string as its only
361      * parameter.
362      *
363      * Note: If you set the getAddressAsComponents parameter to YES, the address
364      * will be passed to the success callback as an object containing the address'
365      * components. Use this option if you want to put the address together manually.
366      *
367      * If no address could be retrieved, the error callback is called, with the
368      * error message as its only parameter. Possible values for this error message
369      * are the following:
370      *
371      *   - M.LOCATION_GEOCODER_ERROR
372      *
373      *   - M.LOCATION_GEOCODER_INVALID_REQUEST
374      *
375      *   - M.LOCATION_GEOCODER_OVER_QUERY_LIMIT
376      *
377      *   - M.LOCATION_GEOCODER_REQUEST_DENIED
378      *
379      *   - M.LOCATION_GEOCODER_UNKNOWN_ERROR
380      *
381      *   - M.LOCATION_GEOCODER_ZERO_RESULTS
382      *
383      * @param {Object} caller The object, calling this function.
384      * @param {Function} onSuccess The method to be called after retrieving the address.
385      * @param {Function} onError The method to be called if retrieving the address went wrong.
386      * @param {M.Location} location The location to be transformed into an address.
387      * @param {Boolean} getAddressAsComponents Return the address as an object containing the components.
388      */
389     getAddressByLocation: function(caller, onSuccess, onError, location, getAddressAsComponents) {
390         if(location && typeof(location) === 'object' && location.type === 'M.Location') {
391             if(!this.geoCoder) {
392                 this.geoCoder = new google.maps.Geocoder();
393             }
394 
395             var that = this;
396 
397             this.geoCoder.geocode({
398                 location: new google.maps.LatLng(location.latitude, location.longitude),
399                 language: M.I18N.getLanguage().substr(0, 2),
400                 region: M.I18N.getLanguage().substr(3, 2)
401             }, function(results, status) {
402                 if (status == google.maps.GeocoderStatus.OK) {
403                     if(results[0] && getAddressAsComponents) {
404                         var components = {};
405                         _.each(results[0].address_components, function(component) {
406                             _.each(component.types, function(type) {
407                                 components[type] = component['long_name'] ? component['long_name'] : component['short_name']
408                             });
409                         });
410                         that.bindToCaller(caller, onSuccess, components)();
411                     } else if(results[0]) {
412                         that.bindToCaller(caller, onSuccess, results[0].formatted_address)();
413                     } else {
414                         that.bindToCaller(caller, onError, M.LOCATION_GEOCODER_ZERO_RESULTS)();
415                     }
416                 } else {
417                     switch (status) {
418                         case 'ERROR':
419                             that.bindToCaller(caller, onError, M.LOCATION_GEOCODER_ERROR)();
420                             break;
421                         case 'INVALID_REQUEST':
422                             that.bindToCaller(caller, onError, M.LOCATION_GEOCODER_INVALID_REQUEST)();
423                             break;
424                         case 'OVER_QUERY_LIMIT':
425                             that.bindToCaller(caller, onError, M.LOCATION_GEOCODER_OVER_QUERY_LIMIT)();
426                             break;
427                         case 'REQUEST_DENIED':
428                             that.bindToCaller(caller, onError, M.LOCATION_GEOCODER_REQUEST_DENIED)();
429                             break;
430                         case 'ZERO_RESULTS':
431                             that.bindToCaller(caller, onError, M.LOCATION_GEOCODER_ZERO_RESULTS)();
432                             break;
433                         default:
434                             that.bindToCaller(caller, onError, M.LOCATION_GEOCODER_UNKNOWN_ERROR)();
435                             break;
436                     }
437                 }
438             });
439         }
440     }
441 
442 });