From 7754e023f2cf2fee9e69ed38a86b65da487919b7 Mon Sep 17 00:00:00 2001 From: Benjamin Wilfing Date: Fri, 8 Feb 2019 16:42:06 +0100 Subject: [PATCH] :bug: Fix zooming/fitBounds issue (see https://github.com/googlemaps/v3-utility-library/issues/437\#issuecomment-400036752) --- dist/angular-google-maps.js | 2054 +++++++++++++++++++++++++++++++---- 1 file changed, 1859 insertions(+), 195 deletions(-) diff --git a/dist/angular-google-maps.js b/dist/angular-google-maps.js index 01c249a50..1d2bff12d 100644 --- a/dist/angular-google-maps.js +++ b/dist/angular-google-maps.js @@ -655,31 +655,31 @@ Nicholas McCready - https://twitter.com/nmccready So it is run to manage the state (cancel, skip, link) as needed. Purpose: The whole point is to check if there is existing async work going on. If so we wait on it. - + arguments: - existingPiecesObj = Queue - sniffedPromise = object wrapper holding a function to a pending (function) promise (promise: fnPromise) with its intended type. - cancelCb = callback which accepts a string, this string is intended to be returned at the end of _async.each iterator - + Where the cancelCb passed msg is 'cancel safe' _async.each will drop out and fall through. Thus canceling the promise gracefully without messing up state. - + Synopsis: - + - Promises have been broken down to 4 states create, update,delete (3 main) and init. (Helps boil down problems in ordering) where (init) is special to indicate that it is one of the first or to allow a create promise to work beyond being after a delete - + - Every Promise that comes in is enqueued and linked to the last promise in the queue. - + - A promise can be skipped or canceled to save cycles. - + Saved Cycles: - Skipped - This will only happen if async work comes in out of order. Where a pending create promise (un-executed) comes in after a delete promise. - Canceled - Where an incoming promise (un-executed promise) is of type delete and the any lastPromise is not a delete type. - - + + NOTE: - You should not muck with existingPieces as its state is dependent on this functional loop. - PromiseQueueManager should not be thought of as a class that has a life expectancy (it has none). It's sole @@ -789,10 +789,10 @@ Nicholas McCready - https://twitter.com/nmccready Author: Nicholas McCready & jfriend00 _async handles things asynchronous-like :), to allow the UI to be free'd to do other things Code taken from http://stackoverflow.com/questions/10344498/best-way-to-iterate-over-an-array-without-blocking-the-ui - + The design of any functionality of _async is to be like lodash/underscore and replicate it but call things asynchronously underneath. Each should be sufficient for most things to be derived from. - + Optional Asynchronous Chunking via promises. */ doChunk = function(collection, chunkSizeOrDontChunk, pauseMilli, chunkCb, pauseCb, overallD, index, _keys) { @@ -5901,7 +5901,7 @@ Original idea from: http://stackoverflow.com/questions/22758950/google-map-drawi being that we cannot tell the difference in Key String vs. a normal value string (TemplateUrl) we will assume that all scope values are string expressions either pointing to a key (propName) or using 'self' to point the model as container/object of interest. - + This may force redundant information into the model, but this appears to be the most flexible approach. */ this.isIconVisibleOnClick = true; @@ -8343,206 +8343,1870 @@ angular.module('uiGmapgoogle-maps.wrapped') //BEGIN REPLACE /* istanbul ignore next */ +function(){ - function ClusterIcon(cluster,styles){cluster.getMarkerClusterer().extend(ClusterIcon,google.maps.OverlayView),this.cluster_=cluster,this.className_=cluster.getMarkerClusterer().getClusterClass(),this.styles_=styles,this.center_=null,this.div_=null,this.sums_=null,this.visible_=!1,this.setMap(cluster.getMap())}function Cluster(mc){this.markerClusterer_=mc,this.map_=mc.getMap(),this.gridSize_=mc.getGridSize(),this.minClusterSize_=mc.getMinimumClusterSize(),this.averageCenter_=mc.getAverageCenter(),this.hideLabel_=mc.getHideLabel(),this.markers_=[],this.center_=null,this.bounds_=null,this.clusterIcon_=new ClusterIcon(this,mc.getStyles())}function MarkerClusterer(map,opt_markers,opt_options){this.extend(MarkerClusterer,google.maps.OverlayView),opt_markers=opt_markers||[],opt_options=opt_options||{},this.markers_=[],this.clusters_=[],this.listeners_=[],this.activeMap_=null,this.ready_=!1,this.gridSize_=opt_options.gridSize||60,this.minClusterSize_=opt_options.minimumClusterSize||2,this.maxZoom_=opt_options.maxZoom||null,this.styles_=opt_options.styles||[],this.title_=opt_options.title||"",this.zoomOnClick_=!0,void 0!==opt_options.zoomOnClick&&(this.zoomOnClick_=opt_options.zoomOnClick),this.averageCenter_=!1,void 0!==opt_options.averageCenter&&(this.averageCenter_=opt_options.averageCenter),this.ignoreHidden_=!1,void 0!==opt_options.ignoreHidden&&(this.ignoreHidden_=opt_options.ignoreHidden),this.enableRetinaIcons_=!1,void 0!==opt_options.enableRetinaIcons&&(this.enableRetinaIcons_=opt_options.enableRetinaIcons),this.hideLabel_=!1,void 0!==opt_options.hideLabel&&(this.hideLabel_=opt_options.hideLabel),this.imagePath_=opt_options.imagePath||MarkerClusterer.IMAGE_PATH,this.imageExtension_=opt_options.imageExtension||MarkerClusterer.IMAGE_EXTENSION,this.imageSizes_=opt_options.imageSizes||MarkerClusterer.IMAGE_SIZES,this.calculator_=opt_options.calculator||MarkerClusterer.CALCULATOR,this.batchSize_=opt_options.batchSize||MarkerClusterer.BATCH_SIZE,this.batchSizeIE_=opt_options.batchSizeIE||MarkerClusterer.BATCH_SIZE_IE,this.clusterClass_=opt_options.clusterClass||"cluster",-1!==navigator.userAgent.toLowerCase().indexOf("msie")&&(this.batchSize_=this.batchSizeIE_),this.setupStyles_(),this.addMarkers(opt_markers,!0),this.setMap(map)}ClusterIcon.prototype.onAdd=function(){var cMouseDownInCluster,cDraggingMapByCluster,cClusterIcon=this;this.div_=document.createElement("div"),this.div_.className=this.className_,this.visible_&&this.show(),this.getPanes().overlayMouseTarget.appendChild(this.div_),this.boundsChangedListener_=google.maps.event.addListener(this.getMap(),"bounds_changed",function(){cDraggingMapByCluster=cMouseDownInCluster}),google.maps.event.addDomListener(this.div_,"mousedown",function(){cMouseDownInCluster=!0,cDraggingMapByCluster=!1}),google.maps.event.addDomListener(this.div_,"click",function(e){if(cMouseDownInCluster=!1,!cDraggingMapByCluster){var theBounds,mz,mc=cClusterIcon.cluster_.getMarkerClusterer();google.maps.event.trigger(mc,"click",cClusterIcon.cluster_),google.maps.event.trigger(mc,"clusterclick",cClusterIcon.cluster_),mc.getZoomOnClick()&&(mz=mc.getMaxZoom(),theBounds=cClusterIcon.cluster_.getBounds(),mc.getMap().fitBounds(theBounds),setTimeout(function(){mc.getMap().fitBounds(theBounds),null!==mz&&mc.getMap().getZoom()>mz&&mc.getMap().setZoom(mz+1)},100)),e.cancelBubble=!0,e.stopPropagation&&e.stopPropagation()}}),google.maps.event.addDomListener(this.div_,"mouseover",function(){var mc=cClusterIcon.cluster_.getMarkerClusterer();google.maps.event.trigger(mc,"mouseover",cClusterIcon.cluster_)}),google.maps.event.addDomListener(this.div_,"mouseout",function(){var mc=cClusterIcon.cluster_.getMarkerClusterer();google.maps.event.trigger(mc,"mouseout",cClusterIcon.cluster_)})},ClusterIcon.prototype.onRemove=function(){this.div_&&this.div_.parentNode&&(this.hide(),google.maps.event.removeListener(this.boundsChangedListener_),google.maps.event.clearInstanceListeners(this.div_),this.div_.parentNode.removeChild(this.div_),this.div_=null)},ClusterIcon.prototype.draw=function(){if(this.visible_){var pos=this.getPosFromLatLng_(this.center_);this.div_.style.top=pos.y+"px",this.div_.style.left=pos.x+"px"}},ClusterIcon.prototype.hide=function(){this.div_&&(this.div_.style.display="none"),this.visible_=!1},ClusterIcon.prototype.show=function(){if(this.div_){var img="",bp=this.backgroundPosition_.split(" "),spriteH=parseInt(bp[0].trim(),10),spriteV=parseInt(bp[1].trim(),10),pos=this.getPosFromLatLng_(this.center_);this.div_.style.cssText=this.createCss(pos),img="",this.div_.innerHTML=img+"
"+(this.cluster_.hideLabel_?" ":this.sums_.text)+"
",this.div_.title="undefined"==typeof this.sums_.title||""===this.sums_.title?this.cluster_.getMarkerClusterer().getTitle():this.sums_.title,this.div_.style.display=""}this.visible_=!0},ClusterIcon.prototype.useStyle=function(sums){this.sums_=sums;var index=Math.max(0,sums.index-1);index=Math.min(this.styles_.length-1,index);var style=this.styles_[index];this.url_=style.url,this.height_=style.height,this.width_=style.width,this.anchorText_=style.anchorText||[0,0],this.anchorIcon_=style.anchorIcon||[parseInt(this.height_/2,10),parseInt(this.width_/2,10)],this.textColor_=style.textColor||"black",this.textSize_=style.textSize||11,this.textDecoration_=style.textDecoration||"none",this.fontWeight_=style.fontWeight||"bold",this.fontStyle_=style.fontStyle||"normal",this.fontFamily_=style.fontFamily||"Arial,sans-serif",this.backgroundPosition_=style.backgroundPosition||"0 0"},ClusterIcon.prototype.setCenter=function(center){this.center_=center},ClusterIcon.prototype.createCss=function(pos){var style=[];return style.push("cursor: pointer;"),style.push("position: absolute; top: "+pos.y+"px; left: "+pos.x+"px;"),style.push("width: "+this.width_+"px; height: "+this.height_+"px;"),style.join("")},ClusterIcon.prototype.getPosFromLatLng_=function(latlng){var pos=this.getProjection().fromLatLngToDivPixel(latlng);return pos.x-=this.anchorIcon_[1],pos.y-=this.anchorIcon_[0],pos.x=parseInt(pos.x,10),pos.y=parseInt(pos.y,10),pos},Cluster.prototype.getSize=function(){return this.markers_.length},Cluster.prototype.getMarkers=function(){return this.markers_},Cluster.prototype.getCenter=function(){return this.center_},Cluster.prototype.getMap=function(){return this.map_},Cluster.prototype.getMarkerClusterer=function(){return this.markerClusterer_},Cluster.prototype.getBounds=function(){var i,bounds=new google.maps.LatLngBounds(this.center_,this.center_),markers=this.getMarkers();for(i=0;imz)marker.getMap()!==this.map_&&marker.setMap(this.map_);else if(mCounti;i++)this.markers_[i].setMap(null);else marker.setMap(null);return!0},Cluster.prototype.isMarkerInClusterBounds=function(marker){return this.bounds_.contains(marker.getPosition())},Cluster.prototype.calculateBounds_=function(){var bounds=new google.maps.LatLngBounds(this.center_,this.center_);this.bounds_=this.markerClusterer_.getExtendedBounds(bounds)},Cluster.prototype.updateIcon_=function(){var mCount=this.markers_.length,mz=this.markerClusterer_.getMaxZoom();if(null!==mz&&this.map_.getZoom()>mz)return void this.clusterIcon_.hide();if(mCounti;i++)if(marker===this.markers_[i])return!0;return!1},MarkerClusterer.prototype.onAdd=function(){var cMarkerClusterer=this;this.activeMap_=this.getMap(),this.ready_=!0,this.repaint(),this.listeners_=[google.maps.event.addListener(this.getMap(),"zoom_changed",function(){cMarkerClusterer.resetViewport_(!1),(this.getZoom()===(this.get("minZoom")||0)||this.getZoom()===this.get("maxZoom"))&&google.maps.event.trigger(this,"idle")}),google.maps.event.addListener(this.getMap(),"idle",function(){cMarkerClusterer.redraw_()})]},MarkerClusterer.prototype.onRemove=function(){var i;for(i=0;i0))for(i=0;id&&(distance=d,clusterToAddTo=cluster));clusterToAddTo&&clusterToAddTo.isMarkerInClusterBounds(marker)?clusterToAddTo.addMarker(marker):(cluster=new Cluster(this),cluster.addMarker(marker),this.clusters_.push(cluster))},MarkerClusterer.prototype.createClusters_=function(iFirst){var i,marker,mapBounds,cMarkerClusterer=this;if(this.ready_){0===iFirst&&(google.maps.event.trigger(this,"clusteringbegin",this),"undefined"!=typeof this.timerRefStatic&&(clearTimeout(this.timerRefStatic),delete this.timerRefStatic)),mapBounds=this.getMap().getZoom()>3?new google.maps.LatLngBounds(this.getMap().getBounds().getSouthWest(),this.getMap().getBounds().getNorthEast()):new google.maps.LatLngBounds(new google.maps.LatLng(85.02070771743472,-178.48388434375),new google.maps.LatLng(-85.08136444384544,178.00048865625));var bounds=this.getExtendedBounds(mapBounds),iLast=Math.min(iFirst+this.batchSize_,this.markers_.length);for(i=iFirst;iLast>i;i++)marker=this.markers_[i],!marker.isAdded&&this.isMarkerInBounds_(marker,bounds)&&(!this.ignoreHidden_||this.ignoreHidden_&&marker.getVisible())&&this.addToClosestCluster_(marker);if(iLast + * This is an enhanced V3 implementation of the V2 MarkerClusterer by Xiaoxi Wu. It is + * based on the V3 MarkerClusterer port by Luke Mahe. MarkerClustererPlus was created + * by Gary Little. + *

+ * v2.0 release: MarkerClustererPlus v2.0 is backward compatible with MarkerClusterer v1.0. It + * adds support for the ignoreHidden, title, batchSizeIE, + * and calculator properties as well as support for four more events. It also allows + * greater control over the styling of the text that appears on the cluster marker. The + * documentation has been significantly improved and the overall code has been simplified and + * polished. Very large numbers of markers can now be managed without causing Javascript timeout + * errors on Internet Explorer. Note that the name of the clusterclick event has been + * deprecated. The new name is click, so please change your application code now. */ + /** - * @fileoverview InfoBox extends the Google Maps JavaScript API V3 OverlayView class. - *

- * An InfoBox behaves like a google.maps.InfoWindow, but it supports several - * additional properties for advanced styling. An InfoBox can also be used as a map label. - *

- * An InfoBox also fires the same events as a google.maps.InfoWindow. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ -/*jslint browser:true */ -/*global google */ /** - * @name InfoBoxOptions - * @class This class represents the optional parameter passed to the {@link InfoBox} constructor. - * @property {string|Node} content The content of the InfoBox (plain text or an HTML DOM node). - * @property {boolean} [disableAutoPan=false] Disable auto-pan on open. - * @property {number} maxWidth The maximum width (in pixels) of the InfoBox. Set to 0 if no maximum. - * @property {Size} pixelOffset The offset (in pixels) from the top left corner of the InfoBox - * (or the bottom left corner if the alignBottom property is true) - * to the map pixel corresponding to position. - * @property {LatLng} position The geographic location at which to display the InfoBox. - * @property {number} zIndex The CSS z-index style value for the InfoBox. - * Note: This value overrides a zIndex setting specified in the boxStyle property. - * @property {string} [boxClass="infoBox"] The name of the CSS class defining the styles for the InfoBox container. - * @property {Object} [boxStyle] An object literal whose properties define specific CSS - * style values to be applied to the InfoBox. Style values defined here override those that may - * be defined in the boxClass style sheet. If this property is changed after the - * InfoBox has been created, all previously set styles (except those defined in the style sheet) - * are removed from the InfoBox before the new style values are applied. - * @property {string} closeBoxMargin The CSS margin style value for the close box. - * The default is "2px" (a 2-pixel margin on all sides). - * @property {string} closeBoxURL The URL of the image representing the close box. - * Note: The default is the URL for Google's standard close box. - * Set this property to "" if no close box is required. - * @property {Size} infoBoxClearance Minimum offset (in pixels) from the InfoBox to the - * map edge after an auto-pan. - * @property {boolean} [isHidden=false] Hide the InfoBox on open. - * [Deprecated in favor of the visible property.] - * @property {boolean} [visible=true] Show the InfoBox on open. - * @property {boolean} alignBottom Align the bottom left corner of the InfoBox to the position - * location (default is false which means that the top left corner of the InfoBox is aligned). - * @property {string} pane The pane where the InfoBox is to appear (default is "floatPane"). - * Set the pane to "mapPane" if the InfoBox is being used as a map label. - * Valid pane names are the property names for the google.maps.MapPanes object. - * @property {boolean} enableEventPropagation Propagate mousedown, mousemove, mouseover, mouseout, - * mouseup, click, dblclick, touchstart, touchend, touchmove, and contextmenu events in the InfoBox - * (default is false to mimic the behavior of a google.maps.InfoWindow). Set - * this property to true if the InfoBox is being used as a map label. + * @name ClusterIconStyle + * @class This class represents the object for values in the styles array passed + * to the {@link MarkerClusterer} constructor. The element in this array that is used to + * style the cluster icon is determined by calling the calculator function. + * + * @property {string} url The URL of the cluster icon image file. Required. + * @property {number} height The display height (in pixels) of the cluster icon. Required. + * @property {number} width The display width (in pixels) of the cluster icon. Required. + * @property {Array} [anchorText] The position (in pixels) from the center of the cluster icon to + * where the text label is to be centered and drawn. The format is [yoffset, xoffset] + * where yoffset increases as you go down from center and xoffset + * increases to the right of center. The default is [0, 0]. + * @property {Array} [anchorIcon] The anchor position (in pixels) of the cluster icon. This is the + * spot on the cluster icon that is to be aligned with the cluster position. The format is + * [yoffset, xoffset] where yoffset increases as you go down and + * xoffset increases to the right of the top-left corner of the icon. The default + * anchor position is the center of the cluster icon. + * @property {string} [textColor="black"] The color of the label text shown on the + * cluster icon. + * @property {number} [textSize=11] The size (in pixels) of the label text shown on the + * cluster icon. + * @property {string} [textDecoration="none"] The value of the CSS text-decoration + * property for the label text shown on the cluster icon. + * @property {string} [fontWeight="bold"] The value of the CSS font-weight + * property for the label text shown on the cluster icon. + * @property {string} [fontStyle="normal"] The value of the CSS font-style + * property for the label text shown on the cluster icon. + * @property {string} [fontFamily="Arial,sans-serif"] The value of the CSS font-family + * property for the label text shown on the cluster icon. + * @property {string} [backgroundPosition="0 0"] The position of the cluster icon image + * within the image defined by url. The format is "xpos ypos" + * (the same format as for the CSS background-position property). You must set + * this property appropriately when the image defined by url represents a sprite + * containing multiple images. Note that the position must be specified in px units. */ - /** - * Creates an InfoBox with the options specified in {@link InfoBoxOptions}. - * Call InfoBox.open to add the box to the map. + * @name ClusterIconInfo + * @class This class is an object containing general information about a cluster icon. This is + * the object that a calculator function returns. + * + * @property {string} text The text of the label to be shown on the cluster icon. + * @property {number} index The index plus 1 of the element in the styles + * array to be used to style the cluster icon. + * @property {string} title The tooltip to display when the mouse moves over the cluster icon. + * If this value is undefined or "", title is set to the + * value of the title property passed to the MarkerClusterer. + */ +/** + * A cluster icon. + * * @constructor - * @param {InfoBoxOptions} [opt_opts] + * @extends google.maps.OverlayView + * @param {Cluster} cluster The cluster with which the icon is to be associated. + * @param {Array} [styles] An array of {@link ClusterIconStyle} defining the cluster icons + * to use for various cluster sizes. + * @private */ -function InfoBox(opt_opts) { +function ClusterIcon(cluster, styles) { + cluster.getMarkerClusterer().extend(ClusterIcon, google.maps.OverlayView); - opt_opts = opt_opts || {}; - - google.maps.OverlayView.apply(this, arguments); + this.cluster_ = cluster; + this.className_ = cluster.getMarkerClusterer().getClusterClass(); + this.styles_ = styles; + this.center_ = null; + this.div_ = null; + this.sums_ = null; + this.visible_ = false; - // Standard options (in common with google.maps.InfoWindow): - // - this.content_ = opt_opts.content || ""; - this.disableAutoPan_ = opt_opts.disableAutoPan || false; - this.maxWidth_ = opt_opts.maxWidth || 0; - this.pixelOffset_ = opt_opts.pixelOffset || new google.maps.Size(0, 0); - this.position_ = opt_opts.position || new google.maps.LatLng(0, 0); - this.zIndex_ = opt_opts.zIndex || null; + this.setMap(cluster.getMap()); // Note: this causes onAdd to be called +} - // Additional options (unique to InfoBox): - // - this.boxClass_ = opt_opts.boxClass || "infoBox"; - this.boxStyle_ = opt_opts.boxStyle || {}; - this.closeBoxMargin_ = opt_opts.closeBoxMargin || "2px"; - this.closeBoxURL_ = opt_opts.closeBoxURL || "http://www.google.com/intl/en_us/mapfiles/close.gif"; - if (opt_opts.closeBoxURL === "") { - this.closeBoxURL_ = ""; - } - this.infoBoxClearance_ = opt_opts.infoBoxClearance || new google.maps.Size(1, 1); - if (typeof opt_opts.visible === "undefined") { - if (typeof opt_opts.isHidden === "undefined") { - opt_opts.visible = true; - } else { - opt_opts.visible = !opt_opts.isHidden; - } +/** + * Adds the icon to the DOM. + */ +ClusterIcon.prototype.onAdd = function () { + var cClusterIcon = this; + var cMouseDownInCluster; + var cDraggingMapByCluster; + var gmVersion = google.maps.version.split("."); + + gmVersion = parseInt(gmVersion[0] * 100, 10) + parseInt(gmVersion[1], 10); + + this.div_ = document.createElement("div"); + this.div_.className = this.className_; + if (this.visible_) { + this.show(); } - this.isHidden_ = !opt_opts.visible; - this.alignBottom_ = opt_opts.alignBottom || false; - this.pane_ = opt_opts.pane || "floatPane"; - this.enableEventPropagation_ = opt_opts.enableEventPropagation || false; + this.getPanes().overlayMouseTarget.appendChild(this.div_); - this.div_ = null; - this.closeListener_ = null; - this.moveListener_ = null; - this.contextListener_ = null; - this.eventListeners_ = null; - this.fixedWidthSet_ = null; -} + // Fix for Issue 157 + this.boundsChangedListener_ = google.maps.event.addListener(this.getMap(), "bounds_changed", function () { + cDraggingMapByCluster = cMouseDownInCluster; + }); -/* InfoBox extends OverlayView in the Google Maps API v3. - */ -InfoBox.prototype = new google.maps.OverlayView(); + google.maps.event.addDomListener(this.div_, "mousedown", function () { + cMouseDownInCluster = true; + cDraggingMapByCluster = false; + }); -/** - * Creates the DIV representing the InfoBox. - * @private - */ -InfoBox.prototype.createInfoBoxDiv_ = function () { +// March 1, 2018: Fix for this 3.32 exp bug, https://issuetracker.google.com/issues/73571522 +// But it doesn't work with earlier releases so do a version check. + if (gmVersion >= 332) { // Ugly version-dependent code + google.maps.event.addDomListener(this.div_, "touchstart", function (e) { + e.stopPropagation(); + }); + } - var i; - var events; - var bw; - var me = this; + google.maps.event.addDomListener(this.div_, "click", function (e) { + cMouseDownInCluster = false; + if (!cDraggingMapByCluster) { + var theBounds; + var mz; + var originalZoom; + var mc = cClusterIcon.cluster_.getMarkerClusterer(); + /** + * This event is fired when a cluster marker is clicked. + * @name MarkerClusterer#click + * @param {Cluster} c The cluster that was clicked. + * @event + */ + google.maps.event.trigger(mc, "click", cClusterIcon.cluster_); + google.maps.event.trigger(mc, "clusterclick", cClusterIcon.cluster_); // deprecated name + + // The default click handler follows. Disable it by setting + // the zoomOnClick property to false. + if (mc.getZoomOnClick()) { + // Zoom into the cluster. + mz = mc.getMaxZoom(); + originalZoom = mc.getMap().getZoom(); + theBounds = cClusterIcon.cluster_.getBounds(); + mc.getMap().fitBounds(theBounds); + // There is a fix for Issue 170 here. + // Also fixes https://github.com/googlemaps/v3-utility-library/issues/437 + setTimeout(function () { + var currentZoom = mc.getMap().getZoom(); + currentZoom = Math.max(currentZoom, originalZoom + 1); + // Don't zoom beyond the max zoom level if maxZoom specified + // or ensure we zoom at least one level over original zoom level. + var newZoom = (mz !== null && (currentZoom > mz) ? mz + 1 : currentZoom); + mc.getMap().setZoom(newZoom); + }, 100); + } - // This handler prevents an event in the InfoBox from being passed on to the map. - // - var cancelHandler = function (e) { - e.cancelBubble = true; - if (e.stopPropagation) { - e.stopPropagation(); + // Prevent event propagation to the map: + e.cancelBubble = true; + if (e.stopPropagation) { + e.stopPropagation(); + } } - }; + }); - // This handler ignores the current event in the InfoBox and conditionally prevents - // the event from being passed on to the map. It is used for the contextmenu event. - // - var ignoreHandler = function (e) { + google.maps.event.addDomListener(this.div_, "mouseover", function () { + var mc = cClusterIcon.cluster_.getMarkerClusterer(); + /** + * This event is fired when the mouse moves over a cluster marker. + * @name MarkerClusterer#mouseover + * @param {Cluster} c The cluster that the mouse moved over. + * @event + */ + google.maps.event.trigger(mc, "mouseover", cClusterIcon.cluster_); + }); - e.returnValue = false; + google.maps.event.addDomListener(this.div_, "mouseout", function () { + var mc = cClusterIcon.cluster_.getMarkerClusterer(); + /** + * This event is fired when the mouse moves out of a cluster marker. + * @name MarkerClusterer#mouseout + * @param {Cluster} c The cluster that the mouse moved out of. + * @event + */ + google.maps.event.trigger(mc, "mouseout", cClusterIcon.cluster_); + }); +}; - if (e.preventDefault) { - e.preventDefault(); - } +/** + * Removes the icon from the DOM. + */ +ClusterIcon.prototype.onRemove = function () { + if (this.div_ && this.div_.parentNode) { + this.hide(); + google.maps.event.removeListener(this.boundsChangedListener_); + google.maps.event.clearInstanceListeners(this.div_); + this.div_.parentNode.removeChild(this.div_); + this.div_ = null; + } +}; - if (!me.enableEventPropagation_) { - cancelHandler(e); - } - }; +/** + * Draws the icon. + */ +ClusterIcon.prototype.draw = function () { + if (this.visible_) { + var pos = this.getPosFromLatLng_(this.center_); + this.div_.style.top = pos.y + "px"; + this.div_.style.left = pos.x + "px"; + this.div_.style.zIndex = google.maps.Marker.MAX_ZINDEX + 1; // Put above all unclustered markers + } +}; - if (!this.div_) { - this.div_ = document.createElement("div"); +/** + * Hides the icon. + */ +ClusterIcon.prototype.hide = function () { + if (this.div_) { + this.div_.style.display = "none"; + } + this.visible_ = false; +}; - this.setBoxStyle_(); - if (typeof this.content_.nodeType === "undefined") { - this.div_.innerHTML = this.getCloseBoxImg_() + this.content_; +/** + * Positions and shows the icon. + */ +ClusterIcon.prototype.show = function () { + if (this.div_) { + var img = ""; + // NOTE: values must be specified in px units + var bp = this.backgroundPosition_.split(" "); + var spriteH = parseInt(bp[0].replace(/^\s+|\s+$/g, ""), 10); + var spriteV = parseInt(bp[1].replace(/^\s+|\s+$/g, ""), 10); + var pos = this.getPosFromLatLng_(this.center_); + this.div_.style.cssText = this.createCss(pos); + img = ""; + this.div_.innerHTML = img + "

" + this.sums_.text + "
"; + if (typeof this.sums_.title === "undefined" || this.sums_.title === "") { + this.div_.title = this.cluster_.getMarkerClusterer().getTitle(); + } else { + this.div_.title = this.sums_.title; + } + this.div_.style.display = ""; + } + this.visible_ = true; +}; - // Add the InfoBox DIV to the DOM - this.getPanes()[this.pane_].appendChild(this.div_); - this.addClickHandler_(); +/** + * Sets the icon styles to the appropriate element in the styles array. + * + * @param {ClusterIconInfo} sums The icon label text and styles index. + */ +ClusterIcon.prototype.useStyle = function (sums) { + this.sums_ = sums; + var index = Math.max(0, sums.index - 1); + index = Math.min(this.styles_.length - 1, index); + var style = this.styles_[index]; + this.url_ = style.url; + this.height_ = style.height; + this.width_ = style.width; + this.anchorText_ = style.anchorText || [0, 0]; + this.anchorIcon_ = style.anchorIcon || [parseInt(this.height_ / 2, 10), parseInt(this.width_ / 2, 10)]; + this.textColor_ = style.textColor || "black"; + this.textSize_ = style.textSize || 11; + this.textDecoration_ = style.textDecoration || "none"; + this.fontWeight_ = style.fontWeight || "bold"; + this.fontStyle_ = style.fontStyle || "normal"; + this.fontFamily_ = style.fontFamily || "Arial,sans-serif"; + this.backgroundPosition_ = style.backgroundPosition || "0 0"; +}; - if (this.div_.style.width) { - this.fixedWidthSet_ = true; +/** + * Sets the position at which to center the icon. + * + * @param {google.maps.LatLng} center The latlng to set as the center. + */ +ClusterIcon.prototype.setCenter = function (center) { + this.center_ = center; +}; - } else { - if (this.maxWidth_ !== 0 && this.div_.offsetWidth > this.maxWidth_) { +/** + * Creates the cssText style parameter based on the position of the icon. + * + * @param {google.maps.Point} pos The position of the icon. + * @return {string} The CSS style text. + */ +ClusterIcon.prototype.createCss = function (pos) { + var style = []; + style.push("cursor: pointer;"); + style.push("position: absolute; top: " + pos.y + "px; left: " + pos.x + "px;"); + style.push("width: " + this.width_ + "px; height: " + this.height_ + "px;"); + style.push("-webkit-user-select: none;"); + style.push("-khtml-user-select: none;"); + style.push("-moz-user-select: none;"); + style.push("-o-user-select: none;"); + style.push("user-select: none;"); + return style.join(""); +}; - this.div_.style.width = this.maxWidth_; - this.div_.style.overflow = "auto"; - this.fixedWidthSet_ = true; - } else { // The following code is needed to overcome problems with MSIE +/** + * Returns the position at which to place the DIV depending on the latlng. + * + * @param {google.maps.LatLng} latlng The position in latlng. + * @return {google.maps.Point} The position in pixels. + */ +ClusterIcon.prototype.getPosFromLatLng_ = function (latlng) { + var pos = this.getProjection().fromLatLngToDivPixel(latlng); + pos.x -= this.anchorIcon_[1]; + pos.y -= this.anchorIcon_[0]; + pos.x = parseInt(pos.x, 10); + pos.y = parseInt(pos.y, 10); + return pos; +}; - bw = this.getBoxWidths_(); - this.div_.style.width = (this.div_.offsetWidth - bw.left - bw.right) + "px"; - this.fixedWidthSet_ = false; - } - } +/** + * Creates a single cluster that manages a group of proximate markers. + * Used internally, do not call this constructor directly. + * @constructor + * @param {MarkerClusterer} mc The MarkerClusterer object with which this + * cluster is associated. + */ +function Cluster(mc) { + this.markerClusterer_ = mc; + this.map_ = mc.getMap(); + this.gridSize_ = mc.getGridSize(); + this.minClusterSize_ = mc.getMinimumClusterSize(); + this.averageCenter_ = mc.getAverageCenter(); + this.markers_ = []; + this.center_ = null; + this.bounds_ = null; + this.clusterIcon_ = new ClusterIcon(this, mc.getStyles()); +} - this.panBox_(this.disableAutoPan_); - if (!this.enableEventPropagation_) { +/** + * Returns the number of markers managed by the cluster. You can call this from + * a click, mouseover, or mouseout event handler + * for the MarkerClusterer object. + * + * @return {number} The number of markers in the cluster. + */ +Cluster.prototype.getSize = function () { + return this.markers_.length; +}; - this.eventListeners_ = []; + +/** + * Returns the array of markers managed by the cluster. You can call this from + * a click, mouseover, or mouseout event handler + * for the MarkerClusterer object. + * + * @return {Array} The array of markers in the cluster. + */ +Cluster.prototype.getMarkers = function () { + return this.markers_; +}; + + +/** + * Returns the center of the cluster. You can call this from + * a click, mouseover, or mouseout event handler + * for the MarkerClusterer object. + * + * @return {google.maps.LatLng} The center of the cluster. + */ +Cluster.prototype.getCenter = function () { + return this.center_; +}; + + +/** + * Returns the map with which the cluster is associated. + * + * @return {google.maps.Map} The map. + * @ignore + */ +Cluster.prototype.getMap = function () { + return this.map_; +}; + + +/** + * Returns the MarkerClusterer object with which the cluster is associated. + * + * @return {MarkerClusterer} The associated marker clusterer. + * @ignore + */ +Cluster.prototype.getMarkerClusterer = function () { + return this.markerClusterer_; +}; + + +/** + * Returns the bounds of the cluster. + * + * @return {google.maps.LatLngBounds} the cluster bounds. + * @ignore + */ +Cluster.prototype.getBounds = function () { + var i; + var bounds = new google.maps.LatLngBounds(this.center_, this.center_); + var markers = this.getMarkers(); + for (i = 0; i < markers.length; i++) { + bounds.extend(markers[i].getPosition()); + } + return bounds; +}; + + +/** + * Removes the cluster from the map. + * + * @ignore + */ +Cluster.prototype.remove = function () { + this.clusterIcon_.setMap(null); + this.markers_ = []; + delete this.markers_; +}; + + +/** + * Adds a marker to the cluster. + * + * @param {google.maps.Marker} marker The marker to be added. + * @return {boolean} True if the marker was added. + * @ignore + */ +Cluster.prototype.addMarker = function (marker) { + var i; + var mCount; + var mz; + + if (this.isMarkerAlreadyAdded_(marker)) { + return false; + } + + if (!this.center_) { + this.center_ = marker.getPosition(); + this.calculateBounds_(); + } else { + if (this.averageCenter_) { + var l = this.markers_.length + 1; + var lat = (this.center_.lat() * (l - 1) + marker.getPosition().lat()) / l; + var lng = (this.center_.lng() * (l - 1) + marker.getPosition().lng()) / l; + this.center_ = new google.maps.LatLng(lat, lng); + this.calculateBounds_(); + } + } + + marker.isAdded = true; + this.markers_.push(marker); + + mCount = this.markers_.length; + mz = this.markerClusterer_.getMaxZoom(); + if (mz !== null && this.map_.getZoom() > mz) { + // Zoomed in past max zoom, so show the marker. + if (marker.getMap() !== this.map_) { + marker.setMap(this.map_); + } + } else if (mCount < this.minClusterSize_) { + // Min cluster size not reached so show the marker. + if (marker.getMap() !== this.map_) { + marker.setMap(this.map_); + } + } else if (mCount === this.minClusterSize_) { + // Hide the markers that were showing. + for (i = 0; i < mCount; i++) { + this.markers_[i].setMap(null); + } + } else { + marker.setMap(null); + } + + this.updateIcon_(); + return true; +}; + + +/** + * Determines if a marker lies within the cluster's bounds. + * + * @param {google.maps.Marker} marker The marker to check. + * @return {boolean} True if the marker lies in the bounds. + * @ignore + */ +Cluster.prototype.isMarkerInClusterBounds = function (marker) { + return this.bounds_.contains(marker.getPosition()); +}; + + +/** + * Calculates the extended bounds of the cluster with the grid. + */ +Cluster.prototype.calculateBounds_ = function () { + var bounds = new google.maps.LatLngBounds(this.center_, this.center_); + this.bounds_ = this.markerClusterer_.getExtendedBounds(bounds); +}; + + +/** + * Updates the cluster icon. + */ +Cluster.prototype.updateIcon_ = function () { + var mCount = this.markers_.length; + var mz = this.markerClusterer_.getMaxZoom(); + + if (mz !== null && this.map_.getZoom() > mz) { + this.clusterIcon_.hide(); + return; + } + + if (mCount < this.minClusterSize_) { + // Min cluster size not yet reached. + this.clusterIcon_.hide(); + return; + } + + var numStyles = this.markerClusterer_.getStyles().length; + var sums = this.markerClusterer_.getCalculator()(this.markers_, numStyles); + this.clusterIcon_.setCenter(this.center_); + this.clusterIcon_.useStyle(sums); + this.clusterIcon_.show(); +}; + + +/** + * Determines if a marker has already been added to the cluster. + * + * @param {google.maps.Marker} marker The marker to check. + * @return {boolean} True if the marker has already been added. + */ +Cluster.prototype.isMarkerAlreadyAdded_ = function (marker) { + var i; + if (this.markers_.indexOf) { + return this.markers_.indexOf(marker) !== -1; + } else { + for (i = 0; i < this.markers_.length; i++) { + if (marker === this.markers_[i]) { + return true; + } + } + } + return false; +}; + + +/** + * @name MarkerClustererOptions + * @class This class represents the optional parameter passed to + * the {@link MarkerClusterer} constructor. + * @property {number} [gridSize=60] The grid size of a cluster in pixels. The grid is a square. + * @property {number} [maxZoom=null] The maximum zoom level at which clustering is enabled or + * null if clustering is to be enabled at all zoom levels. + * @property {boolean} [zoomOnClick=true] Whether to zoom the map when a cluster marker is + * clicked. You may want to set this to false if you have installed a handler + * for the click event and it deals with zooming on its own. + * @property {boolean} [averageCenter=false] Whether the position of a cluster marker should be + * the average position of all markers in the cluster. If set to false, the + * cluster marker is positioned at the location of the first marker added to the cluster. + * @property {number} [minimumClusterSize=2] The minimum number of markers needed in a cluster + * before the markers are hidden and a cluster marker appears. + * @property {boolean} [ignoreHidden=false] Whether to ignore hidden markers in clusters. You + * may want to set this to true to ensure that hidden markers are not included + * in the marker count that appears on a cluster marker (this count is the value of the + * text property of the result returned by the default calculator). + * If set to true and you change the visibility of a marker being clustered, be + * sure to also call MarkerClusterer.repaint(). + * @property {string} [title=""] The tooltip to display when the mouse moves over a cluster + * marker. (Alternatively, you can use a custom calculator function to specify a + * different tooltip for each cluster marker.) + * @property {function} [calculator=MarkerClusterer.CALCULATOR] The function used to determine + * the text to be displayed on a cluster marker and the index indicating which style to use + * for the cluster marker. The input parameters for the function are (1) the array of markers + * represented by a cluster marker and (2) the number of cluster icon styles. It returns a + * {@link ClusterIconInfo} object. The default calculator returns a + * text property which is the number of markers in the cluster and an + * index property which is one higher than the lowest integer such that + * 10^i exceeds the number of markers in the cluster, or the size of the styles + * array, whichever is less. The styles array element used has an index of + * index minus 1. For example, the default calculator returns a + * text value of "125" and an index of 3 + * for a cluster icon representing 125 markers so the element used in the styles + * array is 2. A calculator may also return a title + * property that contains the text of the tooltip to be used for the cluster marker. If + * title is not defined, the tooltip is set to the value of the title + * property for the MarkerClusterer. + * @property {string} [clusterClass="cluster"] The name of the CSS class defining general styles + * for the cluster markers. Use this class to define CSS styles that are not set up by the code + * that processes the styles array. + * @property {Array} [styles] An array of {@link ClusterIconStyle} elements defining the styles + * of the cluster markers to be used. The element to be used to style a given cluster marker + * is determined by the function defined by the calculator property. + * The default is an array of {@link ClusterIconStyle} elements whose properties are derived + * from the values for imagePath, imageExtension, and + * imageSizes. + * @property {boolean} [enableRetinaIcons=false] Whether to allow the use of cluster icons that + * have sizes that are some multiple (typically double) of their actual display size. Icons such + * as these look better when viewed on high-resolution monitors such as Apple's Retina displays. + * Note: if this property is true, sprites cannot be used as cluster icons. + * @property {number} [batchSize=MarkerClusterer.BATCH_SIZE] Set this property to the + * number of markers to be processed in a single batch when using a browser other than + * Internet Explorer (for Internet Explorer, use the batchSizeIE property instead). + * @property {number} [batchSizeIE=MarkerClusterer.BATCH_SIZE_IE] When Internet Explorer is + * being used, markers are processed in several batches with a small delay inserted between + * each batch in an attempt to avoid Javascript timeout errors. Set this property to the + * number of markers to be processed in a single batch; select as high a number as you can + * without causing a timeout error in the browser. This number might need to be as low as 100 + * if 15,000 markers are being managed, for example. + * @property {string} [imagePath=MarkerClusterer.IMAGE_PATH] + * The full URL of the root name of the group of image files to use for cluster icons. + * The complete file name is of the form imagePathn.imageExtension + * where n is the image file number (1, 2, etc.). + * @property {string} [imageExtension=MarkerClusterer.IMAGE_EXTENSION] + * The extension name for the cluster icon image files (e.g., "png" or + * "jpg"). + * @property {Array} [imageSizes=MarkerClusterer.IMAGE_SIZES] + * An array of numbers containing the widths of the group of + * imagePathn.imageExtension image files. + * (The images are assumed to be square.) + */ +/** + * Creates a MarkerClusterer object with the options specified in {@link MarkerClustererOptions}. + * @constructor + * @extends google.maps.OverlayView + * @param {google.maps.Map} map The Google map to attach to. + * @param {Array.} [opt_markers] The markers to be added to the cluster. + * @param {MarkerClustererOptions} [opt_options] The optional parameters. + */ +function MarkerClusterer(map, opt_markers, opt_options) { + // MarkerClusterer implements google.maps.OverlayView interface. We use the + // extend function to extend MarkerClusterer with google.maps.OverlayView + // because it might not always be available when the code is defined so we + // look for it at the last possible moment. If it doesn't exist now then + // there is no point going ahead :) + this.extend(MarkerClusterer, google.maps.OverlayView); + + opt_markers = opt_markers || []; + opt_options = opt_options || {}; + + this.markers_ = []; + this.clusters_ = []; + this.listeners_ = []; + this.activeMap_ = null; + this.ready_ = false; + + this.gridSize_ = opt_options.gridSize || 60; + this.minClusterSize_ = opt_options.minimumClusterSize || 2; + this.maxZoom_ = opt_options.maxZoom || null; + this.styles_ = opt_options.styles || []; + this.title_ = opt_options.title || ""; + this.zoomOnClick_ = true; + if (opt_options.zoomOnClick !== undefined) { + this.zoomOnClick_ = opt_options.zoomOnClick; + } + this.averageCenter_ = false; + if (opt_options.averageCenter !== undefined) { + this.averageCenter_ = opt_options.averageCenter; + } + this.ignoreHidden_ = false; + if (opt_options.ignoreHidden !== undefined) { + this.ignoreHidden_ = opt_options.ignoreHidden; + } + this.enableRetinaIcons_ = false; + if (opt_options.enableRetinaIcons !== undefined) { + this.enableRetinaIcons_ = opt_options.enableRetinaIcons; + } + this.imagePath_ = opt_options.imagePath || MarkerClusterer.IMAGE_PATH; + this.imageExtension_ = opt_options.imageExtension || MarkerClusterer.IMAGE_EXTENSION; + this.imageSizes_ = opt_options.imageSizes || MarkerClusterer.IMAGE_SIZES; + this.calculator_ = opt_options.calculator || MarkerClusterer.CALCULATOR; + this.batchSize_ = opt_options.batchSize || MarkerClusterer.BATCH_SIZE; + this.batchSizeIE_ = opt_options.batchSizeIE || MarkerClusterer.BATCH_SIZE_IE; + this.clusterClass_ = opt_options.clusterClass || "cluster"; + + if (navigator.userAgent.toLowerCase().indexOf("msie") !== -1) { + // Try to avoid IE timeout when processing a huge number of markers: + this.batchSize_ = this.batchSizeIE_; + } + + this.setupStyles_(); + + this.addMarkers(opt_markers, true); + this.setMap(map); // Note: this causes onAdd to be called +} + + +/** + * Implementation of the onAdd interface method. + * @ignore + */ +MarkerClusterer.prototype.onAdd = function () { + var cMarkerClusterer = this; + + this.activeMap_ = this.getMap(); + this.ready_ = true; + + this.repaint(); + + this.prevZoom_ = this.getMap().getZoom(); + + // Add the map event listeners + this.listeners_ = [ + google.maps.event.addListener(this.getMap(), "zoom_changed", function () { + // Fix for bug #407 + // Determines map type and prevents illegal zoom levels + var zoom = this.getMap().getZoom(); + var minZoom = this.getMap().minZoom || 0; + var maxZoom = Math.min(this.getMap().maxZoom || 100, + this.getMap().mapTypes[this.getMap().getMapTypeId()].maxZoom); + zoom = Math.min(Math.max(zoom, minZoom), maxZoom); + + if (this.prevZoom_ != zoom) { + this.prevZoom_ = zoom; + this.resetViewport_(false); + } + }.bind(this)), + google.maps.event.addListener(this.getMap(), "idle", function () { + cMarkerClusterer.redraw_(); + }) + ]; +}; + + +/** + * Implementation of the onRemove interface method. + * Removes map event listeners and all cluster icons from the DOM. + * All managed markers are also put back on the map. + * @ignore + */ +MarkerClusterer.prototype.onRemove = function () { + var i; + + // Put all the managed markers back on the map: + for (i = 0; i < this.markers_.length; i++) { + if (this.markers_[i].getMap() !== this.activeMap_) { + this.markers_[i].setMap(this.activeMap_); + } + } + + // Remove all clusters: + for (i = 0; i < this.clusters_.length; i++) { + this.clusters_[i].remove(); + } + this.clusters_ = []; + + // Remove map event listeners: + for (i = 0; i < this.listeners_.length; i++) { + google.maps.event.removeListener(this.listeners_[i]); + } + this.listeners_ = []; + + this.activeMap_ = null; + this.ready_ = false; +}; + + +/** + * Implementation of the draw interface method. + * @ignore + */ +MarkerClusterer.prototype.draw = function () {}; + + +/** + * Sets up the styles object. + */ +MarkerClusterer.prototype.setupStyles_ = function () { + var i, size; + if (this.styles_.length > 0) { + return; + } + + for (i = 0; i < this.imageSizes_.length; i++) { + size = this.imageSizes_[i]; + this.styles_.push({ + url: this.imagePath_ + (i + 1) + "." + this.imageExtension_, + height: size, + width: size + }); + } +}; + + +/** + * Fits the map to the bounds of the markers managed by the clusterer. + */ +MarkerClusterer.prototype.fitMapToMarkers = function () { + var i; + var markers = this.getMarkers(); + var bounds = new google.maps.LatLngBounds(); + for (i = 0; i < markers.length; i++) { + // March 3, 2018: Bug fix -- honor the ignoreHidden property + if (markers[i].getVisible() || !this.getIgnoreHidden()) { + bounds.extend(markers[i].getPosition()); + } + } + + this.getMap().fitBounds(bounds); +}; + + +/** + * Returns the value of the gridSize property. + * + * @return {number} The grid size. + */ +MarkerClusterer.prototype.getGridSize = function () { + return this.gridSize_; +}; + + +/** + * Sets the value of the gridSize property. + * + * @param {number} gridSize The grid size. + */ +MarkerClusterer.prototype.setGridSize = function (gridSize) { + this.gridSize_ = gridSize; +}; + + +/** + * Returns the value of the minimumClusterSize property. + * + * @return {number} The minimum cluster size. + */ +MarkerClusterer.prototype.getMinimumClusterSize = function () { + return this.minClusterSize_; +}; + +/** + * Sets the value of the minimumClusterSize property. + * + * @param {number} minimumClusterSize The minimum cluster size. + */ +MarkerClusterer.prototype.setMinimumClusterSize = function (minimumClusterSize) { + this.minClusterSize_ = minimumClusterSize; +}; + + +/** + * Returns the value of the maxZoom property. + * + * @return {number} The maximum zoom level. + */ +MarkerClusterer.prototype.getMaxZoom = function () { + return this.maxZoom_; +}; + + +/** + * Sets the value of the maxZoom property. + * + * @param {number} maxZoom The maximum zoom level. + */ +MarkerClusterer.prototype.setMaxZoom = function (maxZoom) { + this.maxZoom_ = maxZoom; +}; + + +/** + * Returns the value of the styles property. + * + * @return {Array} The array of styles defining the cluster markers to be used. + */ +MarkerClusterer.prototype.getStyles = function () { + return this.styles_; +}; + + +/** + * Sets the value of the styles property. + * + * @param {Array.} styles The array of styles to use. + */ +MarkerClusterer.prototype.setStyles = function (styles) { + this.styles_ = styles; +}; + + +/** + * Returns the value of the title property. + * + * @return {string} The content of the title text. + */ +MarkerClusterer.prototype.getTitle = function () { + return this.title_; +}; + + +/** + * Sets the value of the title property. + * + * @param {string} title The value of the title property. + */ +MarkerClusterer.prototype.setTitle = function (title) { + this.title_ = title; +}; + + +/** + * Returns the value of the zoomOnClick property. + * + * @return {boolean} True if zoomOnClick property is set. + */ +MarkerClusterer.prototype.getZoomOnClick = function () { + return this.zoomOnClick_; +}; + + +/** + * Sets the value of the zoomOnClick property. + * + * @param {boolean} zoomOnClick The value of the zoomOnClick property. + */ +MarkerClusterer.prototype.setZoomOnClick = function (zoomOnClick) { + this.zoomOnClick_ = zoomOnClick; +}; + + +/** + * Returns the value of the averageCenter property. + * + * @return {boolean} True if averageCenter property is set. + */ +MarkerClusterer.prototype.getAverageCenter = function () { + return this.averageCenter_; +}; + + +/** + * Sets the value of the averageCenter property. + * + * @param {boolean} averageCenter The value of the averageCenter property. + */ +MarkerClusterer.prototype.setAverageCenter = function (averageCenter) { + this.averageCenter_ = averageCenter; +}; + + +/** + * Returns the value of the ignoreHidden property. + * + * @return {boolean} True if ignoreHidden property is set. + */ +MarkerClusterer.prototype.getIgnoreHidden = function () { + return this.ignoreHidden_; +}; + + +/** + * Sets the value of the ignoreHidden property. + * + * @param {boolean} ignoreHidden The value of the ignoreHidden property. + */ +MarkerClusterer.prototype.setIgnoreHidden = function (ignoreHidden) { + this.ignoreHidden_ = ignoreHidden; +}; + + +/** + * Returns the value of the enableRetinaIcons property. + * + * @return {boolean} True if enableRetinaIcons property is set. + */ +MarkerClusterer.prototype.getEnableRetinaIcons = function () { + return this.enableRetinaIcons_; +}; + + +/** + * Sets the value of the enableRetinaIcons property. + * + * @param {boolean} enableRetinaIcons The value of the enableRetinaIcons property. + */ +MarkerClusterer.prototype.setEnableRetinaIcons = function (enableRetinaIcons) { + this.enableRetinaIcons_ = enableRetinaIcons; +}; + + +/** + * Returns the value of the imageExtension property. + * + * @return {string} The value of the imageExtension property. + */ +MarkerClusterer.prototype.getImageExtension = function () { + return this.imageExtension_; +}; + + +/** + * Sets the value of the imageExtension property. + * + * @param {string} imageExtension The value of the imageExtension property. + */ +MarkerClusterer.prototype.setImageExtension = function (imageExtension) { + this.imageExtension_ = imageExtension; +}; + + +/** + * Returns the value of the imagePath property. + * + * @return {string} The value of the imagePath property. + */ +MarkerClusterer.prototype.getImagePath = function () { + return this.imagePath_; +}; + + +/** + * Sets the value of the imagePath property. + * + * @param {string} imagePath The value of the imagePath property. + */ +MarkerClusterer.prototype.setImagePath = function (imagePath) { + this.imagePath_ = imagePath; +}; + + +/** + * Returns the value of the imageSizes property. + * + * @return {Array} The value of the imageSizes property. + */ +MarkerClusterer.prototype.getImageSizes = function () { + return this.imageSizes_; +}; + + +/** + * Sets the value of the imageSizes property. + * + * @param {Array} imageSizes The value of the imageSizes property. + */ +MarkerClusterer.prototype.setImageSizes = function (imageSizes) { + this.imageSizes_ = imageSizes; +}; + + +/** + * Returns the value of the calculator property. + * + * @return {function} the value of the calculator property. + */ +MarkerClusterer.prototype.getCalculator = function () { + return this.calculator_; +}; + + +/** + * Sets the value of the calculator property. + * + * @param {function(Array., number)} calculator The value + * of the calculator property. + */ +MarkerClusterer.prototype.setCalculator = function (calculator) { + this.calculator_ = calculator; +}; + + +/** + * Returns the value of the batchSizeIE property. + * + * @return {number} the value of the batchSizeIE property. + */ +MarkerClusterer.prototype.getBatchSizeIE = function () { + return this.batchSizeIE_; +}; + + +/** + * Sets the value of the batchSizeIE property. + * + * @param {number} batchSizeIE The value of the batchSizeIE property. + */ +MarkerClusterer.prototype.setBatchSizeIE = function (batchSizeIE) { + this.batchSizeIE_ = batchSizeIE; +}; + + +/** + * Returns the value of the clusterClass property. + * + * @return {string} the value of the clusterClass property. + */ +MarkerClusterer.prototype.getClusterClass = function () { + return this.clusterClass_; +}; + + +/** + * Sets the value of the clusterClass property. + * + * @param {string} clusterClass The value of the clusterClass property. + */ +MarkerClusterer.prototype.setClusterClass = function (clusterClass) { + this.clusterClass_ = clusterClass; +}; + + +/** + * Returns the array of markers managed by the clusterer. + * + * @return {Array} The array of markers managed by the clusterer. + */ +MarkerClusterer.prototype.getMarkers = function () { + return this.markers_; +}; + + +/** + * Returns the number of markers managed by the clusterer. + * + * @return {number} The number of markers. + */ +MarkerClusterer.prototype.getTotalMarkers = function () { + return this.markers_.length; +}; + + +/** + * Returns the current array of clusters formed by the clusterer. + * + * @return {Array} The array of clusters formed by the clusterer. + */ +MarkerClusterer.prototype.getClusters = function () { + return this.clusters_; +}; + + +/** + * Returns the number of clusters formed by the clusterer. + * + * @return {number} The number of clusters formed by the clusterer. + */ +MarkerClusterer.prototype.getTotalClusters = function () { + return this.clusters_.length; +}; + + +/** + * Adds a marker to the clusterer. The clusters are redrawn unless + * opt_nodraw is set to true. + * + * @param {google.maps.Marker} marker The marker to add. + * @param {boolean} [opt_nodraw] Set to true to prevent redrawing. + */ +MarkerClusterer.prototype.addMarker = function (marker, opt_nodraw) { + this.pushMarkerTo_(marker); + if (!opt_nodraw) { + this.redraw_(); + } +}; + + +/** + * Adds an array of markers to the clusterer. The clusters are redrawn unless + * opt_nodraw is set to true. + * + * @param {Array.} markers The markers to add. + * @param {boolean} [opt_nodraw] Set to true to prevent redrawing. + */ +MarkerClusterer.prototype.addMarkers = function (markers, opt_nodraw) { + var key; + for (key in markers) { + if (markers.hasOwnProperty(key)) { + this.pushMarkerTo_(markers[key]); + } + } + if (!opt_nodraw) { + this.redraw_(); + } +}; + + +/** + * Pushes a marker to the clusterer. + * + * @param {google.maps.Marker} marker The marker to add. + */ +MarkerClusterer.prototype.pushMarkerTo_ = function (marker) { + // If the marker is draggable add a listener so we can update the clusters on the dragend: + if (marker.getDraggable()) { + var cMarkerClusterer = this; + google.maps.event.addListener(marker, "dragend", function () { + if (cMarkerClusterer.ready_) { + this.isAdded = false; + cMarkerClusterer.repaint(); + } + }); + } + marker.isAdded = false; + this.markers_.push(marker); +}; + + +/** + * Removes a marker from the cluster. The clusters are redrawn unless + * opt_nodraw is set to true. Returns true if the + * marker was removed from the clusterer. + * + * @param {google.maps.Marker} marker The marker to remove. + * @param {boolean} [opt_nodraw] Set to true to prevent redrawing. + * @return {boolean} True if the marker was removed from the clusterer. + */ +MarkerClusterer.prototype.removeMarker = function (marker, opt_nodraw) { + var removed = this.removeMarker_(marker); + + if (!opt_nodraw && removed) { + this.repaint(); + } + + return removed; +}; + + +/** + * Removes an array of markers from the cluster. The clusters are redrawn unless + * opt_nodraw is set to true. Returns true if markers + * were removed from the clusterer. + * + * @param {Array.} markers The markers to remove. + * @param {boolean} [opt_nodraw] Set to true to prevent redrawing. + * @return {boolean} True if markers were removed from the clusterer. + */ +MarkerClusterer.prototype.removeMarkers = function (markers, opt_nodraw) { + var i, r; + var removed = false; + + for (i = 0; i < markers.length; i++) { + r = this.removeMarker_(markers[i]); + removed = removed || r; + } + + if (!opt_nodraw && removed) { + this.repaint(); + } + + return removed; +}; + + +/** + * Removes a marker and returns true if removed, false if not. + * + * @param {google.maps.Marker} marker The marker to remove + * @return {boolean} Whether the marker was removed or not + */ +MarkerClusterer.prototype.removeMarker_ = function (marker) { + var i; + var index = -1; + if (this.markers_.indexOf) { + index = this.markers_.indexOf(marker); + } else { + for (i = 0; i < this.markers_.length; i++) { + if (marker === this.markers_[i]) { + index = i; + break; + } + } + } + + if (index === -1) { + // Marker is not in our list of markers, so do nothing: + return false; + } + + marker.setMap(null); + this.markers_.splice(index, 1); // Remove the marker from the list of managed markers + return true; +}; + + +/** + * Removes all clusters and markers from the map and also removes all markers + * managed by the clusterer. + */ +MarkerClusterer.prototype.clearMarkers = function () { + this.resetViewport_(true); + this.markers_ = []; +}; + + +/** + * Recalculates and redraws all the marker clusters from scratch. + * Call this after changing any properties. + */ +MarkerClusterer.prototype.repaint = function () { + var oldClusters = this.clusters_.slice(); + this.clusters_ = []; + this.resetViewport_(false); + this.redraw_(); + + // Remove the old clusters. + // Do it in a timeout to prevent blinking effect. + setTimeout(function () { + var i; + for (i = 0; i < oldClusters.length; i++) { + oldClusters[i].remove(); + } + }, 0); +}; + + +/** + * Returns the current bounds extended by the grid size. + * + * @param {google.maps.LatLngBounds} bounds The bounds to extend. + * @return {google.maps.LatLngBounds} The extended bounds. + * @ignore + */ +MarkerClusterer.prototype.getExtendedBounds = function (bounds) { + var projection = this.getProjection(); + + // Turn the bounds into latlng. + var tr = new google.maps.LatLng(bounds.getNorthEast().lat(), + bounds.getNorthEast().lng()); + var bl = new google.maps.LatLng(bounds.getSouthWest().lat(), + bounds.getSouthWest().lng()); + + // Convert the points to pixels and the extend out by the grid size. + var trPix = projection.fromLatLngToDivPixel(tr); + trPix.x += this.gridSize_; + trPix.y -= this.gridSize_; + + var blPix = projection.fromLatLngToDivPixel(bl); + blPix.x -= this.gridSize_; + blPix.y += this.gridSize_; + + // Convert the pixel points back to LatLng + var ne = projection.fromDivPixelToLatLng(trPix); + var sw = projection.fromDivPixelToLatLng(blPix); + + // Extend the bounds to contain the new bounds. + bounds.extend(ne); + bounds.extend(sw); + + return bounds; +}; + + +/** + * Redraws all the clusters. + */ +MarkerClusterer.prototype.redraw_ = function () { + this.createClusters_(0); +}; + + +/** + * Removes all clusters from the map. The markers are also removed from the map + * if opt_hide is set to true. + * + * @param {boolean} [opt_hide] Set to true to also remove the markers + * from the map. + */ +MarkerClusterer.prototype.resetViewport_ = function (opt_hide) { + var i, marker; + // Remove all the clusters + for (i = 0; i < this.clusters_.length; i++) { + this.clusters_[i].remove(); + } + this.clusters_ = []; + + // Reset the markers to not be added and to be removed from the map. + for (i = 0; i < this.markers_.length; i++) { + marker = this.markers_[i]; + marker.isAdded = false; + if (opt_hide) { + marker.setMap(null); + } + } +}; + + +/** + * Calculates the distance between two latlng locations in km. + * + * @param {google.maps.LatLng} p1 The first lat lng point. + * @param {google.maps.LatLng} p2 The second lat lng point. + * @return {number} The distance between the two points in km. + * @see http://www.movable-type.co.uk/scripts/latlong.html +*/ +MarkerClusterer.prototype.distanceBetweenPoints_ = function (p1, p2) { + var R = 6371; // Radius of the Earth in km + var dLat = (p2.lat() - p1.lat()) * Math.PI / 180; + var dLon = (p2.lng() - p1.lng()) * Math.PI / 180; + var a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + + Math.cos(p1.lat() * Math.PI / 180) * Math.cos(p2.lat() * Math.PI / 180) * + Math.sin(dLon / 2) * Math.sin(dLon / 2); + var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); + var d = R * c; + return d; +}; + + +/** + * Determines if a marker is contained in a bounds. + * + * @param {google.maps.Marker} marker The marker to check. + * @param {google.maps.LatLngBounds} bounds The bounds to check against. + * @return {boolean} True if the marker is in the bounds. + */ +MarkerClusterer.prototype.isMarkerInBounds_ = function (marker, bounds) { + return bounds.contains(marker.getPosition()); +}; + + +/** + * Adds a marker to a cluster, or creates a new cluster. + * + * @param {google.maps.Marker} marker The marker to add. + */ +MarkerClusterer.prototype.addToClosestCluster_ = function (marker) { + var i, d, cluster, center; + var distance = 40000; // Some large number + var clusterToAddTo = null; + for (i = 0; i < this.clusters_.length; i++) { + cluster = this.clusters_[i]; + center = cluster.getCenter(); + if (center) { + d = this.distanceBetweenPoints_(center, marker.getPosition()); + if (d < distance) { + distance = d; + clusterToAddTo = cluster; + } + } + } + + if (clusterToAddTo && clusterToAddTo.isMarkerInClusterBounds(marker)) { + clusterToAddTo.addMarker(marker); + } else { + cluster = new Cluster(this); + cluster.addMarker(marker); + this.clusters_.push(cluster); + } +}; + + +/** + * Creates the clusters. This is done in batches to avoid timeout errors + * in some browsers when there is a huge number of markers. + * + * @param {number} iFirst The index of the first marker in the batch of + * markers to be added to clusters. + */ +MarkerClusterer.prototype.createClusters_ = function (iFirst) { + var i, marker; + var mapBounds; + var cMarkerClusterer = this; + if (!this.ready_) { + return; + } + + // Cancel previous batch processing if we're working on the first batch: + if (iFirst === 0) { + /** + * This event is fired when the MarkerClusterer begins + * clustering markers. + * @name MarkerClusterer#clusteringbegin + * @param {MarkerClusterer} mc The MarkerClusterer whose markers are being clustered. + * @event + */ + google.maps.event.trigger(this, "clusteringbegin", this); + + if (typeof this.timerRefStatic !== "undefined") { + clearTimeout(this.timerRefStatic); + delete this.timerRefStatic; + } + } + + // Get our current map view bounds. + // Create a new bounds object so we don't affect the map. + // + // See Comments 9 & 11 on Issue 3651 relating to this workaround for a Google Maps bug: + if (this.getMap().getZoom() > 3) { + mapBounds = new google.maps.LatLngBounds(this.getMap().getBounds().getSouthWest(), + this.getMap().getBounds().getNorthEast()); + } else { + mapBounds = new google.maps.LatLngBounds(new google.maps.LatLng(85.02070771743472, -178.48388434375), new google.maps.LatLng(-85.08136444384544, 178.00048865625)); + } + var bounds = this.getExtendedBounds(mapBounds); + + var iLast = Math.min(iFirst + this.batchSize_, this.markers_.length); + + for (i = iFirst; i < iLast; i++) { + marker = this.markers_[i]; + if (!marker.isAdded && this.isMarkerInBounds_(marker, bounds)) { + if (!this.ignoreHidden_ || (this.ignoreHidden_ && marker.getVisible())) { + this.addToClosestCluster_(marker); + } + } + } + + if (iLast < this.markers_.length) { + this.timerRefStatic = setTimeout(function () { + cMarkerClusterer.createClusters_(iLast); + }, 0); + } else { + delete this.timerRefStatic; + + /** + * This event is fired when the MarkerClusterer stops + * clustering markers. + * @name MarkerClusterer#clusteringend + * @param {MarkerClusterer} mc The MarkerClusterer whose markers are being clustered. + * @event + */ + google.maps.event.trigger(this, "clusteringend", this); + } +}; + + +/** + * Extends an object's prototype by another's. + * + * @param {Object} obj1 The object to be extended. + * @param {Object} obj2 The object to extend with. + * @return {Object} The new extended object. + * @ignore + */ +MarkerClusterer.prototype.extend = function (obj1, obj2) { + return (function (object) { + var property; + for (property in object.prototype) { + this.prototype[property] = object.prototype[property]; + } + return this; + }).apply(obj1, [obj2]); +}; + + +/** + * The default function for determining the label text and style + * for a cluster icon. + * + * @param {Array.} markers The array of markers represented by the cluster. + * @param {number} numStyles The number of marker styles available. + * @return {ClusterIconInfo} The information resource for the cluster. + * @constant + * @ignore + */ +MarkerClusterer.CALCULATOR = function (markers, numStyles) { + var index = 0; + var title = ""; + var count = markers.length.toString(); + + var dv = count; + while (dv !== 0) { + dv = parseInt(dv / 10, 10); + index++; + } + + index = Math.min(index, numStyles); + return { + text: count, + index: index, + title: title + }; +}; + + +/** + * The number of markers to process in one batch. + * + * @type {number} + * @constant + */ +MarkerClusterer.BATCH_SIZE = 2000; + + +/** + * The number of markers to process in one batch (IE only). + * + * @type {number} + * @constant + */ +MarkerClusterer.BATCH_SIZE_IE = 500; + + +/** + * The default root name for the marker cluster images. + * + * @type {string} + * @constant + */ +MarkerClusterer.IMAGE_PATH = "../images/m"; + + +/** + * The default extension name for the marker cluster images. + * + * @type {string} + * @constant + */ +MarkerClusterer.IMAGE_EXTENSION = "png"; + + +/** + * The default array of sizes for the marker cluster images. + * + * @type {Array.} + * @constant + */ +MarkerClusterer.IMAGE_SIZES = [53, 56, 66, 78, 90]; + +if (typeof module == 'object') { + module.exports = MarkerClusterer; +} +/** + * google-maps-utility-library-v3-infobox + * + * @version: 1.1.14 + * @author: Gary Little (inspired by proof-of-concept code from Pamela Fox of Google) + * @contributors: Nicholas McCready + * @date: Fri May 13 2016 16:35:27 GMT-0400 (EDT) + * @license: Apache License 2.0 + */ +/** + * @fileoverview InfoBox extends the Google Maps JavaScript API V3 OverlayView class. + *

+ * An InfoBox behaves like a google.maps.InfoWindow, but it supports several + * additional properties for advanced styling. An InfoBox can also be used as a map label. + *

+ * An InfoBox also fires the same events as a google.maps.InfoWindow. + */ + +/*jslint browser:true */ +/*global google */ + +/** + * @name InfoBoxOptions + * @class This class represents the optional parameter passed to the {@link InfoBox} constructor. + * @property {string|Node} content The content of the InfoBox (plain text or an HTML DOM node). + * @property {boolean} [disableAutoPan=false] Disable auto-pan on open. + * @property {number} maxWidth The maximum width (in pixels) of the InfoBox. Set to 0 if no maximum. + * @property {Size} pixelOffset The offset (in pixels) from the top left corner of the InfoBox + * (or the bottom left corner if the alignBottom property is true) + * to the map pixel corresponding to position. + * @property {LatLng} position The geographic location at which to display the InfoBox. + * @property {number} zIndex The CSS z-index style value for the InfoBox. + * Note: This value overrides a zIndex setting specified in the boxStyle property. + * @property {string} [boxClass="infoBox"] The name of the CSS class defining the styles for the InfoBox container. + * @property {Object} [boxStyle] An object literal whose properties define specific CSS + * style values to be applied to the InfoBox. Style values defined here override those that may + * be defined in the boxClass style sheet. If this property is changed after the + * InfoBox has been created, all previously set styles (except those defined in the style sheet) + * are removed from the InfoBox before the new style values are applied. + * @property {string} closeBoxMargin The CSS margin style value for the close box. + * The default is "2px" (a 2-pixel margin on all sides). + * @property {string} closeBoxURL The URL of the image representing the close box. + * Note: The default is the URL for Google's standard close box. + * Set this property to "" if no close box is required. + * @property {Size} infoBoxClearance Minimum offset (in pixels) from the InfoBox to the + * map edge after an auto-pan. + * @property {boolean} [isHidden=false] Hide the InfoBox on open. + * [Deprecated in favor of the visible property.] + * @property {boolean} [visible=true] Show the InfoBox on open. + * @property {boolean} alignBottom Align the bottom left corner of the InfoBox to the position + * location (default is false which means that the top left corner of the InfoBox is aligned). + * @property {string} pane The pane where the InfoBox is to appear (default is "floatPane"). + * Set the pane to "mapPane" if the InfoBox is being used as a map label. + * Valid pane names are the property names for the google.maps.MapPanes object. + * @property {boolean} enableEventPropagation Propagate mousedown, mousemove, mouseover, mouseout, + * mouseup, click, dblclick, touchstart, touchend, touchmove, and contextmenu events in the InfoBox + * (default is false to mimic the behavior of a google.maps.InfoWindow). Set + * this property to true if the InfoBox is being used as a map label. + */ + +/** + * Creates an InfoBox with the options specified in {@link InfoBoxOptions}. + * Call InfoBox.open to add the box to the map. + * @constructor + * @param {InfoBoxOptions} [opt_opts] + */ +function InfoBox(opt_opts) { + + opt_opts = opt_opts || {}; + + google.maps.OverlayView.apply(this, arguments); + + // Standard options (in common with google.maps.InfoWindow): + // + this.content_ = opt_opts.content || ""; + this.disableAutoPan_ = opt_opts.disableAutoPan || false; + this.maxWidth_ = opt_opts.maxWidth || 0; + this.pixelOffset_ = opt_opts.pixelOffset || new google.maps.Size(0, 0); + this.position_ = opt_opts.position || new google.maps.LatLng(0, 0); + this.zIndex_ = opt_opts.zIndex || null; + + // Additional options (unique to InfoBox): + // + this.boxClass_ = opt_opts.boxClass || "infoBox"; + this.boxStyle_ = opt_opts.boxStyle || {}; + this.closeBoxMargin_ = opt_opts.closeBoxMargin || "2px"; + this.closeBoxURL_ = opt_opts.closeBoxURL || "http://www.google.com/intl/en_us/mapfiles/close.gif"; + if (opt_opts.closeBoxURL === "") { + this.closeBoxURL_ = ""; + } + this.infoBoxClearance_ = opt_opts.infoBoxClearance || new google.maps.Size(1, 1); + + if (typeof opt_opts.visible === "undefined") { + if (typeof opt_opts.isHidden === "undefined") { + opt_opts.visible = true; + } else { + opt_opts.visible = !opt_opts.isHidden; + } + } + this.isHidden_ = !opt_opts.visible; + + this.alignBottom_ = opt_opts.alignBottom || false; + this.pane_ = opt_opts.pane || "floatPane"; + this.enableEventPropagation_ = opt_opts.enableEventPropagation || false; + + this.div_ = null; + this.closeListener_ = null; + this.moveListener_ = null; + this.contextListener_ = null; + this.eventListeners_ = null; + this.fixedWidthSet_ = null; +} + +/* InfoBox extends OverlayView in the Google Maps API v3. + */ +InfoBox.prototype = new google.maps.OverlayView(); + +/** + * Creates the DIV representing the InfoBox. + * @private + */ +InfoBox.prototype.createInfoBoxDiv_ = function () { + + var i; + var events; + var bw; + var me = this; + + // This handler prevents an event in the InfoBox from being passed on to the map. + // + var cancelHandler = function (e) { + e.cancelBubble = true; + if (e.stopPropagation) { + e.stopPropagation(); + } + }; + + // This handler ignores the current event in the InfoBox and conditionally prevents + // the event from being passed on to the map. It is used for the contextmenu event. + // + var ignoreHandler = function (e) { + + e.returnValue = false; + + if (e.preventDefault) { + + e.preventDefault(); + } + + if (!me.enableEventPropagation_) { + + cancelHandler(e); + } + }; + + if (!this.div_) { + + this.div_ = document.createElement("div"); + + this.setBoxStyle_(); + + if (typeof this.content_.nodeType === "undefined") { + this.div_.innerHTML = this.getCloseBoxImg_() + this.content_; + } else { + this.div_.innerHTML = this.getCloseBoxImg_(); + this.div_.appendChild(this.content_); + } + + // Add the InfoBox DIV to the DOM + this.getPanes()[this.pane_].appendChild(this.div_); + + this.addClickHandler_(); + + if (this.div_.style.width) { + + this.fixedWidthSet_ = true; + + } else { + + if (this.maxWidth_ !== 0 && this.div_.offsetWidth > this.maxWidth_) { + + this.div_.style.width = this.maxWidth_; + this.div_.style.overflow = "auto"; + this.fixedWidthSet_ = true; + + } else { // The following code is needed to overcome problems with MSIE + + bw = this.getBoxWidths_(); + + this.div_.style.width = (this.div_.offsetWidth - bw.left - bw.right) + "px"; + this.fixedWidthSet_ = false; + } + } + + this.panBox_(this.disableAutoPan_); + + if (!this.enableEventPropagation_) { + + this.eventListeners_ = []; // Cancel event propagation. // @@ -11555,11 +13219,11 @@ window['RichMarkerPosition'] = RichMarkerPosition; The `id` is a unique identifier for the node, and should **not** change after it's added. It will be used for adding, retrieving and deleting related edges too. - + **Note** that, internally, the ids are kept in an object. JavaScript's object hashes the id `'2'` and `2` to the same key, so please stick to a simple id data type such as number or string. - + _Returns:_ the node object. Feel free to attach additional custom properties on it for graph algorithms' needs. **Undefined if node id already exists**, as to avoid accidental overrides. @@ -11620,7 +13284,7 @@ window['RichMarkerPosition'] = RichMarkerPosition; `addNode()`. `weight` is optional and defaults to 1. Ignoring it effectively makes this an unweighted graph. Under the hood, `weight` is just a normal property of the edge object. - + _Returns:_ the edge object created. Feel free to attach additional custom properties on it for graph algorithms' needs. **Or undefined** if the nodes of id `fromId` or `toId` aren't found, or if an edge already exists between @@ -11717,7 +13381,7 @@ window['RichMarkerPosition'] = RichMarkerPosition; **Note:** not the same as concatenating `getInEdgesOf()` and `getOutEdgesOf()`. Some nodes might have an edge pointing toward itself. This method solves that duplication. - + _Returns:_ an array of edge objects linked to the node, no matter if they're outgoing or coming. Duplicate edge created by self-pointing nodes are removed. Only one copy stays. Empty array if node has no edge. @@ -11744,7 +13408,7 @@ window['RichMarkerPosition'] = RichMarkerPosition; /* Traverse through the graph in an arbitrary manner, visiting each node once. Pass a function of the form `fn(nodeObject, nodeId)`. - + _Returns:_ undefined. */ @@ -11761,7 +13425,7 @@ window['RichMarkerPosition'] = RichMarkerPosition; /* Traverse through the graph in an arbitrary manner, visiting each edge once. Pass a function of the form `fn(edgeObject)`. - + _Returns:_ undefined. */ @@ -11844,7 +13508,7 @@ window['RichMarkerPosition'] = RichMarkerPosition; Heap.prototype.add = function(value) { /* **Remember:** rejects null and undefined for mentioned reasons. - + _Returns:_ the value added. */ @@ -11879,7 +13543,7 @@ window['RichMarkerPosition'] = RichMarkerPosition; Heap.prototype.peekMin = function() { /* Check the smallest item without removing it. - + _Returns:_ the smallest item (the root). */ @@ -12002,13 +13666,13 @@ window['RichMarkerPosition'] = RichMarkerPosition; LinkedList.prototype.at = function(position) { /* Get the item at `position` (optional). Accepts negative index: - + ```js myList.at(-1); // Returns the last element. ``` However, passing a negative index that surpasses the boundary will return undefined: - + ```js myList = new LinkedList([2, 6, 8, 3]) myList.at(-5); // Undefined. @@ -12047,7 +13711,7 @@ window['RichMarkerPosition'] = RichMarkerPosition; boundary). Position specifies the place the value's going to be, and the old node will be pushed higher. `add(-2)` on list of size 7 is the same as `add(5)`. - + _Returns:_ item added. */ @@ -12083,7 +13747,7 @@ window['RichMarkerPosition'] = RichMarkerPosition; /* Remove an item at index `position` (optional). Defaults to the last item. Index can be negative (within the boundary). - + _Returns:_ item removed. */ @@ -12122,7 +13786,7 @@ window['RichMarkerPosition'] = RichMarkerPosition; /* Remove the item using its value instead of position. **Will remove the fist occurrence of `value`.** - + _Returns:_ the value, or undefined if value's not found. */ @@ -12165,9 +13829,9 @@ window['RichMarkerPosition'] = RichMarkerPosition; other methods of this class, `startingPosition` (optional) can be as small as desired; a value of -999 for a list of size 5 will start searching normally, at the beginning. - + **Note:** searches forwardly, **not** backwardly, i.e: - + ```js var myList = new LinkedList([2, 3, 1, 4, 3, 5]) myList.indexOf(3, -3); // Returns 4, not 1 @@ -12292,7 +13956,7 @@ window['RichMarkerPosition'] = RichMarkerPosition; your own. The `makeHash` parameter is optional and accepts a boolean (defaults to `false`) indicating whether or not to produce a new hash (for the first use, naturally). - + _Returns:_ the hash. */ @@ -12332,7 +13996,7 @@ window['RichMarkerPosition'] = RichMarkerPosition; Map.prototype.has = function(key) { /* Check whether a value exists for the key. - + _Returns:_ true or false. */ @@ -12342,7 +14006,7 @@ window['RichMarkerPosition'] = RichMarkerPosition; Map.prototype["delete"] = function(key) { /* Remove the (key, value) pair. - + _Returns:_ **true or false**. Unlike most of this library, this method doesn't return the deleted value. This is so that it conforms to the future JavaScript `map.delete()`'s behavior. @@ -12364,7 +14028,7 @@ window['RichMarkerPosition'] = RichMarkerPosition; Map.prototype.forEach = function(operation) { /* Traverse through the map. Pass a function of the form `fn(key, value)`. - + _Returns:_ undefined. */ @@ -12479,7 +14143,7 @@ window['RichMarkerPosition'] = RichMarkerPosition; Queue.prototype.peek = function() { /* Check the next item to be dequeued, without removing it. - + _Returns:_ the item. */ @@ -12566,7 +14230,7 @@ window['RichMarkerPosition'] = RichMarkerPosition; /* Again, make sure to not pass a value already in the tree, or undefined, or null. - + _Returns:_ value added. */ @@ -12669,7 +14333,7 @@ window['RichMarkerPosition'] = RichMarkerPosition; RedBlackTree.prototype.peekMin = function() { /* Check the minimum value without removing it. - + _Returns:_ the minimum value. */ @@ -12680,7 +14344,7 @@ window['RichMarkerPosition'] = RichMarkerPosition; RedBlackTree.prototype.peekMax = function() { /* Check the maximum value without removing it. - + _Returns:_ the maximum value. */ @@ -13014,7 +14678,7 @@ window['RichMarkerPosition'] = RichMarkerPosition; Trie.prototype.add = function(word) { /* Add a whole string to the trie. - + _Returns:_ the word added. Will return undefined (without adding the value) if the word passed is null or undefined. */ @@ -13063,7 +14727,7 @@ window['RichMarkerPosition'] = RichMarkerPosition; Trie.prototype.longestPrefixOf = function(word) { /* Find all words containing the prefix. The word itself counts as a prefix. - + ```js var trie = new Trie; trie.add('hello'); @@ -13071,7 +14735,7 @@ window['RichMarkerPosition'] = RichMarkerPosition; trie.longestPrefixOf('hello'); // 'hello' trie.longestPrefixOf('helloha!'); // 'hello' ``` - + _Returns:_ the prefix string, or empty string if no prefix found. */ @@ -13096,7 +14760,7 @@ window['RichMarkerPosition'] = RichMarkerPosition; /* Find all words containing the prefix. The word itself counts as a prefix. **Watch out for edge cases.** - + ```js var trie = new Trie; trie.wordsWithPrefix(''); // []. Check later case below. @@ -13110,7 +14774,7 @@ window['RichMarkerPosition'] = RichMarkerPosition; trie.add('zebra'); trie.wordsWithPrefix('hel'); // ['hell', 'hello'] ``` - + _Returns:_ an array of strings, or empty array if no word found. */ @@ -13217,7 +14881,7 @@ window['RichMarkerPosition'] = RichMarkerPosition; var self = this; /* istanbul ignore next */ +function(){ - + /** @preserve OverlappingMarkerSpiderfier https://github.com/jawj/OverlappingMarkerSpiderfier Copyright (c) 2011 - 2013 George MacKerron @@ -14173,4 +15837,4 @@ angular.module('uiGmapgoogle-maps.extensions') }) }; }]); -}( window, angular, _)); \ No newline at end of file +}( window, angular, _));