Sunday, July 26, 2009

Creating Local Tile Servlet

I meant to write my updates when I got back from my trip a week ago, but there are a lot of things at home to take care of after a three week vacation. As promised I did some work on the WorldWind while I was away.

Creating Local Tile Server
With an unsteady internet connection I needed to be able to continue to test WorldWind changes that related to the download code. If I could set up a local server, and point my WorldWind program to check that for tiles I could still check the download code if it works. A side benefit is that in theory if my local tile server is something I coded myself, I'd be able to insert dummy error response codes like 404 or 500 errors in order to test the behavior for those errors in the new download code.

I tried using existing scripts and code that was posted on the WorldWindCentral wiki. But I either don't have the right application server software to run them (PHP, Perl,Python) or the know-how to set it up and use it (IIS 7/aspx.cs). There is also a WorldWind server, written in java posted on the World Wind forums (NASA World Wind WMS Server Config), I tried that too, this would allow me to set up a local server but because it was a self-contained application, but wouldn't let me achieve my second goal of inserting my own custom errors for testing. Even so, it has dependencies on all or some of WorldWind Java SDK. Now on my own, but understanding better what needs to get done, I thought "Why not just write this in a language I know, Java and set up Tomcat with a simple web app?"

Taking the .aspx.cs example, I ported that to a Java Servlet class.

package org.wwc.ammianus.tomcat.tileserver;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.text.DecimalFormat;
import java.util.HashMap;

import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
*

This servlet handles requests from the World Wind.NET application and returns
* tile images or error codes as appropriate. This was a port to Java of a ASP.NET file posted
* at http://worldwindcentral.com/wiki/Data_serving_scripts


*
* @author ammianus
* @version 0.1
*
*
*/
public class TileRequestHandler extends HttpServlet {

/**
* Generated by Eclipse
*/
private static final long serialVersionUID = -6596187549520730077L;
/**
* settings Map which theoretically could be extended for any layer used in WW
* for each layer, use the <DataSetName></DataSetName> tag value from @Images.xml file
* create three map entries <DataSetName>, <DataSetName>_EXT, <DataSetName>_MIME
*/
private HashMap _settings;

private int _requestNumber;

/**
* @see HttpServlet#HttpServlet()
*/
public TileRequestHandler() {
super();
//use these setting which theoretically could be extended for any layer used in WW
//for each layer, use the tag value from @Images.xml file
//create three map entries , _EXT, _MIME
_settings = new HashMap();
_settings.put("geocover2000", "C:\\Users\\Public\\WorldWindCache\\Cache\\Earth\\Images\\NASA Landsat Imagery\\Geocover 2000");
_settings.put("geocover2000_EXT", ".jpg");
_settings.put("geocover2000_MIME", "image/jpeg");

_requestNumber = 0;
}

/**
* @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse response)
*/
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//increment counter
_requestNumber++;

//Un-comment this to test whether WW handles 503 return code with Retry-After header
//
//first request we will send an error with a retry-after header
//
/*if(_requestNumber == 1){
response.setStatus(503);
//this appears to be case insensitive for WorldWind, I just picked a random retry-after period
response.setHeader("retry-after", "38");
System.out.println(_requestNumber+":Response: 503, with retry-after: 38");
return;
}*/

try{


//column
int X = Integer.parseInt(request.getParameter("X").toString());
//row
int Y = Integer.parseInt(request.getParameter("Y").toString());
//level
int L = Integer.parseInt(request.getParameter("L").toString());
//DataSet name
String T = request.getParameter("T").toString(); //T = Dataset in WorldWind parlance


//Un-comment this to test whether WW handles 500 return code
//
//if(T.equals("geocover2000")){
// throw new Exception("Force return of 500 - Server Error");
//}

//Arguments are in this format URL sent from world wind
//e.g. http://localhost:8080/TomcatTileServer/TileRequestHandler?T=geocover2000&L=0&X=47&Y=21
String filePath = _settings.get(T);//WebConfigurationManager.AppSettings[T];

String fileExt = _settings.get(T + "_EXT");
if(L < 3){
DecimalFormat df = new DecimalFormat("0000");
filePath += "\\"+L + "\\" + df.format(Y) + "\\" + df.format(Y) + "_" + df.format(X) + fileExt;
}else{
filePath += "\\level"+(L+1)+fileExt;
}
//request.get
System.out.println(_requestNumber+":Requested File: "+filePath);
File file = new File(filePath);

//if file (image for requested tile) is not found on server, return 404
if (!file.exists())
{
response.sendError(HttpServletResponse.SC_NOT_FOUND, "Not Found");
}
else
{
//set content length to file size
response.setContentLength((int) file.length());
//get mime type for this layer
response.setContentType(_settings.get(T + "_MIME"));
//get the output stream for the response, this will have the file written to it
ServletOutputStream stream = response.getOutputStream();

//read the file contents and write them to the outputstream
FileInputStream fis = new FileInputStream(file);
byte[] bytes = new byte[1024];
int counter = 0;
while(counter < file.length()){
fis.read(bytes);
counter += 1024;
stream.write(bytes);
}
fis.close();
stream.flush();
stream.close();

//done with response
}
}catch(Exception e){
//uncaught Exception return 500 error
response.sendError(500,e.getMessage());
System.out.println(_requestNumber+":Response: send 500 error");
}

}

