r114484 MediaWiki - Code Review archive

Repository:MediaWiki
Revision:r114483‎ | r114484 | r114485 >
Date:11:23, 26 March 2012
Author:netbrain
Status:deferred
Tags:
Comment:
Merge branch 'backup'
Modified paths:
  • /trunk/extensions/Maps/Maps.i18n.magic.php (modified) (history)
  • /trunk/extensions/Maps/Maps.php (modified) (history)
  • /trunk/extensions/Maps/Maps.settings.php (modified) (history)
  • /trunk/extensions/Maps/Maps_Settings.php (modified) (history)
  • /trunk/extensions/Maps/includes/Maps_Line.php (added) (history)
  • /trunk/extensions/Maps/includes/criteria/CriterionLine.php (added) (history)
  • /trunk/extensions/Maps/includes/features/Maps_BasePointLineMap.php (added) (history)
  • /trunk/extensions/Maps/includes/manipulations/Maps_ParamLine.php (added) (history)
  • /trunk/extensions/Maps/includes/parserHooks/Maps_DisplayLine.php (added) (history)
  • /trunk/extensions/Maps/includes/services/GoogleMaps3/GoogleMaps3.php (modified) (history)
  • /trunk/extensions/Maps/includes/services/GoogleMaps3/jquery.googlemap.js (modified) (history)
  • /trunk/extensions/Maps/includes/services/GoogleMaps3/markerclusterer.js (added) (history)
  • /trunk/extensions/Maps/includes/services/OpenLayers/OpenLayers.php (modified) (history)
  • /trunk/extensions/Maps/includes/services/OpenLayers/jquery.openlayers.js (modified) (history)

Diff [purge]

Index: trunk/extensions/Maps/Maps.i18n.magic.php
@@ -13,6 +13,7 @@
1414 'display_map' => array( 0, 'display_map' ),
1515 'display_point' => array( 0, 'display_point' ),
1616 'display_points' => array( 0, 'display_points' ),
 17+ 'display_line' => array( 0, 'display_line' ),
1718 'geocode' => array( 0, 'geocode' ),
1819 'geodistance' => array( 0, 'geodistance' ),
1920 'finddestination' => array( 0, 'finddestination' ),
Index: trunk/extensions/Maps/Maps.settings.php
@@ -31,7 +31,8 @@
3232 'defaultServices',
3333 array(
3434 'display_point' => 'googlemaps3',
35 - 'display_map' => 'googlemaps3'
 35+ 'display_map' => 'googlemaps3',
 36+ 'display_line' => 'googlemaps3'
3637 )
3738 ),
3839
Index: trunk/extensions/Maps/Maps.php
@@ -80,6 +80,7 @@
8181 $wgAutoloadClasses['MapsLayerPage'] = $incDir . 'Maps_LayerPage.php';
8282 $wgAutoloadClasses['MapsLayers'] = $incDir . 'Maps_Layers.php';
8383 $wgAutoloadClasses['MapsLocation'] = $incDir . 'Maps_Location.php';
 84+$wgAutoloadClasses['MapsLine'] = $incDir . 'Maps_Line.php';
8485 $wgAutoloadClasses['iMappingService'] = $incDir . 'iMappingService.php';
8586 $wgAutoloadClasses['MapsMappingServices'] = $incDir . 'Maps_MappingServices.php';
8687 $wgAutoloadClasses['MapsMappingService'] = $incDir . 'Maps_MappingService.php';
@@ -93,12 +94,14 @@
9495 $wgAutoloadClasses['CriterionIsLocation'] = $criDir . 'CriterionIsLocation.php';
9596 $wgAutoloadClasses['CriterionMapDimension'] = $criDir . 'CriterionMapDimension.php';
9697 $wgAutoloadClasses['CriterionMapLayer'] = $criDir . 'CriterionMapLayer.php';
 98+$wgAutoloadClasses['CriterionLine'] = $criDir . 'CriterionLine.php';
9799 unset( $criDir );
98100
99101 // Autoload the "includes/features/" classes.
100102 $ftDir = $incDir . '/features/';
101103 $wgAutoloadClasses['MapsBaseMap'] = $ftDir . 'Maps_BaseMap.php';
102 -$wgAutoloadClasses['MapsBasePointMap'] = $ftDir . 'Maps_BasePointMap.php';
 104+$wgAutoloadClasses['MapsBasePointMap'] = $ftDir . 'Maps_BasePointMap.php';
 105+$wgAutoloadClasses['MapsBasePointLineMap'] = $ftDir . 'Maps_BasePointLineMap.php';
103106 unset( $ftDir );
104107
105108 // Autoload the "includes/geocoders/" classes.
@@ -122,6 +125,7 @@
123126 $wgAutoloadClasses['MapsParamLocation'] = $manDir . 'Maps_ParamLocation.php';
124127 $wgAutoloadClasses['MapsParamService'] = $manDir . 'Maps_ParamService.php';
125128 $wgAutoloadClasses['MapsParamZoom'] = $manDir . 'Maps_ParamZoom.php';
 129+$wgAutoloadClasses['MapsParamLine'] = $manDir . 'Maps_ParamLine.php';
126130 unset( $manDir );
127131
128132 // Autoload the "includes/parserHooks/" classes.
@@ -129,6 +133,7 @@
130134 $wgAutoloadClasses['MapsCoordinates'] = $phDir . 'Maps_Coordinates.php';
131135 $wgAutoloadClasses['MapsDisplayMap'] = $phDir . 'Maps_DisplayMap.php';
132136 $wgAutoloadClasses['MapsDisplayPoint'] = $phDir . 'Maps_DisplayPoint.php';
 137+$wgAutoloadClasses['MapsDisplayLine'] = $phDir . 'Maps_DisplayLine.php';
133138 $wgAutoloadClasses['MapsDistance'] = $phDir . 'Maps_Distance.php';
134139 $wgAutoloadClasses['MapsFinddestination'] = $phDir . 'Maps_Finddestination.php';
135140 $wgAutoloadClasses['MapsGeocode'] = $phDir . 'Maps_Geocode.php';
@@ -165,6 +170,7 @@
166171
167172 $egMapsFeatures['pf'][] = 'MapsDisplayMap::initialize';
168173 $egMapsFeatures['pf'][] = 'MapsDisplayPoint::initialize';
 174+$egMapsFeatures['pf'][] = 'MapsDisplayLine::initialize';
169175
170176 # Parser hooks
171177
@@ -174,6 +180,8 @@
175181 $wgHooks['ParserFirstCallInit'][] = 'MapsDisplayMap::staticInit';
176182 # Required for #display_point.
177183 $wgHooks['ParserFirstCallInit'][] = 'MapsDisplayPoint::staticInit';
 184+ # Required for #display_line.
 185+ $wgHooks['ParserFirstCallInit'][] = 'MapsDisplayLine::staticInit';
178186 # Required for #distance.
179187 $wgHooks['ParserFirstCallInit'][] = 'MapsDistance::staticInit';
180188 # Required for #finddestination.
Index: trunk/extensions/Maps/Maps_Settings.php
@@ -44,7 +44,8 @@
4545 # for, since it's used as a fallback mechanism.
4646 $egMapsDefaultServices = array(
4747 'display_point' => $egMapsDefaultService,
48 - 'display_map' => $egMapsDefaultService
 48+ 'display_map' => $egMapsDefaultService,
 49+ 'display_line' => $egMapsDefaultService
4950 );
5051
5152
Index: trunk/extensions/Maps/includes/Maps_Line.php
@@ -0,0 +1,185 @@
 2+<?php
 3+/**
 4+ * Class that holds metadata on lines made up by locations on map.
 5+ *
 6+ * @since 0.7.2
 7+ *
 8+ * @file Maps_Line.php
 9+ * @ingroup Maps
 10+ *
 11+ * @licence GNU GPL v3
 12+ * @author Kim Eik < kim@heldig.org >
 13+ */
 14+class MapsLine{
 15+
 16+
 17+ /**
 18+ * @var
 19+ */
 20+ protected $lineCoords;
 21+
 22+ /**
 23+ * @var
 24+ */
 25+ protected $title;
 26+
 27+ /**
 28+ * @var
 29+ */
 30+ protected $text;
 31+
 32+ /**
 33+ * @var
 34+ */
 35+ protected $strokeColor;
 36+ /**
 37+ * @var
 38+ */
 39+ protected $strokeOpacity;
 40+ /**
 41+ * @var
 42+ */
 43+ protected $strokeWeight;
 44+
 45+ /**
 46+ *
 47+ */
 48+ function __construct($coords)
 49+ {
 50+ $this->setLineCoords($coords);
 51+ }
 52+
 53+ /**
 54+ * @param \text $text
 55+ */
 56+ public function setText($text)
 57+ {
 58+ $this->text = $text;
 59+ }
 60+
 61+ /**
 62+ * @return \text
 63+ */
 64+ public function getText()
 65+ {
 66+ return $this->text;
 67+ }
 68+
 69+ /**
 70+ * @param \title $title
 71+ */
 72+ public function setTitle($title)
 73+ {
 74+ $this->title = $title;
 75+ }
 76+
 77+ /**
 78+ * @return \title
 79+ */
 80+ public function getTitle()
 81+ {
 82+ return $this->title;
 83+ }
 84+
 85+ protected function setLineCoords($lineCoords)
 86+ {
 87+ foreach($lineCoords as $lineCoord){
 88+ $this->lineCoords[] = new MapsLocation($lineCoord);
 89+ }
 90+ }
 91+
 92+ protected function getLineCoords()
 93+ {
 94+ return $this->lineCoords;
 95+ }
 96+
 97+ public function getJSONObject( $defText = '', $defTitle = '') {
 98+ $posArray = array();
 99+ foreach ($this->lineCoords as $mapLocation){
 100+ $posArray[] = array(
 101+ 'lat' => $mapLocation->getLatitude(),
 102+ 'lon' => $mapLocation->getLongitude()
 103+ );
 104+ }
 105+
 106+ return array(
 107+ 'pos' => $posArray,
 108+ 'text' => $this->hasText() ? $this->getText() : $defText,
 109+ 'title' => $this->hasTitle() ? $this->getTitle() : $defTitle,
 110+ 'strokeColor' => $this->hasStrokeColor() ? $this->getStrokeColor() : '#FF0000',
 111+ 'strokeOpacity' => $this->hasStrokeOpacity() ? $this->getStrokeOpacity() : '1',
 112+ 'strokeWeight' => $this->hasStrokeWeight() ? $this->getStrokeWeight() : '2'
 113+ );
 114+ }
 115+
 116+
 117+ /**
 118+ * @param $strokeColor
 119+ */
 120+ public function setStrokeColor($strokeColor)
 121+ {
 122+ $this->strokeColor = $strokeColor;
 123+ }
 124+
 125+ /**
 126+ * @return
 127+ */
 128+ public function getStrokeColor()
 129+ {
 130+ return $this->strokeColor;
 131+ }
 132+
 133+ /**
 134+ * @param $strokeOpacity
 135+ */
 136+ public function setStrokeOpacity($strokeOpacity)
 137+ {
 138+ $this->strokeOpacity = $strokeOpacity;
 139+ }
 140+
 141+ /**
 142+ * @return
 143+ */
 144+ public function getStrokeOpacity()
 145+ {
 146+ return $this->strokeOpacity;
 147+ }
 148+
 149+ /**
 150+ * @param $strokeWeight
 151+ */
 152+ public function setStrokeWeight($strokeWeight)
 153+ {
 154+ $this->strokeWeight = $strokeWeight;
 155+ }
 156+
 157+ /**
 158+ * @return
 159+ */
 160+ public function getStrokeWeight()
 161+ {
 162+ return $this->strokeWeight;
 163+ }
 164+
 165+
 166+ public function hasText(){
 167+ return $this->text !== '';
 168+ }
 169+
 170+ public function hasTitle(){
 171+ return $this->title !== '';
 172+ }
 173+
 174+ public function hasStrokeColor(){
 175+ return $this->strokeColor !== '';
 176+ }
 177+
 178+ public function hasStrokeOpacity(){
 179+ return $this->strokeOpacity !== '';
 180+ }
 181+
 182+
 183+ public function hasStrokeWeight(){
 184+ return $this->strokeWeight !== '';
 185+ }
 186+}
