精通 Grails: Grails 服务和 Google 地图

将外部技术融入到 Grails 应用程序

精通 Grails 系列的这一期中,Scott Davis 向您展示如何使用免费的 APIs 和 Web 服务将地图添加到 Grails 应用程序。他仍将使用前面几期的 trip-planner 示例应用程序,并通过地理编码、Google Map 和 Grails 服务进行进一步改进。

Scott Davis, 主编, AboutGroovy.com

Scott DavisScott Davis 是国际知名作家、演讲家、软件开发人员。他出版的书籍有 Groovy Recipes: Greasing the Wheels of JavaGIS for Web Developers: Adding Where to Your ApplicationThe Google Maps APIJBoss At Work



2008 年 6 月 12 日

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

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

图 1. 丹佛机场(由 Google Map 友情提供)
丹佛机场(由 Google Map 友情提供)

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

地理编码

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

关于本系列

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 结果
<geonames style="FULL">
  <totalResultsCount>1</totalResultsCount>
  <geoname>
    <name>Denver International Airport</name>
    <lat>39.8583188</lat>
    <lng>-104.6674674</lng>
    <geonameId>5419401</geonameId>
    <countryCode>US</countryCode>
    <countryName>United States</countryName>
    <fcl>S</fcl>
    <fcode>AIRP</fcode>
    <fclName>spot, building, farm</fclName>
    <fcodeName>airport</fcodeName>
    <population/>
    <alternateNames>DEN,KDEN</alternateNames>
    <elevation>1655</elevation>
    <continentCode>NA</continentCode>
    <adminCode1>CO</adminCode1>
    <adminName1>Colorado</adminName1>
    <adminCode2>031</adminCode2>
    <adminName2>Denver County</adminName2>
    <alternateName lang="iata">DEN</alternateName>
    <alternateName lang="icao">KDEN</alternateName>
    <timezone dstOffset="-6.0" gmtOffset="-7.0">America/Denver</timezone>
  </geoname>
</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() 地理编码器服务方法
class GeocoderService {
    boolean transactional = false

    // http://ws.geonames.org/search?name_equals=den&fcode=airp&style=full
    def geocodeAirport(String iata) {
      def base = "http://ws.geonames.org/search?"
      def qs = []
      qs << "name_equals=" + URLEncoder.encode(iata)
      qs << "fcode=airp"
      qs << "style=full"
      def url = new URL(base + qs.join("&"))
      def connection = url.openConnection()

      def result = [:]
      if(connection.responseCode == 200){
        def xml = connection.content.text
        def geonames = new XmlSlurper().parseText(xml)
        result.name = geonames.geoname.name as String 
        result.lat = geonames.geoname.lat as String
        result.lng = geonames.geoname.lng as String
        result.state = geonames.geoname.adminCode1 as String
        result.country = geonames.geoname.countryCode as String
      }
      else{
        log.error("GeocoderService.geocodeAirport FAILED")
        log.error(url)
        log.error(connection.responseCode)
        log.error(connection.responseMessage)
      }      
      return result
    }
}

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

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

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

class AirportController {
  def geocoderService
  def scaffold = Airport
  
  def geocode = {
    def result = geocoderService.geocodeAirport(params.iata)
    render result as JSON
  }  
  
  ...
}

如果您定义一个与服务同名的成员变量,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
class Airport{
  static constraints = {
    name()
    iata(maxSize:3)
    city()
    state(maxSize:2)
    country()
  }
  
  String name
  String iata
  String city
  String state
  String country = "US"
  String lat
  String lng
  
  String toString(){
    "${iata} - ${name}"
  }
}

在命令提示处输入 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
<g:form action="save" method="post" >
  <div class="dialog">
    <table>
      <tbody>                                         
        <tr class="prop">
          <td valign="top" class="name"><label for="iata">Iata:</label></td>
          <td valign="top" 
              class="value ${hasErrors(bean:airport,field:'iata','errors')}">
              <input type="text" 
                     maxlength="3" 
                     id="iata" 
                     name="iata" 
                     value="${fieldValue(bean:airport,field:'iata')}"/>
          </td>
        </tr> 
        <tr class="prop">
          <td valign="top" class="name"><label for="city">City:</label></td>
          <td valign="top" 
              class="value ${hasErrors(bean:airport,field:'city','errors')}">
              <input type="text" 
                     id="city" 
                     name="city" 
                     value="${fieldValue(bean:airport,field:'city')}"/>
          </td>
        </tr>                                                 
      </tbody>
    </table>
  </div>
  <div class="buttons">
    <span class="button"><input class="save" type="submit" value="Create" /></span>
  </div>
