Home > Flex, Google Maps API, tutorial > Twitter-Google Maps Mashup

Twitter-Google Maps Mashup

April 26th, 2009

Love it or hate it, its hard to ignore Twitters recent explosion in popularity. Not only are they super popular to the masses, but they are nice enough to give us an API to use in order to create our own twitter applications. I don’t use it too much, but I figured it would be worth checking out at least from an API stand point.

Turns out, the API is actually just a series of webservice calls that just return a json or atom string, so it is extremely easy to use (and it explains the mass amount of twitter programs out there). I don’t feel it would be too beneficial to create a bunch of ‘This is how you search for people’, ‘This is how you do a post’, so I decided to turn this into a mashup between this and google maps.

What I decided to do an article on is how to take twitters geocoded search feature and place them on a map within google. This article will explain how to call Twitters search service, parse that and then using Googles Geocoder, plot where those users are twittering from.

To make life easier, here is what our end result will look like.
googlewittermashup1

Check out the demo here.

There are a number of things going on here. We are calling/parsing the twitter service/results. We are using Google Maps geocoder to decode the results location to give us a latitude and longtitude. We are then plotting those on the map. Once we do that, we then call up the twitter post when we click on the marker and display the content in a custom information window.

Before any coding goes on, read the section in this article on how to setup your environment for google maps(its the 2nd paragraph). Once you have that setup, you are ready. Since the Twitter API is just a series of webservice calls, you don’t need to download anything to use it.

To add the map to Google Maps, as you might have seen in previous tutorials, first you add it in your mxml:

<maps:Map xmlns:maps="com.google.maps.*" 
id="map" 
mapevent_mapready="onMapReady(event)" 
width="100%" height="100%"
 key="{GoogleConstants.KEY}"/>

The put this in your onMapReady function, initialize everything here:

public function onMapReady(event:MapEvent):void {
  geocoder = new ClientGeocoder();
  geocoder.addEventListener(GeocodingEvent.GEOCODING_SUCCESS, geocoder_geocodingSuccess);
  geocoder.addEventListener(GeocodingEvent.GEOCODING_FAILURE, geocoder_geocodingFailure);
  map.addEventListener(MapMouseEvent.CLICK,mapClicked);
  centerPoint = new LatLng(40.714036986015984,-74.00337009008788);
  map.setCenter(centerPoint, 14, MapType.NORMAL_MAP_TYPE);
  var poly:Polygon = MapUtils.drawCircle(40.714036986015984,-74.00337009008788, 1.0, 0x000080, 1, 0.75, 0x0000FF,.5);  
  map.addOverlay(poly);
}

I decided to drop the default lat/lng over Manhattan (popular enough place to find tweets). The event listeners added for the geocoder is going to be used for when we try to get a location from our twitter posts. We also add an event listen for our map to listen for mouse clicks (to set a new location). After that, I draw the circle around the center point. I “borrowed” the code to draw the circle from another example. No need to reinvent the wheel.

Now you are going to notice that there really isnt too much to using twitters API. First you setup a httservice within your mxml:

<mx:HTTPService
 id="twitterService"
 resultFormat="text"
 result="parseResults(event)"
 fault="fault(event)"
 showBusyCursor="true" />

To make the service call, we hook it into a button to trigger that, which looks like this:

public function getGeoCodes(event:MouseEvent):void{
  var s:String="";  
  if (username.length >0)
    s="&q=from%3A"+username.text;
  twitterService.url = "http://search.twitter.com/search.atom?geocode="+centerPoint.lat()+"%2C"+centerPoint.lng()+"%2C"+stepper.value+"mi"+s;
  geocodesButton.enabled=false;
  geoAlert.visible=true;
  setTimeout(disableGeoCodeButton, 10000);
  twitterService.send();
}

As you saw in the picture, we are able to search by both a radius from a center point (the circle drawn on the map) and optionally by a username. All we do here is call the service using atom and passing the latitude and longitude, along with a user name to it. This event listener is what is attached to the Get Geocodes button. After that, it goes to the parseResults listener.