Index: trunk/extensions/Maps/includes/parserHooks/Maps_DisplayLine.php
@@ -0,0 +1,100 @@
 2+<?php
 3+
 4+/**
 5+ * Class for the 'display_line' parser hooks.
 6+ *
 7+ * @since 0.7
 8+ *
 9+ * @file Maps_DisplayLine.php
 10+ * @ingroup Maps
 11+ *
 12+ * @author Kim Eik
 13+ */
 14+class MapsDisplayLine extends MapsDisplayPoint {
 15+
 16+ /**
 17+ * No LSB in pre-5.3 PHP *sigh*.
 18+ * This is to be refactored as soon as php >=5.3 becomes acceptable.
 19+ */
 20+ public static function staticInit( Parser &$parser ) {
 21+ $instance = new self;
 22+ return $instance->init( $parser );
 23+ }
 24+
 25+
 26+ /**
 27+ * Gets the name of the parser hook.
 28+ *
 29+ * @since 0.4
 30+ *
 31+ * @return string or array of string
 32+ */
 33+ protected function getName(){
 34+ return 'display_line';
 35+ }
 36+
 37+ /**
 38+ * @param array $parameters
 39+ * @return mixed
 40+ */
 41+ public function render( array $parameters ) {
 42+ // Get the instance of the service class.
 43+ $service = MapsMappingServices::getServiceInstance($parameters['mappingservice']);
 44+
 45+ // Get an instance of the class handling the current parser hook and service.
 46+ $mapClass = $service->getFeatureInstance( $this->getName() );
 47+
 48+ return $mapClass->renderMap( $parameters, $this->parser );
 49+ }
 50+
 51+ /**
 52+ * Returns an array containing the parameter info.
 53+ * @see ParserHook::getParameterInfo
 54+ *
 55+ * @since 0.7
 56+ *
 57+ * @return array
 58+ */
 59+ protected function getParameterInfo( $type ) {
 60+ global $egMapsDefaultServices;
 61+
 62+ $params = parent::getParameterInfo($type);
 63+
 64+ $params['mappingservice']->setDefault( $egMapsDefaultServices[$this->getName()] );
 65+ $params['mappingservice']->addManipulations( new MapsParamService( $this->getName() ) );
 66+
 67+ $params['lines'] = new ListParameter( 'lines', ';' );
 68+ $params['lines']->setDefault(array());
 69+ $params['lines']->addCriteria(new CriterionLine());
 70+ $params['lines']->addManipulations( new MapsParamLine() );
 71+
 72+ $params['copycoords'] = new Parameter(
 73+ 'copycoords',
 74+ Parameter::TYPE_BOOLEAN
 75+ );
 76+ $params['copycoords']->setDefault(false);
 77+ $params['copycoords']->setDoManipulationOfDefault( false );
 78+
 79+
 80+ $params['markercluster'] = new Parameter(
 81+ 'markercluster',
 82+ Parameter::TYPE_BOOLEAN
 83+ );
 84+ $params['markercluster']->setDefault(false);
 85+ $params['markercluster']->setDoManipulationOfDefault( false );
 86+
 87+ return $params;
 88+ }
 89+
 90+ /**
 91+ * Returns the list of default parameters.
 92+ * @see ParserHook::getDefaultParameters
 93+ *
 94+ * @since 0.7
 95+ *
 96+ * @return array
 97+ */
 98+ protected function getDefaultParameters( $type ) {
 99+ return array( 'coordinates','lines' );
 100+ }
 101+}
\ No newline at end of file
Index: trunk/extensions/Maps/includes/criteria/CriterionLine.php
@@ -0,0 +1,84 @@
 2+<?php
 3+
 4+/**
 5+ * Parameter criterion stating that the value must be a set of coordinates or an address.
 6+ *
 7+ * @since 0.7
 8+ *
 9+ * @file CriterionLine.php
 10+ * @ingroup Maps
 11+ * @ingroup Criteria
 12+ *
 13+ * @author Kim Eik
 14+ */
 15+
 16+class CriterionLine extends ItemParameterCriterion
 17+{
 18+
 19+ /**
 20+ * Returns true if the parameter value contains atleast 1 comma
 21+ * meaning that there are atleast two enpoints on which to draw a line.
 22+ *
 23+ * @param string $value
 24+ * @param Parameter $parameter
 25+ * @param array $parameters
 26+ *
 27+ * @since 0.4
 28+ *
 29+ * @return boolean
 30+ */
 31+ protected function doValidation($value, Parameter $parameter, array $parameters)
 32+ {
 33+ //need atleast two points to create a line
 34+ $valid = strpos($value, ':') != false;
 35+ if (!$valid) {
 36+ return $valid;
 37+ }
 38+
 39+ //setup geocode deps
 40+ $canGeoCode = MapsGeocoders::canGeocode();
 41+ if ($canGeoCode) {
 42+ $geoService = $parameter->hasDependency('geoservice') ? $parameters['geoservice']->getValue() : '';
 43+ $mappingService = $parameter->hasDependency('mappingservice') ? $parameters['mappingservice']->getValue() : false;
 44+ }
 45+
 46+ //strip away line parameters and check for valid locations
 47+ $parts = preg_split('/[:]/', $value);
 48+ foreach ($parts as $part) {
 49+ $toIndex = strpos($part, '|');
 50+ if ($toIndex != false) {
 51+ $part = substr($part, 0, $toIndex);
 52+ }
 53+
 54+ if($canGeoCode){
 55+ $valid = MapsGeocoders::isLocation(
 56+ $part,
 57+ $geoService,
 58+ $mappingService
 59+ );
 60+ } else {
 61+ $valid = MapsCoordinateParser::areCoordinates($part);
 62+ }
 63+
 64+ if(!$valid){
 65+ break;
 66+ }
 67+ }
 68+ return $valid;
 69+ }
 70+
 71+ /**
 72+ * Gets an internationalized error message to construct a ValidationError with
 73+ * when the criteria validation failed. (for non-list values)
 74+ *
 75+ * @param Parameter $parameter
 76+ *
 77+ * @since 0.4
 78+ *
 79+ * @return string
 80+ */
 81+ protected function getItemErrorMessage(Parameter $parameter)
 82+ {
 83+ return wfMsgExt('validation-error-invalid-line-param', 'parsemag', $parameter->getOriginalName());
 84+ }
 85+}
\ No newline at end of file
Index: trunk/extensions/Maps/includes/features/Maps_BasePointLineMap.php
@@ -0,0 +1,30 @@
 2+<?php
 3+
 4+/**
 5+ *
 6+ * @file Maps_BasePointLineMap.php
 7+ * @ingroup Maps
 8+ *
 9+ * @author Kim Eik
 10+ */
 11+class MapsBasePointLineMap extends MapsBasePointMap{
 12+
 13+ protected function handleMarkerData(array &$params, Parser $parser)
 14+ {
 15+ parent::handleMarkerData($params, $parser);
 16+
 17+ $parserClone = clone $parser;
 18+
 19+ foreach($params['lines'] as &$line){
 20+
 21+ $line['title'] = $parserClone->parse( $line['title'], $parserClone->getTitle(), new ParserOptions() )->getText();
 22+ $line['text'] = $parserClone->parse( $line['text'], $parserClone->getTitle(), new ParserOptions() )->getText();
 23+
 24+ $hasTitleAndtext = $line['title'] !== '' && $line['text'] !== '';
 25+ $line['text'] = ( $hasTitleAndtext ? '<b>' . $line['title'] . '</b><hr />' : $line['title'] ) . $line['text'];
 26+ $line['title'] = strip_tags( $line['title'] );
 27+ }
 28+ }
 29+
 30+
 31+}
Index: trunk/extensions/Maps/includes/services/GoogleMaps3/jquery.googlemap.js
@@ -10,6 +10,7 @@
1111 var _this = this;
1212
1313 this.map = null;
 14+ this.markercluster = null;
1415 this.options = options;
1516
1617 /**
@@ -20,6 +21,13 @@
2122 this.markers = [];
2223
2324 /**
 25+ * All Polylines currently on the map,
 26+ * @type {Array}
 27+ * @private
 28+ */
 29+ this.lines = [];
 30+
 31+ /**
2432 * Creates a new marker with the provided data,
2533 * adds it to the map, and returns it.
2634 * @param {Object} markerData Contains the fields lat, lon, title, text and icon
@@ -133,6 +141,55 @@
134142 }
135143 };
136144
 145+ this.addLine = function(properties){
 146+ var paths = new google.maps.MVCArray();
 147+ for(var x = 0; x < properties.pos.length; x++){
 148+ paths.push(new google.maps.LatLng( properties.pos[x].lat , properties.pos[x].lon ));
 149+ }
 150+
 151+ var line = new google.maps.Polyline({
 152+ map:this.map,
 153+ path:paths,
 154+ strokeColor:properties.strokeColor,
 155+ strokeOpacity:properties.strokeOpacity,
 156+ strokeWeight:properties.strokeWeight
 157+ });
 158+ this.lines.push(line);
 159+
 160+ google.maps.event.addListener(line,"click", function(event){
 161+ if (this.openWindow != undefined) {
 162+ this.openWindow.close();
 163+ }
 164+ this.openWindow = new google.maps.InfoWindow();
 165+ this.openWindow.content = properties.text;
 166+ this.openWindow.position = event.latLng;
 167+ this.openWindow.closeclick = function() {
 168+ line.openWindow = undefined;
 169+ };
 170+ this.openWindow.open(_this.map);
 171+ });
 172+ };
 173+
 174+ this.removeLine = function(line){
 175+ line.setMap( null );
 176+
 177+ for ( var i = this.line.length - 1; i >= 0; i-- ) {
 178+ if ( this.line[i] === line ) {
 179+ delete this.line[i];
 180+ break;
 181+ }
 182+ }
 183+
 184+ delete line;
 185+ };
 186+
 187+ this.removeLines = function(){
 188+ for ( var i = this.lines.length - 1; i >= 0; i-- ) {
 189+ this.lines[i].setMap( null );
 190+ }
 191+ this.lines = [];
 192+ }
 193+
137194 this.setup = function() {
138195 var showEarth = $.inArray( 'earth', options.types ) !== -1;
139196
@@ -311,8 +368,48 @@
312369 _this.resizable();
313370 } );
314371 }
 372+
 373+ /**
 374+ * used in display_line functionality
 375+ * draws paths between markers
 376+ */
 377+ if(options.lines){
 378+ for ( var i = 0; i < options.lines.length; i++ ) {
 379+ this.addLine(options.lines[i]);
 380+ }
 381+ }
 382+
 383+ /**
 384+ * used in display_line functionality
 385+ * allows the copy to clipboard of coordinates
 386+ */
 387+ if(options.copycoords){
 388+ function addRightClickListener(object){
 389+ google.maps.event.addListener( object, 'rightclick', function(event) {
 390+ prompt("CTRL+C, ENTER",event.latLng.Sa+','+event.latLng.Ta);
 391+ });
 392+ }
 393+
 394+ for(var x = 0; x < this.markers.length; x++){
 395+ addRightClickListener(this.markers[x]);
 396+ }
 397+
 398+ for(var x = 0; x < this.lines.length; x++){
 399+ addRightClickListener(this.lines[x]);
 400+ }
 401+
 402+ addRightClickListener(this.map);
 403+ }
 404+
 405+ /**
 406+ * used in display_line functionality
 407+ * allows grouping of markers
 408+ */
 409+ if(options.markercluster){
 410+ this.markercluster = new MarkerClusterer(this.map,this.markers);
 411+ }
 412+
