When there are many objects in a view, it can become difficult to keep an overview. Therefore you can introduce clustering to help reduce the clutter. Figure 1, “Original versus clustered” contrasts the visualization of an original model with the visualization of a derivative transformed through clustering.

nonclusvsclus
Figure 1. Original versus clustered

Using a clustered model

To cluster objects in your model, you make use of an ALcdTransformingModel. A transforming model wraps around an original model, and applies a clustering transformation to it. To create a transforming model, you need at least an original model and a clustering transformer, as Program: Creating a transforming model demonstrates.

Program: Creating a transforming model (from samples/lightspeed/clustering/MainPanel)
ILcdModel transformingModel = TLcdTransformingModelFactory.createTransformingModel(originalModel, clusteringTransformer);

The clustering transformer is responsible for the actual transformation from objects in the original model to clusters.

Configuring a TLcdClusteringTransformer

A TLcdClusteringTransformer aggregates model elements that are close together into a cluster object. The cluster object is added to the transforming model instead of the individual elements. Model elements that are not part of a cluster are added as-is.

The clustering algorithm operates on the basis of a few parameters. You can specify those parameters when you create the TLcdClusteringTransformer. You can define:

  • clusterSize: a measure to express the approximate size of a cluster. The smaller you set the cluster size, the fewer points are clustered together.

  • minimumPoints: the minimum number of points required to form a cluster. For example, when you set this parameter to 3, there will never be a cluster that only represents 2 points. Each cluster will at least contain 3 points.

  • cluster shape provider: an ILcdClusterShapeProvider determines the shape of the cluster based on its contained elements. For example, if a cluster contains cities, you can choose to place the cluster at the point location of the city with the largest population, as demonstrated by Program: A custom shape provider.

Program: A custom shape provider
public static class BiggestCityClusterShapeProvider implements ILcdClusterShapeProvider {

  @Override
  public ILcdShape getShape(Set<Object> aComposingElements, ILcdModel aOriginalModel) {
    ILcdDataObject biggestCity = null;
    for (Object element : aComposingElements) {
      if (element instanceof ILcdDataObject) {
        ILcdDataObject city = (ILcdDataObject) element;
        if (biggestCity == null) {
          biggestCity = city;
          continue;
        }
        int cityPopulation = (int) city.getValue("Population");
        int biggestCityPopulation = (int) biggestCity.getValue("Population");
        if (cityPopulation > biggestCityPopulation) {
          biggestCity = city;
        }
      }
    }
    return (ILcdPoint) biggestCity.getValue("Location");
  }
}

Program: Configuring a custom shape provider shows how you can specify custom parameters for the clustering algorithm.

Program: Configuring a custom shape provider
TLcdClusteringTransformer transformer =
    TLcdClusteringTransformer.newBuilder()
                             .defaultParameters()
                             .clusterSize(100)
                             .minimumPoints(3)
                             .shapeProvider(new BiggestCityClusterShapeProvider())
                             .build()
                             .build();

If no parameters are specified, sensible defaults are used: the location of the element closest to the center of mass of the cluster elements is chosen as the cluster location. That default prevents the positioning of clusters in illogical places, a cluster of cities placed in a nearby body of water, for example.

Clustering by class

One of the key aspects of the clustering algorithm is its use of an ILcdClassifier.

The algorithm organizes the elements to be transformed according to a classifier. An ILcdClassifier returns a classification for a model element. This is a String employed by the algorithm to separate the elements. Two elements with the same classification may or may not be clustered together. Two elements with distinct classifications are never clustered together.

For example, if your model contains cities and you do not want cities to be clustered across country borders, the classifier can simply return the name of the country to which a city belongs. Program: A classifier that classifies cities according to country shows how to classify cities according to their countries.

Program: A classifier that classifies cities according to country
private static class CityClassifier implements ILcdClassifier {

  public static final String NO_CLASSIFICATION = "";

  @Override
  public String getClassification(Object aObject) {
    if (aObject instanceof ILcdDataObject) {
      ILcdDataObject city = (ILcdDataObject) aObject;
      return city.getValue("Country").toString();
    }
    return NO_CLASSIFICATION;
  }

}

Classifications also allow more fine-grained control over the clustering algorithm. You can specify parameters per classification or classification group. Program: Classification specific parameters sets a custom shape provider that will be used for all classifications. However, elements classified as "Belgium" will not be clustered . Note that for this to work, the ILcdClassifier and the configuration of the TLcdClusteringTransformer must work in harmony. In the example, the ILcdClassifier should return "Belgium" for Belgian cities.

Program: Classification specific parameters
TLcdClusteringTransformer transformer =
    TLcdClusteringTransformer.newBuilder()
                             .classifier(new CityClassifier())
                             .defaultParameters()
                             .shapeProvider(new BiggestCityClusterShapeProvider())
                             .build()
                             .forClass("Belgium")
                             .noClustering()
                             .build()
                             .build();

The forClass method is overloaded. You can specify an exact classification or an ILcdFilter<String>. The latter allows you to specify parameters for multiple classes with one forClass call. The order of the forClass calls is important. The parameters of the first classification match are used.

Setting up scale-dependent clustering

For certain data sets, it makes sense to apply another clustering setting when the scale of the view changes. To set up distinct clustering settings for various view scales, create a TLcdClusteringTransformer for each of the scales. Next, use the TLcdClusteringTransformer#createMapScaleDependent method to combine them into a single, scale-dependent TLcdClusteringTransformer.

Program: Creating a scale-dependent clustering transformer (from samples/lightspeed/clustering/MainPanel)
//When zoomed in, cluster the events per country and avoid grouping events
//happening in different countries in different clusters.
TLcdClusteringTransformer zoomedInClusteringTransformer =
    TLcdClusteringTransformer.newBuilder()
                             .classifier(new CountryClassifier())
                             .defaultParameters()
                             .clusterSize(CLUSTER_SIZE)
                             .minimumPoints(MIN_CLUSTER_COUNT)
                             .build()
                             .build();
//When zoomed out, all the events can be clustered together.
//Otherwise, we would end up with overlapping clusters as the countries become rather small
TLcdClusteringTransformer zoomedOutClusteringTransformer =
    TLcdClusteringTransformer.newBuilder()
                             .defaultParameters()
                             .clusterSize(CLUSTER_SIZE)
                             .minimumPoints(MIN_CLUSTER_COUNT)
                             .build()
                             .build();

//Switching between the two clustering approaches should happen at a scale 1 : 25 000 000
double scale = 1.0 / 25000000.0;
TLcdClusteringTransformer scaleDependentClusteringTransformer =
    TLcdClusteringTransformer.createMapScaleDependent(
        new double[]{scale},
        new TLcdClusteringTransformer[]{zoomedInClusteringTransformer, zoomedOutClusteringTransformer}
    );

Working with TLcdCluster objects

The clusters in the transforming model are instances of TLcdCluster. This class implements ILcdDataObject. TLcdClusterDataTypes defines the objects that use the TLcdCluster data model API. Of note are the classification, composing elements and shape properties. You can use those properties to retrieve the classification, the set of contained elements and the cluster shape respectively.