</g:form>

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

图 2. 创建 Airport 表单
创建 Airport

该表提交到 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。第二行将 resultsHashMapparamsHashMap 合并起来(当然,在 Groovy 中合并两个 HashMap 就像把它们添加到一起一样简单)。

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

图 3. 显示 Airport 表单
显示 Airport

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

清单 9. 修改 edit.gsp
<g:form method="post" >
  <input type="hidden" name="id" value="${airport?.id}" />
  <div class="dialog">
    <table>
      <tbody>                                              
        <tr class="prop">
          <td valign="top" class="name"><label for="iata">Iata:</label></td>
          <td valign="top" 
              class="value ${hasErrors(bean:airport,field:'iata','errors')}">
              <input type="text" 
                     maxlength="3" 
                     id="iata" 
                     name="iata" 
                     value="${fieldValue(bean:airport,field:'iata')}"/>
          </td>
        </tr>                         
        <tr class="prop">
          <td valign="top" class="name"><label for="city">City:</label></td>
          <td valign="top" 
              class="value ${hasErrors(bean:airport,field:'city','errors')}">
              <input type="text" 
                     id="city" 
                     name="city" 
                     value="${fieldValue(bean:airport,field:'city')}"/>
          </td>
        </tr> 
        <tr class="prop">
          <td valign="top" class="name">Name:</td>
          <td valign="top" class="value">${airport.name}</td>
        </tr>
        <tr class="prop">
          <td valign="top" class="name">State:</td>
          <td valign="top" class="value">${airport.state}</td>
        </tr>
        <tr class="prop">
          <td valign="top" class="name">Country:</td>
          <td valign="top" class="value">${airport.country}</td>
        </tr>
        <tr class="prop">
          <td valign="top" class="name">Lat:</td>
          <td valign="top" class="value">${airport.lat}</td>
        </tr>
        <tr class="prop">
          <td valign="top" class="name">Lng:</td>
          <td valign="top" class="value">${airport.lng}</td>
        </tr>
      </tbody>
    </table>
  </div>
  <div class="buttons">
    <span class="button"><g:actionSubmit class="save" value="Update" /></span>
    <span class="button">
      <g:actionSubmit class="delete" 
                      onclick="return confirm('Are you sure?');" 
                      value="Delete" />
    </span>
  </div>
</g:form>

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

图 4. 编辑 Airport 表单
编辑 Airport

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

清单 10. 修改 update 闭包
def update = {
    def airport = Airport.get( params.id )
    if(airport) {
        def results = geocoderService.geocodeAirport(params.iata)    
        airport.properties = params + results
        if(!airport.hasErrors() && airport.save()) {
            flash.message = "Airport ${params.id} updated"
            redirect(action:show,id:airport.id)
        }
        else {
            render(view:'edit',model:[airport:airport])
        }
    }
    else {
        flash.message = "Airport not found with id ${params.id}"
        redirect(action:edit,id:params.id)
    }
}

到目前为止,您已经像 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 页面
<!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>
    <meta http-equiv="content-type" content="text/html; charset=utf-8"/>
    <title>Google Maps JavaScript API Example</title>
    <script src="http://maps.google.com/maps?file=api&amp;v=2&amp;key=ABCDE"
            type="text/javascript"></script>
    <script type="text/javascript">
    //<![CDATA[
    function load() {
      if (GBrowserIsCompatible()) {
        var map = new GMap2(document.getElementById("map"));
        map.setCenter(new GLatLng(37.4419, -122.1419), 13);
      }
    }
    //]]>
    </script>
  </head>
  <body onload="load()" onunload="GUnload()">
    <div id="map" style="width: 500px; height: 300px"></div>
  </body>
</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
简单的 Google Map

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

清单 13. 调整基本的地图
  <script type="text/javascript">
  var usCenterPoint = new GLatLng(39.833333, -98.583333)
  var usZoom = 4

  function load() {
    if (GBrowserIsCompatible()) {
      var map = new GMap2(document.getElementById("map"))
      map.setCenter(usCenterPoint, usZoom)
      map.addControl(new GLargeMapControl());
      map.addControl(new GMapTypeControl());        
    }
  }
  </script>
</head>
<body onload="load()" onunload="GUnload()">
  <div id="map" style="width: 800px; height: 400px"></div>
</body>

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

图 6. 调整后的地图
调整后的地图

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

清单 14. 将标记添加到地图
<script type="text/javascript">
var usCenterPoint = new GLatLng(39.833333, -98.583333)
var usZoom = 4

