@react-google-maps/marker-clusterer 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -0
- package/dist/cjs.js +942 -0
- package/dist/cjs.js.map +1 -0
- package/dist/cjs.min.js +2 -0
- package/dist/cjs.min.js.map +1 -0
- package/dist/esm.js +936 -0
- package/dist/esm.js.map +1 -0
- package/dist/esm.min.js +2 -0
- package/dist/esm.min.js.map +1 -0
- package/dist/index.d.ts +218 -0
- package/dist/umd.js +948 -0
- package/dist/umd.js.map +1 -0
- package/dist/umd.min.js +2 -0
- package/dist/umd.min.js.map +1 -0
- package/package.json +98 -0
- package/src/Cluster.tsx +208 -0
- package/src/ClusterIcon.tsx +371 -0
- package/src/Clusterer.tsx +727 -0
- package/src/__tests__/clusterer.test.ts +84 -0
- package/src/index.ts +51 -0
- package/src/setup-tests.js +356 -0
- package/src/types.tsx +47 -0
|
@@ -0,0 +1,727 @@
|
|
|
1
|
+
/* global google */
|
|
2
|
+
/* eslint-disable filenames/match-regex */
|
|
3
|
+
import { Cluster } from './Cluster'
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
MarkerExtended,
|
|
7
|
+
ClustererOptions,
|
|
8
|
+
ClusterIconStyle,
|
|
9
|
+
TCalculator,
|
|
10
|
+
ClusterIconInfo,
|
|
11
|
+
} from './types'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Supports up to 9007199254740991 (Number.MAX_SAFE_INTEGER) markers
|
|
15
|
+
* which is not a problem as max array length is 4294967296 (2**32)
|
|
16
|
+
*/
|
|
17
|
+
const CALCULATOR = function CALCULATOR(
|
|
18
|
+
markers: MarkerExtended[],
|
|
19
|
+
numStyles: number
|
|
20
|
+
): ClusterIconInfo {
|
|
21
|
+
const count = markers.length
|
|
22
|
+
|
|
23
|
+
const numberOfDigits = count.toString().length
|
|
24
|
+
|
|
25
|
+
const index = Math.min(numberOfDigits, numStyles)
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
text: count.toString(),
|
|
29
|
+
index: index,
|
|
30
|
+
title: '',
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const BATCH_SIZE = 2000
|
|
35
|
+
|
|
36
|
+
const BATCH_SIZE_IE = 500
|
|
37
|
+
|
|
38
|
+
const IMAGE_PATH =
|
|
39
|
+
'https://developers.google.com/maps/documentation/javascript/examples/markerclusterer/m'
|
|
40
|
+
|
|
41
|
+
const IMAGE_EXTENSION = 'png'
|
|
42
|
+
|
|
43
|
+
const IMAGE_SIZES = [53, 56, 66, 78, 90]
|
|
44
|
+
|
|
45
|
+
const CLUSTERER_CLASS = 'cluster'
|
|
46
|
+
|
|
47
|
+
export class Clusterer {
|
|
48
|
+
markers: MarkerExtended[]
|
|
49
|
+
clusters: Cluster[]
|
|
50
|
+
listeners: google.maps.MapsEventListener[]
|
|
51
|
+
activeMap: google.maps.Map | google.maps.StreetViewPanorama | null
|
|
52
|
+
ready: boolean
|
|
53
|
+
gridSize: number
|
|
54
|
+
minClusterSize: number
|
|
55
|
+
maxZoom: number | null
|
|
56
|
+
styles: ClusterIconStyle[]
|
|
57
|
+
title: string
|
|
58
|
+
zoomOnClick: boolean
|
|
59
|
+
averageCenter: boolean
|
|
60
|
+
ignoreHidden: boolean
|
|
61
|
+
enableRetinaIcons: boolean
|
|
62
|
+
imagePath: string
|
|
63
|
+
imageExtension: string
|
|
64
|
+
imageSizes: number[]
|
|
65
|
+
calculator: TCalculator
|
|
66
|
+
batchSize: number
|
|
67
|
+
batchSizeIE: number
|
|
68
|
+
clusterClass: string
|
|
69
|
+
timerRefStatic: number | null
|
|
70
|
+
|
|
71
|
+
constructor(
|
|
72
|
+
map: google.maps.Map,
|
|
73
|
+
optMarkers: MarkerExtended[] = [],
|
|
74
|
+
optOptions: ClustererOptions = {}
|
|
75
|
+
) {
|
|
76
|
+
this.extend(Clusterer, google.maps.OverlayView)
|
|
77
|
+
|
|
78
|
+
this.markers = []
|
|
79
|
+
this.clusters = []
|
|
80
|
+
this.listeners = []
|
|
81
|
+
this.activeMap = null
|
|
82
|
+
this.ready = false
|
|
83
|
+
this.gridSize = optOptions.gridSize || 60
|
|
84
|
+
this.minClusterSize = optOptions.minimumClusterSize || 2
|
|
85
|
+
this.maxZoom = optOptions.maxZoom || null
|
|
86
|
+
this.styles = optOptions.styles || []
|
|
87
|
+
|
|
88
|
+
this.title = optOptions.title || ''
|
|
89
|
+
|
|
90
|
+
this.zoomOnClick = true
|
|
91
|
+
|
|
92
|
+
if (optOptions.zoomOnClick !== undefined) {
|
|
93
|
+
this.zoomOnClick = optOptions.zoomOnClick
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
this.averageCenter = false
|
|
97
|
+
|
|
98
|
+
if (optOptions.averageCenter !== undefined) {
|
|
99
|
+
this.averageCenter = optOptions.averageCenter
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
this.ignoreHidden = false
|
|
103
|
+
|
|
104
|
+
if (optOptions.ignoreHidden !== undefined) {
|
|
105
|
+
this.ignoreHidden = optOptions.ignoreHidden
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
this.enableRetinaIcons = false
|
|
109
|
+
|
|
110
|
+
if (optOptions.enableRetinaIcons !== undefined) {
|
|
111
|
+
this.enableRetinaIcons = optOptions.enableRetinaIcons
|
|
112
|
+
}
|
|
113
|
+
this.imagePath = optOptions.imagePath || IMAGE_PATH
|
|
114
|
+
|
|
115
|
+
this.imageExtension = optOptions.imageExtension || IMAGE_EXTENSION
|
|
116
|
+
|
|
117
|
+
this.imageSizes = optOptions.imageSizes || IMAGE_SIZES
|
|
118
|
+
|
|
119
|
+
this.calculator = optOptions.calculator || CALCULATOR
|
|
120
|
+
|
|
121
|
+
this.batchSize = optOptions.batchSize || BATCH_SIZE
|
|
122
|
+
|
|
123
|
+
this.batchSizeIE = optOptions.batchSizeIE || BATCH_SIZE_IE
|
|
124
|
+
|
|
125
|
+
this.clusterClass = optOptions.clusterClass || CLUSTERER_CLASS
|
|
126
|
+
|
|
127
|
+
if (navigator.userAgent.toLowerCase().indexOf('msie') !== -1) {
|
|
128
|
+
// Try to avoid IE timeout when processing a huge number of markers:
|
|
129
|
+
this.batchSize = this.batchSizeIE
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
this.timerRefStatic = null
|
|
133
|
+
|
|
134
|
+
this.setupStyles()
|
|
135
|
+
|
|
136
|
+
this.addMarkers(optMarkers, true)
|
|
137
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-ignore
|
|
138
|
+
// @ts-ignore
|
|
139
|
+
this.setMap(map) // Note: this causes onAdd to be called
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
onAdd() {
|
|
143
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-ignore
|
|
144
|
+
// @ts-ignore
|
|
145
|
+
this.activeMap = this.getMap()
|
|
146
|
+
|
|
147
|
+
this.ready = true
|
|
148
|
+
|
|
149
|
+
this.repaint()
|
|
150
|
+
|
|
151
|
+
// Add the map event listeners
|
|
152
|
+
this.listeners = [
|
|
153
|
+
google.maps.event.addListener(
|
|
154
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-ignore
|
|
155
|
+
// @ts-ignore
|
|
156
|
+
this.getMap(),
|
|
157
|
+
'zoom_changed',
|
|
158
|
+
// eslint-disable-next-line @getify/proper-arrows/this, @getify/proper-arrows/name
|
|
159
|
+
() => {
|
|
160
|
+
this.resetViewport(false)
|
|
161
|
+
// Workaround for this Google bug: when map is at level 0 and "-" of
|
|
162
|
+
// zoom slider is clicked, a "zoom_changed" event is fired even though
|
|
163
|
+
// the map doesn't zoom out any further. In this situation, no "idle"
|
|
164
|
+
// event is triggered so the cluster markers that have been removed
|
|
165
|
+
// do not get redrawn. Same goes for a zoom in at maxZoom.
|
|
166
|
+
if (
|
|
167
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-ignore
|
|
168
|
+
// @ts-ignore
|
|
169
|
+
this.getMap().getZoom() === (this.get('minZoom') || 0) ||
|
|
170
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-ignore
|
|
171
|
+
// @ts-ignore
|
|
172
|
+
this.getMap().getZoom() === this.get('maxZoom')
|
|
173
|
+
) {
|
|
174
|
+
google.maps.event.trigger(this, 'idle')
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
),
|
|
178
|
+
google.maps.event.addListener(
|
|
179
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-ignore
|
|
180
|
+
// @ts-ignore
|
|
181
|
+
this.getMap(),
|
|
182
|
+
'idle',
|
|
183
|
+
// eslint-disable-next-line @getify/proper-arrows/this, @getify/proper-arrows/name
|
|
184
|
+
() => {
|
|
185
|
+
this.redraw()
|
|
186
|
+
}
|
|
187
|
+
),
|
|
188
|
+
]
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// eslint-disable-next-line @getify/proper-arrows/this
|
|
192
|
+
onRemove() {
|
|
193
|
+
// Put all the managed markers back on the map:
|
|
194
|
+
for (let i = 0; i < this.markers.length; i++) {
|
|
195
|
+
if (this.markers[i].getMap() !== this.activeMap) {
|
|
196
|
+
this.markers[i].setMap(this.activeMap)
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Remove all clusters:
|
|
201
|
+
for (let i = 0; i < this.clusters.length; i++) {
|
|
202
|
+
this.clusters[i].remove()
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
this.clusters = []
|
|
206
|
+
|
|
207
|
+
// Remove map event listeners:
|
|
208
|
+
for (let i = 0; i < this.listeners.length; i++) {
|
|
209
|
+
google.maps.event.removeListener(this.listeners[i])
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
this.listeners = []
|
|
213
|
+
|
|
214
|
+
this.activeMap = null
|
|
215
|
+
|
|
216
|
+
this.ready = false
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
220
|
+
draw() {}
|
|
221
|
+
|
|
222
|
+
setupStyles() {
|
|
223
|
+
if (this.styles.length > 0) {
|
|
224
|
+
return
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
for (let i = 0; i < this.imageSizes.length; i++) {
|
|
228
|
+
this.styles.push({
|
|
229
|
+
url: this.imagePath + (i + 1) + '.' + this.imageExtension,
|
|
230
|
+
height: this.imageSizes[i],
|
|
231
|
+
width: this.imageSizes[i],
|
|
232
|
+
})
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
fitMapToMarkers() {
|
|
237
|
+
const markers = this.getMarkers()
|
|
238
|
+
|
|
239
|
+
const bounds = new google.maps.LatLngBounds()
|
|
240
|
+
|
|
241
|
+
for (let i = 0; i < markers.length; i++) {
|
|
242
|
+
const position = markers[i].getPosition()
|
|
243
|
+
if (position) {
|
|
244
|
+
bounds.extend(position)
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-ignore
|
|
249
|
+
// @ts-ignore
|
|
250
|
+
this.getMap().fitBounds(bounds)
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
getGridSize(): number {
|
|
254
|
+
return this.gridSize
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
setGridSize(gridSize: number) {
|
|
258
|
+
this.gridSize = gridSize
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
getMinimumClusterSize(): number {
|
|
262
|
+
return this.minClusterSize
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
setMinimumClusterSize(minimumClusterSize: number) {
|
|
266
|
+
this.minClusterSize = minimumClusterSize
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
getMaxZoom(): number | null {
|
|
270
|
+
return this.maxZoom
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
setMaxZoom(maxZoom: number) {
|
|
274
|
+
this.maxZoom = maxZoom
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
getStyles(): ClusterIconStyle[] {
|
|
278
|
+
return this.styles
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
setStyles(styles: ClusterIconStyle[]) {
|
|
282
|
+
this.styles = styles
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
getTitle(): string {
|
|
286
|
+
return this.title
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
setTitle(title: string) {
|
|
290
|
+
this.title = title
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
getZoomOnClick(): boolean {
|
|
294
|
+
return this.zoomOnClick
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
setZoomOnClick(zoomOnClick: boolean) {
|
|
298
|
+
this.zoomOnClick = zoomOnClick
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
getAverageCenter(): boolean {
|
|
302
|
+
return this.averageCenter
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
setAverageCenter(averageCenter: boolean) {
|
|
306
|
+
this.averageCenter = averageCenter
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
getIgnoreHidden(): boolean {
|
|
310
|
+
return this.ignoreHidden
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
setIgnoreHidden(ignoreHidden: boolean) {
|
|
314
|
+
this.ignoreHidden = ignoreHidden
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
getEnableRetinaIcons(): boolean {
|
|
318
|
+
return this.enableRetinaIcons
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
setEnableRetinaIcons(enableRetinaIcons: boolean) {
|
|
322
|
+
this.enableRetinaIcons = enableRetinaIcons
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
getImageExtension(): string {
|
|
326
|
+
return this.imageExtension
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
setImageExtension(imageExtension: string) {
|
|
330
|
+
this.imageExtension = imageExtension
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
getImagePath(): string {
|
|
334
|
+
return this.imagePath
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
setImagePath(imagePath: string) {
|
|
338
|
+
this.imagePath = imagePath
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
getImageSizes(): number[] {
|
|
342
|
+
return this.imageSizes
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
setImageSizes(imageSizes: number[]) {
|
|
346
|
+
this.imageSizes = imageSizes
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
getCalculator(): TCalculator {
|
|
350
|
+
return this.calculator
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
setCalculator(calculator: TCalculator) {
|
|
354
|
+
this.calculator = calculator
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
getBatchSizeIE(): number {
|
|
358
|
+
return this.batchSizeIE
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
setBatchSizeIE(batchSizeIE: number) {
|
|
362
|
+
this.batchSizeIE = batchSizeIE
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
getClusterClass(): string {
|
|
366
|
+
return this.clusterClass
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
setClusterClass(clusterClass: string) {
|
|
370
|
+
this.clusterClass = clusterClass
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
getMarkers(): MarkerExtended[] {
|
|
374
|
+
return this.markers
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
getTotalMarkers(): number {
|
|
378
|
+
return this.markers.length
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
getClusters(): Cluster[] {
|
|
382
|
+
return this.clusters
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
getTotalClusters(): number {
|
|
386
|
+
return this.clusters.length
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
addMarker(marker: MarkerExtended, optNoDraw: boolean) {
|
|
390
|
+
this.pushMarkerTo(marker)
|
|
391
|
+
|
|
392
|
+
if (!optNoDraw) {
|
|
393
|
+
this.redraw()
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
addMarkers(markers: MarkerExtended[], optNoDraw: boolean) {
|
|
398
|
+
for (const key in markers) {
|
|
399
|
+
if (markers.hasOwnProperty(key)) {
|
|
400
|
+
this.pushMarkerTo(markers[key])
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
if (!optNoDraw) {
|
|
405
|
+
this.redraw()
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
pushMarkerTo(marker: MarkerExtended) {
|
|
410
|
+
// If the marker is draggable add a listener so we can update the clusters on the dragend:
|
|
411
|
+
if (marker.getDraggable()) {
|
|
412
|
+
// eslint-disable-next-line @getify/proper-arrows/name, @getify/proper-arrows/this
|
|
413
|
+
google.maps.event.addListener(marker, 'dragend', () => {
|
|
414
|
+
if (this.ready) {
|
|
415
|
+
marker.isAdded = false
|
|
416
|
+
|
|
417
|
+
this.repaint()
|
|
418
|
+
}
|
|
419
|
+
})
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
marker.isAdded = false
|
|
423
|
+
|
|
424
|
+
this.markers.push(marker)
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
removeMarker_(marker: MarkerExtended): boolean {
|
|
428
|
+
let index = -1
|
|
429
|
+
|
|
430
|
+
if (this.markers.indexOf) {
|
|
431
|
+
index = this.markers.indexOf(marker)
|
|
432
|
+
} else {
|
|
433
|
+
for (let i = 0; i < this.markers.length; i++) {
|
|
434
|
+
if (marker === this.markers[i]) {
|
|
435
|
+
index = i
|
|
436
|
+
|
|
437
|
+
break
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
if (index === -1) {
|
|
443
|
+
// Marker is not in our list of markers, so do nothing:
|
|
444
|
+
return false
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
marker.setMap(null)
|
|
448
|
+
|
|
449
|
+
this.markers.splice(index, 1) // Remove the marker from the list of managed markers
|
|
450
|
+
|
|
451
|
+
return true
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
removeMarker(marker: MarkerExtended, optNoDraw: boolean): boolean {
|
|
455
|
+
const removed = this.removeMarker_(marker)
|
|
456
|
+
|
|
457
|
+
if (!optNoDraw && removed) {
|
|
458
|
+
this.repaint()
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
return removed
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
removeMarkers(markers: MarkerExtended[], optNoDraw: boolean): boolean {
|
|
465
|
+
let removed = false
|
|
466
|
+
|
|
467
|
+
for (let i = 0; i < markers.length; i++) {
|
|
468
|
+
removed = removed || this.removeMarker_(markers[i])
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
if (!optNoDraw && removed) {
|
|
472
|
+
this.repaint()
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
return removed
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
clearMarkers() {
|
|
479
|
+
this.resetViewport(true)
|
|
480
|
+
|
|
481
|
+
this.markers = []
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
repaint() {
|
|
485
|
+
const oldClusters = this.clusters.slice()
|
|
486
|
+
|
|
487
|
+
this.clusters = []
|
|
488
|
+
|
|
489
|
+
this.resetViewport(false)
|
|
490
|
+
|
|
491
|
+
this.redraw()
|
|
492
|
+
|
|
493
|
+
// Remove the old clusters.
|
|
494
|
+
// Do it in a timeout to prevent blinking effect.
|
|
495
|
+
setTimeout(function timeout() {
|
|
496
|
+
for (let i = 0; i < oldClusters.length; i++) {
|
|
497
|
+
oldClusters[i].remove()
|
|
498
|
+
}
|
|
499
|
+
}, 0)
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
getExtendedBounds(bounds: google.maps.LatLngBounds): google.maps.LatLngBounds {
|
|
503
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-ignore
|
|
504
|
+
// @ts-ignore
|
|
505
|
+
const projection = this.getProjection()
|
|
506
|
+
// Convert the points to pixels and the extend out by the grid size.
|
|
507
|
+
const trPix = projection.fromLatLngToDivPixel(
|
|
508
|
+
// Turn the bounds into latlng.
|
|
509
|
+
new google.maps.LatLng(bounds.getNorthEast().lat(), bounds.getNorthEast().lng())
|
|
510
|
+
)
|
|
511
|
+
|
|
512
|
+
trPix.x += this.gridSize
|
|
513
|
+
trPix.y -= this.gridSize
|
|
514
|
+
|
|
515
|
+
const blPix = projection.fromLatLngToDivPixel(
|
|
516
|
+
// Turn the bounds into latlng.
|
|
517
|
+
new google.maps.LatLng(bounds.getSouthWest().lat(), bounds.getSouthWest().lng())
|
|
518
|
+
)
|
|
519
|
+
|
|
520
|
+
blPix.x -= this.gridSize
|
|
521
|
+
blPix.y += this.gridSize
|
|
522
|
+
|
|
523
|
+
// Extend the bounds to contain the new bounds.
|
|
524
|
+
bounds.extend(
|
|
525
|
+
// Convert the pixel points back to LatLng nw
|
|
526
|
+
projection.fromDivPixelToLatLng(trPix)
|
|
527
|
+
)
|
|
528
|
+
|
|
529
|
+
bounds.extend(
|
|
530
|
+
// Convert the pixel points back to LatLng sw
|
|
531
|
+
projection.fromDivPixelToLatLng(blPix)
|
|
532
|
+
)
|
|
533
|
+
|
|
534
|
+
return bounds
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
redraw() {
|
|
538
|
+
// Redraws all the clusters.
|
|
539
|
+
this.createClusters(0)
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
resetViewport(optHide: boolean) {
|
|
543
|
+
// Remove all the clusters
|
|
544
|
+
for (let i = 0; i < this.clusters.length; i++) {
|
|
545
|
+
this.clusters[i].remove()
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
this.clusters = []
|
|
549
|
+
|
|
550
|
+
// Reset the markers to not be added and to be removed from the map.
|
|
551
|
+
for (let i = 0; i < this.markers.length; i++) {
|
|
552
|
+
const marker = this.markers[i]
|
|
553
|
+
|
|
554
|
+
marker.isAdded = false
|
|
555
|
+
|
|
556
|
+
if (optHide) {
|
|
557
|
+
marker.setMap(null)
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
distanceBetweenPoints(p1: google.maps.LatLng, p2: google.maps.LatLng): number {
|
|
563
|
+
const R = 6371 // Radius of the Earth in km
|
|
564
|
+
|
|
565
|
+
const dLat = ((p2.lat() - p1.lat()) * Math.PI) / 180
|
|
566
|
+
const dLon = ((p2.lng() - p1.lng()) * Math.PI) / 180
|
|
567
|
+
|
|
568
|
+
const a =
|
|
569
|
+
Math.sin(dLat / 2) * Math.sin(dLat / 2) +
|
|
570
|
+
Math.cos((p1.lat() * Math.PI) / 180) *
|
|
571
|
+
Math.cos((p2.lat() * Math.PI) / 180) *
|
|
572
|
+
Math.sin(dLon / 2) *
|
|
573
|
+
Math.sin(dLon / 2)
|
|
574
|
+
|
|
575
|
+
return R * (2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)))
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
isMarkerInBounds(marker: MarkerExtended, bounds: google.maps.LatLngBounds): boolean {
|
|
579
|
+
const position = marker.getPosition()
|
|
580
|
+
|
|
581
|
+
if (position) {
|
|
582
|
+
return bounds.contains(position)
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
return false
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
addToClosestCluster(marker: MarkerExtended) {
|
|
589
|
+
let cluster
|
|
590
|
+
|
|
591
|
+
let distance = 40000 // Some large number
|
|
592
|
+
|
|
593
|
+
let clusterToAddTo = null
|
|
594
|
+
|
|
595
|
+
for (let i = 0; i < this.clusters.length; i++) {
|
|
596
|
+
cluster = this.clusters[i]
|
|
597
|
+
|
|
598
|
+
const center = cluster.getCenter()
|
|
599
|
+
|
|
600
|
+
const position = marker.getPosition()
|
|
601
|
+
|
|
602
|
+
if (center && position) {
|
|
603
|
+
const d = this.distanceBetweenPoints(center, position)
|
|
604
|
+
|
|
605
|
+
if (d < distance) {
|
|
606
|
+
distance = d
|
|
607
|
+
|
|
608
|
+
clusterToAddTo = cluster
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
if (clusterToAddTo && clusterToAddTo.isMarkerInClusterBounds(marker)) {
|
|
614
|
+
clusterToAddTo.addMarker(marker)
|
|
615
|
+
} else {
|
|
616
|
+
cluster = new Cluster(this)
|
|
617
|
+
|
|
618
|
+
cluster.addMarker(marker)
|
|
619
|
+
|
|
620
|
+
this.clusters.push(cluster)
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
createClusters(iFirst: number) {
|
|
625
|
+
if (!this.ready) {
|
|
626
|
+
return
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// Cancel previous batch processing if we're working on the first batch:
|
|
630
|
+
if (iFirst === 0) {
|
|
631
|
+
/**
|
|
632
|
+
* This event is fired when the <code>Clusterer</code> begins
|
|
633
|
+
* clustering markers.
|
|
634
|
+
* @name Clusterer#clusteringbegin
|
|
635
|
+
* @param {Clusterer} mc The Clusterer whose markers are being clustered.
|
|
636
|
+
* @event
|
|
637
|
+
*/
|
|
638
|
+
google.maps.event.trigger(this, 'clusteringbegin', this)
|
|
639
|
+
|
|
640
|
+
if (this.timerRefStatic !== null) {
|
|
641
|
+
window.clearTimeout(this.timerRefStatic)
|
|
642
|
+
|
|
643
|
+
// @ts-ignore
|
|
644
|
+
delete this.timerRefStatic
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
// Get our current map view bounds.
|
|
649
|
+
// Create a new bounds object so we don't affect the map.
|
|
650
|
+
//
|
|
651
|
+
// See Comments 9 & 11 on Issue 3651 relating to this workaround for a Google Maps bug:
|
|
652
|
+
const mapBounds =
|
|
653
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-ignore
|
|
654
|
+
// @ts-ignore
|
|
655
|
+
this.getMap().getZoom() > 3
|
|
656
|
+
? new google.maps.LatLngBounds(
|
|
657
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-ignore
|
|
658
|
+
// @ts-ignore
|
|
659
|
+
this.getMap()
|
|
660
|
+
.getBounds()
|
|
661
|
+
.getSouthWest(),
|
|
662
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-ignore
|
|
663
|
+
// @ts-ignore
|
|
664
|
+
this.getMap()
|
|
665
|
+
.getBounds()
|
|
666
|
+
.getNorthEast()
|
|
667
|
+
)
|
|
668
|
+
: new google.maps.LatLngBounds(
|
|
669
|
+
new google.maps.LatLng(85.02070771743472, -178.48388434375),
|
|
670
|
+
new google.maps.LatLng(-85.08136444384544, 178.00048865625)
|
|
671
|
+
)
|
|
672
|
+
|
|
673
|
+
const bounds = this.getExtendedBounds(mapBounds)
|
|
674
|
+
|
|
675
|
+
const iLast = Math.min(iFirst + this.batchSize, this.markers.length)
|
|
676
|
+
|
|
677
|
+
for (let i = iFirst; i < iLast; i++) {
|
|
678
|
+
const marker = this.markers[i]
|
|
679
|
+
|
|
680
|
+
if (!marker.isAdded && this.isMarkerInBounds(marker, bounds)) {
|
|
681
|
+
if (!this.ignoreHidden || (this.ignoreHidden && marker.getVisible())) {
|
|
682
|
+
this.addToClosestCluster(marker)
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
if (iLast < this.markers.length) {
|
|
688
|
+
this.timerRefStatic = window.setTimeout(
|
|
689
|
+
// eslint-disable-next-line @getify/proper-arrows/this, @getify/proper-arrows/name
|
|
690
|
+
() => {
|
|
691
|
+
this.createClusters(iLast)
|
|
692
|
+
},
|
|
693
|
+
0
|
|
694
|
+
)
|
|
695
|
+
} else {
|
|
696
|
+
this.timerRefStatic = null
|
|
697
|
+
|
|
698
|
+
/**
|
|
699
|
+
* This event is fired when the <code>Clusterer</code> stops
|
|
700
|
+
* clustering markers.
|
|
701
|
+
* @name Clusterer#clusteringend
|
|
702
|
+
* @param {Clusterer} mc The Clusterer whose markers are being clustered.
|
|
703
|
+
* @event
|
|
704
|
+
*/
|
|
705
|
+
google.maps.event.trigger(this, 'clusteringend', this)
|
|
706
|
+
|
|
707
|
+
for (let i = 0; i < this.clusters.length; i++) {
|
|
708
|
+
this.clusters[i].updateIcon()
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
extend(obj1: any, obj2: any): any {
|
|
714
|
+
return function applyExtend(object: any) {
|
|
715
|
+
// eslint-disable-next-line guard-for-in
|
|
716
|
+
for (const property in object.prototype) {
|
|
717
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-ignore
|
|
718
|
+
// @ts-ignore
|
|
719
|
+
this.prototype[property] = object.prototype[property]
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-ignore
|
|
723
|
+
// @ts-ignore
|
|
724
|
+
return this
|
|
725
|
+
}.apply(obj1, [obj2])
|
|
726
|
+
}
|
|
727
|
+
}
|