Earlier this month I pushed up a new feature to the Adobe Groups site - a map of all the user groups. (And by all I mean those who entered geographic information.) This feature makes use of the Google Map API and the new CFMAP tag in ColdFusion 9. I thought folks might want a little explanation of the code behind this feature. I don't put this out as the most efficient maps demo or the coolest, but hopefully it will help others.

To begin, let me talk a bit about how I handled the data portion. I knew that I would need longitude and latitude information for user group addresses. While ColdFusion's cfmap supports creating markers on addresses, if you ask Google to geocode too many of them you will get an error. (See this blog post for more information.) For my solution I added three properties to my Group entity:

property name="address" ormtype="string"; property name="longitude" ormtype="string"; property name="latitude" ormtype="string";

I then exposed a free form text field on the user group's setting pages to allow user group managers to enter any address they wanted. They could use something vague, like a zip code, or enter a full address with a specific street location. I simply suggested that UGMs first try their address on the main Google Maps site to see if they liked the result.

Once a manager entered their address, I needed to convert this address into a longitude and latitude value. Luckily Google provides a nice API for this and you can find a ColdFusion wrapper for it on RIAForge. I made the call that even with Google providing (normally) pretty swift APIs, I'd do the geolocation in a background process. So now whenever a UGM makes a change to their address, I clear any existing longitude and latitude. I then created a simple scheduled task to handle geolocation.

<cfquery name="getaddresses"> select name, address, id from `group` where address is not null and address != '' and (longitude = '' or latitude = '' or longitude is null or latitude is null) </cfquery>

<cfoutput>#getaddresses.recordCount# address to geocode.<p></cfoutput>

<cfloop query="getaddresses">

<cfset geocode = new googlegeocoder3()> <cfset geo = geocode.googlegeocoder3(address=address)> <cfif geo.status is "OK"> <cfquery> update group set longitude = <cfqueryparam cfsqltype="cf_sql_varchar" value="#geo.longitude#">, latitude = <cfqueryparam cfsqltype="cf_sql_varchar" value="#geo.latitude#"> where id = <cfqueryparam cfsqltype="cf_sql_integer" value="#id#"> </cfquery> <cfoutput>Set geo info for #name# at #address#<br/></cfoutput> <cfelse> <!--- possibly set a flag to not try again ---> <cfoutput><font color='red'>Bad geo info for #name# at #address#</font><br/></cfoutput> </cfif>

</cfloop>

Done.

I don't think I need to go over the code above - as you can see it's rather simple. I also did not make use of HQL for the queries. I certainly could have but for the first draft, this worked fine. The end result is that every 30 minutes, I look for groups that need geocoding and try to do that operation.

Ok - so now I should be done. I can just get a list of groups that have longitude and latitude information and dump them into a cfmap tag, right? Well that's what I thought at first. I then discovered that some user groups dared to share a room with other groups in the area. When Google is asked to put two or more markers in the exact same spot, only the last one will render. Barnacles. I decided to try something else. I created a structure keyed by a location. Each group will place their marker HTML into a value specified by the key. Now if two groups meet in one place, both of their sets of information will be placed on the one marker. Here is a portion of the code from the view. Assume longlatdata is an array of ORM entities for groups where they all contain a geocode information.

<cfset longlat = {}> <cfloop index="g" array="#longlatdata#"> <cfset lat = g.getLatitude()> <cfset lon = g.getLongitude()> <cfset key = lat & " " & lon> <cfif not structKeyExists(longlat, key)> <cfset longlat[key] = ""> </cfif> <cfset str = "<img src=""#g.getAvatar()#"" align=""left"" width=""75"" height=""75"" style=""margin-right:5px""> <a href=""/group/#g.getId()#"">" & g.getName() & "</a><br/>" & g.getAddress() & "<br clear=""left""/><br/>"> <cfset longlat[key] &= str> </cfloop>

<cfmap centeraddress="St. Louis, MO, USA" name="ugmMap" showcentermarker="false" zoomlevel="4" width="900" height="500" type="hybrid"> <cfloop item="loc" collection="#longlat#"> <cfset lat = listFirst(loc, " ")> <cfset lon = listLast(loc, " ")> <cfmapitem latitude="#lat#" longitude="#lon#" markerwindowcontent="#longlat[loc]#"> </cfloop> </cfmap>

Again - I assume this is all pretty simple but let me know if anything needs further explanation. The choice of St. Louis to center the map was totally arbitrary. So, any comments? Right now I'm a bit worried about handling more and more groups with addresses. Luckily there is an open source MarkerManager to handle large sets of markers. I plan on looking at that next.