function load() {
  if (GBrowserIsCompatible()) {
    var map = new GMap2(document.getElementById("map"))
    map.setCenter(usCenterPoint, usZoom)
    map.addControl(new GLargeMapControl());
    map.addControl(new GMapTypeControl()); 
    
    var marker = new GMarker(new GLatLng(39.8583188, -104.6674674))
    marker.bindInfoWindowHtml("DEN<br/>Denver International Airport")
      map.addOverlay(marker)                       
  }
}
</script>

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

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

图 7. 带标记的地图
带标记的地图

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

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

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

清单 16. 动态地为地图添加标记
<script type="text/javascript">
var usCenterPoint = new GLatLng(39.833333, -98.583333)
var usZoom = 4

function load() {
  if (GBrowserIsCompatible()) {
    var map = new GMap2(document.getElementById("map"))
    map.setCenter(usCenterPoint, usZoom)
    map.addControl(new GLargeMapControl());
    map.addControl(new GMapTypeControl()); 

      <g:each in="${airportList}" status="i" var="airport">
         var point${airport.id} = new GLatLng(${airport.lat}, ${airport.lng})
      var marker${airport.id} = new GMarker(point${airport.id})
      marker${airport.id}.bindInfoWindowHtml("${airport.iata}<br/>${airport.name}")
         map.addOverlay(marker${airport.id})
      </g:each>
  }
}
</script>

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

图 8. 带有多个标记的地图
带有多个标记的地图

这个针对 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 的乐趣。

参考资料

学习

  • 您可以参阅本文在 developerWorks 全球站点上的 英文原文
  • 精通 Grails:请阅读这个系列的更多文章,进一步理解 Grail 以及它可以实现的各种功能。
  • Grails:请访问 Grails 的网站。
  • Grails Framework Reference Documentation:介绍如何开始使用 Grails 以及如何使用 Grails 框架构建 Web 应用程序。
  • GeoNames:GeoNames 地理编码服务基于具象状态传输(Representational State Transfer,REST)架构。
  • Google Maps API:在 Web 页面中嵌入 Google Map。
  • Yahoo! Maps API:在 Web 和桌面应用程序中嵌入 Yahoo! Map。
  • Geocoding:Wikipedia 关于地理编码的词条。
  • GIS for Web Developers(Scott Davis,Pragmatic Programmers,2007):这本由 Scott Davis 撰写的书籍用简单的语言介绍了地理信息系统(Geographic Information Systems,GIS)并演示了它的使用方法。
  • The Google Maps API(Scott Davis,Pragmatic Programmers,2006):学习如何绘制地图、添加注解和路径,以及对数据进行地理编码。
  • OpenLayers API:OpenLayers 是一个用于在大部分 Web 浏览器中显示地图数据的纯 JavaScript 库。
  • Groovy Recipes(Scott Davis,Pragmatic Programmers,2007):通过 Scott Davis 的最新著作更多地了解 Groovy 和 Grails。
  • 实战 Groovy:这份 developerWorks 系列文章专门介绍 Groovy 的实际应用,教授什么时候以及如何成功地应用它们。
  • Groovy:在 Groovy 项目的网站上学习关于它的更多内容。
  • AboutGroovy.com:关注 Groovy 的最新新闻和文章链接。
  • 技术书店:浏览关于这些主题和其他技术主题的图书。
  • developerWorks Java 技术专区:找到数百篇关于 Java 编程各个方面的文章。

获得产品和技术

讨论

条评论

developerWorks: 登录

标有星(*)号的字段是必填字段。


需要一个 IBM ID?
忘记 IBM ID?


忘记密码?
更改您的密码

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件

 


在您首次登录 developerWorks 时,会为您创建一份个人概要。您的个人概要中的信息(您的姓名、国家/地区,以及公司名称)是公开显示的,而且会随着您发布的任何内容一起显示,除非您选择隐藏您的公司名称。您可以随时更新您的 IBM 帐户。

所有提交的信息确保安全。

选择您的昵称



当您初次登录到 developerWorks 时,将会为您创建一份概要信息,您需要指定一个昵称。您的昵称将和您在 developerWorks 发布的内容显示在一起。

昵称长度在 3 至 31 个字符之间。 您的昵称在 developerWorks 社区中必须是唯一的,并且出于隐私保护的原因,不能是您的电子邮件地址。

标有星(*)号的字段是必填字段。

(昵称长度在 3 至 31 个字符之间)

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件.

 


所有提交的信息确保安全。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Java technology
ArticleID=311368
ArticleTitle=精通 Grails: Grails 服务和 Google 地图
publish-date=06122008