Too many pins on your map?
One of the most regularly used framework in the iOS SDK is certainly the Map Kit framework, which lets you easily embed and annotate maps in your applications. In this article we will show you an innovative way to make the best out of this framework.
Map Kit: a convenient but limited framework
In general, Map Kit usage follows this pattern:
- Instantiate an annotation (a model object which conforms to the
- Add your annotation to your map (a
- Tell the map’s delegate what your annotation will look like when displayed on the map
Fairly simple, right? Not quite so: as soon as you have to display a large number of annotations, this naive approach doesn’t work anymore. Here’s why it doesn’t scale:
- Performance: your device will struggle with all those annotations and you’ll experience dramatic frame drops when panning or zooming the map.
- Usability: most annotations will overlap on screen, making them difficult to select.
Lately, when we were developing a transportation app, we had to display over a thousand train stations on the map. As the standard implementation wasn’t smooth enough to our liking, we decided it was time to do some research and figure out a solution to this prevalent problem.
The solution is called clustering. The concept is simple: group together annotations (eg. photos, stations, restaurants: you name it) which are close to each other. But how do you define close? That’s all the question.
In fact, you probably already use clustering: you can find it in action in the Photos app shipped with the iPhone.
Unfortunately for us developers, Apple didn’t think it was a good idea to expose this neat feature in a public API. So we are on our own on this one.
State of the art
There are already few other clustering implementations around. Some of them are commercial (see Superpin or DTClusterMaker), some of them are open source projects (see REVClusterMap). Though these could be perfectly valid for some uses, neither of them seemed to really fit our expectations.
Clustering is an active field of research and there are already many interesting solutions to this problem. However, in our case, three constraints were prevalent:
- The complexity of the algorithm must be as low as possible to allow for fast displaying of the map and smooth zooming and panning.
- We want to be able to animate the transitions when an annotation expands to many (or when several annotations collapse into a single one), like in the Photos app. Thus, our algorithm should be deterministic (no random behavior is allowed while choosing the clusters).
- We want a natural look which implicitly shows the density of the annotations (no grid/tile effect).
Grid effect in Superpin and REVClusterMap
We tried several methods during our research. Let’s see how they perform:
- Naive perfect method: we started grouping the annotations by evaluating the distance between each annotation to every other annotation. The complexity of the resulting algorithm was obviously (really) bad.
- K-means: k-means clustering is a popular clustering method used in many domains including statistics and image segmentation. It leads to an easy-to-implement iterative algorithm. Unfortunately, it proved to be quite slow on our devices. And more important, it is not deterministic, thus we can’t animate during zooming and panning.
- Quadtree: Using a quadtree structure is also a valid approach (used by Superpin, for example). The idea is to build the tree when adding the annotations to the map. Each node respresents the partitioning of the space into four regions. This step can be done offline (read “before showing the map”), so it’s ok if it takes a little time to compute. Updating the clusters to display each time you move the map can then be done quickly, which is nice. There is one major drawback though: you get this infamous grid effect.
We ended up using our own variant of the quadtree approach using another structure: a k-d tree, which in our case consists in a binary tree where the space is subdivided into two regions by a line at each level. What is nice about this structure is that you have full control over where you split your data. For example, you can choose to build a perfectly balanced binary tree by ensuring we have the same number of annotations in every newly created sub-region. But that’s not what we want. Instead, what we do at each step of the construction of the tree is:
- do a Principal Component Analysis to estimate the principal vector describing the distribution of our annotations
- compute the mean of the coordinates of the points
- for each point (x,y), compute the scalar product between the principal vector p and the vector u (x-xMean, y-yMean), xMean and yMean being the mean of the two coordinates of our annotations. The sign of this product determines which partition the point belongs to.
Once the k-d tree is built, we just have to look for clusters through the tree for a given visible region of the map. We do a simple breadth-first search through all nodes of the tree belonging to the visible region of the map, until we reach a maximum number of clusters that we expose as a parameter in our API. So every time you pan or zoom the map, we won’t display more annotations than you need. Plus, you will get nice grouping/degrouping animations because our structure is consistent at any depth in the tree.
Et voilà! If there is one thing to remember of this, it’s that the iOS SDK is a great piece of software that’s just waiting for us to experiment with. It’s actually a lot of fun to work on extending the possibilities of all these frameworks we have in hand.
If you want to see our clustering in action, you can download the free app we developed this method for: RailTime. As you can see on this screenshot, the clusters accurately follow the path of the railway. This is something you wouldn’t see if we were using a quadtree.
We hope you liked this little research article and even maybe learned a little something. If you have any question, don’t hesitate to react to this blog post on Twitter: @applidium.
[UPDATE] We released our library under a BSD license!