markerclusterer.js 50 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633
  1. /**
  2. * @name MarkerClustererPlus for Google Maps V3
  3. * @version 2.1.2 [May 28, 2014]
  4. * @author Gary Little
  5. * @fileoverview
  6. * The library creates and manages per-zoom-level clusters for large amounts of markers.
  7. * <p>
  8. * This is an enhanced V3 implementation of the
  9. * <a href="http://gmaps-utility-library-dev.googlecode.com/svn/tags/markerclusterer/"
  10. * >V2 MarkerClusterer</a> by Xiaoxi Wu. It is based on the
  11. * <a href="http://google-maps-utility-library-v3.googlecode.com/svn/tags/markerclusterer/"
  12. * >V3 MarkerClusterer</a> port by Luke Mahe. MarkerClustererPlus was created by Gary Little.
  13. * <p>
  14. * v2.0 release: MarkerClustererPlus v2.0 is backward compatible with MarkerClusterer v1.0. It
  15. * adds support for the <code>ignoreHidden</code>, <code>title</code>, <code>batchSizeIE</code>,
  16. * and <code>calculator</code> properties as well as support for four more events. It also allows
  17. * greater control over the styling of the text that appears on the cluster marker. The
  18. * documentation has been significantly improved and the overall code has been simplified and
  19. * polished. Very large numbers of markers can now be managed without causing Javascript timeout
  20. * errors on Internet Explorer. Note that the name of the <code>clusterclick</code> event has been
  21. * deprecated. The new name is <code>click</code>, so please change your application code now.
  22. */
  23. /**
  24. * Licensed under the Apache License, Version 2.0 (the "License");
  25. * you may not use this file except in compliance with the License.
  26. * You may obtain a copy of the License at
  27. *
  28. * http://www.apache.org/licenses/LICENSE-2.0
  29. *
  30. * Unless required by applicable law or agreed to in writing, software
  31. * distributed under the License is distributed on an "AS IS" BASIS,
  32. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  33. * See the License for the specific language governing permissions and
  34. * limitations under the License.
  35. */
  36. /**
  37. * @name ClusterIconStyle
  38. * @class This class represents the object for values in the <code>styles</code> array passed
  39. * to the {@link MarkerClusterer} constructor. The element in this array that is used to
  40. * style the cluster icon is determined by calling the <code>calculator</code> function.
  41. *
  42. * @property {string} url The URL of the cluster icon image file. Required.
  43. * @property {number} height The display height (in pixels) of the cluster icon. Required.
  44. * @property {number} width The display width (in pixels) of the cluster icon. Required.
  45. * @property {Array} [anchorText] The position (in pixels) from the center of the cluster icon to
  46. * where the text label is to be centered and drawn. The format is <code>[yoffset, xoffset]</code>
  47. * where <code>yoffset</code> increases as you go down from center and <code>xoffset</code>
  48. * increases to the right of center. The default is <code>[0, 0]</code>.
  49. * @property {Array} [anchorIcon] The anchor position (in pixels) of the cluster icon. This is the
  50. * spot on the cluster icon that is to be aligned with the cluster position. The format is
  51. * <code>[yoffset, xoffset]</code> where <code>yoffset</code> increases as you go down and
  52. * <code>xoffset</code> increases to the right of the top-left corner of the icon. The default
  53. * anchor position is the center of the cluster icon.
  54. * @property {string} [textColor="black"] The color of the label text shown on the
  55. * cluster icon.
  56. * @property {number} [textSize=11] The size (in pixels) of the label text shown on the
  57. * cluster icon.
  58. * @property {string} [textDecoration="none"] The value of the CSS <code>text-decoration</code>
  59. * property for the label text shown on the cluster icon.
  60. * @property {string} [fontWeight="bold"] The value of the CSS <code>font-weight</code>
  61. * property for the label text shown on the cluster icon.
  62. * @property {string} [fontStyle="normal"] The value of the CSS <code>font-style</code>
  63. * property for the label text shown on the cluster icon.
  64. * @property {string} [fontFamily="Arial,sans-serif"] The value of the CSS <code>font-family</code>
  65. * property for the label text shown on the cluster icon.
  66. * @property {string} [backgroundPosition="0 0"] The position of the cluster icon image
  67. * within the image defined by <code>url</code>. The format is <code>"xpos ypos"</code>
  68. * (the same format as for the CSS <code>background-position</code> property). You must set
  69. * this property appropriately when the image defined by <code>url</code> represents a sprite
  70. * containing multiple images. Note that the position <i>must</i> be specified in px units.
  71. */
  72. /**
  73. * @name ClusterIconInfo
  74. * @class This class is an object containing general information about a cluster icon. This is
  75. * the object that a <code>calculator</code> function returns.
  76. *
  77. * @property {string} text The text of the label to be shown on the cluster icon.
  78. * @property {number} index The index plus 1 of the element in the <code>styles</code>
  79. * array to be used to style the cluster icon.
  80. * @property {string} title The tooltip to display when the mouse moves over the cluster icon.
  81. * If this value is <code>undefined</code> or <code>""</code>, <code>title</code> is set to the
  82. * value of the <code>title</code> property passed to the MarkerClusterer.
  83. */
  84. /**
  85. * A cluster icon.
  86. *
  87. * @constructor
  88. * @extends google.maps.OverlayView
  89. * @param {Cluster} cluster The cluster with which the icon is to be associated.
  90. * @param {Array} [styles] An array of {@link ClusterIconStyle} defining the cluster icons
  91. * to use for various cluster sizes.
  92. * @private
  93. */
  94. function ClusterIcon(cluster, styles) {
  95. cluster.getMarkerClusterer().extend(ClusterIcon, google.maps.OverlayView);
  96. this.cluster_ = cluster;
  97. this.className_ = cluster.getMarkerClusterer().getClusterClass();
  98. this.styles_ = styles;
  99. this.center_ = null;
  100. this.div_ = null;
  101. this.sums_ = null;
  102. this.visible_ = false;
  103. this.setMap(cluster.getMap()); // Note: this causes onAdd to be called
  104. }
  105. /**
  106. * Adds the icon to the DOM.
  107. */
  108. ClusterIcon.prototype.onAdd = function () {
  109. var cClusterIcon = this;
  110. var cMouseDownInCluster;
  111. var cDraggingMapByCluster;
  112. this.div_ = document.createElement("div");
  113. this.div_.className = this.className_;
  114. if (this.visible_) {
  115. this.show();
  116. }
  117. this.getPanes().overlayMouseTarget.appendChild(this.div_);
  118. // Fix for Issue 157
  119. this.boundsChangedListener_ = google.maps.event.addListener(this.getMap(), "bounds_changed", function () {
  120. cDraggingMapByCluster = cMouseDownInCluster;
  121. });
  122. google.maps.event.addDomListener(this.div_, "mousedown", function () {
  123. cMouseDownInCluster = true;
  124. cDraggingMapByCluster = false;
  125. });
  126. google.maps.event.addDomListener(this.div_, "click", function (e) {
  127. cMouseDownInCluster = false;
  128. if (!cDraggingMapByCluster) {
  129. var theBounds;
  130. var mz;
  131. var mc = cClusterIcon.cluster_.getMarkerClusterer();
  132. /**
  133. * This event is fired when a cluster marker is clicked.
  134. * @name MarkerClusterer#click
  135. * @param {Cluster} c The cluster that was clicked.
  136. * @event
  137. */
  138. google.maps.event.trigger(mc, "click", cClusterIcon.cluster_);
  139. google.maps.event.trigger(mc, "clusterclick", cClusterIcon.cluster_); // deprecated name
  140. // The default click handler follows. Disable it by setting
  141. // the zoomOnClick property to false.
  142. if (mc.getZoomOnClick()) {
  143. // Zoom into the cluster.
  144. mz = mc.getMaxZoom();
  145. theBounds = cClusterIcon.cluster_.getBounds();
  146. mc.getMap().fitBounds(theBounds);
  147. // There is a fix for Issue 170 here:
  148. setTimeout(function () {
  149. mc.getMap().fitBounds(theBounds);
  150. // Don't zoom beyond the max zoom level
  151. if (mz !== null && (mc.getMap().getZoom() > mz)) {
  152. mc.getMap().setZoom(mz + 1);
  153. }
  154. }, 100);
  155. }
  156. // Prevent event propagation to the map:
  157. e.cancelBubble = true;
  158. if (e.stopPropagation) {
  159. e.stopPropagation();
  160. }
  161. }
  162. });
  163. google.maps.event.addDomListener(this.div_, "mouseover", function () {
  164. var mc = cClusterIcon.cluster_.getMarkerClusterer();
  165. /**
  166. * This event is fired when the mouse moves over a cluster marker.
  167. * @name MarkerClusterer#mouseover
  168. * @param {Cluster} c The cluster that the mouse moved over.
  169. * @event
  170. */
  171. google.maps.event.trigger(mc, "mouseover", cClusterIcon.cluster_);
  172. });
  173. google.maps.event.addDomListener(this.div_, "mouseout", function () {
  174. var mc = cClusterIcon.cluster_.getMarkerClusterer();
  175. /**
  176. * This event is fired when the mouse moves out of a cluster marker.
  177. * @name MarkerClusterer#mouseout
  178. * @param {Cluster} c The cluster that the mouse moved out of.
  179. * @event
  180. */
  181. google.maps.event.trigger(mc, "mouseout", cClusterIcon.cluster_);
  182. });
  183. };
  184. /**
  185. * Removes the icon from the DOM.
  186. */
  187. ClusterIcon.prototype.onRemove = function () {
  188. if (this.div_ && this.div_.parentNode) {
  189. this.hide();
  190. google.maps.event.removeListener(this.boundsChangedListener_);
  191. google.maps.event.clearInstanceListeners(this.div_);
  192. this.div_.parentNode.removeChild(this.div_);
  193. this.div_ = null;
  194. }
  195. };
  196. /**
  197. * Draws the icon.
  198. */
  199. ClusterIcon.prototype.draw = function () {
  200. if (this.visible_) {
  201. var pos = this.getPosFromLatLng_(this.center_);
  202. this.div_.style.top = pos.y + "px";
  203. this.div_.style.left = pos.x + "px";
  204. }
  205. };
  206. /**
  207. * Hides the icon.
  208. */
  209. ClusterIcon.prototype.hide = function () {
  210. if (this.div_) {
  211. this.div_.style.display = "none";
  212. }
  213. this.visible_ = false;
  214. };
  215. /**
  216. * Positions and shows the icon.
  217. */
  218. ClusterIcon.prototype.show = function () {
  219. if (this.div_) {
  220. var img = "";
  221. // NOTE: values must be specified in px units
  222. var bp = this.backgroundPosition_.split(" ");
  223. var spriteH = parseInt(bp[0].replace(/^\s+|\s+$/g, ""), 10);
  224. var spriteV = parseInt(bp[1].replace(/^\s+|\s+$/g, ""), 10);
  225. var pos = this.getPosFromLatLng_(this.center_);
  226. this.div_.style.cssText = this.createCss(pos);
  227. img = "<img src='" + this.url_ + "' style='position: absolute; top: " + spriteV + "px; left: " + spriteH + "px; ";
  228. if (!this.cluster_.getMarkerClusterer().enableRetinaIcons_) {
  229. img += "clip: rect(" + (-1 * spriteV) + "px, " + ((-1 * spriteH) + this.width_) + "px, " +
  230. ((-1 * spriteV) + this.height_) + "px, " + (-1 * spriteH) + "px);";
  231. }
  232. img += "'>";
  233. this.div_.innerHTML = img + "<div style='" +
  234. "position: absolute;" +
  235. "top: " + this.anchorText_[0] + "px;" +
  236. "left: " + this.anchorText_[1] + "px;" +
  237. "color: " + this.textColor_ + ";" +
  238. "font-size: " + this.textSize_ + "px;" +
  239. "font-family: " + this.fontFamily_ + ";" +
  240. "font-weight: " + this.fontWeight_ + ";" +
  241. "font-style: " + this.fontStyle_ + ";" +
  242. "text-decoration: " + this.textDecoration_ + ";" +
  243. "text-align: center;" +
  244. "width: " + this.width_ + "px;" +
  245. "line-height:" + this.height_ + "px;" +
  246. "'>" + this.sums_.text + "</div>";
  247. if (typeof this.sums_.title === "undefined" || this.sums_.title === "") {
  248. this.div_.title = this.cluster_.getMarkerClusterer().getTitle();
  249. } else {
  250. this.div_.title = this.sums_.title;
  251. }
  252. this.div_.style.display = "";
  253. }
  254. this.visible_ = true;
  255. };
  256. /**
  257. * Sets the icon styles to the appropriate element in the styles array.
  258. *
  259. * @param {ClusterIconInfo} sums The icon label text and styles index.
  260. */
  261. ClusterIcon.prototype.useStyle = function (sums) {
  262. this.sums_ = sums;
  263. var index = Math.max(0, sums.index - 1);
  264. index = Math.min(this.styles_.length - 1, index);
  265. var style = this.styles_[index];
  266. this.url_ = style.url;
  267. this.height_ = style.height;
  268. this.width_ = style.width;
  269. this.anchorText_ = style.anchorText || [0, 0];
  270. this.anchorIcon_ = style.anchorIcon || [parseInt(this.height_ / 2, 10), parseInt(this.width_ / 2, 10)];
  271. this.textColor_ = style.textColor || "black";
  272. this.textSize_ = style.textSize || 11;
  273. this.textDecoration_ = style.textDecoration || "none";
  274. this.fontWeight_ = style.fontWeight || "bold";
  275. this.fontStyle_ = style.fontStyle || "normal";
  276. this.fontFamily_ = style.fontFamily || "Arial,sans-serif";
  277. this.backgroundPosition_ = style.backgroundPosition || "0 0";
  278. };
  279. /**
  280. * Sets the position at which to center the icon.
  281. *
  282. * @param {google.maps.LatLng} center The latlng to set as the center.
  283. */
  284. ClusterIcon.prototype.setCenter = function (center) {
  285. this.center_ = center;
  286. };
  287. /**
  288. * Creates the cssText style parameter based on the position of the icon.
  289. *
  290. * @param {google.maps.Point} pos The position of the icon.
  291. * @return {string} The CSS style text.
  292. */
  293. ClusterIcon.prototype.createCss = function (pos) {
  294. var style = [];
  295. style.push("cursor: pointer;");
  296. style.push("position: absolute; top: " + pos.y + "px; left: " + pos.x + "px;");
  297. style.push("width: " + this.width_ + "px; height: " + this.height_ + "px;");
  298. return style.join("");
  299. };
  300. /**
  301. * Returns the position at which to place the DIV depending on the latlng.
  302. *
  303. * @param {google.maps.LatLng} latlng The position in latlng.
  304. * @return {google.maps.Point} The position in pixels.
  305. */
  306. ClusterIcon.prototype.getPosFromLatLng_ = function (latlng) {
  307. var pos = this.getProjection().fromLatLngToDivPixel(latlng);
  308. pos.x -= this.anchorIcon_[1];
  309. pos.y -= this.anchorIcon_[0];
  310. pos.x = parseInt(pos.x, 10);
  311. pos.y = parseInt(pos.y, 10);
  312. return pos;
  313. };
  314. /**
  315. * Creates a single cluster that manages a group of proximate markers.
  316. * Used internally, do not call this constructor directly.
  317. * @constructor
  318. * @param {MarkerClusterer} mc The <code>MarkerClusterer</code> object with which this
  319. * cluster is associated.
  320. */
  321. function Cluster(mc) {
  322. this.markerClusterer_ = mc;
  323. this.map_ = mc.getMap();
  324. this.gridSize_ = mc.getGridSize();
  325. this.minClusterSize_ = mc.getMinimumClusterSize();
  326. this.averageCenter_ = mc.getAverageCenter();
  327. this.markers_ = [];
  328. this.center_ = null;
  329. this.bounds_ = null;
  330. this.clusterIcon_ = new ClusterIcon(this, mc.getStyles());
  331. }
  332. /**
  333. * Returns the number of markers managed by the cluster. You can call this from
  334. * a <code>click</code>, <code>mouseover</code>, or <code>mouseout</code> event handler
  335. * for the <code>MarkerClusterer</code> object.
  336. *
  337. * @return {number} The number of markers in the cluster.
  338. */
  339. Cluster.prototype.getSize = function () {
  340. return this.markers_.length;
  341. };
  342. /**
  343. * Returns the array of markers managed by the cluster. You can call this from
  344. * a <code>click</code>, <code>mouseover</code>, or <code>mouseout</code> event handler
  345. * for the <code>MarkerClusterer</code> object.
  346. *
  347. * @return {Array} The array of markers in the cluster.
  348. */
  349. Cluster.prototype.getMarkers = function () {
  350. return this.markers_;
  351. };
  352. /**
  353. * Returns the center of the cluster. You can call this from
  354. * a <code>click</code>, <code>mouseover</code>, or <code>mouseout</code> event handler
  355. * for the <code>MarkerClusterer</code> object.
  356. *
  357. * @return {google.maps.LatLng} The center of the cluster.
  358. */
  359. Cluster.prototype.getCenter = function () {
  360. return this.center_;
  361. };
  362. /**
  363. * Returns the map with which the cluster is associated.
  364. *
  365. * @return {google.maps.Map} The map.
  366. * @ignore
  367. */
  368. Cluster.prototype.getMap = function () {
  369. return this.map_;
  370. };
  371. /**
  372. * Returns the <code>MarkerClusterer</code> object with which the cluster is associated.
  373. *
  374. * @return {MarkerClusterer} The associated marker clusterer.
  375. * @ignore
  376. */
  377. Cluster.prototype.getMarkerClusterer = function () {
  378. return this.markerClusterer_;
  379. };
  380. /**
  381. * Returns the bounds of the cluster.
  382. *
  383. * @return {google.maps.LatLngBounds} the cluster bounds.
  384. * @ignore
  385. */
  386. Cluster.prototype.getBounds = function () {
  387. var i;
  388. var bounds = new google.maps.LatLngBounds(this.center_, this.center_);
  389. var markers = this.getMarkers();
  390. for (i = 0; i < markers.length; i++) {
  391. bounds.extend(markers[i].getPosition());
  392. }
  393. return bounds;
  394. };
  395. /**
  396. * Removes the cluster from the map.
  397. *
  398. * @ignore
  399. */
  400. Cluster.prototype.remove = function () {
  401. this.clusterIcon_.setMap(null);
  402. this.markers_ = [];
  403. delete this.markers_;
  404. };
  405. /**
  406. * Adds a marker to the cluster.
  407. *
  408. * @param {google.maps.Marker} marker The marker to be added.
  409. * @return {boolean} True if the marker was added.
  410. * @ignore
  411. */
  412. Cluster.prototype.addMarker = function (marker) {
  413. var i;
  414. var mCount;
  415. var mz;
  416. if (this.isMarkerAlreadyAdded_(marker)) {
  417. return false;
  418. }
  419. if (!this.center_) {
  420. this.center_ = marker.getPosition();
  421. this.calculateBounds_();
  422. } else {
  423. if (this.averageCenter_) {
  424. var l = this.markers_.length + 1;
  425. var lat = (this.center_.lat() * (l - 1) + marker.getPosition().lat()) / l;
  426. var lng = (this.center_.lng() * (l - 1) + marker.getPosition().lng()) / l;
  427. this.center_ = new google.maps.LatLng(lat, lng);
  428. this.calculateBounds_();
  429. }
  430. }
  431. marker.isAdded = true;
  432. this.markers_.push(marker);
  433. mCount = this.markers_.length;
  434. mz = this.markerClusterer_.getMaxZoom();
  435. if (mz !== null && this.map_.getZoom() > mz) {
  436. // Zoomed in past max zoom, so show the marker.
  437. if (marker.getMap() !== this.map_) {
  438. marker.setMap(this.map_);
  439. }
  440. } else if (mCount < this.minClusterSize_) {
  441. // Min cluster size not reached so show the marker.
  442. if (marker.getMap() !== this.map_) {
  443. marker.setMap(this.map_);
  444. }
  445. } else if (mCount === this.minClusterSize_) {
  446. // Hide the markers that were showing.
  447. for (i = 0; i < mCount; i++) {
  448. this.markers_[i].setMap(null);
  449. }
  450. } else {
  451. marker.setMap(null);
  452. }
  453. this.updateIcon_();
  454. return true;
  455. };
  456. /**
  457. * Determines if a marker lies within the cluster's bounds.
  458. *
  459. * @param {google.maps.Marker} marker The marker to check.
  460. * @return {boolean} True if the marker lies in the bounds.
  461. * @ignore
  462. */
  463. Cluster.prototype.isMarkerInClusterBounds = function (marker) {
  464. return this.bounds_.contains(marker.getPosition());
  465. };
  466. /**
  467. * Calculates the extended bounds of the cluster with the grid.
  468. */
  469. Cluster.prototype.calculateBounds_ = function () {
  470. var bounds = new google.maps.LatLngBounds(this.center_, this.center_);
  471. this.bounds_ = this.markerClusterer_.getExtendedBounds(bounds);
  472. };
  473. /**
  474. * Updates the cluster icon.
  475. */
  476. Cluster.prototype.updateIcon_ = function () {
  477. var mCount = this.markers_.length;
  478. var mz = this.markerClusterer_.getMaxZoom();
  479. if (mz !== null && this.map_.getZoom() > mz) {
  480. this.clusterIcon_.hide();
  481. return;
  482. }
  483. if (mCount < this.minClusterSize_) {
  484. // Min cluster size not yet reached.
  485. this.clusterIcon_.hide();
  486. return;
  487. }
  488. var numStyles = this.markerClusterer_.getStyles().length;
  489. var sums = this.markerClusterer_.getCalculator()(this.markers_, numStyles);
  490. this.clusterIcon_.setCenter(this.center_);
  491. this.clusterIcon_.useStyle(sums);
  492. this.clusterIcon_.show();
  493. };
  494. /**
  495. * Determines if a marker has already been added to the cluster.
  496. *
  497. * @param {google.maps.Marker} marker The marker to check.
  498. * @return {boolean} True if the marker has already been added.
  499. */
  500. Cluster.prototype.isMarkerAlreadyAdded_ = function (marker) {
  501. var i;
  502. if (this.markers_.indexOf) {
  503. return this.markers_.indexOf(marker) !== -1;
  504. } else {
  505. for (i = 0; i < this.markers_.length; i++) {
  506. if (marker === this.markers_[i]) {
  507. return true;
  508. }
  509. }
  510. }
  511. return false;
  512. };
  513. /**
  514. * @name MarkerClustererOptions
  515. * @class This class represents the optional parameter passed to
  516. * the {@link MarkerClusterer} constructor.
  517. * @property {number} [gridSize=60] The grid size of a cluster in pixels. The grid is a square.
  518. * @property {number} [maxZoom=null] The maximum zoom level at which clustering is enabled or
  519. * <code>null</code> if clustering is to be enabled at all zoom levels.
  520. * @property {boolean} [zoomOnClick=true] Whether to zoom the map when a cluster marker is
  521. * clicked. You may want to set this to <code>false</code> if you have installed a handler
  522. * for the <code>click</code> event and it deals with zooming on its own.
  523. * @property {boolean} [averageCenter=false] Whether the position of a cluster marker should be
  524. * the average position of all markers in the cluster. If set to <code>false</code>, the
  525. * cluster marker is positioned at the location of the first marker added to the cluster.
  526. * @property {number} [minimumClusterSize=2] The minimum number of markers needed in a cluster
  527. * before the markers are hidden and a cluster marker appears.
  528. * @property {boolean} [ignoreHidden=false] Whether to ignore hidden markers in clusters. You
  529. * may want to set this to <code>true</code> to ensure that hidden markers are not included
  530. * in the marker count that appears on a cluster marker (this count is the value of the
  531. * <code>text</code> property of the result returned by the default <code>calculator</code>).
  532. * If set to <code>true</code> and you change the visibility of a marker being clustered, be
  533. * sure to also call <code>MarkerClusterer.repaint()</code>.
  534. * @property {string} [title=""] The tooltip to display when the mouse moves over a cluster
  535. * marker. (Alternatively, you can use a custom <code>calculator</code> function to specify a
  536. * different tooltip for each cluster marker.)
  537. * @property {function} [calculator=MarkerClusterer.CALCULATOR] The function used to determine
  538. * the text to be displayed on a cluster marker and the index indicating which style to use
  539. * for the cluster marker. The input parameters for the function are (1) the array of markers
  540. * represented by a cluster marker and (2) the number of cluster icon styles. It returns a
  541. * {@link ClusterIconInfo} object. The default <code>calculator</code> returns a
  542. * <code>text</code> property which is the number of markers in the cluster and an
  543. * <code>index</code> property which is one higher than the lowest integer such that
  544. * <code>10^i</code> exceeds the number of markers in the cluster, or the size of the styles
  545. * array, whichever is less. The <code>styles</code> array element used has an index of
  546. * <code>index</code> minus 1. For example, the default <code>calculator</code> returns a
  547. * <code>text</code> value of <code>"125"</code> and an <code>index</code> of <code>3</code>
  548. * for a cluster icon representing 125 markers so the element used in the <code>styles</code>
  549. * array is <code>2</code>. A <code>calculator</code> may also return a <code>title</code>
  550. * property that contains the text of the tooltip to be used for the cluster marker. If
  551. * <code>title</code> is not defined, the tooltip is set to the value of the <code>title</code>
  552. * property for the MarkerClusterer.
  553. * @property {string} [clusterClass="cluster"] The name of the CSS class defining general styles
  554. * for the cluster markers. Use this class to define CSS styles that are not set up by the code
  555. * that processes the <code>styles</code> array.
  556. * @property {Array} [styles] An array of {@link ClusterIconStyle} elements defining the styles
  557. * of the cluster markers to be used. The element to be used to style a given cluster marker
  558. * is determined by the function defined by the <code>calculator</code> property.
  559. * The default is an array of {@link ClusterIconStyle} elements whose properties are derived
  560. * from the values for <code>imagePath</code>, <code>imageExtension</code>, and
  561. * <code>imageSizes</code>.
  562. * @property {boolean} [enableRetinaIcons=false] Whether to allow the use of cluster icons that
  563. * have sizes that are some multiple (typically double) of their actual display size. Icons such
  564. * as these look better when viewed on high-resolution monitors such as Apple's Retina displays.
  565. * Note: if this property is <code>true</code>, sprites cannot be used as cluster icons.
  566. * @property {number} [batchSize=MarkerClusterer.BATCH_SIZE] Set this property to the
  567. * number of markers to be processed in a single batch when using a browser other than
  568. * Internet Explorer (for Internet Explorer, use the batchSizeIE property instead).
  569. * @property {number} [batchSizeIE=MarkerClusterer.BATCH_SIZE_IE] When Internet Explorer is
  570. * being used, markers are processed in several batches with a small delay inserted between
  571. * each batch in an attempt to avoid Javascript timeout errors. Set this property to the
  572. * number of markers to be processed in a single batch; select as high a number as you can
  573. * without causing a timeout error in the browser. This number might need to be as low as 100
  574. * if 15,000 markers are being managed, for example.
  575. * @property {string} [imagePath=MarkerClusterer.IMAGE_PATH]
  576. * The full URL of the root name of the group of image files to use for cluster icons.
  577. * The complete file name is of the form <code>imagePath</code>n.<code>imageExtension</code>
  578. * where n is the image file number (1, 2, etc.).
  579. * @property {string} [imageExtension=MarkerClusterer.IMAGE_EXTENSION]
  580. * The extension name for the cluster icon image files (e.g., <code>"png"</code> or
  581. * <code>"jpg"</code>).
  582. * @property {Array} [imageSizes=MarkerClusterer.IMAGE_SIZES]
  583. * An array of numbers containing the widths of the group of
  584. * <code>imagePath</code>n.<code>imageExtension</code> image files.
  585. * (The images are assumed to be square.)
  586. */
  587. /**
  588. * Creates a MarkerClusterer object with the options specified in {@link MarkerClustererOptions}.
  589. * @constructor
  590. * @extends google.maps.OverlayView
  591. * @param {google.maps.Map} map The Google map to attach to.
  592. * @param {Array.<google.maps.Marker>} [opt_markers] The markers to be added to the cluster.
  593. * @param {MarkerClustererOptions} [opt_options] The optional parameters.
  594. */
  595. function MarkerClusterer(map, opt_markers, opt_options) {
  596. // MarkerClusterer implements google.maps.OverlayView interface. We use the
  597. // extend function to extend MarkerClusterer with google.maps.OverlayView
  598. // because it might not always be available when the code is defined so we
  599. // look for it at the last possible moment. If it doesn't exist now then
  600. // there is no point going ahead :)
  601. this.extend(MarkerClusterer, google.maps.OverlayView);
  602. opt_markers = opt_markers || [];
  603. opt_options = opt_options || {};
  604. this.markers_ = [];
  605. this.clusters_ = [];
  606. this.listeners_ = [];
  607. this.activeMap_ = null;
  608. this.ready_ = false;
  609. this.gridSize_ = opt_options.gridSize || 60;
  610. this.minClusterSize_ = opt_options.minimumClusterSize || 2;
  611. this.maxZoom_ = opt_options.maxZoom || null;
  612. this.styles_ = opt_options.styles || [];
  613. this.title_ = opt_options.title || "";
  614. this.zoomOnClick_ = true;
  615. if (opt_options.zoomOnClick !== undefined) {
  616. this.zoomOnClick_ = opt_options.zoomOnClick;
  617. }
  618. this.averageCenter_ = false;
  619. if (opt_options.averageCenter !== undefined) {
  620. this.averageCenter_ = opt_options.averageCenter;
  621. }
  622. this.ignoreHidden_ = false;
  623. if (opt_options.ignoreHidden !== undefined) {
  624. this.ignoreHidden_ = opt_options.ignoreHidden;
  625. }
  626. this.enableRetinaIcons_ = false;
  627. if (opt_options.enableRetinaIcons !== undefined) {
  628. this.enableRetinaIcons_ = opt_options.enableRetinaIcons;
  629. }
  630. this.imagePath_ = opt_options.imagePath || MarkerClusterer.IMAGE_PATH;
  631. this.imageExtension_ = opt_options.imageExtension || MarkerClusterer.IMAGE_EXTENSION;
  632. this.imageSizes_ = opt_options.imageSizes || MarkerClusterer.IMAGE_SIZES;
  633. this.calculator_ = opt_options.calculator || MarkerClusterer.CALCULATOR;
  634. this.batchSize_ = opt_options.batchSize || MarkerClusterer.BATCH_SIZE;
  635. this.batchSizeIE_ = opt_options.batchSizeIE || MarkerClusterer.BATCH_SIZE_IE;
  636. this.clusterClass_ = opt_options.clusterClass || "cluster";
  637. if (navigator.userAgent.toLowerCase().indexOf("msie") !== -1) {
  638. // Try to avoid IE timeout when processing a huge number of markers:
  639. this.batchSize_ = this.batchSizeIE_;
  640. }
  641. this.setupStyles_();
  642. this.addMarkers(opt_markers, true);
  643. this.setMap(map); // Note: this causes onAdd to be called
  644. }
  645. /**
  646. * Implementation of the onAdd interface method.
  647. * @ignore
  648. */
  649. MarkerClusterer.prototype.onAdd = function () {
  650. var cMarkerClusterer = this;
  651. this.activeMap_ = this.getMap();
  652. this.ready_ = true;
  653. this.repaint();
  654. // Add the map event listeners
  655. this.listeners_ = [
  656. google.maps.event.addListener(this.getMap(), "zoom_changed", function () {
  657. cMarkerClusterer.resetViewport_(false);
  658. // Workaround for this Google bug: when map is at level 0 and "-" of
  659. // zoom slider is clicked, a "zoom_changed" event is fired even though
  660. // the map doesn't zoom out any further. In this situation, no "idle"
  661. // event is triggered so the cluster markers that have been removed
  662. // do not get redrawn. Same goes for a zoom in at maxZoom.
  663. if (this.getZoom() === (this.get("minZoom") || 0) || this.getZoom() === this.get("maxZoom")) {
  664. google.maps.event.trigger(this, "idle");
  665. }
  666. }),
  667. google.maps.event.addListener(this.getMap(), "idle", function () {
  668. cMarkerClusterer.redraw_();
  669. })
  670. ];
  671. };
  672. /**
  673. * Implementation of the onRemove interface method.
  674. * Removes map event listeners and all cluster icons from the DOM.
  675. * All managed markers are also put back on the map.
  676. * @ignore
  677. */
  678. MarkerClusterer.prototype.onRemove = function () {
  679. var i;
  680. // Put all the managed markers back on the map:
  681. for (i = 0; i < this.markers_.length; i++) {
  682. if (this.markers_[i].getMap() !== this.activeMap_) {
  683. this.markers_[i].setMap(this.activeMap_);
  684. }
  685. }
  686. // Remove all clusters:
  687. for (i = 0; i < this.clusters_.length; i++) {
  688. this.clusters_[i].remove();
  689. }
  690. this.clusters_ = [];
  691. // Remove map event listeners:
  692. for (i = 0; i < this.listeners_.length; i++) {
  693. google.maps.event.removeListener(this.listeners_[i]);
  694. }
  695. this.listeners_ = [];
  696. this.activeMap_ = null;
  697. this.ready_ = false;
  698. };
  699. /**
  700. * Implementation of the draw interface method.
  701. * @ignore
  702. */
  703. MarkerClusterer.prototype.draw = function () {};
  704. /**
  705. * Sets up the styles object.
  706. */
  707. MarkerClusterer.prototype.setupStyles_ = function () {
  708. var i, size;
  709. if (this.styles_.length > 0) {
  710. return;
  711. }
  712. for (i = 0; i < this.imageSizes_.length; i++) {
  713. size = this.imageSizes_[i];
  714. this.styles_.push({
  715. url: this.imagePath_ + (i + 1) + "." + this.imageExtension_,
  716. height: size,
  717. width: size
  718. });
  719. }
  720. };
  721. /**
  722. * Fits the map to the bounds of the markers managed by the clusterer.
  723. */
  724. MarkerClusterer.prototype.fitMapToMarkers = function () {
  725. var i;
  726. var markers = this.getMarkers();
  727. var bounds = new google.maps.LatLngBounds();
  728. for (i = 0; i < markers.length; i++) {
  729. bounds.extend(markers[i].getPosition());
  730. }
  731. this.getMap().fitBounds(bounds);
  732. };
  733. /**
  734. * Returns the value of the <code>gridSize</code> property.
  735. *
  736. * @return {number} The grid size.
  737. */
  738. MarkerClusterer.prototype.getGridSize = function () {
  739. return this.gridSize_;
  740. };
  741. /**
  742. * Sets the value of the <code>gridSize</code> property.
  743. *
  744. * @param {number} gridSize The grid size.
  745. */
  746. MarkerClusterer.prototype.setGridSize = function (gridSize) {
  747. this.gridSize_ = gridSize;
  748. };
  749. /**
  750. * Returns the value of the <code>minimumClusterSize</code> property.
  751. *
  752. * @return {number} The minimum cluster size.
  753. */
  754. MarkerClusterer.prototype.getMinimumClusterSize = function () {
  755. return this.minClusterSize_;
  756. };
  757. /**
  758. * Sets the value of the <code>minimumClusterSize</code> property.
  759. *
  760. * @param {number} minimumClusterSize The minimum cluster size.
  761. */
  762. MarkerClusterer.prototype.setMinimumClusterSize = function (minimumClusterSize) {
  763. this.minClusterSize_ = minimumClusterSize;
  764. };
  765. /**
  766. * Returns the value of the <code>maxZoom</code> property.
  767. *
  768. * @return {number} The maximum zoom level.
  769. */
  770. MarkerClusterer.prototype.getMaxZoom = function () {
  771. return this.maxZoom_;
  772. };
  773. /**
  774. * Sets the value of the <code>maxZoom</code> property.
  775. *
  776. * @param {number} maxZoom The maximum zoom level.
  777. */
  778. MarkerClusterer.prototype.setMaxZoom = function (maxZoom) {
  779. this.maxZoom_ = maxZoom;
  780. };
  781. /**
  782. * Returns the value of the <code>styles</code> property.
  783. *
  784. * @return {Array} The array of styles defining the cluster markers to be used.
  785. */
  786. MarkerClusterer.prototype.getStyles = function () {
  787. return this.styles_;
  788. };
  789. /**
  790. * Sets the value of the <code>styles</code> property.
  791. *
  792. * @param {Array.<ClusterIconStyle>} styles The array of styles to use.
  793. */
  794. MarkerClusterer.prototype.setStyles = function (styles) {
  795. this.styles_ = styles;
  796. };
  797. /**
  798. * Returns the value of the <code>title</code> property.
  799. *
  800. * @return {string} The content of the title text.
  801. */
  802. MarkerClusterer.prototype.getTitle = function () {
  803. return this.title_;
  804. };
  805. /**
  806. * Sets the value of the <code>title</code> property.
  807. *
  808. * @param {string} title The value of the title property.
  809. */
  810. MarkerClusterer.prototype.setTitle = function (title) {
  811. this.title_ = title;
  812. };
  813. /**
  814. * Returns the value of the <code>zoomOnClick</code> property.
  815. *
  816. * @return {boolean} True if zoomOnClick property is set.
  817. */
  818. MarkerClusterer.prototype.getZoomOnClick = function () {
  819. return this.zoomOnClick_;
  820. };
  821. /**
  822. * Sets the value of the <code>zoomOnClick</code> property.
  823. *
  824. * @param {boolean} zoomOnClick The value of the zoomOnClick property.
  825. */
  826. MarkerClusterer.prototype.setZoomOnClick = function (zoomOnClick) {
  827. this.zoomOnClick_ = zoomOnClick;
  828. };
  829. /**
  830. * Returns the value of the <code>averageCenter</code> property.
  831. *
  832. * @return {boolean} True if averageCenter property is set.
  833. */
  834. MarkerClusterer.prototype.getAverageCenter = function () {
  835. return this.averageCenter_;
  836. };
  837. /**
  838. * Sets the value of the <code>averageCenter</code> property.
  839. *
  840. * @param {boolean} averageCenter The value of the averageCenter property.
  841. */
  842. MarkerClusterer.prototype.setAverageCenter = function (averageCenter) {
  843. this.averageCenter_ = averageCenter;
  844. };
  845. /**
  846. * Returns the value of the <code>ignoreHidden</code> property.
  847. *
  848. * @return {boolean} True if ignoreHidden property is set.
  849. */
  850. MarkerClusterer.prototype.getIgnoreHidden = function () {
  851. return this.ignoreHidden_;
  852. };
  853. /**
  854. * Sets the value of the <code>ignoreHidden</code> property.
  855. *
  856. * @param {boolean} ignoreHidden The value of the ignoreHidden property.
  857. */
  858. MarkerClusterer.prototype.setIgnoreHidden = function (ignoreHidden) {
  859. this.ignoreHidden_ = ignoreHidden;
  860. };
  861. /**
  862. * Returns the value of the <code>enableRetinaIcons</code> property.
  863. *
  864. * @return {boolean} True if enableRetinaIcons property is set.
  865. */
  866. MarkerClusterer.prototype.getEnableRetinaIcons = function () {
  867. return this.enableRetinaIcons_;
  868. };
  869. /**
  870. * Sets the value of the <code>enableRetinaIcons</code> property.
  871. *
  872. * @param {boolean} enableRetinaIcons The value of the enableRetinaIcons property.
  873. */
  874. MarkerClusterer.prototype.setEnableRetinaIcons = function (enableRetinaIcons) {
  875. this.enableRetinaIcons_ = enableRetinaIcons;
  876. };
  877. /**
  878. * Returns the value of the <code>imageExtension</code> property.
  879. *
  880. * @return {string} The value of the imageExtension property.
  881. */
  882. MarkerClusterer.prototype.getImageExtension = function () {
  883. return this.imageExtension_;
  884. };
  885. /**
  886. * Sets the value of the <code>imageExtension</code> property.
  887. *
  888. * @param {string} imageExtension The value of the imageExtension property.
  889. */
  890. MarkerClusterer.prototype.setImageExtension = function (imageExtension) {
  891. this.imageExtension_ = imageExtension;
  892. };
  893. /**
  894. * Returns the value of the <code>imagePath</code> property.
  895. *
  896. * @return {string} The value of the imagePath property.
  897. */
  898. MarkerClusterer.prototype.getImagePath = function () {
  899. return this.imagePath_;
  900. };
  901. /**
  902. * Sets the value of the <code>imagePath</code> property.
  903. *
  904. * @param {string} imagePath The value of the imagePath property.
  905. */
  906. MarkerClusterer.prototype.setImagePath = function (imagePath) {
  907. this.imagePath_ = imagePath;
  908. };
  909. /**
  910. * Returns the value of the <code>imageSizes</code> property.
  911. *
  912. * @return {Array} The value of the imageSizes property.
  913. */
  914. MarkerClusterer.prototype.getImageSizes = function () {
  915. return this.imageSizes_;
  916. };
  917. /**
  918. * Sets the value of the <code>imageSizes</code> property.
  919. *
  920. * @param {Array} imageSizes The value of the imageSizes property.
  921. */
  922. MarkerClusterer.prototype.setImageSizes = function (imageSizes) {
  923. this.imageSizes_ = imageSizes;
  924. };
  925. /**
  926. * Returns the value of the <code>calculator</code> property.
  927. *
  928. * @return {function} the value of the calculator property.
  929. */
  930. MarkerClusterer.prototype.getCalculator = function () {
  931. return this.calculator_;
  932. };
  933. /**
  934. * Sets the value of the <code>calculator</code> property.
  935. *
  936. * @param {function(Array.<google.maps.Marker>, number)} calculator The value
  937. * of the calculator property.
  938. */
  939. MarkerClusterer.prototype.setCalculator = function (calculator) {
  940. this.calculator_ = calculator;
  941. };
  942. /**
  943. * Returns the value of the <code>batchSizeIE</code> property.
  944. *
  945. * @return {number} the value of the batchSizeIE property.
  946. */
  947. MarkerClusterer.prototype.getBatchSizeIE = function () {
  948. return this.batchSizeIE_;
  949. };
  950. /**
  951. * Sets the value of the <code>batchSizeIE</code> property.
  952. *
  953. * @param {number} batchSizeIE The value of the batchSizeIE property.
  954. */
  955. MarkerClusterer.prototype.setBatchSizeIE = function (batchSizeIE) {
  956. this.batchSizeIE_ = batchSizeIE;
  957. };
  958. /**
  959. * Returns the value of the <code>clusterClass</code> property.
  960. *
  961. * @return {string} the value of the clusterClass property.
  962. */
  963. MarkerClusterer.prototype.getClusterClass = function () {
  964. return this.clusterClass_;
  965. };
  966. /**
  967. * Sets the value of the <code>clusterClass</code> property.
  968. *
  969. * @param {string} clusterClass The value of the clusterClass property.
  970. */
  971. MarkerClusterer.prototype.setClusterClass = function (clusterClass) {
  972. this.clusterClass_ = clusterClass;
  973. };
  974. /**
  975. * Returns the array of markers managed by the clusterer.
  976. *
  977. * @return {Array} The array of markers managed by the clusterer.
  978. */
  979. MarkerClusterer.prototype.getMarkers = function () {
  980. return this.markers_;
  981. };
  982. /**
  983. * Returns the number of markers managed by the clusterer.
  984. *
  985. * @return {number} The number of markers.
  986. */
  987. MarkerClusterer.prototype.getTotalMarkers = function () {
  988. return this.markers_.length;
  989. };
  990. /**
  991. * Returns the current array of clusters formed by the clusterer.
  992. *
  993. * @return {Array} The array of clusters formed by the clusterer.
  994. */
  995. MarkerClusterer.prototype.getClusters = function () {
  996. return this.clusters_;
  997. };
  998. /**
  999. * Returns the number of clusters formed by the clusterer.
  1000. *
  1001. * @return {number} The number of clusters formed by the clusterer.
  1002. */
  1003. MarkerClusterer.prototype.getTotalClusters = function () {
  1004. return this.clusters_.length;
  1005. };
  1006. /**
  1007. * Adds a marker to the clusterer. The clusters are redrawn unless
  1008. * <code>opt_nodraw</code> is set to <code>true</code>.
  1009. *
  1010. * @param {google.maps.Marker} marker The marker to add.
  1011. * @param {boolean} [opt_nodraw] Set to <code>true</code> to prevent redrawing.
  1012. */
  1013. MarkerClusterer.prototype.addMarker = function (marker, opt_nodraw) {
  1014. this.pushMarkerTo_(marker);
  1015. if (!opt_nodraw) {
  1016. this.redraw_();
  1017. }
  1018. };
  1019. /**
  1020. * Adds an array of markers to the clusterer. The clusters are redrawn unless
  1021. * <code>opt_nodraw</code> is set to <code>true</code>.
  1022. *
  1023. * @param {Array.<google.maps.Marker>} markers The markers to add.
  1024. * @param {boolean} [opt_nodraw] Set to <code>true</code> to prevent redrawing.
  1025. */
  1026. MarkerClusterer.prototype.addMarkers = function (markers, opt_nodraw) {
  1027. var key;
  1028. for (key in markers) {
  1029. if (markers.hasOwnProperty(key)) {
  1030. this.pushMarkerTo_(markers[key]);
  1031. }
  1032. }
  1033. if (!opt_nodraw) {
  1034. this.redraw_();
  1035. }
  1036. };
  1037. /**
  1038. * Pushes a marker to the clusterer.
  1039. *
  1040. * @param {google.maps.Marker} marker The marker to add.
  1041. */
  1042. MarkerClusterer.prototype.pushMarkerTo_ = function (marker) {
  1043. // If the marker is draggable add a listener so we can update the clusters on the dragend:
  1044. if (marker.getDraggable()) {
  1045. var cMarkerClusterer = this;
  1046. google.maps.event.addListener(marker, "dragend", function () {
  1047. if (cMarkerClusterer.ready_) {
  1048. this.isAdded = false;
  1049. cMarkerClusterer.repaint();
  1050. }
  1051. });
  1052. }
  1053. marker.isAdded = false;
  1054. this.markers_.push(marker);
  1055. };
  1056. /**
  1057. * Removes a marker from the cluster. The clusters are redrawn unless
  1058. * <code>opt_nodraw</code> is set to <code>true</code>. Returns <code>true</code> if the
  1059. * marker was removed from the clusterer.
  1060. *
  1061. * @param {google.maps.Marker} marker The marker to remove.
  1062. * @param {boolean} [opt_nodraw] Set to <code>true</code> to prevent redrawing.
  1063. * @return {boolean} True if the marker was removed from the clusterer.
  1064. */
  1065. MarkerClusterer.prototype.removeMarker = function (marker, opt_nodraw) {
  1066. var removed = this.removeMarker_(marker);
  1067. if (!opt_nodraw && removed) {
  1068. this.repaint();
  1069. }
  1070. return removed;
  1071. };
  1072. /**
  1073. * Removes an array of markers from the cluster. The clusters are redrawn unless
  1074. * <code>opt_nodraw</code> is set to <code>true</code>. Returns <code>true</code> if markers
  1075. * were removed from the clusterer.
  1076. *
  1077. * @param {Array.<google.maps.Marker>} markers The markers to remove.
  1078. * @param {boolean} [opt_nodraw] Set to <code>true</code> to prevent redrawing.
  1079. * @return {boolean} True if markers were removed from the clusterer.
  1080. */
  1081. MarkerClusterer.prototype.removeMarkers = function (markers, opt_nodraw) {
  1082. var i, r;
  1083. var removed = false;
  1084. for (i = 0; i < markers.length; i++) {
  1085. r = this.removeMarker_(markers[i]);
  1086. removed = removed || r;
  1087. }
  1088. if (!opt_nodraw && removed) {
  1089. this.repaint();
  1090. }
  1091. return removed;
  1092. };
  1093. /**
  1094. * Removes a marker and returns true if removed, false if not.
  1095. *
  1096. * @param {google.maps.Marker} marker The marker to remove
  1097. * @return {boolean} Whether the marker was removed or not
  1098. */
  1099. MarkerClusterer.prototype.removeMarker_ = function (marker) {
  1100. var i;
  1101. var index = -1;
  1102. if (this.markers_.indexOf) {
  1103. index = this.markers_.indexOf(marker);
  1104. } else {
  1105. for (i = 0; i < this.markers_.length; i++) {
  1106. if (marker === this.markers_[i]) {
  1107. index = i;
  1108. break;
  1109. }
  1110. }
  1111. }
  1112. if (index === -1) {
  1113. // Marker is not in our list of markers, so do nothing:
  1114. return false;
  1115. }
  1116. marker.setMap(null);
  1117. this.markers_.splice(index, 1); // Remove the marker from the list of managed markers
  1118. return true;
  1119. };
  1120. /**
  1121. * Removes all clusters and markers from the map and also removes all markers
  1122. * managed by the clusterer.
  1123. */
  1124. MarkerClusterer.prototype.clearMarkers = function () {
  1125. this.resetViewport_(true);
  1126. this.markers_ = [];
  1127. };
  1128. /**
  1129. * Recalculates and redraws all the marker clusters from scratch.
  1130. * Call this after changing any properties.
  1131. */
  1132. MarkerClusterer.prototype.repaint = function () {
  1133. var oldClusters = this.clusters_.slice();
  1134. this.clusters_ = [];
  1135. this.resetViewport_(false);
  1136. this.redraw_();
  1137. // Remove the old clusters.
  1138. // Do it in a timeout to prevent blinking effect.
  1139. setTimeout(function () {
  1140. var i;
  1141. for (i = 0; i < oldClusters.length; i++) {
  1142. oldClusters[i].remove();
  1143. }
  1144. }, 0);
  1145. };
  1146. /**
  1147. * Returns the current bounds extended by the grid size.
  1148. *
  1149. * @param {google.maps.LatLngBounds} bounds The bounds to extend.
  1150. * @return {google.maps.LatLngBounds} The extended bounds.
  1151. * @ignore
  1152. */
  1153. MarkerClusterer.prototype.getExtendedBounds = function (bounds) {
  1154. var projection = this.getProjection();
  1155. // Turn the bounds into latlng.
  1156. var tr = new google.maps.LatLng(bounds.getNorthEast().lat(),
  1157. bounds.getNorthEast().lng());
  1158. var bl = new google.maps.LatLng(bounds.getSouthWest().lat(),
  1159. bounds.getSouthWest().lng());
  1160. // Convert the points to pixels and the extend out by the grid size.
  1161. var trPix = projection.fromLatLngToDivPixel(tr);
  1162. trPix.x += this.gridSize_;
  1163. trPix.y -= this.gridSize_;
  1164. var blPix = projection.fromLatLngToDivPixel(bl);
  1165. blPix.x -= this.gridSize_;
  1166. blPix.y += this.gridSize_;
  1167. // Convert the pixel points back to LatLng
  1168. var ne = projection.fromDivPixelToLatLng(trPix);
  1169. var sw = projection.fromDivPixelToLatLng(blPix);
  1170. // Extend the bounds to contain the new bounds.
  1171. bounds.extend(ne);
  1172. bounds.extend(sw);
  1173. return bounds;
  1174. };
  1175. /**
  1176. * Redraws all the clusters.
  1177. */
  1178. MarkerClusterer.prototype.redraw_ = function () {
  1179. this.createClusters_(0);
  1180. };
  1181. /**
  1182. * Removes all clusters from the map. The markers are also removed from the map
  1183. * if <code>opt_hide</code> is set to <code>true</code>.
  1184. *
  1185. * @param {boolean} [opt_hide] Set to <code>true</code> to also remove the markers
  1186. * from the map.
  1187. */
  1188. MarkerClusterer.prototype.resetViewport_ = function (opt_hide) {
  1189. var i, marker;
  1190. // Remove all the clusters
  1191. for (i = 0; i < this.clusters_.length; i++) {
  1192. this.clusters_[i].remove();
  1193. }
  1194. this.clusters_ = [];
  1195. // Reset the markers to not be added and to be removed from the map.
  1196. for (i = 0; i < this.markers_.length; i++) {
  1197. marker = this.markers_[i];
  1198. marker.isAdded = false;
  1199. if (opt_hide) {
  1200. marker.setMap(null);
  1201. }
  1202. }
  1203. };
  1204. /**
  1205. * Calculates the distance between two latlng locations in km.
  1206. *
  1207. * @param {google.maps.LatLng} p1 The first lat lng point.
  1208. * @param {google.maps.LatLng} p2 The second lat lng point.
  1209. * @return {number} The distance between the two points in km.
  1210. * @see http://www.movable-type.co.uk/scripts/latlong.html
  1211. */
  1212. MarkerClusterer.prototype.distanceBetweenPoints_ = function (p1, p2) {
  1213. var R = 6371; // Radius of the Earth in km
  1214. var dLat = (p2.lat() - p1.lat()) * Math.PI / 180;
  1215. var dLon = (p2.lng() - p1.lng()) * Math.PI / 180;
  1216. var a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
  1217. Math.cos(p1.lat() * Math.PI / 180) * Math.cos(p2.lat() * Math.PI / 180) *
  1218. Math.sin(dLon / 2) * Math.sin(dLon / 2);
  1219. var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
  1220. var d = R * c;
  1221. return d;
  1222. };
  1223. /**
  1224. * Determines if a marker is contained in a bounds.
  1225. *
  1226. * @param {google.maps.Marker} marker The marker to check.
  1227. * @param {google.maps.LatLngBounds} bounds The bounds to check against.
  1228. * @return {boolean} True if the marker is in the bounds.
  1229. */
  1230. MarkerClusterer.prototype.isMarkerInBounds_ = function (marker, bounds) {
  1231. return bounds.contains(marker.getPosition());
  1232. };
  1233. /**
  1234. * Adds a marker to a cluster, or creates a new cluster.
  1235. *
  1236. * @param {google.maps.Marker} marker The marker to add.
  1237. */
  1238. MarkerClusterer.prototype.addToClosestCluster_ = function (marker) {
  1239. var i, d, cluster, center;
  1240. var distance = 40000; // Some large number
  1241. var clusterToAddTo = null;
  1242. for (i = 0; i < this.clusters_.length; i++) {
  1243. cluster = this.clusters_[i];
  1244. center = cluster.getCenter();
  1245. if (center) {
  1246. d = this.distanceBetweenPoints_(center, marker.getPosition());
  1247. if (d < distance) {
  1248. distance = d;
  1249. clusterToAddTo = cluster;
  1250. }
  1251. }
  1252. }
  1253. if (clusterToAddTo && clusterToAddTo.isMarkerInClusterBounds(marker)) {
  1254. clusterToAddTo.addMarker(marker);
  1255. } else {
  1256. cluster = new Cluster(this);
  1257. cluster.addMarker(marker);
  1258. this.clusters_.push(cluster);
  1259. }
  1260. };
  1261. /**
  1262. * Creates the clusters. This is done in batches to avoid timeout errors
  1263. * in some browsers when there is a huge number of markers.
  1264. *
  1265. * @param {number} iFirst The index of the first marker in the batch of
  1266. * markers to be added to clusters.
  1267. */
  1268. MarkerClusterer.prototype.createClusters_ = function (iFirst) {
  1269. var i, marker;
  1270. var mapBounds;
  1271. var cMarkerClusterer = this;
  1272. if (!this.ready_) {
  1273. return;
  1274. }
  1275. // Cancel previous batch processing if we're working on the first batch:
  1276. if (iFirst === 0) {
  1277. /**
  1278. * This event is fired when the <code>MarkerClusterer</code> begins
  1279. * clustering markers.
  1280. * @name MarkerClusterer#clusteringbegin
  1281. * @param {MarkerClusterer} mc The MarkerClusterer whose markers are being clustered.
  1282. * @event
  1283. */
  1284. google.maps.event.trigger(this, "clusteringbegin", this);
  1285. if (typeof this.timerRefStatic !== "undefined") {
  1286. clearTimeout(this.timerRefStatic);
  1287. delete this.timerRefStatic;
  1288. }
  1289. }
  1290. // Get our current map view bounds.
  1291. // Create a new bounds object so we don't affect the map.
  1292. //
  1293. // See Comments 9 & 11 on Issue 3651 relating to this workaround for a Google Maps bug:
  1294. if (this.getMap().getZoom() > 3) {
  1295. mapBounds = new google.maps.LatLngBounds(this.getMap().getBounds().getSouthWest(),
  1296. this.getMap().getBounds().getNorthEast());
  1297. } else {
  1298. mapBounds = new google.maps.LatLngBounds(new google.maps.LatLng(85.02070771743472, -178.48388434375), new google.maps.LatLng(-85.08136444384544, 178.00048865625));
  1299. }
  1300. var bounds = this.getExtendedBounds(mapBounds);
  1301. var iLast = Math.min(iFirst + this.batchSize_, this.markers_.length);
  1302. for (i = iFirst; i < iLast; i++) {
  1303. marker = this.markers_[i];
  1304. if (!marker.isAdded && this.isMarkerInBounds_(marker, bounds)) {
  1305. if (!this.ignoreHidden_ || (this.ignoreHidden_ && marker.getVisible())) {
  1306. this.addToClosestCluster_(marker);
  1307. }
  1308. }
  1309. }
  1310. if (iLast < this.markers_.length) {
  1311. this.timerRefStatic = setTimeout(function () {
  1312. cMarkerClusterer.createClusters_(iLast);
  1313. }, 0);
  1314. } else {
  1315. delete this.timerRefStatic;
  1316. /**
  1317. * This event is fired when the <code>MarkerClusterer</code> stops
  1318. * clustering markers.
  1319. * @name MarkerClusterer#clusteringend
  1320. * @param {MarkerClusterer} mc The MarkerClusterer whose markers are being clustered.
  1321. * @event
  1322. */
  1323. google.maps.event.trigger(this, "clusteringend", this);
  1324. }
  1325. };
  1326. /**
  1327. * Extends an object's prototype by another's.
  1328. *
  1329. * @param {Object} obj1 The object to be extended.
  1330. * @param {Object} obj2 The object to extend with.
  1331. * @return {Object} The new extended object.
  1332. * @ignore
  1333. */
  1334. MarkerClusterer.prototype.extend = function (obj1, obj2) {
  1335. return (function (object) {
  1336. var property;
  1337. for (property in object.prototype) {
  1338. this.prototype[property] = object.prototype[property];
  1339. }
  1340. return this;
  1341. }).apply(obj1, [obj2]);
  1342. };
  1343. /**
  1344. * The default function for determining the label text and style
  1345. * for a cluster icon.
  1346. *
  1347. * @param {Array.<google.maps.Marker>} markers The array of markers represented by the cluster.
  1348. * @param {number} numStyles The number of marker styles available.
  1349. * @return {ClusterIconInfo} The information resource for the cluster.
  1350. * @constant
  1351. * @ignore
  1352. */
  1353. MarkerClusterer.CALCULATOR = function (markers, numStyles) {
  1354. var index = 0;
  1355. var title = "";
  1356. var count = markers.length.toString();
  1357. var dv = count;
  1358. while (dv !== 0) {
  1359. dv = parseInt(dv / 10, 10);
  1360. index++;
  1361. }
  1362. index = Math.min(index, numStyles);
  1363. return {
  1364. text: count,
  1365. index: index,
  1366. title: title
  1367. };
  1368. };
  1369. /**
  1370. * The number of markers to process in one batch.
  1371. *
  1372. * @type {number}
  1373. * @constant
  1374. */
  1375. MarkerClusterer.BATCH_SIZE = 2000;
  1376. /**
  1377. * The number of markers to process in one batch (IE only).
  1378. *
  1379. * @type {number}
  1380. * @constant
  1381. */
  1382. MarkerClusterer.BATCH_SIZE_IE = 500;
  1383. /**
  1384. * The default root name for the marker cluster images.
  1385. *
  1386. * @type {string}
  1387. * @constant
  1388. */
  1389. MarkerClusterer.IMAGE_PATH = "http://google-maps-utility-library-v3.googlecode.com/svn/trunk/markerclustererplus/images/m";
  1390. /**
  1391. * The default extension name for the marker cluster images.
  1392. *
  1393. * @type {string}
  1394. * @constant
  1395. */
  1396. MarkerClusterer.IMAGE_EXTENSION = "png";
  1397. /**
  1398. * The default array of sizes for the marker cluster images.
  1399. *
  1400. * @type {Array.<number>}
  1401. * @constant
  1402. */
  1403. MarkerClusterer.IMAGE_SIZES = [53, 56, 66, 78, 90];