markerclusterer.js 34 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315
  1. // ==ClosureCompiler==
  2. // @compilation_level ADVANCED_OPTIMIZATIONS
  3. // @externs_url https://raw.githubusercontent.com/google/closure-compiler/master/contrib/externs/maps/google_maps_api_v3.js
  4. // ==/ClosureCompiler==
  5. /**
  6. * @name MarkerClusterer for Google Maps v3
  7. * @version version 1.0
  8. * @author Luke Mahe
  9. * @fileoverview
  10. * The library creates and manages per-zoom-level clusters for large amounts of
  11. * markers.
  12. * <br/>
  13. * This is a v3 implementation of the
  14. * <a href="http://gmaps-utility-library-dev.googlecode.com/svn/tags/markerclusterer/"
  15. * >v2 MarkerClusterer</a>.
  16. */
  17. /**
  18. * @license
  19. * Copyright 2010 Google Inc. All Rights Reserved.
  20. *
  21. * Licensed under the Apache License, Version 2.0 (the "License");
  22. * you may not use this file except in compliance with the License.
  23. * You may obtain a copy of the License at
  24. *
  25. * http://www.apache.org/licenses/LICENSE-2.0
  26. *
  27. * Unless required by applicable law or agreed to in writing, software
  28. * distributed under the License is distributed on an "AS IS" BASIS,
  29. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  30. * See the License for the specific language governing permissions and
  31. * limitations under the License.
  32. */
  33. /**
  34. * A Marker Clusterer that clusters markers.
  35. *
  36. * @param {google.maps.Map} map The Google map to attach to.
  37. * @param {Array.<google.maps.Marker>=} opt_markers Optional markers to add to
  38. * the cluster.
  39. * @param {Object=} opt_options support the following options:
  40. * 'gridSize': (number) The grid size of a cluster in pixels.
  41. * 'maxZoom': (number) The maximum zoom level that a marker can be part of a
  42. * cluster.
  43. * 'zoomOnClick': (boolean) Whether the default behaviour of clicking on a
  44. * cluster is to zoom into it.
  45. * 'averageCenter': (boolean) Whether the center of each cluster should be
  46. * the average of all markers in the cluster.
  47. * 'minimumClusterSize': (number) The minimum number of markers to be in a
  48. * cluster before the markers are hidden and a count
  49. * is shown.
  50. * 'styles': (object) An object that has style properties:
  51. * 'url': (string) The image url.
  52. * 'height': (number) The image height.
  53. * 'width': (number) The image width.
  54. * 'anchor': (Array) The anchor position of the label text.
  55. * 'textColor': (string) The text color.
  56. * 'textSize': (number) The text size.
  57. * 'backgroundPosition': (string) The position of the backgound x, y.
  58. * 'iconAnchor': (Array) The anchor position of the icon x, y.
  59. * @constructor
  60. * @extends google.maps.OverlayView
  61. */
  62. function MarkerClusterer(map, opt_markers, opt_options) {
  63. // MarkerClusterer implements google.maps.OverlayView interface. We use the
  64. // extend function to extend MarkerClusterer with google.maps.OverlayView
  65. // because it might not always be available when the code is defined so we
  66. // look for it at the last possible moment. If it doesn't exist now then
  67. // there is no point going ahead :)
  68. this.extend(MarkerClusterer, google.maps.OverlayView);
  69. this.map_ = map;
  70. /**
  71. * @type {Array.<google.maps.Marker>}
  72. * @private
  73. */
  74. this.markers_ = [];
  75. /**
  76. * @type {Array.<Cluster>}
  77. */
  78. this.clusters_ = [];
  79. this.sizes = [53, 56, 66, 78, 90];
  80. /**
  81. * @private
  82. */
  83. this.styles_ = [];
  84. /**
  85. * @type {boolean}
  86. * @private
  87. */
  88. this.ready_ = false;
  89. var options = opt_options || {};
  90. /**
  91. * @type {number}
  92. * @private
  93. */
  94. this.gridSize_ = options['gridSize'] || 60;
  95. /**
  96. * @private
  97. */
  98. this.minClusterSize_ = options['minimumClusterSize'] || 2;
  99. /**
  100. * @type {?number}
  101. * @private
  102. */
  103. this.maxZoom_ = options['maxZoom'] || null;
  104. this.styles_ = options['styles'] || [];
  105. /**
  106. * @type {string}
  107. * @private
  108. */
  109. this.imagePath_ = options['imagePath'] ||
  110. this.MARKER_CLUSTER_IMAGE_PATH_;
  111. /**
  112. * @type {string}
  113. * @private
  114. */
  115. this.imageExtension_ = options['imageExtension'] ||
  116. this.MARKER_CLUSTER_IMAGE_EXTENSION_;
  117. /**
  118. * @type {boolean}
  119. * @private
  120. */
  121. this.zoomOnClick_ = true;
  122. if (options['zoomOnClick'] != undefined) {
  123. this.zoomOnClick_ = options['zoomOnClick'];
  124. }
  125. /**
  126. * @type {boolean}
  127. * @private
  128. */
  129. this.averageCenter_ = false;
  130. if (options['averageCenter'] != undefined) {
  131. this.averageCenter_ = options['averageCenter'];
  132. }
  133. this.setupStyles_();
  134. this.setMap(map);
  135. /**
  136. * @type {number}
  137. * @private
  138. */
  139. this.prevZoom_ = this.map_.getZoom();
  140. // Add the map event listeners
  141. var that = this;
  142. google.maps.event.addListener(this.map_, 'zoom_changed', function() {
  143. var zoom = that.map_.getZoom();
  144. if (that.prevZoom_ != zoom) {
  145. that.prevZoom_ = zoom;
  146. that.resetViewport();
  147. }
  148. });
  149. google.maps.event.addListener(this.map_, 'idle', function() {
  150. that.redraw();
  151. });
  152. // Finally, add the markers
  153. if (opt_markers && opt_markers.length) {
  154. this.addMarkers(opt_markers, false);
  155. }
  156. }
  157. /**
  158. * The marker cluster image path.
  159. *
  160. * @type {string}
  161. * @private
  162. */
  163. MarkerClusterer.prototype.MARKER_CLUSTER_IMAGE_PATH_ = '../images/m';
  164. /**
  165. * The marker cluster image path.
  166. *
  167. * @type {string}
  168. * @private
  169. */
  170. MarkerClusterer.prototype.MARKER_CLUSTER_IMAGE_EXTENSION_ = 'png';
  171. /**
  172. * Extends a objects prototype by anothers.
  173. *
  174. * @param {Object} obj1 The object to be extended.
  175. * @param {Object} obj2 The object to extend with.
  176. * @return {Object} The new extended object.
  177. * @ignore
  178. */
  179. MarkerClusterer.prototype.extend = function(obj1, obj2) {
  180. return (function(object) {
  181. for (var property in object.prototype) {
  182. this.prototype[property] = object.prototype[property];
  183. }
  184. return this;
  185. }).apply(obj1, [obj2]);
  186. };
  187. /**
  188. * Implementaion of the interface method.
  189. * @ignore
  190. */
  191. MarkerClusterer.prototype.onAdd = function() {
  192. this.setReady_(true);
  193. };
  194. /**
  195. * Implementaion of the interface method.
  196. * @ignore
  197. */
  198. MarkerClusterer.prototype.draw = function() {};
  199. /**
  200. * Sets up the styles object.
  201. *
  202. * @private
  203. */
  204. MarkerClusterer.prototype.setupStyles_ = function() {
  205. if (this.styles_.length) {
  206. return;
  207. }
  208. for (var i = 0, size; size = this.sizes[i]; i++) {
  209. this.styles_.push({
  210. url: this.imagePath_ + (i + 1) + '.' + this.imageExtension_,
  211. height: size,
  212. width: size
  213. });
  214. }
  215. };
  216. /**
  217. * Fit the map to the bounds of the markers in the clusterer.
  218. */
  219. MarkerClusterer.prototype.fitMapToMarkers = function() {
  220. var markers = this.getMarkers();
  221. var bounds = new google.maps.LatLngBounds();
  222. for (var i = 0, marker; marker = markers[i]; i++) {
  223. bounds.extend(marker.getPosition());
  224. }
  225. this.map_.fitBounds(bounds);
  226. };
  227. /**
  228. * Sets the styles.
  229. *
  230. * @param {Object} styles The style to set.
  231. */
  232. MarkerClusterer.prototype.setStyles = function(styles) {
  233. this.styles_ = styles;
  234. };
  235. /**
  236. * Gets the styles.
  237. *
  238. * @return {Object} The styles object.
  239. */
  240. MarkerClusterer.prototype.getStyles = function() {
  241. return this.styles_;
  242. };
  243. /**
  244. * Whether zoom on click is set.
  245. *
  246. * @return {boolean} True if zoomOnClick_ is set.
  247. */
  248. MarkerClusterer.prototype.isZoomOnClick = function() {
  249. return this.zoomOnClick_;
  250. };
  251. /**
  252. * Whether average center is set.
  253. *
  254. * @return {boolean} True if averageCenter_ is set.
  255. */
  256. MarkerClusterer.prototype.isAverageCenter = function() {
  257. return this.averageCenter_;
  258. };
  259. /**
  260. * Returns the array of markers in the clusterer.
  261. *
  262. * @return {Array.<google.maps.Marker>} The markers.
  263. */
  264. MarkerClusterer.prototype.getMarkers = function() {
  265. return this.markers_;
  266. };
  267. /**
  268. * Returns the number of markers in the clusterer
  269. *
  270. * @return {Number} The number of markers.
  271. */
  272. MarkerClusterer.prototype.getTotalMarkers = function() {
  273. return this.markers_.length;
  274. };
  275. /**
  276. * Sets the max zoom for the clusterer.
  277. *
  278. * @param {number} maxZoom The max zoom level.
  279. */
  280. MarkerClusterer.prototype.setMaxZoom = function(maxZoom) {
  281. this.maxZoom_ = maxZoom;
  282. };
  283. /**
  284. * Gets the max zoom for the clusterer.
  285. *
  286. * @return {number} The max zoom level.
  287. */
  288. MarkerClusterer.prototype.getMaxZoom = function() {
  289. return this.maxZoom_;
  290. };
  291. /**
  292. * The function for calculating the cluster icon image.
  293. *
  294. * @param {Array.<google.maps.Marker>} markers The markers in the clusterer.
  295. * @param {number} numStyles The number of styles available.
  296. * @return {Object} A object properties: 'text' (string) and 'index' (number).
  297. * @private
  298. */
  299. MarkerClusterer.prototype.calculator_ = function(markers, numStyles) {
  300. var index = 0;
  301. var count = markers.length;
  302. var dv = count;
  303. while (dv !== 0) {
  304. dv = parseInt(dv / 10, 10);
  305. index++;
  306. }
  307. index = Math.min(index, numStyles);
  308. return {
  309. text: count,
  310. index: index
  311. };
  312. };
  313. /**
  314. * Set the calculator function.
  315. *
  316. * @param {function(Array, number)} calculator The function to set as the
  317. * calculator. The function should return a object properties:
  318. * 'text' (string) and 'index' (number).
  319. *
  320. */
  321. MarkerClusterer.prototype.setCalculator = function(calculator) {
  322. this.calculator_ = calculator;
  323. };
  324. /**
  325. * Get the calculator function.
  326. *
  327. * @return {function(Array, number)} the calculator function.
  328. */
  329. MarkerClusterer.prototype.getCalculator = function() {
  330. return this.calculator_;
  331. };
  332. /**
  333. * Add an array of markers to the clusterer.
  334. *
  335. * @param {Array.<google.maps.Marker>} markers The markers to add.
  336. * @param {boolean=} opt_nodraw Whether to redraw the clusters.
  337. */
  338. MarkerClusterer.prototype.addMarkers = function(markers, opt_nodraw) {
  339. for (var i = 0, marker; marker = markers[i]; i++) {
  340. this.pushMarkerTo_(marker);
  341. }
  342. if (!opt_nodraw) {
  343. this.redraw();
  344. }
  345. };
  346. /**
  347. * Pushes a marker to the clusterer.
  348. *
  349. * @param {google.maps.Marker} marker The marker to add.
  350. * @private
  351. */
  352. MarkerClusterer.prototype.pushMarkerTo_ = function(marker) {
  353. marker.isAdded = false;
  354. if (marker['draggable']) {
  355. // If the marker is draggable add a listener so we update the clusters on
  356. // the drag end.
  357. var that = this;
  358. google.maps.event.addListener(marker, 'dragend', function() {
  359. marker.isAdded = false;
  360. that.repaint();
  361. });
  362. }
  363. this.markers_.push(marker);
  364. };
  365. /**
  366. * Adds a marker to the clusterer and redraws if needed.
  367. *
  368. * @param {google.maps.Marker} marker The marker to add.
  369. * @param {boolean=} opt_nodraw Whether to redraw the clusters.
  370. */
  371. MarkerClusterer.prototype.addMarker = function(marker, opt_nodraw) {
  372. this.pushMarkerTo_(marker);
  373. if (!opt_nodraw) {
  374. this.redraw();
  375. }
  376. };
  377. /**
  378. * Removes a marker and returns true if removed, false if not
  379. *
  380. * @param {google.maps.Marker} marker The marker to remove
  381. * @return {boolean} Whether the marker was removed or not
  382. * @private
  383. */
  384. MarkerClusterer.prototype.removeMarker_ = function(marker) {
  385. var index = -1;
  386. if (this.markers_.indexOf) {
  387. index = this.markers_.indexOf(marker);
  388. } else {
  389. for (var i = 0, m; m = this.markers_[i]; i++) {
  390. if (m == marker) {
  391. index = i;
  392. break;
  393. }
  394. }
  395. }
  396. if (index == -1) {
  397. // Marker is not in our list of markers.
  398. return false;
  399. }
  400. marker.setMap(null);
  401. this.markers_.splice(index, 1);
  402. return true;
  403. };
  404. /**
  405. * Remove a marker from the cluster.
  406. *
  407. * @param {google.maps.Marker} marker The marker to remove.
  408. * @param {boolean=} opt_nodraw Optional boolean to force no redraw.
  409. * @return {boolean} True if the marker was removed.
  410. */
  411. MarkerClusterer.prototype.removeMarker = function(marker, opt_nodraw) {
  412. var removed = this.removeMarker_(marker);
  413. if (!opt_nodraw && removed) {
  414. this.resetViewport();
  415. this.redraw();
  416. return true;
  417. } else {
  418. return false;
  419. }
  420. };
  421. /**
  422. * Removes an array of markers from the cluster.
  423. *
  424. * @param {Array.<google.maps.Marker>} markers The markers to remove.
  425. * @param {boolean=} opt_nodraw Optional boolean to force no redraw.
  426. */
  427. MarkerClusterer.prototype.removeMarkers = function(markers, opt_nodraw) {
  428. var removed = false;
  429. for (var i = 0, marker; marker = markers[i]; i++) {
  430. var r = this.removeMarker_(marker);
  431. removed = removed || r;
  432. }
  433. if (!opt_nodraw && removed) {
  434. this.resetViewport();
  435. this.redraw();
  436. return true;
  437. }
  438. };
  439. /**
  440. * Sets the clusterer's ready state.
  441. *
  442. * @param {boolean} ready The state.
  443. * @private
  444. */
  445. MarkerClusterer.prototype.setReady_ = function(ready) {
  446. if (!this.ready_) {
  447. this.ready_ = ready;
  448. this.createClusters_();
  449. }
  450. };
  451. /**
  452. * Returns the number of clusters in the clusterer.
  453. *
  454. * @return {number} The number of clusters.
  455. */
  456. MarkerClusterer.prototype.getTotalClusters = function() {
  457. return this.clusters_.length;
  458. };
  459. /**
  460. * Returns the google map that the clusterer is associated with.
  461. *
  462. * @return {google.maps.Map} The map.
  463. */
  464. MarkerClusterer.prototype.getMap = function() {
  465. return this.map_;
  466. };
  467. /**
  468. * Sets the google map that the clusterer is associated with.
  469. *
  470. * @param {google.maps.Map} map The map.
  471. */
  472. MarkerClusterer.prototype.setMap = function(map) {
  473. this.map_ = map;
  474. };
  475. /**
  476. * Returns the size of the grid.
  477. *
  478. * @return {number} The grid size.
  479. */
  480. MarkerClusterer.prototype.getGridSize = function() {
  481. return this.gridSize_;
  482. };
  483. /**
  484. * Sets the size of the grid.
  485. *
  486. * @param {number} size The grid size.
  487. */
  488. MarkerClusterer.prototype.setGridSize = function(size) {
  489. this.gridSize_ = size;
  490. };
  491. /**
  492. * Returns the min cluster size.
  493. *
  494. * @return {number} The grid size.
  495. */
  496. MarkerClusterer.prototype.getMinClusterSize = function() {
  497. return this.minClusterSize_;
  498. };
  499. /**
  500. * Sets the min cluster size.
  501. *
  502. * @param {number} size The grid size.
  503. */
  504. MarkerClusterer.prototype.setMinClusterSize = function(size) {
  505. this.minClusterSize_ = size;
  506. };
  507. /**
  508. * Extends a bounds object by the grid size.
  509. *
  510. * @param {google.maps.LatLngBounds} bounds The bounds to extend.
  511. * @return {google.maps.LatLngBounds} The extended bounds.
  512. */
  513. MarkerClusterer.prototype.getExtendedBounds = function(bounds) {
  514. var projection = this.getProjection();
  515. // Turn the bounds into latlng.
  516. var tr = new google.maps.LatLng(bounds.getNorthEast().lat(),
  517. bounds.getNorthEast().lng());
  518. var bl = new google.maps.LatLng(bounds.getSouthWest().lat(),
  519. bounds.getSouthWest().lng());
  520. // Convert the points to pixels and the extend out by the grid size.
  521. var trPix = projection.fromLatLngToDivPixel(tr);
  522. trPix.x += this.gridSize_;
  523. trPix.y -= this.gridSize_;
  524. var blPix = projection.fromLatLngToDivPixel(bl);
  525. blPix.x -= this.gridSize_;
  526. blPix.y += this.gridSize_;
  527. // Convert the pixel points back to LatLng
  528. var ne = projection.fromDivPixelToLatLng(trPix);
  529. var sw = projection.fromDivPixelToLatLng(blPix);
  530. // Extend the bounds to contain the new bounds.
  531. bounds.extend(ne);
  532. bounds.extend(sw);
  533. return bounds;
  534. };
  535. /**
  536. * Determins if a marker is contained in a bounds.
  537. *
  538. * @param {google.maps.Marker} marker The marker to check.
  539. * @param {google.maps.LatLngBounds} bounds The bounds to check against.
  540. * @return {boolean} True if the marker is in the bounds.
  541. * @private
  542. */
  543. MarkerClusterer.prototype.isMarkerInBounds_ = function(marker, bounds) {
  544. return bounds.contains(marker.getPosition());
  545. };
  546. /**
  547. * Clears all clusters and markers from the clusterer.
  548. */
  549. MarkerClusterer.prototype.clearMarkers = function() {
  550. this.resetViewport(true);
  551. // Set the markers a empty array.
  552. this.markers_ = [];
  553. };
  554. /**
  555. * Clears all existing clusters and recreates them.
  556. * @param {boolean} opt_hide To also hide the marker.
  557. */
  558. MarkerClusterer.prototype.resetViewport = function(opt_hide) {
  559. // Remove all the clusters
  560. for (var i = 0, cluster; cluster = this.clusters_[i]; i++) {
  561. cluster.remove();
  562. }
  563. // Reset the markers to not be added and to be invisible.
  564. for (var i = 0, marker; marker = this.markers_[i]; i++) {
  565. marker.isAdded = false;
  566. if (opt_hide) {
  567. marker.setMap(null);
  568. }
  569. }
  570. this.clusters_ = [];
  571. };
  572. /**
  573. *
  574. */
  575. MarkerClusterer.prototype.repaint = function() {
  576. var oldClusters = this.clusters_.slice();
  577. this.clusters_.length = 0;
  578. this.resetViewport();
  579. this.redraw();
  580. // Remove the old clusters.
  581. // Do it in a timeout so the other clusters have been drawn first.
  582. window.setTimeout(function() {
  583. for (var i = 0, cluster; cluster = oldClusters[i]; i++) {
  584. cluster.remove();
  585. }
  586. }, 0);
  587. };
  588. /**
  589. * Redraws the clusters.
  590. */
  591. MarkerClusterer.prototype.redraw = function() {
  592. this.createClusters_();
  593. };
  594. /**
  595. * Calculates the distance between two latlng locations in km.
  596. * @see http://www.movable-type.co.uk/scripts/latlong.html
  597. *
  598. * @param {google.maps.LatLng} p1 The first lat lng point.
  599. * @param {google.maps.LatLng} p2 The second lat lng point.
  600. * @return {number} The distance between the two points in km.
  601. * @private
  602. */
  603. MarkerClusterer.prototype.distanceBetweenPoints_ = function(p1, p2) {
  604. if (!p1 || !p2) {
  605. return 0;
  606. }
  607. var R = 6371; // Radius of the Earth in km
  608. var dLat = (p2.lat() - p1.lat()) * Math.PI / 180;
  609. var dLon = (p2.lng() - p1.lng()) * Math.PI / 180;
  610. var a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
  611. Math.cos(p1.lat() * Math.PI / 180) * Math.cos(p2.lat() * Math.PI / 180) *
  612. Math.sin(dLon / 2) * Math.sin(dLon / 2);
  613. var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
  614. var d = R * c;
  615. return d;
  616. };
  617. /**
  618. * Add a marker to a cluster, or creates a new cluster.
  619. *
  620. * @param {google.maps.Marker} marker The marker to add.
  621. * @private
  622. */
  623. MarkerClusterer.prototype.addToClosestCluster_ = function(marker) {
  624. var distance = 40000; // Some large number
  625. var clusterToAddTo = null;
  626. var pos = marker.getPosition();
  627. for (var i = 0, cluster; cluster = this.clusters_[i]; i++) {
  628. var center = cluster.getCenter();
  629. if (center) {
  630. var d = this.distanceBetweenPoints_(center, marker.getPosition());
  631. if (d < distance) {
  632. distance = d;
  633. clusterToAddTo = cluster;
  634. }
  635. }
  636. }
  637. if (clusterToAddTo && clusterToAddTo.isMarkerInClusterBounds(marker)) {
  638. clusterToAddTo.addMarker(marker);
  639. } else {
  640. var cluster = new Cluster(this);
  641. cluster.addMarker(marker);
  642. this.clusters_.push(cluster);
  643. }
  644. };
  645. /**
  646. * Creates the clusters.
  647. *
  648. * @private
  649. */
  650. MarkerClusterer.prototype.createClusters_ = function() {
  651. if (!this.ready_) {
  652. return;
  653. }
  654. // Get our current map view bounds.
  655. // Create a new bounds object so we don't affect the map.
  656. var mapBounds = new google.maps.LatLngBounds(this.map_.getBounds().getSouthWest(),
  657. this.map_.getBounds().getNorthEast());
  658. var bounds = this.getExtendedBounds(mapBounds);
  659. for (var i = 0, marker; marker = this.markers_[i]; i++) {
  660. if (!marker.isAdded && this.isMarkerInBounds_(marker, bounds)) {
  661. this.addToClosestCluster_(marker);
  662. }
  663. }
  664. };
  665. /**
  666. * A cluster that contains markers.
  667. *
  668. * @param {MarkerClusterer} markerClusterer The markerclusterer that this
  669. * cluster is associated with.
  670. * @constructor
  671. * @ignore
  672. */
  673. function Cluster(markerClusterer) {
  674. this.markerClusterer_ = markerClusterer;
  675. this.map_ = markerClusterer.getMap();
  676. this.gridSize_ = markerClusterer.getGridSize();
  677. this.minClusterSize_ = markerClusterer.getMinClusterSize();
  678. this.averageCenter_ = markerClusterer.isAverageCenter();
  679. this.center_ = null;
  680. this.markers_ = [];
  681. this.bounds_ = null;
  682. this.clusterIcon_ = new ClusterIcon(this, markerClusterer.getStyles(),
  683. markerClusterer.getGridSize());
  684. }
  685. /**
  686. * Determins if a marker is already added to the cluster.
  687. *
  688. * @param {google.maps.Marker} marker The marker to check.
  689. * @return {boolean} True if the marker is already added.
  690. */
  691. Cluster.prototype.isMarkerAlreadyAdded = function(marker) {
  692. if (this.markers_.indexOf) {
  693. return this.markers_.indexOf(marker) != -1;
  694. } else {
  695. for (var i = 0, m; m = this.markers_[i]; i++) {
  696. if (m == marker) {
  697. return true;
  698. }
  699. }
  700. }
  701. return false;
  702. };
  703. /**
  704. * Add a marker the cluster.
  705. *
  706. * @param {google.maps.Marker} marker The marker to add.
  707. * @return {boolean} True if the marker was added.
  708. */
  709. Cluster.prototype.addMarker = function(marker) {
  710. if (this.isMarkerAlreadyAdded(marker)) {
  711. return false;
  712. }
  713. if (!this.center_) {
  714. this.center_ = marker.getPosition();
  715. this.calculateBounds_();
  716. } else {
  717. if (this.averageCenter_) {
  718. var l = this.markers_.length + 1;
  719. var lat = (this.center_.lat() * (l-1) + marker.getPosition().lat()) / l;
  720. var lng = (this.center_.lng() * (l-1) + marker.getPosition().lng()) / l;
  721. this.center_ = new google.maps.LatLng(lat, lng);
  722. this.calculateBounds_();
  723. }
  724. }
  725. marker.isAdded = true;
  726. this.markers_.push(marker);
  727. var len = this.markers_.length;
  728. if (len < this.minClusterSize_ && marker.getMap() != this.map_) {
  729. // Min cluster size not reached so show the marker.
  730. marker.setMap(this.map_);
  731. }
  732. if (len == this.minClusterSize_) {
  733. // Hide the markers that were showing.
  734. for (var i = 0; i < len; i++) {
  735. this.markers_[i].setMap(null);
  736. }
  737. }
  738. if (len >= this.minClusterSize_) {
  739. marker.setMap(null);
  740. }
  741. this.updateIcon();
  742. return true;
  743. };
  744. /**
  745. * Returns the marker clusterer that the cluster is associated with.
  746. *
  747. * @return {MarkerClusterer} The associated marker clusterer.
  748. */
  749. Cluster.prototype.getMarkerClusterer = function() {
  750. return this.markerClusterer_;
  751. };
  752. /**
  753. * Returns the bounds of the cluster.
  754. *
  755. * @return {google.maps.LatLngBounds} the cluster bounds.
  756. */
  757. Cluster.prototype.getBounds = function() {
  758. var bounds = new google.maps.LatLngBounds(this.center_, this.center_);
  759. var markers = this.getMarkers();
  760. for (var i = 0, marker; marker = markers[i]; i++) {
  761. bounds.extend(marker.getPosition());
  762. }
  763. return bounds;
  764. };
  765. /**
  766. * Removes the cluster
  767. */
  768. Cluster.prototype.remove = function() {
  769. this.clusterIcon_.remove();
  770. this.markers_.length = 0;
  771. delete this.markers_;
  772. };
  773. /**
  774. * Returns the center of the cluster.
  775. *
  776. * @return {number} The cluster center.
  777. */
  778. Cluster.prototype.getSize = function() {
  779. return this.markers_.length;
  780. };
  781. /**
  782. * Returns the center of the cluster.
  783. *
  784. * @return {Array.<google.maps.Marker>} The cluster center.
  785. */
  786. Cluster.prototype.getMarkers = function() {
  787. return this.markers_;
  788. };
  789. /**
  790. * Returns the center of the cluster.
  791. *
  792. * @return {google.maps.LatLng} The cluster center.
  793. */
  794. Cluster.prototype.getCenter = function() {
  795. return this.center_;
  796. };
  797. /**
  798. * Calculated the extended bounds of the cluster with the grid.
  799. *
  800. * @private
  801. */
  802. Cluster.prototype.calculateBounds_ = function() {
  803. var bounds = new google.maps.LatLngBounds(this.center_, this.center_);
  804. this.bounds_ = this.markerClusterer_.getExtendedBounds(bounds);
  805. };
  806. /**
  807. * Determines if a marker lies in the clusters bounds.
  808. *
  809. * @param {google.maps.Marker} marker The marker to check.
  810. * @return {boolean} True if the marker lies in the bounds.
  811. */
  812. Cluster.prototype.isMarkerInClusterBounds = function(marker) {
  813. return this.bounds_.contains(marker.getPosition());
  814. };
  815. /**
  816. * Returns the map that the cluster is associated with.
  817. *
  818. * @return {google.maps.Map} The map.
  819. */
  820. Cluster.prototype.getMap = function() {
  821. return this.map_;
  822. };
  823. /**
  824. * Updates the cluster icon
  825. */
  826. Cluster.prototype.updateIcon = function() {
  827. var zoom = this.map_.getZoom();
  828. var mz = this.markerClusterer_.getMaxZoom();
  829. if (mz && zoom > mz) {
  830. // The zoom is greater than our max zoom so show all the markers in cluster.
  831. for (var i = 0, marker; marker = this.markers_[i]; i++) {
  832. marker.setMap(this.map_);
  833. }
  834. return;
  835. }
  836. if (this.markers_.length < this.minClusterSize_) {
  837. // Min cluster size not yet reached.
  838. this.clusterIcon_.hide();
  839. return;
  840. }
  841. var numStyles = this.markerClusterer_.getStyles().length;
  842. var sums = this.markerClusterer_.getCalculator()(this.markers_, numStyles);
  843. this.clusterIcon_.setCenter(this.center_);
  844. this.clusterIcon_.setSums(sums);
  845. this.clusterIcon_.show();
  846. };
  847. /**
  848. * A cluster icon
  849. *
  850. * @param {Cluster} cluster The cluster to be associated with.
  851. * @param {Object} styles An object that has style properties:
  852. * 'url': (string) The image url.
  853. * 'height': (number) The image height.
  854. * 'width': (number) The image width.
  855. * 'anchor': (Array) The anchor position of the label text.
  856. * 'textColor': (string) The text color.
  857. * 'textSize': (number) The text size.
  858. * 'backgroundPosition: (string) The background postition x, y.
  859. * @param {number=} opt_padding Optional padding to apply to the cluster icon.
  860. * @constructor
  861. * @extends google.maps.OverlayView
  862. * @ignore
  863. */
  864. function ClusterIcon(cluster, styles, opt_padding) {
  865. cluster.getMarkerClusterer().extend(ClusterIcon, google.maps.OverlayView);
  866. this.styles_ = styles;
  867. this.padding_ = opt_padding || 0;
  868. this.cluster_ = cluster;
  869. this.center_ = null;
  870. this.map_ = cluster.getMap();
  871. this.div_ = null;
  872. this.sums_ = null;
  873. this.visible_ = false;
  874. this.setMap(this.map_);
  875. }
  876. /**
  877. * Triggers the clusterclick event and zoom's if the option is set.
  878. *
  879. * @param {google.maps.MouseEvent} event The event to propagate
  880. */
  881. ClusterIcon.prototype.triggerClusterClick = function(event) {
  882. var markerClusterer = this.cluster_.getMarkerClusterer();
  883. // Trigger the clusterclick event.
  884. google.maps.event.trigger(markerClusterer, 'clusterclick', this.cluster_, event);
  885. if (markerClusterer.isZoomOnClick()) {
  886. // Zoom into the cluster.
  887. this.map_.fitBounds(this.cluster_.getBounds());
  888. }
  889. };
  890. /**
  891. * Adding the cluster icon to the dom.
  892. * @ignore
  893. */
  894. ClusterIcon.prototype.onAdd = function() {
  895. this.div_ = document.createElement('DIV');
  896. if (this.visible_) {
  897. var pos = this.getPosFromLatLng_(this.center_);
  898. this.div_.style.cssText = this.createCss(pos);
  899. this.div_.innerHTML = this.sums_.text;
  900. }
  901. var panes = this.getPanes();
  902. panes.overlayMouseTarget.appendChild(this.div_);
  903. var that = this;
  904. var isDragging = false;
  905. google.maps.event.addDomListener(this.div_, 'click', function(event) {
  906. // Only perform click when not preceded by a drag
  907. if (!isDragging) {
  908. that.triggerClusterClick(event);
  909. }
  910. });
  911. google.maps.event.addDomListener(this.div_, 'mousedown', function() {
  912. isDragging = false;
  913. });
  914. google.maps.event.addDomListener(this.div_, 'mousemove', function() {
  915. isDragging = true;
  916. });
  917. };
  918. /**
  919. * Returns the position to place the div dending on the latlng.
  920. *
  921. * @param {google.maps.LatLng} latlng The position in latlng.
  922. * @return {google.maps.Point} The position in pixels.
  923. * @private
  924. */
  925. ClusterIcon.prototype.getPosFromLatLng_ = function(latlng) {
  926. var pos = this.getProjection().fromLatLngToDivPixel(latlng);
  927. if (typeof this.iconAnchor_ === 'object' && this.iconAnchor_.length === 2) {
  928. pos.x -= this.iconAnchor_[0];
  929. pos.y -= this.iconAnchor_[1];
  930. } else {
  931. pos.x -= parseInt(this.width_ / 2, 10);
  932. pos.y -= parseInt(this.height_ / 2, 10);
  933. }
  934. return pos;
  935. };
  936. /**
  937. * Draw the icon.
  938. * @ignore
  939. */
  940. ClusterIcon.prototype.draw = function() {
  941. if (this.visible_) {
  942. var pos = this.getPosFromLatLng_(this.center_);
  943. this.div_.style.top = pos.y + 'px';
  944. this.div_.style.left = pos.x + 'px';
  945. }
  946. };
  947. /**
  948. * Hide the icon.
  949. */
  950. ClusterIcon.prototype.hide = function() {
  951. if (this.div_) {
  952. this.div_.style.display = 'none';
  953. }
  954. this.visible_ = false;
  955. };
  956. /**
  957. * Position and show the icon.
  958. */
  959. ClusterIcon.prototype.show = function() {
  960. if (this.div_) {
  961. var pos = this.getPosFromLatLng_(this.center_);
  962. this.div_.style.cssText = this.createCss(pos);
  963. this.div_.style.display = '';
  964. }
  965. this.visible_ = true;
  966. };
  967. /**
  968. * Remove the icon from the map
  969. */
  970. ClusterIcon.prototype.remove = function() {
  971. this.setMap(null);
  972. };
  973. /**
  974. * Implementation of the onRemove interface.
  975. * @ignore
  976. */
  977. ClusterIcon.prototype.onRemove = function() {
  978. if (this.div_ && this.div_.parentNode) {
  979. this.hide();
  980. this.div_.parentNode.removeChild(this.div_);
  981. this.div_ = null;
  982. }
  983. };
  984. /**
  985. * Set the sums of the icon.
  986. *
  987. * @param {Object} sums The sums containing:
  988. * 'text': (string) The text to display in the icon.
  989. * 'index': (number) The style index of the icon.
  990. */
  991. ClusterIcon.prototype.setSums = function(sums) {
  992. this.sums_ = sums;
  993. this.text_ = sums.text;
  994. this.index_ = sums.index;
  995. if (this.div_) {
  996. this.div_.innerHTML = sums.text;
  997. }
  998. this.useStyle();
  999. };
  1000. /**
  1001. * Sets the icon to the the styles.
  1002. */
  1003. ClusterIcon.prototype.useStyle = function() {
  1004. var index = Math.max(0, this.sums_.index - 1);
  1005. index = Math.min(this.styles_.length - 1, index);
  1006. var style = this.styles_[index];
  1007. this.url_ = style['url'];
  1008. this.height_ = style['height'];
  1009. this.width_ = style['width'];
  1010. this.textColor_ = style['textColor'];
  1011. this.anchor_ = style['anchor'];
  1012. this.textSize_ = style['textSize'];
  1013. this.backgroundPosition_ = style['backgroundPosition'];
  1014. this.iconAnchor_ = style['iconAnchor'];
  1015. };
  1016. /**
  1017. * Sets the center of the icon.
  1018. *
  1019. * @param {google.maps.LatLng} center The latlng to set as the center.
  1020. */
  1021. ClusterIcon.prototype.setCenter = function(center) {
  1022. this.center_ = center;
  1023. };
  1024. /**
  1025. * Create the css text based on the position of the icon.
  1026. *
  1027. * @param {google.maps.Point} pos The position.
  1028. * @return {string} The css style text.
  1029. */
  1030. ClusterIcon.prototype.createCss = function(pos) {
  1031. var style = [];
  1032. style.push('background-image:url(' + this.url_ + ');');
  1033. var backgroundPosition = this.backgroundPosition_ ? this.backgroundPosition_ : '0 0';
  1034. style.push('background-position:' + backgroundPosition + ';');
  1035. if (typeof this.anchor_ === 'object') {
  1036. if (typeof this.anchor_[0] === 'number' && this.anchor_[0] > 0 &&
  1037. this.anchor_[0] < this.height_) {
  1038. style.push('height:' + (this.height_ - this.anchor_[0]) +
  1039. 'px; padding-top:' + this.anchor_[0] + 'px;');
  1040. } else if (typeof this.anchor_[0] === 'number' && this.anchor_[0] < 0 &&
  1041. -this.anchor_[0] < this.height_) {
  1042. style.push('height:' + this.height_ + 'px; line-height:' + (this.height_ + this.anchor_[0]) +
  1043. 'px;');
  1044. } else {
  1045. style.push('height:' + this.height_ + 'px; line-height:' + this.height_ +
  1046. 'px;');
  1047. }
  1048. if (typeof this.anchor_[1] === 'number' && this.anchor_[1] > 0 &&
  1049. this.anchor_[1] < this.width_) {
  1050. style.push('width:' + (this.width_ - this.anchor_[1]) +
  1051. 'px; padding-left:' + this.anchor_[1] + 'px;');
  1052. } else {
  1053. style.push('width:' + this.width_ + 'px; text-align:center;');
  1054. }
  1055. } else {
  1056. style.push('height:' + this.height_ + 'px; line-height:' +
  1057. this.height_ + 'px; width:' + this.width_ + 'px; text-align:center;');
  1058. }
  1059. var txtColor = this.textColor_ ? this.textColor_ : 'black';
  1060. var txtSize = this.textSize_ ? this.textSize_ : 11;
  1061. style.push('cursor:pointer; top:' + pos.y + 'px; left:' +
  1062. pos.x + 'px; color:' + txtColor + '; position:absolute; font-size:' +
  1063. txtSize + 'px; font-family:Arial,sans-serif; font-weight:bold');
  1064. return style.join('');
  1065. };
  1066. // Export Symbols for Closure
  1067. // If you are not going to compile with closure then you can remove the
  1068. // code below.
  1069. window['MarkerClusterer'] = MarkerClusterer;
  1070. MarkerClusterer.prototype['addMarker'] = MarkerClusterer.prototype.addMarker;
  1071. MarkerClusterer.prototype['addMarkers'] = MarkerClusterer.prototype.addMarkers;
  1072. MarkerClusterer.prototype['clearMarkers'] =
  1073. MarkerClusterer.prototype.clearMarkers;
  1074. MarkerClusterer.prototype['fitMapToMarkers'] =
  1075. MarkerClusterer.prototype.fitMapToMarkers;
  1076. MarkerClusterer.prototype['getCalculator'] =
  1077. MarkerClusterer.prototype.getCalculator;
  1078. MarkerClusterer.prototype['getGridSize'] =
  1079. MarkerClusterer.prototype.getGridSize;
  1080. MarkerClusterer.prototype['getExtendedBounds'] =
  1081. MarkerClusterer.prototype.getExtendedBounds;
  1082. MarkerClusterer.prototype['getMap'] = MarkerClusterer.prototype.getMap;
  1083. MarkerClusterer.prototype['getMarkers'] = MarkerClusterer.prototype.getMarkers;
  1084. MarkerClusterer.prototype['getMaxZoom'] = MarkerClusterer.prototype.getMaxZoom;
  1085. MarkerClusterer.prototype['getStyles'] = MarkerClusterer.prototype.getStyles;
  1086. MarkerClusterer.prototype['getTotalClusters'] =
  1087. MarkerClusterer.prototype.getTotalClusters;
  1088. MarkerClusterer.prototype['getTotalMarkers'] =
  1089. MarkerClusterer.prototype.getTotalMarkers;
  1090. MarkerClusterer.prototype['redraw'] = MarkerClusterer.prototype.redraw;
  1091. MarkerClusterer.prototype['removeMarker'] =
  1092. MarkerClusterer.prototype.removeMarker;
  1093. MarkerClusterer.prototype['removeMarkers'] =
  1094. MarkerClusterer.prototype.removeMarkers;
  1095. MarkerClusterer.prototype['resetViewport'] =
  1096. MarkerClusterer.prototype.resetViewport;
  1097. MarkerClusterer.prototype['repaint'] =
  1098. MarkerClusterer.prototype.repaint;
  1099. MarkerClusterer.prototype['setCalculator'] =
  1100. MarkerClusterer.prototype.setCalculator;
  1101. MarkerClusterer.prototype['setGridSize'] =
  1102. MarkerClusterer.prototype.setGridSize;
  1103. MarkerClusterer.prototype['setMaxZoom'] =
  1104. MarkerClusterer.prototype.setMaxZoom;
  1105. MarkerClusterer.prototype['onAdd'] = MarkerClusterer.prototype.onAdd;
  1106. MarkerClusterer.prototype['draw'] = MarkerClusterer.prototype.draw;
  1107. Cluster.prototype['getCenter'] = Cluster.prototype.getCenter;
  1108. Cluster.prototype['getSize'] = Cluster.prototype.getSize;
  1109. Cluster.prototype['getMarkers'] = Cluster.prototype.getMarkers;
  1110. ClusterIcon.prototype['onAdd'] = ClusterIcon.prototype.onAdd;
  1111. ClusterIcon.prototype['draw'] = ClusterIcon.prototype.draw;
  1112. ClusterIcon.prototype['onRemove'] = ClusterIcon.prototype.onRemove;