/**
* @see HttpServlet#doPost(HttpServletRequest request, HttpServletResponse response)
*/

protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// TODO Auto-generated method stub
}

}

As you can see, not much there. In my WEB-INF/web.xml, I included the new servlet
<?xml version="1.0" encoding="UTF-8"?>
<web-app xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" schemalocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" id="WebApp_ID" version="2.5">
<display-name>TomcatTileServer</display-name>
<welcome-file-list>
<welcome-file>index.html</welcome-file>
<welcome-file>index.htm</welcome-file>
<welcome-file>index.jsp</welcome-file>
<welcome-file>default.html</welcome-file>
<welcome-file>default.htm</welcome-file>
<welcome-file>default.jsp</welcome-file>
</welcome-file-list>
<servlet>
<description></description>
<display-name>TileRequestHandler</display-name>
<servlet-name>TileRequestHandler</servlet-name>
<servlet-class>org.wwc.ammianus.tomcat.tileserver.TileRequestHandler</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>TileRequestHandler</servlet-name>
<url-pattern>/TileRequestHandler</url-pattern>
</servlet-mapping>
</web-app>
Now I was typically running this from my Eclipse IDE (Eclipse J2EE 3.4.1, Apache Tomcat 6), but I also could export the project in Eclipse as a war and just drop it into Tomcat's webapps folder.

To access this server there is the format that the URL would take:
http://localhost:8080/TomcatTileServer/TileRequestHandler?T=geocover2000&L=0&X=48&Y=21

To break this down:
  • T = the name of the layer, or DataSet name ( tag value from @Images.xml)
  • L = level, (0 - x) where 0 is the highest level
  • X = column
  • Y = row
From this the TileServer, based on its implementation and organization of the images, will try to look up locally a file that matches that. In my case it was something like C:\Users\Public\WorldWindCache\Cache\Earth\Images\NASA Landsat Imagery\Geocover 2000\0\0021\0021_0049.jpg".

Integrating Local Server with WorldWind
Thanks to WorldWind forums for giving me this information. Now to make WorldWind point to my local tile servlet instead of the default NASA WMS server, you need to edit the @Images.xml file in your WorldWind's Config\Earth directory. There is probably an @Images.tmp, copy this and rename it to @Images.xml.

Edit the file, and for the DataSetName your local server will handle, update the tag such as:
<ServerUrl>http://localhost:8080/TomcatTileServer/TileRequestHandler</ServerUrl>
And then restart World Wind you should be good to go