top
Loading...
基于Servlet的GoogleEarth應用
從某種程度來說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>"+

"<name>"+pBean.getName()+"</name>"+
"<description><![CDATA["+pBean.getDescription()+"]]></description>"+
"<address>"+pBean.getAddress()+"</address>"+
"<styleUrl>"+pBean.getStyleURL()+"</styleUrl>"+
"<Point>"+

"<coordinates>"+pBean.getCoordinates().getCoordinate()+"</coordinates>"+
"</Point>"+
"</Placemark>";
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"?>"
+ "<kml xmlns="http://earth.google.com/kml/2.0">"
+ "<Placemark>"
+ "<name><![CDATA[<font color="red">"+name+"</font>]]></name>"

+"<description><![CDATA["+message+"<br><i>"+lastDate+"</i>]]></description>"
+ "<Point>"
+ "<coordinates>"+userlon+","+userlat+",0</coordinates>"
+ "</Point>"
+ "</Placemark>"
+ "</kml>";
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>Location: "+lastLocation.getCoords()+
"IP address: "+lastLocation.getIpAddress()+
"Updated: "+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客戶端,至此你已擁有了一個創建自己互動旅程的概念性工具箱。
作者:http://www.zhujiangroad.com
來源:http://www.zhujiangroad.com
北斗有巢氏 有巢氏北斗