Tuesday, May 11, 2010

ADVODNA GPS Track Viewer

We’ve already established that I’m a little geeky. I think taking way too many pictures and videos and recording GPS tracks is fun and I’ve found it very helpful in planning this trip when other people have done the same. With that in mind, I started looking the other day into how to display GPS information stored in .gpx files in Google Maps.

A Google search came up with several samples of people doing similar things and I settled on this one to blatantly steal. In my own defense, there were several others with a lot of the same code which leads me to believe they all cannibalized a Google Maps API sample somewhere. I set out to build my own hybrid that did exactly what I wanted.

Problem #1: I could only get the track information to display on the map if 1) I converted tracks to routes using GPSBabel and 2) renamed the file with a .xml extension. Well, okay, I can see why GMaps might like routes better than tracks and the conversion is fast, but I don’t really want to rename my files to .xml and then have anyone who downloads them have to rename them back to .gpx. Just seems silly.

A look inside the code found that the GPX file was being loaded by the GMaps API with an HTTP call. Thus the API was receiving the file based on the MIME type setting of the server serving it. The files were living on my Internet Information Server 6.0 which knew to return files with a .xml extension as XML files but didn’t know what to do with .gpx files.

Well, what if I could tell my server to load a GPS track viewing page whenever someone requested a .gpx file? That way I could just attach a GPX file to any blog post, it’d be FTP’d to my server by Windows Live Writer, and if anyone clicked the link, they’d see the file right on a map.

More searches revealed the concept of HTTPHandlers in IIS and ASP.NET. Basically, an HTTPHandler is a little bit of code that runs when a certain file type is called. I wrote a simple handler in VB that basically just redirects to my viewer page with the path to the GPX file on the query string.

Here’s the code for the handler:

Imports System.Web

Public Class GPXHandler
    Implements IHttpHandler

    Public Sub ProcessRequest(ByVal context As  _
        System.Web.HttpContext) Implements _
        System.Web.IHttpHandler.ProcessRequest
        Dim Request As HttpRequest = context.Request
        Dim Response As HttpResponse = context.Response
        Dim Server As HttpServerUtility = context.Server

        Response.Redirect("/GPXtoGoogleMaps.aspx?GPX=" &    Request.ServerVariables("SCRIPT_NAME"))
    End Sub

    Public ReadOnly Property IsReusable() As Boolean _
            Implements System.Web.IHttpHandler.IsReusable
        Get
            Return False
        End Get
    End Property
End Class

Problem #2: Now that my server knew what to do with .gpx files, how could I get it NOT to do that when the code in the track viewer page made the HTTP call through the Google API to load the GPX? Well the answer was replacing the GMaps HTTP loading routine:

var request = GXmlHttp.create();
var URL = document.URL;
URL = URL.replace(/\.html$/,"");
URL = URL.concat(".gpx");
request.open("GET", URL, true);

With a server-side call that loads the contents of the GPX file into a text string and feeds it to a client-side script that parses the string as if it was XML (which it is, just with a different extension). This avoids the server trying to activate the HTTPHander ‘cause it’s not an HTTP call, it’s a filesystem call.

Server Side (ASP.NET):

GPX = Server.MapPath(Request("GPX"))

Dim objStreamReader As StreamReader
objStreamReader = File.OpenText(GPX)
      
GPXXML = objStreamReader.ReadToEnd()
GPXXML = GPXXML.Replace(vbCrLf, "")
       
objStreamReader.Close()
objStreamReader.Dispose()

Client Side:

if (window.DOMParser)
        {
            parser = new DOMParser();
            gpxDoc = parser.parseFromString('<%=GPXXML%>', "text/xml");
        }
else // Internet Explorer
        {
            gpxDoc = new ActiveXObject("Microsoft.XMLDOM");
            gpxDoc.async = "false";
            gpxDoc.loadXML('<%=GPXXML%>');
        }

Yeah! It worked! View source and copy some design elements from the Blogger site, add a “Return” link fed by the HTTP_REFERER server variable and we’re in business. Oh yeah, then a “Download” link to an ASPX page that forces a download of the GPX file sent on the query string:

<%@ Page Language="VB"%>
<%@ Import Namespace="System.IO" %>

    <script runat=server>
                               
        Public Sub Page_Load(ByVal sender As Object, ByVal e As EventArgs)
           
            Dim path As Path
            Dim fullpath As String = path.GetFullPath(Request("GPX"))
            Dim name As String = path.GetFileName(fullpath)
            Dim ext As String = path.GetExtension(fullpath)
            If Not ext = ".gpx" Then
                Response.Redirect(Request.ServerVariables("HTTP_REFERRER"))
            End If

            Response.Expires = 0
            Response.Buffer = True

            Response.Clear()
            Response.ContentType = "text/xml"
            Response.AddHeader("Content-Disposition", "attachment;filename=" & name)
            Response.WriteFile(Request("GPX"))
            Response.End()
        End Sub
       
</script>

Try it out. Below is a .gpx file I’m attaching to this post from within Windows Live Writer using the “Attach File” plugin. It’s just a link to a file that will be FTP’d to resources.advodna.com by WLW. When you click on it, the “resources” IIS Server with receive a request to display a .gpx file, fire the HTTPHander which will redirect to the GPXtoGoogleMaps.aspx page with the path to the GPX file on the query string. The ASPX page will read the .gpx file on the server-side and load its contents as a string into a variable before the page even loads. When the page loads, the client-side JavaScript will run and find a value has been set for a text string to parse into XML, which it will do and then feed to the rest of the map display code as if it was loaded by the GMAPS API.

Well, I was pretty happy with it anyway…