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

Last change on this file since 7793 was 7787, checked in by janveeden, 15 years ago

Radius in which nodes are clustered is smaller now.

File size: 21.6 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 };
265
266 /**
267 * Get all clusters in viewport.
268 * @return {Array of Cluster}
269 */
270 this.getClustersInViewport_ = function () {
271 var clusters = [];
272 var curBounds = map_.getBounds();
273 for (var i = 0; i < clusters_.length; i ++) {
274 if (clusters_[i].isInBounds(curBounds)) {
275 clusters.push(clusters_[i]);
276 }
277 }
278 return clusters;
279 };
280
281 /**
282 * Get max zoom level.
283 * @private
284 * @return {Number}
285 */
286 this.getMaxZoom_ = function () {
287 return maxZoom_;
288 };
289
290 /**
291 * Get map object.
292 * @private
293 * @return {GMap2}
294 */
295 this.getMap_ = function () {
296 return map_;
297 };
298
299 /**
300 * Get grid size
301 * @private
302 * @return {Number}
303 */
304 this.getGridSize_ = function () {
305 return gridSize_;
306 };
307
308 /**
309 * Get total number of markers.
310 * @return {Number}
311 */
312 this.getTotalMarkers = function () {
313 var result = 0;
314 for (var i = 0; i < clusters_.length; ++i) {
315 result += clusters_[i].getTotalMarkers();
316 }
317 return result;
318 };
319
320 /**
321 * Get total number of clusters.
322 * @return {int}
323 */
324 this.getTotalClusters = function () {
325 return clusters_.length;
326 };
327
328 /**
329 * Collect all markers of clusters in viewport and regroup them.
330 */
331 this.resetViewport = function () {
332 var clusters = this.getClustersInViewport_();
333 var tmpMarkers = [];
334 var removed = 0;
335
336 for (var i = 0; i < clusters.length; ++i) {
337 var cluster = clusters[i];
338 var oldZoom = cluster.getCurrentZoom();
339 if (oldZoom === null) {
340 continue;
341 }
342 var curZoom = map_.getZoom();
343 if (curZoom !== oldZoom) {
344
345 // If the cluster zoom level changed then destroy the cluster
346 // and collect its markers.
347 var mks = cluster.getMarkers();
348 for (var j = 0; j < mks.length; ++j) {
349 var newMarker = {
350 'isAdded': false,
351 'marker': mks[j].marker
352 };
353 tmpMarkers.push(newMarker);
354 }
355 cluster.clearMarkers();
356 removed++;
357 for (j = 0; j < clusters_.length; ++j) {
358 if (cluster === clusters_[j]) {
359 clusters_.splice(j, 1);
360 }
361 }
362 }
363 }
364
365 // Add the markers collected into marker cluster to reset
366 reAddMarkers_(tmpMarkers);
367 this.redraw_();
368 };
369
370
371 /**
372 * Add a set of markers.
373 *
374 * @param {Array of GMarker} markers The markers you want to add.
375 */
376 this.addMarkers = function (markers) {
377 for (var i = 0; i < markers.length; ++i) {
378 this.addMarker(markers[i], true);
379 }
380 this.redraw_();
381 };
382
383 // initialize
384 if (typeof opt_markers === "object" && opt_markers !== null) {
385 this.addMarkers(opt_markers);
386 }
387
388 // when map move end, regroup.
389 mcfn_ = GEvent.addListener(map_, "moveend", function () {
390 me_.resetViewport();
391 });
392}
393
394/**
395 * Create a cluster to collect markers.
396 * A cluster includes some markers which are in a block of area.
397 * If there are more than one markers in cluster, the cluster
398 * will create a {@link ClusterMarker_} and show the total number
399 * of markers in cluster.
400 *
401 * @constructor
402 * @private
403 * @param {MarkerClusterer} markerClusterer The marker cluster object
404 */
405function Cluster(markerClusterer) {
406 var center_ = null;
407 var markers_ = [];
408 var markerClusterer_ = markerClusterer;
409 var map_ = markerClusterer.getMap_();
410 var clusterMarker_ = null;
411 var zoom_ = map_.getZoom();
412
413 /**
414 * Get markers of this cluster.
415 *
416 * @return {Array of GMarker}
417 */
418 this.getMarkers = function () {
419 return markers_;
420 };
421
422 /**
423 * If this cluster intersects certain bounds.
424 *
425 * @param {GLatLngBounds} bounds A bounds to test
426 * @return {Boolean} Is this cluster intersects the bounds
427 */
428 this.isInBounds = function (bounds) {
429 if (center_ === null) {
430 return false;
431 }
432
433 if (!bounds) {
434 bounds = map_.getBounds();
435 }
436 var sw = map_.fromLatLngToDivPixel(bounds.getSouthWest());
437 var ne = map_.fromLatLngToDivPixel(bounds.getNorthEast());
438
439 var centerxy = map_.fromLatLngToDivPixel(center_);
440 var inViewport = true;
441 var gridSize = markerClusterer.getGridSize_();
442 if (zoom_ !== map_.getZoom()) {
443 var dl = map_.getZoom() - zoom_;
444 gridSize = Math.pow(2, dl) * gridSize;
445 }
446 if (ne.x !== sw.x && (centerxy.x + gridSize < sw.x || centerxy.x - gridSize > ne.x)) {
447 inViewport = false;
448 }
449 if (inViewport && (centerxy.y + gridSize < ne.y || centerxy.y - gridSize > sw.y)) {
450 inViewport = false;
451 }
452 return inViewport;
453 };
454
455 /**
456 * Get cluster center.
457 *
458 * @return {GLatLng}
459 */
460 this.getCenter = function () {
461 return center_;
462 };
463
464 /**
465 * Add a marker.
466 *
467 * @param {Object} marker An object of marker you want to add:
468 * {Boolean} isAdded If the marker is added on map.
469 * {GMarker} marker The marker you want to add.
470 */
471 this.addMarker = function (marker) {
472 if (center_ === null) {
473 /*var pos = marker['marker'].getLatLng();
474 pos = map.fromLatLngToContainerPixel(pos);
475 pos.x = parseInt(pos.x - pos.x % (GRIDWIDTH * 2) + GRIDWIDTH);
476 pos.y = parseInt(pos.y - pos.y % (GRIDWIDTH * 2) + GRIDWIDTH);
477 center = map.fromContainerPixelToLatLng(pos);*/
478 center_ = marker.marker.getLatLng();
479 }
480 markers_.push(marker);
481 };
482
483 /**
484 * Remove a marker from cluster.
485 *
486 * @param {GMarker} marker The marker you want to remove.
487 * @return {Boolean} Whether find the marker to be removed.
488 */
489 this.removeMarker = function (marker) {
490 for (var i = 0; i < markers_.length; ++i) {
491 if (marker === markers_[i].marker) {
492 if (markers_[i].isAdded) {
493 map_.removeOverlay(markers_[i].marker);
494 }
495 markers_.splice(i, 1);
496 return true;
497 }
498 }
499 return false;
500 };
501
502 /**
503 * Get current zoom level of this cluster.
504 * Note: the cluster zoom level and map zoom level not always the same.
505 *
506 * @return {Number}
507 */
508 this.getCurrentZoom = function () {
509 return zoom_;
510 };
511
512 /**
513 * Redraw a cluster.
514 * @private
515 * @param {Boolean} isForce If redraw by force, no matter if the cluster is
516 * in viewport.
517 */
518 this.redraw_ = function (isForce) {
519 if (!isForce && !this.isInBounds()) {
520 return;
521 }
522
523 // Set cluster zoom level.
524 zoom_ = map_.getZoom();
525 var i = 0;
526 var mz = markerClusterer.getMaxZoom_();
527 if (mz === null) {
528 mz = map_.getCurrentMapType().getMaximumResolution();
529 }
530 if (zoom_ >= mz || this.getTotalMarkers() === 1) {
531
532 // If current zoom level is beyond the max zoom level or the cluster
533 // have only one marker, the marker(s) in cluster will be showed on map.
534 for (i = 0; i < markers_.length; ++i) {
535 if (markers_[i].isAdded) {
536 if (markers_[i].marker.isHidden()) {
537 markers_[i].marker.show();
538 }
539 } else {
540 map_.addOverlay(markers_[i].marker);
541 markers_[i].isAdded = true;
542 }
543 }
544 if (clusterMarker_ !== null) {
545 clusterMarker_.hide();
546 }
547 } else {
548 // Else add a cluster marker on map to show the number of markers in
549 // this cluster.
550 for (i = 0; i < markers_.length; ++i) {
551 if (markers_[i].isAdded && (!markers_[i].marker.isHidden())) {
552 markers_[i].marker.hide();
553 }
554 }
555 if (clusterMarker_ === null) {
556 clusterMarker_ = new ClusterMarker_(center_, this.getTotalMarkers(), markerClusterer_.getStyles_(), markerClusterer_.getGridSize_(), markers_);
557 map_.addOverlay(clusterMarker_);
558 } else {
559 if (clusterMarker_.isHidden()) {
560 clusterMarker_.show();
561 }
562 clusterMarker_.redraw(true);
563 }
564 }
565 };
566
567 /**
568 * Remove all the markers from this cluster.
569 */
570 this.clearMarkers = function () {
571 if (clusterMarker_ !== null) {
572 map_.removeOverlay(clusterMarker_);
573 }
574 for (var i = 0; i < markers_.length; ++i) {
575 if (markers_[i].isAdded) {
576 map_.removeOverlay(markers_[i].marker);
577 }
578 }
579 markers_ = [];
580 };
581
582 /**
583 * Get number of markers.
584 * @return {Number}
585 */
586 this.getTotalMarkers = function () {
587 return markers_.length;
588 };
589}
590
591/**
592 * ClusterMarker_ creates a marker that shows the number of markers that
593 * a cluster contains.
594 *
595 * @constructor
596 * @private
597 * @param {GLatLng} latlng Marker's lat and lng.
598 * @param {Number} count Number to show.
599 * @param {Array of Object} styles The image list to be showed:
600 * {String} url Image url.
601 * {Number} height Image height.
602 * {Number} width Image width.
603 * {Array of Number} anchor Text anchor of image left and top.
604 * {String} textColor text color.
605 * @param {Number} padding Padding of marker center.
606 */
607function ClusterMarker_(latlng, count, styles, padding, markerArray) {
608 var index = 0;
609 this.markerArray = markerArray;
610 var dv = count;
611 while (dv !== 0) {
612 dv = parseInt(dv / 10, 10);
613 index ++;
614 }
615
616 if (styles.length < index) {
617 index = styles.length;
618 }
619 this.url_ = styles[index - 1].url;
620 this.height_ = styles[index - 1].height;
621 this.width_ = styles[index - 1].width;
622 this.textColor_ = styles[index - 1].opt_textColor;
623 this.anchor_ = styles[index - 1].opt_anchor;
624 this.latlng_ = latlng;
625 this.index_ = index;
626 this.styles_ = styles;
627 this.text_ = count;
628 this.padding_ = padding;
629}
630
631ClusterMarker_.prototype = new GOverlay();
632
633/**
634 * Initialize cluster marker.
635 * @private
636 */
637ClusterMarker_.prototype.initialize = function (map) {
638 this.map_ = map;
639 var markerArray = this.markerArray;
640 var div = document.createElement("div");
641 var latlng = this.latlng_;
642 var pos = map.fromLatLngToDivPixel(latlng);
643 pos.x -= parseInt(this.width_ / 2, 10);
644 pos.y -= parseInt(this.height_ / 2, 10);
645 var mstyle = "";
646 if (document.all) {
647 mstyle = 'filter:progid:DXImageTransform.Microsoft.AlphaImageLoader(sizingMethod=scale,src="' + this.url_ + '");';
648 } else {
649 mstyle = "background:url(" + this.url_ + ");";
650 }
651 if (typeof this.anchor_ === "object") {
652 if (typeof this.anchor_[0] === "number" && this.anchor_[0] > 0 && this.anchor_[0] < this.height_) {
653 mstyle += 'height:' + (this.height_ - this.anchor_[0]) + 'px;padding-top:' + this.anchor_[0] + 'px;';
654 } else {
655 mstyle += 'height:' + this.height_ + 'px;line-height:' + this.height_ + 'px;';
656 }
657 if (typeof this.anchor_[1] === "number" && this.anchor_[1] > 0 && this.anchor_[1] < this.width_) {
658 mstyle += 'width:' + (this.width_ - this.anchor_[1]) + 'px;padding-left:' + this.anchor_[1] + 'px;';
659 } else {
660 mstyle += 'width:' + this.width_ + 'px;text-align:center;';
661 }
662 } else {
663 mstyle += 'height:' + this.height_ + 'px;line-height:' + this.height_ + 'px;';
664 mstyle += 'width:' + this.width_ + 'px;text-align:center;';
665 }
666 var txtColor = this.textColor_ ? this.textColor_ : 'black';
667
668 div.style.cssText = mstyle + 'cursor:pointer;top:' + pos.y + "px;left:" +
669 pos.x + "px;color:" + txtColor + ";position:absolute;font-size:11px;" +
670 'font-family:Arial,sans-serif;font-weight:bold';
671 div.innerHTML = this.text_;
672 map.getPane(G_MAP_MAP_PANE).appendChild(div);
673 var padding = this.padding_;
674 GEvent.addDomListener(div, "doubleclick", function () {
675 var pos = map.fromLatLngToDivPixel(latlng);
676 var sw = new GPoint(pos.x - padding, pos.y + padding);
677 sw = map.fromDivPixelToLatLng(sw);
678 var ne = new GPoint(pos.x + padding, pos.y - padding);
679 ne = map.fromDivPixelToLatLng(ne);
680 var zoom = map.getBoundsZoomLevel(new GLatLngBounds(sw, ne), map.getSize());
681 map.setCenter(latlng, zoom);
682 });
683 //Jan: We add our own mouseover listener for the cluster.
684 GEvent.addDomListener(div, "mouseover", function() {
685 mouseOverCluster(markerArray);
686 });
687
688 GEvent.addDomListener(div, "click", function() {
689 clickCluster(markerArray);
690 });
691 this.div_ = div;
692};
693
694/**
695 * Remove this overlay.
696 * @private
697 */
698ClusterMarker_.prototype.remove = function () {
699 this.div_.parentNode.removeChild(this.div_);
700};
701
702/**
703 * Copy this overlay.
704 * @private
705 */
706ClusterMarker_.prototype.copy = function () {
707 return new ClusterMarker_(this.latlng_, this.index_, this.text_, this.styles_, this.padding_);
708};
709
710/**
711 * Redraw this overlay.
712 * @private
713 */
714ClusterMarker_.prototype.redraw = function (force) {
715 if (!force) {
716 return;
717 }
718 var pos = this.map_.fromLatLngToDivPixel(this.latlng_);
719 pos.x -= parseInt(this.width_ / 2, 10);
720 pos.y -= parseInt(this.height_ / 2, 10);
721 this.div_.style.top = pos.y + "px";
722 this.div_.style.left = pos.x + "px";
723};
724
725/**
726 * Hide this cluster marker.
727 */
728ClusterMarker_.prototype.hide = function () {
729 this.div_.style.display = "none";
730};
731
732/**
733 * Show this cluster marker.
734 */
735ClusterMarker_.prototype.show = function () {
736 this.div_.style.display = "";
737};
738
739/**
740 * Get whether the cluster marker is hidden.
741 * @return {Boolean}
742 */
743ClusterMarker_.prototype.isHidden = function () {
744 return this.div_.style.display === "none";
745};
Note: See TracBrowser for help on using the repository browser.