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: 26.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 /** 13 * A constant value for map type: roadmap 14 * 15 * @type String 16 */ 17 M.MAP_ROADMAP = 'ROADMAP'; 18 19 /** 20 * A constant value for map type: hybrid 21 * 22 * @type String 23 */ 24 M.MAP_HYBRID = 'HYBRID'; 25 26 /** 27 * A constant value for map type: satellite 28 * 29 * @type String 30 */ 31 M.MAP_SATELLITE = 'SATELLITE'; 32 33 /** 34 * A constant value for map type: terrain 35 * 36 * @type String 37 */ 38 M.MAP_TERRAIN = 'TERRAIN'; 39 40 /** 41 * A global reference to the first instances of M.MapView. We use this to have a accessible hook 42 * to the map we can pass to google as a callback object. 43 * 44 * @type Object 45 */ 46 M.INITIAL_MAP = null; 47 48 /** 49 * @class 50 * 51 * M.MapView is the prototype of a map view. It defines a set of methods for 52 * displaying a map, setting markers and showing the current location. This 53 * map view is based on google maps, but other implementations are possible. 54 * 55 * @extends M.View 56 */ 57 M.MapView = M.View.extend( 58 /** @scope M.MapView.prototype */ { 59 60 /** 61 * The type of this object. 62 * 63 * @type String 64 */ 65 type: 'M.MapView', 66 67 /** 68 * This property is used to save a reference to the actual google map. It 69 * is set automatically when the map is firstly initialized. 70 * 71 * @type Object 72 */ 73 map: null, 74 75 /** 76 * This property is used to store the map's M.MapMarkerViews. If a marker 77 * is set within the init() method or by calling the addMarker() method, 78 * it is automatically pushed into this array. 79 * 80 * @type Object 81 */ 82 markers: null, 83 84 /** 85 * Determines whether to display the map view 'inset' or at full width. 86 * 87 * @type Boolean 88 */ 89 isInset: YES, 90 91 /** 92 * This property specifies the zoom level for this map view. It is directly 93 * mapped to the zoom property of a google map view. For further information 94 * see the google maps API specification: 95 * 96 * http://code.google.com/intl/de-DE/apis/maps/documentation/javascript/reference.html#MapOptions 97 * 98 * @type Number 99 */ 100 zoomLevel: 15, 101 102 /** 103 * This property specifies the map type for this map view. It is directly 104 * mapped to the 'mapTypeId' property of a google map view. Possible values 105 * for this property are: 106 * 107 * - M.MAP_ROADMAP --> This map type displays a normal street map. 108 * - M.MAP_HYBRID --> This map type displays a transparent layer of major streets on satellite images. 109 * - M.MAP_SATELLITE --> This map type displays satellite images. 110 * - M.MAP_TERRAIN --> This map type displays maps with physical features such as terrain and vegetation. 111 * 112 * For further information see the google maps API specification: 113 * 114 * http://code.google.com/intl/en-US/apis/maps/documentation/javascript/reference.html#MapOptions 115 * 116 * @type String 117 */ 118 mapType: M.MAP_ROADMAP, 119 120 /** 121 * This property specifies whether or not to display the map type controls 122 * inside of this map view. For further information see the google maps API 123 * specification: 124 * 125 * http://code.google.com/intl/en-US/apis/maps/documentation/javascript/reference.html#MapOptions 126 * 127 * @type Boolean 128 */ 129 showMapTypeControl: NO, 130 131 /** 132 * This property specifies whether or not to display the navigation controls 133 * inside of this map view. For further information see the google maps API 134 * specification: 135 * 136 * http://code.google.com/intl/en-US/apis/maps/documentation/javascript/reference.html#MapOptions 137 * 138 * @type Boolean 139 */ 140 showNavigationControl: NO, 141 142 /** 143 * This property specifies whether or not to display the street view controls 144 * inside of this map view. For further information see the google maps API 145 * specification: 146 * 147 * http://code.google.com/intl/en-US/apis/maps/documentation/javascript/reference.html#MapOptions 148 * 149 * @type Boolean 150 */ 151 showStreetViewControl: NO, 152 153 /** 154 * This property specifies whether the map is draggable or not. If set to NO, 155 * a user won't be able to move the map, respectively the visible sector. For 156 * further information see the google maps API specification: 157 * 158 * http://code.google.com/intl/en-US/apis/maps/documentation/javascript/reference.html#MapOptions 159 * 160 * @type Boolean 161 */ 162 isDraggable: YES, 163 164 /** 165 * This property specifies the initial location for this map view, as an M.Location 166 * object. Its latitude and longitude properties are directly mapped to the center 167 * property of a google map view. For further information see the google maps API 168 * specification: 169 * 170 * http://code.google.com/intl/en-US/apis/maps/documentation/javascript/reference.html#MapOptions 171 * 172 * @type M.Location 173 */ 174 175 initialLocation: M.Location.extend({ 176 latitude: 48.813338, 177 longitude: 9.178463 178 }), 179 180 /** 181 * This property determines whether or not to show a marker at the map view's 182 * initial location. This location can be specified by the initialLocation 183 * property of this map view. 184 * 185 * @type Boolean 186 */ 187 setMarkerAtInitialLocation: NO, 188 189 /** 190 * This property can be used to specify the animation type for this map view's 191 * markers. The following three values are possible: 192 * 193 * M.MAP_MARKER_ANIMATION_NONE --> no animation 194 * M.MAP_MARKER_ANIMATION_DROP --> the marker drops onto the map 195 * M.MAP_MARKER_ANIMATION_BOUNCE --> the marker constantly bounces 196 * 197 * @type String 198 */ 199 markerAnimationType: M.MAP_MARKER_ANIMATION_NONE, 200 201 /** 202 * This property spacifies whether or not this map has already been initialized. 203 * 204 * @type Boolean 205 */ 206 isInitialized: NO, 207 208 /** 209 * This property specifies whether or not to remove all existing markers on a 210 * map update. A map update can either be an automatic update due to content 211 * binding or a implicit call of the map view's updateMap() method. 212 * 213 * @type Boolean 214 */ 215 removeMarkersOnUpdate: YES, 216 217 /** 218 * If set, contains the map view's callback in sub a object named 'error', 219 * which will be called if no connection is available and the map service 220 * (google maps api) can not be loaded. 221 * 222 * @type Object 223 */ 224 callbacks: null, 225 226 /** 227 * This flag can be used to specify whether or not to load the google places 228 * library. By default this property is set to YES. If you do not need the 229 * library, you should set this to NO in order to save some bandwidth. 230 * 231 * @type Boolean 232 */ 233 loadPlacesLibrary: YES, 234 235 /** 236 * This property specifies the recommended events for this type of view. 237 * 238 * @type Array 239 */ 240 recommendedEvents: ['click', 'tap'], 241 242 /** 243 * Renders a map view, respectively a map view container. 244 * 245 * @private 246 * @returns {String} The map view's html representation. 247 */ 248 render: function() { 249 this.html += '<div data-fullscreen="true" id="' + this.id + '"'; 250 this.html += !this.isInset ? ' class="ui-listview"' : ''; 251 this.html += '><div id="' + this.id + '_map"' + this.style() + '></div></div>'; 252 253 return this.html; 254 }, 255 256 /** 257 * This method is called if the bound content changed. This content must be 258 * an array of M.Location objects or M.MapMarkerView objects. This method 259 * will take care of a re-rendering of the map view and all of its bound 260 * markers. 261 * 262 * If M.Location objects are passed, the default settings for map markers 263 * of this map view are assigned. 264 * 265 * Note that you can not use individual click events for your markers if 266 * you pass M.Location objects. 267 */ 268 renderUpdate: function() { 269 /* check if content binding is valid */ 270 var content = null; 271 if(!(this.contentBinding && this.contentBinding.target && typeof(this.contentBinding.target) === 'object' && this.contentBinding.property && typeof(this.contentBinding.property) === 'string' && this.contentBinding.target[this.contentBinding.property])) { 272 M.Logger.log('No valid content binding specified for M.MapView (' + this.id + ')!', M.WARN); 273 return; 274 } 275 276 /* get the marker / location objects from content binding */ 277 var content = this.contentBinding.target[this.contentBinding.property]; 278 var markers = []; 279 280 /* save a reference to the map */ 281 var that = this; 282 283 /* if we got locations, transform to markers */ 284 if(content && content[0] && content[0].type === 'M.Location') { 285 _.each(content, function(location) { 286 if(location && typeof(location) === 'object' && location.type === 'M.Location') { 287 markers.push(M.MapMarkerView.init({ 288 location: location, 289 map: that 290 })); 291 } 292 }); 293 /* otherwise check and filter for map markers */ 294 } else if(content && content[0] && content[0].type === 'M.MapMarkerView') { 295 markers = _.select(content, function(marker) { 296 return (marker && marker.type === 'M.MapMarkerView') 297 }) 298 } 299 300 /* remove current markers */ 301 if(this.removeMarkersOnUpdate) { 302 this.removeAllMarkers(); 303 } 304 305 /* add all new markers */ 306 _.each(markers, function(marker) { 307 that.addMarker(marker); 308 }) 309 }, 310 311 /** 312 * This method is responsible for registering events for view elements and its child views. It 313 * basically passes the view's event-property to M.EventDispatcher to bind the appropriate 314 * events. 315 * 316 * We use this to disable event registration for M.MapView, since we only use the 'events' property 317 * for determining the handler for possible map markers of this map. 318 */ 319 registerEvents: function() { 320 321 }, 322 323 /** 324 * Applies some style-attributes to the map view. 325 * 326 * @private 327 * @returns {String} The maps view's styling as html representation. 328 */ 329 style: function() { 330 var html = ''; 331 if(this.cssClass) { 332 html += ' class="' + this.cssClass + '"'; 333 } 334 return html; 335 }, 336 337 /** 338 * This method is used to initialize a map view, typically out of a controller. 339 * With its options parameter you can set or update almost every parameter of 340 * a map view. This allows you to define a map view within your view, but then 341 * update its parameters later when you want this view to display a map. 342 * 343 * The options parameter must be passed as a simple object, containing all of 344 * the M.MapView's properties you want to be updated. Such an options object 345 * could look like the following: 346 * 347 * { 348 * zoomLevel: 12, 349 * mapType: M.MAP_HYBRID, 350 * initialLocation: location 351 * } 352 * 353 * While all properties of the options parameter can be given as Number, String 354 * or a constant value, the location must be a valid M.Location object. 355 * 356 * Once the google api is initialized, the success callback specified with the 357 * options parameter is called. If an error occurs (e.g. no network connection), 358 * the error callback is called instead. They can be specified like the 359 * following: 360 * 361 * { 362 * callbacks: { 363 * success: { 364 * target: this, 365 * action: function() { 366 * // success callback 367 * } 368 * }, 369 * error: { 370 * target: this, 371 * action: function() { 372 * // error callback 373 * } 374 * } 375 * } 376 * } 377 * 378 * @param {Object} options The options for the map view. 379 * @param {Boolean} isUpdate Indicates whether this is an update call or not. 380 */ 381 initMap: function(options, isUpdate) { 382 if(!this.isInitialized || isUpdate) { 383 if(!isUpdate) { 384 this.markers = []; 385 } 386 387 if(typeof(google) === 'undefined') { 388 /* store the passed params and this map globally for further use */ 389 M.INITIAL_MAP = { 390 map: this, 391 options: options, 392 isUpdate: isUpdate 393 }; 394 395 /* check the connection status */ 396 M.Environment.getConnectionStatus({ 397 target: this, 398 action: 'didRetrieveConnectionStatus' 399 }); 400 } else { 401 this.googleDidLoad(options, isUpdate, true); 402 } 403 } else { 404 M.Logger.log('The M.MapView has already been initialized', M.WARN); 405 } 406 }, 407 408 /** 409 * This method is used internally to retrieve the connection status. If there is a connection 410 * available, we will include the google maps api. 411 * 412 * @private 413 */ 414 didRetrieveConnectionStatus: function(connectionStatus) { 415 if(connectionStatus === M.ONLINE) { 416 $.getScript( 417 'http://maps.google.com/maps/api/js?' + (this.loadPlacesLibrary ? 'libraries=places&' : '') + 'sensor=true&callback=M.INITIAL_MAP.map.googleDidLoad' 418 ); 419 } else { 420 var callback = M.INITIAL_MAP.options ? M.INITIAL_MAP.options.callbacks : null; 421 if(callback && M.EventDispatcher.checkHandler(callback.error)){ 422 this.bindToCaller(callback.error.target, callback.error.action)(); 423 } 424 } 425 }, 426 427 /** 428 * This method is used internally to initialite the map if the google api hasn't been loaded 429 * before. If so, we use this method as callback for google. 430 * 431 * @private 432 */ 433 googleDidLoad: function(options, isUpdate, isInternalCall) { 434 if(!isInternalCall) { 435 options = M.INITIAL_MAP.options; 436 isUpdate = M.INITIAL_MAP.isUpdate; 437 } 438 439 for(var i in options) { 440 switch (i) { 441 case 'zoomLevel': 442 this[i] = (typeof(options[i]) === 'number' && options[i] > 0) ? (options[i] > 22 ? 22 : options[i]) : this[i]; 443 break; 444 case 'mapType': 445 this[i] = (options[i] === M.MAP_ROADMAP || options[i] === M.MAP_HYBRID || options[i] === M.MAP_SATELLITE || options[i] === M.MAP_TERRAIN) ? options[i] : this[i]; 446 break; 447 case 'markerAnimationType': 448 this[i] = (options[i] === M.MAP_MARKER_ANIMATION_BOUNCE || options[i] === M.MAP_MARKER_ANIMATION_DROP) ? options[i] : this[i]; 449 break; 450 case 'showMapTypeControl': 451 case 'showNavigationControl': 452 case 'showStreetViewControl': 453 case 'isDraggable': 454 case 'setMarkerAtInitialLocation': 455 case 'removeMarkersOnUpdate': 456 this[i] = typeof(options[i]) === 'boolean' ? options[i] : this[i]; 457 break; 458 case 'initialLocation': 459 this[i] = (typeof(options[i]) === 'object' && options[i].type === 'M.Location') ? options[i] : this[i]; 460 break; 461 case 'callbacks': 462 this[i] = (typeof(options[i]) === 'object') ? options[i] : this[i]; 463 break; 464 default: 465 break; 466 } 467 }; 468 if(isUpdate) { 469 if(this.removeMarkersOnUpdate) { 470 this.removeAllMarkers(); 471 } 472 this.map.setOptions({ 473 zoom: this.zoomLevel, 474 center: new google.maps.LatLng(this.initialLocation.latitude, this.initialLocation.longitude), 475 mapTypeId: google.maps.MapTypeId[this.mapType], 476 mapTypeControl: this.showMapTypeControl, 477 navigationControl: this.showNavigationControl, 478 streetViewControl: this.showStreetViewControl, 479 draggable: this.isDraggable 480 }); 481 } else { 482 this.map = new google.maps.Map($('#' + this.id + '_map')[0], { 483 zoom: this.zoomLevel, 484 center: new google.maps.LatLng(this.initialLocation.latitude, this.initialLocation.longitude), 485 mapTypeId: google.maps.MapTypeId[this.mapType], 486 mapTypeControl: this.showMapTypeControl, 487 navigationControl: this.showNavigationControl, 488 streetViewControl: this.showStreetViewControl, 489 draggable: this.isDraggable 490 }); 491 } 492 493 if(this.setMarkerAtInitialLocation) { 494 var that = this; 495 this.addMarker(M.MapMarkerView.init({ 496 location: this.initialLocation, 497 map: that.map 498 })); 499 } 500 501 this.isInitialized = YES; 502 503 /* now call callback of "the outside world" */ 504 if(!isUpdate && this.callbacks.success && M.EventDispatcher.checkHandler(this.callbacks.success)) { 505 this.bindToCaller(this.callbacks.success.target, this.callbacks.success.action)(); 506 } 507 }, 508 509 /** 510 * This method is used to update a map view, typically out of a controller. 511 * With its options parameter you can update or update almost every parameter 512 * of a map view. This allows you to define a map view within your view, but 513 * then update its parameters later when you want this view to display a map 514 * and to update those options over and over again for this map. 515 * 516 * The options parameter must be passed as a simple object, containing all of 517 * the M.MapView's properties you want to be updated. Such an options object 518 * could look like the following: 519 * 520 * { 521 * zoomLevel: 12, 522 * mapType: M.MAP_HYBRID, 523 * initialLocation: location 524 * } 525 * 526 * While all properties of the options parameter can be given as Number, String 527 * or a constant value, the location must be a valid M.Location object. 528 * 529 * @param {Object} options The options for the map view. 530 */ 531 updateMap: function(options) { 532 this.initMap(options, YES); 533 }, 534 535 /** 536 * This method can be used to add a marker to the map view. Simply pass a 537 * valid M.MapMarkerView object and a map marker is created automatically, 538 * displayed on the map and added to this map view's markers property. 539 * 540 * @param {M.MapMarkerView} marker The marker to be added. 541 */ 542 addMarker: function(marker) { 543 if(marker && typeof(marker) === 'object' && marker.type === 'M.MapMarkerView') { 544 var that = this; 545 marker.marker = new google.maps.Marker({ 546 map: that.map, 547 draggable: NO, 548 animation: google.maps.Animation[marker.markerAnimationType ? marker.markerAnimationType : that.markerAnimationType], 549 position: new google.maps.LatLng(marker.location.latitude, marker.location.longitude), 550 icon: marker.icon 551 }); 552 marker.registerEvents(); 553 this.markers.push( 554 marker 555 ); 556 } else { 557 M.Logger.log('No valid M.MapMarkerView passed for addMarker().', M.WARN); 558 } 559 }, 560 561 /** 562 * This method can be used to remove a certain marker from the map view. In 563 * order to do this, you need to pass the M.MapMarkerView object that you 564 * want to be removed from the map view. 565 * 566 * @param {M.MapMarkerView} marker The marker to be removed. 567 */ 568 removeMarker: function(marker) { 569 if(marker && typeof(marker) === 'object' && marker.type === 'M.MapMarkerView') { 570 var didRemoveMarker = NO; 571 this.markers = _.select(this.markers, function(m) { 572 if(marker === m){ 573 m.marker.setMap(null); 574 didRemoveMarker = YES; 575 } 576 return !(marker === m); 577 }); 578 if(!didRemoveMarker) { 579 M.Logger.log('No marker found matching the passed marker in removeMarker().', M.WARN); 580 } 581 } else { 582 M.Logger.log('No valid M.MapMarkerView passed for removeMarker().', M.WARN); 583 } 584 }, 585 586 /** 587 * This method removes all markers from this map view. It both cleans up the 588 * markers array and deletes the marker's visual representation from the map 589 * view. 590 */ 591 removeAllMarkers: function() { 592 _.each(this.markers, function(marker) { 593 marker.marker.setMap(null); 594 }); 595 this.markers = []; 596 } 597 598 });