精通 Grails: Grails 服务和 Google 地图,将外部技术融入到 Grails 应用程序

自本系列的 第一篇文章 开始,我就一直在构建一个 trip-planner 应用程序。目前基本的模型-视图-控制器(Model-View-Controller,MVC)框架已经准备就绪,我将加入一些外部技术,具体来讲,我将加入地图功能。虽然我可以表示 “我的旅程是从丹佛到罗利,途经圣何塞和西雅图”,但地图将能更好地描述旅途路线。您可能知道西雅图和罗利是在美国的两端,但地图能够帮助您显示出两个城市之间的距离。

这个应用程序有什么用?本文的末尾为您提供一个大体的介绍。请访问 http://maps.google.com 并在搜索框内输入 IATA 代码 DEN。将出现丹佛国际机场(Denver International Airport),如图 1 所示

图 1. 丹佛机场(由 Google Map 友情提供)
精通 Grails: Grails 服务和 Google 地图,将外部技术融入到 Grails 应用程序

除了能显示您在 HTML 表创建的美国机场以外,trip planner 还将在地图上把机场描绘出来。在本文中,我将使用免费的 Google Maps API。我还可以使用免费的 Yahoo! Maps API,等等(参见 参考资料)。一旦了解在线 Web 地图绘制的基本原理之后,您将发现不同的 API 之间能够合理地互换。在讨论该解决方案的地图绘制部分之前,您需要了解如何将一个简单的三个字母的字符串(如 DEN)转换为地图上的一点。

地理编码

当向 Google Map 输入 DEN 时,这个应用程序在幕后进行了一些转换。您可能用街道地址(如 123 Main Street)的方式想象地理位置,但 Google Map 需要一个纬度/经度点,以便在地图上把它显示出来。这并不需要您自己设法提供纬度/经度点,应用程序会替您把人类能够识别的地址转换为纬度/经度点。这一转换过程称为地理编码(参见 参考资料)。

精通 Grails: Grails 服务和 Google 地图,将外部技术融入到 Grails 应用程序
关于本系列

Grails 是一种新型 Web 开发框架,它将常见的 Spring 和 Hibernate 等 Java™ 技术与当前流行的约定优于配置等实践相结合。Grails 是用 Groovy 编写的,它可以提供与遗留 Java 代码的无缝集成,同时还可以加入脚本编制语言的灵活性和动态性。学习完 Grails 之后,您将彻底改变看待 Web 开发的方式。

浏览 Web 时,也会发生一个类似的转换。从技术角度来说,联系远程 Web 服务器的惟一方式是提供服务器的 IP 地址。幸运的是,您不需要自己输入 IP 地址。只要将友好的 URL 输入到 Web 浏览器,它将调用域名系统(DNS)服务器。DNS 服务器会将 URL 转换为对应的 IP 地址,然后浏览器与远程服务器建立 HTTP 连接。所有这些对用户而言都是透明的。DNS 使 Web 的使用容易了很多。同样,地理编码器也使基于 Web 的地图绘制应用程序更加容易使用。

在 Web 上快速搜索免费地理编码器 会产生许多符合 trip planner 地理编码需求的结果。Google 和 Yahoo! 都提供地理编码服务,并把它作为 API 的标准部分,但针对这个应用程序,我将使用由 geonames.org(参见 参考资料)提供的免费地理编码服务。它的 RESTful API 允许我指明我提供的是 IATA 代码,而不是通用的文本搜索术语。比如,ORD 并不是指内布拉斯加州 Ord. 市的居民,ORD 指的是 Chicago O'Hare International Airport。

在 Web 浏览器中输入 URL http://ws.geonames.org/search?name_equals=den&fcode=airp&style=full。您将看到 XML 响应,如清单 1 所示:


清单 1. 来自地理编码请求的 XML 结果

  1. <geonamesstyle="FULL">
  2. <totalResultsCount>1</totalResultsCount>
  3. <geoname>
  4. <name>DenverInternationalAirport</name>
  5. <lat>39.8583188</lat>
  6. <lng>-104.6674674</lng>
  7. <geonameId>5419401</geonameId>
  8. <countryCode>US</countryCode>
  9. <countryName>UnitedStates</countryName>
  10. <fcl>S</fcl>
  11. <fcode>AIRP</fcode>
  12. <fclName>spot,building,farm</fclName>
  13. <fcodeName>airport</fcodeName>
  14. <population/>
  15. <alternateNames>DEN,KDEN</alternateNames>
  16. <elevation>1655</elevation>
  17. <continentCode>NA</continentCode>
  18. <adminCode1>CO</adminCode1>
  19. <adminName1>Colorado</adminName1>
  20. <adminCode2>031</adminCode2>
  21. <adminName2>DenverCounty</adminName2>
  22. <alternateNamelang="iata">DEN</alternateName>
  23. <alternateNamelang="icao">KDEN</alternateName>
  24. <timezonedstOffset="-6.0"gmtOffset="-7.0">America/Denver</timezone>
  25. </geoname>
  26. </geonames>

您在 URL 中输入的 name_equals 参数是该机场的 IATA 代码。这只是在每个查询中需要更改的 URL 的一部分。fcode=airp 表明您正在搜索的特征代码是一个机场。style 参数 — shortmediumlongfull — 指定了 XML 响应的详细程度。

现在已经准备好地理编码器,下一步就是将它与 Grails 应用程序集成在一起。为此,您需要一个服务

Grails 服务

