PageRenderTime 54ms CodeModel.GetById 28ms RepoModel.GetById 0ms app.codeStats 0ms

/media/js/backbone.googlemaps.js

https://bitbucket.org/semion/busido
JavaScript | 397 lines | 236 code | 84 blank | 77 comment | 43 complexity | 2014a158063a9538185f11ffdb979bda MD5 | raw file
  1. /*!
  2. * Backbone.GoogleMaps
  3. * A Backbone JS layer for the GoogleMaps API
  4. * Copyright (c)2012 Edan Schwartz
  5. * Distributed under MIT license
  6. * https://github.com/eschwartz/backbone.googlemaps
  7. */
  8. Backbone.GoogleMaps = (function(Backbone, _, $){
  9. var GoogleMaps = {};
  10. /**
  11. * GoogleMaps.Location
  12. * --------------------
  13. * Representing a lat/lng location on a map
  14. */
  15. GoogleMaps.Location = Backbone.Model.extend({
  16. constructor: function() {
  17. _.bindAll(this, 'select', 'deselect', 'toggleSelect', 'getLatlng');
  18. this.defaults = _.extend({}, {
  19. lat : 0,
  20. lng : 0,
  21. selected : false,
  22. title : ""
  23. }, this.defaults);
  24. Backbone.Model.prototype.constructor.apply(this, arguments);
  25. },
  26. select: function() {
  27. if(!this.get("selected")) {
  28. this.set("selected", true);
  29. this.trigger('selected', this);
  30. Backbone.Events.trigger("location:selected", this);
  31. }
  32. },
  33. deselect: function() {
  34. if(this.get("selected")) {
  35. this.set("selected", false);
  36. this.trigger("deselected", this);
  37. Backbone.Events.trigger('location:deselected', this);
  38. }
  39. },
  40. toggleSelect: function() {
  41. if(this.get("selected")) {
  42. this.deselect();
  43. }
  44. else {
  45. this.select();
  46. }
  47. },
  48. getLatlng: function() {
  49. return new google.maps.LatLng(this.get("lat"), this.get("lng"));
  50. }
  51. });
  52. /**
  53. * GoogleMaps.LocationCollection
  54. * ------------------------------
  55. * A collection of map locations
  56. */
  57. GoogleMaps.LocationCollection = Backbone.Collection.extend({
  58. model: GoogleMaps.Location,
  59. constructor: function() {
  60. Backbone.Collection.prototype.constructor.apply(this, arguments);
  61. // Deselect other models on model select
  62. // ie. Only a single model can be selected in a collection
  63. Backbone.Events.on("location:selected", function(selectedModel) {
  64. this.each(function(model) {
  65. if(selectedModel.cid !== model.cid) { model.deselect(); }
  66. });
  67. }, this);
  68. }
  69. });
  70. /**
  71. * GoogleMaps.MapView
  72. * ------------------
  73. * Base maps overlay view from which all other overlay views extend
  74. */
  75. GoogleMaps.MapView = Backbone.View.extend({
  76. // Hash of Google Map events
  77. // Events will be attached to this.gOverlay (google map or overlay)
  78. // eg `zoom_changed': 'handleZoomChange'
  79. mapEvents: {},
  80. overlayOptions: {},
  81. constructor: function() {
  82. _.bindAll(this);
  83. Backbone.View.prototype.constructor.apply(this, arguments);
  84. // Ensure map and API loaded
  85. if(!google || !google.maps) throw new Error("Google maps API is not loaded.");
  86. if(!this.options.map && !this.map) throw new Error("A map must be specified.");
  87. this.gOverlay = this.map = this.options.map || this.map;
  88. // Set this.overlay options
  89. this.overlayOptions || (this.overlayOptions = this.options.overlayOptions);
  90. },
  91. // Attach listeners to the this.gOverlay
  92. // From the `mapEvents` hash
  93. bindMapEvents: function(mapEvents) {
  94. mapEvents || (mapEvents = this.mapEvents);
  95. for(event in mapEvents) {
  96. var handler = mapEvents[event];
  97. google.maps.event.addListener(this.gOverlay, event, this[handler]);
  98. }
  99. },
  100. render: function() {
  101. if(this.beforeRender) { this.beforeRender(); }
  102. this.bindMapEvents();
  103. if(this.onRender) { this.onRender(); }
  104. return this;
  105. },
  106. // Clean up view
  107. // Remove overlay from map and remove event listeners
  108. close: function() {
  109. if(this.beforeClose) { this.beforeClose(); }
  110. google.maps.event.clearInstanceListeners(this.gOverlay);
  111. if(this.gOverlay.setMap) { this.gOverlay.setMap(null); }
  112. this.gOverlay = null;
  113. if(this.onClose) { this.onClose(); }
  114. }
  115. });
  116. /**
  117. * GoogleMaps.InfoWindow
  118. * ---------------------
  119. * View controller for a google.maps.InfoWindow overlay instance
  120. */
  121. GoogleMaps.InfoWindow = GoogleMaps.MapView.extend({
  122. constructor: function() {
  123. GoogleMaps.MapView.prototype.constructor.apply(this, arguments);
  124. _.bindAll(this, 'render', 'close');
  125. // Require a related marker instance
  126. if(!this.options.marker && !this.marker) throw new Error("A marker must be specified for InfoWindow view.");
  127. this.marker = this.options.marker || this.marker;
  128. // Set InfoWindow template
  129. this.template = this.template || this.options.template;
  130. },
  131. // Render
  132. render: function() {
  133. if(this.beforeRender) { this.beforeRender(); }
  134. GoogleMaps.MapView.prototype.render.apply(this, arguments);
  135. // Render element
  136. var tmpl = (this.template)? $(this.template).html(): '<h2><%=title %></h2>';
  137. this.$el.html(_.template(tmpl, this.model.toJSON()));
  138. // Create InfoWindow
  139. this.gOverlay = new google.maps.InfoWindow(_.extend({
  140. content: this.$el[0]
  141. }, this.overlayOptions));
  142. // Display InfoWindow on map
  143. this.gOverlay.open(this.map, this.marker);
  144. if(this.onRender) { this.onRender(); }
  145. return this;
  146. },
  147. // Close and delete window, and clean up view
  148. close: function() {
  149. if(this.beforeClose) { this.beforeClose(); }
  150. GoogleMaps.MapView.prototype.close.apply(this, arguments);
  151. if(this.onClose) { this.onClose(); }
  152. return this;
  153. }
  154. });
  155. /**
  156. * GoogleMaps.MarkerView
  157. * ---------------------
  158. * View controller for a marker overlay
  159. */
  160. GoogleMaps.MarkerView = GoogleMaps.MapView.extend({
  161. // Set associated InfoWindow view
  162. infoWindow: GoogleMaps.InfoWindow,
  163. constructor: function() {
  164. GoogleMaps.MapView.prototype.constructor.apply(this, arguments);
  165. _.bindAll(this, 'render', 'close', 'openDetail', 'closeDetail', 'toggleSelect');
  166. // Ensure model
  167. if(!this.model) throw new Error("A model must be specified for a MarkerView");
  168. // Instantiate marker, with user defined properties
  169. this.gOverlay = new google.maps.Marker(_.extend({
  170. position: this.model.getLatlng(),
  171. map: this.map,
  172. title: this.model.title,
  173. animation: google.maps.Animation.DROP,
  174. visible: false // hide, until render
  175. }, this.overlayOptions));
  176. // Add default mapEvents
  177. _.extend(this.mapEvents, {
  178. 'click' : 'toggleSelect' // Select model on marker click
  179. });
  180. // Show detail view on model select
  181. this.model.on("selected", this.openDetail, this);
  182. this.model.on("deselected", this.closeDetail, this);
  183. this.model.on("change:lat change:lng", this.refreshOverlay, this);
  184. // Sync location model lat/lng with marker position
  185. this.bindMapEvents({
  186. 'position_changed': 'updateModelPosition'
  187. });
  188. },
  189. // update overlay position if lat or lng change
  190. refreshOverlay: function() {
  191. // Only update overlay if we're not already in sync
  192. // Otherwise we end up in an endless loop of
  193. // update model <--eventhandler--> update overlay
  194. if(!this.model.getLatlng().equals(this.gOverlay.getPosition())) {
  195. this.gOverlay.setOptions({
  196. position: this.model.getLatlng()
  197. });
  198. }
  199. },
  200. updateModelPosition: function() {
  201. var newPosition = this.gOverlay.getPosition();
  202. // Only update model if we're not already in sync
  203. // Otherwise we end up in an endless loop of
  204. // update model <--eventhandler--> update overlay
  205. if(!this.model.getLatlng().equals(newPosition)) {
  206. this.model.set({
  207. lat: newPosition.lat(),
  208. lng: newPosition.lng()
  209. });
  210. }
  211. },
  212. toggleSelect: function() {
  213. this.model.toggleSelect();
  214. },
  215. // Show the google maps marker overlay
  216. render: function() {
  217. if(this.beforeRender) { this.beforeRender(); }
  218. GoogleMaps.MapView.prototype.render.apply(this, arguments);
  219. this.gOverlay.setVisible(true);
  220. if(this.onRender) { this.onRender(); }
  221. return this;
  222. },
  223. close: function() {
  224. if(this.beforeClose) { this.beforeClose(); }
  225. this.closeDetail();
  226. GoogleMaps.MapView.prototype.close.apply(this, arguments);
  227. this.model.off();
  228. if(this.onClose) { this.onClose() }
  229. return this;
  230. },
  231. openDetail: function() {
  232. this.detailView = new this.infoWindow({
  233. model: this.model,
  234. map: this.map,
  235. marker: this.gOverlay
  236. });
  237. this.detailView.render();
  238. },
  239. closeDetail: function() {
  240. if(this.detailView) {
  241. this.detailView.close();
  242. this.detailView = null;
  243. }
  244. }
  245. });
  246. /**
  247. * GoogleMaps.MarkerCollectionView
  248. * -------------------------------
  249. * Collection of MarkerViews
  250. */
  251. GoogleMaps.MarkerCollectionView = Backbone.View.extend({
  252. markerView: GoogleMaps.MarkerView,
  253. markerViewChildren: {},
  254. constructor: function() {
  255. Backbone.View.prototype.constructor.apply(this, arguments);
  256. _.bindAll(this, 'render', 'closeChildren', 'closeChild', 'addChild', 'refresh', 'close');
  257. // Ensure map property
  258. if(!this.options.map && !this.map) throw new Error("A map must be specified on MarkerCollectionView instantiation");
  259. this.map || (this.map = this.options.map);
  260. // Bind to collection
  261. this.collection.on("reset", this.refresh, this);
  262. this.collection.on("add", this.addChild, this);
  263. this.collection.on("remove", this.closeChild, this);
  264. google.maps.event.addListener(this.map, 'idle', this.handleIdleMap);
  265. },
  266. // Render MarkerViews for all models in collection
  267. render: function(collection) {
  268. var collection = collection || this.collection;
  269. if(this.beforeRender) { this.beforeRender(); }
  270. // Create marker views for each model
  271. collection.each(this.addChild);
  272. if(this.onRender) { this.onRender(); }
  273. return this;
  274. },
  275. // Close all child MarkerViews
  276. closeChildren: function() {
  277. for(cid in this.markerViewChildren) {
  278. this.closeChild(this.markerViewChildren[cid]);
  279. }
  280. },
  281. closeChild: function(child) {
  282. // Param can be child's model, or child view itself
  283. var childView = (child instanceof Backbone.Model)? this.markerViewChildren[child.cid]: child;
  284. childView.close();
  285. delete this.markerViewChildren[childView.model.cid];
  286. },
  287. // Add a MarkerView and render
  288. addChild: function(childModel) {
  289. var markerView = new this.markerView({
  290. model: childModel,
  291. map: this.map
  292. });
  293. this.markerViewChildren[childModel.cid] = markerView;
  294. markerView.render();
  295. },
  296. refresh: function() {
  297. this.closeChildren();
  298. this.render();
  299. },
  300. // Close all child MarkerViews
  301. close: function() {
  302. this.closeChildren();
  303. this.collection.off();
  304. },
  305. getMapBounds: function(){
  306. return this.map.getCenter().toUrlValue(10);
  307. },
  308. handleIdleMap: function(){
  309. console.log("idle fired");
  310. }
  311. })
  312. return GoogleMaps;
  313. })(Backbone, _, window.jQuery || window.Zepto || window.ender);