gmaps v3 Cluster Manager

2010-11-21

So google maps v3 has been released for a while now. In fact, it seems to be a good year and a half if this blog post has any credibility :p Yet I've not taken the time to play around with it.

But last week I was asked to create a map for schaatsen.nl with a few dots. So I figured a marker cluster manager would be quite useful. And I thought upgrading my existing script would be done quickly. Bad judgment call.

What I did not expect is that the v3 api does not expose nearly the same information as v2 did. Really, what the hell were they thinking? I can only assume they did this to fight application bloat. And what I had forgotten is that I was terrible at JavaScript back then :p Well, maybe not really. But using objects as arrays? Ouch...

Anyways, I used half this weekend to rewrite the script so it would work with v3. First of all, here's the result. A working marker cluster manager for google maps v3. The rest of this blog post will explain the pains I went through.

First of all, my code was bloated to heck. I mean, factories? I can clearly recollect which book I had read prior to writing that piece of junk. Factory patterns probably looked good at some point, but my full rewrite of this script will not have them so explicitly. They over complicate the code for the end user. And myself.

It wasn't long before I hit a snag at gmap's end. It's no longer possible to simply translate a point to a latlng and back. The v3 api simply doesn't have the method. It has the old methods on a MapCanvasProjection object, but it didn't seem to be the intended way and there was no zoom parameter, meaning you'd have to add one object for every zoom level. Note that there are 18 by default. Nice.

So after some searching on the group I found a post explaining it. I wrote two functions and posted it in my notes, to be never forgotten again.

The next problem was a little bigger. But let me first explain how the cluster manager works.

Markers are basically just images on a separate layer from the map. When two of these images overlap, they obscure each other, effectively blocking access to one or the other. In certain applications, it's very common to have lot's of markers on or near the same position. Like zoomed out on world level, all the points from the same (small) country will be very near to each other. Large markers are more likely to block other markers. Writing a blog post from the same location every time will put the marker on the same position every time (a reason blogs don't usually have maps ;)). Etc.

So the solution is using a cluster manager, which will detect these overlapping markers and group them together (creating a cluster). The core of this mechanism is really simple, checking the bounds for the image used for each marker and checking whether they are overlapping with the bound of any other marker (or cluster). If overlapping with another marker, combine the two in a new cluster. If overlapping with a cluster, add the marker to the cluster.

Every zoom level has it's own set of clusters. This is where things get computationally a little bit bigger. There are about 18 levels for regular maps. So you have to recompute the clusters on every level, as they are normally different for every level. (My script does this, of course, as it's the only way.)

To do this we obviously need to know the bounds of the image. And that's a problem in v3. It clearly needs to compute these bounds. In fact, it has these bounds. But it doesn't expose them... :(

To create a marker you have to ways of specifying its icon. Either give a string that points to the image to be used as is, or supply a MarkerImage object that tells gmaps which image to use. Optionally you can use it to create markers using a classic sprite map, change the position of the anchor or to resize the original image to certain dimensions. Using the object is our problem here, as the object itself has virtually no api. And the api that exists is broken. Meh.

So according to the api the object should expose a coord and type property. However, it doesn't expose the coord so I can't use it. Otherwise I could've worked around the rest. Pity.

When you check marker.getIcon() it will return whatever you used to create the icon. For the object you will see five properties (same as the five arguments of the constructor), but only one is not changed by the minifier; anchor. So we get anchor, albeit undocumented (==bad). We can get the url (it's the only string type property in the enumeration), also undocumented. And we can get the origin because it's the only other Point, but it's useless on it's own.

There exists another marker property though. It's called pixelBounds. It is also undocumented, but it is exactly what we want! Together with the origin of course. The pixel bounds are the bottom-left and top-right corner of the marker image, relative from the anchor. If only this was exposed... Alas, no such luck.

So as it stands, I decided to go the easy and safe way. The cluster manager right now demands the size and, if you changed it, the anchor to be supplied when you add markers. There's no documented alternative, I checked.

So the cluster manager works okay. You can supply click events (setting the CMeventClick property on the marker before adding it to the manager) which are accessible by clicking the marker and the info window of the cluster. You can also supply a dragend event (through the CMeventDrag property of the marker), which will fire after the manager re-processed the marker in the clusters. Note that after dragging, the marker is removed from and added to the map to fix attached events and cleanup.

You can also change the events using the changeMarkerEvent method of the manager. Easily extended, although I'm not happy with this mechanism. It's legacy.

As I said it's a conversion from an old script. It's a little bloated and could use a rewrite to something cleaner. But it works, and is relatively easy to use.

Marker Cluster Manager for Google Maps v3

Hope it useful for you :)