Click or drag to resize

Clustering Tutorial

Verizon Connect Logo
Print this page
Learn more about Verizon Connect GeoBase.
Get information about the latest release
Overview

Clusters allow you render composite objects that represent multiple map features. This tutorial demonstrates the basics of generating clusters and displaying them on a map. In particular, you will generate clusters that display counts of points of interest (pois). The clusters change as you pan the map or change its zoom level.

Getting Started

The First step is to create a form with a map on it to display the clusters we will be generating:

  1. Create a new Visual Studio project with a Windows Forms application. Remember to add a reference to geobase.net.dll to your project, and if you are using .NET 4 switch from the Client Profile to the full framework.

  2. Add a MapCtrl to fill your form. Set the Name property to 'mapCtrl', the DragBehavior property to Hand and the Zoom property to 20.

    Your form should look something like the following screen shot:

  3. To make the code simpler and more readable, add the following using directives to the top of Form1.cs:

    C#
    using Telogis.GeoBase;
    using Telogis.GeoBase.Clustering;
    using Telogis.GeoBase.DataSources;
    using Telogis.GeoBase.Concurrency;
Creating a point feature source

Before we can generate clusters, we must have a data source that supplies information about the map features that we want to group into clusters. Our data source will use a DataQuery to fetch all POIs that are currently on the map.

  1. Create a new class for the point feature source (below the Form1 class):

    C#
    // A source of map features to participate in clustering
    class POISource : IPointFeatureSource {
    
    }
  2. We will want our point feature source to dynamically generate data, depending on the current center and zoom level of the map. In order to do this, add a data member to your POISource class that can hold a reference to the map and a constructor that assigns the map to that data member. This code should be placed within the new point feature class.

    C#
    // keep the map we are generating features for as a private member
     private MapCtrl m_map;
    
     public POISource(MapCtrl map) {
         m_map = map;
     }
  3. We are now ready to implement the IPointFeatureSource members that are required of all objects that supply map features to a cluster layer. These consist of the RefreshRate property (which indicates how often cluster data is refreshed), and two methods for asynchronously supplying point features:

    C#
    // refresh the points every 10 seconds
     public TimeSpan RefreshRate {
        get { return TimeSpan.FromSeconds(10); }
    }
    
    // start asynchronously fetching the POIs
    public IAsyncResult BeginQuery(BoundingBox queryBounds, AsyncCallback callback, object state) {
        // Get the current bounding box for the map
        BoundingBox dataBounds = m_map.XYtoBoundingBox(0, 0, m_map.Width, m_map.Height);
    
        // Don't get any POIs if the map covers too large an area.
        if (dataBounds.TopLeft.DistanceTo(dataBounds.BottomRight, DistanceUnit.KILOMETERS) > 100) {
            return new SyncResult<SimpleFeature[]>(callback, state, new SimpleFeature[0]);
        }
    
        // Find all POIs on the map -- this can take a while
        Poi[] pois = DataQuery.QueryPoi(dataBounds);
    
        // Create an array to hold the pois as SimpleFeature objects
        SimpleFeature[] results = new SimpleFeature[pois.Length];
        for (int i = 0; i < results.Length; i++) {
            results[i] = new SimpleFeature(pois[i].Location, pois[i].Type.ToString());
        }
    
        // Because this is asynchronous, use SyncResult to return the results
        return new SyncResult<SimpleFeature[]>(callback, state, results);
    }
    
    // Finish up the asynchronous fetching of POIs
    public IEnumerable<IPointFeature> EndQuery(IAsyncResult asyncResult) {
        // Just return the results that were generated by BeginQuery
        return ((SyncResult<SimpleFeature[]>)asyncResult).Result;
    }

The code for the entire POISource class is as follows (for reference; this should not be added to the page):

C#
// A source of map features to participate in clustering
class POISource : IPointFeatureSource {

    // keep the map we are generating features for as a private member
    private MapCtrl m_map;

    public POISource(MapCtrl map) {
        m_map = map;
    }
    // refresh the points every 10 seconds
     public TimeSpan RefreshRate {
        get { return TimeSpan.FromSeconds(10); }
    }

    // Start asynchronously fetching the POIs
    public IAsyncResult BeginQuery(BoundingBox queryBounds, AsyncCallback callback, object state) {
        // Get the current bounding box for the map
        BoundingBox dataBounds = m_map.XYtoBoundingBox(0, 0, m_map.Width, m_map.Height);

        // Don't get any POIs if the map covers too large an area.
        if (dataBounds.TopLeft.DistanceTo(dataBounds.BottomRight, DistanceUnit.KILOMETERS) > 100) {
            return new SyncResult<SimpleFeature[]>(callback, state, new SimpleFeature[0]);
        }

        // Find all pois on the map -- this can take a while
        Poi[] pois = DataQuery.QueryPoi(dataBounds);

        // Create an array to hold the pois as SimpleFeature objects
        SimpleFeature[] results = new SimpleFeature[pois.Length];
        for (int i = 0; i < results.Length; i++) {
            results[i] = new SimpleFeature(pois[i].Location, pois[i].Type.ToString());
        }

        // Because this is asynchronous, use SyncResult to return the results
        return new SyncResult<SimpleFeature[]>(callback, state, results);
    }

    // Finish up the asynchronous fetching of POIs
    public IEnumerable<IPointFeature> EndQuery(IAsyncResult asyncResult) {
        // Just return the results that were generated by BeginQuery
        return ((SyncResult<SimpleFeature[]>)asyncResult).Result;
    }
}
Generating and rendering clusters

Now that we have a data source, we are ready to generate and render clusters.

  1. Add the following members to the class for your form:

    C#
    private IPointFeatureSource data_source;
    private LocalClusterLayer lcl;
    private ClusterRenderer cr;

    We will use a LocalClusterLayer to fetch features from an IPointFeatureSource (our POISource) and use them to generate clusters. We will use a ClusterRenderer to fetch clusters from the LocalClusterLayer and render them on the map.

  2. Add a Load event handler to your form. It creates the POISource, local cluster layer, and cluster renderer, hooking them all together:

    C#
    private void Form1_Load(object sender, EventArgs e) {
        data_source = new POISource(mapCtrl);
    
        // use the built-in column clustering algorithm.
        // use ClusterVisualizationDot to create cluster images. 
        // ClusterVisualizationDot does not require any summary object,
        // so we do not need to supply a ClusterSummaryDelegate.
        lcl = new LocalClusterLayer(new ColumnClustering(), data_source, null, new ClusterVisualizationDot());
    
        // Create a ClusterRenderer for the cluster layer and assign it to the
        // map so that the clusters are drawn on the map.
        cr = new ClusterRenderer(lcl);
        mapCtrl.Renderer = cr;
    }
Testing your application

Run your application. When it starts up, you will see the map, which then fills with clusters (the clusters may take some time to display):

Try panning and scrolling the map. Within ten seconds of a change, the map updates to show the clusters for the new center and zoom level.