到目前为止,通过学习 精通 Grails 系列文章,您应该已经明白域类、控制器和 Groovy 服务器页面(Groovy Server Pages,GSP 是如何协调工作的。它们简化了在单一数据类型上执行基本的创建/检索/更新/删除(Create/Retrieve/Update/Delete,CRUD)操作。这个地理编码服务似乎略微超出了简单 Grails Object Relational Mapping(GORM)转换(从关系数据库记录到普通的旧 Groovy 对象(plain old Groovy objects,POGO))的范围。同样,这个服务很可能由多种方法使用。稍后您将看到,对 IATA 代码进行地理编码需要用到 saveupdate。Grails 为您提供了保存常用方法的位置,并且超越了任何单个的域类:即服务。

要创建 Grails 服务,请在命令行输入 grails create-service Geocoder。在文本编辑器中查看 grails-app/services/GeocoderService.groovy,如清单 2 所示:


清单 2. 一个无存根(stubbed-out)Grails 服务

                
class GeocoderService {
    boolean transactional = true
    def serviceMethod() {

    }
}

如果使用同一个方法进行多个数据库查询,那么将涉及到 transactional 字段。它将所有内容都包装在一个单个数据库事务中,如果任何一个查询失败,该数据库事务将回滚到原来的状态。因为在本示例中您远程地调用 Web 服务,所以可以安全地将它设置为 false

名称 serviceMethod 是一个占位符(placeholder),可以将其改为更具描述性的内容(服务可以包含任意多种方法)。在清单 3 中, 我把名称改为 geocodeAirport


清单 3. geocodeAirport() 地理编码器服务方法

  1. classGeocoderService{
  2. booleantransactional=false
  3. //http://ws.geonames.org/search?name_equals=den&fcode=airp&style=full
  4. defgeocodeAirport(Stringiata){
  5. defbase="http://ws.geonames.org/search?"
  6. defqs=[]
  7. qs<<"name_equals="+URLEncoder.encode(iata)
  8. qs<<"fcode=airp"
  9. qs<<"style=full"
  10. defurl=newURL(base+qs.join("&"))
  11. defconnection=url.openConnection()
  12. defresult=[:]
  13. if(connection.responseCode==200){
  14. defxml=connection.content.text
  15. defgeonames=newXmlSlurper().parseText(xml)
  16. result.name=geonames.geoname.nameasString
  17. result.lat=geonames.geoname.latasString
  18. result.lng=geonames.geoname.lngasString
  19. result.state=geonames.geoname.adminCode1asString
  20. result.country=geonames.geoname.countryCodeasString
  21. }
  22. else{
  23. log.error("GeocoderService.geocodeAirportFAILED")
  24. log.error(url)
  25. log.error(connection.responseCode)
  26. log.error(connection.responseMessage)
  27. }
  28. returnresult
  29. }
  30. }

geocodeAirport 方法的第一部分构建 URL 并进行连接。查询字符串元素先集中在一个 ArrayList 里,然后和一个 & 符号连接起来。方法的最后部分使用 Groovy XmlSlurper 解析 XML 结果并将结果存储在 hashmap 里。

Groovy 服务不可以直接从 URL 访问。如果您想在 Web 浏览器中测试这个新的服务方法,请将一个简单的闭包添加到 AirportController,如清单 4 所示:


清单 4. 在控制器中向服务提供一个 URL

  1. importgrails.converters.*
  2. classAirportController{
  3. defgeocoderService
  4. defscaffold=Airport
  5. defgeocode={
  6. defresult=geocoderService.geocodeAirport(params.iata)
  7. renderresultasJSON
  8. }
  9. ...
  10. }

如果您定义一个与服务同名的成员变量,Spring 会自动地将服务注入控制器(要想让这种方法奏效,您必须把服务名的第一个字母由大写改为小写,使它遵循 Java 风格的变量命名约定)。

要测试服务,请在 Web 浏览器中输入 URL http://localhost:9090/trip/airport/geocode?iata=den。您将看到如清单 5 所示的结果:


清单 5. 地理编码器请求的结果
                
{"name":"Denver International Airport",
"lat":"39.8583188",
"lng":"-104.6674674",
"state":"CO",
"country":"US"}

AirportController 中的 geocode 闭包只是用于对服务进行检查。因此,可以把它删除,或者保留下来供以后的 Ajax 调用使用。下一步是重新构造 Airport 基础设施,以利用这个新的地理编码服务。

加入服务

首先,把新的 latlng 字段添加到 grails-app/domain/Airport.groovy,如清单 6 所示:


清单 6. 把 latlng 字段添加到 Airport POGO

  1. classAirport{
  2. staticconstraints={
  3. name()
  4. iata(maxSize:3)
  5. city()
  6. state(maxSize:2)
  7. country()
  8. }
  9. Stringname
  10. Stringiata
  11. Stringcity
  12. Stringstate
  13. Stringcountry="US"
  14. Stringlat
  15. Stringlng
  16. StringtoString(){
  17. "${iata}-${name}"
  18. }
  19. }

在命令提示处输入 grails generate-views Airport 来创建 GSP 文件。借助 AirportController.groovy 的 def scaffold = Airport 行,从运行时开始就一直在动态搭建 GSP 文件。要想对这个视图进行更改,我必须先处理代码。

创建新的 Airport 时,我将把用户可编辑字段限制为 iatacity。要想让地理编码查询能够工作,必须具备 iata 字段。我没有更改 city,因为我喜欢由自己来提供这个信息。DEN 真的就在丹佛(Denver),但 ORD(Chicago O'Hare)却在伊里诺斯州的罗斯蒙特(Rosemont),而 CVG(俄亥俄州辛辛那提机场,Cincinnati,Ohio airport)则在肯塔基州的佛罗伦萨市(Florence)。将这两个字段留在 create.gsp 里,其余的删除。现在 create.gsp 如清单 7 所示:


清单 7. 修改 create.gsp

  1. <g:formaction="save"method="post">
  2. <divclass="dialog">
  3. <table>
  4. <tbody>
  5. <trclass="prop">
  6. <tdvalign="top"class="name"><labelfor="iata">Iata:</label></td>
  7. <tdvalign="top"
  8. class="value${hasErrors(bean:airport,field:'iata','errors')}">
  9. <inputtype="text"
  10. maxlength="3"
  11. id="iata"
  12. name="iata"
  13. value="${fieldValue(bean:airport,field:'iata')}"/>
  14. </td>
  15. </tr>
  16. <trclass="prop">
  17. <tdvalign="top"class="name"><labelfor="city">City:</label></td>
  18. <tdvalign="top"
  19. class="value${hasErrors(bean:airport,field:'city','errors')}">
  20. <inputtype="text"
  21. id="city"
  22. name="city"
  23. value="${fieldValue(bean:airport,field:'city')}"/>
  24. </td>
  25. </tr>
  26. </tbody>
  27. </table>
  28. </div>
  29. <divclass="buttons">
  30. <spanclass="button"><inputclass="save"type="submit"value="Create"/></span>
  31. </div>
  32. </g:form>

图 2 展示了所产生的表单:


图 2. 创建 Airport 表单
精通 Grails: Grails 服务和 Google 地图,将外部技术融入到 Grails 应用程序

该表提交到 AirportController 中的 save 闭包。将清单 8 中的代码添加到控制器,以在保存新的 Airport 之前调用 geocodeAirport


清单 8. 修改 save 闭包
                def save = {
    def results = geocoderService.geocodeAirport(params.iata)    
    def airport = new Airport(params + results)
    if(!airport.hasErrors() && airport.save()) {
        flash.message = "Airport ${airport.id} created"
        redirect(action:show,id:airport.id)
    }
    else {
        render(view:'create',model:[airport:airport])
    }
}

如果在命令提示处输入 grails generate-controller Airport,方法的主要部分将与您所看到的一样。仅仅是开始的两行与默认生成的闭包不同。第一行从 geocoder 服务获得一个 HashMap。第二行将 results HashMapparams HashMap 合并起来(当然,在 Groovy 中合并两个 HashMap 就像把它们添加到一起一样简单)。

如果数据库保存成功的话,将重定向到显示操作。幸运的是,不需要更改 show.gsp,如图 3 所示:


图 3. 显示 Airport 表单
精通 Grails: Grails 服务和 Google 地图,将外部技术融入到 Grails 应用程序

要编辑 Airport,必须保持 iatacity 字段在 edit.gsp 中不变。您可以从 show.gsp 复制和粘贴其余的字段,把它们变为只读字段(或者,如果您能从 前期文章 体会到 “复制和粘贴是面向对象编程的最低级形式” 的话,您可以把常用字段提取到一个局部模板并在 show.gsp 和 edit.gsp 中呈现它)。清单 9 展示了修改后的 edit.gsp:


清单 9. 修改 edit.gsp

  1. <g:formmethod="post">
  2. <inputtype="hidden"name="id"value="${airport?.id}"/>
  3. <divclass="dialog">
  4. <table>
  5. <tbody>
  6. <trclass="prop">
  7. <tdvalign="top"class="name"><labelfor="iata">Iata:</label></td>
  8. <tdvalign="top"
  9. class="value${hasErrors(bean:airport,field:'iata','errors')}">
  10. <inputtype="text"
  11. maxlength="3"
  12. id="iata"
  13. name="iata"
  14. value="${fieldValue(bean:airport,field:'iata')}"/>
  15. </td>
  16. </tr>
  17. <trclass="prop">
  18. <tdvalign="top"class="name"><labelfor="city">City:</label></td>
  19. <tdvalign="top"
  20. class="value${hasErrors(bean:airport,field:'city','errors')}">
  21. <inputtype="text"
  22. id="city"
  23. name="city"
  24. value="${fieldValue(bean:airport,field:'city')}"/>
  25. </td>
  26. </tr>
  27. <trclass="prop">
  28. <tdvalign="top"class="name">Name:</td>
  29. <tdvalign="top"class="value">${airport.name}</td>
  30. </tr>
  31. <trclass="prop">
  32. <tdvalign="top"class="name">State:</td>
  33. <tdvalign="top"class="value">${airport.state}</td>
  34. </tr>
  35. <trclass="prop">
  36. <tdvalign="top"class="name">Country:</td>
  37. <tdvalign="top"class="value">${airport.country}</td>
  38. </tr>
  39. <trclass="prop">
  40. <tdvalign="top"class="name">Lat:</td>
  41. <tdvalign="top"class="value">${airport.lat}</td>
  42. </tr>
  43. <trclass="prop">
  44. <tdvalign="top"class="name">Lng:</td>
  45. <tdvalign="top"class="value">${airport.lng}</td>
  46. </tr>
  47. </tbody>
  48. </table>
  49. </div>
  50. <divclass="buttons">
  51. <spanclass="button"><g:actionSubmitclass="save"value="Update"/></span>
  52. <spanclass="button">
  53. <g:actionSubmitclass="delete"
  54. onclick="returnconfirm('Areyousure?');"
  55. value="Delete"/>
  56. </span>
  57. </div>
  58. </g:form>

所产生的表单如图 4 所示:


图 4. 编辑 Airport 表单
精通 Grails: Grails 服务和 Google 地图,将外部技术融入到 Grails 应用程序

单击 Update 按钮将表单值发送到 update 闭包。将服务调用和 hashmap 合并添加到默认代码,如清单 10 所示:


清单 10. 修改 update 闭包

  1. defupdate={
  2. defairport=Airport.get(params.id)
  3. if(airport){
  4. defresults=geocoderService.geocodeAirport(params.iata)
  5. airport.properties=params+results
  6. if(!airport.hasErrors()&&airport.save()){
  7. flash.message="Airport${params.id}updated"
  8. redirect(action:show,id:airport.id)
  9. }
  10. else{
  11. render(view:'edit',model:[airport:airport])
  12. }
  13. }
  14. else{
  15. flash.message="Airportnotfoundwithid${params.id}"
  16. redirect(action:edit,id:params.id)
  17. }
  18. }

到目前为止,您已经像 Google Map 一样无缝地将地理编码集成到您的应用程序里。花点时间想一想在应用程序中捕获地址的所有位置 — 顾客、雇员、远程办公室、仓库和零售点等等。通过简单地添加几个字段以存储纬度/经度坐标和加入一个地理编码服务,就能够设置一些简易的地图来显示对象 — 这正是我下一步的工作。

Google Map

许多人都知道为了易于使用,Google Map 针对 Web 地图绘制设置了标准。但很少人知道到这个标准也适用于将 Google Map 嵌入到您自己的 Web 页面中。为数据点获取纬度/经度坐标是这个应用中最困难的部分,但我们已经解决了这个问题。

要将 Google Map 嵌入到 Grails 应用程序中,首先要做的是获得一个免费的 API 密匙。注册页面详细说明了使用条款。实际上,只要您的应用程序是免费的,Google 也将免费为您提供 API。这意味着您不能对 Google Map 应用程序进行密码保护、收取访问费用或把它托管在防火墙后面(做个广告:由我撰写的 GIS for Web Developers 一书将逐步指导您使用免费数据和开发源码软件构建类似于 Google Map 的应用程序;参见 参考资料。这使您不再受到 Google 的 API 使用限制的约束)。

API 密匙通常绑定到一个特定的 URL 和目录。在表单中输入 http://localhost:9090/trip 并单击 Generate API Key 按钮。确认页面将显示刚生成的 API 密匙、与密匙相关联的 URL 和一个 “get you started on your way to mapping glory” 的示例 Web 页面。

为了将这个示例页面并入到 Grails 应用程序,需要在 grails-app/views/airport 目录中创建一个名为 map.gsp 的文件。从 Google 将示例页面复制到 map.gsp。 清单 11 展示了 map.gsp 的内容:


清单 11. 一个简单的 Google Map Web 页面

  1. <!DOCTYPEhtmlPUBLIC"-//W3C//DTDXHTML1.0Strict//EN"
  2. "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
  3. <htmlxmlns="http://www.w3.org/1999/xhtml">
  4. <head>
  5. <metahttp-equiv="content-type"content="text/html;charset=utf-8"/>
  6. <title>GoogleMapsJavaScriptAPIExample</title>
  7. <scriptsrc="http://maps.google.com/maps?file=api&v=2&key=ABCDE"
  8. type="text/javascript"></script>
  9. <scripttype="text/javascript">
  10. //<![CDATA[
  11. functionload(){
  12. if(GBrowserIsCompatible()){
  13. varmap=newGMap2(document.getElementById("map"));
  14. map.setCenter(newGLatLng(37.4419,-122.1419),13);
  15. }
  16. }
  17. //]]>
  18. </script>
  19. </head>
  20. <bodyonload="load()"onunload="GUnload()">
  21. <divid="map"style="width:500px;height:300px"></div>
  22. </body>
  23. </html>

注意,API 密匙嵌入在页面顶部的脚本 URL 里。在 load 方法中,您正在实例化一个新的 GMap2 对象。这就是出现在 <div /> 里的地图,同时 map 的 ID 出现在页面的底端。如果想让地图变大些,可以在层叠样式表(Cascading Style Sheets,CSS)的 style 属性中调整地图的宽度和高度。目前,这个地图以加利福尼亚州的帕洛阿图市为中心,缩放倍数为 13 级(0 级是最小的。级别越大越接近街道级别的视图)。您可以快速地调整这些值。同时,将一个空 map 闭包添加到 AirlineController,如清单 12 所示:


清单 12. 添加 map 闭包
                
class AirportController {
  def map = {}

  ...
}  

现在,浏览 http://localhost:9090/trip/airport/map,您将看到已嵌入的 Google Map,如图 5 所示:


图 5. 简单的 Google Map
精通 Grails: Grails 服务和 Google 地图,将外部技术融入到 Grails 应用程序

现在先回到 map.gsp 并调整值,如清单 13 所示:


清单 13. 调整基本的地图

  1. <scripttype="text/javascript">
  2. varusCenterPoint=newGLatLng(39.833333,-98.583333)
  3. varusZoom=4
  4. functionload(){
  5. if(GBrowserIsCompatible()){
  6. varmap=newGMap2(document.getElementById("map"))
  7. map.setCenter(usCenterPoint,usZoom)
  8. map.addControl(newGLargeMapControl());
  9. map.addControl(newGMapTypeControl());
  10. }
  11. }
  12. </script>
  13. </head>
  14. <bodyonload="load()"onunload="GUnload()">
  15. <divid="map"style="width:800px;height:400px"></div>
  16. </body>

要查看整个美国,将尺寸设置为 800 x 400 像素是比较好的。清单 13 调整了中心点和缩放级别,使您能够看到完整的地图。您还可以添加许多不同的地图控制。清单 13 中的 GLargeMapControlGMapTypeControl 分别在地图左边和右上角提供了常用的控制。在您调试时不断点击浏览器的 Refresh 按钮,查看修改后的效果。图 6 反映了对清单 13 所做的调整:


图 6. 调整后的地图
精通 Grails: Grails 服务和 Google 地图,将外部技术融入到 Grails 应用程序

现在基本的地图已经做好, 接下来就可以添加标记了 — 为每个机场添加图钉。在将这一过程自动化之前,我在清单 14 中手工添加了一些简单的标记:


清单 14. 将标记添加到地图

  1. <scripttype="text/javascript">
  2. varusCenterPoint=newGLatLng(39.833333,-98.583333)
  3. varusZoom=4
  4. functionload(){
  5. if(GBrowserIsCompatible()){
  6. varmap=newGMap2(document.getElementById("map"))
  7. map.setCenter(usCenterPoint,usZoom)
  8. map.addControl(newGLargeMapControl());
  9. map.addControl(newGMapTypeControl());
  10. varmarker=newGMarker(newGLatLng(39.8583188,-104.6674674))
  11. marker.bindInfoWindowHtml("DEN<br/>DenverInternationalAirport")
  12. map.addOverlay(marker)
  13. }
  14. }
  15. </script>

GMarker 构造器采用了一个 GLatLng 点。bindInfoWindowHtml 方法提供了用户单击标记时在 Info 窗口内显示的 HTML 文件片段。最后,清单 14 还通过使用 addOverlay 方法将标记添加到地图。

图 7 展示了添加标记后的地图:


图 7. 带标记的地图
精通 Grails: Grails 服务和 Google 地图,将外部技术融入到 Grails 应用程序

现在您已经知道如何添加一个单一的点,但要自动地添加数据库里的所有点,还需要做两个小的更改。第一个更改是在 AirportController 中生成 map 闭包,这会返回一个 Airport 列表,如清单 15 所示:


清单 15. 返回一个 Airport 列表
                
def map = {
  [airportList: Airport.list()]
}

接下来,需要遍历 Airport 列表并为每个 Airport 创建标记。在本系列的早期文章中,曾经介绍使用 <g:each> 标记向 HTML 表添加行。清单 16 使用这个标记创建必要的 JavaScript 行,这样才能在地图上显示 Airport


清单 16. 动态地为地图添加标记

  1. <scripttype="text/javascript">
  2. varusCenterPoint=newGLatLng(39.833333,-98.583333)
  3. varusZoom=4
  4. functionload(){
  5. if(GBrowserIsCompatible()){
  6. varmap=newGMap2(document.getElementById("map"))
  7. map.setCenter(usCenterPoint,usZoom)
  8. map.addControl(newGLargeMapControl());
  9. map.addControl(newGMapTypeControl());
  10. <g:eachin="${airportList}"status="i"var="airport">
  11. varpoint${airport.id}=newGLatLng(${airport.lat},${airport.lng})
  12. varmarker${airport.id}=newGMarker(point${airport.id})
  13. marker${airport.id}.bindInfoWindowHtml("${airport.iata}<br/>${airport.name}")
  14. map.addOverlay(marker${airport.id})
  15. </g:each>
  16. }
  17. }
  18. </script>

图 8 展示了自动添加了标记后的地图:


图 8. 带有多个标记的地图
精通 Grails: Grails 服务和 Google 地图,将外部技术融入到 Grails 应用程序

这个针对 Google Maps API 的简单介绍只涉及到些皮毛,您可以进行的操作远不止这些。您可能已经决定利用事件模型来实现 Ajax 调用,当单击标记时就会返回 JavaScript Object Notation(JSON)数据。您可以使用 GPolyline 在地图上描绘一个旅途中的各段行程。可以实现无限的可能性。要获得更多的信息,可以参考 Google 的在线文档。您也可以从我的 PDF 书籍 Google Maps API 获得关于 API 的适当介绍(参见 参考资料)。

结束语

地图添加到 Grails 应用程序需要具备 3 个条件。

第一个条件是对数据进行地理编码。有许多免费的地理编码器,它们可以将人类能识别的地理位置转换为纬度/经度点。几乎能够对所有的内容进行地理编码:街道地址、城市、县城、国家、邮政区码、电话号码、IP 地址等,甚至包括机场的 IATA 代码。

在您找到合适的地理编码器之后,创建一个 Grails 服务以把远程 Web 服务调用封装在可重用方法调用中。服务是为在单一区域对象上超越简单 CRUD 操作的方法而准备的。在默认情况下,服务并不与 URL 相关联,但是您可以轻易地在控制器中创建一个闭包,使这些服务可以通过 Web 找到。

最后,利用免费的 Web 地图绘制 API(比如 Google Map)在地图上描绘纬度/经度点。这些免费的服务通常也要求您的应用程序可以免费访问。如果您希望地图是隐私的,请考虑使用开放源代码 API(比如 OpenLayers),它提供了和 Google Map 一样的用户体验,但没有 Google Map 那样的使用限制(参见 参考资料)。您将需要提供自己的地图绘制层,但可以将整个应用程序托管到自己的服务器上,从而保持它的隐私性。

在下一篇文章,我将讨论使 Grails 应用程序适用于移动电话的方法。您将看到如何优化 iPhone 的显示视图。我还将演示通过电子邮件从 Grails 发送信息,这个信息将在移动电话中显示为 SMS 消息。 那时,您将享受精通 Grails 的乐趣。