GPS tracking using SVG - Part 1

a paper for the 2002 SVG Open Developers Conference.

15/July/2002
Richard Bennett
mail@richardinfo.com
richard.b@gritechnologies.com
Phone: + 32 475 71 91 84

GRI Technologies LLC
1875 Hampshire Ave.
St. Paul, MN 55116 USA
www.gritechnologies.com

Keywords: Webmapping; Dynamic maps; GPS; GIS; Web Applications; SVG-scripting

Abstract:

In this paper I describe the approach we took to solving several issues we encountered while writing an application that visually tracks municipal-maintenance vehicles over a map in real-time.
The application uses client-side scripting inside an SVG file to relay the information gathered by the server from the GPS units.
The main difference with existing systems is that this application runs over the internet, and the vehicles move in real-time while you watch them.

Introduction

Please let me introduce myself; I'm Richard Bennett, an independent software developer, and I'd like to talk about a few aspects of the work I'm doing for GRI Technologies in Minnesota USA.
In a nutshell; we are visualizing GPS navigational coordinates (Longitude/Latitude) projected over a map, and accessible through the internet.
We are using client-side scripting so we can update the GPS coordinates in real-time while the user watches the map,
and we use SVG's graphics canvas, that enables us to manipulate raster images in combination with vector graphics.

The basic demo, illustrating the topics covered in part 1 of this paper, is available here:

examples/example2.svg (Opens in a new window)

Some SVG related issues that needed solving:

During our work we found several issues that needed solving, but in the context of this paper I'll restrict myself to those that are related to SVG.

SVG's canvas:

As I'm sure many of you know, one major advantage to SVG is that it's canvas is boundless, and can extend as far as we want.
This gives us some nice possibilities, but also offers some new challenges to be solved.
One of those is projection.

Projection refers to the way maps from a round source, like the earth, are projected onto a flat surface, like the SVG canvas:

Spherical and cartesian projections

We get our GPS coordinates as Longitude and Latitude, and have to convert them to feet (or meters where applicable), to allow a 1 foot to 1 pixels conversion.
Naturally this converted result needs to be in relation to a starting point, which I refer to as the base-point.
What I mean is, 43 Latitude and 93 Longitude represent a point on the globe, 45000 feet by 51.000 feet mean nothing without a base-point to reference them from.

I'm not going to go too deeply into the various projection systems, suffice to say the mathematics can get extremely complicated, if high accuracy is needed.
Naturally when tracking vehicles by GPS we need quite a high degree of accuracy, but because we're doing the plotting in real-time in Javascript, we don't want to be doing immense calculations to convert each point to feet.
One way to simplify the calculations needed for high accuracy is to keep the area we cover between base-points as small as is practical in use.
Basically, if we try to cover the whole of the US, referencing from one base-point, we need much more accurate projection, than if we use a finer grid of base-points:

Country-wide versus grid base-points.

The system we use is known as "Township and Range", it has been set-up across the USA, and basically each county has their own projection formula to spread the survey error out over that particular county. In other words the bounding box corners for each county are slightly adjusted to compensate the curvature of the earth. more information is available here: http://www.geocommunicator.gov/lsi/

As our application was primarily designed to run inside a county-wide area, we didn't need to worry about switching base-points as a vehicle moved; we simply set the appropriate value in the config file.

The Maths:

As I said, I wanted to keep the calculations simple, so they wouldn't slow down or complicate our scripts, while offering the level of accuracy we needed. So I opted for some basic trigonometry:

	var lat1=44.500622 	//basepoint - 0 feet
	var lon1=-95.299650	//basepoint - 0 feet
	var lat2=44.85268	//point 1 to project
	var lon2=-93.48806	//point 2 to project
	
	var latDist=Math.abs(lat1-lat2)*364618.5
	var lonDist=Math.abs(lon1-lon2)*364618.5
	//this gets the difference in degrees between the basepoint, 
	//and the second point, and multiplies it by feet-per-degree-longitude, at the equator.
	//(69.05*5280)approx
	//This is done for Lat and Lon respectively.
	
	var avgLat=((lat1*1+lat2*1)/2)*0.017453292519
	//multiplying by 1 is to make sure we have a number.
	//multiplying by 0.017453292519 is to convert to degrees radian needed by Javascript Math functions
	
	lonDist=lonDist*Math.cos(avgLat)
	//apply the trigonometry
	
	xfeet=lonDist (= 469700 feet)
	yfeet=-latDist (= -128366 feet)

For the lowdown on the maths aspect, have a look at this informative page: http://showcase.netins.net/web/bgunzy/sept30.htm (opens in new window)

The Negative Y-axis:

