OpenFlights

News from OpenFlights, the site for flight logging, mapping, stats and sharing

Customized OpenLayers cluster strategies

14 Comments

Extremely technical content ahead — if you’re looking for news about OpenFlights the website, check out this post instead. Programmers using OpenLayers, read on.

One of the most powerful features in the OpenLayers framework, yet one of the worst-documented, is the Cluster strategy. Now, implementing a default clustering strategy couldn’t be much easier:

var strategy = new OpenLayers.Strategy.Cluster({distance: 15, threshold: 3});
airportLayer = new OpenLayers.Layer.Vector("Airports", {strategies: [strategy]});

And ta-dah, any group of 3 or more (threshold) vectors in airportLayer within 15 pixels of each other (distance) at the current zoom level are now merged into one.

Customizing the appearance of those clusters, though, gets a little more complicated. The OpenLayers Cluster Strategy Threshold example demonstrates a “simple” case:

var style = new OpenLayers.Style({
    strokeWidth: "${width}"
  } , {
    context: {
      width: function(feature) {
        return (feature.cluster) ? 2 : 1;
      }
    }
  );
clusters = new OpenLayers.Layer.Vector("Clusters", {
    strategies: [strategy],
    styleMap: new OpenLayers.StyleMap({"default": style})});

To understand what’s happening here, you first need to grok that clustered vectors replace the original vectors on the layer entirely: if you look at the layer’s features array, you will see a single cluster feature in place of the original vector features you added. This clustering is done in the background after you have added all the features, but before the rendering.

With that in mind, we define the vector property strokeWidth to be equal to the value substitution ${value}, but not as a static attribute, but an inline function defined in the context block. This inline function is then executed for every single feature and cluster as it’s being rendered, and in it, by examining feature.cluster we can see if it’s a single feature (null) or a cluster of features (not null). There’s also a second special attribute called feature.attributes.count, which can be used to tell how many features that particular cluster contains.

Now, that’s all well and good if you’re clustering identical objects that can be replaced by a generic icon that doesn’t tell anything about what’s underneath… but what about a case like clustering airports, some of which are much more important (= have more flights) than others?

The key is that feature.cluster is in fact an array containing all the features embedded in it. feature.cluster.length is equivalent to feature.attributes.count, and by looping through the array members of feature.cluster, we can examine the individual features that make it up. Here’s a practical example.

The “Most Important” Strategy

We want to set up our clustering so that the most “important” object hides the less important objects under it. Assume each vector is created with the attributes importance, which is larger for more important objects, and icon, a graphic used to represent the vector:

var point = new OpenLayers.Geometry.Point(x, y);
var feature = new OpenLayers.Feature.Vector(point);
feature.attributes = { icon: "/img/myicon.png", label: "myVector", importance: 10 };
vectorLayer.addFeatures([feature]);

We add icon as a substituted value into the Style object:

var style = new OpenLayers.Style({
    externalGraphic: "${icon}",
    label: "${label}",
    graphicWidth: 15,
    graphicHeight: 15,
    opacity: 1, ...

And then the corresponding context code right under it, where we loop through all cluster members to find the most “important” one:

  ... } , context: {
    icon: function(feature) {
      if(feature.cluster) {
        maxImportance = 0;
        for(var c = 0; c < feature.cluster.length; c++) {
          i = feature.cluster[c].attributes.importance;
          if(i > maxImportance) {
            maxImportance = i;
            mainFeature = c;
          }
        }
        feature.attributes.icon = feature.cluster[mainFeature].attributes.icon;
        feature.attributes.label = feature.cluster[mainFeature].attributes.label;
      }
      return feature.attributes.icon;
    }, ...

This code thus handles both cases:

  • If the feature is a cluster, we look through its component vector features and set the cluster icon according to the most important component.
  • If the feature is a single vector, we simply return the icon set earlier.

Note that we also set the value of the attribute label, which is used by the Style object to generate the tooltip for the icon. These can be read with little getter functions like this:

    label: function(feature) { return feature.attributes.label; }, ...

Attributes are processed in order, so make sure the function that does the heavy lifting goes first, and the getter functions come after it.

And there we have it, a customized cluster strategy! For an actual example, take a look at openflights.js and search for “new OpenLayers.Style”; or to see this (more or less) in action, zoom around the map at OpenFlights.

Useful? Buggy? Still confused? Let us know.

Clusterrifically yours,
-jani

14 thoughts on “Customized OpenLayers cluster strategies

  1. Hello,
    This tutorial is a good help. You may add this little tip (that can save lots of time): to apply a cluster strategy, it seems you have to add all features in once time (an array of features), not one by one.

    Like

  2. Hey Jani, Great site! really like how you did the labels…. but im alittle confused. is it possible for you to show me how you would create a label for a marker loaded from say a kml file? or just a sample openlayers only version? Other then that the site is great!

    Like

  3. nevermind figured it out.

    Like

  4. Hey thanks!

    great work! i will test it later. i needed that info, like you said: documentation of this feature is baaaaaaad.

    thank you very much!

    Like

  5. Thanks a million! I have been looking for days to solve my problem. This is exactly what I needed. Brilliant.

    Like

  6. Thank you! Just what I needed as well.

    (Another question though, I looked at openflights.js and cannot figure out how you got the following working: “Select region While holding down Shift key, click on the map and draw a rectangle with your mouse.” I see no reference to shift keys in your code, just a regular old SelectFeature without even specifying “box: true”. How did you do that magic? thanks)

    Like

  7. @Harry: This is built-in functionality for OpenLayers, it should be enabled right out of the box.

    Like

  8. Is it possible to drag and remove a single feature from cluster?

    Like

  9. First of all, thanks for the explanation. However, I have to solve a slightly different task.

    I have a map with point features, with an attribute “point_group” (integers from “1” to “4”).
    What if, I just want to cluster points of the same group together?

    At the end the clusters shouldn’t overlap each other, thus I want to do it within one layer. Is something like that possible?

    Thanks in advance for your help!!!

    Like

  10. Anybody knows how to realize “The “Most Important” Strategy” in gwt-openlayers? My features have numeric values attached to them and I need to put sums of underlying feature values in clusters’ labels. So in JS it would look like
    context: {
    label: function(feature) { … }
    }
    I suppose but I can’t find how to do the same thing in GWT.

    Like

  11. I would like to note one more thing here which I observed is cluster functionality will not work if you go on adding features one by one.
    You have to add all the features in one go by using array etc.
    Is my understanding correct?

    Like

Leave a comment