315413 };
316 -
317414 this.setup();
318415
319416 return this;
Index: trunk/extensions/Maps/includes/services/GoogleMaps3/markerclusterer.js
@@ -0,0 +1,1596 @@
 2+/*jslint browser: true, confusion: true, sloppy: true, vars: true, nomen: false, plusplus: false, indent: 2 */
 3+/*global window,google */
 4+
 5+/**
 6+ * @name MarkerClustererPlus for Google Maps V3
 7+ * @version 2.0.9 [February 20, 2012]
 8+ * @author Gary Little
 9+ * @fileoverview
 10+ * The library creates and manages per-zoom-level clusters for large amounts of markers.
 11+ * <p>
 12+ * This is an enhanced V3 implementation of the
 13+ * <a href="http://gmaps-utility-library-dev.googlecode.com/svn/tags/markerclusterer/"
 14+ * >V2 MarkerClusterer</a> by Xiaoxi Wu. It is based on the
 15+ * <a href="http://google-maps-utility-library-v3.googlecode.com/svn/tags/markerclusterer/"
 16+ * >V3 MarkerClusterer</a> port by Luke Mahe. MarkerClustererPlus was created by Gary Little.
 17+ * <p>
 18+ * v2.0 release: MarkerClustererPlus v2.0 is backward compatible with MarkerClusterer v1.0. It
 19+ * adds support for the <code>ignoreHidden</code>, <code>title</code>, <code>printable</code>,
 20+ * <code>batchSizeIE</code>, and <code>calculator</code> properties as well as support for
 21+ * four more events. It also allows greater control over the styling of the text that appears
 22+ * on the cluster marker. The documentation has been significantly improved and the overall
 23+ * code has been simplified and polished. Very large numbers of markers can now be managed
 24+ * without causing Javascript timeout errors on Internet Explorer. Note that the name of the
 25+ * <code>clusterclick</code> event has been deprecated. The new name is <code>click</code>,
 26+ * so please change your application code now.
 27+ */
 28+
 29+/**
 30+ * Licensed under the Apache License, Version 2.0 (the "License");
 31+ * you may not use this file except in compliance with the License.
 32+ * You may obtain a copy of the License at
 33+ *
 34+ * http://www.apache.org/licenses/LICENSE-2.0
 35+ *
 36+ * Unless required by applicable law or agreed to in writing, software
 37+ * distributed under the License is distributed on an "AS IS" BASIS,
 38+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 39+ * See the License for the specific language governing permissions and
 40+ * limitations under the License.
 41+ */
 42+
 43+
 44+/**
 45+ * @name ClusterIconStyle
 46+ * @class This class represents the object for values in the <code>styles</code> array passed
 47+ * to the {@link MarkerClusterer} constructor. The element in this array that is used to
 48+ * style the cluster icon is determined by calling the <code>calculator</code> function.
 49+ *
 50+ * @property {string} url The URL of the cluster icon image file. Required.
 51+ * @property {number} height The height (in pixels) of the cluster icon. Required.
 52+ * @property {number} width The width (in pixels) of the cluster icon. Required.
 53+ * @property {Array} [anchor] The anchor position (in pixels) of the label text to be shown on
 54+ * the cluster icon, relative to the top left corner of the icon.
 55+ * The format is <code>[yoffset, xoffset]</code>. The <code>yoffset</code> must be positive
 56+ * and less than <code>height</code> and the <code>xoffset</code> must be positive and less
 57+ * than <code>width</code>. The default is to anchor the label text so that it is centered
 58+ * on the icon.
 59+ * @property {Array} [anchorIcon] The anchor position (in pixels) of the cluster icon. This is the
 60+ * spot on the cluster icon that is to be aligned with the cluster position. The format is
 61+ * <code>[yoffset, xoffset]</code> where <code>yoffset</code> increases as you go down and
 62+ * <code>xoffset</code> increases to the right. The default anchor position is the center of the
 63+ * cluster icon.
 64+ * @property {string} [textColor="black"] The color of the label text shown on the
 65+ * cluster icon.
 66+ * @property {number} [textSize=11] The size (in pixels) of the label text shown on the
 67+ * cluster icon.
 68+ * @property {number} [textDecoration="none"] The value of the CSS <code>text-decoration</code>
 69+ * property for the label text shown on the cluster icon.
 70+ * @property {number} [fontWeight="bold"] The value of the CSS <code>font-weight</code>
 71+ * property for the label text shown on the cluster icon.
 72+ * @property {number} [fontStyle="normal"] The value of the CSS <code>font-style</code>
 73+ * property for the label text shown on the cluster icon.
 74+ * @property {number} [fontFamily="Arial,sans-serif"] The value of the CSS <code>font-family</code>
 75+ * property for the label text shown on the cluster icon.
 76+ * @property {string} [backgroundPosition="0 0"] The position of the cluster icon image
 77+ * within the image defined by <code>url</code>. The format is <code>"xpos ypos"</code>
 78+ * (the same format as for the CSS <code>background-position</code> property). You must set
 79+ * this property appropriately when the image defined by <code>url</code> represents a sprite
 80+ * containing multiple images.
 81+ */
 82+/**
 83+ * @name ClusterIconInfo
 84+ * @class This class is an object containing general information about a cluster icon. This is
 85+ * the object that a <code>calculator</code> function returns.
 86+ *
 87+ * @property {string} text The text of the label to be shown on the cluster icon.
 88+ * @property {number} index The index plus 1 of the element in the <code>styles</code>
 89+ * array to be used to style the cluster icon.
 90+ */
 91+/**
 92+ * A cluster icon.
 93+ *
 94+ * @constructor
 95+ * @extends google.maps.OverlayView
 96+ * @param {Cluster} cluster The cluster with which the icon is to be associated.
 97+ * @param {Array} [styles] An array of {@link ClusterIconStyle} defining the cluster icons
 98+ * to use for various cluster sizes.
 99+ * @private
 100+ */
 101+function ClusterIcon(cluster, styles) {
 102+ cluster.getMarkerClusterer().extend(ClusterIcon, google.maps.OverlayView);
 103+
 104+ this.cluster_ = cluster;
 105+ this.styles_ = styles;
 106+ this.center_ = null;
 107+ this.div_ = null;
 108+ this.sums_ = null;
 109+ this.visible_ = false;
 110+
 111+ this.setMap(cluster.getMap()); // Note: this causes onAdd to be called
 112+}
 113+
 114+
 115+/**
 116+ * Adds the icon to the DOM.
 117+ */
 118+ClusterIcon.prototype.onAdd = function () {
 119+ var cClusterIcon = this;
 120+ var cMouseDownInCluster;
 121+ var cDraggingMapByCluster;
 122+
 123+ this.div_ = document.createElement("div");
 124+ if (this.visible_) {
 125+ this.show();
 126+ }
 127+
 128+ this.getPanes().overlayMouseTarget.appendChild(this.div_);
 129+
 130+ // Fix for Issue 157
 131+ google.maps.event.addListener(this.getMap(), "bounds_changed", function () {
 132+ cDraggingMapByCluster = cMouseDownInCluster;
 133+ });
 134+
 135+ google.maps.event.addDomListener(this.div_, "mousedown", function () {
 136+ cMouseDownInCluster = true;
 137+ cDraggingMapByCluster = false;
 138+ });
 139+
 140+ google.maps.event.addDomListener(this.div_, "click", function (e) {
 141+ cMouseDownInCluster = false;
 142+ if (!cDraggingMapByCluster) {
 143+ var mz;
 144+ var mc = cClusterIcon.cluster_.getMarkerClusterer();
 145+ /**
 146+ * This event is fired when a cluster marker is clicked.
 147+ * @name MarkerClusterer#click
 148+ * @param {Cluster} c The cluster that was clicked.
 149+ * @event
 150+ */
 151+ google.maps.event.trigger(mc, "click", cClusterIcon.cluster_);
 152+ google.maps.event.trigger(mc, "clusterclick", cClusterIcon.cluster_); // deprecated name
 153+
 154+ // The default click handler follows. Disable it by setting
 155+ // the zoomOnClick property to false.
 156+ if (mc.getZoomOnClick()) {
 157+ // Zoom into the cluster.
 158+ mz = mc.getMaxZoom();
 159+ mc.getMap().fitBounds(cClusterIcon.cluster_.getBounds());
 160+ // Don't zoom beyond the max zoom level
 161+ if (mz !== null && (mc.getMap().getZoom() > mz)) {
 162+ mc.getMap().setZoom(mz + 1);
 163+ }
 164+ }
 165+
 166+ // Prevent event propagation to the map:
 167+ e.cancelBubble = true;
 168+ if (e.stopPropagation) {
 169+ e.stopPropagation();
 170+ }
 171+ }
 172+ });
 173+
 174+ google.maps.event.addDomListener(this.div_, "mouseover", function () {
 175+ var mc = cClusterIcon.cluster_.getMarkerClusterer();
 176+ /**
 177+ * This event is fired when the mouse moves over a cluster marker.
 178+ * @name MarkerClusterer#mouseover
 179+ * @param {Cluster} c The cluster that the mouse moved over.
 180+ * @event
 181+ */
 182+ google.maps.event.trigger(mc, "mouseover", cClusterIcon.cluster_);
 183+ });
 184+
 185+ google.maps.event.addDomListener(this.div_, "mouseout", function () {
 186+ var mc = cClusterIcon.cluster_.getMarkerClusterer();
 187+ /**
 188+ * This event is fired when the mouse moves out of a cluster marker.
 189+ * @name MarkerClusterer#mouseout
 190+ * @param {Cluster} c The cluster that the mouse moved out of.
 191+ * @event
 192+ */
 193+ google.maps.event.trigger(mc, "mouseout", cClusterIcon.cluster_);
 194+ });
 195+};
 196+
 197+
 198+/**
 199+ * Removes the icon from the DOM.
 200+ */
 201+ClusterIcon.prototype.onRemove = function () {
 202+ if (this.div_ && this.div_.parentNode) {
 203+ this.hide();
 204+ google.maps.event.clearInstanceListeners(this.div_);
 205+ this.div_.parentNode.removeChild(this.div_);
 206+ this.div_ = null;
 207+ }
 208+};
 209+
 210+
 211+/**
 212+ * Draws the icon.
 213+ */
 214+ClusterIcon.prototype.draw = function () {
 215+ if (this.visible_) {
 216+ var pos = this.getPosFromLatLng_(this.center_);
 217+ this.div_.style.top = pos.y + "px";
 218+ this.div_.style.left = pos.x + "px";
 219+ }
 220+};
 221+
 222+
 223+/**
 224+ * Hides the icon.
 225+ */
 226+ClusterIcon.prototype.hide = function () {
 227+ if (this.div_) {
 228+ this.div_.style.display = "none";
 229+ }
 230+ this.visible_ = false;
 231+};
 232+
 233+
 234+/**
 235+ * Positions and shows the icon.
 236+ */
 237+ClusterIcon.prototype.show = function () {
 238+ if (this.div_) {
 239+ var pos = this.getPosFromLatLng_(this.center_);
 240+ this.div_.style.cssText = this.createCss(pos);
 241+ if (this.cluster_.printable_) {
 242+ // (Would like to use "width: inherit;" below, but doesn't work with MSIE)
 243+ this.div_.innerHTML = "<img src='" + this.url_ + "'><div style='position: absolute; top: 0px; left: 0px; width: " + this.width_ + "px;'>" + this.sums_.text + "</div>";
 244+ } else {
 245+ this.div_.innerHTML = this.sums_.text;
 246+ }
 247+ this.div_.title = this.cluster_.getMarkerClusterer().getTitle();
 248+ this.div_.style.display = "";
 249+ }
 250+ this.visible_ = true;
 251+};
 252+
 253+
 254+/**
 255+ * Sets the icon styles to the appropriate element in the styles array.
 256+ *
 257+ * @param {ClusterIconInfo} sums The icon label text and styles index.
 258+ */
 259+ClusterIcon.prototype.useStyle = function (sums) {
 260+ this.sums_ = sums;
 261+ var index = Math.max(0, sums.index - 1);
 262+ index = Math.min(this.styles_.length - 1, index);
 263+ var style = this.styles_[index];
 264+ this.url_ = style.url;
 265+ this.height_ = style.height;
 266+ this.width_ = style.width;
 267+ this.anchor_ = style.anchor;
 268+ this.anchorIcon_ = style.anchorIcon || [parseInt(this.height_ / 2, 10), parseInt(this.width_ / 2, 10)];
 269+ this.textColor_ = style.textColor || "black";
 270+ this.textSize_ = style.textSize || 11;
 271+ this.textDecoration_ = style.textDecoration || "none";
 272+ this.fontWeight_ = style.fontWeight || "bold";
 273+ this.fontStyle_ = style.fontStyle || "normal";
 274+ this.fontFamily_ = style.fontFamily || "Arial,sans-serif";
 275+ this.backgroundPosition_ = style.backgroundPosition || "0 0";
 276+};
 277+
 278+
 279+/**
 280+ * Sets the position at which to center the icon.
 281+ *
 282+ * @param {google.maps.LatLng} center The latlng to set as the center.
 283+ */
 284+ClusterIcon.prototype.setCenter = function (center) {
 285+ this.center_ = center;
 286+};
 287+
 288+
 289+/**
 290+ * Creates the cssText style parameter based on the position of the icon.
 291+ *
 292+ * @param {google.maps.Point} pos The position of the icon.
 293+ * @return {string} The CSS style text.
 294+ */
 295+ClusterIcon.prototype.createCss = function (pos) {
 296+ var style = [];
 297+ if (!this.cluster_.printable_) {
 298+ style.push('background-image:url(' + this.url_ + ');');
 299+ style.push('background-position:' + this.backgroundPosition_ + ';');
 300+ }
 301+
 302+ if (typeof this.anchor_ === 'object') {
 303+ if (typeof this.anchor_[0] === 'number' && this.anchor_[0] > 0 &&
 304+ this.anchor_[0] < this.height_) {
 305+ style.push('height:' + (this.height_ - this.anchor_[0]) +
 306+ 'px; padding-top:' + this.anchor_[0] + 'px;');
 307+ } else {
 308+ style.push('height:' + this.height_ + 'px; line-height:' + this.height_ +
 309+ 'px;');
 310+ }
 311+ if (typeof this.anchor_[1] === 'number' && this.anchor_[1] > 0 &&
 312+ this.anchor_[1] < this.width_) {
 313+ style.push('width:' + (this.width_ - this.anchor_[1]) +
 314+ 'px; padding-left:' + this.anchor_[1] + 'px;');
 315+ } else {
 316+ style.push('width:' + this.width_ + 'px; text-align:center;');
 317+ }
 318+ } else {
 319+ style.push('height:' + this.height_ + 'px; line-height:' +
 320+ this.height_ + 'px; width:' + this.width_ + 'px; text-align:center;');
 321+ }
 322+
 323+ style.push('cursor:pointer; top:' + pos.y + 'px; left:' +
 324+ pos.x + 'px; color:' + this.textColor_ + '; position:absolute; font-size:' +
 325+ this.textSize_ + 'px; font-family:' + this.fontFamily_ + '; font-weight:' +
 326+ this.fontWeight_ + '; font-style:' + this.fontStyle_ + '; text-decoration:' +
 327+ this.textDecoration_ + ';');
 328+
 329+ return style.join("");
 330+};
 331+
 332+
 333+/**
 334+ * Returns the position at which to place the DIV depending on the latlng.
 335+ *
 336+ * @param {google.maps.LatLng} latlng The position in latlng.
 337+ * @return {google.maps.Point} The position in pixels.
 338+ */
 339+ClusterIcon.prototype.getPosFromLatLng_ = function (latlng) {
 340+ var pos = this.getProjection().fromLatLngToDivPixel(latlng);
 341+ pos.x -= this.anchorIcon_[1];
 342+ pos.y -= this.anchorIcon_[0];
 343+ return pos;
 344+};
 345+
 346+
 347+/**
 348+ * Creates a single cluster that manages a group of proximate markers.
 349+ * Used internally, do not call this constructor directly.
 350+ * @constructor
 351+ * @param {MarkerClusterer} mc The <code>MarkerClusterer</code> object with which this
 352+ * cluster is associated.
 353+ */
 354+function Cluster(mc) {
 355+ this.markerClusterer_ = mc;
 356+ this.map_ = mc.getMap();
 357+ this.gridSize_ = mc.getGridSize();
 358+ this.minClusterSize_ = mc.getMinimumClusterSize();
 359+ this.averageCenter_ = mc.getAverageCenter();
 360+ this.printable_ = mc.getPrintable();
 361+ this.markers_ = [];
 362+ this.center_ = null;
 363+ this.bounds_ = null;
 364+ this.clusterIcon_ = new ClusterIcon(this, mc.getStyles());
 365+}
 366+
 367+
 368+/**
 369+ * Returns the number of markers managed by the cluster. You can call this from
 370+ * a <code>click</code>, <code>mouseover</code>, or <code>mouseout</code> event handler
 371+ * for the <code>MarkerClusterer</code> object.
 372+ *
 373+ * @return {number} The number of markers in the cluster.
 374+ */
 375+Cluster.prototype.getSize = function () {
 376+ return this.markers_.length;
 377+};
 378+
 379+
 380+/**
 381+ * Returns the array of markers managed by the cluster. You can call this from
 382+ * a <code>click</code>, <code>mouseover</code>, or <code>mouseout</code> event handler
 383+ * for the <code>MarkerClusterer</code> object.
 384+ *
 385+ * @return {Array} The array of markers in the cluster.
 386+ */
 387+Cluster.prototype.getMarkers = function () {
 388+ return this.markers_;
 389+};
 390+
 391+
 392+/**
 393+ * Returns the center of the cluster. You can call this from
 394+ * a <code>click</code>, <code>mouseover</code>, or <code>mouseout</code> event handler
 395+ * for the <code>MarkerClusterer</code> object.
 396+ *
 397+ * @return {google.maps.LatLng} The center of the cluster.
 398+ */
 399+Cluster.prototype.getCenter = function () {
 400+ return this.center_;
 401+};
 402+
 403+
 404+/**
 405+ * Returns the map with which the cluster is associated.
 406+ *
 407+ * @return {google.maps.Map} The map.
 408+ * @ignore
 409+ */
 410+Cluster.prototype.getMap = function () {
 411+ return this.map_;
 412+};
 413+
 414+
 415+/**
 416+ * Returns the <code>MarkerClusterer</code> object with which the cluster is associated.
 417+ *
 418+ * @return {MarkerClusterer} The associated marker clusterer.
 419+ * @ignore
 420+ */
 421+Cluster.prototype.getMarkerClusterer = function () {
 422+ return this.markerClusterer_;
 423+};
 424+
 425+
 426+/**
 427+ * Returns the bounds of the cluster.
 428+ *
 429+ * @return {google.maps.LatLngBounds} the cluster bounds.
 430+ * @ignore
 431+ */
 432+Cluster.prototype.getBounds = function () {
 433+ var i;
 434+ var bounds = new google.maps.LatLngBounds(this.center_, this.center_);
 435+ var markers = this.getMarkers();
 436+ for (i = 0; i < markers.length; i++) {
 437+ bounds.extend(markers[i].getPosition());
 438+ }
 439+ return bounds;
 440+};
 441+
 442+
 443+/**
 444+ * Removes the cluster from the map.
 445+ *
 446+ * @ignore
 447+ */
 448+Cluster.prototype.remove = function () {
 449+ this.clusterIcon_.setMap(null);
 450+ this.markers_ = [];
 451+ delete this.markers_;
 452+};
 453+
 454+
 455+/**
 456+ * Adds a marker to the cluster.
 457+ *
 458+ * @param {google.maps.Marker} marker The marker to be added.
 459+ * @return {boolean} True if the marker was added.
 460+ * @ignore
 461+ */
 462+Cluster.prototype.addMarker = function (marker) {
 463+ var i;
 464+ var mCount;
 465+ var mz;
 466+
 467+ if (this.isMarkerAlreadyAdded_(marker)) {
 468+ return false;
 469+ }
 470+
 471+ if (!this.center_) {
 472+ this.center_ = marker.getPosition();
 473+ this.calculateBounds_();
 474+ } else {
 475+ if (this.averageCenter_) {
 476+ var l = this.markers_.length + 1;
 477+ var lat = (this.center_.lat() * (l - 1) + marker.getPosition().lat()) / l;
 478+ var lng = (this.center_.lng() * (l - 1) + marker.getPosition().lng()) / l;
 479+ this.center_ = new google.maps.LatLng(lat, lng);
 480+ this.calculateBounds_();
 481+ }
 482+ }
 483+
 484+ marker.isAdded = true;
 485+ this.markers_.push(marker);
 486+
 487+ mCount = this.markers_.length;
 488+ mz = this.markerClusterer_.getMaxZoom();
 489+ if (mz !== null && this.map_.getZoom() > mz) {
 490+ // Zoomed in past max zoom, so show the marker.
 491+ if (marker.getMap() !== this.map_) {
 492+ marker.setMap(this.map_);
 493+ }
 494+ } else if (mCount < this.minClusterSize_) {
 495+ // Min cluster size not reached so show the marker.
 496+ if (marker.getMap() !== this.map_) {
 497+ marker.setMap(this.map_);
 498+ }
 499+ } else if (mCount === this.minClusterSize_) {
 500+ // Hide the markers that were showing.
 501+ for (i = 0; i < mCount; i++) {
 502+ this.markers_[i].setMap(null);
 503+ }
 504+ } else {
 505+ marker.setMap(null);
 506+ }
 507+
 508+ this.updateIcon_();
 509+ return true;
 510+};
 511+
 512+
 513+/**
 514+ * Determines if a marker lies within the cluster's bounds.
 515+ *
 516+ * @param {google.maps.Marker} marker The marker to check.
 517+ * @return {boolean} True if the marker lies in the bounds.
 518+ * @ignore
 519+ */
 520+Cluster.prototype.isMarkerInClusterBounds = function (marker) {
 521+ return this.bounds_.contains(marker.getPosition());
 522+};
 523+
 524+
 525+/**
 526+ * Calculates the extended bounds of the cluster with the grid.
 527+ */
 528+Cluster.prototype.calculateBounds_ = function () {
 529+ var bounds = new google.maps.LatLngBounds(this.center_, this.center_);
 530+ this.bounds_ = this.markerClusterer_.getExtendedBounds(bounds);
 531+};
 532+
 533+
 534+/**
 535+ * Updates the cluster icon.
 536+ */
 537+Cluster.prototype.updateIcon_ = function () {
 538+ var mCount = this.markers_.length;
 539+ var mz = this.markerClusterer_.getMaxZoom();
 540+
 541+ if (mz !== null && this.map_.getZoom() > mz) {
 542+ this.clusterIcon_.hide();
 543+ return;
 544+ }
 545+
 546+ if (mCount < this.minClusterSize_) {
 547+ // Min cluster size not yet reached.
 548+ this.clusterIcon_.hide();
 549+ return;
 550+ }
 551+
 552+ var numStyles = this.markerClusterer_.getStyles().length;
 553+ var sums = this.markerClusterer_.getCalculator()(this.markers_, numStyles);
 554+ this.clusterIcon_.setCenter(this.center_);
 555+ this.clusterIcon_.useStyle(sums);
 556+ this.clusterIcon_.show();
 557+};
 558+
 559+
 560+/**
 561+ * Determines if a marker has already been added to the cluster.
 562+ *
 563+ * @param {google.maps.Marker} marker The marker to check.
 564+ * @return {boolean} True if the marker has already been added.
 565+ */
 566+Cluster.prototype.isMarkerAlreadyAdded_ = function (marker) {
 567+ var i;
 568+ if (this.markers_.indexOf) {
 569+ return this.markers_.indexOf(marker) !== -1;
 570+ } else {
 571+ for (i = 0; i < this.markers_.length; i++) {
 572+ if (marker === this.markers_[i]) {
 573+ return true;
 574+ }
 575+ }
 576+ }
 577+ return false;
 578+};
 579+
 580+
 581+/**
 582+ * @name MarkerClustererOptions
 583+ * @class This class represents the optional parameter passed to
 584+ * the {@link MarkerClusterer} constructor.
 585+ * @property {number} [gridSize=60] The grid size of a cluster in pixels. The grid is a square.
 586+ * @property {number} [maxZoom=null] The maximum zoom level at which clustering is enabled or
 587+ * <code>null</code> if clustering is to be enabled at all zoom levels.
 588+ * @property {boolean} [zoomOnClick=true] Whether to zoom the map when a cluster marker is
 589+ * clicked. You may want to set this to <code>false</code> if you have installed a handler
 590+ * for the <code>click</code> event and it deals with zooming on its own.
 591+ * @property {boolean} [averageCenter=false] Whether the position of a cluster marker should be
 592+ * the average position of all markers in the cluster. If set to <code>false</code>, the
 593+ * cluster marker is positioned at the location of the first marker added to the cluster.
 594+ * @property {number} [minimumClusterSize=2] The minimum number of markers needed in a cluster
 595+ * before the markers are hidden and a cluster marker appears.
 596+ * @property {boolean} [ignoreHidden=false] Whether to ignore hidden markers in clusters. You
 597+ * may want to set this to <code>true</code> to ensure that hidden markers are not included
 598+ * in the marker count that appears on a cluster marker (this count is the value of the
 599+ * <code>text</code> property of the result returned by the default <code>calculator</code>).
 600+ * If set to <code>true</code> and you change the visibility of a marker being clustered, be
 601+ * sure to also call <code>MarkerClusterer.repaint()</code>.
 602+ * @property {boolean} [printable=false] Whether to make the cluster icons printable. Do not
 603+ * set to <code>true</code> if the <code>url</code> fields in the <code>styles</code> array
 604+ * refer to image sprite files.
 605+ * @property {string} [title=""] The tooltip to display when the mouse moves over a cluster
 606+ * marker.
 607+ * @property {function} [calculator=MarkerClusterer.CALCULATOR] The function used to determine
 608+ * the text to be displayed on a cluster marker and the index indicating which style to use
 609+ * for the cluster marker. The input parameters for the function are (1) the array of markers
 610+ * represented by a cluster marker and (2) the number of cluster icon styles. It returns a
 611+ * {@link ClusterIconInfo} object. The default <code>calculator</code> returns a
 612+ * <code>text</code> property which is the number of markers in the cluster and an
 613+ * <code>index</code> property which is one higher than the lowest integer such that
 614+ * <code>10^i</code> exceeds the number of markers in the cluster, or the size of the styles
 615+ * array, whichever is less. The <code>styles</code> array element used has an index of
 616+ * <code>index</code> minus 1. For example, the default <code>calculator</code> returns a
 617+ * <code>text</code> value of <code>"125"</code> and an <code>index</code> of <code>3</code>
 618+ * for a cluster icon representing 125 markers so the element used in the <code>styles</code>
 619+ * array is <code>2</code>.
 620+ * @property {Array} [styles] An array of {@link ClusterIconStyle} elements defining the styles
 621+ * of the cluster markers to be used. The element to be used to style a given cluster marker
 622+ * is determined by the function defined by the <code>calculator</code> property.
 623+ * The default is an array of {@link ClusterIconStyle} elements whose properties are derived
 624+ * from the values for <code>imagePath</code>, <code>imageExtension</code>, and
 625+ * <code>imageSizes</code>.
 626+ * @property {number} [batchSize=MarkerClusterer.BATCH_SIZE] Set this property to the
 627+ * number of markers to be processed in a single batch when using a browser other than
 628+ * Internet Explorer (for Internet Explorer, use the batchSizeIE property instead).
 629+ * @property {number} [batchSizeIE=MarkerClusterer.BATCH_SIZE_IE] When Internet Explorer is
 630+ * being used, markers are processed in several batches with a small delay inserted between
 631+ * each batch in an attempt to avoid Javascript timeout errors. Set this property to the
 632+ * number of markers to be processed in a single batch; select as high a number as you can
 633+ * without causing a timeout error in the browser. This number might need to be as low as 100
 634+ * if 15,000 markers are being managed, for example.
 635+ * @property {string} [imagePath=MarkerClusterer.IMAGE_PATH]
 636+ * The full URL of the root name of the group of image files to use for cluster icons.
 637+ * The complete file name is of the form <code>imagePath</code>n.<code>imageExtension</code>
 638+ * where n is the image file number (1, 2, etc.).
 639+ * @property {string} [imageExtension=MarkerClusterer.IMAGE_EXTENSION]
 640+ * The extension name for the cluster icon image files (e.g., <code>"png"</code> or
 641+ * <code>"jpg"</code>).
 642+ * @property {Array} [imageSizes=MarkerClusterer.IMAGE_SIZES]
 643+ * An array of numbers containing the widths of the group of
 644+ * <code>imagePath</code>n.<code>imageExtension</code> image files.
 645+ * (The images are assumed to be square.)
 646+ */
 647+/**
 648+ * Creates a MarkerClusterer object with the options specified in {@link MarkerClustererOptions}.
 649+ * @constructor
 650+ * @extends google.maps.OverlayView
 651+ * @param {google.maps.Map} map The Google map to attach to.
 652+ * @param {Array.<google.maps.Marker>} [opt_markers] The markers to be added to the cluster.
 653+ * @param {MarkerClustererOptions} [opt_options] The optional parameters.
 654+ */
 655+function MarkerClusterer(map, opt_markers, opt_options) {
 656+ // MarkerClusterer implements google.maps.OverlayView interface. We use the
 657+ // extend function to extend MarkerClusterer with google.maps.OverlayView
 658+ // because it might not always be available when the code is defined so we
 659+ // look for it at the last possible moment. If it doesn't exist now then
 660+ // there is no point going ahead :)
 661+ this.extend(MarkerClusterer, google.maps.OverlayView);
 662+
 663+ opt_markers = opt_markers || [];
 664+ opt_options = opt_options || {};
 665+
 666+ this.markers_ = [];
 667+ this.clusters_ = [];
 668+ this.listeners_ = [];
 669+ this.activeMap_ = null;
 670+ this.ready_ = false;
 671+
 672+ this.gridSize_ = opt_options.gridSize || 60;
 673+ this.minClusterSize_ = opt_options.minimumClusterSize || 2;
 674+ this.maxZoom_ = opt_options.maxZoom || null;
 675+ this.styles_ = opt_options.styles || [];
 676+ this.title_ = opt_options.title || "";
 677+ this.zoomOnClick_ = true;
 678+ if (opt_options.zoomOnClick !== undefined) {
 679+ this.zoomOnClick_ = opt_options.zoomOnClick;
 680+ }
 681+ this.averageCenter_ = false;
 682+ if (opt_options.averageCenter !== undefined) {
 683+ this.averageCenter_ = opt_options.averageCenter;
 684+ }
 685+ this.ignoreHidden_ = false;
 686+ if (opt_options.ignoreHidden !== undefined) {
 687+ this.ignoreHidden_ = opt_options.ignoreHidden;
 688+ }
 689+ this.printable_ = false;
 690+ if (opt_options.printable !== undefined) {
 691+ this.printable_ = opt_options.printable;
 692+ }
 693+ this.imagePath_ = opt_options.imagePath || MarkerClusterer.IMAGE_PATH;
 694+ this.imageExtension_ = opt_options.imageExtension || MarkerClusterer.IMAGE_EXTENSION;
 695+ this.imageSizes_ = opt_options.imageSizes || MarkerClusterer.IMAGE_SIZES;
 696+ this.calculator_ = opt_options.calculator || MarkerClusterer.CALCULATOR;
 697+ this.batchSize_ = opt_options.batchSize || MarkerClusterer.BATCH_SIZE;
 698+ this.batchSizeIE_ = opt_options.batchSizeIE || MarkerClusterer.BATCH_SIZE_IE;
 699+
 700+ if (navigator.userAgent.toLowerCase().indexOf("msie") !== -1) {
 701+ // Try to avoid IE timeout when processing a huge number of markers:
 702+ this.batchSize_ = this.batchSizeIE_;
 703+ }
 704+
 705+ this.setupStyles_();
 706+
 707+ this.addMarkers(opt_markers, true);
 708+ this.setMap(map); // Note: this causes onAdd to be called
 709+}
 710+
 711+
 712+/**
 713+ * Implementation of the onAdd interface method.
 714+ * @ignore
 715+ */
 716+MarkerClusterer.prototype.onAdd = function () {
 717+ var cMarkerClusterer = this;
 718+
 719+ this.activeMap_ = this.getMap();
 720+ this.ready_ = true;
 721+
 722+ this.repaint();
 723+
 724+ // Add the map event listeners
 725+ this.listeners_ = [
 726+ google.maps.event.addListener(this.getMap(), "zoom_changed", function () {
 727+ cMarkerClusterer.resetViewport_(false);
 728+ // Workaround for this Google bug: when map is at level 0 and "-" of
 729+ // zoom slider is clicked, a "zoom_changed" event is fired even though
 730+ // the map doesn't zoom out any further. In this situation, no "idle"
 731+ // event is triggered so the cluster markers that have been removed
 732+ // do not get redrawn.
 733+ if (this.getZoom() === 0) {
 734+ google.maps.event.trigger(this, "idle");
 735+ }
 736+ }),
 737+ google.maps.event.addListener(this.getMap(), "idle", function () {
 738+ cMarkerClusterer.redraw_();
 739+ })
 740+ ];
 741+};
 742+
 743+
 744+/**
 745+ * Implementation of the onRemove interface method.
 746+ * Removes map event listeners and all cluster icons from the DOM.
 747+ * All managed markers are also put back on the map.
 748+ * @ignore
 749+ */
 750+MarkerClusterer.prototype.onRemove = function () {
 751+ var i;
 752+
 753+ // Put all the managed markers back on the map:
 754+ for (i = 0; i < this.markers_.length; i++) {
 755+ this.markers_[i].setMap(this.activeMap_);
 756+ }
 757+
 758+ // Remove all clusters:
 759+ for (i = 0; i < this.clusters_.length; i++) {
 760+ this.clusters_[i].remove();
 761+ }
 762+ this.clusters_ = [];
 763+
 764+ // Remove map event listeners:
 765+ for (i = 0; i < this.listeners_.length; i++) {
 766+ google.maps.event.removeListener(this.listeners_[i]);
 767+ }
 768+ this.listeners_ = [];
 769+
 770+ this.activeMap_ = null;
 771+ this.ready_ = false;
 772+};
 773+
 774+
 775+/**
 776+ * Implementation of the draw interface method.
 777+ * @ignore
 778+ */
 779+MarkerClusterer.prototype.draw = function () {};
 780+
 781+
 782+/**
 783+ * Sets up the styles object.
 784+ */
 785+MarkerClusterer.prototype.setupStyles_ = function () {
 786+ var i, size;
 787+ if (this.styles_.length > 0) {
 788+ return;
 789+ }
 790+
 791+ for (i = 0; i < this.imageSizes_.length; i++) {
 792+ size = this.imageSizes_[i];
 793+ this.styles_.push({
 794+ url: this.imagePath_ + (i + 1) + "." + this.imageExtension_,
 795+ height: size,
 796+ width: size
 797+ });
 798+ }
 799+};
 800+
 801+
 802+/**
 803+ * Fits the map to the bounds of the markers managed by the clusterer.
 804+ */
 805+MarkerClusterer.prototype.fitMapToMarkers = function () {
 806+ var i;
 807+ var markers = this.getMarkers();
 808+ var bounds = new google.maps.LatLngBounds();
 809+ for (i = 0; i < markers.length; i++) {
 810+ bounds.extend(markers[i].getPosition());
 811+ }
 812+
 813+ this.getMap().fitBounds(bounds);
 814+};
 815+
 816+
 817+/**
 818+ * Returns the value of the <code>gridSize</code> property.
 819+ *
 820+ * @return {number} The grid size.
 821+ */
 822+MarkerClusterer.prototype.getGridSize = function () {
 823+ return this.gridSize_;
 824+};
 825+
 826+
 827+/**
 828+ * Sets the value of the <code>gridSize</code> property.
 829+ *
 830+ * @param {number} gridSize The grid size.
 831+ */
 832+MarkerClusterer.prototype.setGridSize = function (gridSize) {
 833+ this.gridSize_ = gridSize;
 834+};
 835+
 836+
 837+/**
 838+ * Returns the value of the <code>minimumClusterSize</code> property.
 839+ *
 840+ * @return {number} The minimum cluster size.
 841+ */
 842+MarkerClusterer.prototype.getMinimumClusterSize = function () {
 843+ return this.minClusterSize_;
 844+};
 845+
 846+/**
 847+ * Sets the value of the <code>minimumClusterSize</code> property.
 848+ *
 849+ * @param {number} minimumClusterSize The minimum cluster size.
 850+ */
 851+MarkerClusterer.prototype.setMinimumClusterSize = function (minimumClusterSize) {
 852+ this.minClusterSize_ = minimumClusterSize;
 853+};
 854+
 855+
 856+/**
 857+ * Returns the value of the <code>maxZoom</code> property.
 858+ *
 859+ * @return {number} The maximum zoom level.
 860+ */
 861+MarkerClusterer.prototype.getMaxZoom = function () {
 862+ return this.maxZoom_;
 863+};
 864+
 865+
 866+/**
 867+ * Sets the value of the <code>maxZoom</code> property.
 868+ *
 869+ * @param {number} maxZoom The maximum zoom level.
 870+ */
 871+MarkerClusterer.prototype.setMaxZoom = function (maxZoom) {
 872+ this.maxZoom_ = maxZoom;
 873+};
 874+
 875+
 876+/**
 877+ * Returns the value of the <code>styles</code> property.
 878+ *
 879+ * @return {Array} The array of styles defining the cluster markers to be used.
 880+ */
 881+MarkerClusterer.prototype.getStyles = function () {
 882+ return this.styles_;
 883+};
 884+
 885+
 886+/**
 887+ * Sets the value of the <code>styles</code> property.
 888+ *
 889+ * @param {Array.<ClusterIconStyle>} styles The array of styles to use.
 890+ */
 891+MarkerClusterer.prototype.setStyles = function (styles) {
 892+ this.styles_ = styles;
 893+};
 894+
 895+
 896+/**
 897+ * Returns the value of the <code>title</code> property.
 898+ *
 899+ * @return {string} The content of the title text.
 900+ */
 901+MarkerClusterer.prototype.getTitle = function () {
 902+ return this.title_;
 903+};
 904+
 905+
 906+/**
 907+ * Sets the value of the <code>title</code> property.
 908+ *
 909+ * @param {string} title The value of the title property.
 910+ */
 911+MarkerClusterer.prototype.setTitle = function (title) {
 912+ this.title_ = title;
 913+};
 914+
 915+
 916+/**
 917+ * Returns the value of the <code>zoomOnClick</code> property.
 918+ *
 919+ * @return {boolean} True if zoomOnClick property is set.
 920+ */
 921+MarkerClusterer.prototype.getZoomOnClick = function () {
 922+ return this.zoomOnClick_;
 923+};
 924+
 925+
 926+/**
 927+ * Sets the value of the <code>zoomOnClick</code> property.
 928+ *
 929+ * @param {boolean} zoomOnClick The value of the zoomOnClick property.
 930+ */
 931+MarkerClusterer.prototype.setZoomOnClick = function (zoomOnClick) {
 932+ this.zoomOnClick_ = zoomOnClick;
 933+};
 934+
 935+
 936+/**
 937+ * Returns the value of the <code>averageCenter</code> property.
 938+ *
 939+ * @return {boolean} True if averageCenter property is set.
 940+ */
 941+MarkerClusterer.prototype.getAverageCenter = function () {
 942+ return this.averageCenter_;
 943+};
 944+
 945+
 946+/**
 947+ * Sets the value of the <code>averageCenter</code> property.
 948+ *
 949+ * @param {boolean} averageCenter The value of the averageCenter property.
 950+ */
 951+MarkerClusterer.prototype.setAverageCenter = function (averageCenter) {
 952+ this.averageCenter_ = averageCenter;
 953+};
 954+
 955+
 956+/**
 957+ * Returns the value of the <code>ignoreHidden</code> property.
 958+ *
 959+ * @return {boolean} True if ignoreHidden property is set.
 960+ */
 961+MarkerClusterer.prototype.getIgnoreHidden = function () {
 962+ return this.ignoreHidden_;
 963+};
 964+
 965+
 966+/**
 967+ * Sets the value of the <code>ignoreHidden</code> property.
 968+ *
 969+ * @param {boolean} ignoreHidden The value of the ignoreHidden property.
 970+ */
 971+MarkerClusterer.prototype.setIgnoreHidden = function (ignoreHidden) {
 972+ this.ignoreHidden_ = ignoreHidden;
 973+};
 974+
 975+
 976+/**
 977+ * Returns the value of the <code>imageExtension</code> property.
 978+ *
 979+ * @return {string} The value of the imageExtension property.
 980+ */
 981+MarkerClusterer.prototype.getImageExtension = function () {
 982+ return this.imageExtension_;
 983+};
 984+
 985+
 986+/**
 987+ * Sets the value of the <code>imageExtension</code> property.
 988+ *
 989+ * @param {string} imageExtension The value of the imageExtension property.
 990+ */
 991+MarkerClusterer.prototype.setImageExtension = function (imageExtension) {
 992+ this.imageExtension_ = imageExtension;
 993+};
 994+
 995+
 996+/**
 997+ * Returns the value of the <code>imagePath</code> property.
 998+ *
 999+ * @return {string} The value of the imagePath property.
 1000+ */
 1001+MarkerClusterer.prototype.getImagePath = function () {
 1002+ return this.imagePath_;
 1003+};
 1004+
 1005+
 1006+/**
 1007+ * Sets the value of the <code>imagePath</code> property.
 1008+ *
 1009+ * @param {string} imagePath The value of the imagePath property.
 1010+ */
 1011+MarkerClusterer.prototype.setImagePath = function (imagePath) {
 1012+ this.imagePath_ = imagePath;
 1013+};
 1014+
 1015+
 1016+/**
 1017+ * Returns the value of the <code>imageSizes</code> property.
 1018+ *
 1019+ * @return {Array} The value of the imageSizes property.
 1020+ */
 1021+MarkerClusterer.prototype.getImageSizes = function () {
 1022+ return this.imageSizes_;
 1023+};
 1024+
 1025+
 1026+/**
 1027+ * Sets the value of the <code>imageSizes</code> property.
 1028+ *
 1029+ * @param {Array} imageSizes The value of the imageSizes property.
 1030+ */
 1031+MarkerClusterer.prototype.setImageSizes = function (imageSizes) {
 1032+ this.imageSizes_ = imageSizes;
 1033+};
 1034+
 1035+
 1036+/**
 1037+ * Returns the value of the <code>calculator</code> property.
 1038+ *
 1039+ * @return {function} the value of the calculator property.
 1040+ */
 1041+MarkerClusterer.prototype.getCalculator = function () {
 1042+ return this.calculator_;
 1043+};
 1044+
 1045+
 1046+/**
 1047+ * Sets the value of the <code>calculator</code> property.
 1048+ *
 1049+ * @param {function(Array.<google.maps.Marker>, number)} calculator The value
 1050+ * of the calculator property.
 1051+ */
 1052+MarkerClusterer.prototype.setCalculator = function (calculator) {
 1053+ this.calculator_ = calculator;
 1054+};
 1055+
 1056+
 1057+/**
 1058+ * Returns the value of the <code>printable</code> property.
 1059+ *
 1060+ * @return {boolean} the value of the printable property.
 1061+ */
 1062+MarkerClusterer.prototype.getPrintable = function () {
 1063+ return this.printable_;
 1064+};
 1065+
 1066+
 1067+/**
 1068+ * Sets the value of the <code>printable</code> property.
 1069+ *
 1070+ * @param {boolean} printable The value of the printable property.
 1071+ */
 1072+MarkerClusterer.prototype.setPrintable = function (printable) {
 1073+ this.printable_ = printable;
 1074+};
 1075+
 1076+
 1077+/**
 1078+ * Returns the value of the <code>batchSizeIE</code> property.
 1079+ *
 1080+ * @return {number} the value of the batchSizeIE property.
 1081+ */
 1082+MarkerClusterer.prototype.getBatchSizeIE = function () {
 1083+ return this.batchSizeIE_;
 1084+};
 1085+
 1086+
 1087+/**
 1088+ * Sets the value of the <code>batchSizeIE</code> property.
 1089+ *
 1090+ * @param {number} batchSizeIE The value of the batchSizeIE property.
 1091+ */
 1092+MarkerClusterer.prototype.setBatchSizeIE = function (batchSizeIE) {
 1093+ this.batchSizeIE_ = batchSizeIE;
 1094+};
 1095+
 1096+
 1097+/**
 1098+ * Returns the array of markers managed by the clusterer.
 1099+ *
 1100+ * @return {Array} The array of markers managed by the clusterer.
 1101+ */
 1102+MarkerClusterer.prototype.getMarkers = function () {
 1103+ return this.markers_;
 1104+};
 1105+
 1106+
 1107+/**
 1108+ * Returns the number of markers managed by the clusterer.
 1109+ *
 1110+ * @return {number} The number of markers.
 1111+ */
 1112+MarkerClusterer.prototype.getTotalMarkers = function () {
 1113+ return this.markers_.length;
 1114+};
 1115+
 1116+
 1117+/**
 1118+ * Returns the current array of clusters formed by the clusterer.
 1119+ *
 1120+ * @return {Array} The array of clusters formed by the clusterer.
 1121+ */
 1122+MarkerClusterer.prototype.getClusters = function () {
 1123+ return this.clusters_;
 1124+};
 1125+
 1126+
 1127+/**
 1128+ * Returns the number of clusters formed by the clusterer.
 1129+ *
 1130+ * @return {number} The number of clusters formed by the clusterer.
 1131+ */
 1132+MarkerClusterer.prototype.getTotalClusters = function () {
 1133+ return this.clusters_.length;
 1134+};
 1135+
 1136+
 1137+/**
 1138+ * Adds a marker to the clusterer. The clusters are redrawn unless
 1139+ * <code>opt_nodraw</code> is set to <code>true</code>.
 1140+ *
 1141+ * @param {google.maps.Marker} marker The marker to add.
 1142+ * @param {boolean} [opt_nodraw] Set to <code>true</code> to prevent redrawing.
 1143+ */
 1144+MarkerClusterer.prototype.addMarker = function (marker, opt_nodraw) {
 1145+ this.pushMarkerTo_(marker);
 1146+ if (!opt_nodraw) {
 1147+ this.redraw_();
 1148+ }
 1149+};
 1150+
 1151+
 1152+/**
 1153+ * Adds an array of markers to the clusterer. The clusters are redrawn unless
 1154+ * <code>opt_nodraw</code> is set to <code>true</code>.
 1155+ *
 1156+ * @param {Array.<google.maps.Marker>} markers The markers to add.
 1157+ * @param {boolean} [opt_nodraw] Set to <code>true</code> to prevent redrawing.
 1158+ */
 1159+MarkerClusterer.prototype.addMarkers = function (markers, opt_nodraw) {
 1160+ var i;
 1161+ for (i = 0; i < markers.length; i++) {
 1162+ this.pushMarkerTo_(markers[i]);
 1163+ }
 1164+ if (!opt_nodraw) {
 1165+ this.redraw_();
 1166+ }
 1167+};
 1168+
 1169+
 1170+/**
 1171+ * Pushes a marker to the clusterer.
 1172+ *
 1173+ * @param {google.maps.Marker} marker The marker to add.
 1174+ */
 1175+MarkerClusterer.prototype.pushMarkerTo_ = function (marker) {
 1176+ // If the marker is draggable add a listener so we can update the clusters on the dragend:
 1177+ if (marker.getDraggable()) {
 1178+ var cMarkerClusterer = this;
 1179+ google.maps.event.addListener(marker, "dragend", function () {
 1180+ if (cMarkerClusterer.ready_) {
 1181+ this.isAdded = false;
 1182+ cMarkerClusterer.repaint();
 1183+ }
 1184+ });
 1185+ }
 1186+ marker.isAdded = false;
 1187+ this.markers_.push(marker);
 1188+};
 1189+
 1190+
 1191+/**
 1192+ * Removes a marker from the cluster. The clusters are redrawn unless
 1193+ * <code>opt_nodraw</code> is set to <code>true</code>. Returns <code>true</code> if the
 1194+ * marker was removed from the clusterer.
 1195+ *
 1196+ * @param {google.maps.Marker} marker The marker to remove.
 1197+ * @param {boolean} [opt_nodraw] Set to <code>true</code> to prevent redrawing.
 1198+ * @return {boolean} True if the marker was removed from the clusterer.
 1199+ */
 1200+MarkerClusterer.prototype.removeMarker = function (marker, opt_nodraw) {
 1201+ var removed = this.removeMarker_(marker);
 1202+
 1203+ if (!opt_nodraw && removed) {
 1204+ this.repaint();
 1205+ }
 1206+
 1207+ return removed;
 1208+};
 1209+
 1210+
 1211+/**
 1212+ * Removes an array of markers from the cluster. The clusters are redrawn unless
 1213+ * <code>opt_nodraw</code> is set to <code>true</code>. Returns <code>true</code> if markers
 1214+ * were removed from the clusterer.
 1215+ *
 1216+ * @param {Array.<google.maps.Marker>} markers The markers to remove.
 1217+ * @param {boolean} [opt_nodraw] Set to <code>true</code> to prevent redrawing.
 1218+ * @return {boolean} True if markers were removed from the clusterer.
 1219+ */
 1220+MarkerClusterer.prototype.removeMarkers = function (markers, opt_nodraw) {
 1221+ var i, r;
 1222+ var removed = false;
 1223+
 1224+ for (i = 0; i < markers.length; i++) {
 1225+ r = this.removeMarker_(markers[i]);
 1226+ removed = removed || r;
 1227+ }
 1228+
 1229+ if (!opt_nodraw && removed) {
 1230+ this.repaint();
 1231+ }
 1232+
 1233+ return removed;
 1234+};
 1235+
 1236+
 1237+/**
 1238+ * Removes a marker and returns true if removed, false if not.
 1239+ *
 1240+ * @param {google.maps.Marker} marker The marker to remove
 1241+ * @return {boolean} Whether the marker was removed or not
 1242+ */
 1243+MarkerClusterer.prototype.removeMarker_ = function (marker) {
 1244+ var i;
 1245+ var index = -1;
 1246+ if (this.markers_.indexOf) {
 1247+ index = this.markers_.indexOf(marker);
 1248+ } else {
 1249+ for (i = 0; i < this.markers_.length; i++) {
 1250+ if (marker === this.markers_[i]) {
 1251+ index = i;
 1252+ break;
 1253+ }
 1254+ }
 1255+ }
 1256+
 1257+ if (index === -1) {
 1258+ // Marker is not in our list of markers, so do nothing:
 1259+ return false;
 1260+ }
 1261+
 1262+ marker.setMap(null);
 1263+ this.markers_.splice(index, 1); // Remove the marker from the list of managed markers
 1264+ return true;
 1265+};
 1266+
 1267+
 1268+/**
 1269+ * Removes all clusters and markers from the map and also removes all markers
 1270+ * managed by the clusterer.
 1271+ */
 1272+MarkerClusterer.prototype.clearMarkers = function () {
 1273+ this.resetViewport_(true);
 1274+ this.markers_ = [];
 1275+};
 1276+
 1277+
 1278+/**
 1279+ * Recalculates and redraws all the marker clusters from scratch.
 1280+ * Call this after changing any properties.
 1281+ */
 1282+MarkerClusterer.prototype.repaint = function () {
 1283+ var oldClusters = this.clusters_.slice();
 1284+ this.clusters_ = [];
 1285+ this.resetViewport_(false);
 1286+ this.redraw_();
 1287+
 1288+ // Remove the old clusters.
 1289+ // Do it in a timeout to prevent blinking effect.
 1290+ setTimeout(function () {
 1291+ var i;
 1292+ for (i = 0; i < oldClusters.length; i++) {
 1293+ oldClusters[i].remove();
 1294+ }
 1295+ }, 0);
 1296+};
 1297+
 1298+
 1299+/**
 1300+ * Returns the current bounds extended by the grid size.
 1301+ *
 1302+ * @param {google.maps.LatLngBounds} bounds The bounds to extend.
 1303+ * @return {google.maps.LatLngBounds} The extended bounds.
 1304+ * @ignore
 1305+ */
 1306+MarkerClusterer.prototype.getExtendedBounds = function (bounds) {
 1307+ var projection = this.getProjection();
 1308+
 1309+ // Turn the bounds into latlng.
 1310+ var tr = new google.maps.LatLng(bounds.getNorthEast().lat(),
 1311+ bounds.getNorthEast().lng());
 1312+ var bl = new google.maps.LatLng(bounds.getSouthWest().lat(),
 1313+ bounds.getSouthWest().lng());
 1314+
 1315+ // Convert the points to pixels and the extend out by the grid size.
 1316+ var trPix = projection.fromLatLngToDivPixel(tr);
 1317+ trPix.x += this.gridSize_;
 1318+ trPix.y -= this.gridSize_;
 1319+
 1320+ var blPix = projection.fromLatLngToDivPixel(bl);
 1321+ blPix.x -= this.gridSize_;
 1322+ blPix.y += this.gridSize_;
 1323+
 1324+ // Convert the pixel points back to LatLng
 1325+ var ne = projection.fromDivPixelToLatLng(trPix);
 1326+ var sw = projection.fromDivPixelToLatLng(blPix);
 1327+
 1328+ // Extend the bounds to contain the new bounds.
 1329+ bounds.extend(ne);
 1330+ bounds.extend(sw);
 1331+
 1332+ return bounds;
 1333+};
 1334+
 1335+
 1336+/**
 1337+ * Redraws all the clusters.
 1338+ */
 1339+MarkerClusterer.prototype.redraw_ = function () {
 1340+ this.createClusters_(0);
 1341+};
 1342+
 1343+
 1344+/**
 1345+ * Removes all clusters from the map. The markers are also removed from the map
 1346+ * if <code>opt_hide</code> is set to <code>true</code>.
 1347+ *
 1348+ * @param {boolean} [opt_hide] Set to <code>true</code> to also remove the markers
 1349+ * from the map.
 1350+ */
 1351+MarkerClusterer.prototype.resetViewport_ = function (opt_hide) {
 1352+ var i, marker;
 1353+ // Remove all the clusters
 1354+ for (i = 0; i < this.clusters_.length; i++) {
 1355+ this.clusters_[i].remove();
 1356+ }
 1357+ this.clusters_ = [];
 1358+
 1359+ // Reset the markers to not be added and to be removed from the map.
 1360+ for (i = 0; i < this.markers_.length; i++) {
 1361+ marker = this.markers_[i];
 1362+ marker.isAdded = false;
 1363+ if (opt_hide) {
 1364+ marker.setMap(null);
 1365+ }
 1366+ }
 1367+};
 1368+
 1369+
 1370+/**
 1371+ * Calculates the distance between two latlng locations in km.
 1372+ *
 1373+ * @param {google.maps.LatLng} p1 The first lat lng point.
 1374+ * @param {google.maps.LatLng} p2 The second lat lng point.
 1375+ * @return {number} The distance between the two points in km.
 1376+ * @see http://www.movable-type.co.uk/scripts/latlong.html
 1377+ */
 1378+MarkerClusterer.prototype.distanceBetweenPoints_ = function (p1, p2) {
 1379+ var R = 6371; // Radius of the Earth in km
 1380+ var dLat = (p2.lat() - p1.lat()) * Math.PI / 180;
 1381+ var dLon = (p2.lng() - p1.lng()) * Math.PI / 180;
 1382+ var a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
 1383+ Math.cos(p1.lat() * Math.PI / 180) * Math.cos(p2.lat() * Math.PI / 180) *
 1384+ Math.sin(dLon / 2) * Math.sin(dLon / 2);
 1385+ var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
 1386+ var d = R * c;
 1387+ return d;
 1388+};
 1389+
 1390+
 1391+/**
 1392+ * Determines if a marker is contained in a bounds.
 1393+ *
 1394+ * @param {google.maps.Marker} marker The marker to check.
 1395+ * @param {google.maps.LatLngBounds} bounds The bounds to check against.
 1396+ * @return {boolean} True if the marker is in the bounds.
 1397+ */
 1398+MarkerClusterer.prototype.isMarkerInBounds_ = function (marker, bounds) {
 1399+ return bounds.contains(marker.getPosition());
 1400+};
 1401+
 1402+
 1403+/**
 1404+ * Adds a marker to a cluster, or creates a new cluster.
 1405+ *
 1406+ * @param {google.maps.Marker} marker The marker to add.
 1407+ */
 1408+MarkerClusterer.prototype.addToClosestCluster_ = function (marker) {
 1409+ var i, d, cluster, center;
 1410+ var distance = 40000; // Some large number
 1411+ var clusterToAddTo = null;
 1412+ for (i = 0; i < this.clusters_.length; i++) {
 1413+ cluster = this.clusters_[i];
 1414+ center = cluster.getCenter();
 1415+ if (center) {
 1416+ d = this.distanceBetweenPoints_(center, marker.getPosition());
 1417+ if (d < distance) {
 1418+ distance = d;
 1419+ clusterToAddTo = cluster;
 1420+ }
 1421+ }
 1422+ }
 1423+
 1424+ if (clusterToAddTo && clusterToAddTo.isMarkerInClusterBounds(marker)) {
 1425+ clusterToAddTo.addMarker(marker);
 1426+ } else {
 1427+ cluster = new Cluster(this);
 1428+ cluster.addMarker(marker);
 1429+ this.clusters_.push(cluster);
 1430+ }
 1431+};
 1432+
 1433+
 1434+/**
 1435+ * Creates the clusters. This is done in batches to avoid timeout errors
 1436+ * in some browsers when there is a huge number of markers.
 1437+ *
 1438+ * @param {number} iFirst The index of the first marker in the batch of
 1439+ * markers to be added to clusters.
 1440+ */
 1441+MarkerClusterer.prototype.createClusters_ = function (iFirst) {
 1442+ var i, marker;
 1443+ var mapBounds;
 1444+ var cMarkerClusterer = this;
 1445+ if (!this.ready_) {
 1446+ return;
 1447+ }
 1448+
 1449+ // Cancel previous batch processing if we're working on the first batch:
 1450+ if (iFirst === 0) {
 1451+ /**
 1452+ * This event is fired when the <code>MarkerClusterer</code> begins
 1453+ * clustering markers.
 1454+ * @name MarkerClusterer#clusteringbegin
 1455+ * @param {MarkerClusterer} mc The MarkerClusterer whose markers are being clustered.
 1456+ * @event
 1457+ */
 1458+ google.maps.event.trigger(this, "clusteringbegin", this);
 1459+
 1460+ if (typeof this.timerRefStatic !== "undefined") {
 1461+ clearTimeout(this.timerRefStatic);
 1462+ delete this.timerRefStatic;
 1463+ }
 1464+ }
 1465+
 1466+ // Get our current map view bounds.
 1467+ // Create a new bounds object so we don't affect the map.
 1468+ //
 1469+ // See Comments 9 & 11 on Issue 3651 relating to this workaround for a Google Maps bug:
 1470+ if (this.getMap().getZoom() > 3) {
 1471+ mapBounds = new google.maps.LatLngBounds(this.getMap().getBounds().getSouthWest(),
 1472+ this.getMap().getBounds().getNorthEast());
 1473+ } else {
 1474+ mapBounds = new google.maps.LatLngBounds(new google.maps.LatLng(85.02070771743472, -178.48388434375), new google.maps.LatLng(-85.08136444384544, 178.00048865625));
 1475+ }
 1476+ var bounds = this.getExtendedBounds(mapBounds);
 1477+
 1478+ var iLast = Math.min(iFirst + this.batchSize_, this.markers_.length);
 1479+
 1480+ for (i = iFirst; i < iLast; i++) {
 1481+ marker = this.markers_[i];
 1482+ if (!marker.isAdded && this.isMarkerInBounds_(marker, bounds)) {
 1483+ if (!this.ignoreHidden_ || (this.ignoreHidden_ && marker.getVisible())) {
 1484+ this.addToClosestCluster_(marker);
 1485+ }
 1486+ }
 1487+ }
 1488+
 1489+ if (iLast < this.markers_.length) {
 1490+ this.timerRefStatic = setTimeout(function () {
 1491+ cMarkerClusterer.createClusters_(iLast);
 1492+ }, 0);
 1493+ } else {
 1494+ delete this.timerRefStatic;
 1495+
 1496+ /**
 1497+ * This event is fired when the <code>MarkerClusterer</code> stops
 1498+ * clustering markers.
 1499+ * @name MarkerClusterer#clusteringend
 1500+ * @param {MarkerClusterer} mc The MarkerClusterer whose markers are being clustered.
 1501+ * @event
 1502+ */
 1503+ google.maps.event.trigger(this, "clusteringend", this);
 1504+ }
 1505+};
 1506+
 1507+
 1508+/**
 1509+ * Extends an object's prototype by another's.
 1510+ *
 1511+ * @param {Object} obj1 The object to be extended.
 1512+ * @param {Object} obj2 The object to extend with.
 1513+ * @return {Object} The new extended object.
 1514+ * @ignore
 1515+ */
 1516+MarkerClusterer.prototype.extend = function (obj1, obj2) {
 1517+ return (function (object) {
 1518+ var property;
 1519+ for (property in object.prototype) {
 1520+ this.prototype[property] = object.prototype[property];
 1521+ }
 1522+ return this;
 1523+ }).apply(obj1, [obj2]);
 1524+};
 1525+
 1526+
 1527+/**
 1528+ * The default function for determining the label text and style
 1529+ * for a cluster icon.
 1530+ *
 1531+ * @param {Array.<google.maps.Marker>} markers The array of markers represented by the cluster.
 1532+ * @param {number} numStyles The number of marker styles available.
 1533+ * @return {ClusterIconInfo} The information resource for the cluster.
 1534+ * @constant
 1535+ * @ignore
 1536+ */
 1537+MarkerClusterer.CALCULATOR = function (markers, numStyles) {
 1538+ var index = 0;
 1539+ var count = markers.length.toString();
 1540+
 1541+ var dv = count;
 1542+ while (dv !== 0) {
 1543+ dv = parseInt(dv / 10, 10);
 1544+ index++;
 1545+ }
 1546+
 1547+ index = Math.min(index, numStyles);
 1548+ return {
 1549+ text: count,
 1550+ index: index
 1551+ };
 1552+};
 1553+
 1554+
 1555+/**
 1556+ * The number of markers to process in one batch.
 1557+ *
 1558+ * @type {number}
 1559+ * @constant
 1560+ */
 1561+MarkerClusterer.BATCH_SIZE = 2000;
 1562+
 1563+
 1564+/**
 1565+ * The number of markers to process in one batch (IE only).
 1566+ *
 1567+ * @type {number}
 1568+ * @constant
 1569+ */
 1570+MarkerClusterer.BATCH_SIZE_IE = 500;
 1571+
 1572+
 1573+/**
 1574+ * The default root name for the marker cluster images.
 1575+ *
 1576+ * @type {string}
 1577+ * @constant
 1578+ */
 1579+MarkerClusterer.IMAGE_PATH = "http://google-maps-utility-library-v3.googlecode.com/svn/trunk/markerclustererplus/images/m";
 1580+
 1581+
 1582+/**
 1583+ * The default extension name for the marker cluster images.
 1584+ *
 1585+ * @type {string}
 1586+ * @constant
 1587+ */
 1588+MarkerClusterer.IMAGE_EXTENSION = "png";
 1589+
 1590+
 1591+/**
 1592+ * The default array of sizes for the marker cluster images.
 1593+ *
 1594+ * @type {Array.<number>}
 1595+ * @constant
 1596+ */
 1597+MarkerClusterer.IMAGE_SIZES = [53, 56, 66, 78, 90];
