Jorj's home page: bike trip 6/99 gear

To get the live updates of our position and pictures on to the web, we needed a few pieces of miscellaneous technology:

My GPS is a Garmin GPS II+. The camera is an ancient parallel-port Connectix QuickCam. The laptop is a Toshiba Libretto 100 CT (running RedHat Linux), and the wireless communication is done via CDPD (Cellular Digital Packet Data) modem - a Sierra Wireless AirCard 200.

The scripts are mostly written in perl.

The main script that runs on the laptop works like this:

My email is received on a different machine than the web server is on. So there's an extra step here: when any of the three types of email (current position, QuickCam picture, or tracking digest) are received, they're forwarded to the web server.

When the web server receives one of the three messages, it runs it through a script that knows how to unpack that kind of data. It then adds the data to three different data files: one denotes when pictures were taken, one has the tracking digest information, and one has stripped-down picture data for the map (so that the picture timestamps aren't visible on the top-level map).

That's the end of how the data gets on to the web page. The next set of scripts are responsible for displaying the web page, allowing you to zoom in or out and pan:

There are three CGI scripts involved in displaying the map. The first is "bikemap", which displays the instructions and trip log. It also calls the second script, called "getmap", to retrieve a map from the TIGER mapping service.

The "getmap" script downloads a copy of the map (based on where the user has clicked, what zoom level we're at, what size it's told, and so on) from the TIGER mapping service. Originally, the "bikemap" script just used a URL to tell your web browser to download the correct map; however, many browsers wound up caching the image (even though the page is marked as 'no-cache') in situations that are undesirable (for example, if a new set of data is added to the data files, the URL for the picture doesn't change; if the browser has it cached, then the user doesn't see the new data). Just before leaving on our 1999 trip, I noticed that something changed at the TIGER mapping service, and one of their two servers stopped plotting my data points. I sent them mail, and the (very polite) reply was basically "this bug will never be fixed." So the "getmap" script has a second function: it only downloads the picture from the one server that I know works. (I was reluctant to put this hack into the script, but they left me no choice.)

With the first two scripts, you can see the page with a map. The map is a "form", which allows you to click anywhere on it. The third script is where that form data is sent (i.e. your web browser tells this script, called "newmap", where you clicked, or what button you clicked on). This was the tricky and time-consuming part: given the data in the picture (which this script can't really see), the script has to figure out what you probably clicked on and then call the "bikemap" script with a new set of arguments to have it display the right thing. After some testing, I found that if the "getmap" script requests a picture that is distorted (in terms of the degrees of latitude and longitude not being equal over the distance shown, such that the distance (in miles) horizontally across the image is the same as the distance vertically across the image), then I can do some rather simple conversions. The simple conversions are mostly done with ugly pieces of code like this:

use Math::Trig;

sub calc_distance {
    local ($lat1, $lon1, $lat2, $lon2) = @_;
    local ($c, $d);
    $c = 57.3; # radian conversion factor

## This equation is correct, but has small amounts of error which can
## be significant for small values.
##    $d = acos(sin($lat1/$c)*sin($lat2/$c) + cos($lat1/$c)*cos($lat2/$c) * 
##            cos(($lon2-$lon1)/$c));

## This is better, but more complicated.
    $lat1 /= $c;
    $lat2 /= $c;
    $lon1 /= $c;
    $lon2 /= $c;
    $d = 2*Math::Trig::asin(sqrt((sin(($lat1-$lat2)/2))**2 +
	cos($lat1)*cos($lat2)*(sin(($lon1-$lon2)/2))**2));

# convert to miles and return
    return $d*(.5*7915.6*.86838);
}

sub zoom_to_scaley {
    my ($zoom) = @_;
    $zoom = ($zoom-1)*2;
    $zoom=1 if ($zoom < 1);
    return ( 40.0 / $zoom );
}

sub calc_scalex {
    my ($scaley, $lat, $lon) = @_;
    my ($scalex, $toplat, $leftlon, $v, $h, $r);

    $scalex = $scaley;
    $toplat = $lat+($scaley/2);
    $leftlon = $lon-($scalex/2);
    $v=&calc_distance($toplat, $lon, $toplat-$scaley, $lon);
    $h=&calc_distance($lat, $leftlon, $lat, $leftlon+$scalex);
    $r = $v/$h;
    $scalex *= $r;

    return $scalex;
}

These three functions are the core of the entire web page. The first can calculate the distance between two points (in miles). The second takes a "zoom in" value and converts it to degrees of latitude (horizontal). The third takes the value from the second and calculates the appropriate height for the image (in degrees of longitude).

When you click somewhere on the map (and have "show image" selected), "newmap" calculates the approximate position in the picture where every picture was taken (remember that this data is stored in latitude and longitude). It compares these positions with where you clicked to determine whether or not you clicked on a red dot (in which case, it should display the picture). It may look simple when you do it, but there's quite a bit of guessing going on in the background!

If you click on the map to zoom and center or center it, "newmap" just has to calculate the new center of the map (in latitude and longitude). It converts the point that you clicked on from pixels to lat and lon, and passes the correct arguments back to "bikemap", which redisplays the page.

The simplest case is when you tell it to zoom in or out (or zoom to a specific zoom level). In these cases, it modified the zoom value and tells "bikemap" to show the correct new picture.