Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / database / MySQL

World's Fastest Server Visualization

4.20/5 (2 votes)
13 Apr 2009CPOL5 min read 22.5K   120  
Ruby on Rails application for the world's fastest server data.

Introduction

In this article, we're going to build a web application to consume and use data from the World's Fastest Server SOAP Web Service. We're going to build this app with Ruby on Rails, a popular web framework. Ruby is a high-level, dynamic, object-oriented language. Along the way, we'll also be sprinkling in some JavaScript and Perl.

Right now, the Web Service for the drag bike doesn't have a lot of data in it, but our application will load all the data from the Web Service into our local database and allow the trips to be visualized on a map and Google Street View.

Our Application

The primary feature of our app is a way to view a given trip by the World's Fastest Server on a Google map, along with accompanying street view:

Image 1

Using the controls on the page, the user can step forward/back in the trip (each step corresponding to a waypoint from the Web Service). We automatically adjust the angle of the street view camera (the yaw) to point in the direction of the next waypoint.

Starting out

To start with, we need to consume data from the SOAP Web Service. Our application is primarily going to be displaying trip data after it's been recorded, so it makes sense for us to load the data periodically from the Web Service but store it locally for faster and easier access.

To do this, we'll create a Rails database migration. Rails migrations are written in a simple Ruby DSL that makes it easy to describe changes to your database schema so you can apply and reverse those changes easily. Here's an excerpt of our migration that creates the 'waypoints' table:

create_table :waypoints do |t|
  t.integer :hms_id, :null => false
  t.integer :trip_id, :null => false
  t.datetime :recorded_at, :null => false
  t.datetime :created_at, :null => false
  t.decimal :lng, :precision => 15, :scale => 12
  t.decimal :lat, :precision => 15, :scale => 12
end

As you can see, we've created a few tables that mirror the structure of the Web Service we'll be loading the data from, although we've made some changes to make it follow Rails conventions.

Loading the data

We've got a problem now though, which is that the Ruby community has largely moved away from SOAP in favor of REST Web Services, and the SOAP libraries that exist are no longer well-maintained. Rather than fit a round peg in a square hole, let's turn to an easier solution, Perl's SOAP::Lite. We'll create a simple Perl script that uses SOAP::Lite to load our data:

PERL
#!perl -w

use SOAP::Lite;
use Switch;

my $service = SOAP::Lite
  -> service('http://www.theworldsfastestserver.com/webservice/bikedata.cfc?wsdl');
$service->outputxml(1);

my $trip_id = SOAP::Data->name( 'TripID' => $ARGV[1] );

switch ($ARGV[0]) {
  case "trips"          { print $service->GetTripIDsXML() }
  case "types"          { print $service->GetTripTypesXML() }
  case "waypoints"      { print $service->GetWaypointsForTripXML($trip_id) }
  case "accelerations"  { print $service->GetAccelForTripXML($trip_id) }
}

We can call this script, passing in command-line arguments to specify the Web Service to call, and the trip ID, if needed. It simply outputs the raw XML, which we'll parse with Nokogiri, a Ruby XML parser. We'll create a WfsImporter class to handle all this. Here's the code that runs the Perl SOAP script, loads the XML of a given type (trip, waypoint, or acceleration), and parses it down to the child objects:

def self.records_xml(type, hms_id = nil)
  xml = load_xml(type, hms_id)
  
  case type
    when :trips           then xml/:Trip
    when :waypoints       then xml/:WayPoints/:WayPoints
    when :accelerations   then xml/:Accelerometer/:Accelerometer
  end
end

