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:
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:
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:
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:
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 GLatLng
s.
Now, we just need our function to let the user move the map forward and back. We'll output some debug data as well:
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: