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