\ No newline at end of file
Index: trunk/extensions/Maps/includes/services/GoogleMaps3/GoogleMaps3.php
@@ -27,8 +27,9 @@
2828 'remoteBasePath' => $egMapsScriptPath . '/includes/services/GoogleMaps3',
2929 'group' => 'ext.maps',
3030 'scripts' => array(
 31+ 'markerclusterer.js',
3132 'jquery.googlemap.js',
32 - 'ext.maps.googlemaps3.js',
 33+ 'ext.maps.googlemaps3.js'
3334 ),
3435 'messages' => array(
3536 'maps-googlemaps3-incompatbrowser'
@@ -75,7 +76,8 @@
7677 MapsMappingServices::registerService( 'googlemaps3', 'MapsGoogleMaps3' );
7778 $googleMaps = MapsMappingServices::getServiceInstance( 'googlemaps3' );
7879 $googleMaps->addFeature( 'display_map', 'MapsBaseMap' );
79 - $googleMaps->addFeature( 'display_point', 'MapsBasePointMap' );
 80+ $googleMaps->addFeature( 'display_point', 'MapsBasePointMap' );
 81+ $googleMaps->addFeature( 'display_line', 'MapsBasePointLineMap' );
8082
8183 return true;
8284 }
Index: trunk/extensions/Maps/includes/services/OpenLayers/OpenLayers.php
@@ -51,7 +51,8 @@
5252 'MapsOpenLayers',
5353 array(
5454 'display_point' => 'MapsBasePointMap',
55 - 'display_map' => 'MapsBaseMap'
 55+ 'display_map' => 'MapsBaseMap',
 56+ 'display_line' => 'MapsBasePointLineMap'
5657 )
5758 );
5859
Index: trunk/extensions/Maps/includes/services/OpenLayers/jquery.openlayers.js
@@ -7,7 +7,7 @@
88 */
99
1010 (function( $ ){ $.fn.openlayers = function( mapElementId, options ) {
11 -
 11+
1212 this.getOLMarker = function( markerLayer, markerData ) {
1313 var marker;
1414
@@ -103,6 +103,28 @@
104104
105105 map.addControl( new OpenLayers.Control.Attribution() );
106106 }
 107+
 108+ this.addLine = function(properties){
 109+ var pos = new Array();
 110+ for(var x = 0; x < properties.pos.length; x++){
 111+ var point = new OpenLayers.Geometry.Point(properties.pos[x].lon,properties.pos[x].lat);
 112+ point.transform(
 113+ new OpenLayers.Projection("EPSG:4326"), // transform from WGS 1984
 114+ map.getProjectionObject() // to Spherical Mercator Projection
 115+ );
 116+ pos.push(point);
 117+ }
 118+
 119+ var style = {
 120+ 'strokeColor':properties.strokeColor,
 121+ 'strokeWidth': properties.strokeWeight,
 122+ 'strokeOpacity': properties.strokeOpacity
 123+ }
 124+
 125+ var line = new OpenLayers.Geometry.LineString(pos);
 126+ var lineFeature = new OpenLayers.Feature.Vector(line, null, style);
 127+ this.lineLayer.addFeatures([lineFeature]);
 128+ }
107129
108130 /**
109131 * Gets a valid control name (with excat lower and upper case letters),
@@ -154,7 +176,7 @@
155177 mapOptions.maxResolution = 156543.0339;
156178 mapOptions.maxExtent = new OpenLayers.Bounds(-20037508, -20037508, 20037508, 20037508.34);
157179 }
158 -
 180+
159181 this.map = new OpenLayers.Map( mapElementId, mapOptions );
160182 var map = this.map;
161183 this.addControls( map, options.controls, this.get( 0 ) );
@@ -195,7 +217,59 @@
196218 mw.loader.using( 'ext.maps.resizable', function() {
197219 _this.resizable();
198220 } );
199 - }
 221+ }
 222+
 223+ if(options.lines){
 224+ this.lineLayer = new OpenLayers.Layer.Vector("Line Layer");
 225+ map.addLayer(this.lineLayer);
 226+ map.addControl(new OpenLayers.Control.DrawFeature(this.lineLayer, OpenLayers.Handler.Path));
 227+
 228+ for ( var i = 0; i < options.lines.length; i++ ) {
 229+ this.addLine(options.lines[i]);
 230+ }
 231+ }
 232+
 233+ if(options.copycoords){
 234+ map.div.oncontextmenu = function(){return false;};
 235+ OpenLayers.Control.Click = OpenLayers.Class(OpenLayers.Control, {
 236+ defaultHandlerOptions: {
 237+ 'single': true,
 238+ 'double': false,
 239+ 'pixelTolerance': 0,
 240+ 'stopSingle': false,
 241+ 'stopDouble': false
 242+ },
 243+ handleRightClicks:true,
 244+
 245+ initialize: function(options) {
 246+ this.handlerOptions = OpenLayers.Util.extend(
 247+ {}, this.defaultHandlerOptions
 248+ );
 249+ OpenLayers.Control.prototype.initialize.apply(
 250+ this, arguments
 251+ );
 252+ this.handler = new OpenLayers.Handler.Click(
 253+ this, this.eventMethods, this.handlerOptions
 254+ );
 255+ }
 256+
 257+ })
 258+ var click = new OpenLayers.Control.Click({
 259+ eventMethods:{
 260+ 'rightclick': function(e){
 261+ var lonlat = map.getLonLatFromViewPortPx(e.xy);
 262+ lonlat = lonlat.transform(new OpenLayers.Projection("EPSG:900913"), new OpenLayers.Projection("EPSG:4326"));
 263+ prompt("CTRL+C, ENTER",lonlat.lat+','+lonlat.lon);
 264+ }
 265+ }
 266+ });
 267+ map.addControl(click);
 268+ click.activate();
 269+ }
 270+
 271+ if(options.markercluster){
 272+ alert('no support for clustering in openlayers');
 273+ }
200274
201275 return this;
202276
Index: trunk/extensions/Maps/includes/manipulations/Maps_ParamLine.php
@@ -0,0 +1,38 @@
 2+<?php
 3+class MapsParamLine extends ItemParameterManipulation {
 4+
 5+
 6+
 7+ /**
 8+ * Constructor
 9+ */
 10+ public function __construct() {
 11+ parent::__construct();
 12+ }
 13+
 14+ /**
 15+ * Manipulate an actual value.
 16+ *
 17+ * @param string $value
 18+ * @param Parameter $parameter
 19+ * @param array $parameters
 20+ *
 21+ * @since 0.4
 22+ *
 23+ * @return mixed
 24+ */
 25+ public function doManipulation(&$value, Parameter $parameter, array &$parameters)
 26+ {
 27+ $parts = preg_split('/[\|]+/',$value);
 28+ $lineCoords = preg_split('/[:]/',$parts[0]);
 29+
 30+ $value = new MapsLine($lineCoords);
 31+ $value->setTitle( isset($parts[1]) ? $parts[1] : '' );
 32+ $value->setText( isset($parts[2]) ? $parts[2] : '' );
 33+ $value->setStrokeColor( isset($parts[3]) ? $parts[3] : '' );
 34+ $value->setStrokeOpacity( isset($parts[4]) ? $parts[4] : '' );
 35+ $value->setStrokeWeight( isset($parts[5]) ? $parts[5] : '' );
 36+
 37+ $value = $value->getJSONObject();
 38+ }
 39+}
\ No newline at end of file

Status & tagging log