From 383b332421c1673e526411079c46e1c3ff2ec4d6 Mon Sep 17 00:00:00 2001 From: Oskar Kumor Date: Wed, 10 Apr 2024 21:24:06 +0200 Subject: [PATCH] feat(google-maps): switch to non-deprecated API for clustering and support for advanced markers --- src/dev-app/google-map/google-map-demo.html | 31 +- src/dev-app/google-map/google-map-demo.ts | 5 +- .../map-marker-clusterer/README.md | 23 +- .../map-marker-clusterer.spec.ts | 356 +++-------- .../map-marker-clusterer.ts | 584 +++++------------- .../marker-clusterer-types.ts | 300 +++++---- src/google-maps/public-api.ts | 6 - 7 files changed, 424 insertions(+), 881 deletions(-) diff --git a/src/dev-app/google-map/google-map-demo.html b/src/dev-app/google-map/google-map-demo.html index c1c1062509eb..759fb5ca5784 100644 --- a/src/dev-app/google-map/google-map-demo.html +++ b/src/dev-app/google-map/google-map-demo.html @@ -9,8 +9,11 @@ (mapMousemove)="handleMove($event)" (mapRightclick)="handleRightclick()" [mapTypeId]="mapTypeId"> - - + @for (markerPosition of markerPositions; track markerPosition) { @@ -19,7 +22,29 @@ [options]="markerOptions" (mapClick)="clickMarker(marker)"> } - + --> + + + + @for (markerPosition of markerPositions; track markerPosition) { + + + + + + + + + + } + + @if (hasAdvancedMarker) { ` tag. Unlike the other Google Maps components, MapMarkerClusterer does not have an `options` input, so any input (listed in the [documentation](https://googlemaps.github.io/js-markerclustererplus/index.html) for the `MarkerClusterer` class) should be set directly. +The `MapMarkerClusterer` component wraps the [`MarkerClusterer` class](https://googlemaps.github.io/js-markerclusterer/classes/MarkerClusterer.html) from the [Google Maps JavaScript MarkerClusterer Library](https://github.com/googlemaps/js-markerclusterer). The `MapMarkerClusterer` component displays a cluster of markers that are children of the `` tag. Unlike the other Google Maps components, MapMarkerClusterer does not have an `options` input, so any input (listed in the [documentation](https://googlemaps.github.io/js-markerclusterer/) for the `MarkerClusterer` class) should be set directly. ## Loading the Library -Like the Google Maps JavaScript API, the MarkerClustererPlus library needs to be loaded separately. This can be accomplished by using this script tag: +Like the Google Maps JavaScript API, the MarkerClusterer library needs to be loaded separately. This can be accomplished by using this script tag: ```html - + ``` Additional information can be found by looking at [Marker Clustering](https://developers.google.com/maps/documentation/javascript/marker-clustering) in the Google Maps JavaScript API documentation. @@ -17,20 +17,18 @@ Additional information can be found by looking at [Marker Clustering](https://de ```typescript // google-map-demo.component.ts import {Component} from '@angular/core'; -import {GoogleMap, MapMarker, MapMarkerClusterer} from '@angular/google-maps'; +import {GoogleMap, MapAdvancedMarker, MapMarkerClusterer} from '@angular/google-maps'; @Component({ selector: 'google-map-demo', templateUrl: 'google-map-demo.html', standalone: true, - imports: [GoogleMap, MapMarker, MapMarkerClusterer], + imports: [GoogleMap, MapAdvancedMarker, MapMarkerClusterer], }) export class GoogleMapDemo { center: google.maps.LatLngLiteral = {lat: 24, lng: 12}; zoom = 4; markerPositions: google.maps.LatLngLiteral[] = []; - markerClustererImagePath = - 'https://developers.google.com/maps/documentation/javascript/examples/markerclusterer/m'; addMarker(event: google.maps.MapMouseEvent) { this.markerPositions.push(event.latLng.toJSON()); @@ -40,15 +38,16 @@ export class GoogleMapDemo { ```html - - + (mapClick)="addMarker($event)" + [mapId]="'123'"> + @for (position of markerPositions; track position) { - + } diff --git a/src/google-maps/map-marker-clusterer/map-marker-clusterer.spec.ts b/src/google-maps/map-marker-clusterer/map-marker-clusterer.spec.ts index 1274056c6a96..32dceda13ee2 100644 --- a/src/google-maps/map-marker-clusterer/map-marker-clusterer.spec.ts +++ b/src/google-maps/map-marker-clusterer/map-marker-clusterer.spec.ts @@ -1,8 +1,8 @@ import {Component, ViewChild} from '@angular/core'; -import {ComponentFixture, TestBed, fakeAsync, flush} from '@angular/core/testing'; +import {waitForAsync, ComponentFixture, TestBed} from '@angular/core/testing'; -import {DEFAULT_OPTIONS, GoogleMap} from '../google-map/google-map'; -import {MapMarker} from '../map-marker/map-marker'; +import {DEFAULT_OPTIONS} from '../google-map/google-map'; +import {GoogleMapsModule} from '../google-maps-module'; import { createMapConstructorSpy, createMapSpy, @@ -12,13 +12,6 @@ import { createMarkerSpy, } from '../testing/fake-google-map-utils'; import {MapMarkerClusterer} from './map-marker-clusterer'; -import { - AriaLabelFn, - Calculator, - ClusterIconStyle, - MarkerClusterer, - MarkerClustererOptions, -} from './marker-clusterer-types'; describe('MapMarkerClusterer', () => { let mapSpy: jasmine.SpyObj; @@ -28,9 +21,18 @@ describe('MapMarkerClusterer', () => { const anyMarkerMatcher = jasmine.any(Object) as unknown as google.maps.Marker; + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + imports: [GoogleMapsModule], + declarations: [TestApp], + }); + })); + beforeEach(() => { + TestBed.compileComponents(); + mapSpy = createMapSpy(DEFAULT_OPTIONS); - createMapConstructorSpy(mapSpy); + createMapConstructorSpy(mapSpy).and.callThrough(); const markerSpy = createMarkerSpy({}); // The spy target function cannot be an arrow-function as this breaks when created @@ -40,148 +42,90 @@ describe('MapMarkerClusterer', () => { }); markerClustererSpy = createMarkerClustererSpy(); - markerClustererConstructorSpy = createMarkerClustererConstructorSpy(markerClustererSpy); + markerClustererConstructorSpy = + createMarkerClustererConstructorSpy(markerClustererSpy).and.callThrough(); fixture = TestBed.createComponent(TestApp); }); afterEach(() => { (window.google as any) = undefined; - (window as any).MarkerClusterer = undefined; + (window as any).markerClusterer = undefined; }); - it('throws an error if the clustering library has not been loaded', fakeAsync(() => { - (window as any).MarkerClusterer = undefined; - markerClustererConstructorSpy = createMarkerClustererConstructorSpy(markerClustererSpy, false); - - expect(() => { - fixture.detectChanges(); - flush(); - }).toThrowError(/MarkerClusterer class not found, cannot construct a marker cluster/); - })); + it('throws an error if the clustering library has not been loaded', () => { + (window as any).markerClusterer = undefined; + markerClustererConstructorSpy = createMarkerClustererConstructorSpy( + markerClustererSpy, + false, + ).and.callThrough(); + + expect(() => fixture.detectChanges()).toThrow( + new Error( + 'MarkerClusterer class not found, cannot construct a marker cluster. ' + + 'Please install the MarkerClusterer library: ' + + 'https://github.com/googlemaps/js-markerclusterer', + ), + ); + }); - it('initializes a Google Map Marker Clusterer', fakeAsync(() => { + it('initializes a Google Map Marker Clusterer', () => { fixture.detectChanges(); - flush(); - - expect(markerClustererConstructorSpy).toHaveBeenCalledWith(mapSpy, [], { - ariaLabelFn: undefined, - averageCenter: undefined, - batchSize: undefined, - batchSizeIE: undefined, - calculator: undefined, - clusterClass: undefined, - enableRetinaIcons: undefined, - gridSize: undefined, - ignoreHidden: undefined, - imageExtension: undefined, - imagePath: undefined, - imageSizes: undefined, - maxZoom: undefined, - minimumClusterSize: undefined, - styles: undefined, - title: undefined, - zIndex: undefined, - zoomOnClick: undefined, + + expect(markerClustererConstructorSpy).toHaveBeenCalledWith({ + map: mapSpy, + renderer: undefined, + algorithm: undefined, + onClusterClick: jasmine.any(Function), }); - })); + }); - it('sets marker clusterer inputs', fakeAsync(() => { - fixture.componentInstance.ariaLabelFn = (testString: string) => testString; - fixture.componentInstance.averageCenter = true; - fixture.componentInstance.batchSize = 1; - fixture.componentInstance.clusterClass = 'testClusterClass'; - fixture.componentInstance.enableRetinaIcons = true; - fixture.componentInstance.gridSize = 2; - fixture.componentInstance.ignoreHidden = true; - fixture.componentInstance.imageExtension = 'testImageExtension'; - fixture.componentInstance.imagePath = 'testImagePath'; - fixture.componentInstance.imageSizes = [3]; - fixture.componentInstance.maxZoom = 4; - fixture.componentInstance.minimumClusterSize = 5; - fixture.componentInstance.styles = []; - fixture.componentInstance.title = 'testTitle'; - fixture.componentInstance.zIndex = 6; - fixture.componentInstance.zoomOnClick = true; + it('sets marker clusterer inputs', () => { + fixture.componentInstance.algorithm = {name: 'custom'}; + fixture.componentInstance.renderer = {render: () => null!}; fixture.detectChanges(); - flush(); - - expect(markerClustererConstructorSpy).toHaveBeenCalledWith(mapSpy, [], { - ariaLabelFn: jasmine.any(Function), - averageCenter: true, - batchSize: 1, - batchSizeIE: undefined, - calculator: undefined, - clusterClass: 'testClusterClass', - enableRetinaIcons: true, - gridSize: 2, - ignoreHidden: true, - imageExtension: 'testImageExtension', - imagePath: 'testImagePath', - imageSizes: [3], - maxZoom: 4, - minimumClusterSize: 5, - styles: [], - title: 'testTitle', - zIndex: 6, - zoomOnClick: true, + + expect(markerClustererConstructorSpy).toHaveBeenCalledWith({ + map: mapSpy, + algorithm: fixture.componentInstance.algorithm, + renderer: fixture.componentInstance.renderer, + onClusterClick: jasmine.any(Function), }); - })); + }); - it('sets marker clusterer options', fakeAsync(() => { - fixture.detectChanges(); - flush(); - const options: MarkerClustererOptions = { - enableRetinaIcons: true, - gridSize: 1337, - ignoreHidden: true, - imageExtension: 'png', - }; - fixture.componentInstance.options = options; + it('recreates the clusterer if the options change', () => { + fixture.componentInstance.algorithm = {name: 'custom1'}; fixture.detectChanges(); - expect(markerClustererSpy.setOptions).toHaveBeenCalledWith(jasmine.objectContaining(options)); - })); - it('gives precedence to specific inputs over options', fakeAsync(() => { - fixture.detectChanges(); - flush(); - const options: MarkerClustererOptions = { - enableRetinaIcons: true, - gridSize: 1337, - ignoreHidden: true, - imageExtension: 'png', - }; - const expectedOptions: MarkerClustererOptions = { - enableRetinaIcons: false, - gridSize: 42, - ignoreHidden: false, - imageExtension: 'jpeg', - }; - fixture.componentInstance.enableRetinaIcons = expectedOptions.enableRetinaIcons; - fixture.componentInstance.gridSize = expectedOptions.gridSize; - fixture.componentInstance.ignoreHidden = expectedOptions.ignoreHidden; - fixture.componentInstance.imageExtension = expectedOptions.imageExtension; - fixture.componentInstance.options = options; + expect(markerClustererConstructorSpy).toHaveBeenCalledWith({ + map: mapSpy, + algorithm: jasmine.objectContaining({name: 'custom1'}), + renderer: undefined, + onClusterClick: jasmine.any(Function), + }); + + fixture.componentInstance.algorithm = {name: 'custom2'}; fixture.detectChanges(); - expect(markerClustererSpy.setOptions).toHaveBeenCalledWith( - jasmine.objectContaining(expectedOptions), - ); - })); + expect(markerClustererConstructorSpy).toHaveBeenCalledWith({ + map: mapSpy, + algorithm: jasmine.objectContaining({name: 'custom2'}), + renderer: undefined, + onClusterClick: jasmine.any(Function), + }); + }); - it('sets Google Maps Markers in the MarkerClusterer', fakeAsync(() => { + it('sets Google Maps Markers in the MarkerClusterer', () => { fixture.detectChanges(); - flush(); expect(markerClustererSpy.addMarkers).toHaveBeenCalledWith([ anyMarkerMatcher, anyMarkerMatcher, ]); - })); + }); - it('updates Google Maps Markers in the Marker Clusterer', fakeAsync(() => { + it('updates Google Maps Markers in the Marker Clusterer', () => { fixture.detectChanges(); - flush(); expect(markerClustererSpy.addMarkers).toHaveBeenCalledWith([ anyMarkerMatcher, @@ -190,109 +134,40 @@ describe('MapMarkerClusterer', () => { fixture.componentInstance.state = 'state2'; fixture.detectChanges(); - flush(); expect(markerClustererSpy.addMarkers).toHaveBeenCalledWith([anyMarkerMatcher], true); expect(markerClustererSpy.removeMarkers).toHaveBeenCalledWith([anyMarkerMatcher], true); - expect(markerClustererSpy.repaint).toHaveBeenCalledTimes(1); + expect(markerClustererSpy.render).toHaveBeenCalledTimes(1); fixture.componentInstance.state = 'state0'; fixture.detectChanges(); - flush(); expect(markerClustererSpy.addMarkers).toHaveBeenCalledWith([], true); expect(markerClustererSpy.removeMarkers).toHaveBeenCalledWith( [anyMarkerMatcher, anyMarkerMatcher], true, ); - expect(markerClustererSpy.repaint).toHaveBeenCalledTimes(2); - })); + expect(markerClustererSpy.render).toHaveBeenCalledTimes(2); + }); - it('exposes marker clusterer methods', fakeAsync(() => { + it('initializes event handlers on the map related to clustering', () => { fixture.detectChanges(); - flush(); - const markerClustererComponent = fixture.componentInstance.markerClusterer; - markerClustererComponent.fitMapToMarkers(5); - expect(markerClustererSpy.fitMapToMarkers).toHaveBeenCalledWith(5); - - markerClustererSpy.getAverageCenter.and.returnValue(true); - expect(markerClustererComponent.getAverageCenter()).toBe(true); - - markerClustererSpy.getBatchSizeIE.and.returnValue(6); - expect(markerClustererComponent.getBatchSizeIE()).toBe(6); - - const calculator = (markers: google.maps.Marker[], count: number) => ({ - index: 1, - text: 'testText', - title: 'testTitle', - }); - markerClustererSpy.getCalculator.and.returnValue(calculator); - expect(markerClustererComponent.getCalculator()).toBe(calculator); - - markerClustererSpy.getClusterClass.and.returnValue('testClusterClass'); - expect(markerClustererComponent.getClusterClass()).toBe('testClusterClass'); - - markerClustererSpy.getClusters.and.returnValue([]); - expect(markerClustererComponent.getClusters()).toEqual([]); - - markerClustererSpy.getEnableRetinaIcons.and.returnValue(true); - expect(markerClustererComponent.getEnableRetinaIcons()).toBe(true); - - markerClustererSpy.getGridSize.and.returnValue(7); - expect(markerClustererComponent.getGridSize()).toBe(7); - - markerClustererSpy.getIgnoreHidden.and.returnValue(true); - expect(markerClustererComponent.getIgnoreHidden()).toBe(true); - - markerClustererSpy.getImageExtension.and.returnValue('testImageExtension'); - expect(markerClustererComponent.getImageExtension()).toBe('testImageExtension'); - - markerClustererSpy.getImagePath.and.returnValue('testImagePath'); - expect(markerClustererComponent.getImagePath()).toBe('testImagePath'); - - markerClustererSpy.getImageSizes.and.returnValue([]); - expect(markerClustererComponent.getImageSizes()).toEqual([]); - - markerClustererSpy.getMaxZoom.and.returnValue(8); - expect(markerClustererComponent.getMaxZoom()).toBe(8); - - markerClustererSpy.getMinimumClusterSize.and.returnValue(9); - expect(markerClustererComponent.getMinimumClusterSize()).toBe(9); - - markerClustererSpy.getStyles.and.returnValue([]); - expect(markerClustererComponent.getStyles()).toEqual([]); - - markerClustererSpy.getTitle.and.returnValue('testTitle'); - expect(markerClustererComponent.getTitle()).toBe('testTitle'); - - markerClustererSpy.getTotalClusters.and.returnValue(10); - expect(markerClustererComponent.getTotalClusters()).toBe(10); - - markerClustererSpy.getTotalMarkers.and.returnValue(11); - expect(markerClustererComponent.getTotalMarkers()).toBe(11); + expect(mapSpy.addListener).toHaveBeenCalledWith('clusteringbegin', jasmine.any(Function)); + expect(mapSpy.addListener).not.toHaveBeenCalledWith('clusteringend', jasmine.any(Function)); + }); - markerClustererSpy.getZIndex.and.returnValue(12); - expect(markerClustererComponent.getZIndex()).toBe(12); + it('emits to clusterClick when the `onClusterClick` callback is invoked', () => { + fixture.detectChanges(); - markerClustererSpy.getZoomOnClick.and.returnValue(true); - expect(markerClustererComponent.getZoomOnClick()).toBe(true); - })); + expect(fixture.componentInstance.onClusterClick).not.toHaveBeenCalled(); - it('initializes marker clusterer event handlers', fakeAsync(() => { + const callback = markerClustererConstructorSpy.calls.mostRecent().args[0].onClusterClick; + callback({}, {}, {}); fixture.detectChanges(); - flush(); - expect(markerClustererSpy.addListener).toHaveBeenCalledWith( - 'clusteringbegin', - jasmine.any(Function), - ); - expect(markerClustererSpy.addListener).not.toHaveBeenCalledWith( - 'clusteringend', - jasmine.any(Function), - ); - expect(markerClustererSpy.addListener).toHaveBeenCalledWith('click', jasmine.any(Function)); - })); + expect(fixture.componentInstance.onClusterClick).toHaveBeenCalledTimes(1); + }); }); @Component({ @@ -300,67 +175,22 @@ describe('MapMarkerClusterer', () => { template: ` - @if (state === 'state1') { - - } - @if (state === 'state1' || state === 'state2') { - - } - @if (state === 'state2') { - - } + (clusterClick)="onClusterClick()" + [renderer]="renderer" + [algorithm]="algorithm"> + + + `, - standalone: true, - imports: [GoogleMap, MapMarker, MapMarkerClusterer], }) class TestApp { @ViewChild(MapMarkerClusterer) markerClusterer: MapMarkerClusterer; - - ariaLabelFn?: AriaLabelFn; - averageCenter?: boolean; - batchSize?: number; - batchSizeIE?: number; - calculator?: Calculator; - clusterClass?: string; - enableRetinaIcons?: boolean; - gridSize?: number; - ignoreHidden?: boolean; - imageExtension?: string; - imagePath?: string; - imageSizes?: number[]; - maxZoom?: number; - minimumClusterSize?: number; - styles?: ClusterIconStyle[]; - title?: string; - zIndex?: number; - zoomOnClick?: boolean; - options?: MarkerClustererOptions; - + renderer: Renderer; + algorithm: Algorithm; state = 'state1'; - - onClusteringBegin() {} - onClusterClick() {} + onClusteringBegin = jasmine.createSpy('onclusteringbegin spy'); + onClusterClick = jasmine.createSpy('clusterClick spy'); } diff --git a/src/google-maps/map-marker-clusterer/map-marker-clusterer.ts b/src/google-maps/map-marker-clusterer/map-marker-clusterer.ts index c9edcf57c9c4..6e1b1ba916d2 100644 --- a/src/google-maps/map-marker-clusterer/map-marker-clusterer.ts +++ b/src/google-maps/map-marker-clusterer/map-marker-clusterer.ts @@ -1,3 +1,4 @@ +import {MapAdvancedMarker} from './../map-advanced-marker/map-advanced-marker'; /** * @license * Copyright Google LLC All Rights Reserved. @@ -8,6 +9,7 @@ // Workaround for: https://github.com/bazelbuild/rules_nodejs/issues/1265 /// +/// import { AfterContentInit, @@ -26,30 +28,18 @@ import { ViewEncapsulation, inject, } from '@angular/core'; -import {Observable, Subject} from 'rxjs'; -import {take, takeUntil} from 'rxjs/operators'; +import {Observable, Subscription, combineLatest, merge} from 'rxjs'; import {GoogleMap} from '../google-map/google-map'; import {MapEventManager} from '../map-event-manager'; import {MapMarker} from '../map-marker/map-marker'; -import { - AriaLabelFn, - Calculator, - Cluster, - ClusterIconStyle, - MarkerClusterer as MarkerClustererInstance, - MarkerClustererOptions, -} from './marker-clusterer-types'; - -/** Default options for a clusterer. */ -const DEFAULT_CLUSTERER_OPTIONS: MarkerClustererOptions = {}; -/** - * The clusterer has to be defined and referred to as a global variable, - * otherwise it'll cause issues when minified through Closure. - */ -declare const MarkerClusterer: typeof MarkerClustererInstance; +declare const markerClusterer: { + MarkerClusterer: typeof MarkerClusterer; + defaultOnClusterClickHandler: onClusterClickHandler; +}; +type MapMarkerComponent = MapMarker | MapAdvancedMarker; /** * Angular component for implementing a Google Maps Marker Clusterer. * @@ -59,473 +49,203 @@ declare const MarkerClusterer: typeof MarkerClustererInstance; selector: 'map-marker-clusterer', exportAs: 'mapMarkerClusterer', changeDetection: ChangeDetectionStrategy.OnPush, - standalone: true, - template: '', + template: '', encapsulation: ViewEncapsulation.None, + standalone: true, }) -export class MapMarkerClusterer implements OnInit, AfterContentInit, OnChanges, OnDestroy { - private readonly _currentMarkers = new Set(); - private readonly _eventManager = new MapEventManager(inject(NgZone)); - private readonly _destroy = new Subject(); +export class MapMarkerClusterer implements OnInit, OnChanges, OnDestroy { + private readonly _currentMarkers = new Set(); + private readonly _ngZone = inject(NgZone); + private readonly _closestMapEventManager = new MapEventManager(this._ngZone); + private _markersSubscription = Subscription.EMPTY; /** Whether the clusterer is allowed to be initialized. */ private readonly _canInitialize: boolean; + /** + * Used to customize how the marker cluster is rendered. + * See https://googlemaps.github.io/js-markerclusterer/interfaces/Renderer.html. + */ @Input() - ariaLabelFn: AriaLabelFn = () => ''; - - @Input() - set averageCenter(averageCenter: boolean) { - this._averageCenter = averageCenter; - } - private _averageCenter: boolean; - - @Input() batchSize?: number; - - @Input() - set batchSizeIE(batchSizeIE: number) { - this._batchSizeIE = batchSizeIE; - } - private _batchSizeIE: number; - - @Input() - set calculator(calculator: Calculator) { - this._calculator = calculator; - } - private _calculator: Calculator; - - @Input() - set clusterClass(clusterClass: string) { - this._clusterClass = clusterClass; - } - private _clusterClass: string; - - @Input() - set enableRetinaIcons(enableRetinaIcons: boolean) { - this._enableRetinaIcons = enableRetinaIcons; - } - private _enableRetinaIcons: boolean; - - @Input() - set gridSize(gridSize: number) { - this._gridSize = gridSize; - } - private _gridSize: number; - - @Input() - set ignoreHidden(ignoreHidden: boolean) { - this._ignoreHidden = ignoreHidden; - } - private _ignoreHidden: boolean; - - @Input() - set imageExtension(imageExtension: string) { - this._imageExtension = imageExtension; - } - private _imageExtension: string; - - @Input() - set imagePath(imagePath: string) { - this._imagePath = imagePath; - } - private _imagePath: string; - - @Input() - set imageSizes(imageSizes: number[]) { - this._imageSizes = imageSizes; - } - private _imageSizes: number[]; - - @Input() - set maxZoom(maxZoom: number) { - this._maxZoom = maxZoom; - } - private _maxZoom: number; - - @Input() - set minimumClusterSize(minimumClusterSize: number) { - this._minimumClusterSize = minimumClusterSize; - } - private _minimumClusterSize: number; - - @Input() - set styles(styles: ClusterIconStyle[]) { - this._styles = styles; - } - private _styles: ClusterIconStyle[]; - - @Input() - set title(title: string) { - this._title = title; - } - private _title: string; - - @Input() - set zIndex(zIndex: number) { - this._zIndex = zIndex; - } - private _zIndex: number; - - @Input() - set zoomOnClick(zoomOnClick: boolean) { - this._zoomOnClick = zoomOnClick; - } - private _zoomOnClick: boolean; - - @Input() - set options(options: MarkerClustererOptions) { - this._options = options; - } - private _options: MarkerClustererOptions; + renderer: Renderer; /** - * See - * googlemaps.github.io/v3-utility-library/modules/ - * _google_markerclustererplus.html#clusteringbegin + * Algorithm used to cluster the markers. + * See https://googlemaps.github.io/js-markerclusterer/interfaces/Algorithm.html. */ + @Input() + algorithm: Algorithm; + + /** Emits when clustering has started. */ @Output() readonly clusteringbegin: Observable = - this._eventManager.getLazyEmitter('clusteringbegin'); + this._closestMapEventManager.getLazyEmitter('clusteringbegin'); - /** - * See - * googlemaps.github.io/v3-utility-library/modules/_google_markerclustererplus.html#clusteringend - */ + /** Emits when clustering is done. */ @Output() readonly clusteringend: Observable = - this._eventManager.getLazyEmitter('clusteringend'); + this._closestMapEventManager.getLazyEmitter('clusteringend'); /** Emits when a cluster has been clicked. */ @Output() - readonly clusterClick: Observable = this._eventManager.getLazyEmitter('click'); + readonly clusterClick: EventEmitter = new EventEmitter(); @ContentChildren(MapMarker, {descendants: true}) _markers: QueryList; + @ContentChildren(MapAdvancedMarker, {descendants: true}) + _advancedMarkers: QueryList; - /** - * The underlying MarkerClusterer object. - * - * See - * googlemaps.github.io/v3-utility-library/classes/ - * _google_markerclustererplus.markerclusterer.html - */ - markerClusterer?: MarkerClustererInstance; - - /** Event emitted when the clusterer is initialized. */ - @Output() readonly markerClustererInitialized: EventEmitter = - new EventEmitter(); + /** Underlying MarkerClusterer object used to interact with Google Maps. */ + markerClusterer?: MarkerClusterer; - constructor( - private readonly _googleMap: GoogleMap, - private readonly _ngZone: NgZone, - ) { - this._canInitialize = _googleMap._isBrowser; + constructor(private readonly _googleMap: GoogleMap) { + this._canInitialize = this._googleMap._isBrowser; } ngOnInit() { if (this._canInitialize) { + this._init(); + // The `clusteringbegin` and `clusteringend` events are + // emitted on the map so that's why set it as the target. + this._closestMapEventManager.setTarget(this._googleMap.googleMap!); + } + } + + _init() { + if (markerClusterer?.MarkerClusterer && this._googleMap.googleMap) { + this._createCluster(this._googleMap.googleMap); + this._watchForMarkerChanges(); + } else { this._ngZone.runOutsideAngular(() => { this._googleMap._resolveMap().then(map => { - if ( - typeof MarkerClusterer !== 'function' && - (typeof ngDevMode === 'undefined' || ngDevMode) - ) { - throw Error( - 'MarkerClusterer class not found, cannot construct a marker cluster. ' + - 'Please install the MarkerClustererPlus library: ' + - 'https://github.com/googlemaps/js-markerclustererplus', - ); - } - - // Create the object outside the zone so its events don't trigger change detection. - // We'll bring it back in inside the `MapEventManager` only for the events that the - // user has subscribed to. - this.markerClusterer = this._ngZone.runOutsideAngular(() => { - return new MarkerClusterer(map, [], this._combineOptions()); - }); - - this._assertInitialized(); - this._eventManager.setTarget(this.markerClusterer); - this.markerClustererInitialized.emit(this.markerClusterer); + this._createCluster(map); + this._watchForMarkerChanges(); }); }); } } - ngAfterContentInit() { - if (this._canInitialize) { - if (this.markerClusterer) { - this._watchForMarkerChanges(); - } else { - this.markerClustererInitialized - .pipe(take(1), takeUntil(this._destroy)) - .subscribe(() => this._watchForMarkerChanges()); - } - } - } - ngOnChanges(changes: SimpleChanges) { - const { - markerClusterer: clusterer, - ariaLabelFn, - _averageCenter, - _batchSizeIE, - _calculator, - _styles, - _clusterClass, - _enableRetinaIcons, - _gridSize, - _ignoreHidden, - _imageExtension, - _imagePath, - _imageSizes, - _maxZoom, - _minimumClusterSize, - _title, - _zIndex, - _zoomOnClick, - } = this; + const change = changes['renderer'] || changes['algorithm']; - if (clusterer) { - if (changes['options']) { - clusterer.setOptions(this._combineOptions()); - } - if (changes['ariaLabelFn']) { - clusterer.ariaLabelFn = ariaLabelFn; - } - if (changes['averageCenter'] && _averageCenter !== undefined) { - clusterer.setAverageCenter(_averageCenter); - } - if (changes['batchSizeIE'] && _batchSizeIE !== undefined) { - clusterer.setBatchSizeIE(_batchSizeIE); - } - if (changes['calculator'] && !!_calculator) { - clusterer.setCalculator(_calculator); - } - if (changes['clusterClass'] && _clusterClass !== undefined) { - clusterer.setClusterClass(_clusterClass); - } - if (changes['enableRetinaIcons'] && _enableRetinaIcons !== undefined) { - clusterer.setEnableRetinaIcons(_enableRetinaIcons); - } - if (changes['gridSize'] && _gridSize !== undefined) { - clusterer.setGridSize(_gridSize); - } - if (changes['ignoreHidden'] && _ignoreHidden !== undefined) { - clusterer.setIgnoreHidden(_ignoreHidden); - } - if (changes['imageExtension'] && _imageExtension !== undefined) { - clusterer.setImageExtension(_imageExtension); - } - if (changes['imagePath'] && _imagePath !== undefined) { - clusterer.setImagePath(_imagePath); - } - if (changes['imageSizes'] && _imageSizes) { - clusterer.setImageSizes(_imageSizes); - } - if (changes['maxZoom'] && _maxZoom !== undefined) { - clusterer.setMaxZoom(_maxZoom); - } - if (changes['minimumClusterSize'] && _minimumClusterSize !== undefined) { - clusterer.setMinimumClusterSize(_minimumClusterSize); - } - if (changes['styles'] && _styles) { - clusterer.setStyles(_styles); - } - if (changes['title'] && _title !== undefined) { - clusterer.setTitle(_title); - } - if (changes['zIndex'] && _zIndex !== undefined) { - clusterer.setZIndex(_zIndex); - } - if (changes['zoomOnClick'] && _zoomOnClick !== undefined) { - clusterer.setZoomOnClick(_zoomOnClick); - } + // Since the options are set in the constructor, we have to recreate the cluster if they change. + if (this.markerClusterer && this._googleMap.googleMap && change && !change.isFirstChange()) { + this._createCluster(this._googleMap.googleMap); + this._watchForMarkerChanges(); } } ngOnDestroy() { - this._destroy.next(); - this._destroy.complete(); - this._eventManager.destroy(); - this.markerClusterer?.setMap(null); - } - - fitMapToMarkers(padding: number | google.maps.Padding) { - this._assertInitialized(); - this.markerClusterer.fitMapToMarkers(padding); + this._markersSubscription.unsubscribe(); + this._closestMapEventManager.destroy(); + this._destroyCluster(); } - getAverageCenter(): boolean { - this._assertInitialized(); - return this.markerClusterer.getAverageCenter(); - } - - getBatchSizeIE(): number { - this._assertInitialized(); - return this.markerClusterer.getBatchSizeIE(); - } - - getCalculator(): Calculator { - this._assertInitialized(); - return this.markerClusterer.getCalculator(); - } - - getClusterClass(): string { - this._assertInitialized(); - return this.markerClusterer.getClusterClass(); - } - - getClusters(): Cluster[] { - this._assertInitialized(); - return this.markerClusterer.getClusters(); - } - - getEnableRetinaIcons(): boolean { - this._assertInitialized(); - return this.markerClusterer.getEnableRetinaIcons(); - } - - getGridSize(): number { - this._assertInitialized(); - return this.markerClusterer.getGridSize(); - } - - getIgnoreHidden(): boolean { - this._assertInitialized(); - return this.markerClusterer.getIgnoreHidden(); - } - - getImageExtension(): string { - this._assertInitialized(); - return this.markerClusterer.getImageExtension(); - } - - getImagePath(): string { - this._assertInitialized(); - return this.markerClusterer.getImagePath(); - } - - getImageSizes(): number[] { - this._assertInitialized(); - return this.markerClusterer.getImageSizes(); - } - - getMaxZoom(): number { - this._assertInitialized(); - return this.markerClusterer.getMaxZoom(); - } - - getMinimumClusterSize(): number { - this._assertInitialized(); - return this.markerClusterer.getMinimumClusterSize(); - } - - getStyles(): ClusterIconStyle[] { - this._assertInitialized(); - return this.markerClusterer.getStyles(); - } - - getTitle(): string { - this._assertInitialized(); - return this.markerClusterer.getTitle(); - } - - getTotalClusters(): number { - this._assertInitialized(); - return this.markerClusterer.getTotalClusters(); - } - - getTotalMarkers(): number { - this._assertInitialized(); - return this.markerClusterer.getTotalMarkers(); - } - - getZIndex(): number { - this._assertInitialized(); - return this.markerClusterer.getZIndex(); - } - - getZoomOnClick(): boolean { + private _watchForMarkerChanges() { this._assertInitialized(); - return this.markerClusterer.getZoomOnClick(); + const initialMarkers: Marker[] = []; + const mapMarkerComponent = [ + ...this._markers.toArray(), + this._advancedMarkers.toArray(), + ] as MapMarkerComponent[]; + for (const marker of this._getInternalMarkers(mapMarkerComponent)) { + this._currentMarkers.add(marker); + initialMarkers.push(marker); + } + this.markerClusterer.addMarkers(initialMarkers); + + this._markersSubscription.unsubscribe(); + this._markersSubscription = merge( + this._markers.changes, + this._advancedMarkers.changes, + ).subscribe((markerComponents: MapMarker[] | MapAdvancedMarker[]) => { + this._assertInitialized(); + const newMarkers = new Set(this._getInternalMarkers(markerComponents)); + const markersToAdd: Marker[] = []; + const markersToRemove: Marker[] = []; + for (const marker of Array.from(newMarkers)) { + if (!this._currentMarkers.has(marker)) { + this._currentMarkers.add(marker); + markersToAdd.push(marker); + } + } + for (const marker of Array.from(this._currentMarkers)) { + if (!newMarkers.has(marker)) { + markersToRemove.push(marker); + } + } + this.markerClusterer.addMarkers(markersToAdd, true); + this.markerClusterer.removeMarkers(markersToRemove, true); + this.markerClusterer.render(); + for (const marker of markersToRemove) { + this._currentMarkers.delete(marker); + } + }); } - private _combineOptions(): MarkerClustererOptions { - const options = this._options || DEFAULT_CLUSTERER_OPTIONS; - return { - ...options, - ariaLabelFn: this.ariaLabelFn ?? options.ariaLabelFn, - averageCenter: this._averageCenter ?? options.averageCenter, - batchSize: this.batchSize ?? options.batchSize, - batchSizeIE: this._batchSizeIE ?? options.batchSizeIE, - calculator: this._calculator ?? options.calculator, - clusterClass: this._clusterClass ?? options.clusterClass, - enableRetinaIcons: this._enableRetinaIcons ?? options.enableRetinaIcons, - gridSize: this._gridSize ?? options.gridSize, - ignoreHidden: this._ignoreHidden ?? options.ignoreHidden, - imageExtension: this._imageExtension ?? options.imageExtension, - imagePath: this._imagePath ?? options.imagePath, - imageSizes: this._imageSizes ?? options.imageSizes, - maxZoom: this._maxZoom ?? options.maxZoom, - minimumClusterSize: this._minimumClusterSize ?? options.minimumClusterSize, - styles: this._styles ?? options.styles, - title: this._title ?? options.title, - zIndex: this._zIndex ?? options.zIndex, - zoomOnClick: this._zoomOnClick ?? options.zoomOnClick, - }; - } + private _createCluster(map: google.maps.Map | undefined) { + if ( + !markerClusterer?.MarkerClusterer && + (typeof ngDevMode === 'undefined' || ngDevMode) && + !map + ) { + throw Error( + 'MarkerClusterer class not found, cannot construct a marker cluster. ' + + 'Please install the MarkerClusterer library: ' + + 'https://github.com/googlemaps/js-markerclusterer', + ); + } - private _watchForMarkerChanges() { - this._assertInitialized(); + this._destroyCluster(); + // Create the object outside the zone so its events don't trigger change detection. + // We'll bring it back in inside the `MapEventManager` only for the events that the + // user has subscribed to. this._ngZone.runOutsideAngular(() => { - this._getInternalMarkers(this._markers).then(markers => { - const initialMarkers: google.maps.Marker[] = []; - for (const marker of markers) { - this._currentMarkers.add(marker); - initialMarkers.push(marker); - } - this.markerClusterer.addMarkers(initialMarkers); + this.markerClusterer = new markerClusterer.MarkerClusterer({ + map: map, + renderer: this.renderer, + algorithm: this.algorithm, + onClusterClick: (event, cluster, map) => { + if (this.clusterClick.observers.length) { + this._ngZone.run(() => this.clusterClick.emit(cluster)); + } else { + markerClusterer.defaultOnClusterClickHandler(event, cluster, map); + } + }, }); }); + } - this._markers.changes - .pipe(takeUntil(this._destroy)) - .subscribe((markerComponents: MapMarker[]) => { - this._assertInitialized(); - this._ngZone.runOutsideAngular(() => { - this._getInternalMarkers(markerComponents).then(markers => { - const newMarkers = new Set(markers); - const markersToAdd: google.maps.Marker[] = []; - const markersToRemove: google.maps.Marker[] = []; - for (const marker of Array.from(newMarkers)) { - if (!this._currentMarkers.has(marker)) { - this._currentMarkers.add(marker); - markersToAdd.push(marker); - } - } - for (const marker of Array.from(this._currentMarkers)) { - if (!newMarkers.has(marker)) { - markersToRemove.push(marker); - } - } - this.markerClusterer.addMarkers(markersToAdd, true); - this.markerClusterer.removeMarkers(markersToRemove, true); - this.markerClusterer.repaint(); - for (const marker of markersToRemove) { - this._currentMarkers.delete(marker); - } - }); - }); - }); + private _destroyCluster() { + // TODO(crisbeto): the naming here seems odd, but the `MarkerCluster` method isn't + // exposed. All this method seems to do at the time of writing is to call into `reset`. + // See: https://github.com/googlemaps/js-markerclusterer/blob/main/src/markerclusterer.ts#L205 + this.markerClusterer?.onRemove(); + this.markerClusterer = undefined; } - private _getInternalMarkers( - markers: MapMarker[] | QueryList, - ): Promise { - return Promise.all(markers.map(markerComponent => markerComponent._resolveMarker())); + private _getInternalMarkers(markers: MapMarkerComponent[]): Marker[] { + return markers + .filter(markerComponent => { + if (markerComponent instanceof MapMarker) { + return markerComponent.marker!; + } + return markerComponent.advancedMarker!; + }) + .map(markerComponent => { + if (markerComponent instanceof MapMarker) { + return markerComponent.marker!; + } + return markerComponent.advancedMarker!; + }); } - private _assertInitialized(): asserts this is {markerClusterer: MarkerClustererInstance} { + private _assertInitialized(): asserts this is {markerClusterer: MarkerClusterer} { if (typeof ngDevMode === 'undefined' || ngDevMode) { + if (!this._googleMap.googleMap) { + throw Error( + 'Cannot access Google Map information before the API has been initialized. ' + + 'Please wait for the API to load before trying to interact with it.', + ); + } if (!this.markerClusterer) { throw Error( 'Cannot interact with a MarkerClusterer before it has been initialized. ' + diff --git a/src/google-maps/map-marker-clusterer/marker-clusterer-types.ts b/src/google-maps/map-marker-clusterer/marker-clusterer-types.ts index 8c5f99d34c33..3295a9deaeac 100644 --- a/src/google-maps/map-marker-clusterer/marker-clusterer-types.ts +++ b/src/google-maps/map-marker-clusterer/marker-clusterer-types.ts @@ -8,177 +8,155 @@ /// +// tslint:disable + +declare type onClusterClickHandler = ( + event: google.maps.MapMouseEvent, + cluster: Cluster, + map: google.maps.Map, +) => void; + +type Marker = google.maps.Marker | google.maps.marker.AdvancedMarkerElement; + /** - * Class for clustering markers on a Google Map. - * - * See - * googlemaps.github.io/v3-utility-library/classes/_google_markerclustererplus.markerclusterer.html + * MarkerClusterer creates and manages per-zoom-level clusters for large amounts + * of markers. See {@link MarkerClustererOptions} for more details. */ -export declare class MarkerClusterer { - constructor( - map: google.maps.Map, - markers?: google.maps.Marker[], - options?: MarkerClustererOptions, - ); - ariaLabelFn: AriaLabelFn; - static BATCH_SIZE: number; - static BATCH_SIZE_IE: number; - static IMAGE_EXTENSION: string; - static IMAGE_PATH: string; - static IMAGE_SIZES: number[]; - addListener(eventName: string, handler: Function): google.maps.MapsEventListener; - addMarker(marker: MarkerClusterer, nodraw: boolean): void; - addMarkers(markers: google.maps.Marker[], nodraw?: boolean): void; - bindTo(key: string, target: google.maps.MVCObject, targetKey: string, noNotify: boolean): void; - changed(key: string): void; - clearMarkers(): void; - fitMapToMarkers(padding: number | google.maps.Padding): void; - get(key: string): any; - getAverageCenter(): boolean; - getBatchSizeIE(): number; - getCalculator(): Calculator; - getClusterClass(): string; - getClusters(): Cluster[]; - getEnableRetinaIcons(): boolean; - getGridSize(): number; - getIgnoreHidden(): boolean; - getImageExtension(): string; - getImagePath(): string; - getImageSizes(): number[]; - getMap(): google.maps.Map | google.maps.StreetViewPanorama; - getMarkers(): google.maps.Marker[]; - getMaxZoom(): number; - getMinimumClusterSize(): number; - getPanes(): google.maps.MapPanes; - getProjection(): google.maps.MapCanvasProjection; - getStyles(): ClusterIconStyle[]; - getTitle(): string; - getTotalClusters(): number; - getTotalMarkers(): number; - getZIndex(): number; - getZoomOnClick(): boolean; - notify(key: string): void; - removeMarker(marker: google.maps.Marker, nodraw: boolean): boolean; - removeMarkers(markers: google.maps.Marker[], nodraw?: boolean): boolean; - repaint(): void; - set(key: string, value: any): void; - setAverageCenter(averageCenter: boolean): void; - setBatchSizeIE(batchSizeIE: number): void; - setCalculator(calculator: Calculator): void; - setClusterClass(clusterClass: string): void; - setEnableRetinaIcons(enableRetinaIcons: boolean): void; - setGridSize(gridSize: number): void; - setIgnoreHidden(ignoreHidden: boolean): void; - setImageExtension(imageExtension: string): void; - setImagePath(imagePath: string): void; - setImageSizes(imageSizes: number[]): void; - setMap(map: google.maps.Map | null): void; - setMaxZoom(maxZoom: number): void; - setMinimumClusterSize(minimumClusterSize: number): void; - setStyles(styles: ClusterIconStyle[]): void; - setTitle(title: string): void; - setValues(values: any): void; - setZIndex(zIndex: number): void; - setZoomOnClick(zoomOnClick: boolean): void; - // Note: This one doesn't appear in the docs page, but it exists at runtime. - setOptions(options: MarkerClustererOptions): void; - unbind(key: string): void; - unbindAll(): void; - static CALCULATOR(markers: google.maps.Marker[], numStyles: number): ClusterIconInfo; - static withDefaultStyle(overrides: ClusterIconStyle): ClusterIconStyle; +declare class MarkerClusterer { + /** @see {@link MarkerClustererOptions.onClusterClick} */ + onClusterClick: onClusterClickHandler; + constructor({map, markers, algorithm, renderer, onClusterClick}: MarkerClustererOptions); + addMarker(marker: Marker, noDraw?: boolean): void; + addMarkers(markers: Marker[], noDraw?: boolean): void; + removeMarker(marker: Marker, noDraw?: boolean): boolean; + removeMarkers(markers: Marker[], noDraw?: boolean): boolean; + clearMarkers(noDraw?: boolean): void; + /** + * Recalculates and draws all the marker clusters. + */ + render(): void; + onAdd(): void; + onRemove(): void; } -/** - * Cluster class from the @google/markerclustererplus library. - * - * See googlemaps.github.io/v3-utility-library/classes/_google_markerclustererplus.cluster.html - */ -export declare class Cluster { - constructor(markerClusterer: MarkerClusterer); - getCenter(): google.maps.LatLng; - getMarkers(): google.maps.Marker[]; - getSize(): number; - updateIcon(): void; +interface MarkerClustererOptions { + markers?: Marker[]; + /** + * An algorithm to cluster markers. Default is {@link SuperClusterAlgorithm}. Must + * provide a `calculate` method accepting {@link AlgorithmInput} and returning + * an array of {@link Cluster}. + */ + algorithm?: Algorithm; + map?: google.maps.Map | null; + /** + * An object that converts a {@link Cluster} into a `Marker`. + * Default is {@link DefaultRenderer}. + */ + renderer?: Renderer; + onClusterClick?: onClusterClickHandler; } -/** - * Options for constructing a MarkerClusterer from the @google/markerclustererplus library. - * - * See - * googlemaps.github.io/v3-utility-library/classes/ - * _google_markerclustererplus.markerclustereroptions.html - */ -export declare interface MarkerClustererOptions { - ariaLabelFn?: AriaLabelFn; - averageCenter?: boolean; - batchSize?: number; - batchSizeIE?: number; - calculator?: Calculator; - clusterClass?: string; - enableRetinaIcons?: boolean; - gridSize?: number; - ignoreHidden?: boolean; - imageExtension?: string; - imagePath?: string; - imageSizes?: number[]; - maxZoom?: number; - minimumClusterSize?: number; - styles?: ClusterIconStyle[]; - title?: string; - zIndex?: number; - zoomOnClick?: boolean; +declare enum MarkerClustererEvents { + CLUSTERING_BEGIN = 'clusteringbegin', + CLUSTERING_END = 'clusteringend', + CLUSTER_CLICK = 'click', } -/** - * Style interface for a marker cluster icon. - * - * See - * googlemaps.github.io/v3-utility-library/interfaces/ - * _google_markerclustererplus.clustericonstyle.html - */ -export declare interface ClusterIconStyle { - anchorIcon?: [number, number]; - anchorText?: [number, number]; - backgroundPosition?: string; - className?: string; - fontFamily?: string; - fontStyle?: string; - fontWeight?: string; - height: number; - textColor?: string; - textDecoration?: string; - textLineHeight?: number; - textSize?: number; - url?: string; - width: number; +interface ClusterOptions { + position?: google.maps.LatLng | google.maps.LatLngLiteral; + markers?: Marker[]; } -/** - * Info interface for a marker cluster icon. - * - * See - * googlemaps.github.io/v3-utility-library/interfaces/ - * _google_markerclustererplus.clustericoninfo.html - */ -export declare interface ClusterIconInfo { - index: number; - text: string; - title: string; +declare class Cluster { + marker: Marker; + readonly markers?: Marker[]; + constructor({markers, position}: ClusterOptions); + get bounds(): google.maps.LatLngBounds | undefined; + get position(): google.maps.LatLng; + /** + * Get the count of **visible** markers. + */ + get count(): number; + /** + * Add a marker to the cluster. + */ + push(marker: Marker): void; + /** + * Cleanup references and remove marker from map. + */ + delete(): void; } -/** - * Function type alias for determining the aria label on a Google Maps marker cluster. - * - * See googlemaps.github.io/v3-utility-library/modules/_google_markerclustererplus.html#arialabelfn - */ -export declare type AriaLabelFn = (text: string) => string; +declare class ClusterStats { + readonly markers: { + sum: number; + }; + readonly clusters: { + count: number; + markers: { + mean: number; + sum: number; + min: number; + max: number; + }; + }; + constructor(markers: Marker[], clusters: Cluster[]); +} -/** - * Function type alias for calculating how a marker cluster is displayed. - * - * See googlemaps.github.io/v3-utility-library/modules/_google_markerclustererplus.html#calculator - */ -export declare type Calculator = ( - markers: google.maps.Marker[], - clusterIconStylesCount: number, -) => ClusterIconInfo; +interface Renderer { + /** + * Turn a {@link Cluster} into a `Marker`. + * + * Below is a simple example to create a marker with the number of markers in the cluster as a label. + * + * ```typescript + * return new Marker({ + * position, + * label: String(markers.length), + * }); + * ``` + */ + render(cluster: Cluster, stats: ClusterStats): Marker; +} + +declare class DefaultRenderer implements Renderer { + /** + * The default render function for the library used by {@link MarkerClusterer}. + * + * Currently set to use the following: + * + * ```typescript + * // change color if this cluster has more markers than the mean cluster + * const color = + * count > Math.max(10, stats.clusters.markers.mean) + * ? "#ff0000" + * : "#0000ff"; + * + * // create svg url with fill color + * const svg = window.btoa(` + * + * + * + * + * + * `); + * + * // create marker using svg icon + * return new Marker({ + * position, + * icon: { + * url: `data:image/svg+xml;base64,${svg}`, + * scaledSize: new google.maps.Size(45, 45), + * }, + * label: { + * text: String(count), + * color: "rgba(255,255,255,0.9)", + * fontSize: "12px", + * }, + * // adjust zIndex to be above other markers + * zIndex: 1000 + count, + * }); + * ``` + */ + render({count, position}: Cluster, stats: ClusterStats): Marker; +} diff --git a/src/google-maps/public-api.ts b/src/google-maps/public-api.ts index ed4a5a3251b3..9c7fb7b9dff0 100644 --- a/src/google-maps/public-api.ts +++ b/src/google-maps/public-api.ts @@ -30,10 +30,4 @@ export {MapTrafficLayer} from './map-traffic-layer/map-traffic-layer'; export {MapTransitLayer} from './map-transit-layer/map-transit-layer'; export {MapHeatmapLayer, HeatmapData} from './map-heatmap-layer/map-heatmap-layer'; export {MapGeocoder, MapGeocoderResponse} from './map-geocoder/map-geocoder'; -export { - MarkerClustererOptions, - ClusterIconStyle, - AriaLabelFn, - Calculator, -} from './map-marker-clusterer/marker-clusterer-types'; export {MapEventManager} from './map-event-manager';