图 1. 丹佛机场(由 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 结果1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
| <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 参数 —short、medium、long 或 full— 指定了 XML 响应的详细程度。
现在已经准备好地理编码器,下一步就是将它与 Grails 应用程序集成在一起。为此,您需要一个服务。
Grails 服务到目前为止,通过学习 系列文章,您应该已经明白域类、控制器和 Groovy 服务器页面(Groovy Server Pages,GSP 是如何协调工作的。它们简化了在单一数据类型上执行基本的创建/检索/更新/删除(Create/Retrieve/Update/Delete,CRUD)操作。这个地理编码服务似乎略微超出了简单 Grails Object Relational Mapping(GORM)转换(从关系数据库记录到普通的旧 Groovy 对象(plain old Groovy objects,POGO))的范围。同样,这个服务很可能由多种方法使用。稍后您将看到,对 IATA 代码进行地理编码需要用到 save 和 update。Grails 为您提供了保存常用方法的位置,并且超越了任何单个的域类:即服务。
要创建 Grails 服务,请在命令行输入 grails create-service Geocoder。在文本编辑器中查看 grails-app/services/GeocoderService.groovy,如清单 2 所示:
清单 2. 一个无存根(stubbed-out)Grails 服务1
2
3
4
5
6
| class GeocoderService {
boolean transactional = true
def serviceMethod() {
}
}
|
如果使用同一个方法进行多个数据库查询,那么将涉及到 transactional 字段。它将所有内容都包装在一个单个数据库事务中,如果任何一个查询失败,该数据库事务将回滚到原来的状态。因为在本示例中您远程地调用 Web 服务,所以可以安全地将它设置为 false。
名称 serviceMethod 是一个占位符(placeholder),可以将其改为更具描述性的内容(服务可以包含任意多种方法)。在清单 3 中, 我把名称改为 geocodeAirport:
清单 3. geocodeAirport() 地理编码器服务方法1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
| 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. 在控制器中向服务提供一个 URL1
2
3
4
5
6
7
8
9
10
11
12
13
| 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. 地理编码器请求的结果1
2
3
4
5
| {"name":"Denver International Airport",
"lat":"39.8583188",
"lng":"-104.6674674",
"state":"CO",
"country":"US"}
|
AirportController 中的 geocode 闭包只是用于对服务进行检查。因此,可以把它删除,或者保留下来供以后的 Ajax 调用使用。下一步是重新构造 Airport 基础设施,以利用这个新的地理编码服务。 |