public function parseResults(event:ResultEvent):void{
    var rawData:String = String( event.result );
    var arr:XMLDocument= new XMLDocument(rawData);
    var decoder:SimpleXMLDecoder = new SimpleXMLDecoder();
    var data:Object = decoder.decodeXML(arr);
    var array:Array = [];
    if (data.feed){
        var array = ArrayUtil.toArray(data.feed.entry);
        for each (var o:Object in array){
           markerManager.addTwit(o);
           geocoder.geocode(o["location"]);   
        }
    }
}

Once it comes back we parse out our results. The entire twitter object comes back within the returned XML. First we save that tweet in an object I call MarkerManager (maybe should be called TwitterMarkerManager?). After that, we then use Googles Geocoder to find a location to plot for the location given within the specific tweet.

public function geocoder_geocodingSuccess(e:GeocodingEvent):void{
  var point:LatLng = e.response.placemarks[0].point as LatLng;
  var nm:NamedMarker = new NamedMarker(e.response.name,point);
  addMarker(nm,point);
}
 
public function addMarker(nm:NamedMarker,point:LatLng):void{
  var marker:Marker = new Marker(point);
    markerManager.addMarkerName(nm);
    marker.addEventListener(MapMouseEvent.CLICK,getTwit);
    map.addOverlay(marker);  
}

A Marker is an object within the Google Maps API. It is an object that is placed on the maps and gives us (by default) that nice red looking pin on the map. Unfortuantely, it doesnt have a name property for me to ues on this, so I just extended Marker and added the property, giving us NamedMarker. The event.response.placemarkers (which seems to always turn an array of length 1??) is where the result of what googles says the latitude/longitude your location mapped to. We then add the NamedMarker with the event listener for retrieve the tweet associated with it to the MarkerMananger object. After that, we add the Marker to the map.

For the sake of clarity, lets also take a look at this ill-named MarkerManager:

public class MarkerManager{
  private var _markers:ArrayCollection
  private var _tweets:ArrayCollection;
 
  public function MarkerManager(){
    _tweets = new ArrayCollection();
    _markers = new ArrayCollection();
  }
  public function addMarkerName(nm:NamedMarker):void{
    _markers.addItem(nm);
  }
  public function getTwitByLatLng(latlng:LatLng):ArrayCollection{
    var arr:ArrayCollection = new ArrayCollection();
    for each (var nm:NamedMarker in _markers){
      if ((nm.getLatLng().lat() - latlng.lat() == 0 && nm.getLatLng().lng() - latlng.lng() == 0)){
        var s:String = nm.name;
        arr= getTwit(s);
      }
    }  
    return  arr;
  }
 
  public function cleanMarkerInfo():void{
    _markers=new ArrayCollection();
    _tweets = new ArrayCollection();
  }
 
  public function addTwit(twit:Object):void{
  _tweets.addItem(twit);
  }
 
  public function getTwit(name:String):ArrayCollection{
    var arr:ArrayCollection = new ArrayCollection();
    for each (var t:Object in _tweets){
      if (t.location == name)
        arr.addItem(t);
    }
    return arr;
  }
}

It’s a pretty straight forward utility class. It just holds the markers and tweets and then retrieves them.

At this point, you are able to retrieve tweets, add them to your map and mark them with a Marker object. Now we want to view them. We need to create a customer object in order to display the information we want. Before we look at that, this is the event that handles a click on a marker

public function getTwit(event:MapMouseEvent):void{
  var arr:ArrayCollection = markerManager.getTwitByLatLng(event.latLng);
  var tc:TwitterControl = new TwitterControl();
  var marker:Marker = event.target as Marker;
  tc.setTweets(arr);
  marker.openInfoWindow(new InfoWindowOptions({customContent:tc,width:400,height:200,drawDefaultFrame:true}));
}

First we get any tweets with that latitude an longitude associated with that marker we just clicked. Within the manager, it first checks the lat/lng against all the saved NamedMarkers. If it finds a match, it will go grab the tweet with that name then return it. It then sets the property to the TwitterControl, which is the custom InfoWindow object.

