As you may know, Twitter recently introduced geocoding into their tweets, so now when you tweet, if you do so from a device with GPS capabilities, your position is recorded along with your post. That got me thinking… since I travel around a lot, I wonder if I can map my position on Bing maps.
First I thought I could just import my twitter stream as Bing maps says it supports the <geo:point> element, but this didn’t work. I think that may be down to some XSS protection built into Bing maps, though I’m not 100% certain as I didn’t bother to delve into it after I found that it didn’t work. Mainly because it’s not the best solution due to the fact that Twitter ages your posts and eventually they will “fall off the end”. So if you want to be able to record your locations (via your tweets) for a whole year say, then you are going to have to store that information yourself, and that’s what we are going to look at in this post.
So first thing we need is a class to hold the information we want to store, which is Date (including the time) as well as latitude and longitude, kind of the Time And Relative Dimension In Space. Now, I wonder what we could call such a class… :-) Anyway, the class looks like this:
using System;
using DevExpress.Xpo;
namespace sharedModel
{
public class Tardis : XPObject
{
public Tardis(Session session) : base(session) { }
private DateTime date;
public DateTime Date
{
get
{
return date;
}
set
{
SetPropertyValue("Date", ref date, value);
}
}
private decimal lat;
public decimal Lat
{
get
{
return lat;
}
set
{
SetPropertyValue("Lat", ref lat, value);
}
}
private decimal @long;
public decimal Long
{
get
{
return @long;
}
set
{
SetPropertyValue("Long", ref @long, value);
}
}
private long status_Id;
public long Status_Id
{
get
{
return status_Id;
}
set
{
SetPropertyValue("Status_Id", ref status_Id, value);
}
}
}
}
There isn’t anything here that you’ve not seen before so we’ll say no more about it, other than we’ll have to put it in a DLL project as we want to share it between a console and a web project that we are going to go ahead and create now.
Firstly the console project, which is going to fetch and store our most recent tweets:
using System;
using System.Linq;
using System.Xml.Linq;
using DevExpress.Xpo;
using sharedModel;
namespace LocationFetcher
{
class Program
{
static void Main(string[] args)
{
//GS - Specify the SQL Server DB we want to use
XpoDefault.Session.ConnectionString =
@"data source=.;integrated security=true;" +
"initial catalog=TweetLocations;";
//GS - Use a linq query to get that last status_id recorded
XPQuery<Tardis> tardisQuery =
new XPQuery<Tardis>(XpoDefault.Session);
var lastStatusId =
(from t in tardisQuery select (t.Status_Id)).Max();
//GS - If there are no persisted tardis values,
//set the lastSessionId = 1 as Twitter can't handle 0
lastStatusId = (lastStatusId == 0) ? 1 : lastStatusId;
//GS - Fetch Tweets newer than lastStatusId
XDocument timeline =
XDocument.Load(String.Format(
@"http://api.twitter.com/1/statuses/user_timeline.xml?"
+ "screen_name=garyshort&since_id={0}", lastStatusId));
//GS - If there have been no updates since last id then
//we're done here
if (timeline.Descendants("status").Count<XElement>() == 0)
{
Console.WriteLine("No new Tweets to process!");
return;
}
//GS - Add the georss namespaces
XNamespace nsGeoRSS = "http://www.georss.org/georss";
//GS - Persist the status information
using (UnitOfWork uow = new UnitOfWork())
{
//GS - Specify the DB we're using
uow.ConnectionString =
@"data source=.;integrated security=true;" +
@"initial catalog=TweetLocations;";
foreach (var status in timeline.Root.Descendants("status"))
{
//GS - If there is no geo tag then skip this status
if (status.Element("geo").IsEmpty)
continue;
new Tardis(uow)
{
Date = DateTime.ParseExact(status.Element("created_at")
.Value, "ddd MMM dd HH:mm:ss zz00 yyyy", null),
Status_Id = long.Parse(status.Element("id").Value),
Lat = Decimal.Parse(status.Element("geo").Element(
nsGeoRSS + "point").Value.Split(" "
.ToCharArray())[0]),
Long = Decimal.Parse(status.Element("geo").Element(
nsGeoRSS + "point").Value.Split(" "
.ToCharArray())[1]),
};
}
uow.CommitChanges();
}
}
}
}
Taking a look at this script can see that it’s pretty standard XPO stuff. Up front we tell XPO that we are going to be using a SQL Server database and where to find it. Then we use Linq to XPO to retrieve the latest status id and we only ask Twitter for tweets that were posted after this id, this should stop us from having duplicates in the database. After that, we simply iterate across the tweets returned by Twitter and store them in the database.
The next thing we need to do is to create a web script that will return our stored TARDIS entries as a GeoRSS feed. The script to do that looks like this:
using System;
using System.Linq;
using System.Xml;
using System.Text;
using DevExpress.Xpo;
using sharedModel;
public partial class GetTwitterLocations : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
//GS - Connect to the db and return all the locations ordered
//by status id - that's newest tweets last
XPQuery<Tardis> tardisQuery =
new XPQuery<Tardis>(XpoDefault.Session);
var tweetLocations = from t in tardisQuery
orderby t.Status_Id ascending
select t;
//GS - Clear any previous response and state we're returning XML
Response.Clear();
Response.ContentType = "text/xml";
//GS - Instantiate an XML writer to use
using (XmlTextWriter writer = new XmlTextWriter(Response.OutputStream,
Encoding.UTF8))
{
//GS - Set indentation level and start writing the document
writer.Formatting = Formatting.Indented;
writer.Indentation = 3;
writer.Namespaces = true;
writer.WriteStartDocument();
//GS - Identify this as an RSS document
writer.WriteStartElement("rss");
writer.WriteAttributeString("version", "2.0");
//GS - Add the GeoRSS namespace
writer.WriteAttributeString("xmlns:geo",
"http://www.w3.org/2003/01/geo/wgs84_pos#");
//GS - Add in each of the locations
foreach (var t in tweetLocations)
{
writer.WriteStartElement("item");
writer.WriteElementString("title", t.Date.ToLongDateString());
writer.WriteElementString("geo:lat", t.Lat.ToString());
writer.WriteElementString("geo:long", t.Long.ToString());
writer.WriteEndElement();
}
//GS - Close tags, flush and send
writer.WriteEndElement();
writer.WriteEndDocument();
writer.Flush();
writer.Close();
}
Response.End();
}
}
The first thing to note here is that we use the default session, but how does the default session get set to the SQL Server database that we are using? I mean you can’t set it in this method as the session is already established and XPO will throw and exception if you try to change the database after you have established the session. To avoid this you must set the database connection in the Global.asax file, like so:
void Application_Start(object sender, EventArgs e)
{
DevExpress.Xpo.XpoDefault.Session.ConnectionString =
@"data source=.;integrated security=true;" +
@"initial catalog=TweetLocations;";
}
Next, you’ll note, we ask XPO for the TARDIS entries in ascending order. This is because I make numerous posts form the same location. In this example I don’t want to filter that list and only map one tweet per location, instead I want to ensure that the most recent one is “on top”. The rest of the script is fairly well commented and I don’t think it needs further explanation. Drop me a note in the comments if you have any questions.
When you navigate to this page the feed is generated, like so:
Which means the only thing left to do is to write a web page which will show where I’ve been tweeting from recently. And that script looks like this:
<%@ Page Language="C#" AutoEventWireup="true" CodeFile="Default.aspx.cs"
Inherits="_Default" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html>
<head>
<title>Where's Gary?</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<script
src="http://ecn.dev.virtualearth.net/mapcontrol/mapcontrol.ashx?v=6.2">
</script>
<script type="text/javascript">
var map = null;
//GS - Initialise the page
function InitPage() {
GetMap();
AddMyLayer();
}
//GS - Load the map
function GetMap() {
map = new VEMap('myMap');
map.LoadMap();
}
//GS - Add the layer and load in our tweet location data
function AddMyLayer() {
var l = new VEShapeLayer();
var veLayerSpec = new VEShapeSourceSpecification(VEDataType.GeoRSS,
"http://localhost:8341/twitterMap/GetTwitterLocations.aspx", l);
map.ImportShapeLayerData(veLayerSpec);
}
</script>
</head>
<body onload="InitPage();">
<div id="myMap" style="position:relative;"></div>
</body>
</html>
Again this script is well commented and so you should be able to see exactly what is going on. Pointing your browser at this page shows the following map:
Here you can see that I’ve recently been to Germany and if you hover over the pins you can see the latest date that I was there:
Well that’s a bit of fun really but you can see how you can use this to instrument your social network if you so desired. Here I’m just showing the location of my own tweets, but you could just as easily map your followers with with greatest amount of retweets of your post, for example. The possibilities are endless and I’ll leave you to ponder those in your own time, but if you build something cool along these lines, with XPO, then leave a comment or drop me an email and I’ll highlight it on this blog.
Although we are mapping Time And Relative Dimension In Space and we can vary our location (relative dimension) sadly this application will not yet let us vary time, so I guess we can’t compete with the Doctor just yet. Maybe that’ll be an 11.1 feature for XPO – until that does happen, happy XPOing! :-)