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 });