Thursday, September 6, 2012

Virtual Cycling part 2

Yesterday I explained my new project and described the hardware. Today I'll start on the software. Although I write this in Visual Studio, all you really need is notepad. I'm going to write this specifically for Google Chrome because it handles Google Maps' memory leaks better. I'm also going to make the software very barebones and simple.

The best way to start this is to create a new folder under Program Files called RouteWalker. In that folder, create a new HTML file called RouteWalker.html. You may need to create a new text file and rename it when you're done.

Whether you created an html or a txt file, make it look like this.

 
<!DOCTYPE html> 
<html style="height:100%">
<head>
</head>
<body onload="initialize()">
</body>
</html>

This is your basic empty web page except it will call a javascript function called initialize() when it has loaded. We will write initialize() soon. We are going to use three Google javascript libraries and write our own javascript so add the following lines between the <HEAD> tags.

 
<script type="text/javascript" src="http://www.google.com/jsapi"></script> 
<script type="text/javascript" src="http://maps.googleapis.com/maps/api/js?sensor=false"></script>
<script type="text/javascript" src="http://maps.google.com/maps/api/js?sensor=false&libraries=geometry&v=3"></script>
<script type="text/javascript">//<![CDATA[

    //]]>
</script>

The rest of today's javascript will go into the blank line above. The initialize() function will start by loading a gpx file (a type of xml file) into an array of point objects. Our point objects will contain a longitude, latitude, elevation, total distance to this point, distance to next point, and bearing to next point. This requires some math. With that information we can walk the route. All calculations are in metric. To start with we need to define a few variables. Add the following declarations into the blank line above.

 
var R = 6371;   // radius earth in km
var route = []; // an empty array
var courseLengthkm = 0; // course length

Let's write initialize(). All it does for now is call a function that will load the gpx file, but later it will do a little more. I've hardcoded the gpx file name for simplicity. Here is the initialize() function - add it after the declarations above.

 
function initialize() {
    loadRoute('MyRide.gpx');
}

The meat of the javascript today is the loadRoute() function. It starts by loading the specified file into an XML document. Because I'm coding for Chrome I'm using an XMLHttpRequest object. Other browsers load files in different ways. Insert the following code  below the code above.

 
function loadRoute(filename) {
    var xmlDoc;
    var nodes;
    var distancem;
    var bearing;
    var rideDistancekm = 0;

     try {
            var xmlHTTP = new window.XMLHttpRequest();
            xmlHTTP.open("GET", filename, false);
            xmlHTTP.overrideMimeType("text/xml");
            xmlHTTP.send(null);
            xmlDoc = xmlHTTP.responseXML;
    }
    catch (ex) {
        alert("LoadXML: " + ex.message);
        return;
    }

Now we have the file loaded and parsed into XML. The coordinate data we need will either be in a series of rtept or trkpt elements. They both have the same structure, just different names. Let's go get them.

 
nodes = xmlDoc.getElementsByTagName("rtept");
if (nodes.length == 0)
    nodes = xmlDoc.getElementsByTagName("trkpt");


So now nodes is a collection of XML elements. Let's iterate through them and build our route array. For each XML element we create a new point object and then populate it. Note we do not process the last element because it has no 'next' element. Lastly we add each point to the route array.

    try { 
        for (var i = 0; i < nodes.length - 1; i++) {
            var point = { 'latitude': 0, 'longitude': 0, 'bearing': 0, 'distancem': 0, 'totalAtStartkm': 0, 'elevation': 0};
            point.latitude = parseFloat(nodes[i].getAttribute('lat'));
            point.longitude = parseFloat(nodes[i].getAttribute('lon'));
            var nextLat = parseFloat(nodes[i + 1].getAttribute('lat'));
            var nextLng = parseFloat(nodes[i + 1].getAttribute('lon'));
            var eleNode = nodes[i].getElementsByTagName('ele')[0];
            point.elevation = parseFloat(eleNode.textContent);
            bearing = normalizeBearing(getBearing(point.latitude, point.longitude, nextLat, nextLng));
            distancem = getDistance(point.latitude, point.longitude, nextLat, nextLng);
            point.bearing = parseInt(bearing);
            point.distancem = distancem;
            point.totalAtStartkm = courseLengthkm;
            courseLengthkm += distancem / 1000;
            route.push(point);
        }
        alert(route.length + ' points');
    }
    catch (ex)
    {   alert('Parse XML:' + ex.message);  }
//Place holder
}

There are several helper functions we use here so we need to write them next. Insert the following code BEFORE function initialize()

 
// Get bearing between 0 and 359
function normalizeBearing(bearing) {
    if (bearing >= 360) return (bearing - 360);
    if (bearing < 0) return (bearing + 360);
    return bearing;
}

function getBearing(lat1, long1, lat2, long2) {
    return google.maps.geometry.spherical.computeHeading(new google.maps.LatLng(lat1, long1), new google.maps.LatLng(lat2, long2));
}

function getDistance(lat1, long1, lat2, long2) {
    return google.maps.geometry.spherical.computeDistanceBetween(new google.maps.LatLng(lat1, long1), new google.maps.LatLng(lat2, long2));
}

Save your file and rename it to RouteWalker.html. Now down load a sample gpx file like this one (go to the Export tab on the right of the page and select gpx route or track) and save it as MyRide.gpx in the RouteWalker folder.

Now right-click on the RouteWalker.html file and select 'open with Chrome'. You will see an error like 'NETWORK_ERR. XMLHttpRequest Error 101'. This is Chrome's way of telling you it doesn't think you have the security chops to open a file like this. You get around this problem by launching Chrome with a special parameter.

Create a shortcut in RouteWalker with the following properties.

 
<Path to Chrome> --allow-file-access-from-files  "C:\Program Files\RouteWalker\RouteWalker.html"

When you double-click the shortcut you will now see an alert telling how many points are in your gpx file. If you used my example file the answer is 1866. Tomorrow we'll start using Google maps.

No comments:

Post a Comment