source: trunk/src/map/inc/markerClusterer.js@ 7833

Last change on this file since 7833 was 7833, checked in by ddboer, 15 years ago

Clusters now get a red highlight behind them

File size: 21.8 KB
Line 
1/**
2 * @name MarkerClusterer
3 * @version 1.0
4 * @author Xiaoxi Wu
5 * @copyright (c) 2009 Xiaoxi Wu
6 * @fileoverview
7 * This javascript library creates and manages per-zoom-level
8 * clusters for large amounts of markers (hundreds or thousands).
9 * This library was inspired by the <a href="http://www.maptimize.com">
10 * Maptimize</a> hosted clustering solution.
11 * <br /><br/>
12 * <b>How it works</b>:<br/>
13 * The <code>MarkerClusterer</code> will group markers into clusters according to
14 * their distance from a cluster's center. When a marker is added,
15 * the marker cluster will find a position in all the clusters, and
16 * if it fails to find one, it will create a new cluster with the marker.
17 * The number of markers in a cluster will be displayed
18 * on the cluster marker. When the map viewport changes,
19 * <code>MarkerClusterer</code> will destroy the clusters in the viewport
20 * and regroup them into new clusters.
21 *
22 */
23
24/*
25 * Licensed under the Apache License, Version 2.0 (the "License");
26 * you may not use this file except in compliance with the License.
27 * You may obtain a copy of the License at
28 *
29 * http://www.apache.org/licenses/LICENSE-2.0
30 *
31 * Unless required by applicable law or agreed to in writing, software
32 * distributed under the License is distributed on an "AS IS" BASIS,
33 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
34 * See the License for the specific language governing permissions and
35 * limitations under the License.
36 */
37
38
39/**
40 * @name MarkerClustererOptions
41 * @class This class represents optional arguments to the {@link MarkerClusterer}
42 * constructor.
43 * @property {Number} [maxZoom] The max zoom level monitored by a
44 * marker cluster. If not given, the marker cluster assumes the maximum map
45 * zoom level. When maxZoom is reached or exceeded all markers will be shown
46 * without cluster.
47 * @property {Number} [gridSize=60] The grid size of a cluster in pixel. Each
48 * cluster will be a square. If you want the algorithm to run faster, you can set
49 * this value larger.
50 * @property {Array of MarkerStyleOptions} [styles]
51 * Custom styles for the cluster markers.
52 * The array should be ordered according to increasing cluster size,
53 * with the style for the smallest clusters first, and the style for the
54 * largest clusters last.
55 */
56
57/**
58 * @name MarkerStyleOptions
59 * @class An array of these is passed into the {@link MarkerClustererOptions}
60 * styles option.
61 * @property {String} [url] Image url.
62 * @property {Number} [height] Image height.
63 * @property {Number} [height] Image width.
64 * @property {Array of Number} [opt_anchor] Anchor for label text, like [24, 12].
65 * If not set, the text will align center and middle.
66 * @property {String} [opt_textColor="black"] Text color.
67 */
68
69/**
70 * Creates a new MarkerClusterer to cluster markers on the map.
71 *
72 * @constructor
73 * @param {GMap2} map The map that the markers should be added to.
74 * @param {Array of GMarker} opt_markers Initial set of markers to be clustered.
75 * @param {MarkerClustererOptions} opt_opts A container for optional arguments.
76 */
77function MarkerClusterer(map, opt_markers, opt_opts) {
78 // private members
79 var clusters_ = [];
80 var map_ = map;
81 var maxZoom_ = 15;
82 var me_ = this;
83 var gridSize_ = 40;
84 var sizes = [53, 56, 66, 78, 90];
85 var styles_ = [];
86 var leftMarkers_ = [];
87 var mcfn_ = null;
88
89 var i = 0;
90 for (i = 1; i <= 5; ++i) {
91 styles_.push({
92 'url': "http://gmaps-utility-library.googlecode.com/svn/trunk/markerclusterer/images/m" + i + ".png",
93 'height': sizes[i - 1],
94 'width': sizes[i - 1]
95 });
96 }
97
98 if (typeof opt_opts === "object" && opt_opts !== null) {
99 if (typeof opt_opts.gridSize === "number" && opt_opts.gridSize > 0) {
100 gridSize_ = opt_opts.gridSize;
101 }
102 if (typeof opt_opts.maxZoom === "number") {
103 maxZoom_ = opt_opts.maxZoom;
104 }
105 if (typeof opt_opts.styles === "object" && opt_opts.styles !== null && opt_opts.styles.length !== 0) {
106 styles_ = opt_opts.styles;
107 }
108 }
109
110 /**
111 * When we add a marker, the marker may not in the viewport of map, then we don't deal with it, instead
112 * we add the marker into a array called leftMarkers_. When we reset MarkerClusterer we should add the
113 * leftMarkers_ into MarkerClusterer.
114 */
115 function addLeftMarkers_() {
116 if (leftMarkers_.length === 0) {
117 return;
118 }
119 var leftMarkers = [];
120 for (i = 0; i < leftMarkers_.length; ++i) {
121 me_.addMarker(leftMarkers_[i], true, null, null, true);
122 }
123 leftMarkers_ = leftMarkers;
124 }
125
126 /**
127 * Get cluster marker images of this marker cluster. Mostly used by {@link Cluster}
128 * @private
129 * @return {Array of String}
130 */
131 this.getStyles_ = function () {
132 return styles_;
133 };
134
135 /**
136 * Remove all markers from MarkerClusterer.
137 */
138 this.clearMarkers = function () {
139 for (var i = 0; i < clusters_.length; ++i) {
140 if (typeof clusters_[i] !== "undefined" && clusters_[i] !== null) {
141 clusters_[i].clearMarkers();
142 }
143 }
144 clusters_ = [];
145 leftMarkers_ = [];
146 GEvent.removeListener(mcfn_);
147 };
148
149 /**
150 * Check a marker, whether it is in current map viewport.
151 * @private
152 * @return {Boolean} if it is in current map viewport
153 */
154 function isMarkerInViewport_(marker) {
155 return map_.getBounds().containsLatLng(marker.getLatLng());
156 }
157
158 /**
159 * When reset MarkerClusterer, there will be some markers get out of its cluster.
160 * These markers should be add to new clusters.
161 * @param {Array of GMarker} markers Markers to add.
162 */
163 function reAddMarkers_(markers) {
164 var len = markers.length;
165 var clusters = [];
166 for (var i = len - 1; i >= 0; --i) {
167 me_.addMarker(markers[i].marker, true, markers[i].isAdded, clusters, true);
168 }
169 addLeftMarkers_();
170 }
171
172 /**
173 * Add a marker.
174 * @private
175 * @param {GMarker} marker Marker you want to add
176 * @param {Boolean} opt_isNodraw Whether redraw the cluster contained the marker
177 * @param {Boolean} opt_isAdded Whether the marker is added to map. Never use it.
178 * @param {Array of Cluster} opt_clusters Provide a list of clusters, the marker
179 * cluster will only check these cluster where the marker should join.
180 */
181 this.addMarker = function (marker, opt_isNodraw, opt_isAdded, opt_clusters, opt_isNoCheck) {
182 if (opt_isNoCheck !== true) {
183 if (!isMarkerInViewport_(marker)) {
184 leftMarkers_.push(marker);
185 return;
186 }
187 }
188
189 var isAdded = opt_isAdded;
190 var clusters = opt_clusters;
191 var pos = map_.fromLatLngToDivPixel(marker.getLatLng());
192
193 if (typeof isAdded !== "boolean") {
194 isAdded = false;
195 }
196 if (typeof clusters !== "object" || clusters === null) {
197 clusters = clusters_;
198 }
199
200 var length = clusters.length;
201 var cluster = null;
202 for (var i = length - 1; i >= 0; i--) {
203 cluster = clusters[i];
204 var center = cluster.getCenter();
205 if (center === null) {
206 continue;
207 }
208 center = map_.fromLatLngToDivPixel(center);
209
210 // Found a cluster which contains the marker.
211 if (pos.x >= center.x - gridSize_ && pos.x <= center.x + gridSize_ &&
212 pos.y >= center.y - gridSize_ && pos.y <= center.y + gridSize_) {
213 cluster.addMarker({
214 'isAdded': isAdded,
215 'marker': marker
216 });
217 if (!opt_isNodraw) {
218 cluster.redraw_();
219 }
220 return;
221 }
222 }
223
224 // No cluster contain the marker, create a new cluster.
225 cluster = new Cluster(this, map);
226 cluster.addMarker({
227 'isAdded': isAdded,
228 'marker': marker
229 });
230 if (!opt_isNodraw) {
231 cluster.redraw_();
232 }
233
234 // Add this cluster both in clusters provided and clusters_
235 clusters.push(cluster);
236 if (clusters !== clusters_) {
237 clusters_.push(cluster);
238 }
239 };
240
241 /**
242 * Remove a marker.
243 *
244 * @param {GMarker} marker The marker you want to remove.
245 */
246
247 this.removeMarker = function (marker) {
248 for (var i = 0; i < clusters_.length; ++i) {
249 if (clusters_[i].remove(marker)) {
250 clusters_[i].redraw_();
251 return;
252 }
253 }
254 };
255
256 /**
257 * Redraw all clusters in viewport.
258 */
259 this.redraw_ = function () {
260 var clusters = this.getClustersInViewport_();
261 for (var i = 0; i < clusters.length; ++i) {
262 clusters[i].redraw_(true);
263 }
264 mapZoomed();
265 };
266
267 /**
268 * Get all clusters in viewport.
269 * @return {Array of Cluster}
270 */
271 this.getClustersInViewport_ = function () {
272 var clusters = [];
273 var curBounds = map_.getBounds();
274 for (var i = 0; i < clusters_.length; i ++) {
275 if (clusters_[i].isInBounds(curBounds)) {
276 clusters.push(clusters_[i]);
277 }
278 }
279 return clusters;
280 };
281
282 /**
283 * Get max zoom level.
284 * @private
285 * @return {Number}
286 */
287 this.getMaxZoom_ = function () {
288 return maxZoom_;
289 };
290
291 /**
292 * Get map object.
293 * @private
294 * @return {GMap2}
295 */
296 this.getMap_ = function () {
297 return map_;
298 };
299
300 /**
301 * Get grid size
302 * @private
303 * @return {Number}
304 */
305 this.getGridSize_ = function () {
306 return gridSize_;
307 };
308
309 /**
310 * Get total number of markers.
311 * @return {Number}
312 */
313 this.getTotalMarkers = function () {
314 var result = 0;
315 for (var i = 0; i < clusters_.length; ++i) {
316 result += clusters_[i].getTotalMarkers();
317 }
318 return result;
319 };
320
321 /**
322 * Get total number of clusters.
323 * @return {int}
324 */
325 this.getTotalClusters = function () {
326 return clusters_.length;
327 };
328
329 /**
330 * Collect all markers of clusters in viewport and regroup them.
331 */
332 this.resetViewport = function () {
333 var clusters = this.getClustersInViewport_();
334 var tmpMarkers = [];
335 var removed = 0;
336
337 for (var i = 0; i < clusters.length; ++i) {
338 var cluster = clusters[i];
339 var oldZoom = cluster.getCurrentZoom();
340 if (oldZoom === null) {
341 continue;
342 }
343 var curZoom = map_.getZoom();
344 if (curZoom !== oldZoom) {
345
346 // If the cluster zoom level changed then destroy the cluster
347 // and collect its markers.
348 var mks = cluster.getMarkers();
349 for (var j = 0; j < mks.length; ++j) {
350 var newMarker = {
351 'isAdded': false,
352 'marker': mks[j].marker
353 };
354 tmpMarkers.push(newMarker);
355 }
356 cluster.clearMarkers();
357 removed++;
358 for (j = 0; j < clusters_.length; ++j) {
359 if (cluster === clusters_[j]) {
360 clusters_.splice(j, 1);
361 }
362 }
363 }
364 }
365
366 // Add the markers collected into marker cluster to reset
367 reAddMarkers_(tmpMarkers);
368 this.redraw_();
369 };
370
371
372 /**
373 * Add a set of markers.
374 *
375 * @param {Array of GMarker} markers The markers you want to add.
376 */
377 this.addMarkers = function (markers) {
378 for (var i = 0; i < markers.length; ++i) {
379 this.addMarker(markers[i], true);
380 }
381 this.redraw_();
382 };
383
384 // initialize
385 if (typeof opt_markers === "object" && opt_markers !== null) {
386 this.addMarkers(opt_markers);
387 }
388
389 // when map move end, regroup.
390 mcfn_ = GEvent.addListener(map_, "moveend", function () {
391 me_.resetViewport();
392 });
393}
394
395/**
396 * Create a cluster to collect markers.
397 * A cluster includes some markers which are in a block of area.
398 * If there are more than one markers in cluster, the cluster
399 * will create a {@link ClusterMarker_} and show the total number
400 * of markers in cluster.
401 *
402 * @constructor
403 * @private
404 * @param {MarkerClusterer} markerClusterer The marker cluster object
405 */
406function Cluster(markerClusterer) {
407 var center_ = null;
408 var markers_ = [];
409 var markerClusterer_ = markerClusterer;
410 var map_ = markerClusterer.getMap_();
411 var clusterMarker_ = null;
412 var zoom_ = map_.getZoom();
413
414 /**
415 * Get markers of this cluster.
416 *
417 * @return {Array of GMarker}
418 */
419 this.getMarkers = function () {
420 return markers_;
421 };
422
423 /**
424 * If this cluster intersects certain bounds.
425 *
426 * @param {GLatLngBounds} bounds A bounds to test
427 * @return {Boolean} Is this cluster intersects the bounds
428 */
429 this.isInBounds = function (bounds) {
430 if (center_ === null) {
431 return false;
432 }
433
434 if (!bounds) {
435 bounds = map_.getBounds();
436 }
437 var sw = map_.fromLatLngToDivPixel(bounds.getSouthWest());
438 var ne = map_.fromLatLngToDivPixel(bounds.getNorthEast());
439
440 var centerxy = map_.fromLatLngToDivPixel(center_);
441 var inViewport = true;
442 var gridSize = markerClusterer.getGridSize_();
443 if (zoom_ !== map_.getZoom()) {
444 var dl = map_.getZoom() - zoom_;
445 gridSize = Math.pow(2, dl) * gridSize;
446 }
447 if (ne.x !== sw.x && (centerxy.x + gridSize < sw.x || centerxy.x - gridSize > ne.x)) {
448 inViewport = false;
449 }
450 if (inViewport && (centerxy.y + gridSize < ne.y || centerxy.y - gridSize > sw.y)) {
451 inViewport = false;
452 }
453 return inViewport;
454 };
455
456 /**
457 * Get cluster center.
458 *
459 * @return {GLatLng}
460 */
461 this.getCenter = function () {
462 return center_;
463 };
464
465 /**
466 * Add a marker.
467 *
468 * @param {Object} marker An object of marker you want to add:
469 * {Boolean} isAdded If the marker is added on map.
470 * {GMarker} marker The marker you want to add.
471 */
472 this.addMarker = function (marker) {
473 if (center_ === null) {
474 /*var pos = marker['marker'].getLatLng();
475 pos = map.fromLatLngToContainerPixel(pos);
476 pos.x = parseInt(pos.x - pos.x % (GRIDWIDTH * 2) + GRIDWIDTH);
477 pos.y = parseInt(pos.y - pos.y % (GRIDWIDTH * 2) + GRIDWIDTH);
478 center = map.fromContainerPixelToLatLng(pos);*/
479 center_ = marker.marker.getLatLng();
480 }
481 markers_.push(marker);
482 };
483
484 /**
485 * Remove a marker from cluster.
486 *
487 * @param {GMarker} marker The marker you want to remove.
488 * @return {Boolean} Whether find the marker to be removed.
489 */
490 this.removeMarker = function (marker) {
491 for (var i = 0; i < markers_.length; ++i) {
492 if (marker === markers_[i].marker) {
493 if (markers_[i].isAdded) {
494 map_.removeOverlay(markers_[i].marker);
495 }
496 markers_.splice(i, 1);
497 return true;
498 }
499 }
500 return false;
501 };
502
503 /**
504 * Get current zoom level of this cluster.
505 * Note: the cluster zoom level and map zoom level not always the same.
506 *
507 * @return {Number}
508 */
509 this.getCurrentZoom = function () {
510 return zoom_;
511 };
512
513 /**
514 * Redraw a cluster.
515 * @private
516 * @param {Boolean} isForce If redraw by force, no matter if the cluster is
517 * in viewport.
518 */
519 this.redraw_ = function (isForce) {
520 if (!isForce && !this.isInBounds()) {
521 return;
522 }
523
524 // Set cluster zoom level.
525 zoom_ = map_.getZoom();
526 var i = 0;
527 var mz = markerClusterer.getMaxZoom_();
528 if (mz === null) {
529 mz = map_.getCurrentMapType().getMaximumResolution();
530 }
531 if (zoom_ >= mz || this.getTotalMarkers() === 1) {
532
533 // If current zoom level is beyond the max zoom level or the cluster
534 // have only one marker, the marker(s) in cluster will be showed on map.
535 for (i = 0; i < markers_.length; ++i) {
536 if (markers_[i].isAdded) {
537 if (markers_[i].marker.isHidden()) {
538 markers_[i].marker.show();
539 }
540 } else {
541 map_.addOverlay(markers_[i].marker);
542 markers_[i].isAdded = true;
543 }
544 }
545 if (clusterMarker_ !== null) {
546 clusterMarker_.hide();
547 }
548 } else {
549 // Else add a cluster marker on map to show the number of markers in
550 // this cluster.
551 for (i = 0; i < markers_.length; ++i) {
552 if (markers_[i].isAdded && (!markers_[i].marker.isHidden())) {
553 markers_[i].marker.hide();
554 }
555 }
556 if (clusterMarker_ === null) {
557 clusterMarker_ = new ClusterMarker_(center_, this.getTotalMarkers(), markerClusterer_.getStyles_(), markerClusterer_.getGridSize_(), markers_);
558 map_.addOverlay(clusterMarker_);
559 } else {
560 if (clusterMarker_.isHidden()) {
561 clusterMarker_.show();
562 }
563 clusterMarker_.redraw(true);
564 }
565 }
566 };
567
568 /**
569 * Remove all the markers from this cluster.
570 */
571 this.clearMarkers = function () {
572 if (clusterMarker_ !== null) {
573 map_.removeOverlay(clusterMarker_);
574 }
575 for (var i = 0; i < markers_.length; ++i) {
576 if (markers_[i].isAdded) {
577 map_.removeOverlay(markers_[i].marker);
578 }
579 }
580 markers_ = [];
581 };
582
583 /**
584 * Get number of markers.
585 * @return {Number}
586 */
587 this.getTotalMarkers = function () {
588 return markers_.length;
589 };
590}
591
592/**
593 * ClusterMarker_ creates a marker that shows the number of markers that
594 * a cluster contains.
595 *
596 * @constructor
597 * @private
598 * @param {GLatLng} latlng Marker's lat and lng.
599 * @param {Number} count Number to show.
600 * @param {Array of Object} styles The image list to be showed:
601 * {String} url Image url.
602 * {Number} height Image height.
603 * {Number} width Image width.
604 * {Array of Number} anchor Text anchor of image left and top.
605 * {String} textColor text color.
606 * @param {Number} padding Padding of marker center.
607 */
608function ClusterMarker_(latlng, count, styles, padding, markerArray) {
609 var index = 0;
610 this.markerArray = markerArray;
611 var dv = count;
612 while (dv !== 0) {
613 dv = parseInt(dv / 10, 10);
614 index ++;
615 }
616
617 if (styles.length < index) {
618 index = styles.length;
619 }
620 this.url_ = styles[index - 1].url;
621 this.height_ = styles[index - 1].height;
622 this.width_ = styles[index - 1].width;
623 this.textColor_ = styles[index - 1].opt_textColor;
624 this.anchor_ = styles[index - 1].opt_anchor;
625 this.latlng_ = latlng;
626 this.index_ = index;
627 this.styles_ = styles;
628 this.text_ = count;
629 this.padding_ = padding;
630}
631
632ClusterMarker_.prototype = new GOverlay();
633
634/**
635 * Initialize cluster marker.
636 * @private
637 */
638ClusterMarker_.prototype.initialize = function (map) {
639 this.map_ = map;
640 var markerArray = this.markerArray;
641 var div = document.createElement("div");
642 var latlng = this.latlng_;
643 var pos = map.fromLatLngToDivPixel(latlng);
644 pos.x -= parseInt(this.width_ / 2, 10);
645 pos.y -= parseInt(this.height_ / 2, 10);
646 var mstyle = "";
647 if (document.all) {
648 mstyle = 'filter:progid:DXImageTransform.Microsoft.AlphaImageLoader(sizingMethod=scale,src="' + this.url_ + '");';
649 } else {
650 mstyle = "background:url(" + this.url_ + ");";
651 }
652 if (typeof this.anchor_ === "object") {
653 if (typeof this.anchor_[0] === "number" && this.anchor_[0] > 0 && this.anchor_[0] < this.height_) {
654 mstyle += 'height:' + (this.height_ - this.anchor_[0]) + 'px;padding-top:' + this.anchor_[0] + 'px;';
655 } else {
656 mstyle += 'height:' + this.height_ + 'px;line-height:' + this.height_ + 'px;';
657 }
658 if (typeof this.anchor_[1] === "number" && this.anchor_[1] > 0 && this.anchor_[1] < this.width_) {
659 mstyle += 'width:' + (this.width_ - this.anchor_[1]) + 'px;padding-left:' + this.anchor_[1] + 'px;';
660 } else {
661 mstyle += 'width:' + this.width_ + 'px;text-align:center;';
662 }
663 } else {
664 mstyle += 'height:' + this.height_ + 'px;line-height:' + this.height_ + 'px;';
665 mstyle += 'width:' + this.width_ + 'px;text-align:center;';
666 }
667 var txtColor = this.textColor_ ? this.textColor_ : 'black';
668
669 div.style.cssText = mstyle + 'cursor:pointer;top:' + pos.y + "px;left:" +
670 pos.x + "px;color:" + txtColor + ";position:absolute;font-size:11px;" +
671 'font-family:Arial,sans-serif;font-weight:bold';
672 div.innerHTML = this.text_;
673 map.getPane(G_MAP_MARKER_PANE).appendChild(div);
674 var padding = this.padding_;
675 GEvent.addDomListener(div, "doubleclick", function () {
676 var pos = map.fromLatLngToDivPixel(latlng);
677 var sw = new GPoint(pos.x - padding, pos.y + padding);
678 sw = map.fromDivPixelToLatLng(sw);
679 var ne = new GPoint(pos.x + padding, pos.y - padding);
680 ne = map.fromDivPixelToLatLng(ne);
681 var zoom = map.getBoundsZoomLevel(new GLatLngBounds(sw, ne), map.getSize());
682 map.setCenter(latlng, zoom);
683 });
684 //Jan: We add our own mouseover listener for the cluster.
685 GEvent.addDomListener(div, "mouseover", function() {
686 mouseOverCluster(markerArray);
687 });
688
689 GEvent.addDomListener(div, "click", function() {
690
691
692 highlightCurrentMarker(latlng);
693 clickCluster(markerArray, latLng);
694 });
695
696 GEvent.addDomListener(div, "mouseout", function() {
697 mouseOutCluster(markerArray);
698
699 });
700 this.div_ = div;
701};
702
703/**
704 * Remove this overlay.
705 * @private
706 */
707ClusterMarker_.prototype.remove = function () {
708 this.div_.parentNode.removeChild(this.div_);
709};
710
711/**
712 * Copy this overlay.
713 * @private
714 */
715ClusterMarker_.prototype.copy = function () {
716 return new ClusterMarker_(this.latlng_, this.index_, this.text_, this.styles_, this.padding_);
717};
718
719/**
720 * Redraw this overlay.
721 * @private
722 */
723ClusterMarker_.prototype.redraw = function (force) {
724 if (!force) {
725 return;
726 }
727 var pos = this.map_.fromLatLngToDivPixel(this.latlng_);
728 pos.x -= parseInt(this.width_ / 2, 10);
729 pos.y -= parseInt(this.height_ / 2, 10);
730 this.div_.style.top = pos.y + "px";
731 this.div_.style.left = pos.x + "px";
732};
733
734/**
735 * Hide this cluster marker.
736 */
737ClusterMarker_.prototype.hide = function () {
738 this.div_.style.display = "none";
739};
740
741/**
742 * Show this cluster marker.
743 */
744ClusterMarker_.prototype.show = function () {
745 this.div_.style.display = "";
746};
747
748/**
749 * Get whether the cluster marker is hidden.
750 * @return {Boolean}
751 */
752ClusterMarker_.prototype.isHidden = function () {
753 return this.div_.style.display === "none";
754};
Note: See TracBrowser for help on using the repository browser.