Google Maps and user activity integration

Recently, a client approached us to create functionality for their Drupal site similar to what Zappos.com does, but based on user activity of the site. The idea is that we would present the user a map, and plot the latest user activity update as a point on the map using geocoded data based on IP address. Then, after a certain interval of time, we make an ajax call to the server to see if there has been any new activity updates in this interval. Kind of like a Facebook wall meets maps. Sounds easy enough, right?
 

Contrib Modules Used

    Activity Module - I used this to set up what user activities would be recorded. It's actually pretty good and has a decent number of triggers you can set out of the box. For example, "when a user logs in, record [username] has logged in." The module uses tokens from the token module to do the string replacements. The client later asked me to add in the user's picture, and thus I had to write a new custom module to add in the token to the list of available tokens.
    GeoUser - This module is what I used to gather the (approximate) latitude and longitude for a user. Like many other modules, it had an external dependency (GeoCityLite.dat) that needed to be put in the sites/all/libraries directory, but the lookup was actually pretty fast. I didn't do any load testing here or anything, but it actually only does the lookup when a user logs in. (Note: at the time of this post, this module hasn't been updated since April of 2008)

Custom Module Time

I am pretty familiar with the Google Maps API, so I decided to keep it as simple as possible. I created a module that printed out a page that displayed a Google Map with the latest user activity post and got the lat/long from the db and plotted it on a map. That's pretty easy if you just look at the API reference. The tricky part was figuring out the query to combine the tables from the activity and geouser tables. The result of that looked something like this:

$sql = "select message as data, lat as lat, lon as lon, activity.aid
from activity_messages
inner join activity_targets on activity_targets.amid=activity_messages.amid
inner join activity on activity.aid=activity_targets.aid
inner join node on activity.uid=node.uid
inner join geouser on activity.uid=geouser.uid
order by activity.aid desc
limit 1";
$result = db_query($sql);

All you need to do then is get the message, and the lat/lon pass it off to the Google Maps API, and you get a map. So now that I had a map, I looked at the Zappos site in firebug and figured out that they were just doing an ajax call to the server every 5 seconds or so. So I wrote a couple of functions in my module:

function custom_map_menu() {
  $items['map_update'] = array(
    'title' => t("Maps Update"),
    'page callback' => 'custom_map_update',
    'access arguments' => array('access content'),
  );
  return $items;
}

function custom_map_update() {

  if(is_numeric($_GET['time'])) {
    $end_time = $_GET['time'];
    $start_time = $end_time - variable_get("custom_map_seconds",5);

  $sql = "select message as data, lat as lat, lon as lon, activity.aid
  from activity_messages
  inner join activity_targets on activity_targets.amid=activity_messages.amid
  inner join activity on activity.aid=activity_targets.aid
  inner join node on activity.uid=node.uid
  inner join geouser on activity.uid=geouser.uid
  where activity_targets.uid=0 and activity.created >=%d and activity.created <= %d
  order by activity.aid desc
  limit 1";
  $result = db_query($sql,$start_time,$end_time);
  $array = db_fetch_array($result);
  $array['data'] = str_replace('"',"'",$array['data']);

    if(!empty($array['lat']) && !empty($array['lat'])) {
      print json_encode($array);
    }
  }
}

In the code above, the hook_menu just maps a url to a function call. In the function custom_map_update() I pass in the current unix timestamp and then subtract 5 seconds from it to get the start_time and end_time. I then pass those values to my DB query and if I have results, I encode the results in JSON.

In the function that I used to display the initial map, I included a jquery.timer.js so that I could just call a javascript function and pass it a value and it would repeat. In this function (again that displays the intiial map, I integrated the code for the timer.js and the Google Maps API:

<script type='text/javascript'>

      <?php $now = time();?>
      <?php $milliseconds = variable_get("custom_map_seconds",5) * 1000;?>
      $.timer(<?php print $milliseconds;?>, function (timer) {
          var time = <?php print $now; ?>;       
          time = time + (i*<?php print variable_get("custom_map_seconds",5);?>);
         
          marker.setMap();
          infowindow.close();
          var data = $.getJSON('/map_update?time='+time+'',function(data) {
           
            var latlng = new google.maps.LatLng(data.lat,data.lon);
            marker = new google.maps.Marker({
                              position: latlng,
                              map: map,
                              flat: true
                            });
            infowindow = new google.maps.InfoWindow({content: data.data, maxWidth: 300});
            map.setCenter(latlng);
            infowindow.open(map,marker);
            return
           
        });
        i = i+1;
        marker.setMap();
        infowindow.close();
      });
</script>

That's the meat of it. What's going on here is every 5 seconds (based on the timer function call), I'm doing an ajax call to my custom_map_update() function and passing it the current unix timestamp so that it can determine exactly which interval to query the database on. It then gets the contents of the JSON object (if it finds any), prints the marker and the information window, and recenters the map.
Final Thoughts

This solution worked out pretty well. But the end result only works well if a LOT of people are performing activities and you have templates set up to record them. Something that would probably be an easier load on the server than doing that nasty query every N seconds would be to do the query once, and return the latest 100 results in ascending order. Then you could just loop through the JSON object to print out the messages and the points on the map. Why would I consider doing it this way? Mostly for performance reasons. If we were willing to sacrifice a certain degree of fresh content, I think it would end up being a better user experience because after every N seconds, you actually get a new result and the map re-centers itself. On the bottom of the front page of Drupal.org they did something similar to what I am describing here (without the recentering of course because you are looking at a map of the entire world).

Share This