package com.bbn.openmap.maptileservlet; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.net.MalformedURLException; import java.net.URL; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Properties; import java.util.logging.Level; import java.util.logging.Logger; import javax.servlet.ServletConfig; import javax.servlet.ServletContext; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import com.bbn.openmap.util.ComponentFactory; import com.bbn.openmap.util.PropUtils; import com.bbn.openmap.util.http.HttpConnection; import com.bbn.openmap.util.wanderer.Wanderer; import com.bbn.openmap.util.wanderer.WandererCallback; /** * MapTileServlet is a servlet class that fields requests for map tiles. It can * handle multiple MapTileSets, each one defined by a properties file. The * web.xml file for this servlet lets you specify the directory where these * properties files are, under the TileSetDefinitions attribute. The properties * files in that directory are automatically read and used to create * MapTileSets. The default deployed name and location of this directory is the * WEB-INF/classes/tileSetDefinitions directory, but any location can be * specified. * * Each maptileset properties file should specify a name of the tile set, which * is used in the path to reach those tiles. The MapTileSet object is used by * the MapTileServlet to handle the specific configuration of the tile set, and * the MapTileSet object classname to use can be specified in the maptileset * properties under the 'class' property. The StandardMapTileSet is used by * default, it assumes the tile set is stored in a z/x/y file structure. The * TileMillMapTileSet knows how to use mbtiles files created using TileMill. The * RelayMapTileSet uses a local z/x/y directory structure as a cache for tiles * to disperse, but goes to another server location to fetch new tiles it * doesn't have. Each MapTileSet has configuration information in its javadoc. * See the web.xml file for more information about configuring this * MapTileServlet. * * @author dietrick */ public class MapTileServlet extends HttpServlet { public final static String TILE_SET_DESCRIPTION_ATTRIBUTE = "TileSetDefinitions"; protected Map<String, MapTileSet> mapTileSets; /** * A do-nothing constructor - init does all the work. */ public MapTileServlet() { super(); mapTileSets = Collections.synchronizedMap(new HashMap<String, MapTileSet>()); } /** * Called when the servlet is loaded. */ public void init(ServletConfig config) throws ServletException { super.init(config); ServletContext context = config.getServletContext(); String descriptions = context.getInitParameter(TILE_SET_DESCRIPTION_ATTRIBUTE); Logger logger = getLogger(); logger.info("Looking for Tile Set Descriptions at: " + descriptions); if (descriptions != null) { // Changing descriptions to a folder containing properties files // defining tile sets. try { URL descriptionFolder = PropUtils.getResourceOrFileOrURL(descriptions); PropertiesWanderer wanderer = new PropertiesWanderer(new File(descriptionFolder.getFile())); } catch (MalformedURLException e) { logger.warning("unable to open for Tile Set properties file given " + descriptions); } catch (NullPointerException npe) { logger.warning("Can't find directory holding Tile Set properties files: " + descriptions); } } } /** * Given a URL to a properties file describing a MapTileSet, create it and * add it to the list. * * @param tileSetProperties * @throws IOException * @throws MalformedURLException */ protected void parseAndAddMapTileSet(URL tileSetProperties) throws IOException, MalformedURLException { Properties descProps = new Properties(); Logger logger = getLogger(); logger.info("going to read props"); InputStream descURLStream = tileSetProperties.openStream(); descProps.load(descURLStream); logger.info("loaded " + tileSetProperties.toString() + " " + descProps.toString()); MapTileSet mts = createMapTileSetFromProperties(descProps); if (mts != null && mts.allGood()) { String mtsName = mts.getName(); mapTileSets.put(mts.getName(), mts); logger.info("Adding " + mtsName + " dataset"); } descURLStream.close(); } protected MapTileSet createMapTileSetFromProperties(Properties props) { String className = props.getProperty(MapTileSet.CLASS_ATTRIBUTE); Logger logger = getLogger(); if (className == null) { MapTileSet mts = new StandardMapTileSet(); mts.setProperties(props); return mts; } else { if (logger.isLoggable(Level.FINE)) { logger.fine("Creating special map tile set: " + className); } try { Object obj = ComponentFactory.create(className, null, props); if (obj instanceof MapTileSet) { return (MapTileSet) obj; } else { logger.fine("Had trouble creating " + (obj == null ? className : obj.getClass().getName()) + ", not a MapTileSet"); } } catch (Exception e) { getLogger().severe("Problem creating " + className + ", " + e.getMessage()); } } return null; } /** * Handles */ public void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { OutputStream out = resp.getOutputStream(); String pathInfo = req.getPathInfo(); Logger logger = getLogger(); if (logger.isLoggable(Level.FINE)) { logger.fine("received: " + pathInfo); } // Empty path request, let's return summary catalog, might be of some // help. if (pathInfo.length() <= 1) { String tilePathHeader = req.getServerName() + ":" + req.getServerPort() + req.getContextPath(); StringBuilder builder = new StringBuilder("<html><body>Map Tile Sets:<p>"); for (MapTileSet mts : mapTileSets.values()) { String description = mts.getDescription(); builder.append("Tile set name: <a href=\"http://").append(tilePathHeader).append("/").append(mts.getName()).append("/map\">"); builder.append(mts.getName()).append("</a>, description: "); builder.append(description == null ? "n/a" : description).append("<br>"); } builder.append("</body></html>"); resp.setContentType(HttpConnection.CONTENT_HTML); OutputStreamWriter osw = new OutputStreamWriter(out); out.write(builder.toString().getBytes()); osw.flush(); return; } MapTileSet mts = getMapTileSetForRequest(pathInfo); if (mts != null) { if (pathInfo.endsWith("map")) { String tilePathHeader = req.getServerName() + ":" + req.getServerPort() + req.getContextPath(); String map = getMap(tilePathHeader, mts); resp.setContentType(HttpConnection.CONTENT_HTML); OutputStreamWriter osw = new OutputStreamWriter(out); out.write(map.getBytes()); osw.flush(); return; } try { resp.setContentType(HttpConnection.CONTENT_PNG); byte[] imageData = mts.getImageData(pathInfo); OutputStreamWriter osw = new OutputStreamWriter(out); out.write(imageData, 0, imageData.length); osw.flush(); } catch (Exception e) { if (logger.isLoggable(Level.FINE)) { getLogger().fine("Tile not found: " + pathInfo); } HttpConnection.writeHttpResponse(out, HttpConnection.CONTENT_PLAIN, "Problem loading " + pathInfo + " from map tile set:" + mts.getName()); } } else { HttpConnection.writeHttpResponse(out, HttpConnection.CONTENT_PLAIN, "Map Tile Set not found for request: " + pathInfo); } } protected MapTileSet getMapTileSetForRequest(String pathInfo) { if (pathInfo.startsWith("/")) { pathInfo = pathInfo.substring(1); } String key = pathInfo; // That first part of the path is the MapTileSet name. int slash = pathInfo.indexOf('/'); if (slash > 0) { key = pathInfo.substring(0, slash); } return mapTileSets.get(key); } /** * Given a starting directory, look for properties files that describe * MapTileSets. * * @author dietrick */ private class PropertiesWanderer extends Wanderer implements WandererCallback { public PropertiesWanderer(File startingDirectory) { setCallback(this); handleEntry(startingDirectory); } /* * (non-Javadoc) * * @see * com.bbn.openmap.util.wanderer.WandererCallback#handleDirectory(java * .io.File) */ public boolean handleDirectory(File directory) { // Do nothing to directories return true; } /* * (non-Javadoc) * * @see * com.bbn.openmap.util.wanderer.WandererCallback#handleFile(java.io * .File) */ public boolean handleFile(File file) { getLogger().fine("Checking " + file); try { String name = file.getName(); if (name.endsWith("properties")) { parseAndAddMapTileSet(file.toURI().toURL()); } } catch (MalformedURLException murle) { getLogger().warning("Unable to read/load " + file + ", murle"); } catch (IOException e) { getLogger().warning("Unable to read/load " + file + ", ioe"); } return true; } } /** * Holder for this class's Logger. This allows for lazy initialization of * the logger. */ private static final class LoggerHolder { /** * The logger for this class */ private static final Logger LOGGER = Logger.getLogger(MapTileServlet.class.getName()); /** * Prevent instantiation */ private LoggerHolder() { throw new AssertionError("This should never be instantiated"); } } /** * Get the logger for this class. * * @return logger for this class */ private static Logger getLogger() { return LoggerHolder.LOGGER; } /** * Creates a HTML string that will display a Leaflet map with the map tiles * for the MapTileSet. * * @param tileReqHeader the server:port/context string of this servlet. * @param mts the MapTileSet to display. * @return html text. */ protected String getMap(String tileReqHeader, MapTileSet mts) { String name = mts.getName(); List<String> nameList = new ArrayList<String>(); nameList.add(name); for (MapTileSet set : mapTileSets.values()) { if (!name.equals(set.getName())) { nameList.add(set.getName()); } } StringBuilder ret = new StringBuilder(); ret.append("<html><head><link rel=\"stylesheet\" href=\"http://cdn.leafletjs.com/leaflet-0.7.2/leaflet.css\" />"); ret.append("<script src=\"http://cdn.leafletjs.com/leaflet-0.7.2/leaflet.js\"></script></head><body>"); ret.append("<div id=\"map\" style=\"position:absolute; top:20px; left:20px; right:20px; bottom:20px;overflow:hidden;min-height;200px\"></div>"); ret.append("<script>"); StringBuilder layerControlList = null; for (String mtsName : nameList) { ret.append("var ").append(mtsName).append("Url=\'http://").append(tileReqHeader).append("/").append(mtsName).append("/{z}/{x}/{y}.png\';"); ret.append("var ").append(mtsName).append("=L.tileLayer(").append(mtsName).append("Url);"); if (layerControlList == null) { layerControlList = new StringBuilder("var baseMaps={"); layerControlList.append("\"").append(mtsName).append("\":").append(mtsName); } else { layerControlList.append(",\"").append(mtsName).append("\":").append(mtsName); } } if (layerControlList != null) { layerControlList.append("};"); ret.append(layerControlList.toString()); } ret.append("var map = new L.Map('map', {center:new L.LatLng(0, 0), zoom:1, maxZoom:20, minZoom:0, layers:[").append(name).append("]});"); ret.append("L.control.scale().addTo(map);"); ret.append("L.control.layers(baseMaps).addTo(map);"); ret.append("</script></body></html>"); return ret.toString(); } }