从这一点来说google earth客户端是我们时代的技术标志。google earth并非是第一个地球浏览客户端,而且与它的先驱、不为人知的keyhole非常相似。但是凭着google的大名以及基础版对最终用户免费,它完成了市场渗透并得到公认――这是另一个值得大书特书的有趣话题。
本文只有一个基本使命:即向你展示在servlet和google earth客户端之间发送和接收信息是多么的容易。有了这种程度的交互,你就能用基本的java编程技能创建设想的服务。
使用许可及竞争者
截至本文发稿时google earth还处于beta阶段(版本号3.0.0616),许可证是商业的(见客户端的帮助部分)。如果你想寻求等价的开源范例,我建议你去关注优秀的nasa world wind(nasa世界风)项目
基础知识
google earth客户端以第二版的锁位标记语言(kml)解析xml数据,它有一个专用的命名空间。庞大的kml配置信息可能会影响到gui显示,开发这种需要平衡利弊的应用的难点在于需要了解更多的kml细节而不是编程技巧。kml实体的简要列表包括:
*placements(位置),标明在地球上的坐标
*folders(夹子),帮助组织其它的特征信息
*documents(文档),存放可能包含风格元素的folder的容器
*image overlays(图片叠加),用来添加图片
*network links(网络链接),描述在何处以及如何与服务器或者servlet(本文采用的方式)连接
本文为了简化的目的,主要探讨了folder、placement和network-link元素的使用;此外还用folder定义了一段旅程(tour),它里面包含了一系列的placement。
在windows上安装了google earth后,文件扩展名kml和mime(multipurpose internet mail extensions,多用途网络邮件扩展)类型“application/keyhole”即被注册。这意味着只要点击kml文件或通过tcp/ip接收“application/keyhole”mime类型的文件就会激活google earth客户端。
如果返回的kml文本为:
<folder><name>hello world [127.0.0.1] </name></folder>
则程序将显示如下内容:
图1 hello world folder的gui显示
要想激活earth客户端,只需浏览适当的url地址--就好比从资源地址(http://localhost:8080/tour/hello)下载helloservlet源程序。这样就能激活doget()方法,然后重定向到dopost()方法,在所有的web浏览器里都会看到以下结果:
protected void doget(httpservletrequest request, httpservletresponse response)
throws servletexception, ioexception
{
dopost(request, response);
}
protected void dopost(httpservletrequest request, httpservletresponse response)
throws servletexception, ioexception
{
response.setcontenttype("application/keyhole");
printwriter out = response.getwriter();
string message ="<folder><name>hello world ["
+ request.getremoteaddr()+ "]</name></folder>";
out.println(message);
}
不要小看这段简单的代码,里面的方法暗藏着玄机。服务器可以作为各种数据类型和google earth之间的中介。不妨设想像这样一个场景:在旅程数据中包含有不同的xml方言,在返回响应前由服务器完成扩展风格语言(extensible stylesheet language)的转换。再进一步,服务器可以选择返回哪一种响应,以允许个性化处理。kml文档实体允许风格定义,可根据ip地址范围改变风格,使得不同的用户看到的风格可能会不一样。
作为实践,我们将从使用google earth和输出kml文件开始。在google earth的顶部是add菜单,可以在这里添加placement、folder和image overlay,然后用file菜单保存生成的kml文件。我强烈推荐编辑导出的xml文件以了解改动对google earth的影响。好了,让我们开始与这位世界之王共舞!
了解城市定位
本节给出一个面向教学的应用:一个用来教授学生城市名称与地理位置间关系的程序。我们将创建一个以类似于抽签的方式将城市位置随机发送给客户端的servlet。城市的位置(placement)用kml表示。placement实体里封装了html链接,将用户引导到相关的有趣站点。这样我们就可以使用户在web浏览器和google earth间进行交互。
学生可以通过在鼠标置于链接之上时出现的菜单中选择refresh来选择下一个placement,如图2所示。
图2 刷新网络链接生成一个新位置(在这里是伦敦)时的gui显示
我们这个应用的后台处理用到了network-link(网络链接)实体,network-link从http://location加载数据文件。将此文件存于桌面并双击,google earth开始运行,并从服务器端加载下面的kml代码段。
city.kml
<?xml version="1.0" encoding="utf-8"?>
<kml xmlns="http://earth.google.com/kml/2.0">
<networklink>
<description>refresh me</description>
<name>random city</name>
<visibility>1</visibility>
<open>1</open>
<refreshvisibility>1</refreshvisibility>
<flytoview>1</flytoview>
<url>
<href>http://location </href>
</url>
</networklink>
</kml>
该配置中的实体含义为:
*visibility(可见性),定义了网络链接是否可见
*open(展开),说明是否展开标签
*refreshvisibility(刷新可见性),定义是否取代用户对刷新位置可见性的设定
*flytoview(巡视),如果设为1,用户可以在view窗口“飞越”位置上空
许多实体通常都可以跨根元素使用(如description)。注意标签名是大小写敏感的,所以编码时要小心以免出现难以排查的错误。在我看来,各标签值与它们对gui的交互作用关系并不总是符合逻辑的,因此你可能对任何新的kml代码段的运用都需要花些时间。
注意
在默认情况下,firefox、opera和ie浏览器对于从web上接收的扩展名为kml的文件反应是不同的。激活网络链接文件最通用的方法是避免服务器将kml文件初始化,并允许用户将文件下载到桌面,这样就能通过双击来启动它们。另一种更好的方法是将kml文件嵌入到jsp(javaserver pages)页面里并允许jsp页面返回“application/keyhole”mime类型的kml代码段。假使对内容类型做修改并去掉xml模式,city.jsp就成了city.kml文件。
该代码的开头为:
<%response.setcontenttype("application/keyhole");%>
<networklink> 回到前面的代码,servlet返回了一个在description元素中带有html代码的placement。为遵守xml规范,我们将html代码段放入<!cdata[]]>分割标签中,以避免使xml解析器混淆:
<placemark>
<name>london</name>
<description>
<![cdata[<a href="http://www.visitlondon.com/choose_site/?originalurl=/">london</a>]]>
</description>
<address>london, uk</address>
<styleurl>root://stylemaps#default+nicon=0x304+hicon=0x314</styleurl>
<point>
<coordinates>-0.1261969953775406,51.50019836425783,50</coordinates>
</point>
</placemark>
在placement里出现了三个新实体:
*address(地址),包含地址的逻辑标签
*styleurl,定义在此处要显示的图片
*point/coordinates(点/坐标),位置的柱面坐标
servlet通过以下代码生成一个随机的placement响应:
manager.kmlrenderofrandomplacement();
我们的整个应用都是最基础的,servlet没有保持跟踪状态。management类根据数据的组织重画各个窗口。manager.java的init方法将数据加载到property bean数组中。显然,真实的应用需要与数据库通信,象ibatis或hibernate这样的持久层管理框架将会很有用。placement bean用来为返回的placement准备数据,该bean有一个代表其自身的属性点。当开发者对kml编程的细节以及如何到达google earth gui中的某个点有了更多的了解之后,就可以对此模型进行扩充。
下面的quizservlet是对manager.java的轻量封装,该servlet对每个post或get请求都返回一个有效的kml响应。
quizservlet.java
package test.google.earth.servlet;
import java.io.ioexception;
import java.io.printwriter;
import javax.servlet.servletexception;
import javax.servlet.http.*;
import javax.servlet.servletconfig;
import test.google.earth.manager.manager;
public class quizservlet extends httpservlet
{
private manager manager;
public void init(servletconfig config) throws servletexception {
super.init(config);
this.manager= new manager();
manager.init();
}
protected void doget(httpservletrequest request, httpservletresponse response)
throws servletexception, ioexception
{
dopost(request, response);
}
protected void dopost(httpservletrequest request, httpservletresponse response)
throws servletexception, ioexception
{
response.setcontenttype("application/keyhole");
printwriter out = response.getwriter();
out.println(manager.kmlrenderofrandomplacement());
}
}
manager.java
package test.google.earth.manager;
import java.util.random;
import test.google.earth.bean.placementbean;
import test.google.earth.bean.pointbean;
public class manager {
private placementbean[] cityarray;
private string styleurl;
private string open;
private random generator;
private int idx;
public manager(){}
public void init(){
this.styleurl="root://stylemaps#default+nicon=0x304+hicon=0x314";
this.open="1";
this.generator = new random();
string[] coords = {"-0.1261969953775406,51.50019836425783,50",
"12.5,41.889999,50","4.889999,52.369998,0"};
string[] name = {"london","italy","amsterdam"};
string[] address={"london, uk","rome, italy","amsterdam, netherlands"};
string[] description={
"<a href=/"http://www.visitlondon.com/choose_site/?originalurl=//">london</a>",
"<a href=/"http://www.roma2000.it//">rome</a>",
"<a href=/"http://www.uva.nl//">university of amsterdam</a>"};
this.idx=coords.length;
cityarray= new placementbean[coords.length];
//init the array of placements
for (int i =0; i<coords.length;i++){
placementbean placementbean = new placementbean();
placementbean.setaddress(address[i]);
placementbean.setdescription(description[i]);
placementbean.setname(name[i]);
placementbean.setopen(open);
placementbean.setstyleurl(styleurl);
pointbean pointbean = new pointbean();
pointbean.setcoordinate(coords[i]);
placementbean.setcoordinates(pointbean);
this.cityarray[i]=placementbean;
}
}
public synchronized placementbean nextrandomplacement(){
return cityarray[ generator.nextint( this.idx )];
}
public synchronized string kmlrenderofrandomplacement(){
return renderkmlplacement(nextrandomplacement());
}
private string renderkmlplacement(placementbean pbean){
string klmstring="<placemark>/n"+
"/t<name>"+pbean.getname()+"</name>/n"+
"/t<description><![cdata["+pbean.getdescription()+"]]></description>"+
"/t<address>"+pbean.getaddress()+"</address>/n"+
"/t<styleurl>"+pbean.getstyleurl()+"</styleurl>/n"+
"/t<point>/n"+
"/t/t<coordinates>"+pbean.getcoordinates().getcoordinate()+"</coordinates>/n"+
"/t</point>/n"+
"</placemark>/n";
return klmstring;
}
}
为了直接将远程服务器上的图片加到placement上,styleurl标签需要一个指向web的链接(如http:/imageserver/image.gif),这就使代码能在view窗口的placement处填充一个图片(在本应用中是一个国旗)。
对此方法做进一步研究,就可以设计出一个场景:用户在与google earth客户端交互的同时还能填写web表单。图3给出了这一基本构思的示意图。
做进一步研究,就可以设计出一个场景:用户在与google earth客户端交互的同时还能填写web表单。图3给出了这一基本构思的示意图。
图3 基于表单的旅行服务的潜在基本构思
在两个servlet服务器的前端是apache web服务器。第一个是表单服务器,根据发送的参数返回web表单;第二个是旅程服务器,生成placement列表封装在folder中成为一个旅程。旅程服务器处理图片的url,图片本身以静态方式存储于文件系统中以改善性能。
互动流程如下:
1. 用户登录到表单服务器。
2. 服务器通过目录服务(可以是轻量目录访问服务)验证用户身份,并将用户的ip地址存入一个会话表中。
3. 表单服务器重定向到旅程服务器。
4. 旅程服务器检查正在会话中的已注册用户的ip地址。
5. 根据存储在数据库中的用户历史信息返回一个旅程。
6. google earth聚焦到一个位置(placement)并请求一张图片。
7. 用户点击placement中的一个链接,触发表单服务器生成并返回一个表单。
8. 学生填写表单,然后继续旅行。
9. 如此几番后,学生退出会话,引发应用向相关教师发送一个将学生的回答转化为专用格式报告的email,至此服务器完成了作业的交付。
由此可见,基于上述构想创建一个具备功能性和教育性的应用是可能的。然而,我们还不能以定期的方式直接从客户端向servlet反馈信息,除非学生对位置进行刷新。在下一部分我们将深入探讨这一问题。
双向交流
在上面的代码示例中,网络链接需要等待我们的刷新操作。幸运的是,我们可以让google earth以get方法定期地发送view窗口中用户的位置,如下所示:
<?xml version="1.0" encoding="utf-8"?>
<kml xmlns="http://earth.google.com/kml/2.0">
<folder>
<description>examples of bi directional flow of information</description>
<name>network links</name>
<visibility>1</visibility>
<open>1</open>
<networklink>
<description>lets send coordinates once in a while</description>
<name>message pushing</name>
<visibility>1</visibility>
<open>1</open>
<refreshvisibility>1</refreshvisibility>
<flytoview>0</flytoview>
<url>
<href>http://localhost:8081/tour/message</href>
<refreshinverval>2</refreshinverval>
<viewrefreshmode>onstop</viewrefreshmode>
<viewrefreshtime>1</viewrefreshtime>
</url>
</networklink>
</folder>
</kml>
实际的动作由url实体完成。viewrefreshtime标签定义了经过多少秒服务器接收下一套earth坐标,viewrefreshmode标签设置为onstop就意味着当停止在view窗口里移动时更新earth坐标。图4是上述配置最终效果的一个截图。
图4 网络链接和关联html的gui显示
好了,我们可以把那些讨厌的坐标发给服务器了。我们可以用它来做什么呢?让我们从创建一个消息服务开始。图5给出了两个流程。
图5 google earth、servlet和浏览器之间的信息流
首先,通过浏览器发送消息并接收坐标:
1. 浏览器以post方法发送参数名和消息
2. serlet以类似于以下形式的文本消息返回从google earth客户端收到的最后坐标:
location: -0.134539,51.497,-0.117855,51.5034
ip address: 127.0.0.1
updated: fri oct 21 11:42:45 cest 2005
其次,在servlet与google earth客户端之间传递坐标并接收位置(placement):
1. 每经过δt时间,google earth通过get方法发送用户在view窗口中的坐标。
2. servlet把消息放在placement中返回,placement通过坐标来粗略计算在何处放置返回的消息。请注意我已从kml教程中将对中算法拷贝过来。
返回生成的位置(placement)类似于下面的kml:
<?xml version="1.0" encoding="utf-8"?>
<kml xmlns="http://earth.google.com/kml/2.0">
<placemark>
<name><![cdata[<font color="red">alan berg</font>]]></name>/
<description><![cdata[blah blah <i> fri oct 21 11:42:45 cest 2005</i>]]>
</description>
<point>
<coordinates>4.889999,52.369998,0</coordinates>
</point>
</placemark>
</kml>
以下就是构成这一协奏乐章的servlet代码:
messageservlet.java
package test.google.earth.servlet;
import java.io.ioexception;
import java.io.printwriter;
import javax.servlet.servletconfig;
import javax.servlet.servletexception;
import javax.servlet.http.*;
import test.google.earth.bean.lastlocationbean;
import test.google.earth.bean.lastmessagebean;
import java.util.date;
public class messageservlet extends httpservlet
{
private static lastmessagebean lastmessage=new lastmessagebean();
private static lastlocationbean lastlocation= new lastlocationbean();
public void init(servletconfig config) throws servletexception {
super.init(config);
lastmessage.setmessage("no message yet");
lastmessage.setname("system");
lastmessage.setupdated(new date());
lastlocation.setcoords("no contact with a client yet");
lastlocation.setipaddress("");
lastlocation.setupdated(new date());
}
protected void doget(httpservletrequest request, httpservletresponse response)
throws servletexception, ioexception
{
string coords = request.getparameter("bbox");
if (coords==null){
return;
}
string message;
string name;
date lastdate;
string ipaddress = request.getremoteaddr();
synchronized(this) {
lastlocation.setcoords(coords);
lastlocation.setipaddress(ipaddress);
lastlocation.setupdated(new date());
message=lastmessage.getmessage();
name=lastmessage.getname();
lastdate=lastmessage.getupdated();
}
response.setcontenttype("application/keyhole");
printwriter out = response.getwriter();
string[] coparts= coords.split(",");
float userlon;
float userlat;
try{
userlon = ((float.parsefloat(coparts[2]) - float.parsefloat(coparts[0]))/2)+
float.parsefloat(coparts[0]);
userlat = ((float.parsefloat(coparts[3]) - float.parsefloat(coparts[1]))/2) +
float.parsefloat(coparts[1]);
}catch(numberformatexception e){
return;
}
string klmstring = "<?xml version=/"1.0/" encoding=/"utf-8/"?>/n"
+ "<kml xmlns=/"http://earth.google.com/kml/2.0/">/n"
+ "<placemark>/n"
+ "<name><![cdata[<font color=/"red/">"+name+"</font>]]></name>/n"
+"<description><![cdata["+message+"<br><i>"+lastdate+"</i>]]></description>/n"
+ "<point>/n"
+ "<coordinates>"+userlon+","+userlat+",0</coordinates>/n"
+ "</point>/n"
+ "</placemark>/n"
+ "</kml>/n";
out.println(klmstring);
}
protected void dopost(httpservletrequest request, httpservletresponse response)
throws servletexception, ioexception
{
string name = request.getparameter("name");
if (name==null){
return;
}
string message;
printwriter out;
synchronized(this) {
lastmessage.setmessage(request.getparameter("message"));
lastmessage.setname(name);
lastmessage.setupdated(new date());
message="<pre>/nlocation: "+lastlocation.getcoords()+
"/nip address: "+lastlocation.getipaddress()+
"/nupdated: "+lastlocation.getupdated();
}
response.setcontenttype("text/html");
out = response.getwriter();
out.println(message);
}
}
来自浏览器的消息保存在静态成员lastmessagebean中,坐标保存在lastlocationbean中,且每个bean都只有一个实例。此外,在执行getting或setting操作时对所有的静态bean都进行同步。我们用单个实例来达到简化的目的,有助于限制要编写的代码数量。然而,更有实用价值的示例应具备跟踪ip地址的会话管理功能并生成相应的处理结果。
有一个不起眼的错误,在placement实体的名字标签里使用html标签会导致显示问题:整个标签在google earth客户端的“places”菜单区按html显示,但在view窗口里却按文本显示。我认为这种不一致是个bug。
在本示例中google earth客户端推送坐标,servlet返回kml代码段。既然知道能用坐标推送上下文关联信息,我们可以强制通过段中的链接来进行交互,必要的话还可以让浏览器成为宿主。本文展示了如何控制google earth客户端,至此你已拥有了一个创建自己互动旅程的概念性工具箱。
新闻热点
疑难解答