在 Forge - 创建混合应用
上个月,我们开始研究 Google Maps API,它允许我们将动态(和启用 Ajax)的地图嵌入到我们的 Web 应用程序中。那篇文章演示了创建此类地图是多么容易,并在屏幕上放置了标记。
本月,我们尝试一些更雄心勃勃的事情。具体来说,我们将加入创建混合应用的人们的行列,这些混合应用是 Web 服务的组合,通常(但并非总是)具有地图组件。混合应用是以新颖的方式组合两个或多个 Web API,使信息比单独存在时更易于访问和信息丰富。
我见过的最早的混合应用之一是芝加哥犯罪地图。芝加哥警察局定期发布城市内发生的犯罪事件公告及其大致位置。使用此地图,您可以确定您所在街区免受犯罪的程度,并查找城市其他地区的模式。此混合应用从芝加哥警察局的公共信息中获取信息,并在 Google Maps 页面上显示。
当它发布时,我住在芝加哥,并且(当然)使用该列表来找出我的社区有多安全。这些信息一直都可以从警察局获得,但只有在地图应用程序的背景下,我才真正能够理解和内化这些数据。事实上,这是混合应用教会我们的重要课程之一——信息合成和可访问的图形显示可以对最终用户产生巨大的影响。
当地图软件首次可用时,没有官方方法可以将地图用于非官方目的。许多有进取心的开发人员研究了用于创建地图的 JavaScript,并为自己的用途反向工程了 API。谷歌以及雅虎和 MapQuest 之后都发布了 API,使我们有可能使用他们的系统创建地图应用程序。这使得带有地图的混合应用比以往任何时候都更受欢迎,越来越多的网站和博客都在研究它们。
本月,我演示了一个 Google Maps 与亚马逊二手书服务的简单混合应用。该应用程序将相对简单。用户将输入 ISBN,很快就会显示美国谷歌地图。标记将放置在地图上,指示可以购买到二手书的几个地点。因此,如果纽约市、芝加哥和旧金山有这本书的副本,我们将在地图上看到三个标记,每个城市一个。通过这种方式,我们将看到如何将来自两家不同公司的两个不同的 Web API 结合在一起,为最终用户创建一个有趣且有用的显示。
本月的代码示例假定您已经注册了亚马逊 Web 服务 ID 以及 Google Maps ID。有关在哪里获取这些 ID 的信息,请参阅本文的在线资源。
我们的第一个挑战是创建一个地图,其中包含列表中每个位置的一个图形标记。上个月我们已经看到了如何使用 PHP 来做到这一点。本月,我们首先将程序转换为 ERB,这是一种 ASP 或 PHP 风格的模板,它使用 Ruby 而不是另一种语言。您可以在清单 1 中看到文件 mashup.rhtml。
清单 1. mashup.rhtml,我们地图的第一个(简单)版本
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <script src="http://maps.google.com/maps?file=api&v=1&key= ↪ABQIAAAAQQK9JhAXQ9eq-G55q\gu ↪1ExTnGAXa-Bs2i826H4DeSQaC3Vqy-xSjDFaTYAO0N5YPQWIEeUbqJMhhbA" type="text/javascript"></script> </head> <body> <h2>Here is your map</h2> <div id="map" style="width: 400px; height: 400px"></div> <script type="text/javascript"> var map = new GMap(document.getElementById("map")); map.centerAndZoom(new GPoint(-87.740070, 42.037030), 13); map.addControl(new GSmallMapControl()); map.addControl(new GMapTypeControl()); <% array = [-87.740070, -87.730000] %> <% array.each_with_index do |item, index| %> var myMarker<%= index %> = new GMarker(new GPoint(<%= item %>, 42.037030)); map.addOverlay(myMarker<%= index %>); <% end %> </script> <h2>Here are the locations</h2> <ul> <% array.each do |item| %> <li><%= item %></li> <% end %> </ul> </body> </html>
在服务器上正确解析 ERB 文件的一种方法是在 Ruby on Rails 上运行,它使用 ERB 作为默认模板机制。但是对于像这样的一个小混合应用,使用 Rails 就有点大材小用了。因此,我决定单独使用一个简单的 ERB(Embedded Ruby,用于 HTML-Ruby 模板)。
为了使其工作,我在服务器的 cgi-bin 目录中安装了 eruby(参见资源)。然后我告诉 Apache,任何带有 .rhtml 扩展名的文件都应该使用 eruby 解析
AddType application/x-httpd-eruby .rhtml Action application/x-httpd-eruby /cgi-bin/eruby
重启服务器后,我能够创建 HTML-Ruby 模板而没有任何问题,只要它们具有 .rhtml 扩展名。清单 1 中的文件 mashup.rhtml 是使用我的 HTML-Ruby 模板创建地图的简单尝试。与所有 Google Maps 应用程序一样,我们的最终输出将是一个 HTML 页面,其中包括一些 JavaScript,用于调用从 Google Maps 服务器下载的函数。我们的 Ruby 代码将输出 JavaScript 代码,然后该代码将在用户的浏览器中执行。
为了演示我们确实可以为两个固定点做到这一点,ERB 文件定义了一个纬度数组,这两个纬度都位于我家附近,在伊利诺伊州斯科基的短距离内
<% array = [-87.740070, -87.730000] %>
接下来,我们迭代此数组的元素,使用 each_with_index 方法来获取数组元素和我们当前所在的数组中的索引
<% array.each_with_index do |item, index| %>
现在我们有了纬度和它的唯一编号,我们可以输出一些 JavaScript
var myMarker<%= index %> = new GMarker(new GPoint(<%= item%>, 42.037030)); map.addOverlay(myMarker<%= index %>);
上面代码中发生的事情并不难理解,但是当您第一次阅读它时可能会有点复杂。基本上,我们循环的每次迭代都会声明一个新的 JavaScript 变量。第一次迭代创建 myMarker0,第二次迭代创建 myMarker1。这是可能的,因为我们有当前 Ruby 数组元素的索引,并且因为我们确保在 myMarker 和 Ruby 输出 <%= index %> 之间不插入任何空格。
然后将 myMarkerX 变量定义为 GMarker 的新实例——即 Google 地图上的标记——位于由纬度(项目变量)和经度(固定值 42.037030)定义的点。
最后,为了让用户清楚地看到所有点的位置,我们在页面底部打印一些文本。结果是地图上有两个标记,并且每个标记的位置都以文本形式列出。
这个地图是一个不错的开始,但远未达到我们想要完成的目标。而且,最大的障碍之一是 Google Maps 希望获得经度/纬度对。亚马逊的 Web 服务确实返回有关第三方供应商的信息,但它为我们提供了城市和州信息。因此,我们需要一种将城市和州名称转换为纬度和经度的方法。
最简单的方法是依赖其他人,他们可以将地址转换为经度/纬度对。此类地理编码器服务作为 Web 服务存在于 Internet 上;其中一些是免费提供的,另一些则收费。最著名的免费地理编码器服务之一是 geocoder.us。要使用此地理编码器,我们只需使用 REST 风格的 URL,如下所示:http://geocoder.us/service/rest?address=ADDRESS,将 ADDRESS 替换为我们要去的地方。例如,要找到我的房子,我们会说,http://geocoder.us/service/rest?address=9120+Niles+Center+Road+Skokie+IL。
地理编码器服务返回一个 XML 文档,如下所示
<rdf:RDF> <geo:Point rdf:nodeID="aid77952462"> <dc:description>9120 Niles Center Rd, Skokie IL 60076</dc:description> <geo:long>-87.743874</geo:long> <geo:lat>42.046517</geo:lat> </geo:Point> </rdf:RDF>
由于经度和纬度很好地分隔在 XML 内部,因此很容易在我们的程序中提取它,然后将其插入到我们生成的 JavaScript 中。但是,从查看 geocoder.us 文档来看,它似乎无法处理城市名称(即没有街道地址)。
幸运的是,至少有一个免费的地理编码器服务可以处理城市名称,返回类似风格的 XML 文档。我们按如下方式提交城市名称,再次使用 REST 风格的请求:http://brainoff.com/geocoder/rest?city=Skokie,IL,US。
我们得到以下结果
<rdf:RDF> <geo:Point> <geo:long>-87.762660</geo:long> <geo:lat>42.034680</geo:lat> </geo:Point> </rdf:RDF>
如您所见,我们从这个查询中获得的经度和纬度点略有不同。如果我们希望创建用于驾驶方向的地图,这将更加重要。但是,我们已经知道我们将在此应用程序中查看整个美国地图,并且相差几个街区,甚至两英里也不会有任何区别。
清单 2. mashup2.rhtml
<% require 'net/http' %> <% require 'rexml/document' %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <script src="http://maps.google.com/maps?file=api&v=1&key= ↪ABQIAAAAQQK9JhAXQ9eq-G55q\ ↪gu1ExTnGAXa-Bs2i826H4DeSQaC3Vqy-xSjDFaTYAO0N5YPQWIEeUbqJMhhbA" type="text/javascript"></script> </head> <body> <h2>Here is your map</h2> <div id="map" style="width: 400px; height: 400px"></div> <script type="text/javascript"> var map = new GMap(document.getElementById("map")); map.centerAndZoom(new GPoint(-87.740070, 42.037030), 13); map.addControl(new GSmallMapControl()); map.addControl(new GMapTypeControl()); <% final_list = [ ] %> <% cities = ["Skokie,IL,US", "Longmeadow,MA,US", "Somerville,MA,US", "Old+Westbury,NY,US"] %> <% cities.each_with_index do |city, index| %> <% geocoder_response = Net::HTTP.get_response('brainoff.com', "/geocoder/rest/?city=#{city}") %> <% xml = REXML::Document.new(geocoder_response.body) %> <% longitude = xml.root.elements["/rdf:RDF/geo:Point/geo:long"].text %> <% latitude = xml.root.elements["/rdf:RDF/geo:Point/geo:lat"].text %> <% final_list << {'city' => city, 'longitude' => longitude, 'latitude' => latitude } %> var myMarker<%= index %> = new GMarker(new GPoint(<%= longitude %>, <%= latitude %>)); map.addOverlay(myMarker<%= index %>); <% end %> </script> <body> <h2>Your cities</h2> <table border="1"> <tr> <th>City</th> <th>Longitude</th> <th>Latitude</th> </tr> <% final_list.each do |city| %> <tr> <td><%= city['city'] %></td> <td><%= city['longitude'] %></td> <td><%= city['latitude'] %></td> </tr> <% end %> </table> </body> </html>
我们现在可以更新我们的 ERB 文件,使其具有城市数组,而不是经度/纬度对,如您在清单 2 中所见。我们通过导入处理此附加功能所需的两个 Ruby 类来开始该文件
<% require 'net/http' %> <% require 'rexml/document' %>
尽管我们的起始(和中心)点从相同的经度/纬度位置开始,但我们从缩放级别 13 开始,这将足够大以显示所有城市。
然后我们定义了四个城市,并将它们放在一个名为 cities 的数组中,显示了我居住过的四个美国城市。请注意,此数组的每个元素都是一个字符串,其中包含城市名称、州缩写和 US(代表美国)。另请注意,当城市名称有空格时,我们必须将其替换为 + 号(或 %20),以便 Web 服务请求正常工作
<% cities = ["Skokie,IL,US", "Longmeadow,MA,US", "Somerville,MA,US", "Old+Westbury,NY,US"] %>
然后我们迭代这些城市,将每个城市用作我们的 Web 服务地理编码器的参数
<% geocoder_response = Net::HTTP.get_response('brainoff.com', "/geocoder/rest/?city=#{city}") %>
正如我们之前看到的,地理编码器 Web 服务的結果是 XML 格式的。为了从 XML 中提取此查询的结果,我们使用了 Ruby 自带的 REXML 库。这允许我们检索 geo:long 和 geo:lat 元素,然后抓取元素的文本内容
<% longitude = xml.root.elements["/rdf:RDF/geo:Point/geo:long"].text %> <% latitude = xml.root.elements["/rdf:RDF/geo:Point/geo:lat"].text %>
完成这项艰苦的工作后,我们现在插入适当的 JavaScript
var myMarker<%= index %> = new GMarker(new GPoint(<%= longitude %>, <%= latitude %>)); map.addOverlay(myMarker<%= index %>);
在此过程中,我们将城市名称和位置收集到一个名为 final_list 的数组中。然后,我们可以使用它在文档末尾生成一个列表
<% final_list.each do |city| %> <tr> <td><%= city['city'] %></td> <td><%= city['longitude'] %></td> <td><%= city['latitude'] %></td> </tr> <% end %>
果然,这会生成一个页面,其中包含一个 Google 地图,显示所有这些位置,并在底部有一个列表。
尽管以上内容不错,但城市信息仍然是硬编码的。我们想要的是能够检索有关特定书籍的第三方卖家的信息。这意味着我们必须从用户那里获取 ISBN,向亚马逊询问该书的第三方卖家,然后获取每个卖家所在的城市和州。我们的代码在很大程度上将保持不变,除了我们定义城市数组的方式,这将更加复杂。您可以在清单 3 中看到生成的代码。
清单 3. 添加亚马逊信息
<% require 'cgi' %> <% require 'net/http' %> <% require 'rexml/document' %> <% cgi = CGI.new %> <% isbn = cgi['isbn'] %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <title>Where you can find used copies of ISBN <%= isbn %></title> <script src="http://maps.google.com/maps?file=api&v=1&key= ↪ABQIAAAAQQK9JhAXQ9eq-G55qgu1ExTnGAXa-Bs2i826H4DeSQaC3Vqy- ↪xSjDFaTYAO0N5YPQWIEeUbqJMhhbA" type="text/javascript"></script> </head> <body> <h2>Here is your map</h2> <div id="map" style="width: 800px; height: 600px"></div> <script type="text/javascript"> var map = new GMap(document.getElementById("map")); map.centerAndZoom(new GPoint(-87.740070, 42.037030), 13); map.addControl(new GSmallMapControl()); map.addControl(new GMapTypeControl()); <% final_list = [ ] %> <% amazon_params = {'Service' => 'AWSECommerceService', 'Operation' => 'ItemLookup', 'AWSAccessKeyId' => 'XXX', 'ItemId' => isbn, 'ResponseGroup' => 'Medium,OfferFull', 'MerchantId' => 'All'}.map {|key,value| "#{key}=#{value}"}.join("&") amazon_response = Net::HTTP.get_response('webservices.amazon.com', '/onca/xml?' << amazon_params) xml = REXML::Document.new(amazon_response.body) # Get the vendors, and use that information to get their locations cities = [ ] xml.root.elements.each("Items/Item/Offers/Offer/Seller/SellerId") do |seller| # Now get information about each vendor amazon_vendor_params = {'Service' => 'AWSECommerceService', 'Operation' => 'SellerLookup', 'AWSAccessKeyId' => 'XXX', 'SellerId' => seller.text}.map {|key,value| "#{key}=#{value}"}.join("&") vendor_response = Net::HTTP.get_response('webservices.amazon.com', '/onca/xml?' << amazon_vendor_params) vendor_xml = REXML::Document.new(vendor_response.body) vendor_city = vendor_xml.root.elements["/SellerLookupResponse/Sellers/ Seller/Location/City"].text vendor_state = vendor_xml.root.elements["/SellerLookupResponse/Sellers/ Seller/Location/State"].text cities << "#{vendor_city},#{vendor_state},US" end cities.each_with_index do |city, index| geocoder_response = Net::HTTP.get_response('brainoff.com', "/geocoder/rest/?city=#{city.gsub(' ','+')}") geocoder_xml = REXML::Document.new(geocoder_response.body) next if geocoder_xml.root.nil? longitude = geocoder_xml.root.elements["/rdf:RDF/geo:Point/geo:long"].text latitude = geocoder_xml.root.elements["/rdf:RDF/geo:Point/geo:lat"].text final_list << {'city' => city, 'longitude' => longitude, 'latitude' => latitude } %> var myMarker<%= index %> = new GMarker(new GPoint(<%= longitude %>, <%= latitude %>)); map.addOverlay(myMarker<%= index %>); <% end %> </script> <body> <h2>Your cities</h2> <table border="1"> <tr> <th>City</th> <th>Longitude</th> <th>Latitude</th> </tr> <% final_list.each do |city| %> <tr> <td><%= city['city'].gsub(",", ", ") %></td> <td><%= city['longitude'] %></td> <td><%= city['latitude'] %></td> </tr> <% end %> </table> </body> </html>
从最终用户那里获取 ISBN 非常简单。在文件顶部,我们导入 CGI 类
<% require 'cgi' %>
现在我们可以检索用户输入的 ISBN
<% cgi = CGI.new %> <% isbn = cgi['isbn'] %>
我们使用此 ISBN 查找所有拥有这本书副本的第三方卖家。(实际上,我们只会查看最多十个第三方供应商;亚马逊一次只返回十个项目,我们不会通过查找其他结果页面来使我们的代码复杂化。)我们将每个返回的供应商放入我们的供应商数组中。
因此,让我们首先获取有关我们书籍二手副本供应商的信息。我们通过向亚马逊发送针对我们的 ISBN 的 REST 请求来做到这一点
amazon_params = {'Service' => 'AWSECommerceService', 'Operation' => 'ItemLookup', 'AWSAccessKeyId' => 'XXX', 'ItemId' => isbn, 'ResponseGroup' => 'Medium,OfferFull', 'MerchantId' => 'All'}.map {|key,value| "#{key}=#{value}"}.join("&") amazon_response = Net::HTTP.get_response('webservices.amazon.com', '/onca/xml?' << amazon_params)
以上是我跟踪名称和值的首选技术,尤其是在我传递很多名称和值时——我创建一个哈希,用 = 符号连接键和值,然后用 & 符号连接对本身。这给了我一个可以交给亚马逊的字符串。
然后我收到的 XML 响应包含大量信息,包括有关每个报价的详细信息。这实际上是我在这里关心的全部;我没有跟踪书的价格(这当然很有用),而是跟踪我们可以抓取的每个二手副本的位置。但是我们无法立即获得它;ItemLookup 请求仅获取卖家 ID 和有关每个卖家的一些基本信息。我们需要从每个报价节点中抓取卖家 ID,然后使用它执行第二个亚马逊请求,获取有关供应商的信息
xml.root.elements.each("Items/Item/Offers/Offer/Seller/SellerId") do |seller| # Now get information about each vendor amazon_vendor_params = {'Service' => 'AWSECommerceService', 'Operation' => 'SellerLookup', 'AWSAccessKeyId' => 'XXX', 'SellerId' => seller.text}.map {|key,value| "#{key}=#{value}"}.join("&") vendor_response = Net::HTTP.get_response('webservices.amazon.com', '/onca/xml?' << amazon_vendor_params) vendor_xml = REXML::Document.new(vendor_response.body)
此代码向亚马逊发送请求,取回 XML 正文,然后查找供应商将生成的城市和州元素。不幸的是,对于美国以外的国家/地区,无论是在地理编码还是亚马逊方面,都没有快速简便的方法来处理。亚马逊的假设似乎是加拿大有点像美国,这是错误的。因此,我们将始终获得城市和州,并假设它在美国。如果我们的假设被证明是错误的,我们将允许自己被地理编码器纠正。
当我们抓取有关每个供应商的信息时,我们将城市和州信息粘贴到城市数组中。现在我们将使用相同的数组,就像我们在 mashup2.rhtml 中所做的那样——除了现在,来源不是硬编码列表,而是我们从亚马逊信息中整理出来的列表。我们只需要进行两处更改即可使事情正常工作:检查我们是否没有从地理编码器收到 nil(表明存在错误,通常是因为供应商在加拿大),以及使用 gsub 将空格字符更改为城市名称中的 + 符号。
结果非常棒,即使它们不完整且有点粗糙:通过访问诸如 http://maps.lerner.co.il/mashup3.rhtml?isbn=0812931432 之类的 URL,我们可以看到许多二手副本位于美国的位置。这不一定反映书的成本、其状况或运费——但看到不同的书最终出现在哪里,以及哪些城市往往拥有更多(和更少)的二手书,可能会很有趣且很有意思。
创建混合应用,即现有 Web 服务的组合,可能非常有趣,并且可以通过将数据放在地图上来更容易地查看数据中的模式。它要求您对底层技术及其怪癖有很好的理解——但是通过一些工作,您会发现创建此类混合应用可能既有趣又令人兴奋,甚至很有娱乐性。此外,随着 Web 变得越来越互连,并且随着应用程序继续模糊桌面和 Web 之间的界限,我们应该期望看到更多此类混合应用,而不是更少。
本文资源: /article/9013。
Reuven M. Lerner,一位长期的 Web/数据库顾问,目前是伊利诺伊州埃文斯顿西北大学学习科学专业的博士生。他和他的妻子最近庆祝了他们的儿子 Amotz David 的诞生。