Another issue we had to deal with is that our SVG canvas has a positive Y-axis.
So x="0" y="0" is the left top corner, instead of the left bottom, as we would expect in cartography. There are various ways to work around this - we opted to use negative Y-values, and set the viewBox attribute to reflect the correct area.

Here are a few diagrams to illustrate this principal:

If we place an SVG object at 100px x 100px in the regular SVG canvas, it will show like this:

The standard viewBox could be:
50px(x) 50px(y) 100px(width) 100px(height)
Which looks like this:

When we invert the Y-axis, we get our cartographic coordinate-system:

And our modified viewBox showing the appropriate region would be like this:
50px(x) -150px(y) 100px(width) 100px(height)
Note that we need to subtract the viewBox's height from it's negative Y-value, which looks like this:

viewBox.x remains unchanged at: x = 50
viewBox.y becomes -y and we subtract the viewbox.height from that:
viewBox.y = -50 - 100 = -150

Let's Get Plotting:

If we want to plot our GPS coordinates on the screen, we'll need something to plot them over, of course; that would be the background map.
We can't load a whole county-wide map at street-level-resolution in one image, as the file would be far to large, so we load one low-resolution background-map, and load the more detailed sections of the map as the user zooms in tighter.
While we researched how we could best accomplish this, there were several interesting points we noted, here are a few:

We ended up having one lower-resolution, county-wide map as background, for use when fully zoomed out, and overlaying it with dynamically loaded higher-resolution maps.
We also use an HTA-version that can load high-resolution maps from CD or HD - but that's another story.

Some code:

Ok, let's get our hands dirty here;
If we knew our background map's exact dimensions, we could position it at the correct place using our own simple projection formula.
But as the background map is often scanned, or otherwise less than perfect, we built a little application that allows us to skew, rotate, stretch and move the map in relation to some known coordinates, until everything aligns nicely. We can then copy and paste the resulting matrix as transformation for our background map, like this:

Example 1:

<!-- All irrelevant data snipped for readability --> 
<svg viewBox="380443 -277501 190000 190000">
	<image id="background" width="200000px" height="200000px" xlink:href="../images/background.png" 
		transform="matrix(0.977361 0.009773 0.004848 0.969687 374295.752393 -282622.677087)" />
</svg>

example1.svg (Opens in a new window)
This example covers and area of about 180.000 by 180.000 feet, or 34 by 34 miles.

That's our background map - now let's plot something over it...
The question is, how do we get the GPS coordinates into the SVG file?
There are several ways possible, ranging from manual input, to passing them in the URL-string, but I'd like to focus on loading them from an XML-file on the server, for this paper.
In our case the server queries the GPS modems every 10 seconds or so, and saves the result in a database.
This is then queried to build the xml page containing the latest coordinates.
For the sake of this demo, I have saved a static xml file containing the coordinates of a route followed by a vehicle. (the coordinates have not been modified in any way)
This is what it looks like:

Data.xml file:

<?xml version="1.0" encoding="ISO-8859-1" ?> 
<vehicle id="3003" trackcolor="#FF0000">
	<!-- The data is sent as: Unixtimestamp,"Latitude","-Longitude",direction_in_degrees -->
	<!-- Each record is separated by a pipe ( | ) -->
	1014298320,44.85268,-93.48806,089|
	1014298322,44.85267,-93.4879,081 |
	1014298323,44.85275,-93.48773,065|
	1014298336,44.85349,-93.4867,043 |
	1014298337,44.85355,-93.48661,043|
	1014298339,44.85362,-93.48653,043|
	1014298340,44.85375,-93.48638,037|
	1014298341,44.85382,-93.48633,029|
	1014298342,44.85389,-93.48629,021|
	1014298350,44.85435,-93.4861,035 |
	1014298351,44.85443,-93.48596,051|
	1014298352,44.85445,-93.4859,058
	<!-- other records snipped for readability -->
</vehicle>

In the real file all the contents of the vehicle tag is on one line, as Batik seems to choke on the linebreaks otherwise.
data.xml (Opens in a new window)

So, we know we've got our data ready and waiting, let's add a marker to the map to represent the vehicle.
I designed a marker in Jasc Webdraw, and added the components to our SVG in the defs section.
That way the pointer is available for us to create instances of it dynamically from script.
For the sake of simplicity I will restrict this demo to 1 vehicle only, so I simply add the components for one vehicle to our SVG document.
That looks like this:

<g id="viewer">
	<ellipse cx="450" cy="450" rx="900" ry="900"
	style="fill:rgb(234,168,26);stroke:rgb(0,0,0);stroke-width:20;fill-opacity:0.6"/>
	<g id="pointer">
	<rect x="0" y="0" width="900" height="900"
	 style="fill:none;stroke:none"/>
	<path style="stroke-linejoin:bevel;fill:rgb(234,168,26);stroke:rgb(0,0,0);stroke-width:20;fill-opacity:0.6"
	d="M950 0 L950 950 L0 950 C650 950 950 450 950 0"/>
	</g>
	<text id="trackName" x="-320px" y="700px" style="fill:rgb(0,0,0);font-size:700;font-family: Arial;">1234</text>
</g>	

Now we can start to bring the various aspects together.
Here's a list of what we need to do to get the xml file loaded, and the vehicle moving:

  1. Add an onload event that calls init() to the main SVG object.
  2. Get a handle on our vehicle components, to allow us to manipulate their position.
  3. Build a "Vehicle" widget, that lets us parse, manipulate, and store the coordinates.
  4. Use getURL() to load our XML file
  5. Parse the coordinates into arrays, and project them to feet in relation to our base-point.
  6. Animate our marker, to move along the coordinates, at the speed given by the Unix timestamp. (We'll speed it up x 20 for easier visibility)

Ok, let's do Step 1:

We define our SVG document, and setup our init() call:

<?xml version="1.0" encoding="ISO-8859-1" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN"
	"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg xmlns="http://www.w3.org/2000/svg" 
	 xmlns:xlink="http://www.w3.org/1999/xlink"
	 xmlns:a3="http://ns.adobe.com/AdobeSVGViewerExtensions/3.0/"
	 a3:scriptImplementation="Adobe"
	 viewBox="380443 -277501 190000 190000"
	 xml:space="preserve" preserveAspectRatio="xMidYMid meet"
	 onload="init(evt)">
	 
<script type="text/ecmascript" a3:scriptImplementation="Adobe"><![CDATA[
	var svgns = "http://www.w3.org/2000/svg";
	function init(evt){
		if (!window.svgDocument)svgDocument = evt.target.ownerDocument;
		//Code comes here
	}
</script>
</svg>

Now you might wonder why there is so much code, when a simple <svg><script></script></svg> would also work.
That's because we're using a template that offers very good cross-browser/platform/implementation support.
What we're doing is declaring the namespaces and doctype properly, so Batik will also run the file.
Then we specify scriptImplementation="Adobe", so when run under Adobe SVG Viewer 3, our app will use ASV3's built-in scripting engine, to avoid script errors from differences between browsers, or when ASV3 is run as COM, where no scripting engine is available. Batik ignores this, and will still use it's own scripting engine.
For the same reason we test whether "window.svgDocument" is available (as it is when run in ASV3), and if not we assign it the document object.
Much of the groundwork and testing of these compatibility issues has been done by Kevin Lindsey. (www.kevlindev.com )

Then on to Step 2:

We get a handle on our vehicle components, to allow us to manipulate their position later on:

function init(evt){
	if (!window.svgDocument)svgDocument = evt.target.ownerDocument;
	vehicleViewer=svgDocument.getElementById("viewer")
	vehiclePointer=svgDocument.getElementById("pointer")
	vehicleName=svgDocument.getElementById("trackName")
}

Note that we don't use "var", so that variables have a global scope.

Now for Step 3:

At this point some people might find what follows a little complicated.
I'll try to explain the principals:
The Vehicle widget is a Javascript object.
We have to invoke an instance of this widget using the "new" keyword.
Then our new instance will have all the methods that the widget has, some of which are:
vehicle.parseData(data as string);
vehicle.moveTo(counter as integer);

The reason we do this, is to encapsulate all the conversion, parsing, and plotting functionality inside the widget, allowing us to use nice simple code call the widget's methods.
Then we can simply make one instance of the Vehicle widget for each vehicle we would like to track, allowing for multiple vehicles moving over the same map.
If you don't understand the widget's inner workings, it doesn't matter, you can still learn to use it, the same way as you can drive a car without knowing how the engine works.

//This is the basic version of the Vehicle widget - the later version contains more methods.
//The Vehicle widget:*********************************************
function Vehicle(id){
	this.id=id
	this.lon=[]
	this.lat=[]
	this.xfeet=[]
	this.yfeet=[]
	this.seconds=[]
	this.compas=[]
	this.lastDate=0
	//Set the vehicle name:
	VehicleName.firstChild.data=this.id
}
Vehicle.prototype.parseData=function(string){
	var t=string.split("|")
	for(i in t){
		var s=t[i].split(",")
		//fill the arrays with data:
		this.lat[i]=s[1]
		this.lon[i]=s[2]
		this.compas[i]=s[3]
		//Do the projection:
		var lat1=44.500622 //basepoint
		var lon1=-95.299650 //basepoint
		var lat2=this.lat[i]
		var lon2=this.lon[i]
		var latDist=Math.abs(lat1-lat2)*364618.5
		var lonDist=Math.abs(lon1-lon2)*364618.5
		var avgLat=((lat1*1+lat2*1)/2)*0.017453292519
		lonDist=lonDist*Math.cos(avgLat)
		//and add the new values:
		this.xfeet[i]=lonDist
		this.yfeet[i]="-"+latDist
		//get the elapsed time from the timestamp:
		if(this.lastDate==0){
			this.seconds[i]=55
			this.lastDate=new Date(parseInt(s[0]+"000")).getTime()
		}else{
			var diff=new Date(parseInt(s[0]+"000")).getTime()
			this.seconds[i]=diff-this.lastDate
			this.lastDate=diff
		}
	}
	//Start off the plotting:
	plotVehicle()
}
Vehicle.prototype.moveTo=function(i){
	//Move the vehicle to the correct location
	var trans  = "translate(" + this.xfeet[i] + "," + this.yfeet[i] + ")";
	var vs=450//*viewerSize
	var offst = "translate(-"+vs+",-"+vs+")";
	var comb = trans+" "+offst
	VehicleViewer.setAttribute("transform", comb)
	//turn the pointer with the direction.
	var rotate="translate(450,450) rotate("+(this.compas[i]-135)+") "
	VehiclePointer.setAttribute("transform", rotate)
}
//end of Vehicle widget*********************************************

And Step 4:

Now for the real business - inside init() we call:
getURL("data.xml",loaded);
This loads the data.xml file, and calls the loaded() function, passing the file's contents as argument:

function init(){
	getURL("data.xml",loaded);
}
function loaded(obj){
    if(obj.success){
	var frag = parseXML(obj.content, svgDocument)
	//make an instance of our Vehicle widget, passing the ID as argument.
	vehicle=new Vehicle(frag.firstChild.getAttribute("id"))
	vehicle.parseData(frag.firstChild.firstChild.getData())
    }
}

As you see we make our instance of the Vehicle widget here, and call it's parseData() function, to load all the coordinates into memory.

I put together a test .xml file, and a test loader, to show how we use getURL() and parseXML() to load and parse the external XML file.
Although ASV3 is pretty flexible with all this, Batik will only really function by sticking to this syntax. For instance, adding a doctype, namespace declaration, or linebreaks to the xml file will not work well with Batik.

Example 5:

//LOADER:
function init(evt){
	if (!window.svgDocument)svgDocument = evt.target.ownerDocument;
	getURL("dataTest.xml",loaded);	
}
function loaded(obj){
	if(obj.success){
		var frag = parseXML(obj.content,svgDocument) 
		var fragId=frag.firstChild.getAttribute("id")
		var fragColor=frag.firstChild.getAttribute("trackcolor")
		var fragData=frag.firstChild.firstChild.getData()
		alert("ID = "+fragId+", color = "+fragColor+" and the tag contents = \""+fragData+"\"")
	}
}
//XML FILE:
<?xml version="1.0" encoding="ISO-8859-1" ?> 
<vehicle id="3004" trackcolor="#FF0000">This is the test contents of the vehicle tag</vehicle>

Try it:
example5.svg (Opens in a new window)


TIP: To get an idea of the contents of the XML file we can use this quick and dirty DOM viewer code:
	var msg="";	
	for(i in frag)msg+=i+"\n"; 
	alert(msg)
And we replace "frag" with longer parts of the DOM, as we explore what is available.

Step 5:

Parsing the coordinates into arrays, and projecting them, is done inside our Vehicle widget now, so we don't need to worry about this anymore, and can jump to...

Step 6:

This involved animating our marker, to move along the coordinates from the XML file:

var count=0
function plotVehicle(){
	vehicle.moveTo(count)
	count++;if(count>vehicle.seconds.length)count=0
	setTimeout("plotVehicle()",(vehicle.seconds[count]/20))
}

That's nice and simple.
We call our widget's "moveTo()" function, passing a counter as argument - the widget takes care of all the movement.
Then we use setTimeout to call the next position, delaying it by the difference in time between the last two records. In this case divided by 20 to speed up playback.

So now we have a fully working example, that loads in coordinates from an xml file, and plots a vehicles movement over a map:

Example 2:

The SVG file:
example2.svg (Opens in a new window)

The color-coded source-code:
example2.html (Opens in a new window)

That just about wraps it up for part 1.
Our application does exactly what we want it to.
There's just one thing though - the map isn't too great. If this is to be of any use we'll need higher resolution maps, but without needing a massive download... that's what we look at in part 2:

Continue to part 2

Cheers,
Richard Bennett.
mail@richardinfo.com

Valid XHTML 1.0! Copyright © Richard Bennett and GRI Technologies LLC 2001-2003 All rights reserved.