@bmlt-enabled/croutonjs 3.16.3 → 3.17.1

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/crouton-map.js ADDED
@@ -0,0 +1,2294 @@
1
+ function CroutonMap(config) {
2
+ var self = this;
3
+ self.config = {
4
+ google_api_key: null, // Required if using the show_map option. Be sure to add an HTTP restriction as well.
5
+ distance_units: 'mi'
6
+ };
7
+ Object.assign(self.config, config);
8
+ self.map = null;
9
+ self.geocoder = null;
10
+ self.map_objects = [];
11
+ self.map_clusters = [];
12
+ self.oms = null;
13
+ self.markerClusterer = null;
14
+ self.handlebarMapOptions = null;
15
+ self.loadGapi = function(callbackFunctionName) {
16
+ var tag = document.createElement('script');
17
+ tag.src = "https://maps.googleapis.com/maps/api/js?key=" + self.config['google_api_key'] + "&callback=" + callbackFunctionName;
18
+ tag.defer = true;
19
+ tag.async = true;
20
+ var firstScriptTag = document.getElementsByTagName('script')[0];
21
+ firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);
22
+ };
23
+ self.getCurrentLocation = function(callback) {
24
+ if (navigator.geolocation) {
25
+ navigator.geolocation.getCurrentPosition(callback, self.errorHandler);
26
+ } else {
27
+ $('.geo').removeClass("hide").addClass("show").html('<p>Geolocation is not supported by your browser</p>');
28
+ }
29
+ };
30
+
31
+ CroutonMap.prototype.addCurrentLocationPin = function(latitude, longitude) {
32
+ var latlng = new google.maps.LatLng(latitude, longitude);
33
+ self.map.setCenter(latlng);
34
+
35
+ var currentLocationMarker = new google.maps.Marker({
36
+ "map": self.map,
37
+ "position": latlng,
38
+ });
39
+
40
+ self.addToMapObjectCollection(currentLocationMarker);
41
+
42
+ var infoWindow = new google.maps.InfoWindow();
43
+ infoWindow.setContent('Current Location');
44
+ infoWindow.open(self.map, currentLocationMarker);
45
+ };
46
+
47
+ self.findMarkerById = function(id) {
48
+ for (var m = 0; m < self.map_objects.length; m++) {
49
+ var map_object = self.map_objects[m];
50
+ if (parseInt(map_object['id']) === id) {
51
+ return map_object;
52
+ }
53
+ }
54
+
55
+ return null;
56
+ };
57
+
58
+ self.rowClick = function(id) {
59
+ var map_marker = self.findMarkerById(id);
60
+ if (!map_marker) return;
61
+ self.map.setCenter(map_marker.getPosition());
62
+ self.map.setZoom(17);
63
+ google.maps.event.trigger(map_marker, "click");
64
+ };
65
+
66
+ self.addToMapObjectCollection = function(obj) {
67
+ self.map_objects.push(obj);
68
+ };
69
+
70
+ self.clearAllMapClusters = function() {
71
+ while (self.map_clusters.length > 0) {
72
+ self.map_clusters[0].setMap(null);
73
+ self.map_clusters.splice(0, 1);
74
+ }
75
+
76
+ if (self.oms !== null) {
77
+ self.oms.removeAllMarkers();
78
+ }
79
+
80
+ if (self.markerClusterer !== null) {
81
+ self.markerClusterer.clearMarkers();
82
+ }
83
+ };
84
+
85
+ self.clearAllMapObjects = function() {
86
+ while (self.map_objects.length > 0) {
87
+ self.map_objects[0].setMap(null);
88
+ self.map_objects.splice(0, 1);
89
+ }
90
+
91
+ //infoWindow.close();
92
+ };
93
+ }
94
+ CroutonMap.prototype.mapSearchClickMode = function() {
95
+ var self = this;
96
+ self.mapClickSearchMode = true;
97
+ self.map.setOptions({
98
+ draggableCursor: 'crosshair',
99
+ zoomControl: false,
100
+ gestureHandling: 'none'
101
+ });
102
+ };
103
+ CroutonMap.prototype.mapSearchPanZoomMode = function() {
104
+ var self = this;
105
+ self.mapClickSearchMode = false;
106
+ self.map.setOptions({
107
+ draggableCursor: 'default',
108
+ zoomControl: true,
109
+ gestureHandling: 'auto'
110
+ });
111
+ };
112
+
113
+ CroutonMap.prototype.mapSearchNearMeMode = function() {
114
+ var self = this;
115
+ self.mapSearchPanZoomMode();
116
+ self.getCurrentLocation(function(position) {
117
+ crouton.searchByCoordinates(position.coords.latitude, position.coords.longitude);
118
+ });
119
+ };
120
+
121
+ CroutonMap.prototype.mapSearchTextMode = function(location) {
122
+ var self = this;
123
+ self.mapSearchPanZoomMode();
124
+ if (location !== undefined && location !== null && location !== "") {
125
+ self.geocoder.geocode({'address': location}, function (results, status) {
126
+ if (status === 'OK') {
127
+ crouton.searchByCoordinates(results[0].geometry.location.lat(), results[0].geometry.location.lng());
128
+ } else {
129
+ console.log('Geocode was not successful for the following reason: ' + status);
130
+ }
131
+ });
132
+ }
133
+ };
134
+
135
+ CroutonMap.prototype.renderMap = function() {
136
+ var self = this;
137
+ self.geocoder = new google.maps.Geocoder();
138
+ jQuery.when(jQuery.getJSON(self.config['template_path'] + "/themes/" + self.config['theme'] + ".json").then(
139
+ function (data, textStatus, jqXHR) {
140
+ return self.config["theme_js"] = data["google_map_theme"];
141
+ }
142
+ )).then(function() {
143
+ self.map = new google.maps.Map(document.getElementById(self.domElementName), {
144
+ zoom: self.config['map_search']['zoom'] || 10,
145
+ center: {
146
+ lat: self.config['map_search']['latitude'],
147
+ lng: self.config['map_search']['longitude'],
148
+ },
149
+ mapTypeControl: false,
150
+ styles: self.config["theme_js"]
151
+ });
152
+
153
+ var controlDiv = document.createElement('div');
154
+
155
+ // Set CSS for the control border
156
+ var controlUI = document.createElement('div');
157
+ controlUI.className = 'mapcontrolcontainer';
158
+ controlUI.title = 'Click to recenter the map';
159
+ controlDiv.appendChild(controlUI);
160
+
161
+ // Set CSS for the control interior
162
+ var clickSearch = document.createElement('div');
163
+ clickSearch.className = 'mapcontrols';
164
+ clickSearch.innerHTML = '<label for="nearme" class="mapcontrolslabel"><input type="radio" id="nearme" name="mapcontrols"> ' + crouton.localization.getWord('near_me') + '</label><label for="textsearch" class="mapcontrolslabel"><input type="radio" id="textsearch" name="mapcontrols"> ' + crouton.localization.getWord('text_search') + '</label><label for="clicksearch" class="mapcontrolslabel"><input type="radio" id="clicksearch" name="mapcontrols"> ' + crouton.localization.getWord('click_search') + '</label>';
165
+ controlUI.appendChild(clickSearch);
166
+ controlDiv.index = 1;
167
+
168
+ google.maps.event.addDomListener(clickSearch, 'click', function () {
169
+ var controlsButtonSelections = jQuery("input:radio[name='mapcontrols']:checked").attr("id");
170
+ if (controlsButtonSelections === "textsearch") {
171
+ self.mapSearchTextMode(prompt("Enter a location or postal code:"));
172
+ } else if (controlsButtonSelections === "nearme") {
173
+ self.mapSearchNearMeMode();
174
+ } else if (controlsButtonSelections === "clicksearch") {
175
+ self.mapSearchClickMode();
176
+ }
177
+ });
178
+
179
+ self.map.controls[google.maps.ControlPosition.TOP_LEFT].push(controlDiv);
180
+ self.map.addListener('click', function (data) {
181
+ if (self.mapClickSearchMode) {
182
+ self.mapSearchPanZoomMode();
183
+ crouton.searchByCoordinates(data.latLng.lat(), data.latLng.lng());
184
+ }
185
+ });
186
+
187
+ if (self.config['map_search']['auto']) {
188
+ self.mapSearchNearMeMode();
189
+ } else if (self.config['map_search']['location'] !== undefined) {
190
+ self.mapSearchTextMode(self.config['map_search']['location']);
191
+ } else if (self.config['map_search']['coordinates_search']) {
192
+ crouton.searchByCoordinates(self.config['map_search']['latitude'], self.config['map_search']['longitude']);
193
+ }
194
+ })
195
+ };
196
+ CroutonMap.prototype.showMap = function() {
197
+ }
198
+ CroutonMap.prototype.render = function(domElementName) {
199
+ self = this;
200
+ self.domElementName = domElementName;
201
+ this.loadGapi('croutonMap.renderMap');
202
+ }
203
+ CroutonMap.prototype.reload = function(meetingData, formatsData) {
204
+ this.meetingData = meetingData;
205
+ this.formatsData = formatsData;
206
+ }
207
+ CroutonMap.prototype.initialize = function(domElementName, meetingData, formatsData, handlebarMapOptions=null) {
208
+ this.meetingData = meetingData;
209
+ this.formatsData = formatsData;
210
+ this.handlebarMapOptions = handlebarMapOptions;
211
+ this.domElementName = domElementName;
212
+ this.loadGapi('croutonMap.initMap');
213
+ }
214
+ CroutonMap.prototype.initMap = function(callback=null) {
215
+ var self = this;
216
+ if (self.map == null) {
217
+ var mapOpt = { zoom: 3, maxZoom: 17 };
218
+ if (self.handlebarMapOptions) mapOpt = {
219
+ center: new google.maps.LatLng(self.handlebarMapOptions.lat, self.handlebarMapOptions.lng),
220
+ zoom: self.handlebarMapOptions.zoom,
221
+ mapTypeId:google.maps.MapTypeId.ROADMAP
222
+ };
223
+ self.map = new google.maps.Map(document.getElementById(self.domElementName), mapOpt );
224
+ }
225
+ self.fillMap();
226
+ if (callback) callback();
227
+ }
228
+ CroutonMap.prototype.fillMap = function(filteredIds=null) {
229
+ var self = this;
230
+ if (self.map == null) return;
231
+ self.clearAllMapObjects();
232
+ self.clearAllMapClusters();
233
+ const physicalMeetings = self.meetingData.filter(m => m.venue_type != venueType.VIRTUAL);
234
+ const filteredMeetings = (filteredIds===null) ? physicalMeetings
235
+ : physicalMeetings.filter((m) => filteredIds.includes(m.id_bigint));
236
+ const bounds = filteredMeetings.reduce(
237
+ function (bounds, m) {
238
+ return bounds.extend(new google.maps.LatLng(m.latitude, m.longitude))
239
+ }, new google.maps.LatLngBounds());
240
+ // We now have the full rectangle of our meeting search results. Scale the map to fit them.
241
+ if (!self.handlebarMapOptions) self.map.fitBounds(bounds);
242
+ if (self.map.getZoom()>18) self.map.setZoom(18);
243
+ var infoWindow = new google.maps.InfoWindow();
244
+
245
+ // Create OverlappingMarkerSpiderfier instance
246
+ self.oms = new OverlappingMarkerSpiderfier(self.map, {
247
+ markersWontMove: true,
248
+ markersWontHide: true,
249
+ });
250
+
251
+ self.oms.addListener('format', function (marker, status) {
252
+ var iconURL;
253
+ if (status === OverlappingMarkerSpiderfier.markerStatus.SPIDERFIED
254
+ || status === OverlappingMarkerSpiderfier.markerStatus.SPIDERFIABLE
255
+ || status === OverlappingMarkerSpiderfier.markerStatus.UNSPIDERFIED) {
256
+ iconURL = self.config['template_path'] + '/NAMarkerR.png';
257
+ } else if (status === OverlappingMarkerSpiderfier.markerStatus.UNSPIDERFIABLE) {
258
+ iconURL = self.config['template_path'] + '/NAMarkerB.png';
259
+ } else {
260
+ iconURL = null;
261
+ }
262
+
263
+ var iconSize = new google.maps.Size(22, 32);
264
+ marker.setIcon({
265
+ url: iconURL,
266
+ size: iconSize,
267
+ scaledSize: iconSize
268
+ });
269
+ });
270
+
271
+ self.map.addListener('zoom_changed', function() {
272
+ self.map.addListener('idle', function() {
273
+ var spidered = self.oms.markersNearAnyOtherMarker();
274
+ for (var i = 0; i < spidered.length; i ++) {
275
+ spidered[i].icon.url = self.config['template_path'] + '/NAMarkerR.png';
276
+ }
277
+ });
278
+ });
279
+
280
+ // This is necessary to make the Spiderfy work
281
+ self.oms.addListener('click', function (marker) {
282
+ marker.zIndex = 999;
283
+ infoWindow.setContent(marker.desc);
284
+ infoWindow.open(self.map, marker);
285
+ });
286
+ infoWindow.addListener('closeclick', function() {
287
+ jQuery(".bmlt-data-row > td").removeClass("rowHighlight");
288
+ });
289
+ // Add some markers to the map.
290
+ filteredMeetings.map(function (location, i) {
291
+ var marker_html = '<dl><dt><strong>';
292
+ marker_html += location.meeting_name;
293
+ marker_html += '</strong></dt>';
294
+ marker_html += '<dd><em>';
295
+ marker_html += crouton.localization.getDayOfTheWeekWord(location.weekday_tinyint);
296
+ var time = location.start_time.toString().split(':');
297
+ var hour = parseInt(time[0]);
298
+ var minute = parseInt(time[1]);
299
+ var pm = 'AM';
300
+ if (hour >= 12) {
301
+ pm = 'PM';
302
+ if (hour > 12) {
303
+ hour -= 12;
304
+ }
305
+ }
306
+ hour = hour.toString();
307
+ minute = (minute > 9) ? minute.toString() : ('0' + minute.toString());
308
+ marker_html += ' ' + hour + ':' + minute + ' ' + pm;
309
+ marker_html += '</em><br>';
310
+ marker_html += location.location_text;
311
+ marker_html += '<br>';
312
+
313
+ if (typeof location.location_street !== "undefined") {
314
+ marker_html += location.location_street + '<br>';
315
+ }
316
+ if (typeof location.location_municipality !== "undefined") {
317
+ marker_html += location.location_municipality + ' ';
318
+ }
319
+ if (typeof location.location_province !== "undefined") {
320
+ marker_html += location.location_province + ' ';
321
+ }
322
+ if (typeof location.location_postal_code_1 !== "undefined") {
323
+ marker_html += location.location_postal_code_1;
324
+ }
325
+
326
+ marker_html += '<br>';
327
+ var url = 'https://maps.google.com/maps?q=' + location.latitude + ',' + location.longitude + '&hl=' + self.config['short_language'];
328
+ marker_html += '<a target=\"_blank\" href="' + url + '">';
329
+ marker_html += crouton.localization.getWord('map');
330
+ marker_html += '</a>';
331
+ marker_html += '</dd></dl>';
332
+
333
+ var latLng = {"lat": parseFloat(location.latitude), "lng": parseFloat(location.longitude)};
334
+
335
+ var marker = new google.maps.Marker({
336
+ position: latLng
337
+ });
338
+
339
+ marker['id'] = location['id_bigint'];
340
+ marker['day_id'] = location['weekday_tinyint'];
341
+
342
+ self.addToMapObjectCollection(marker);
343
+ self.oms.addMarker(marker);
344
+
345
+ self.map_clusters.push(marker);
346
+ google.maps.event.addListener(marker, 'click', function (evt) {
347
+ jQuery(".bmlt-data-row > td").removeClass("rowHighlight");
348
+ jQuery("#meeting-data-row-" + marker['id'] + " > td").addClass("rowHighlight");
349
+ crouton.dayTab(marker['day_id']);
350
+ infoWindow.setContent(marker_html);
351
+ infoWindow.open(self.map, marker);
352
+ });
353
+ return marker;
354
+ });
355
+
356
+ // Add a marker clusterer to manage the markers.
357
+ self.markerClusterer = new MarkerClusterer(self.map, self.map_clusters, {
358
+ imagePath: self.config['template_path'] + '/m',
359
+ maxZoom: self.config['map_max_zoom'],
360
+ zoomOnClick: false
361
+ });
362
+ };
363
+
364
+ /**
365
+ * @name MarkerClusterer for Google Maps v3
366
+ * @version version 1.0.1
367
+ * @author Luke Mahe
368
+ * @fileoverview
369
+ * The library creates and manages per-zoom-level clusters for large amounts of
370
+ * markers.
371
+ * <br/>
372
+ * This is a v3 implementation of the
373
+ * <a href="http://gmaps-utility-library-dev.googlecode.com/svn/tags/markerclusterer/"
374
+ * >v2 MarkerClusterer</a>.
375
+ */
376
+
377
+ /**
378
+ * Licensed under the Apache License, Version 2.0 (the "License");
379
+ * you may not use this file except in compliance with the License.
380
+ * You may obtain a copy of the License at
381
+ *
382
+ * http://www.apache.org/licenses/LICENSE-2.0
383
+ *
384
+ * Unless required by applicable law or agreed to in writing, software
385
+ * distributed under the License is distributed on an "AS IS" BASIS,
386
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
387
+ * See the License for the specific language governing permissions and
388
+ * limitations under the License.
389
+ */
390
+
391
+
392
+ /**
393
+ * A Marker Clusterer that clusters markers.
394
+ *
395
+ * @param {google.maps.Map} map The Google map to attach to.
396
+ * @param {Array.<google.maps.Marker>=} opt_markers Optional markers to add to
397
+ * the cluster.
398
+ * @param {Object=} opt_options support the following options:
399
+ * 'gridSize': (number) The grid size of a cluster in pixels.
400
+ * 'maxZoom': (number) The maximum zoom level that a marker can be part of a
401
+ * cluster.
402
+ * 'zoomOnClick': (boolean) Whether the default behaviour of clicking on a
403
+ * cluster is to zoom into it.
404
+ * 'averageCenter': (boolean) Whether the center of each cluster should be
405
+ * the average of all markers in the cluster.
406
+ * 'minimumClusterSize': (number) The minimum number of markers to be in a
407
+ * cluster before the markers are hidden and a count
408
+ * is shown.
409
+ * 'styles': (object) An object that has style properties:
410
+ * 'url': (string) The image url.
411
+ * 'height': (number) The image height.
412
+ * 'width': (number) The image width.
413
+ * 'anchor': (Array) The anchor position of the label text.
414
+ * 'textColor': (string) The text color.
415
+ * 'textSize': (number) The text size.
416
+ * 'backgroundPosition': (string) The position of the backgound x, y.
417
+ * @constructor
418
+ * @extends google.maps.OverlayView
419
+ */
420
+ function MarkerClusterer(map, opt_markers, opt_options) {
421
+ // MarkerClusterer implements google.maps.OverlayView interface. We use the
422
+ // extend function to extend MarkerClusterer with google.maps.OverlayView
423
+ // because it might not always be available when the code is defined so we
424
+ // look for it at the last possible moment. If it doesn't exist now then
425
+ // there is no point going ahead :)
426
+ this.extend(MarkerClusterer, google.maps.OverlayView);
427
+ this.map_ = map;
428
+
429
+ /**
430
+ * @type {Array.<google.maps.Marker>}
431
+ * @private
432
+ */
433
+ this.markers_ = [];
434
+
435
+ /**
436
+ * @type {Array.<Cluster>}
437
+ */
438
+ this.clusters_ = [];
439
+
440
+ this.sizes = [53, 56, 66, 78, 90];
441
+
442
+ /**
443
+ * @private
444
+ */
445
+ this.styles_ = [];
446
+
447
+ /**
448
+ * @type {boolean}
449
+ * @private
450
+ */
451
+ this.ready_ = false;
452
+
453
+ var options = opt_options || {};
454
+
455
+ /**
456
+ * @type {number}
457
+ * @private
458
+ */
459
+ this.gridSize_ = options['gridSize'] || 60;
460
+
461
+ /**
462
+ * @private
463
+ */
464
+ this.minClusterSize_ = options['minimumClusterSize'] || 2;
465
+
466
+
467
+ /**
468
+ * @type {?number}
469
+ * @private
470
+ */
471
+ this.maxZoom_ = options['maxZoom'] || null;
472
+
473
+ this.styles_ = options['styles'] || [];
474
+
475
+ /**
476
+ * @type {string}
477
+ * @private
478
+ */
479
+ this.imagePath_ = options['imagePath'] ||
480
+ this.MARKER_CLUSTER_IMAGE_PATH_;
481
+
482
+ /**
483
+ * @type {string}
484
+ * @private
485
+ */
486
+ this.imageExtension_ = options['imageExtension'] ||
487
+ this.MARKER_CLUSTER_IMAGE_EXTENSION_;
488
+
489
+ /**
490
+ * @type {boolean}
491
+ * @private
492
+ */
493
+ this.zoomOnClick_ = true;
494
+
495
+ if (options['zoomOnClick'] != undefined) {
496
+ this.zoomOnClick_ = options['zoomOnClick'];
497
+ }
498
+
499
+ /**
500
+ * @type {boolean}
501
+ * @private
502
+ */
503
+ this.averageCenter_ = false;
504
+
505
+ if (options['averageCenter'] != undefined) {
506
+ this.averageCenter_ = options['averageCenter'];
507
+ }
508
+
509
+ this.setupStyles_();
510
+
511
+ this.setMap(map);
512
+
513
+ /**
514
+ * @type {number}
515
+ * @private
516
+ */
517
+ this.prevZoom_ = this.map_.getZoom();
518
+
519
+ // Add the map event listeners
520
+ var that = this;
521
+ google.maps.event.addListener(this.map_, 'zoom_changed', function() {
522
+ // Determines map type and prevent illegal zoom levels
523
+ var zoom = that.map_.getZoom();
524
+ var minZoom = that.map_.minZoom || 0;
525
+ var maxZoom = Math.min(that.map_.maxZoom || 100,
526
+ that.map_.mapTypes[that.map_.getMapTypeId()].maxZoom);
527
+ zoom = Math.min(Math.max(zoom,minZoom),maxZoom);
528
+
529
+ if (that.prevZoom_ != zoom) {
530
+ that.prevZoom_ = zoom;
531
+ that.resetViewport();
532
+ }
533
+ });
534
+
535
+ google.maps.event.addListener(this.map_, 'idle', function() {
536
+ that.redraw();
537
+ });
538
+
539
+ // Finally, add the markers
540
+ if (opt_markers && (opt_markers.length || Object.keys(opt_markers).length)) {
541
+ this.addMarkers(opt_markers, false);
542
+ }
543
+ }
544
+
545
+
546
+ /**
547
+ * The marker cluster image path.
548
+ *
549
+ * @type {string}
550
+ * @private
551
+ */
552
+ MarkerClusterer.prototype.MARKER_CLUSTER_IMAGE_PATH_ = '../images/m';
553
+
554
+
555
+ /**
556
+ * The marker cluster image path.
557
+ *
558
+ * @type {string}
559
+ * @private
560
+ */
561
+ MarkerClusterer.prototype.MARKER_CLUSTER_IMAGE_EXTENSION_ = 'png';
562
+
563
+
564
+ /**
565
+ * Extends a objects prototype by anothers.
566
+ *
567
+ * @param {Object} obj1 The object to be extended.
568
+ * @param {Object} obj2 The object to extend with.
569
+ * @return {Object} The new extended object.
570
+ * @ignore
571
+ */
572
+ MarkerClusterer.prototype.extend = function(obj1, obj2) {
573
+ return (function(object) {
574
+ for (var property in object.prototype) {
575
+ this.prototype[property] = object.prototype[property];
576
+ }
577
+ return this;
578
+ }).apply(obj1, [obj2]);
579
+ };
580
+
581
+
582
+ /**
583
+ * Implementaion of the interface method.
584
+ * @ignore
585
+ */
586
+ MarkerClusterer.prototype.onAdd = function() {
587
+ this.setReady_(true);
588
+ };
589
+
590
+ /**
591
+ * Implementaion of the interface method.
592
+ * @ignore
593
+ */
594
+ MarkerClusterer.prototype.draw = function() {};
595
+
596
+ /**
597
+ * Sets up the styles object.
598
+ *
599
+ * @private
600
+ */
601
+ MarkerClusterer.prototype.setupStyles_ = function() {
602
+ if (this.styles_.length) {
603
+ return;
604
+ }
605
+
606
+ for (var i = 0, size; size = this.sizes[i]; i++) {
607
+ this.styles_.push({
608
+ url: this.imagePath_ + (i + 1) + '.' + this.imageExtension_,
609
+ height: size,
610
+ width: size
611
+ });
612
+ }
613
+ };
614
+
615
+ /**
616
+ * Fit the map to the bounds of the markers in the clusterer.
617
+ */
618
+ MarkerClusterer.prototype.fitMapToMarkers = function() {
619
+ var markers = this.getMarkers();
620
+ var bounds = new google.maps.LatLngBounds();
621
+ for (var i = 0, marker; marker = markers[i]; i++) {
622
+ bounds.extend(marker.getPosition());
623
+ }
624
+
625
+ this.map_.fitBounds(bounds);
626
+ };
627
+
628
+
629
+ /**
630
+ * Sets the styles.
631
+ *
632
+ * @param {Object} styles The style to set.
633
+ */
634
+ MarkerClusterer.prototype.setStyles = function(styles) {
635
+ this.styles_ = styles;
636
+ };
637
+
638
+
639
+ /**
640
+ * Gets the styles.
641
+ *
642
+ * @return {Object} The styles object.
643
+ */
644
+ MarkerClusterer.prototype.getStyles = function() {
645
+ return this.styles_;
646
+ };
647
+
648
+
649
+ /**
650
+ * Whether zoom on click is set.
651
+ *
652
+ * @return {boolean} True if zoomOnClick_ is set.
653
+ */
654
+ MarkerClusterer.prototype.isZoomOnClick = function() {
655
+ return this.zoomOnClick_;
656
+ };
657
+
658
+ /**
659
+ * Whether average center is set.
660
+ *
661
+ * @return {boolean} True if averageCenter_ is set.
662
+ */
663
+ MarkerClusterer.prototype.isAverageCenter = function() {
664
+ return this.averageCenter_;
665
+ };
666
+
667
+
668
+ /**
669
+ * Returns the array of markers in the clusterer.
670
+ *
671
+ * @return {Array.<google.maps.Marker>} The markers.
672
+ */
673
+ MarkerClusterer.prototype.getMarkers = function() {
674
+ return this.markers_;
675
+ };
676
+
677
+
678
+ /**
679
+ * Returns the number of markers in the clusterer
680
+ *
681
+ * @return {Number} The number of markers.
682
+ */
683
+ MarkerClusterer.prototype.getTotalMarkers = function() {
684
+ return this.markers_.length;
685
+ };
686
+
687
+
688
+ /**
689
+ * Sets the max zoom for the clusterer.
690
+ *
691
+ * @param {number} maxZoom The max zoom level.
692
+ */
693
+ MarkerClusterer.prototype.setMaxZoom = function(maxZoom) {
694
+ this.maxZoom_ = maxZoom;
695
+ };
696
+
697
+
698
+ /**
699
+ * Gets the max zoom for the clusterer.
700
+ *
701
+ * @return {number} The max zoom level.
702
+ */
703
+ MarkerClusterer.prototype.getMaxZoom = function() {
704
+ return this.maxZoom_;
705
+ };
706
+
707
+
708
+ /**
709
+ * The function for calculating the cluster icon image.
710
+ *
711
+ * @param {Array.<google.maps.Marker>} markers The markers in the clusterer.
712
+ * @param {number} numStyles The number of styles available.
713
+ * @return {Object} A object properties: 'text' (string) and 'index' (number).
714
+ * @private
715
+ */
716
+ MarkerClusterer.prototype.calculator_ = function(markers, numStyles) {
717
+ var index = 0;
718
+ var count = markers.length;
719
+ var dv = count;
720
+ while (dv !== 0) {
721
+ dv = parseInt(dv / 10, 10);
722
+ index++;
723
+ }
724
+
725
+ index = Math.min(index, numStyles);
726
+ return {
727
+ text: count,
728
+ index: index
729
+ };
730
+ };
731
+
732
+
733
+ /**
734
+ * Set the calculator function.
735
+ *
736
+ * @param {function(Array, number)} calculator The function to set as the
737
+ * calculator. The function should return a object properties:
738
+ * 'text' (string) and 'index' (number).
739
+ *
740
+ */
741
+ MarkerClusterer.prototype.setCalculator = function(calculator) {
742
+ this.calculator_ = calculator;
743
+ };
744
+
745
+
746
+ /**
747
+ * Get the calculator function.
748
+ *
749
+ * @return {function(Array, number)} the calculator function.
750
+ */
751
+ MarkerClusterer.prototype.getCalculator = function() {
752
+ return this.calculator_;
753
+ };
754
+
755
+
756
+ /**
757
+ * Add an array of markers to the clusterer.
758
+ *
759
+ * @param {Array.<google.maps.Marker>} markers The markers to add.
760
+ * @param {boolean=} opt_nodraw Whether to redraw the clusters.
761
+ */
762
+ MarkerClusterer.prototype.addMarkers = function(markers, opt_nodraw) {
763
+ if (markers.length) {
764
+ for (var i = 0, marker; marker = markers[i]; i++) {
765
+ this.pushMarkerTo_(marker);
766
+ }
767
+ } else if (Object.keys(markers).length) {
768
+ for (var marker in markers) {
769
+ this.pushMarkerTo_(markers[marker]);
770
+ }
771
+ }
772
+ if (!opt_nodraw) {
773
+ this.redraw();
774
+ }
775
+ };
776
+
777
+
778
+ /**
779
+ * Pushes a marker to the clusterer.
780
+ *
781
+ * @param {google.maps.Marker} marker The marker to add.
782
+ * @private
783
+ */
784
+ MarkerClusterer.prototype.pushMarkerTo_ = function(marker) {
785
+ marker.isAdded = false;
786
+ if (marker['draggable']) {
787
+ // If the marker is draggable add a listener so we update the clusters on
788
+ // the drag end.
789
+ var that = this;
790
+ google.maps.event.addListener(marker, 'dragend', function() {
791
+ marker.isAdded = false;
792
+ that.repaint();
793
+ });
794
+ }
795
+ this.markers_.push(marker);
796
+ };
797
+
798
+
799
+ /**
800
+ * Adds a marker to the clusterer and redraws if needed.
801
+ *
802
+ * @param {google.maps.Marker} marker The marker to add.
803
+ * @param {boolean=} opt_nodraw Whether to redraw the clusters.
804
+ */
805
+ MarkerClusterer.prototype.addMarker = function(marker, opt_nodraw) {
806
+ this.pushMarkerTo_(marker);
807
+ if (!opt_nodraw) {
808
+ this.redraw();
809
+ }
810
+ };
811
+
812
+
813
+ /**
814
+ * Removes a marker and returns true if removed, false if not
815
+ *
816
+ * @param {google.maps.Marker} marker The marker to remove
817
+ * @return {boolean} Whether the marker was removed or not
818
+ * @private
819
+ */
820
+ MarkerClusterer.prototype.removeMarker_ = function(marker) {
821
+ var index = -1;
822
+ if (this.markers_.indexOf) {
823
+ index = this.markers_.indexOf(marker);
824
+ } else {
825
+ for (var i = 0, m; m = this.markers_[i]; i++) {
826
+ if (m == marker) {
827
+ index = i;
828
+ break;
829
+ }
830
+ }
831
+ }
832
+
833
+ if (index == -1) {
834
+ // Marker is not in our list of markers.
835
+ return false;
836
+ }
837
+
838
+ marker.setMap(null);
839
+
840
+ this.markers_.splice(index, 1);
841
+
842
+ return true;
843
+ };
844
+
845
+
846
+ /**
847
+ * Remove a marker from the cluster.
848
+ *
849
+ * @param {google.maps.Marker} marker The marker to remove.
850
+ * @param {boolean=} opt_nodraw Optional boolean to force no redraw.
851
+ * @return {boolean} True if the marker was removed.
852
+ */
853
+ MarkerClusterer.prototype.removeMarker = function(marker, opt_nodraw) {
854
+ var removed = this.removeMarker_(marker);
855
+
856
+ if (!opt_nodraw && removed) {
857
+ this.resetViewport();
858
+ this.redraw();
859
+ return true;
860
+ } else {
861
+ return false;
862
+ }
863
+ };
864
+
865
+
866
+ /**
867
+ * Removes an array of markers from the cluster.
868
+ *
869
+ * @param {Array.<google.maps.Marker>} markers The markers to remove.
870
+ * @param {boolean=} opt_nodraw Optional boolean to force no redraw.
871
+ */
872
+ MarkerClusterer.prototype.removeMarkers = function(markers, opt_nodraw) {
873
+ var removed = false;
874
+
875
+ for (var i = 0, marker; marker = markers[i]; i++) {
876
+ var r = this.removeMarker_(marker);
877
+ removed = removed || r;
878
+ }
879
+
880
+ if (!opt_nodraw && removed) {
881
+ this.resetViewport();
882
+ this.redraw();
883
+ return true;
884
+ }
885
+ };
886
+
887
+
888
+ /**
889
+ * Sets the clusterer's ready state.
890
+ *
891
+ * @param {boolean} ready The state.
892
+ * @private
893
+ */
894
+ MarkerClusterer.prototype.setReady_ = function(ready) {
895
+ if (!this.ready_) {
896
+ this.ready_ = ready;
897
+ this.createClusters_();
898
+ }
899
+ };
900
+
901
+
902
+ /**
903
+ * Returns the number of clusters in the clusterer.
904
+ *
905
+ * @return {number} The number of clusters.
906
+ */
907
+ MarkerClusterer.prototype.getTotalClusters = function() {
908
+ return this.clusters_.length;
909
+ };
910
+
911
+
912
+ /**
913
+ * Returns the google map that the clusterer is associated with.
914
+ *
915
+ * @return {google.maps.Map} The map.
916
+ */
917
+ MarkerClusterer.prototype.getMap = function() {
918
+ return this.map_;
919
+ };
920
+
921
+
922
+ /**
923
+ * Sets the google map that the clusterer is associated with.
924
+ *
925
+ * @param {google.maps.Map} map The map.
926
+ */
927
+ MarkerClusterer.prototype.setMap = function(map) {
928
+ this.map_ = map;
929
+ };
930
+
931
+
932
+ /**
933
+ * Returns the size of the grid.
934
+ *
935
+ * @return {number} The grid size.
936
+ */
937
+ MarkerClusterer.prototype.getGridSize = function() {
938
+ return this.gridSize_;
939
+ };
940
+
941
+
942
+ /**
943
+ * Sets the size of the grid.
944
+ *
945
+ * @param {number} size The grid size.
946
+ */
947
+ MarkerClusterer.prototype.setGridSize = function(size) {
948
+ this.gridSize_ = size;
949
+ };
950
+
951
+
952
+ /**
953
+ * Returns the min cluster size.
954
+ *
955
+ * @return {number} The grid size.
956
+ */
957
+ MarkerClusterer.prototype.getMinClusterSize = function() {
958
+ return this.minClusterSize_;
959
+ };
960
+
961
+ /**
962
+ * Sets the min cluster size.
963
+ *
964
+ * @param {number} size The grid size.
965
+ */
966
+ MarkerClusterer.prototype.setMinClusterSize = function(size) {
967
+ this.minClusterSize_ = size;
968
+ };
969
+
970
+
971
+ /**
972
+ * Extends a bounds object by the grid size.
973
+ *
974
+ * @param {google.maps.LatLngBounds} bounds The bounds to extend.
975
+ * @return {google.maps.LatLngBounds} The extended bounds.
976
+ */
977
+ MarkerClusterer.prototype.getExtendedBounds = function(bounds) {
978
+ var projection = this.getProjection();
979
+
980
+ // Turn the bounds into latlng.
981
+ var tr = new google.maps.LatLng(bounds.getNorthEast().lat(),
982
+ bounds.getNorthEast().lng());
983
+ var bl = new google.maps.LatLng(bounds.getSouthWest().lat(),
984
+ bounds.getSouthWest().lng());
985
+
986
+ // Convert the points to pixels and the extend out by the grid size.
987
+ var trPix = projection.fromLatLngToDivPixel(tr);
988
+ trPix.x += this.gridSize_;
989
+ trPix.y -= this.gridSize_;
990
+
991
+ var blPix = projection.fromLatLngToDivPixel(bl);
992
+ blPix.x -= this.gridSize_;
993
+ blPix.y += this.gridSize_;
994
+
995
+ // Convert the pixel points back to LatLng
996
+ var ne = projection.fromDivPixelToLatLng(trPix);
997
+ var sw = projection.fromDivPixelToLatLng(blPix);
998
+
999
+ // Extend the bounds to contain the new bounds.
1000
+ bounds.extend(ne);
1001
+ bounds.extend(sw);
1002
+
1003
+ return bounds;
1004
+ };
1005
+
1006
+
1007
+ /**
1008
+ * Determins if a marker is contained in a bounds.
1009
+ *
1010
+ * @param {google.maps.Marker} marker The marker to check.
1011
+ * @param {google.maps.LatLngBounds} bounds The bounds to check against.
1012
+ * @return {boolean} True if the marker is in the bounds.
1013
+ * @private
1014
+ */
1015
+ MarkerClusterer.prototype.isMarkerInBounds_ = function(marker, bounds) {
1016
+ return bounds.contains(marker.getPosition());
1017
+ };
1018
+
1019
+
1020
+ /**
1021
+ * Clears all clusters and markers from the clusterer.
1022
+ */
1023
+ MarkerClusterer.prototype.clearMarkers = function() {
1024
+ this.resetViewport(true);
1025
+
1026
+ // Set the markers a empty array.
1027
+ this.markers_ = [];
1028
+ };
1029
+
1030
+
1031
+ /**
1032
+ * Clears all existing clusters and recreates them.
1033
+ * @param {boolean} opt_hide To also hide the marker.
1034
+ */
1035
+ MarkerClusterer.prototype.resetViewport = function(opt_hide) {
1036
+ // Remove all the clusters
1037
+ for (var i = 0, cluster; cluster = this.clusters_[i]; i++) {
1038
+ cluster.remove();
1039
+ }
1040
+
1041
+ // Reset the markers to not be added and to be invisible.
1042
+ for (var i = 0, marker; marker = this.markers_[i]; i++) {
1043
+ marker.isAdded = false;
1044
+ if (opt_hide) {
1045
+ marker.setMap(null);
1046
+ }
1047
+ }
1048
+
1049
+ this.clusters_ = [];
1050
+ };
1051
+
1052
+ /**
1053
+ *
1054
+ */
1055
+ MarkerClusterer.prototype.repaint = function() {
1056
+ var oldClusters = this.clusters_.slice();
1057
+ this.clusters_.length = 0;
1058
+ this.resetViewport();
1059
+ this.redraw();
1060
+
1061
+ // Remove the old clusters.
1062
+ // Do it in a timeout so the other clusters have been drawn first.
1063
+ window.setTimeout(function() {
1064
+ for (var i = 0, cluster; cluster = oldClusters[i]; i++) {
1065
+ cluster.remove();
1066
+ }
1067
+ }, 0);
1068
+ };
1069
+
1070
+
1071
+ /**
1072
+ * Redraws the clusters.
1073
+ */
1074
+ MarkerClusterer.prototype.redraw = function() {
1075
+ this.createClusters_();
1076
+ };
1077
+
1078
+
1079
+ /**
1080
+ * Calculates the distance between two latlng locations in km.
1081
+ * @see http://www.movable-type.co.uk/scripts/latlong.html
1082
+ *
1083
+ * @param {google.maps.LatLng} p1 The first lat lng point.
1084
+ * @param {google.maps.LatLng} p2 The second lat lng point.
1085
+ * @return {number} The distance between the two points in km.
1086
+ * @private
1087
+ */
1088
+ MarkerClusterer.prototype.distanceBetweenPoints_ = function(p1, p2) {
1089
+ if (!p1 || !p2) {
1090
+ return 0;
1091
+ }
1092
+
1093
+ var R = 6371; // Radius of the Earth in km
1094
+ var dLat = (p2.lat() - p1.lat()) * Math.PI / 180;
1095
+ var dLon = (p2.lng() - p1.lng()) * Math.PI / 180;
1096
+ var a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
1097
+ Math.cos(p1.lat() * Math.PI / 180) * Math.cos(p2.lat() * Math.PI / 180) *
1098
+ Math.sin(dLon / 2) * Math.sin(dLon / 2);
1099
+ var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
1100
+ var d = R * c;
1101
+ return d;
1102
+ };
1103
+
1104
+
1105
+ /**
1106
+ * Add a marker to a cluster, or creates a new cluster.
1107
+ *
1108
+ * @param {google.maps.Marker} marker The marker to add.
1109
+ * @private
1110
+ */
1111
+ MarkerClusterer.prototype.addToClosestCluster_ = function(marker) {
1112
+ var distance = 40000; // Some large number
1113
+ var clusterToAddTo = null;
1114
+ var pos = marker.getPosition();
1115
+ for (var i = 0, cluster; cluster = this.clusters_[i]; i++) {
1116
+ var center = cluster.getCenter();
1117
+ if (center) {
1118
+ var d = this.distanceBetweenPoints_(center, marker.getPosition());
1119
+ if (d < distance) {
1120
+ distance = d;
1121
+ clusterToAddTo = cluster;
1122
+ }
1123
+ }
1124
+ }
1125
+
1126
+ if (clusterToAddTo && clusterToAddTo.isMarkerInClusterBounds(marker)) {
1127
+ clusterToAddTo.addMarker(marker);
1128
+ } else {
1129
+ var cluster = new Cluster(this);
1130
+ cluster.addMarker(marker);
1131
+ this.clusters_.push(cluster);
1132
+ }
1133
+ };
1134
+
1135
+
1136
+ /**
1137
+ * Creates the clusters.
1138
+ *
1139
+ * @private
1140
+ */
1141
+ MarkerClusterer.prototype.createClusters_ = function() {
1142
+ if (!this.ready_) {
1143
+ return;
1144
+ }
1145
+
1146
+ // Get our current map view bounds.
1147
+ // Create a new bounds object so we don't affect the map.
1148
+ var mapBounds = new google.maps.LatLngBounds(this.map_.getBounds().getSouthWest(),
1149
+ this.map_.getBounds().getNorthEast());
1150
+ var bounds = this.getExtendedBounds(mapBounds);
1151
+
1152
+ for (var i = 0, marker; marker = this.markers_[i]; i++) {
1153
+ if (!marker.isAdded && this.isMarkerInBounds_(marker, bounds)) {
1154
+ this.addToClosestCluster_(marker);
1155
+ }
1156
+ }
1157
+ };
1158
+
1159
+
1160
+ /**
1161
+ * A cluster that contains markers.
1162
+ *
1163
+ * @param {MarkerClusterer} markerClusterer The markerclusterer that this
1164
+ * cluster is associated with.
1165
+ * @constructor
1166
+ * @ignore
1167
+ */
1168
+ function Cluster(markerClusterer) {
1169
+ this.markerClusterer_ = markerClusterer;
1170
+ this.map_ = markerClusterer.getMap();
1171
+ this.gridSize_ = markerClusterer.getGridSize();
1172
+ this.minClusterSize_ = markerClusterer.getMinClusterSize();
1173
+ this.averageCenter_ = markerClusterer.isAverageCenter();
1174
+ this.center_ = null;
1175
+ this.markers_ = [];
1176
+ this.bounds_ = null;
1177
+ this.clusterIcon_ = new ClusterIcon(this, markerClusterer.getStyles(),
1178
+ markerClusterer.getGridSize());
1179
+ }
1180
+
1181
+ /**
1182
+ * Determins if a marker is already added to the cluster.
1183
+ *
1184
+ * @param {google.maps.Marker} marker The marker to check.
1185
+ * @return {boolean} True if the marker is already added.
1186
+ */
1187
+ Cluster.prototype.isMarkerAlreadyAdded = function(marker) {
1188
+ if (this.markers_.indexOf) {
1189
+ return this.markers_.indexOf(marker) != -1;
1190
+ } else {
1191
+ for (var i = 0, m; m = this.markers_[i]; i++) {
1192
+ if (m == marker) {
1193
+ return true;
1194
+ }
1195
+ }
1196
+ }
1197
+ return false;
1198
+ };
1199
+
1200
+
1201
+ /**
1202
+ * Add a marker the cluster.
1203
+ *
1204
+ * @param {google.maps.Marker} marker The marker to add.
1205
+ * @return {boolean} True if the marker was added.
1206
+ */
1207
+ Cluster.prototype.addMarker = function(marker) {
1208
+ if (this.isMarkerAlreadyAdded(marker)) {
1209
+ return false;
1210
+ }
1211
+
1212
+ if (!this.center_) {
1213
+ this.center_ = marker.getPosition();
1214
+ this.calculateBounds_();
1215
+ } else {
1216
+ if (this.averageCenter_) {
1217
+ var l = this.markers_.length + 1;
1218
+ var lat = (this.center_.lat() * (l-1) + marker.getPosition().lat()) / l;
1219
+ var lng = (this.center_.lng() * (l-1) + marker.getPosition().lng()) / l;
1220
+ this.center_ = new google.maps.LatLng(lat, lng);
1221
+ this.calculateBounds_();
1222
+ }
1223
+ }
1224
+
1225
+ marker.isAdded = true;
1226
+ this.markers_.push(marker);
1227
+
1228
+ var len = this.markers_.length;
1229
+ if (len < this.minClusterSize_ && marker.getMap() != this.map_) {
1230
+ // Min cluster size not reached so show the marker.
1231
+ marker.setMap(this.map_);
1232
+ }
1233
+
1234
+ if (len == this.minClusterSize_) {
1235
+ // Hide the markers that were showing.
1236
+ for (var i = 0; i < len; i++) {
1237
+ this.markers_[i].setMap(null);
1238
+ }
1239
+ }
1240
+
1241
+ if (len >= this.minClusterSize_) {
1242
+ marker.setMap(null);
1243
+ }
1244
+
1245
+ this.updateIcon();
1246
+ return true;
1247
+ };
1248
+
1249
+
1250
+ /**
1251
+ * Returns the marker clusterer that the cluster is associated with.
1252
+ *
1253
+ * @return {MarkerClusterer} The associated marker clusterer.
1254
+ */
1255
+ Cluster.prototype.getMarkerClusterer = function() {
1256
+ return this.markerClusterer_;
1257
+ };
1258
+
1259
+
1260
+ /**
1261
+ * Returns the bounds of the cluster.
1262
+ *
1263
+ * @return {google.maps.LatLngBounds} the cluster bounds.
1264
+ */
1265
+ Cluster.prototype.getBounds = function() {
1266
+ var bounds = new google.maps.LatLngBounds(this.center_, this.center_);
1267
+ var markers = this.getMarkers();
1268
+ for (var i = 0, marker; marker = markers[i]; i++) {
1269
+ bounds.extend(marker.getPosition());
1270
+ }
1271
+ return bounds;
1272
+ };
1273
+
1274
+
1275
+ /**
1276
+ * Removes the cluster
1277
+ */
1278
+ Cluster.prototype.remove = function() {
1279
+ this.clusterIcon_.remove();
1280
+ this.markers_.length = 0;
1281
+ delete this.markers_;
1282
+ };
1283
+
1284
+
1285
+ /**
1286
+ * Returns the center of the cluster.
1287
+ *
1288
+ * @return {number} The cluster center.
1289
+ */
1290
+ Cluster.prototype.getSize = function() {
1291
+ return this.markers_.length;
1292
+ };
1293
+
1294
+
1295
+ /**
1296
+ * Returns the center of the cluster.
1297
+ *
1298
+ * @return {Array.<google.maps.Marker>} The cluster center.
1299
+ */
1300
+ Cluster.prototype.getMarkers = function() {
1301
+ return this.markers_;
1302
+ };
1303
+
1304
+
1305
+ /**
1306
+ * Returns the center of the cluster.
1307
+ *
1308
+ * @return {google.maps.LatLng} The cluster center.
1309
+ */
1310
+ Cluster.prototype.getCenter = function() {
1311
+ return this.center_;
1312
+ };
1313
+
1314
+
1315
+ /**
1316
+ * Calculated the extended bounds of the cluster with the grid.
1317
+ *
1318
+ * @private
1319
+ */
1320
+ Cluster.prototype.calculateBounds_ = function() {
1321
+ var bounds = new google.maps.LatLngBounds(this.center_, this.center_);
1322
+ this.bounds_ = this.markerClusterer_.getExtendedBounds(bounds);
1323
+ };
1324
+
1325
+
1326
+ /**
1327
+ * Determines if a marker lies in the clusters bounds.
1328
+ *
1329
+ * @param {google.maps.Marker} marker The marker to check.
1330
+ * @return {boolean} True if the marker lies in the bounds.
1331
+ */
1332
+ Cluster.prototype.isMarkerInClusterBounds = function(marker) {
1333
+ return this.bounds_.contains(marker.getPosition());
1334
+ };
1335
+
1336
+
1337
+ /**
1338
+ * Returns the map that the cluster is associated with.
1339
+ *
1340
+ * @return {google.maps.Map} The map.
1341
+ */
1342
+ Cluster.prototype.getMap = function() {
1343
+ return this.map_;
1344
+ };
1345
+
1346
+
1347
+ /**
1348
+ * Updates the cluster icon
1349
+ */
1350
+ Cluster.prototype.updateIcon = function() {
1351
+ var zoom = this.map_.getZoom();
1352
+ var mz = this.markerClusterer_.getMaxZoom();
1353
+
1354
+ if (mz && zoom > mz) {
1355
+ // The zoom is greater than our max zoom so show all the markers in cluster.
1356
+ for (var i = 0, marker; marker = this.markers_[i]; i++) {
1357
+ marker.setMap(this.map_);
1358
+ }
1359
+ return;
1360
+ }
1361
+
1362
+ if (this.markers_.length < this.minClusterSize_) {
1363
+ // Min cluster size not yet reached.
1364
+ this.clusterIcon_.hide();
1365
+ return;
1366
+ }
1367
+
1368
+ var numStyles = this.markerClusterer_.getStyles().length;
1369
+ var sums = this.markerClusterer_.getCalculator()(this.markers_, numStyles);
1370
+ this.clusterIcon_.setCenter(this.center_);
1371
+ this.clusterIcon_.setSums(sums);
1372
+ this.clusterIcon_.show();
1373
+ };
1374
+
1375
+
1376
+ /**
1377
+ * A cluster icon
1378
+ *
1379
+ * @param {Cluster} cluster The cluster to be associated with.
1380
+ * @param {Object} styles An object that has style properties:
1381
+ * 'url': (string) The image url.
1382
+ * 'height': (number) The image height.
1383
+ * 'width': (number) The image width.
1384
+ * 'anchor': (Array) The anchor position of the label text.
1385
+ * 'textColor': (string) The text color.
1386
+ * 'textSize': (number) The text size.
1387
+ * 'backgroundPosition: (string) The background postition x, y.
1388
+ * @param {number=} opt_padding Optional padding to apply to the cluster icon.
1389
+ * @constructor
1390
+ * @extends google.maps.OverlayView
1391
+ * @ignore
1392
+ */
1393
+ function ClusterIcon(cluster, styles, opt_padding) {
1394
+ cluster.getMarkerClusterer().extend(ClusterIcon, google.maps.OverlayView);
1395
+
1396
+ this.styles_ = styles;
1397
+ this.padding_ = opt_padding || 0;
1398
+ this.cluster_ = cluster;
1399
+ this.center_ = null;
1400
+ this.map_ = cluster.getMap();
1401
+ this.div_ = null;
1402
+ this.sums_ = null;
1403
+ this.visible_ = false;
1404
+
1405
+ this.setMap(this.map_);
1406
+ }
1407
+
1408
+
1409
+ /**
1410
+ * Triggers the clusterclick event and zoom's if the option is set.
1411
+ */
1412
+ ClusterIcon.prototype.triggerClusterClick = function() {
1413
+ var markerClusterer = this.cluster_.getMarkerClusterer();
1414
+
1415
+ // Trigger the clusterclick event.
1416
+ google.maps.event.trigger(markerClusterer, 'clusterclick', this.cluster_);
1417
+
1418
+ if (markerClusterer.isZoomOnClick()) {
1419
+ // Zoom into the cluster.
1420
+ this.map_.fitBounds(this.cluster_.getBounds());
1421
+ }
1422
+ };
1423
+
1424
+
1425
+ /**
1426
+ * Adding the cluster icon to the dom.
1427
+ * @ignore
1428
+ */
1429
+ ClusterIcon.prototype.onAdd = function() {
1430
+ this.div_ = document.createElement('DIV');
1431
+ if (this.visible_) {
1432
+ var pos = this.getPosFromLatLng_(this.center_);
1433
+ this.div_.style.cssText = this.createCss(pos);
1434
+ this.div_.innerHTML = this.sums_.text;
1435
+ }
1436
+
1437
+ var panes = this.getPanes();
1438
+ panes.overlayMouseTarget.appendChild(this.div_);
1439
+
1440
+ var that = this;
1441
+ google.maps.event.addDomListener(this.div_, 'click', function() {
1442
+ that.triggerClusterClick();
1443
+ });
1444
+ };
1445
+
1446
+
1447
+ /**
1448
+ * Returns the position to place the div dending on the latlng.
1449
+ *
1450
+ * @param {google.maps.LatLng} latlng The position in latlng.
1451
+ * @return {google.maps.Point} The position in pixels.
1452
+ * @private
1453
+ */
1454
+ ClusterIcon.prototype.getPosFromLatLng_ = function(latlng) {
1455
+ var pos = this.getProjection().fromLatLngToDivPixel(latlng);
1456
+ pos.x -= parseInt(this.width_ / 2, 10);
1457
+ pos.y -= parseInt(this.height_ / 2, 10);
1458
+ return pos;
1459
+ };
1460
+
1461
+
1462
+ /**
1463
+ * Draw the icon.
1464
+ * @ignore
1465
+ */
1466
+ ClusterIcon.prototype.draw = function() {
1467
+ if (this.visible_) {
1468
+ var pos = this.getPosFromLatLng_(this.center_);
1469
+ this.div_.style.top = pos.y + 'px';
1470
+ this.div_.style.left = pos.x + 'px';
1471
+ }
1472
+ };
1473
+
1474
+
1475
+ /**
1476
+ * Hide the icon.
1477
+ */
1478
+ ClusterIcon.prototype.hide = function() {
1479
+ if (this.div_) {
1480
+ this.div_.style.display = 'none';
1481
+ }
1482
+ this.visible_ = false;
1483
+ };
1484
+
1485
+
1486
+ /**
1487
+ * Position and show the icon.
1488
+ */
1489
+ ClusterIcon.prototype.show = function() {
1490
+ if (this.div_) {
1491
+ var pos = this.getPosFromLatLng_(this.center_);
1492
+ this.div_.style.cssText = this.createCss(pos);
1493
+ this.div_.style.display = '';
1494
+ }
1495
+ this.visible_ = true;
1496
+ };
1497
+
1498
+
1499
+ /**
1500
+ * Remove the icon from the map
1501
+ */
1502
+ ClusterIcon.prototype.remove = function() {
1503
+ this.setMap(null);
1504
+ };
1505
+
1506
+
1507
+ /**
1508
+ * Implementation of the onRemove interface.
1509
+ * @ignore
1510
+ */
1511
+ ClusterIcon.prototype.onRemove = function() {
1512
+ if (this.div_ && this.div_.parentNode) {
1513
+ this.hide();
1514
+ this.div_.parentNode.removeChild(this.div_);
1515
+ this.div_ = null;
1516
+ }
1517
+ };
1518
+
1519
+
1520
+ /**
1521
+ * Set the sums of the icon.
1522
+ *
1523
+ * @param {Object} sums The sums containing:
1524
+ * 'text': (string) The text to display in the icon.
1525
+ * 'index': (number) The style index of the icon.
1526
+ */
1527
+ ClusterIcon.prototype.setSums = function(sums) {
1528
+ this.sums_ = sums;
1529
+ this.text_ = sums.text;
1530
+ this.index_ = sums.index;
1531
+ if (this.div_) {
1532
+ this.div_.innerHTML = sums.text;
1533
+ }
1534
+
1535
+ this.useStyle();
1536
+ };
1537
+
1538
+
1539
+ /**
1540
+ * Sets the icon to the the styles.
1541
+ */
1542
+ ClusterIcon.prototype.useStyle = function() {
1543
+ var index = Math.max(0, this.sums_.index - 1);
1544
+ index = Math.min(this.styles_.length - 1, index);
1545
+ var style = this.styles_[index];
1546
+ this.url_ = style['url'];
1547
+ this.height_ = style['height'];
1548
+ this.width_ = style['width'];
1549
+ this.textColor_ = style['textColor'];
1550
+ this.anchor_ = style['anchor'];
1551
+ this.textSize_ = style['textSize'];
1552
+ this.backgroundPosition_ = style['backgroundPosition'];
1553
+ };
1554
+
1555
+
1556
+ /**
1557
+ * Sets the center of the icon.
1558
+ *
1559
+ * @param {google.maps.LatLng} center The latlng to set as the center.
1560
+ */
1561
+ ClusterIcon.prototype.setCenter = function(center) {
1562
+ this.center_ = center;
1563
+ };
1564
+
1565
+
1566
+ /**
1567
+ * Create the css text based on the position of the icon.
1568
+ *
1569
+ * @param {google.maps.Point} pos The position.
1570
+ * @return {string} The css style text.
1571
+ */
1572
+ ClusterIcon.prototype.createCss = function(pos) {
1573
+ var style = [];
1574
+ style.push('background-image:url(' + this.url_ + ');');
1575
+ var backgroundPosition = this.backgroundPosition_ ? this.backgroundPosition_ : '0 0';
1576
+ style.push('background-position:' + backgroundPosition + ';');
1577
+
1578
+ if (typeof this.anchor_ === 'object') {
1579
+ if (typeof this.anchor_[0] === 'number' && this.anchor_[0] > 0 &&
1580
+ this.anchor_[0] < this.height_) {
1581
+ style.push('height:' + (this.height_ - this.anchor_[0]) +
1582
+ 'px; padding-top:' + this.anchor_[0] + 'px;');
1583
+ } else {
1584
+ style.push('height:' + this.height_ + 'px; line-height:' + this.height_ +
1585
+ 'px;');
1586
+ }
1587
+ if (typeof this.anchor_[1] === 'number' && this.anchor_[1] > 0 &&
1588
+ this.anchor_[1] < this.width_) {
1589
+ style.push('width:' + (this.width_ - this.anchor_[1]) +
1590
+ 'px; padding-left:' + this.anchor_[1] + 'px;');
1591
+ } else {
1592
+ style.push('width:' + this.width_ + 'px; text-align:center;');
1593
+ }
1594
+ } else {
1595
+ style.push('height:' + this.height_ + 'px; line-height:' +
1596
+ this.height_ + 'px; width:' + this.width_ + 'px; text-align:center;');
1597
+ }
1598
+
1599
+ var txtColor = this.textColor_ ? this.textColor_ : 'black';
1600
+ var txtSize = this.textSize_ ? this.textSize_ : 11;
1601
+
1602
+ style.push('cursor:pointer; top:' + pos.y + 'px; left:' +
1603
+ pos.x + 'px; color:' + txtColor + '; position:absolute; font-size:' +
1604
+ txtSize + 'px; font-family:Arial,sans-serif; font-weight:bold');
1605
+ return style.join('');
1606
+ };
1607
+
1608
+ // Generated by CoffeeScript 1.12.2
1609
+
1610
+ /** @preserve OverlappingMarkerSpiderfier
1611
+ https://github.com/jawj/OverlappingMarkerSpiderfier
1612
+ Copyright (c) 2011 - 2017 George MacKerron
1613
+ Released under the MIT licence: http://opensource.org/licenses/mit-license
1614
+ Note: The Google Maps API v3 must be included *before* this code
1615
+ */
1616
+
1617
+ (function() {
1618
+ var callbackName, callbackRegEx, ref, ref1, scriptTag, tag,
1619
+ hasProp = {}.hasOwnProperty,
1620
+ slice = [].slice;
1621
+
1622
+ this['OverlappingMarkerSpiderfier'] = (function() {
1623
+ var ge, gm, j, len, mt, p, ref, twoPi, x;
1624
+
1625
+ p = _Class.prototype;
1626
+
1627
+ ref = [_Class, p];
1628
+ for (j = 0, len = ref.length; j < len; j++) {
1629
+ x = ref[j];
1630
+ x['VERSION'] = '1.0.3';
1631
+ }
1632
+
1633
+ twoPi = Math.PI * 2;
1634
+
1635
+ gm = ge = mt = null;
1636
+
1637
+ _Class['markerStatus'] = {
1638
+ 'SPIDERFIED': 'SPIDERFIED',
1639
+ 'SPIDERFIABLE': 'SPIDERFIABLE',
1640
+ 'UNSPIDERFIABLE': 'UNSPIDERFIABLE',
1641
+ 'UNSPIDERFIED': 'UNSPIDERFIED'
1642
+ };
1643
+
1644
+ function _Class(map1, opts) {
1645
+ var k, lcH, lcU, v;
1646
+ this.map = map1;
1647
+ if (opts == null) {
1648
+ opts = {};
1649
+ }
1650
+ if (this.constructor.hasInitialized == null) {
1651
+ this.constructor.hasInitialized = true;
1652
+ gm = google.maps;
1653
+ ge = gm.event;
1654
+ mt = gm.MapTypeId;
1655
+ p['keepSpiderfied'] = false;
1656
+ p['ignoreMapClick'] = false;
1657
+ p['markersWontHide'] = false;
1658
+ p['markersWontMove'] = false;
1659
+ p['basicFormatEvents'] = false;
1660
+ p['nearbyDistance'] = 20;
1661
+ p['circleSpiralSwitchover'] = 9;
1662
+ p['circleFootSeparation'] = 23;
1663
+ p['circleStartAngle'] = twoPi / 12;
1664
+ p['spiralFootSeparation'] = 26;
1665
+ p['spiralLengthStart'] = 11;
1666
+ p['spiralLengthFactor'] = 4;
1667
+ p['spiderfiedZIndex'] = gm.Marker.MAX_ZINDEX + 20000;
1668
+ p['highlightedLegZIndex'] = gm.Marker.MAX_ZINDEX + 10000;
1669
+ p['usualLegZIndex'] = gm.Marker.MAX_ZINDEX + 1;
1670
+ p['legWeight'] = 1.5;
1671
+ p['legColors'] = {
1672
+ 'usual': {},
1673
+ 'highlighted': {}
1674
+ };
1675
+ lcU = p['legColors']['usual'];
1676
+ lcH = p['legColors']['highlighted'];
1677
+ lcU[mt.HYBRID] = lcU[mt.SATELLITE] = '#fff';
1678
+ lcH[mt.HYBRID] = lcH[mt.SATELLITE] = '#f00';
1679
+ lcU[mt.TERRAIN] = lcU[mt.ROADMAP] = '#444';
1680
+ lcH[mt.TERRAIN] = lcH[mt.ROADMAP] = '#f00';
1681
+ this.constructor.ProjHelper = function(map) {
1682
+ return this.setMap(map);
1683
+ };
1684
+ this.constructor.ProjHelper.prototype = new gm.OverlayView();
1685
+ this.constructor.ProjHelper.prototype['draw'] = function() {};
1686
+ }
1687
+ for (k in opts) {
1688
+ if (!hasProp.call(opts, k)) continue;
1689
+ v = opts[k];
1690
+ this[k] = v;
1691
+ }
1692
+ this.projHelper = new this.constructor.ProjHelper(this.map);
1693
+ this.initMarkerArrays();
1694
+ this.listeners = {};
1695
+ this.formatIdleListener = this.formatTimeoutId = null;
1696
+ this.addListener('click', function(marker, e) {
1697
+ return ge.trigger(marker, 'spider_click', e);
1698
+ });
1699
+ this.addListener('format', function(marker, status) {
1700
+ return ge.trigger(marker, 'spider_format', status);
1701
+ });
1702
+ if (!this['ignoreMapClick']) {
1703
+ ge.addListener(this.map, 'click', (function(_this) {
1704
+ return function() {
1705
+ return _this['unspiderfy']();
1706
+ };
1707
+ })(this));
1708
+ }
1709
+ ge.addListener(this.map, 'maptypeid_changed', (function(_this) {
1710
+ return function() {
1711
+ return _this['unspiderfy']();
1712
+ };
1713
+ })(this));
1714
+ ge.addListener(this.map, 'zoom_changed', (function(_this) {
1715
+ return function() {
1716
+ _this['unspiderfy']();
1717
+ if (!_this['basicFormatEvents']) {
1718
+ return _this.formatMarkers();
1719
+ }
1720
+ };
1721
+ })(this));
1722
+ }
1723
+
1724
+ p.initMarkerArrays = function() {
1725
+ this.markers = [];
1726
+ return this.markerListenerRefs = [];
1727
+ };
1728
+
1729
+ p['addMarker'] = function(marker, spiderClickHandler) {
1730
+ marker.setMap(this.map);
1731
+ return this['trackMarker'](marker, spiderClickHandler);
1732
+ };
1733
+
1734
+ p['trackMarker'] = function(marker, spiderClickHandler) {
1735
+ var listenerRefs;
1736
+ if (marker['_oms'] != null) {
1737
+ return this;
1738
+ }
1739
+ marker['_oms'] = true;
1740
+ listenerRefs = [
1741
+ ge.addListener(marker, 'click', (function(_this) {
1742
+ return function(e) {
1743
+ return _this.spiderListener(marker, e);
1744
+ };
1745
+ })(this))
1746
+ ];
1747
+ if (!this['markersWontHide']) {
1748
+ listenerRefs.push(ge.addListener(marker, 'visible_changed', (function(_this) {
1749
+ return function() {
1750
+ return _this.markerChangeListener(marker, false);
1751
+ };
1752
+ })(this)));
1753
+ }
1754
+ if (!this['markersWontMove']) {
1755
+ listenerRefs.push(ge.addListener(marker, 'position_changed', (function(_this) {
1756
+ return function() {
1757
+ return _this.markerChangeListener(marker, true);
1758
+ };
1759
+ })(this)));
1760
+ }
1761
+ if (spiderClickHandler != null) {
1762
+ listenerRefs.push(ge.addListener(marker, 'spider_click', spiderClickHandler));
1763
+ }
1764
+ this.markerListenerRefs.push(listenerRefs);
1765
+ this.markers.push(marker);
1766
+ if (this['basicFormatEvents']) {
1767
+ this.trigger('format', marker, this.constructor['markerStatus']['UNSPIDERFIED']);
1768
+ } else {
1769
+ this.trigger('format', marker, this.constructor['markerStatus']['UNSPIDERFIABLE']);
1770
+ this.formatMarkers();
1771
+ }
1772
+ return this;
1773
+ };
1774
+
1775
+ p.markerChangeListener = function(marker, positionChanged) {
1776
+ if (this.spiderfying || this.unspiderfying) {
1777
+ return;
1778
+ }
1779
+ if ((marker['_omsData'] != null) && (positionChanged || !marker.getVisible())) {
1780
+ this['unspiderfy'](positionChanged ? marker : null);
1781
+ }
1782
+ return this.formatMarkers();
1783
+ };
1784
+
1785
+ p['getMarkers'] = function() {
1786
+ return this.markers.slice(0);
1787
+ };
1788
+
1789
+ p['removeMarker'] = function(marker) {
1790
+ this['forgetMarker'](marker);
1791
+ return marker.setMap(null);
1792
+ };
1793
+
1794
+ p['forgetMarker'] = function(marker) {
1795
+ var i, l, len1, listenerRef, listenerRefs;
1796
+ if (marker['_omsData'] != null) {
1797
+ this['unspiderfy']();
1798
+ }
1799
+ i = this.arrIndexOf(this.markers, marker);
1800
+ if (i < 0) {
1801
+ return this;
1802
+ }
1803
+ listenerRefs = this.markerListenerRefs.splice(i, 1)[0];
1804
+ for (l = 0, len1 = listenerRefs.length; l < len1; l++) {
1805
+ listenerRef = listenerRefs[l];
1806
+ ge.removeListener(listenerRef);
1807
+ }
1808
+ delete marker['_oms'];
1809
+ this.markers.splice(i, 1);
1810
+ this.formatMarkers();
1811
+ return this;
1812
+ };
1813
+
1814
+ p['removeAllMarkers'] = p['clearMarkers'] = function() {
1815
+ var l, len1, marker, markers;
1816
+ markers = this['getMarkers']();
1817
+ this['forgetAllMarkers']();
1818
+ for (l = 0, len1 = markers.length; l < len1; l++) {
1819
+ marker = markers[l];
1820
+ marker.setMap(null);
1821
+ }
1822
+ return this;
1823
+ };
1824
+
1825
+ p['forgetAllMarkers'] = function() {
1826
+ var i, l, len1, len2, listenerRef, listenerRefs, marker, n, ref1;
1827
+ this['unspiderfy']();
1828
+ ref1 = this.markers;
1829
+ for (i = l = 0, len1 = ref1.length; l < len1; i = ++l) {
1830
+ marker = ref1[i];
1831
+ listenerRefs = this.markerListenerRefs[i];
1832
+ for (n = 0, len2 = listenerRefs.length; n < len2; n++) {
1833
+ listenerRef = listenerRefs[n];
1834
+ ge.removeListener(listenerRef);
1835
+ }
1836
+ delete marker['_oms'];
1837
+ }
1838
+ this.initMarkerArrays();
1839
+ return this;
1840
+ };
1841
+
1842
+ p['addListener'] = function(eventName, func) {
1843
+ var base;
1844
+ ((base = this.listeners)[eventName] != null ? base[eventName] : base[eventName] = []).push(func);
1845
+ return this;
1846
+ };
1847
+
1848
+ p['removeListener'] = function(eventName, func) {
1849
+ var i;
1850
+ i = this.arrIndexOf(this.listeners[eventName], func);
1851
+ if (!(i < 0)) {
1852
+ this.listeners[eventName].splice(i, 1);
1853
+ }
1854
+ return this;
1855
+ };
1856
+
1857
+ p['clearListeners'] = function(eventName) {
1858
+ this.listeners[eventName] = [];
1859
+ return this;
1860
+ };
1861
+
1862
+ p.trigger = function() {
1863
+ var args, eventName, func, l, len1, ref1, ref2, results;
1864
+ eventName = arguments[0], args = 2 <= arguments.length ? slice.call(arguments, 1) : [];
1865
+ ref2 = (ref1 = this.listeners[eventName]) != null ? ref1 : [];
1866
+ results = [];
1867
+ for (l = 0, len1 = ref2.length; l < len1; l++) {
1868
+ func = ref2[l];
1869
+ results.push(func.apply(null, args));
1870
+ }
1871
+ return results;
1872
+ };
1873
+
1874
+ p.generatePtsCircle = function(count, centerPt) {
1875
+ var angle, angleStep, circumference, i, l, legLength, ref1, results;
1876
+ circumference = this['circleFootSeparation'] * (2 + count);
1877
+ legLength = circumference / twoPi;
1878
+ angleStep = twoPi / count;
1879
+ results = [];
1880
+ for (i = l = 0, ref1 = count; 0 <= ref1 ? l < ref1 : l > ref1; i = 0 <= ref1 ? ++l : --l) {
1881
+ angle = this['circleStartAngle'] + i * angleStep;
1882
+ results.push(new gm.Point(centerPt.x + legLength * Math.cos(angle), centerPt.y + legLength * Math.sin(angle)));
1883
+ }
1884
+ return results;
1885
+ };
1886
+
1887
+ p.generatePtsSpiral = function(count, centerPt) {
1888
+ var angle, i, l, legLength, pt, ref1, results;
1889
+ legLength = this['spiralLengthStart'];
1890
+ angle = 0;
1891
+ results = [];
1892
+ for (i = l = 0, ref1 = count; 0 <= ref1 ? l < ref1 : l > ref1; i = 0 <= ref1 ? ++l : --l) {
1893
+ angle += this['spiralFootSeparation'] / legLength + i * 0.0005;
1894
+ pt = new gm.Point(centerPt.x + legLength * Math.cos(angle), centerPt.y + legLength * Math.sin(angle));
1895
+ legLength += twoPi * this['spiralLengthFactor'] / angle;
1896
+ results.push(pt);
1897
+ }
1898
+ return results;
1899
+ };
1900
+
1901
+ p.spiderListener = function(marker, e) {
1902
+ var l, len1, m, mPt, markerPt, markerSpiderfied, nDist, nearbyMarkerData, nonNearbyMarkers, pxSq, ref1;
1903
+ markerSpiderfied = marker['_omsData'] != null;
1904
+ if (!(markerSpiderfied && this['keepSpiderfied'])) {
1905
+ this['unspiderfy']();
1906
+ }
1907
+ if (markerSpiderfied || this.map.getStreetView().getVisible() || this.map.getMapTypeId() === 'GoogleEarthAPI') {
1908
+ return this.trigger('click', marker, e);
1909
+ } else {
1910
+ nearbyMarkerData = [];
1911
+ nonNearbyMarkers = [];
1912
+ nDist = this['nearbyDistance'];
1913
+ pxSq = nDist * nDist;
1914
+ markerPt = this.llToPt(marker.position);
1915
+ ref1 = this.markers;
1916
+ for (l = 0, len1 = ref1.length; l < len1; l++) {
1917
+ m = ref1[l];
1918
+ if (!((m.map != null) && m.getVisible())) {
1919
+ continue;
1920
+ }
1921
+ mPt = this.llToPt(m.position);
1922
+ if (this.ptDistanceSq(mPt, markerPt) < pxSq) {
1923
+ nearbyMarkerData.push({
1924
+ marker: m,
1925
+ markerPt: mPt
1926
+ });
1927
+ } else {
1928
+ nonNearbyMarkers.push(m);
1929
+ }
1930
+ }
1931
+ if (nearbyMarkerData.length === 1) {
1932
+ return this.trigger('click', marker, e);
1933
+ } else {
1934
+ return this.spiderfy(nearbyMarkerData, nonNearbyMarkers);
1935
+ }
1936
+ }
1937
+ };
1938
+
1939
+ p['markersNearMarker'] = function(marker, firstOnly) {
1940
+ var l, len1, m, mPt, markerPt, markers, nDist, pxSq, ref1, ref2, ref3;
1941
+ if (firstOnly == null) {
1942
+ firstOnly = false;
1943
+ }
1944
+ if (this.projHelper.getProjection() == null) {
1945
+ throw "Must wait for 'idle' event on map before calling markersNearMarker";
1946
+ }
1947
+ nDist = this['nearbyDistance'];
1948
+ pxSq = nDist * nDist;
1949
+ markerPt = this.llToPt(marker.position);
1950
+ markers = [];
1951
+ ref1 = this.markers;
1952
+ for (l = 0, len1 = ref1.length; l < len1; l++) {
1953
+ m = ref1[l];
1954
+ if (m === marker || (m.map == null) || !m.getVisible()) {
1955
+ continue;
1956
+ }
1957
+ mPt = this.llToPt((ref2 = (ref3 = m['_omsData']) != null ? ref3.usualPosition : void 0) != null ? ref2 : m.position);
1958
+ if (this.ptDistanceSq(mPt, markerPt) < pxSq) {
1959
+ markers.push(m);
1960
+ if (firstOnly) {
1961
+ break;
1962
+ }
1963
+ }
1964
+ }
1965
+ return markers;
1966
+ };
1967
+
1968
+ p.markerProximityData = function() {
1969
+ var i1, i2, l, len1, len2, m, m1, m1Data, m2, m2Data, mData, n, nDist, pxSq, ref1, ref2;
1970
+ if (this.projHelper.getProjection() == null) {
1971
+ throw "Must wait for 'idle' event on map before calling markersNearAnyOtherMarker";
1972
+ }
1973
+ nDist = this['nearbyDistance'];
1974
+ pxSq = nDist * nDist;
1975
+ mData = (function() {
1976
+ var l, len1, ref1, ref2, ref3, results;
1977
+ ref1 = this.markers;
1978
+ results = [];
1979
+ for (l = 0, len1 = ref1.length; l < len1; l++) {
1980
+ m = ref1[l];
1981
+ results.push({
1982
+ pt: this.llToPt((ref2 = (ref3 = m['_omsData']) != null ? ref3.usualPosition : void 0) != null ? ref2 : m.position),
1983
+ willSpiderfy: false
1984
+ });
1985
+ }
1986
+ return results;
1987
+ }).call(this);
1988
+ ref1 = this.markers;
1989
+ for (i1 = l = 0, len1 = ref1.length; l < len1; i1 = ++l) {
1990
+ m1 = ref1[i1];
1991
+ if (!((m1.getMap() != null) && m1.getVisible())) {
1992
+ continue;
1993
+ }
1994
+ m1Data = mData[i1];
1995
+ if (m1Data.willSpiderfy) {
1996
+ continue;
1997
+ }
1998
+ ref2 = this.markers;
1999
+ for (i2 = n = 0, len2 = ref2.length; n < len2; i2 = ++n) {
2000
+ m2 = ref2[i2];
2001
+ if (i2 === i1) {
2002
+ continue;
2003
+ }
2004
+ if (!((m2.getMap() != null) && m2.getVisible())) {
2005
+ continue;
2006
+ }
2007
+ m2Data = mData[i2];
2008
+ if (i2 < i1 && !m2Data.willSpiderfy) {
2009
+ continue;
2010
+ }
2011
+ if (this.ptDistanceSq(m1Data.pt, m2Data.pt) < pxSq) {
2012
+ m1Data.willSpiderfy = m2Data.willSpiderfy = true;
2013
+ break;
2014
+ }
2015
+ }
2016
+ }
2017
+ return mData;
2018
+ };
2019
+
2020
+ p['markersNearAnyOtherMarker'] = function() {
2021
+ var i, l, len1, m, mData, ref1, results;
2022
+ mData = this.markerProximityData();
2023
+ ref1 = this.markers;
2024
+ results = [];
2025
+ for (i = l = 0, len1 = ref1.length; l < len1; i = ++l) {
2026
+ m = ref1[i];
2027
+ if (mData[i].willSpiderfy) {
2028
+ results.push(m);
2029
+ }
2030
+ }
2031
+ return results;
2032
+ };
2033
+
2034
+ p.setImmediate = function(func) {
2035
+ return window.setTimeout(func, 0);
2036
+ };
2037
+
2038
+ p.formatMarkers = function() {
2039
+ if (this['basicFormatEvents']) {
2040
+ return;
2041
+ }
2042
+ if (this.formatTimeoutId != null) {
2043
+ return;
2044
+ }
2045
+ return this.formatTimeoutId = this.setImmediate((function(_this) {
2046
+ return function() {
2047
+ _this.formatTimeoutId = null;
2048
+ if (_this.projHelper.getProjection() != null) {
2049
+ return _this._formatMarkers();
2050
+ } else {
2051
+ if (_this.formatIdleListener != null) {
2052
+ return;
2053
+ }
2054
+ return _this.formatIdleListener = ge.addListenerOnce(_this.map, 'idle', function() {
2055
+ return _this._formatMarkers();
2056
+ });
2057
+ }
2058
+ };
2059
+ })(this));
2060
+ };
2061
+
2062
+ p._formatMarkers = function() {
2063
+ var i, l, len1, len2, marker, n, proximities, ref1, results, results1, status;
2064
+ if (this['basicFormatEvents']) {
2065
+ results = [];
2066
+ for (l = 0, len1 = markers.length; l < len1; l++) {
2067
+ marker = markers[l];
2068
+ status = marker['_omsData'] != null ? 'SPIDERFIED' : 'UNSPIDERFIED';
2069
+ results.push(this.trigger('format', marker, this.constructor['markerStatus'][status]));
2070
+ }
2071
+ return results;
2072
+ } else {
2073
+ proximities = this.markerProximityData();
2074
+ ref1 = this.markers;
2075
+ results1 = [];
2076
+ for (i = n = 0, len2 = ref1.length; n < len2; i = ++n) {
2077
+ marker = ref1[i];
2078
+ status = marker['_omsData'] != null ? 'SPIDERFIED' : proximities[i].willSpiderfy ? 'SPIDERFIABLE' : 'UNSPIDERFIABLE';
2079
+ results1.push(this.trigger('format', marker, this.constructor['markerStatus'][status]));
2080
+ }
2081
+ return results1;
2082
+ }
2083
+ };
2084
+
2085
+ p.makeHighlightListenerFuncs = function(marker) {
2086
+ return {
2087
+ highlight: (function(_this) {
2088
+ return function() {
2089
+ return marker['_omsData'].leg.setOptions({
2090
+ strokeColor: _this['legColors']['highlighted'][_this.map.mapTypeId],
2091
+ zIndex: _this['highlightedLegZIndex']
2092
+ });
2093
+ };
2094
+ })(this),
2095
+ unhighlight: (function(_this) {
2096
+ return function() {
2097
+ return marker['_omsData'].leg.setOptions({
2098
+ strokeColor: _this['legColors']['usual'][_this.map.mapTypeId],
2099
+ zIndex: _this['usualLegZIndex']
2100
+ });
2101
+ };
2102
+ })(this)
2103
+ };
2104
+ };
2105
+
2106
+ p.spiderfy = function(markerData, nonNearbyMarkers) {
2107
+ var bodyPt, footLl, footPt, footPts, highlightListenerFuncs, leg, marker, md, nearestMarkerDatum, numFeet, spiderfiedMarkers;
2108
+ this.spiderfying = true;
2109
+ numFeet = markerData.length;
2110
+ bodyPt = this.ptAverage((function() {
2111
+ var l, len1, results;
2112
+ results = [];
2113
+ for (l = 0, len1 = markerData.length; l < len1; l++) {
2114
+ md = markerData[l];
2115
+ results.push(md.markerPt);
2116
+ }
2117
+ return results;
2118
+ })());
2119
+ footPts = numFeet >= this['circleSpiralSwitchover'] ? this.generatePtsSpiral(numFeet, bodyPt).reverse() : this.generatePtsCircle(numFeet, bodyPt);
2120
+ spiderfiedMarkers = (function() {
2121
+ var l, len1, results;
2122
+ results = [];
2123
+ for (l = 0, len1 = footPts.length; l < len1; l++) {
2124
+ footPt = footPts[l];
2125
+ footLl = this.ptToLl(footPt);
2126
+ nearestMarkerDatum = this.minExtract(markerData, (function(_this) {
2127
+ return function(md) {
2128
+ return _this.ptDistanceSq(md.markerPt, footPt);
2129
+ };
2130
+ })(this));
2131
+ marker = nearestMarkerDatum.marker;
2132
+ leg = new gm.Polyline({
2133
+ map: this.map,
2134
+ path: [marker.position, footLl],
2135
+ strokeColor: this['legColors']['usual'][this.map.mapTypeId],
2136
+ strokeWeight: this['legWeight'],
2137
+ zIndex: this['usualLegZIndex']
2138
+ });
2139
+ marker['_omsData'] = {
2140
+ usualPosition: marker.getPosition(),
2141
+ usualZIndex: marker.getZIndex(),
2142
+ leg: leg
2143
+ };
2144
+ if (this['legColors']['highlighted'][this.map.mapTypeId] !== this['legColors']['usual'][this.map.mapTypeId]) {
2145
+ highlightListenerFuncs = this.makeHighlightListenerFuncs(marker);
2146
+ marker['_omsData'].hightlightListeners = {
2147
+ highlight: ge.addListener(marker, 'mouseover', highlightListenerFuncs.highlight),
2148
+ unhighlight: ge.addListener(marker, 'mouseout', highlightListenerFuncs.unhighlight)
2149
+ };
2150
+ }
2151
+ this.trigger('format', marker, this.constructor['markerStatus']['SPIDERFIED']);
2152
+ marker.setPosition(footLl);
2153
+ marker.setZIndex(Math.round(this['spiderfiedZIndex'] + footPt.y));
2154
+ results.push(marker);
2155
+ }
2156
+ return results;
2157
+ }).call(this);
2158
+ delete this.spiderfying;
2159
+ this.spiderfied = true;
2160
+ return this.trigger('spiderfy', spiderfiedMarkers, nonNearbyMarkers);
2161
+ };
2162
+
2163
+ p['unspiderfy'] = function(markerNotToMove) {
2164
+ var l, len1, listeners, marker, nonNearbyMarkers, ref1, status, unspiderfiedMarkers;
2165
+ if (markerNotToMove == null) {
2166
+ markerNotToMove = null;
2167
+ }
2168
+ if (this.spiderfied == null) {
2169
+ return this;
2170
+ }
2171
+ this.unspiderfying = true;
2172
+ unspiderfiedMarkers = [];
2173
+ nonNearbyMarkers = [];
2174
+ ref1 = this.markers;
2175
+ for (l = 0, len1 = ref1.length; l < len1; l++) {
2176
+ marker = ref1[l];
2177
+ if (marker['_omsData'] != null) {
2178
+ marker['_omsData'].leg.setMap(null);
2179
+ if (marker !== markerNotToMove) {
2180
+ marker.setPosition(marker['_omsData'].usualPosition);
2181
+ }
2182
+ marker.setZIndex(marker['_omsData'].usualZIndex);
2183
+ listeners = marker['_omsData'].hightlightListeners;
2184
+ if (listeners != null) {
2185
+ ge.removeListener(listeners.highlight);
2186
+ ge.removeListener(listeners.unhighlight);
2187
+ }
2188
+ delete marker['_omsData'];
2189
+ if (marker !== markerNotToMove) {
2190
+ status = this['basicFormatEvents'] ? 'UNSPIDERFIED' : 'SPIDERFIABLE';
2191
+ this.trigger('format', marker, this.constructor['markerStatus'][status]);
2192
+ }
2193
+ unspiderfiedMarkers.push(marker);
2194
+ } else {
2195
+ nonNearbyMarkers.push(marker);
2196
+ }
2197
+ }
2198
+ delete this.unspiderfying;
2199
+ delete this.spiderfied;
2200
+ this.trigger('unspiderfy', unspiderfiedMarkers, nonNearbyMarkers);
2201
+ return this;
2202
+ };
2203
+
2204
+ p.ptDistanceSq = function(pt1, pt2) {
2205
+ var dx, dy;
2206
+ dx = pt1.x - pt2.x;
2207
+ dy = pt1.y - pt2.y;
2208
+ return dx * dx + dy * dy;
2209
+ };
2210
+
2211
+ p.ptAverage = function(pts) {
2212
+ var l, len1, numPts, pt, sumX, sumY;
2213
+ sumX = sumY = 0;
2214
+ for (l = 0, len1 = pts.length; l < len1; l++) {
2215
+ pt = pts[l];
2216
+ sumX += pt.x;
2217
+ sumY += pt.y;
2218
+ }
2219
+ numPts = pts.length;
2220
+ return new gm.Point(sumX / numPts, sumY / numPts);
2221
+ };
2222
+
2223
+ p.llToPt = function(ll) {
2224
+ return this.projHelper.getProjection().fromLatLngToDivPixel(ll);
2225
+ };
2226
+
2227
+ p.ptToLl = function(pt) {
2228
+ return this.projHelper.getProjection().fromDivPixelToLatLng(pt);
2229
+ };
2230
+
2231
+ p.minExtract = function(set, func) {
2232
+ var bestIndex, bestVal, index, item, l, len1, val;
2233
+ for (index = l = 0, len1 = set.length; l < len1; index = ++l) {
2234
+ item = set[index];
2235
+ val = func(item);
2236
+ if ((typeof bestIndex === "undefined" || bestIndex === null) || val < bestVal) {
2237
+ bestVal = val;
2238
+ bestIndex = index;
2239
+ }
2240
+ }
2241
+ return set.splice(bestIndex, 1)[0];
2242
+ };
2243
+
2244
+ p.arrIndexOf = function(arr, obj) {
2245
+ var i, l, len1, o;
2246
+ if (arr.indexOf != null) {
2247
+ return arr.indexOf(obj);
2248
+ }
2249
+ for (i = l = 0, len1 = arr.length; l < len1; i = ++l) {
2250
+ o = arr[i];
2251
+ if (o === obj) {
2252
+ return i;
2253
+ }
2254
+ }
2255
+ return -1;
2256
+ };
2257
+
2258
+ return _Class;
2259
+
2260
+ })();
2261
+
2262
+ callbackRegEx = /(\?.*(&|&amp;)|\?)spiderfier_callback=(\w+)/;
2263
+
2264
+ scriptTag = document.currentScript;
2265
+
2266
+ if (scriptTag == null) {
2267
+ scriptTag = ((function() {
2268
+ var j, len, ref, ref1, results;
2269
+ ref = document.getElementsByTagName('script');
2270
+ results = [];
2271
+ for (j = 0, len = ref.length; j < len; j++) {
2272
+ tag = ref[j];
2273
+ if ((ref1 = tag.getAttribute('src')) != null ? ref1.match(callbackRegEx) : void 0) {
2274
+ results.push(tag);
2275
+ }
2276
+ }
2277
+ return results;
2278
+ })())[0];
2279
+ }
2280
+
2281
+ if (scriptTag != null) {
2282
+ callbackName = (ref = scriptTag.getAttribute('src')) != null ? (ref1 = ref.match(callbackRegEx)) != null ? ref1[3] : void 0 : void 0;
2283
+ if (callbackName) {
2284
+ if (typeof window[callbackName] === "function") {
2285
+ window[callbackName]();
2286
+ }
2287
+ }
2288
+ }
2289
+
2290
+ if (typeof window['spiderfier_callback'] === "function") {
2291
+ window['spiderfier_callback']();
2292
+ }
2293
+
2294
+ }).call(this);