def self.load_xml(name, hms_id = nil)
  Nokogiri::XML(%x[perl #{RAILS_ROOT}/script/wfs.pl #{name} #{hms_id}])
end

Now, we've got nice objects for the Web Service data, but we only really want some of this data anyway, and it needs to be massaged a bit into the naming and conventions we like. We'll make a simple hash that defines the attributes we want to use, and what names those attributes should have in our objects (which are sometimes the same):

ATTR_MAPS = {    
  :trips          => {  :trip_type_id => :trip_type_id, :name => :name, 
                        :description => :description, :start_datetime => :started_at, 
                        :end_datetime => :ended_at },
  :waypoints      => {  :Timestamp => :recorded_at, :latitude_decimal => :lat, 
                        :longitude_decimal => :lng, :utc_time => :soap_time, 
                        :utc_date => :soap_date },
                  
  :accelerations  => {  :timestamp => :recorded_at, :miliseconds => :milliseconds, 
                        :utc_date => :soap_date, :utc_time => :soap_time, 
                        :axis => :axis, :value => :g_force},
}

Now, we'll use the XML loading methods in combination with our attribute map to parse the XML from the Web Service and load it into our database.

def self.import_records(type, parent = nil)
  records_xml(type, parent.try(:hms_id)).each do |record_xml|
    hms_id = record_xml['ID']
    attrs = ATTR_MAPS[type].map_to_hash{ |xml_attr, db_attr| 
                 {db_attr => record_xml.at(xml_attr).content } }
    
    record = (parent ? parent.send(type) : Trip).find_or_initialize_by_hms_id(hms_id)
    record.update_attributes(attrs)
  end
end

We're adding an "hms_id" to each object so we can avoid re-creating the same objects when we import multiple times. The actual key in our database will be generated by Rails and will be different from the HMS one.

Finally, here's some simple code to batch the import process for all trips and their children:

def self.import_all
  import_records(:trips)
  Trip.all.each do |trip|
    import_records(:waypoints, trip)
    import_records(:accelerations, trip)
  end
end

Creating the app

Our app will allow trips to be browsed and the details of each trip's waypoints/accelerations to be viewed, but this is pretty standard stuff - you can take a look at the code if you are interested. The unique thing will be a page allowing a trip to be viewed on a Google map, along with using Google Street View (assuming it's available) to display where the bike was, and even which direction it was facing at each recorded waypoint.

We'll need some JavaScript code to perform functions like setting up our map. Within the page visualize.html.erb itself, we'll just put these two functions:

JavaScript
function initialize() {
  load_data();
  setup_map();
    move(0);
}

function load_data(){
  <% @trip.waypoints.each_with_index do |waypoint, i| % >
      positions[<%= i % >] = new GLatLng(<%= waypoint.lat_lng % >);

      <%= "yaws[#{i - 1}] = #{@last.heading_to(waypoint)};" if @last % >
      <% @last = waypoint % >
    <% end % >
    yaws.push(yaws[yaws.length - 1]);
}

The initializer simply calls other functions we've defined. load_data is defined here so we can mix eRb and JavaScript. In this case, we're looping through each of @trip.waypoints and using it to create two arrays: positions, an array of GLatLng objects created from the lat_lng property of each waypoint, and yaws, an array of the presumed direction from one waypoint to the next, based on the forward direction between each waypoint. We're using the heading_to method on each waypoint, which is mixed into the object via GeoKit's acts_as_mappable.

Now, in maps.js, we'll define the other functions we need:

JavaScript
function setup_map(){
  my_pano = new GStreetviewPanorama($('pano'));
  my_map = new GMap2($('map_canvas'));      
  
  var polyline = new GPolyline(positions, "#ff0000", 10, 0.7);
  my_map.addOverlay(polyline);
}

This will create new objects for the street view and the map, and draw the entire trip's route on the map with a GPolyline, using our array of GLatLngs.

Now, we just need our function to let the user move the map forward and back. We'll output some debug data as well:

JavaScript
function move(increment){
  step = step + increment;
  write_position();

  if(positions[step])
  {
    my_pano.setLocationAndPOV(positions[step], {yaw:yaws[step]});
    my_map.setCenter(positions[step], 15);
    
    if(current_marker)
      my_map.removeOverlay(current_marker);

    current_marker = new GMarker(positions[step]);
    my_map.addOverlay(current_marker);
  }
}

function write_position()
{
   position_string = positions[step] ? "lat/lng: " + 
            positions[step] : "no data remaining";
  $('current_pos').innerHTML = "step: " + step + 
            " | " + position_string;
}

The move function lets us step an arbitrary number of steps forward or back, using write_position to output some debug data about our current spot. If the position we're jumping to exists, we'll update our street view to the new location and camera orientation, and center our map on the new position. We'll then check if we have an old marker on the map, delete it if so, and then add a new one for the current waypoint we're displaying.

We'll add links in the page to allow the user to move the map forward and back:

<%= link_to_function 'step back', 'move(-1)' %> | 
  <%= link_to_function 'step forward', 'move(1)' %>

And, we're done. You can see the finished result here:

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)