<?xml version="1.0" encoding="utf-8"?>
<mx:VBox xmlns:mx="http://www.adobe.com/2006/mxml" width="400" height="200" >
  <mx:Script>
    <![CDATA[
      import mx.collections.ArrayCollection;
      import com.google.maps.overlays.Marker;
      import mx.controls.Text;
      public var currentTweet:Object;
      public var tweets:ArrayCollection;
 
      [Bindable]public var totalText:String;       
      [Bindable]private var _content:String;
      [Bindable]private var _author:String;
      [Bindable]private var _time:String;
 
      private function setTweet(index:int):void{
        var tweet:Object = tweets.getItemAt(index);
 
        if (tweet){
          currentTweet = tweet;
          _author= tweet.author.name;
          _content = tweet.title;
        }
      }
      public function nextClick(e:MouseEvent):void{
        var i:int = tweets.getItemIndex(currentTweet);
        if (i+1 < tweets.length)
          setTweet(i+1);
      }
      public function prevClick(e:MouseEvent):void{
        var i:int = tweets.getItemIndex(currentTweet);
        if (i-1 >=0)
          setTweet(i-1);
      }
      public function setTweets(arr:ArrayCollection):void{
        tweets = arr;
        totalText = "Total tweets : " +tweets.length.toString();
        setTweet(0);
      }
    ]]>
  </mx:Script>
  <mx:Text id="total" text="{totalText}" />
  <mx:TextArea editable="false" wordWrap="true" width="100%" text="{_author}" id="author"/>
  <mx:TextArea editable="false" wordWrap="true" height="50" text="{_content}" width="100%" id="content"/>
  <mx:HBox>
    <mx:Button click="prevClick(event)" label="Prev"/>
    <mx:Button click="nextClick(event)" label="Next"/>
 
  </mx:HBox>
</mx:VBox>

Nothing really special about this. Just a few display options and controls. This is what will go through the arrayCollection of tweets though and display the author and content of that tweet.

Here is one thing to note. When you do a search, you are given the last 15 posts in that area. You might notice though, you are only getting about 4 markers? Well if you look back at the beginning, we added a listener on the geocoder for failure. Why is it that on the picture I provided has around 8 to 10 of them? After looking what was failing, I noticed some of them were things that didnt make sense (IE: LIVIN IT UPTWN IN QUEENZZZZ) and as awesome as google is, that is not being mapped to an address.

What is more important, I did notice something else was failing to map to an address, which was posts from iPhones. Another interesting note, when locations come back, normally it would be something like “Brooklyn”, “NYC”, “Queens”, “Astoria”, etc, which were mapped to generic locations, not exact location of the tweet. BUT, a post from an iPhone gives their latitude and longitude (which I am assuming is at the time of the tweet). Now that is awesome/creepy! So how do we parse those?

Here is what the geocoder failure event looks like:

public function geocoder_geocodingFailure(e:GeocodingEvent):void{
  if (e.name.indexOf("iPhone") > -1){
    //iphone found
    var point:LatLng = MapUtils.cleanIphone(e.name);
    var nm:NamedMarker = new NamedMarker("iPhone", point);
    addMarker(nm,point);
  }
}

We scan the name (which is the location property on the tweet) for the word “iPhone” and if we have a match, we then get the lat/lng from it and add it to our marker list. We still have to change a few other things . In our addTwit function in the manager, change it to this:

public function addTwit(twit:Object):void{
 if ((twit.location as String).indexOf("iPhone") > -1){
   //handle iphone
   var latlng:LatLng = MapUtils.cleanIphone(twit.location);
   twit.location = "iPhone: "+latlng.lat()+","+latlng.lng();
 }
 _tweets.addItem(twit);
}

then change getTwitByLatLng to this:

public function getTwitByLatLng(latlng:LatLng):ArrayCollection{
  var arr:ArrayCollection = new ArrayCollection();
  for each (var nm:NamedMarker in _markers){
    if ((nm.getLatLng().lat() - latlng.lat() == 0 && nm.getLatLng().lng() - latlng.lng() == 0)){
      var s:String = nm.name;
      if (s.indexOf("iPhone") > -1){
        s = nm.name+": " + latlng.lat()+","+latlng.lng();
      }
      arr= getTwit(s);
    }
  }  
  return  arr;
}

