/**
 * Copyright 2019 Google LLC. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */ name author Gary Little
 *  fileoverview import { ClusterIconInfo , ClusterIconStyle } from "./cluster-icon" ; import { Cluster } from "./cluster" ; import { OverlayViewSafe } from "./overlay-view-safe" ; param text type AriaLabelFn = ( text : string ) => string ; export type Calculator = ( markers : google . maps . Marker [ ] , clusterIconStylesCount : number ) => ClusterIconInfo ; export interface ClusterAugmentedMarker extends google . maps . Marker { isAdded ? : boolean ; } /**
 * This event is fired on the  MarkerClusterer instance when the `MarkerClusterer` stops clustering markers.
 *
 * Example:
 * ```typescript
 *  mc.addListener('clusteringend', (mc: MarkerClusterer) => {})
 * ```
 *
 *  param mc event export declare function clusteringend ( mc ) ; /**
 * This event is fired on the  MarkerClusterer instance when the `MarkerClusterer` begins clustering markers.
 *
 * Example:
 * ```typescript
 *  mc.addListener('clusteringbegin', (mc: MarkerClusterer) => {})
 * ```
 *
 *  param mc event export declare function clusteringbegin ( mc ) ; /**
 * Optional parameter passed to the  MarkerClusterer constructor.
  export interface MarkerClustererOptions { default gridSize ? : number ; default maxZoom ? : number ; default zoomOnClick ? : boolean ; default averageCenter ? : boolean ; default minimumClusterSize ? : number ; default zIndex ? : number ; /**
     * Whether to ignore hidden markers in clusters. You
     * may want to set this to `true` to ensure that hidden markers are not included
     * in the marker count that appears on a cluster marker (this count is the value of the
     * `text` property of the result returned by the default   calculator ).
     * If set to `true` and you change the visibility of a marker being clustered, be
     * sure to also call  MarkerClusterer .
     *
     *  default ignoreHidden ? : boolean ; default title ? : string ; /**
     * The function used to determine
     * the text to be displayed on a cluster marker and the index indicating which style to use
     * for the cluster marker. The input parameters for the function are (1) the array of markers
     * represented by a cluster marker and (2) the number of cluster icon styles. It returns a
     *  ClusterIconInfo object. The default `calculator` returns a
     * `text` property which is the number of markers in the cluster and an
     * `index` property which is one higher than the lowest integer such that
     * `10^i` exceeds the number of markers in the cluster, or the size of the styles
     * array, whichever is less. The `styles` array element used has an index of
     * `index` minus 1. For example, the default `calculator` returns a
     * `text` value of `"125"` and an `index` of `3`
     * for a cluster icon representing 125 markers so the element used in the `styles`
     * array is `2`. A `calculator` may also return a `title`
     * property that contains the text of the tooltip to be used for the cluster marker. If
     * `title` is not defined, the tooltip is set to the value of the `title`
     * property for the MarkerClusterer.
     *
     *  default MarkerClusterer . CALCULATOR calculator ? : Calculator ; default clusterClass ? : string ; /**
     * An array of  ClusterIconStyle elements defining the styles
     * of the cluster markers to be used. The element to be used to style a given cluster marker
     * is determined by the function defined by the `calculator` property.
     * The default is an array of  ClusterIconStyle elements whose properties are derived
     * from the values for `imagePath`, `imageExtension`, and `imageSizes`.
     *
     *  default styles ? : ClusterIconStyle [ ] ; default enableRetinaIcons ? : boolean ; default batchSize ? : number ; default batchSizeIE ? : number ; default imagePath ? : string ; default imageExtension ? : string ; default imageSizes ? : number [ ] ; /**
     * A function to take the text attribute associated with the cluster and output a string to attach an
     * ariaLabel to the cluster
     */ ariaLabelFn ? : AriaLabelFn ; } ignore const getOption = < T , K extends keyof T > ( options , prop , def ) : T [ K ] => { if ( options [ prop ] !== undefined ) { return options [ prop ] ; } else { return def ; } } ; export class MarkerClusterer extends OverlayViewSafe { /**
     * The number of markers to process in one batch.
     */ static BATCH_SIZE = 2000 ; /**
     * The number of markers to process in one batch (IE only).
     */ static BATCH_SIZE_IE = 500 ; /**
     * The default root name for the marker cluster images.
     */ static IMAGE_PATH = "../images/m" ; /**
     * The default extension name for the marker cluster images.
     */ static IMAGE_EXTENSION = "png" ; /**
     * The default array of sizes for the marker cluster images.
     */ static IMAGE_SIZES : number [ ] = [ 53 , 56 , 66 , 78 , 90 ] ; private markers_ : ClusterAugmentedMarker [ ] = [ ] ; private clusters_ : Cluster [ ] = [ ] ; private listeners_ : google . maps . MapsEventListener [ ] = [ ] ; private activeMap_ : google . maps . Map = null ; private ready_ = false ; public ariaLabelFn = this . options . ariaLabelFn || ( ( ) : string => "" ) ; private zIndex_ = this . options . zIndex || Number ( google . maps . Marker . MAX_ZINDEX ) + 1 ; private gridSize_ = this . options . gridSize || 60 ; private minClusterSize_ = this . options . minimumClusterSize || 2 ; private maxZoom_ = this . options . maxZoom || null ; private styles_ : ClusterIconStyle [ ] = this . options . styles || [ ] ; private title_ = this . options . title || "" ; private zoomOnClick_ = getOption ( this . options , "zoomOnClick" , true ) ; private averageCenter_ = getOption ( this . options , "averageCenter" , false ) ; private ignoreHidden_ = getOption ( this . options , "ignoreHidden" , false ) ; private enableRetinaIcons_ = getOption ( this . options , "enableRetinaIcons" , false ) ; private imagePath_ = this . options . imagePath || MarkerClusterer . IMAGE_PATH ; private imageExtension_ = this . options . imageExtension || MarkerClusterer . IMAGE_EXTENSION ; private imageSizes_ = this . options . imageSizes || MarkerClusterer . IMAGE_SIZES ; private calculator_ = this . options . calculator || MarkerClusterer . CALCULATOR ; private batchSize_ = this . options . batchSize || MarkerClusterer . BATCH_SIZE ; private batchSizeIE_ = this . options . batchSizeIE || MarkerClusterer . BATCH_SIZE_IE ; private clusterClass_ = this . options . clusterClass || "cluster" ; private prevZoom_ : number ; private timerRefStatic : number ; /**
     * Creates a MarkerClusterer object with the options specified in  MarkerClustererOptions .
     *  param map param markers param options constructor ( map , markers = [ ] , private options = { } ) { super ( ) ; if ( navigator . userAgent . toLowerCase ( ) . indexOf ( "msie" ) !== - 1 ) { this . batchSize_ = this . batchSizeIE_ ; } this . setupStyles_ ( ) ; this . addMarkers ( markers , true ) ; this . setMap ( map ) ; } ignore onAdd ( ) { this . activeMap_ = this . getMap ( ) as google . maps . Map ; this . ready_ = true ; this . repaint ( ) ; this . prevZoom_ = this . getMap ( ) . getZoom ( ) ; this . listeners_ = [ google . maps . event . addListener ( this . getMap ( ) , "zoom_changed" , ( ) => { const map = this . getMap ( ) as any ; const minZoom = map . minZoom || 0 ; const maxZoom = Math . min ( map . maxZoom || 100 , map . mapTypes [ map . getMapTypeId ( ) ] . maxZoom ) ; const zoom = Math . min ( Math . max ( this . getMap ( ) . getZoom ( ) , minZoom ) , maxZoom ) ; if ( this . prevZoom_ != zoom ) { this . prevZoom_ = zoom ; this . resetViewport_ ( false ) ; } } ) , google . maps . event . addListener ( this . getMap ( ) , "idle" , ( ) => { this . redraw_ ( ) ; } ) , ] ; } ignore onRemove ( ) { for ( let i = 0 ; i < this . markers_ . length ; i ++ ) { if ( this . markers_ [ i ] . getMap ( ) !== this . activeMap_ ) { this . markers_ [ i ] . setMap ( this . activeMap_ ) ; } } for ( let i = 0 ; i < this . clusters_ . length ; i ++ ) { this . clusters_ [ i ] . remove ( ) ; } this . clusters_ = [ ] ; for ( let i = 0 ; i < this . listeners_ . length ; i ++ ) { google . maps . event . removeListener ( this . listeners_ [ i ] ) ; } this . listeners_ = [ ] ; this . activeMap_ = null ; this . ready_ = false ; } ignore draw ( ) { } /**
     * Sets up the styles object.
     */ private setupStyles_ ( ) { if ( this . styles_ . length > 0 ) { return ; } for ( let i = 0 ; i < this . imageSizes_ . length ; i ++ ) { const size = this . imageSizes_ [ i ] ; this . styles_ . push ( MarkerClusterer . withDefaultStyle ( { url : this . imagePath_ + ( i + 1 ) + "." + this . imageExtension_ , height : size , width : size } ) ) ; } } /**
     *  Fits the map to the bounds of the markers managed by the clusterer.
     */ fitMapToMarkers ( padding ) { const markers = this . getMarkers ( ) ; const bounds = new google . maps . LatLngBounds ( ) ; for ( let i = 0 ; i < markers . length ; i ++ ) { if ( markers [ i ] . getVisible ( ) || ! this . getIgnoreHidden ( ) ) { bounds . extend ( markers [ i ] . getPosition ( ) ) ; } } ( this . getMap ( ) as google . maps . Map ) . fitBounds ( bounds , padding ) ; } return getGridSize ( ) { return this . gridSize_ ; } param gridSize setGridSize ( gridSize ) { this . gridSize_ = gridSize ; } return getMinimumClusterSize ( ) { return this . minClusterSize_ ; } param minimumClusterSize setMinimumClusterSize ( minimumClusterSize ) { this . minClusterSize_ = minimumClusterSize ; } return getMaxZoom ( ) { return this . maxZoom_ ; } param maxZoom setMaxZoom ( maxZoom ) { this . maxZoom_ = maxZoom ; } getZIndex ( ) { return this . zIndex_ ; } setZIndex ( zIndex ) { this . zIndex_ = zIndex ; } return getStyles ( ) { return this . styles_ ; } param styles setStyles ( styles ) { this . styles_ = styles ; } return getTitle ( ) { return this . title_ ; } param title setTitle ( title ) { this . title_ = title ; } return getZoomOnClick ( ) { return this . zoomOnClick_ ; } param zoomOnClick setZoomOnClick ( zoomOnClick ) { this . zoomOnClick_ = zoomOnClick ; } return getAverageCenter ( ) { return this . averageCenter_ ; } param averageCenter setAverageCenter ( averageCenter ) { this . averageCenter_ = averageCenter ; } return getIgnoreHidden ( ) { return this . ignoreHidden_ ; } param ignoreHidden setIgnoreHidden ( ignoreHidden ) { this . ignoreHidden_ = ignoreHidden ; } return getEnableRetinaIcons ( ) { return this . enableRetinaIcons_ ; } param enableRetinaIcons setEnableRetinaIcons ( enableRetinaIcons ) { this . enableRetinaIcons_ = enableRetinaIcons ; } return getImageExtension ( ) { return this . imageExtension_ ; } param imageExtension setImageExtension ( imageExtension ) { this . imageExtension_ = imageExtension ; } return getImagePath ( ) { return this . imagePath_ ; } param imagePath setImagePath ( imagePath ) { this . imagePath_ = imagePath ; } return getImageSizes ( ) { return this . imageSizes_ ; } param imageSizes setImageSizes ( imageSizes ) { this . imageSizes_ = imageSizes ; } return getCalculator ( ) { return this . calculator_ ; } param calculator setCalculator ( calculator ) { this . calculator_ = calculator ; } return getBatchSizeIE ( ) { return this . batchSizeIE_ ; } param batchSizeIE setBatchSizeIE ( batchSizeIE ) { this . batchSizeIE_ = batchSizeIE ; } return getClusterClass ( ) { return this . clusterClass_ ; } param clusterClass setClusterClass ( clusterClass ) { this . clusterClass_ = clusterClass ; } return getMarkers ( ) { return this . markers_ ; } return getTotalMarkers ( ) { return this . markers_ . length ; } return getClusters ( ) { return this . clusters_ ; } return getTotalClusters ( ) { return this . clusters_ . length ; } param marker param nodraw addMarker ( marker , nodraw ? ) { this . pushMarkerTo_ ( marker ) ; if ( ! nodraw ) { this . redraw_ ( ) ; } } param markers param nodraw addMarkers ( markers , nodraw ? ) { for ( const key in markers ) { if ( Object . prototype . hasOwnProperty . call ( markers , key ) ) { this . pushMarkerTo_ ( markers [ key ] ) ; } } if ( ! nodraw ) { this . redraw_ ( ) ; } } param marker private pushMarkerTo_ ( marker ) { if ( marker . getDraggable ( ) ) { google . maps . event . addListener ( marker , "dragend" , ( ) => { if ( this . ready_ ) { marker . isAdded = false ; this . repaint ( ) ; } } ) ; } marker . isAdded = false ; this . markers_ . push ( marker ) ; } param marker param nodraw return removeMarker ( marker , nodraw ? ) { const removed = this . removeMarker_ ( marker ) ; if ( ! nodraw && removed ) { this . repaint ( ) ; } return removed ; } param markers param nodraw return removeMarkers ( markers , nodraw ? ) { let removed = false ; for ( let i = 0 ; i < markers . length ; i ++ ) { const r = this . removeMarker_ ( markers [ i ] ) ; removed = removed || r ; } if ( ! nodraw && removed ) { this . repaint ( ) ; } return removed ; } param marker return private removeMarker_ ( marker ) { let index = - 1 ; if ( this . markers_ . indexOf ) { index = this . markers_ . indexOf ( marker ) ; } else { for ( let i = 0 ; i < this . markers_ . length ; i ++ ) { if ( marker === this . markers_ [ i ] ) { index = i ; break ; } } } if ( index === - 1 ) { return false ; } marker . setMap ( null ) ; this . markers_ . splice ( index , 1 ) ; return true ; } /**
     * Removes all clusters and markers from the map and also removes all markers
     *  managed by the clusterer.
     */ clearMarkers ( ) { this . resetViewport_ ( true ) ; this . markers_ = [ ] ; } /**
     * Recalculates and redraws all the marker clusters from scratch.
     *  Call this after changing any properties.
     */ repaint ( ) { const oldClusters = this . clusters_ . slice ( ) ; this . clusters_ = [ ] ; this . resetViewport_ ( false ) ; this . redraw_ ( ) ; setTimeout ( ( ) => { for ( let i = 0 ; i < oldClusters . length ; i ++ ) { oldClusters [ i ] . remove ( ) ; } } , 0 ) ; } param bounds return ignore getExtendedBounds ( bounds ) { const projection = this . getProjection ( ) ; const tr = new google . maps . LatLng ( bounds . getNorthEast ( ) . lat ( ) , bounds . getNorthEast ( ) . lng ( ) ) ; const bl = new google . maps . LatLng ( bounds . getSouthWest ( ) . lat ( ) , bounds . getSouthWest ( ) . lng ( ) ) ; const trPix = projection . fromLatLngToDivPixel ( tr ) ; trPix . x += this . gridSize_ ; trPix . y -= this . gridSize_ ; const blPix = projection . fromLatLngToDivPixel ( bl ) ; blPix . x -= this . gridSize_ ; blPix . y += this . gridSize_ ; const ne = projection . fromDivPixelToLatLng ( trPix ) ; const sw = projection . fromDivPixelToLatLng ( blPix ) ; bounds . extend ( ne ) ; bounds . extend ( sw ) ; return bounds ; } /**
     * Redraws all the clusters.
     */ private redraw_ ( ) { this . createClusters_ ( 0 ) ; } param hide private resetViewport_ ( hide ? ) { for ( let i = 0 ; i < this . clusters_ . length ; i ++ ) { this . clusters_ [ i ] . remove ( ) ; } this . clusters_ = [ ] ; for ( let i = 0 ; i < this . markers_ . length ; i ++ ) { const marker = this . markers_ [ i ] ; marker . isAdded = false ; if ( hide ) { marker . setMap ( null ) ; } } } param p1 param p2 return link private distanceBetweenPoints_ ( p1 , p2 ) { const R = 6371 ; const dLat = ( ( p2 . lat ( ) - p1 . lat ( ) ) * Math . PI ) / 180 ; const dLon = ( ( p2 . lng ( ) - p1 . lng ( ) ) * Math . PI ) / 180 ; const a = Math . sin ( dLat / 2 ) * Math . sin ( dLat / 2 ) + Math . cos ( ( p1 . lat ( ) * Math . PI ) / 180 ) * Math . cos ( ( p2 . lat ( ) * Math . PI ) / 180 ) * Math . sin ( dLon / 2 ) * Math . sin ( dLon / 2 ) ; const c = 2 * Math . atan2 ( Math . sqrt ( a ) , Math . sqrt ( 1 - a ) ) ; return R * c ; } param marker param bounds return private isMarkerInBounds_ ( marker , bounds ) { return bounds . contains ( marker . getPosition ( ) ) ; } param marker private addToClosestCluster_ ( marker ) { let distance = 40000 ; let clusterToAddTo = null ; for ( let i = 0 ; i < this . clusters_ . length ; i ++ ) { const cluster = this . clusters_ [ i ] ; const center = cluster . getCenter ( ) ; if ( center ) { const d = this . distanceBetweenPoints_ ( center , marker . getPosition ( ) ) ; if ( d < distance ) { distance = d ; clusterToAddTo = cluster ; } } } if ( clusterToAddTo && clusterToAddTo . isMarkerInClusterBounds ( marker ) ) { clusterToAddTo . addMarker ( marker ) ; } else { const cluster = new Cluster ( this ) ; cluster . addMarker ( marker ) ; this . clusters_ . push ( cluster ) ; } } param iFirst private createClusters_ ( iFirst ) { if ( ! this . ready_ ) { return ; } if ( iFirst === 0 ) { google . maps . event . trigger ( this , "clusteringbegin" , this ) ; if ( typeof this . timerRefStatic !== "undefined" ) { clearTimeout ( this . timerRefStatic ) ; delete this . timerRefStatic ; } } const mapBounds = new google . maps . LatLngBounds ( ( this . getMap ( ) as google . maps . Map ) . getBounds ( ) . getSouthWest ( ) , ( this . getMap ( ) as google . maps . Map ) . getBounds ( ) . getNorthEast ( ) ) ; const bounds = this . getExtendedBounds ( mapBounds ) ; const iLast = Math . min ( iFirst + this . batchSize_ , this . markers_ . length ) ; for ( let i = iFirst ; i < iLast ; i ++ ) { const marker = this . markers_ [ i ] ; if ( ! marker . isAdded && this . isMarkerInBounds_ ( marker , bounds ) ) { if ( ! this . ignoreHidden_ || ( this . ignoreHidden_ && marker . getVisible ( ) ) ) { this . addToClosestCluster_ ( marker ) ; } } } if ( iLast < this . markers_ . length ) { this . timerRefStatic = window . setTimeout ( ( ) => { this . createClusters_ ( iLast ) ; } , 0 ) ; } else { delete this . timerRefStatic ; google . maps . event . trigger ( this , "clusteringend" , this ) ; for ( let i = 0 ; i < this . clusters_ . length ; i ++ ) { this . clusters_ [ i ] . updateIcon ( ) ; } } } param markers param numStyles return static CALCULATOR ( markers , numStyles ) { let index = 0 ; const count = markers . length ; let dv = count ; while ( dv !== 0 ) { dv = Math . floor ( dv / 10 ) ; index ++ ; } index = Math . min ( index , numStyles ) ; return { text : count . toString ( ) , index : index , title : "" } ; } param overrides static withDefaultStyle ( overrides ) { return { textColor : "black" , textSize : 11 , textDecoration : "none" , textLineHeight : overrides . height , fontWeight : "bold" , fontStyle : "normal" , fontFamily : "Arial,sans-serif" , backgroundPosition : "0 0" , ... overrides } ; } }