├── GoogleMapsAsset.php ├── .php_cs ├── LICENSE.md ├── composer.json ├── README.md ├── GoogleMaps.php └── assets ├── googlemap.js └── markerclusterer_compiled.js /GoogleMapsAsset.php: -------------------------------------------------------------------------------- 1 | exclude('vendor') 5 | ->in([__DIR__]); 6 | 7 | $config = PhpCsFixer\Config::create() 8 | ->setUsingCache(false) 9 | ->setRules([ 10 | '@Symfony' => true, 11 | 'phpdoc_align' => false, 12 | 'phpdoc_summary' => false, 13 | 'phpdoc_inline_tag' => false, 14 | 'pre_increment' => false, 15 | 'heredoc_to_nowdoc' => false, 16 | 'cast_spaces' => false, 17 | 'include' => false, 18 | 'phpdoc_no_package' => false, 19 | 'concat_space' => ['spacing' => 'one'], 20 | 'ordered_imports' => true, 21 | 'array_syntax' => ['syntax' => 'short'], 22 | ]) 23 | ->setFinder($finder); 24 | 25 | return $config; 26 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 yii2mod 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "yii2mod/yii2-google-maps-markers", 3 | "description": "Google Maps Markers displays a set of user addresses as markers on the map.", 4 | "type": "yii2-extension", 5 | "keywords": [ 6 | "yii2", 7 | "extension", 8 | "google maps markers", 9 | "google maps" 10 | ], 11 | "license": "MIT", 12 | "authors": [ 13 | { 14 | "name": "Igor Chepurnoy", 15 | "email": "igorzfort@gmail.com" 16 | }, 17 | { 18 | "name": "Dmitry Semenov", 19 | "email": "disemx@gmail.com" 20 | }, 21 | { 22 | "name": "Alexander Harchenko", 23 | "email": "morontt@gmail.com", 24 | "homepage": "https://morontt.info" 25 | } 26 | ], 27 | "require": { 28 | "yiisoft/yii2": "*" 29 | }, 30 | "require-dev": { 31 | "friendsofphp/php-cs-fixer": "~2.0" 32 | }, 33 | "autoload": { 34 | "psr-4": { 35 | "yii2mod\\google\\maps\\markers\\": "" 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | 4 | 5 |

Google Maps Markers Widget for Yii2

6 |
7 |

8 | 9 | GoogleMaps Widget displays a set of user addresses as markers on the map. 10 | 11 | [![Latest Stable Version](https://poser.pugx.org/yii2mod/yii2-google-maps-markers/v/stable)](https://packagist.org/packages/yii2mod/yii2-google-maps-markers) 12 | [![Total Downloads](https://poser.pugx.org/yii2mod/yii2-google-maps-markers/downloads)](https://packagist.org/packages/yii2mod/yii2-google-maps-markers) 13 | [![License](https://poser.pugx.org/yii2mod/yii2-google-maps-markers/license)](https://packagist.org/packages/yii2mod/yii2-google-maps-markers) 14 | [![Build Status](https://travis-ci.org/yii2mod/yii2-google-maps-markers.svg?branch=master)](https://travis-ci.org/yii2mod/yii2-google-maps-markers) 15 | 16 | Installation 17 | ------------ 18 | 19 | The preferred way to install this extension is through [composer](http://getcomposer.org/download/). 20 | 21 | Either run 22 | 23 | ``` 24 | php composer.phar require yii2mod/yii2-google-maps-markers "*" 25 | ``` 26 | 27 | or add 28 | 29 | ``` 30 | "yii2mod/yii2-google-maps-markers": "*" 31 | ``` 32 | 33 | to the require section of your composer.json. 34 | 35 | Usage 36 | ----- 37 | 38 | To use GoogleMaps, you need to configure its [[userLocations]] property. For example: 39 | 40 | ```php 41 | echo yii2mod\google\maps\markers\GoogleMaps::widget([ 42 | 'userLocations' => [ 43 | [ 44 | 'location' => [ 45 | 'address' => 'Kharkiv', 46 | 'country' => 'Ukraine', 47 | ], 48 | 'htmlContent' => '

Kharkiv

', 49 | ], 50 | [ 51 | 'location' => [ 52 | 'city' => 'New York', 53 | 'country' => 'United States', 54 | ], 55 | 'htmlContent' => '

New York

', 56 | ], 57 | ], 58 | ]); 59 | ``` 60 | 61 | Configuration 62 | ------------- 63 | 64 | To configure the Google Maps key or other options like language, version, library, or map options: 65 | 66 | ```php 67 | echo yii2mod\google\maps\markers\GoogleMaps::widget([ 68 | 'userLocations' => [...], 69 | 'googleMapsUrlOptions' => [ 70 | 'key' => 'this_is_my_key', 71 | 'language' => 'id', 72 | 'version' => '3.1.18', 73 | ], 74 | 'googleMapsOptions' => [ 75 | 'mapTypeId' => 'roadmap', 76 | 'tilt' => 45, 77 | 'zoom' => 5, 78 | ], 79 | ]); 80 | ``` 81 | 82 | OR via yii params configuration. For example: 83 | 84 | ```php 85 | 'params' => [ 86 | 'googleMapsUrlOptions' => [ 87 | 'key' => 'this_is_my_key', 88 | 'language' => 'id', 89 | 'version' => '3.1.18', 90 | ], 91 | 'googleMapsOptions' => [ 92 | 'mapTypeId' => 'roadmap', 93 | 'tilt' => 45, 94 | 'zoom' => 10, 95 | ], 96 | ], 97 | ``` 98 | 99 | To get key, please visit [page](https://developers.google.com/maps/documentation/javascript/get-api-key) 100 | 101 | Google Maps Options 102 | ------------------- 103 | 104 | You can find them on the [options page](https://developers.google.com/maps/documentation/javascript/reference) 105 | 106 | #### Example 107 | ------------ 108 | 109 | ![Alt text](http://res.cloudinary.com/zfort/image/upload/v1441115988/Map_preview_hcwb1x.png "Example") 110 | -------------------------------------------------------------------------------- /GoogleMaps.php: -------------------------------------------------------------------------------- 1 | [ 21 | * [ 22 | * 'location' => [ 23 | * 'address' => 'Kharkov', 24 | * 'country' => 'Ukraine', 25 | * ], 26 | * 'htmlContent' => '

Kharkov

' 27 | * ], 28 | * [ 29 | * 'location' => [ 30 | * 'city' => 'New York', 31 | * 'country' => 'Usa', 32 | * ], 33 | * 'htmlContent' => '

New York

' 34 | * ], 35 | * ] 36 | * ]); 37 | * ``` 38 | */ 39 | class GoogleMaps extends Widget 40 | { 41 | /** 42 | * @var array user locations array 43 | */ 44 | public $userLocations = []; 45 | 46 | /** 47 | * @var string main wrapper height 48 | */ 49 | public $wrapperHeight = '500px'; 50 | 51 | /** 52 | * @var string google maps url 53 | */ 54 | public $googleMapsUrl = 'https://maps.googleapis.com/maps/api/js?'; 55 | 56 | /** 57 | * libraries - Example: geometry, places. Default - empty string 58 | * version - 3.exp (Default) 59 | * 60 | * @var array google maps url options(v, language, key, libraries) 61 | */ 62 | public $googleMapsUrlOptions = []; 63 | 64 | /** 65 | * Google Maps options (mapTypeId, tilt, zoom, etc...) 66 | * 67 | * @see https://developers.google.com/maps/documentation/javascript/reference 68 | * 69 | * @var array 70 | */ 71 | public $googleMapsOptions = []; 72 | 73 | /** 74 | * Example listener for infowindow object: 75 | * 76 | * ```php 77 | * [ 78 | * [ 79 | * 'object' => 'infowindow', 80 | * 'event' => 'domready', 81 | * 'handler' => (new \yii\web\JsExpression('function() { 82 | * // your custom js code 83 | * }')) 84 | * ] 85 | * ] 86 | * ``` 87 | * 88 | * @var array google map listeners 89 | */ 90 | public $googleMapsListeners = []; 91 | 92 | /** 93 | * @see https://developers.google.com/maps/documentation/javascript/reference#InfoWindowOptions 94 | * 95 | * @var array 96 | */ 97 | public $infoWindowOptions = []; 98 | 99 | /** 100 | * @var string google maps container id 101 | */ 102 | public $containerId = 'map_canvas'; 103 | 104 | /** 105 | * @var bool render empty map, if userLocations is empty. Defaults to 'true' 106 | */ 107 | public $renderEmptyMap = true; 108 | 109 | /** 110 | * Json array for yii.googleMapManager with users address and html contents 111 | * 112 | * @var array 113 | */ 114 | protected $geocodeData = []; 115 | 116 | /** 117 | * Init widget 118 | */ 119 | public function init() 120 | { 121 | parent::init(); 122 | 123 | if (is_array($this->userLocations) === false) { 124 | throw new InvalidConfigException('The "userLocations" property must be of the type array'); 125 | } 126 | 127 | $this->googleMapsOptions = $this->getGoogleMapsOptions(); 128 | $this->infoWindowOptions = $this->getInfoWindowOptions(); 129 | $this->googleMapsUrlOptions = $this->getGoogleMapsUrlOptions(); 130 | } 131 | 132 | /** 133 | * Executes the widget. 134 | */ 135 | public function run() 136 | { 137 | if (empty($this->userLocations) && $this->renderEmptyMap === false) { 138 | return; 139 | } 140 | 141 | $this->geocodeData = $this->getGeoCodeData(); 142 | 143 | echo Html::beginTag('div', ['id' => $this->getId(), 'style' => "height: {$this->wrapperHeight}"]); 144 | echo Html::tag('div', '', ['id' => $this->containerId]); 145 | echo Html::endTag('div'); 146 | 147 | $this->registerAssets(); 148 | 149 | parent::run(); 150 | } 151 | 152 | /** 153 | * Register assets 154 | */ 155 | protected function registerAssets() 156 | { 157 | $view = $this->getView(); 158 | GoogleMapsAsset::register($view); 159 | $view->registerJsFile($this->getGoogleMapsApiUrl(), ['position' => View::POS_HEAD]); 160 | $options = $this->getClientOptions(); 161 | $view->registerJs("yii.googleMapManager.initModule({$options})", $view::POS_END, 'google-api-js'); 162 | } 163 | 164 | /** 165 | * Get place urls and htmlContent 166 | * 167 | * @return string 168 | */ 169 | protected function getGeoCodeData() 170 | { 171 | $result = []; 172 | foreach ($this->userLocations as $data) { 173 | $result[] = [ 174 | 'country' => ArrayHelper::getValue($data['location'], 'country'), 175 | 'address' => implode(',', ArrayHelper::getValue($data, 'location')), 176 | 'htmlContent' => ArrayHelper::getValue($data, 'htmlContent'), 177 | ]; 178 | } 179 | 180 | return $result; 181 | } 182 | 183 | /** 184 | * Get google maps api url 185 | * 186 | * @return string 187 | */ 188 | protected function getGoogleMapsApiUrl() 189 | { 190 | return $this->googleMapsUrl . http_build_query($this->googleMapsUrlOptions); 191 | } 192 | 193 | /** 194 | * Get google maps url options 195 | * 196 | * @return array 197 | */ 198 | protected function getGoogleMapsUrlOptions() 199 | { 200 | if (isset(Yii::$app->params['googleMapsUrlOptions']) && empty($this->googleMapsUrlOptions)) { 201 | $this->googleMapsUrlOptions = Yii::$app->params['googleMapsUrlOptions']; 202 | } 203 | 204 | return ArrayHelper::merge([ 205 | 'v' => '3.exp', 206 | 'key' => null, 207 | 'libraries' => null, 208 | 'language' => 'en', 209 | ], $this->googleMapsUrlOptions); 210 | } 211 | 212 | /** 213 | * Get google maps options 214 | * 215 | * @return array 216 | */ 217 | protected function getGoogleMapsOptions() 218 | { 219 | if (isset(Yii::$app->params['googleMapsOptions']) && empty($this->googleMapsOptions)) { 220 | $this->googleMapsOptions = Yii::$app->params['googleMapsOptions']; 221 | } 222 | 223 | return ArrayHelper::merge([ 224 | 'mapTypeId' => 'roadmap', 225 | 'tilt' => 45, 226 | 'zoom' => 2, 227 | ], $this->googleMapsOptions); 228 | } 229 | 230 | /** 231 | * Get info window options 232 | * 233 | * @return array 234 | */ 235 | protected function getInfoWindowOptions() 236 | { 237 | if (isset(Yii::$app->params['infoWindowOptions']) && empty($this->infoWindowOptions)) { 238 | $this->infoWindowOptions = Yii::$app->params['infoWindowOptions']; 239 | } 240 | 241 | return ArrayHelper::merge([ 242 | 'content' => '', 243 | 'maxWidth' => 350, 244 | ], $this->infoWindowOptions); 245 | } 246 | 247 | /** 248 | * Get google map client options 249 | * 250 | * @return string 251 | */ 252 | protected function getClientOptions() 253 | { 254 | return Json::encode([ 255 | 'geocodeData' => $this->geocodeData, 256 | 'mapOptions' => $this->googleMapsOptions, 257 | 'listeners' => $this->googleMapsListeners, 258 | 'containerId' => $this->containerId, 259 | 'renderEmptyMap' => $this->renderEmptyMap, 260 | 'infoWindowOptions' => $this->infoWindowOptions, 261 | ]); 262 | } 263 | } 264 | -------------------------------------------------------------------------------- /assets/googlemap.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Google Map manager - renders map and put markers 3 | * Address priority - House Number, Street Direction, Street Name, Street Suffix, City, State, Zip, Country 4 | */ 5 | yii.googleMapManager = (function ($) { 6 | var pub = { 7 | nextAddress: 0, 8 | zeroResult: 0, 9 | delay: 300, 10 | bounds: [], 11 | geocoder: [], 12 | markerClusterer: false, 13 | markers: [], 14 | infoWindow: [], 15 | infoWindowOptions: [], 16 | containerId: 'map_canvas', 17 | geocodeData: [], 18 | mapOptions: { 19 | center: new google.maps.LatLng(53.666464, -2.686693) 20 | }, 21 | listeners: [], 22 | renderEmptyMap: true, 23 | map: null, 24 | init: function () { 25 | }, 26 | initModule: function (options) { 27 | initOptions(options).done(function () { 28 | google.maps.event.addDomListener(window, 'load', initializeMap()); 29 | }); 30 | }, 31 | /** 32 | * Get address and place it on map 33 | */ 34 | getAddress: function (location, htmlContent, loadMap) { 35 | var search = location.address; 36 | pub.geocoder.geocode({'address': search}, function (results, status) { 37 | if (status == google.maps.GeocoderStatus.OK) { 38 | var place = results[0]; 39 | pub.drawMarker(place, htmlContent); 40 | pub.delay = 300; 41 | } 42 | else if (status == google.maps.GeocoderStatus.OVER_QUERY_LIMIT) { 43 | pub.nextAddress--; 44 | pub.delay = 2000; 45 | } 46 | else if (status == google.maps.GeocoderStatus.ZERO_RESULTS) { 47 | //If first query return zero results, then set address the value of the country 48 | if (location.address != location.country) { 49 | pub.nextAddress--; 50 | pub.geocodeData[pub.nextAddress].address = pub.geocodeData[pub.nextAddress].country; 51 | } else { 52 | pub.drawMarker(pub.mapOptions.center, htmlContent); 53 | } 54 | } 55 | loadMap(); 56 | }); 57 | }, 58 | updatePosition: function (position) { 59 | var coordinates = [position]; 60 | if (!pub.isPositionUnique(position)) { 61 | var latitude = position.lat(); 62 | var lngModify = (Math.abs(Math.cos(latitude)) / 111111) * -5; 63 | var iteration = 0; 64 | while (true) { 65 | iteration++; 66 | var newLng = position.lng() + (lngModify * iteration); 67 | position = new google.maps.LatLng(latitude + 0.00001, newLng); 68 | if (pub.isPositionUnique(position)) { 69 | break; 70 | } 71 | lngModify *= -1; 72 | } 73 | } 74 | 75 | coordinates.push(position); 76 | 77 | var path = new google.maps.Polyline({ 78 | path: coordinates, 79 | geodesic: true, 80 | strokeColor: '#00AAFF', 81 | strokeOpacity: 1.0, 82 | strokeWeight: 0.4 83 | }); 84 | path.setMap(pub.map); 85 | 86 | return position; 87 | }, 88 | isPositionUnique: function (position) { 89 | if (pub.markers.length != 0) { 90 | for (var i = 0; i < pub.markers.length; i++) { 91 | var existingMarker = pub.markers[i]; 92 | var pos = existingMarker.getPosition(); 93 | //if a marker already exists in the same position as this marker 94 | if (position.equals(pos)) { 95 | return false; 96 | } 97 | } 98 | } 99 | 100 | return true; 101 | }, 102 | drawMarker: function (place, htmlContent) { 103 | var position = pub.updatePosition(place.geometry.location); 104 | pub.bounds.extend(position); 105 | var marker = new google.maps.Marker({ 106 | map: pub.map, 107 | position: position 108 | }); 109 | bindInfoWindow(marker, pub.map, pub.infoWindow, htmlContent); 110 | pub.markerClusterer.addMarker(marker); 111 | pub.markers.push(marker); 112 | if (pub.nextAddress == pub.geocodeData.length) { 113 | if (pub.userOptions.mapOptions.center) { 114 | pub.map.setCenter(pub.mapOptions.center); 115 | } else { 116 | google.maps.event.addListenerOnce(pub.map, 'bounds_changed', function () { 117 | if (pub.map.getZoom() > 17) { 118 | pub.map.setZoom(17); 119 | } 120 | }); 121 | pub.map.fitBounds(pub.bounds); 122 | } 123 | } 124 | } 125 | }; 126 | 127 | /** 128 | * Setup googleMapManager properties 129 | */ 130 | function initOptions(options) { 131 | var deferred = $.Deferred(); 132 | pub.bounds = new google.maps.LatLngBounds(); 133 | pub.geocoder = new google.maps.Geocoder(); 134 | pub.infoWindow = new google.maps.InfoWindow(pub.infoWindowOptions); 135 | pub.map = null; 136 | pub.markerClusterer = null; 137 | pub.geocodeData = []; 138 | pub.nextAddress = 0; 139 | pub.zeroResult = 0; 140 | pub.markers = []; 141 | pub.userOptions = options; 142 | $.extend(true, pub, options); 143 | deferred.resolve(); 144 | 145 | return deferred; 146 | } 147 | 148 | /** 149 | * Register listeners 150 | */ 151 | function registerListeners() { 152 | for (listener in pub.listeners) { 153 | if (pub.listeners.hasOwnProperty(listener)) { 154 | var object = pub.listeners[listener].object; 155 | var event = pub.listeners[listener].event; 156 | var handler = pub.listeners[listener].handler; 157 | google.maps.event.addListener(pub[object], event, handler); 158 | } 159 | } 160 | } 161 | 162 | /** 163 | * Binds a map marker and infoWindow together on click 164 | * @param marker 165 | * @param map 166 | * @param infoWindow 167 | * @param html 168 | */ 169 | function bindInfoWindow(marker, map, infoWindow, html) { 170 | google.maps.event.addListener(marker, 'click', function () { 171 | infoWindow.setContent(html); 172 | infoWindow.open(map, marker); 173 | }); 174 | } 175 | 176 | function initializeMap() { 177 | var container = document.getElementById(pub.containerId); 178 | container.style.width = '100%'; 179 | container.style.height = '100%'; 180 | pub.map = new google.maps.Map(container, pub.mapOptions); 181 | setTimeout(function () { 182 | pub.markerClusterer = new MarkerClusterer(pub.map, [], {gridSize: 50, maxZoom: 17}); 183 | registerListeners(); 184 | loadMap(); 185 | }, 1000); 186 | } 187 | 188 | /** 189 | * Dynamic call fetchPlaces function with delay 190 | */ 191 | function loadMap() { 192 | setTimeout(function () { 193 | if (pub.nextAddress < pub.geocodeData.length) { 194 | var location = { 195 | country: pub.geocodeData[pub.nextAddress].country, 196 | address: pub.geocodeData[pub.nextAddress].address 197 | }; 198 | var htmlContent = pub.geocodeData[pub.nextAddress].htmlContent; 199 | pub.getAddress(location, htmlContent, loadMap); 200 | pub.nextAddress++; 201 | } 202 | }, pub.delay); 203 | } 204 | 205 | return pub; 206 | })(jQuery); 207 | -------------------------------------------------------------------------------- /assets/markerclusterer_compiled.js: -------------------------------------------------------------------------------- 1 | function MarkerClusterer(t,e,r){this.extend(MarkerClusterer,google.maps.OverlayView),this.map_=t,this.markers_=[],this.clusters_=[],this.sizes=[53,56,66,78,90],this.styles_=[],this.ready_=!1;var s=r||{};this.gridSize_=s.gridSize||60,this.minClusterSize_=s.minimumClusterSize||2,this.maxZoom_=s.maxZoom||null,this.styles_=s.styles||[],this.imagePath_=s.imagePath||this.MARKER_CLUSTER_IMAGE_PATH_,this.imageExtension_=s.imageExtension||this.MARKER_CLUSTER_IMAGE_EXTENSION_,this.zoomOnClick_=!0,void 0!=s.zoomOnClick&&(this.zoomOnClick_=s.zoomOnClick),this.averageCenter_=!1,void 0!=s.averageCenter&&(this.averageCenter_=s.averageCenter),this.setupStyles_(),this.setMap(t),this.prevZoom_=this.map_.getZoom();var o=this;google.maps.event.addListener(this.map_,"zoom_changed",function(){var t=o.map_.getZoom();o.prevZoom_!=t&&(o.prevZoom_=t,o.resetViewport())}),google.maps.event.addListener(this.map_,"idle",function(){o.redraw()}),e&&e.length&&this.addMarkers(e,!1)}function Cluster(t){this.markerClusterer_=t,this.map_=t.getMap(),this.gridSize_=t.getGridSize(),this.minClusterSize_=t.getMinClusterSize(),this.averageCenter_=t.isAverageCenter(),this.center_=null,this.markers_=[],this.bounds_=null,this.clusterIcon_=new ClusterIcon(this,t.getStyles(),t.getGridSize())}function ClusterIcon(t,e,r){t.getMarkerClusterer().extend(ClusterIcon,google.maps.OverlayView),this.styles_=e,this.padding_=r||0,this.cluster_=t,this.center_=null,this.map_=t.getMap(),this.div_=null,this.sums_=null,this.visible_=!1,this.setMap(this.map_)}MarkerClusterer.prototype.MARKER_CLUSTER_IMAGE_PATH_="../images/m",MarkerClusterer.prototype.MARKER_CLUSTER_IMAGE_EXTENSION_="png",MarkerClusterer.prototype.extend=function(t,e){return function(t){for(var e in t.prototype)this.prototype[e]=t.prototype[e];return this}.apply(t,[e])},MarkerClusterer.prototype.onAdd=function(){this.setReady_(!0)},MarkerClusterer.prototype.draw=function(){},MarkerClusterer.prototype.setupStyles_=function(){if(!this.styles_.length)for(var t,e=0;t=this.sizes[e];e++)this.styles_.push({url:this.imagePath_+(e+1)+"."+this.imageExtension_,height:t,width:t})},MarkerClusterer.prototype.fitMapToMarkers=function(){for(var t,e=this.getMarkers(),r=new google.maps.LatLngBounds,s=0;t=e[s];s++)r.extend(t.getPosition());this.map_.fitBounds(r)},MarkerClusterer.prototype.setStyles=function(t){this.styles_=t},MarkerClusterer.prototype.getStyles=function(){return this.styles_},MarkerClusterer.prototype.isZoomOnClick=function(){return this.zoomOnClick_},MarkerClusterer.prototype.isAverageCenter=function(){return this.averageCenter_},MarkerClusterer.prototype.getMarkers=function(){return this.markers_},MarkerClusterer.prototype.getTotalMarkers=function(){return this.markers_.length},MarkerClusterer.prototype.setMaxZoom=function(t){this.maxZoom_=t},MarkerClusterer.prototype.getMaxZoom=function(){return this.maxZoom_},MarkerClusterer.prototype.calculator_=function(t,e){for(var r=0,s=t.length,o=s;0!==o;)o=parseInt(o/10,10),r++;return r=Math.min(r,e),{text:s,index:r}},MarkerClusterer.prototype.setCalculator=function(t){this.calculator_=t},MarkerClusterer.prototype.getCalculator=function(){return this.calculator_},MarkerClusterer.prototype.addMarkers=function(t,e){for(var r,s=0;r=t[s];s++)this.pushMarkerTo_(r);e||this.redraw()},MarkerClusterer.prototype.pushMarkerTo_=function(t){if(t.isAdded=!1,t.draggable){var e=this;google.maps.event.addListener(t,"dragend",function(){t.isAdded=!1,e.repaint()})}this.markers_.push(t)},MarkerClusterer.prototype.addMarker=function(t,e){this.pushMarkerTo_(t),e||this.redraw()},MarkerClusterer.prototype.removeMarker_=function(t){var e=-1;if(this.markers_.indexOf)e=this.markers_.indexOf(t);else for(var r,s=0;r=this.markers_[s];s++)if(r==t){e=s;break}return e!=-1&&(t.setMap(null),this.markers_.splice(e,1),!0)},MarkerClusterer.prototype.removeMarker=function(t,e){var r=this.removeMarker_(t);return!(e||!r)&&(this.resetViewport(),this.redraw(),!0)},MarkerClusterer.prototype.removeMarkers=function(t,e){for(var r,s=!1,o=0;r=t[o];o++){var i=this.removeMarker_(r);s=s||i}if(!e&&s)return this.resetViewport(),this.redraw(),!0},MarkerClusterer.prototype.setReady_=function(t){this.ready_||(this.ready_=t,this.createClusters_())},MarkerClusterer.prototype.getTotalClusters=function(){return this.clusters_.length},MarkerClusterer.prototype.getMap=function(){return this.map_},MarkerClusterer.prototype.setMap=function(t){this.map_=t},MarkerClusterer.prototype.getGridSize=function(){return this.gridSize_},MarkerClusterer.prototype.setGridSize=function(t){this.gridSize_=t},MarkerClusterer.prototype.getMinClusterSize=function(){return this.minClusterSize_},MarkerClusterer.prototype.setMinClusterSize=function(t){this.minClusterSize_=t},MarkerClusterer.prototype.getExtendedBounds=function(t){var e=this.getProjection(),r=new google.maps.LatLng(t.getNorthEast().lat(),t.getNorthEast().lng()),s=new google.maps.LatLng(t.getSouthWest().lat(),t.getSouthWest().lng()),o=e.fromLatLngToDivPixel(r);o.x+=this.gridSize_,o.y-=this.gridSize_;var i=e.fromLatLngToDivPixel(s);i.x-=this.gridSize_,i.y+=this.gridSize_;var n=e.fromDivPixelToLatLng(o),a=e.fromDivPixelToLatLng(i);return t.extend(n),t.extend(a),t},MarkerClusterer.prototype.isMarkerInBounds_=function(t,e){return e.contains(t.getPosition())},MarkerClusterer.prototype.clearMarkers=function(){this.resetViewport(!0),this.markers_=[]},MarkerClusterer.prototype.resetViewport=function(t){for(var e,r=0;e=this.clusters_[r];r++)e.remove();for(var s,r=0;s=this.markers_[r];r++)s.isAdded=!1,t&&s.setMap(null);this.clusters_=[]},MarkerClusterer.prototype.repaint=function(){var t=this.clusters_.slice();this.clusters_.length=0,this.resetViewport(),this.redraw(),window.setTimeout(function(){for(var e,r=0;e=t[r];r++)e.remove()},0)},MarkerClusterer.prototype.redraw=function(){this.createClusters_()},MarkerClusterer.prototype.distanceBetweenPoints_=function(t,e){if(!t||!e)return 0;var r=6371,s=(e.lat()-t.lat())*Math.PI/180,o=(e.lng()-t.lng())*Math.PI/180,i=Math.sin(s/2)*Math.sin(s/2)+Math.cos(t.lat()*Math.PI/180)*Math.cos(e.lat()*Math.PI/180)*Math.sin(o/2)*Math.sin(o/2),n=2*Math.atan2(Math.sqrt(i),Math.sqrt(1-i)),a=r*n;return a},MarkerClusterer.prototype.addToClosestCluster_=function(t){for(var e,r=4e4,s=null,o=(t.getPosition(),0);e=this.clusters_[o];o++){var i=e.getCenter();if(i){var n=this.distanceBetweenPoints_(i,t.getPosition());n=this.minClusterSize_&&t.setMap(null),this.updateIcon(),!0},Cluster.prototype.getMarkerClusterer=function(){return this.markerClusterer_},Cluster.prototype.getBounds=function(){for(var t,e=new google.maps.LatLngBounds(this.center_,this.center_),r=this.getMarkers(),s=0;t=r[s];s++)e.extend(t.getPosition());return e},Cluster.prototype.remove=function(){this.clusterIcon_.remove(),this.markers_.length=0,delete this.markers_},Cluster.prototype.getSize=function(){return this.markers_.length},Cluster.prototype.getMarkers=function(){return this.markers_},Cluster.prototype.getCenter=function(){return this.center_},Cluster.prototype.calculateBounds_=function(){var t=new google.maps.LatLngBounds(this.center_,this.center_);this.bounds_=this.markerClusterer_.getExtendedBounds(t)},Cluster.prototype.isMarkerInClusterBounds=function(t){return this.bounds_.contains(t.getPosition())},Cluster.prototype.getMap=function(){return this.map_},Cluster.prototype.updateIcon=function(){var t=this.map_.getZoom(),e=this.markerClusterer_.getMaxZoom();if(e&&t>e)for(var r,s=0;r=this.markers_[s];s++)r.setMap(this.map_);else{if(this.markers_.length0&&this.anchor_[0]0&&this.anchor_[1]