Now when you run this, especially in dense areas of Manhattan, we will see a bunch more pop up, all with the ever so interesting and informative twitter posts that everyone is dying to read! Side note, I did notice another location failing that also gave its lat/lng, but I felt for the sake of the tutorial, I did not want to cover it with parsers. Feel free to add it in the source yourself though.

Here are some other things to keep in mind. One, not that I think I have to worry about it, but the Google Maps API allows 15,000 geocode searches in a 24 hour period. If you really want to play with it, I would recommend you download the source/swf yourself and play around with it. As for the twitter API, they limit you on searches as well (hence me putting a 10 second timer on the button). What is more annoying, they tell you that you get 100 searches or something like that an hour. Unfortunately, they do not tell you how many geocode searches you get. They just say it is different. Long story short, depending on how popular this article gets, it might break it for the day hehe.

On top of that, I tried testing it out on myself and failed. I put up a new twitter account, made sure my location was set, and placed a tweet up there. I was let down when I could not find myself. Then after examining the twitter post through their own search API by username, I noticed my location was not set on the posts, explaining why I could not find myself. Feel free to try it out with an iPhone though.

If you did not notice, I opted to use .atom results over json results. I would have preferred to use json, but I could not get the json to parse correctly using the built in json parser in actionscript3 corelib. Not sure why, but when I used atom, everything was fine. Lastly, the only reason I added the username portion is if someone is using an iPhone, the results are even better, as you get to see where they actually were. Some results I got from an iPhone user would always point to the same location (shrug?), but a few of them mapped out their day on the map. :)

I feel Twitter did a really good job at making it simplistic to utilize their services. It really cannot get that much easier than just calling a webservice with some query results. It does expalin the extremely large amount of Twitter applications out there. This project somewhat ballooned out of control with the amount of things I wanted to do with it, especially considering I just wanted this to be a post on using the Twitter API. There are many other functions with the Twitter API that can be used (posting, searching, etc) that is worth checking out.

Questions? Concerns? Feel free to comment!

Check out the source here.

Check out the demo here. Please heed the warning I gave before on possibly crashing it though. Once again, if you really want to play with it, I recommend just downloading the source and running it on your machine. Thanks!

Flex, Google Maps API, tutorial , , ,

  1. April 27th, 2009 at 04:19 | #1

    Twitter is like a breath of fresh air on the Social Media scene. I have been on it for just a few weeks now and I have met several interesting people. It is a platform to network with people you would like to meet in real life.

    http://Spryka.com

  2. May 3rd, 2009 at 20:04 | #2

    This is really cool. I hope you keep working on it and polish it off.

  3. Supriya Tenany
    September 16th, 2009 at 06:56 | #3

    what is this event called when the mouse moves over the map? Also, how can I drag the marker with this mouse movement over the map?

  4. Chris
    November 3rd, 2009 at 13:20 | #4

    Great Tut!!!

    I have one question: How can I add the User’s profile pic?

    Thx!

  5. February 13th, 2010 at 03:06 | #5

    I tried copying your the folders in your com directory to the com folder in my project, but none of the components come through to my project. What could be wrong?

  6. Peter
    April 20th, 2010 at 23:03 | #6

    Hey, your tut was a great start point for my project and everything works fine! but I was wondering how I could include the User’s profile pic and how to use my own custom icons instead of Google’s default red balloon?

    If you could point me in the right direction it would be GREATLY APPRECIATED! :D

    Thanks

  7. July 19th, 2010 at 00:22 | #7

    A house is just a place to keep your stuff while you go out and get more stuff.

    Sent from my Android phone

  8. October 19th, 2011 at 22:07 | #8

    Are you offer applet development services? If so, I’m in need of an applet that can be embedded in a website to provide a heat-intensity map of twitter traffic based upon a twitter search. Please contact me.

  1. April 17th, 2010 at 03:30 | #1
  2. April 22nd, 2010 at 02:14 | #2