在这个项目中,将使用Ruby on Rails,这是一个流行的Web框架,来构建一个应用程序。Ruby是一种高级、动态、面向对象的编程语言。在此过程中,还将使用一些JavaScript和Perl。目前,拖动自行车的Web服务中并没有很多数据,但应用程序将从Web服务加载所有数据到本地数据库,并允许在地图和Google街景上可视化行程。
应用程序的主要功能是在Google地图上查看世界上最快的服务器的给定行程,以及伴随的街景视图:使用页面上的控件,用户可以向前/向后步骤行程(每个步骤对应Web服务中的一个航点)。将自动调整街景视图相机(偏航)的角度,使其指向下一个航点的方向。
首先,需要从SOAP Web服务中消费数据。应用程序主要将在行程记录后显示行程数据,因此,定期从Web服务加载数据并将其存储在本地以便于更快、更容易地访问是有意义的。为此,将创建一个Rails数据库迁移。Rails迁移是用简单的Ruby DSL编写的,它使得描述对数据库架构的更改变得容易,因此可以轻松地应用和撤销这些更改。以下是创建'waypoints'表的迁移摘录:
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
如所见,创建了一些表,这些表反映了将从中加载数据的Web服务的结构,尽管进行了一些更改以遵循Rails约定。
现在面临一个问题,即Ruby社区已经大量转向REST Web服务,而不是SOAP,而且现有的SOAP库不再得到良好维护。与其强行适应,不如转向一个更简单的解决方案,即Perl的SOAP::Lite。将创建一个简单的Perl脚本,使用SOAP::Lite来加载数据:
#!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)
}
}
可以调用这个脚本,通过命令行参数指定要调用的Web服务,以及如果需要的话,行程ID。它简单地输出原始XML,将使用Nokogiri,一个Ruby XML解析器来解析它。将创建一个WfsImporter类来处理所有这些。以下是运行PerlSOAP脚本、加载给定类型的XML(行程、航点或加速度)并将其解析为子对象的代码:
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
现在,有了Web服务数据的漂亮对象,但实际上只需要其中的一些数据,并且需要将其稍微调整以符合喜欢的命名和约定。将制作一个简单的哈希,定义想要使用的属性,以及这些属性在对象中的名称(有时名称是相同的):
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},
}
现在,将使用XML加载方法结合属性映射来解析Web服务的XML并将其加载到数据库中。
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
为每个对象添加了一个"hms_id",以便在多次导入时避免重新创建相同的对象。数据库中的实际键将由Rails生成,与HMS的不同。
最后,这里是一些简单的代码,用于批量导入所有行程及其子项:
def self.import_all
import_records(:trips)
Trip.all.each do |trip|
import_records(:waypoints, trip)
import_records(:accelerations, trip)
end
end
应用程序将允许浏览行程,并查看每个行程的航点/加速度的详细信息,但这是非常标准的东西 - 如果感兴趣,可以查看代码。独特的是将有一个页面允许在Google地图上查看行程,同时使用Google街景(如果可用)来显示自行车的位置,甚至在每个记录的航点处自行车的朝向。
将需要一些JavaScript代码来执行设置地图等功能。在页面本身visualize.html.erb中,将只放置这两个函数:
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 %>);
<% if @last % >
yaws[<%= i - 1 %>] = <%= @last.heading_to(waypoint) %>;
<% @last = waypoint % >
<% end % >
<% end % >
yaws.push(yaws[yaws.length - 1]);
}
初始化器简单地调用定义的其他函数。load_data在这里定义,以便可以混合使用eRb和JavaScript。在这种情况下,循环遍历每个@trip.waypoints,并使用它来创建两个数组:positions,一个由每个航点的lat_lng属性创建的GLatLng对象数组,以及yaws,一个基于每个航点之间的前进方向假设的从一个航点到下一个航点的方向数组。使用GeoKit的acts_as_mappable混合到对象上的heading_to方法。
现在,在maps.js中,将定义需要的其他函数:
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);
}
这将为街景和地图创建新对象,并使用GLatLng数组绘制整个行程的路线,使用GPolyline。
现在,只需要函数让用户向前和向后移动地图。还将输出一些调试数据:
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;
}
move函数让可以任意向前或向后移动几步,使用write_position输出有关当前位置的一些调试数据。如果跳跃的位置存在,将更新街景到新的位置和相机方向,并使地图以新位置为中心。然后,将检查地图上是否有旧标记,如果有,则删除它,然后为当前显示的航点添加一个新的标记。
将在页面上添加链接,允许用户向前和向后移动地图:
<%= link_to_function 'step back', 'move(-1)' %> |
<%= link_to_function 'step forward', 'move(1)' %>
完成了。可以在这里看到最终结果:
一个带有虚拟数据的示例行程