Clustering in Google Maps leicht gemacht: die SimpleCluster Klasse

Zusätzlich zu meiner SimpleMarker Klasse habe ich eine Engine zum Clustern von Markern entworfen, wenn viele Marker nah beieinander sind. Bitte beachten Sie, dass es für diese Thematik deutlich bessere Implementationen gibt, beispielsweise der MarkerManager aus der Google Maps Utility Library (mein SimpleMarker ist Kompatibel zu diesem MarkerManager).

Die Präsentation meines Codes dient also vorwiegend zu Lernzwecken, weswegen ich auch versucht habe, den Code so einfach wie nur möglich zu gestalten. Wenn Sie also an der genauen Funktionsweise eines Clusterers interessiert sind, lesen Sie ruhig weiter ;-).

Grundsätzlich verwendet man Clustering, wenn sich sehr viele Marker auf einer sehr geringen Fläche befinden. Wie man sicherlich vorstellen kann, würde der Benutzer in so einem Fall kaum zwischen den Markern unterscheiden können, da sich alle Marker überlappen. Dies ist alles andere als Benutzerfreundlich. Wäre es nicht besser, wenn die überlappenden Marker einfach zu einem großen Marker zusammemngefasst werden? Genau das tut meine Klasse. Zusätztlich schaut sie, welche Marker sich überhaupt im Blickfeld des Benutzers befinden und blendet alle anderen aus. Auch, wenn die Marker nicht direkt sichtbar sind, führt dies zu einer spürbaren Leistungssteigerung.

Die Frage ist, wann sind zwei Marker nah genug, um zusammengefasst zu werden? Man könnte zum Beispiel die physische Position der Marker betrachten und einfach alle Marker im Umkreis einer bestimten Entfernung (z.B. 1 Kilometer) zusammenfassen. Leider hat diese Lösung einen Haken: verwendet man einen konstanten Radius, kann die Methode nicht über alle Zoomstufen hinweg funktionieren. Weiterhin würde diese Methode komplett versagen, wenn die Größen der einzelnen Marker stark variieren würden.

Was ich in der SimpleCluster Klasse stattdessen gemacht habe, ist, die Koordinaten der Eckpunkte von Markern zu berechnen und miteinander zu vergleichen, um so Überlappungen zu erkennen. Dazu habe ich meinen Markern die SimpleMarker.getBounds() Methode gegeben. Diese Methode verknüpft die Ecken des jeweiligen Markers abhängig von der Zoomstufe mit den LatLng-Koordinaten, auf denen sie liegen. Nun kann man mit Hilfe der API-Methode LatLngBounds.intersects() ganz leicht prüfen, ob sich zwei Marker überlappen. In diesem Fall werden die beiden Marker versteckt und ein spezieller Cluster-Marker erstellt. Dies ist der dazugehörige Code:

// Markers where the bounds are nulled were already assigned to sth
for(var i = 0; i < this._markers.length - 1; i++) {
  if (this._markers[i].sc_visible[zoom]) {
    var matches = 0;
    var bounds = new google.maps.LatLngBounds();
 
    for (var j = i+1; j < this._markers.length; j++) {
      if (
        this._markers[j].sc_visible[zoom] &&
        this._markers[i].getBounds(zoom).intersects(
          this._markers[j].getBounds(zoom))
      ) {
        bounds.extend(this._markers[j].getPosition());
        this._markers[j].sc_visible[zoom] = false;
        matches++;
      }
    }
 
    // If there was an intersection with this marker,
    // create a cluster icon on its position
    if (matches > 0) {
      bounds.extend(this._markers[i].getPosition());
      this._markers[i].sc_visible[zoom] = false;
      var mark = new SimpleMarker(this._map,
        this._markers[i].getPosition(),
        {
          image: '[long url]',
          title: (matches+1) + " Marker in this area"
        });
      mark.sc_bounds = bounds;

      // Default event for a cluster-marker: Zoom in
      // to the area where its clustered markers are
      // located
      google.maps.event.addListener(mark, 'click', function() {
        this.getMap().fitBounds(this.sc_bounds);
      }); 
   
      // Keep our marker for later usage
      this._clusters[zoom].push(mark);
    }
  }
}

Ein paar Anmerkungen zu den Variablen in meiner Klasse: jeder Marker, der verwaltet wird (unabhängig davon, ob sichtbar oder nicht), wird im Array SimpleCluster._markers hinterlegt. Zu jedem Marker fügen wir ein Attribut hinzu, welches angibt, bei welcher Zoomstufe der Marker sichtbar sein soll und bei welcher nicht. Es handelt sich hierbei um das marker.sc_visible Attribut. Die speziellen Cluster-Marker für jede Zoomstufe werden im Array SimpleCluster._clusters gespeichert. Dieses Zwischenspeichern bewirkt eine hohe Leistung, wenn der Benutzer häufig die Zoomstufe wechselt.

Das Berechnen der Cluster ist allerdings nur die halbe Geschichte. Die andere Hälfte befasst sich mit dem Anzeigen der Marker je nachdem, welches Gebiet momentan angezeigt wird. Dies wird in der SimpleCluster.draw() Methode erledigt, die nach Möglichkeit nach jedem Bewegen der Karte sowie wechseln der Zoomstufe aufgerufen werden sollte. Im Konstruktor der Klasse werden diese Events automatisch hinzugefügt. Das eigentliche Zeichnen ist dann eher simpel und besteht aus den folgenden zwei for-Schleifen:

// Iterate through possible markers
for(var i = 0; i < this._markers.length; i++) {
  if (
    this._markers[i].sc_visible[this._zoomVal] &&
    newBounds.contains(this._markers[i].getPosition())
  ) {
    // Display marker if was hidden before and is now in range
    if (!this._markers[i].getMap()) 
      this._markers[i].setMap(this._map);
  } else {
    // Hide marker if marker was shown and now is out of bounds
    this._markers[i].setMap(null);
  }
}
 
// Iterate through clusters and do the same as for markers
for(var i = 0; i < this._clusters[this._zoomVal].length; i++) {
  if (newBounds.contains(this._clusters[this._zoomVal][i].getPosition())) {
    // Display cluster if was hidden before and is now in range
    if (!this._clusters[this._zoomVal][i].getMap()) 
      this._clusters[this._zoomVal][i].setMap(this._map);
  } else {
    // Hide cluster if cluster was shown and now is out of bounds
    this._clusters[this._zoomVal][i].setMap(null);
  }
}

Hier wird nicht nur geprüft, ob ein Marker generell angezeigt werden soll (oder, ob er durch einen Cluster-Marker repräsentiert wird), sondern auch, ob er innerhalb des Blickfelds des Benutzers, spezifiziert durch newBounds, liegt. Damit wird sichergestellt, dass keine Marker gezeichnet werden, die der Benutzer ohnehin nicht sehen kann. Generell sollte man vermeiden, Dinge zu zeichnen, die für den Benutzer von keinem Nutzen sind. Je komplexer die DOM-Struktur, desto schlechter ist die Leistung der Webseite.

Das wäre soweit alles. Wie man sieht, ist Clustering gar nicht so kompliziert, wie man es zu Beginn annimmt. Das einzig Wichtige ist eine gute Methode zur Erkennung von Markern, die zusammengefasst werden müssen. Der Rest ist simples Ein- und Ausblenden von Markern.

Eine Demonstration der Klasse kann auf demo.k621.de gefunden werden. Der Quellcode ist sowohl komprimiert als auch unkomprimiert angehängt. Allerdings, wie bereits zu Beginn erwähnt, würde ich die Klasse nur zu Lernzwecken verwenden, nicht in einer richtigen Produktionsumgebung.

Anhänge