//********************************************************************** // //<copyright> // //BBN Technologies //10 Moulton Street //Cambridge, MA 02138 //(617) 873-8000 // //Copyright (C) BBNT Solutions LLC. All rights reserved. // //</copyright> //********************************************************************** // //$Source: ///cvs/darwars/ambush/aar/src/com/bbn/ambush/mission/MissionHandler.java,v //$ //$RCSfile: MissionHandler.java,v $ //$Revision: 1.10 $ //$Date: 2004/10/21 20:08:31 $ //$Author: dietrick $ // //********************************************************************** package com.bbn.openmap.dataAccess.mapTile; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.GraphicsEnvironment; import java.awt.Paint; import java.awt.geom.Point2D; import java.awt.geom.Rectangle2D; import java.awt.image.BufferedImage; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.PrintStream; import java.net.MalformedURLException; import java.net.URL; import java.util.ArrayList; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Properties; import java.util.TreeMap; import java.util.logging.Level; import com.bbn.openmap.Environment; import com.bbn.openmap.Layer; import com.bbn.openmap.image.ImageFormatter; import com.bbn.openmap.image.ImageServer; import com.bbn.openmap.image.PNG32ImageFormatter; import com.bbn.openmap.image.SunJPEGFormatter; import com.bbn.openmap.layer.imageTile.MapTileLayer; import com.bbn.openmap.layer.shape.ShapeLayer; import com.bbn.openmap.omGraphics.OMColor; import com.bbn.openmap.proj.Mercator; import com.bbn.openmap.proj.Proj; import com.bbn.openmap.proj.Projection; import com.bbn.openmap.proj.coords.LatLonPoint; import com.bbn.openmap.util.ComponentFactory; import com.bbn.openmap.util.Debug; import com.bbn.openmap.util.PropUtils; /** * The MapTileMaker is an ImageServer extension that knows how to create image * tile sets, like the kind of tiles used by Google Maps and OpenStreetMap, Tile * Map Service (TMS). It uses ZoomLayerMarker objects to define how tiles are * created for different zoom levels. You can run this class as an application. * With the -create option, it will create a sample properties file to * demonstrate what properties are needed to run it. * <p> * * The properties look much like the ImageServer properties, with a couple of * additional values: * <p> * * <pre> * ### MapTileMaker/ImageServer properties ### * antialiasing=false * # Image formatter definition * formatters=formatter1 * # Layer definitions for layers that are available for zoom levels * layers=layer1 layer2 ... * rootDir=Path to top level directory for tiles * zoomLevels=zoom1 zoom2 * * formatter1=.class=com.bbn.openmap.image.PNGImageIOFormatter * layer1.class=com.bbn.openmap.layer.shape.ShapeLayer * # ... layer1 properties follow, see layer docs for specific properties for that layer * * # Then, for each zoom level * zoom1.class=com.bbn.openmap.image.ZoomLevelInfo * #Optional, to limit tile areas created, in sets of 4, must be in lat,lon order * zoom1.bounds=lat lon lat lon * zoom1.description=Tiles for zoom level 4 * #Marker names for layers to be rendered, the property prefixes for the layers held by TileMaker * zoom1.layers=layer1 layer2 * zoom1.name=ZoomLayerInfo 4 * zoom1.zoomLevel=4 * # If defined, copies of zoomLevel tiles will be scaled through range level. * zoom1.range=0 * * # and repeat for every zoomLevel defined * </pre> * * @author dietrick */ public class MapTileMaker extends ImageServer implements EmptyTileHandler { public final static String ROOT_DIRECTORY_PROPERTY = "rootDir"; public final static String ZOOM_LEVELS_PROPERTY = "zoomLevels"; protected String rootDir; protected List<ZoomLevelMaker> zoomLevels; protected MapTileCoordinateTransform mtcTransform = new OSMMapTileCoordinateTransform(); protected int TILE_SIZE = mtcTransform.getTileSize(); /** * Empty constructor that expects to be configured later. */ public MapTileMaker() { } /** * To create the TileMaker, you hand it a set of properties that let it * create an array of layers, and also to set the properties for those * layers. The properties file for the ImageServer looks strikingly similar * to the openmap.properties file. So, all the layers get set up here... */ public MapTileMaker(Properties props) { super(props); } /** * Same as the other constructor, except that the properties can have a * prefix in front of them. The format of the prefix has to match how the * property is specified the the properties file, which may include the * period - i.e server1.imageServer.layers, the server1. is the prefix that * should get passed in. The ImageMaster does this. */ public MapTileMaker(String prefix, Properties props) { super(prefix, props, null); } /** * Create an TileMaker that should be configured with a Properties file. The * prefix given is to scope the ImageServer properties to this instance. The * Hashtable is for reusing any layers that may already be instantiated. */ public MapTileMaker(String prefix, Properties props, Map<String, Layer> instantiatedLayers) { super(prefix, props, instantiatedLayers); } /** * Create an TileMaker from an array of Layers and an ImageFormatter. It's * assumed that the layers are already configured. * * @param layers the array of layers. * @param formatter the ImageFormatter to use for the output image format. */ public MapTileMaker(Layer[] layers, ImageFormatter formatter) { super(layers, formatter); } @SuppressWarnings("unchecked") public void setProperties(String prefix, Properties props) { super.setProperties(prefix, props); prefix = PropUtils.getScopedPropertyPrefix(prefix); rootDir = props.getProperty(prefix + ROOT_DIRECTORY_PROPERTY, rootDir); List<ZoomLevelMaker> zoomLevels = (List<ZoomLevelMaker>) PropUtils.objectsFromProperties(props, prefix + ZOOM_LEVELS_PROPERTY, ComponentFactory.ClassNameProperty); getZoomLevels().addAll(zoomLevels); } public Properties getProperties(Properties props) { props = super.getProperties(props); String prefix = PropUtils.getScopedPropertyPrefix(this); props.put(prefix + ROOT_DIRECTORY_PROPERTY, PropUtils.unnull(rootDir)); StringBuffer buf = new StringBuffer(); for (ZoomLevelMaker zfi : getZoomLevels()) { buf.append(zfi.getPropertyPrefix()).append(" "); zfi.getProperties(props); } if (buf.length() > 0) { props.put(prefix + ZOOM_LEVELS_PROPERTY, buf.toString().trim()); } return props; } public Properties getPropertyInfo(Properties props) { props = super.getPropertyInfo(props); PropUtils.setI18NPropertyInfo(Environment.getI18n(), props, com.bbn.openmap.dataAccess.mapTile.MapTileMaker.class, ROOT_DIRECTORY_PROPERTY, "Tile Directory", "Root directory for holding tile files.", "com.bbn.openmap.util.propertyEditor.DirectoryPropertyEditor"); return props; } /** * Creating the tile using the ImageServer methodology, knowing that the * MapTileMaker has been configured with an openmap.properties.file and * knows about layers and their marker names. * * @param uvx uv x pixel coordinate * @param uvy uv y pixel coordinate * @param zoomInfo zoom level for image tile * @param proj projection for tile * @return byte[] for raw image bytes */ public byte[] makeTile(double uvx, double uvy, ZoomLevelMaker zoomInfo, Proj proj) { Point2D center = tileUVToLatLon(new Point2D.Double(uvx + .5, uvy + .5), zoomInfo.getZoomLevel()); proj.setScale(mtcTransform.getScaleForZoom(zoomInfo.getZoomLevel())); proj.setCenter(center); proj.setHeight(TILE_SIZE); proj.setWidth(TILE_SIZE); return createImage(proj, -1, -1, zoomInfo.getLayers()); } /** * Creating a tile more freely, when you have a set of layers you want to * draw into the tile. * * @param uvx uv x pixel coordinate * @param uvy uv y pixel coordinate * @param zoomLevel zoom level for tile * @param layers layers to include in image * @param proj projection for tile * @param background the paint to use for the background of the image. * @return byte[] for raw image bytes */ public byte[] makeTile(double uvx, double uvy, int zoomLevel, List<Layer> layers, Proj proj, Paint background) { Point2D center = tileUVToLatLon(new Point2D.Double(uvx + .5, uvy + .5), zoomLevel); proj.setScale(mtcTransform.getScaleForZoom(zoomLevel)); proj.setCenter(center); proj.setHeight(TILE_SIZE); proj.setWidth(TILE_SIZE); return createImageFromLayers(proj, -1, -1, layers, background); } /** * The main call to make for a tile to be created. This method will cause * the correct mapTile method to be called, depending on the configuration * of the ZoomLevelMakers. * * @param uvx * @param uvy * @param zoomInfo * @param proj * @return the final file path used, with any extensions added. * @throws IOException */ public String makeTileFile(double uvx, double uvy, ZoomLevelMaker zoomInfo, Proj proj) throws IOException { byte[] imageBytes = zoomInfo.makeTile(uvx, uvy, this, proj); String filePath = zoomInfo.formatImageFilePath(getRootDir(), (int) uvx, (int) uvy); return writeImageFile(imageBytes, filePath, true); } /** * EmptyTileHandler method, called when a MapTileFactory needs to create and * return a missing tile. * * @param imagePath the path of the missing tile that is going to be used as * cache lookup later. * @param x the uv x coordinate of the tile. * @param y the uv y coordinate of the tile. * @param zoomLevel the zoom level of the tile. * @param mtcTransform the transform that converts x,y coordinates to * lat/lon and describes the layout of the uv tile coordinates. * @param proj the map projection, in case that matters what should be * returned for the empty tile. * @return BufferedImage for image tile, or null if there's a problem */ public BufferedImage getImageForEmptyTile(String imagePath, int x, int y, int zoomLevel, MapTileCoordinateTransform mtcTransform, Projection proj) { Layer[] layers = null; List<ZoomLevelMaker> zoomLevels = getZoomLevels(); if (zoomLevels != null && !zoomLevels.isEmpty()) { for (ZoomLevelMaker zoomLevelMaker : zoomLevels) { if (zoomLevelMaker.getZoomLevel() == zoomLevel) { List<Layer> layerList = zoomLevelMaker.getLayerList(); if (layerList != null) { layers = layerList.toArray(new Layer[layerList.size()]); } else { // The zoom level was defined, but layers for the zoom // level weren't, use top-level layers. layers = getLayers(); } break; } } } else { // particular layer configurations for zoom levels aren't defined, // use all layers. layers = getLayers(); } // Either layers weren't defined at all, or a particular zoom level // wasn't defined. if (layers == null) { if (logger.isLoggable(Level.FINE)) { logger.fine("There are no layers defined for zoom level" + zoomLevel + " in MapTileMaker, can't create image"); } return null; } LatLonPoint center = tileUVToLatLon(new Point2D.Double(x + .5, y + .5), zoomLevel); Mercator m = new Mercator(center, mtcTransform.getScaleForZoom(zoomLevel), TILE_SIZE, TILE_SIZE); BufferedImage bufferedImage = new BufferedImage(TILE_SIZE, TILE_SIZE, BufferedImage.TYPE_INT_ARGB); GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment(); Graphics graphics = ge.createGraphics(bufferedImage); graphics.setClip(0, 0, TILE_SIZE, TILE_SIZE); if (graphics == null) { return null; } m.drawBackground((Graphics2D) graphics, background); if (layers != null) { for (int i = layers.length - 1; i >= 0; i--) { try { layers[i].renderDataForProjection(proj, graphics); } catch (Exception e) { if (logger.isLoggable(Level.FINE)) { logger.fine("Problem rendering layer " + i); } } } } else if (logger.isLoggable(Level.FINE)) { logger.fine("no layers available for image"); } graphics.dispose(); return bufferedImage; } /** * Main call to make a set of tiles. The ZoomLevelMaker objects have to be * configured correctly. Each ZoomLevelMaker has to have its zoom levels set * (initial and range), the bounds of the areas where tiles are desired, and * the layers desired on those tiles. For the layers, the ZoomLevelInfo can * have a List of Strings corresponding to the property prefixes of the * layers already set on the MapTileMaker, or it can have a List of Layer * objects to use. The root output directory has to be set in the * MapTileMaker. The image formatter also needs to be set. */ public void makeTiles() { if (rootDir != null) { File rd = new File(rootDir); if (!rd.exists()) { rd.mkdir(); } } Proj proj = new Mercator(new LatLonPoint.Double(), 10000, MapTileCoordinateTransform.TILE_SIZE, MapTileCoordinateTransform.TILE_SIZE); List<ZoomLevelMaker> zoomLevels = getZoomLevels(); for (ZoomLevelMaker zfi : zoomLevels) { logger.info("writing zoom level " + zfi.getName() + " tiles..."); int zoomLevel = zfi.getZoomLevel(); for (Rectangle2D bounds : zfi.getUVBounds(mtcTransform, zoomLevel)) { if (logger.isLoggable(Level.FINE)) { logger.fine(" creating tiles " + bounds); } int startx = (int) bounds.getX(); int starty = (int) bounds.getY(); int xofflimit = (int) bounds.getWidth(); int yofflimit = (int) bounds.getHeight(); for (int xoff = 0; xoff < xofflimit; xoff++) { int x = startx + xoff; // Reset every x loop for first time check through y loop String parentDirPath = null; for (int yoff = 0; yoff < yofflimit; yoff++) { int y = starty + yoff; if (parentDirPath == null) { parentDirPath = zfi.formatParentDirectoryName(getRootDir(), x, y); File parentDir = new File(parentDirPath); if (!parentDir.exists()) { parentDir.mkdirs(); } } try { String outputFile = makeTileFile(x, y, zfi, proj); if (logger.isLoggable(Level.FINER)) { logger.finer("wrote: " + outputFile); } } catch (IOException ioe) { logger.warning("Caught IOException writing " + x + ", " + y + ", " + zfi); } } } } // At this point, for a specific ZoomLevelInfo, the basic tiles for // it have been created. Now we can check the range and create tiles // for the range out of the new tiles. int range = zfi.getRange(); if (range < zoomLevel) { Properties rangeProps = new Properties(); MapTileLayer tileLayer = new MapTileLayer(); StandardMapTileFactory tileFactory = new StandardMapTileFactory(); tileFactory.setRootDir(getRootDir()); tileFactory.setFileExt(getFormatter().getFormatLabel()); tileLayer.setTileFactory(tileFactory); List<Layer> subLayers = new ArrayList<Layer>(); subLayers.add(tileLayer); for (int rangeZoomLevel = zoomLevel - 1; rangeZoomLevel >= range; rangeZoomLevel--) { tileLayer.setZoomLevel(rangeZoomLevel); ZoomLevelInfo rangeZFI = new ZoomLevelInfo(); rangeZFI.setZoomLevel(rangeZoomLevel); rangeZFI.setScale(mtcTransform.getScaleForZoom(rangeZoomLevel)); // Create new tiles from the tiles one zoom level up tileLayer.setZoomLevel(rangeZoomLevel + 1); for (Rectangle2D rawBounds : zfi.getBounds()) { Rectangle2D bounds = rangeZFI.getUVBounds(rawBounds, mtcTransform, rangeZoomLevel); if (logger.isLoggable(Level.INFO)) { logger.fine(" creating subtiles " + bounds); } int startx = (int) bounds.getX(); int starty = (int) bounds.getY(); int xofflimit = (int) bounds.getWidth(); int yofflimit = (int) bounds.getHeight(); for (int xoff = 0; xoff < xofflimit; xoff++) { int x = startx + xoff; // Reset every x loop for first time check through y // loop String parentDirPath = null; for (int yoff = 0; yoff < yofflimit; yoff++) { int y = starty + yoff; if (parentDirPath == null) { parentDirPath = rangeZFI.formatParentDirectoryName(getRootDir(), x, y); File parentDir = new File(parentDirPath); if (!parentDir.exists()) { parentDir.mkdirs(); } } try { byte[] imageBytes = makeTile(x, y, rangeZoomLevel, subLayers, proj, OMColor.clear); String filePath = rangeZFI.formatImageFilePath(getRootDir(), (int) x, (int) y); String outputFile = writeImageFile(imageBytes, filePath, true); if (logger.isLoggable(Level.INFO)) { logger.finer("wrote: " + outputFile); } } catch (IOException ioe) { logger.warning("Caught IOException writing " + x + ", " + y + ", " + zfi); } } } } } } } logger.info("done writing tiles"); } public String getRootDir() { return rootDir; } public void setRootDir(String rootDir) { this.rootDir = rootDir; } public List<ZoomLevelMaker> getZoomLevels() { if (zoomLevels == null) { zoomLevels = new LinkedList<ZoomLevelMaker>(); } return zoomLevels; } public void setZoomLevels(List<ZoomLevelMaker> zoomLevels) { this.zoomLevels = zoomLevels; } public void createDefaultZoomLevels(int maxZoomLevel) { Layer[] layers = getLayers(); List<ZoomLevelMaker> zoomLevels = getZoomLevels(); List<String> layerNames = new LinkedList<String>(); for (int i = 0; i < layers.length; i++) { String layerName = layers[i].getPropertyPrefix(); if (layerName != null) { layerNames.add(layerName); } else { logger.info("no name for layer[" + i + "]"); } } zoomLevels.clear(); for (int i = 0; i <= maxZoomLevel; i++) { ZoomLevelMaker zfi = new ZoomLevelMaker("ZoomLayerInfo " + i, "Tiles for zoom level " + i, i); zfi.setLayers(layerNames); zfi.setPropertyPrefix("zoom" + i); zoomLevels.add(zfi); } } /** * @param latlon a Point2D whose x component is the longitude and y * component is the latitude * @param zoom Tile Map Service (TMS) style zoom level (0-19 usually) * @return The "tile number" whose x and y components each are floating * point numbers that represent the distance in number of tiles from * the origin of the whole map at this zoom level. At zoom=0, the * lat,lon point of 0,0 maps to 0.5,0.5 since there is only one tile * at zoom level 0. */ public Point2D latLonToTileUV(Point2D latlon, int zoom) { return mtcTransform.latLonToTileUV(latlon, zoom, null); } public Point2D latLonToTileUV(Point2D latlon, int zoom, Point2D ret) { return mtcTransform.latLonToTileUV(latlon, zoom, ret); } /** * @param tileUV a Point2D whose x,y coordinates represent the distance in * number of tiles (each 256x256) from the origin (where the origin * is 90lat,-180lon) * @param zoom Tile Map Service (TMS) style zoom level (0-19 usually) * @return a LatLonPoint whose x coordinate is the longitude and y * coordinate is the latitude, decimal degrees. */ public LatLonPoint tileUVToLatLon(Point2D tileUV, int zoom) { return mtcTransform.tileUVToLatLon(tileUV, zoom, null); } public LatLonPoint tileUVToLatLon(Point2D tileUV, int zoom, LatLonPoint ret) { return mtcTransform.tileUVToLatLon(tileUV, zoom, ret); } public static void main(String[] args) { com.bbn.openmap.util.ArgParser ap = new com.bbn.openmap.util.ArgParser("MapTileMaker"); ap.add("properties", "The properties file to use for image tiles.", 1); ap.add("create", "Create a sample properties file at a path", 1); if (!ap.parse(args)) { ap.printUsage(); System.exit(0); } String arg[]; Properties props = null; arg = ap.getArgValues("properties"); if (arg != null) { String ps = arg[0]; try { URL url = PropUtils.getResourceOrFileOrURL(null, ps); InputStream inputStream = url.openStream(); props = new Properties(); props.load(inputStream); MapTileMaker tim = new MapTileMaker(props); tim.makeTiles(); } catch (MalformedURLException murle) { Debug.error("TileMaker can't find properties file: " + arg[0]); } catch (IOException ioe) { Debug.error("TileMaker can't create images: IOException"); } } arg = ap.getArgValues("create"); if (arg != null) { String outputFile = arg[0]; MapTileMaker tim; if (props == null) { ShapeLayer shapeLayer = new ShapeLayer(); props = new Properties(); props.put("shape.prettyName", "Countries"); props.put("shape.shapeFile", "data/shape/world/cntry02/cntry02.shp"); props.put("shape.fillColor", "FFBBBBBB"); shapeLayer.setProperties("shape", props); tim = new MapTileMaker(new Layer[] { shapeLayer }, new SunJPEGFormatter()); tim.createDefaultZoomLevels(4); tim.setRootDir("<Path to top level directory for tiles>"); tim.setFormatter(new PNG32ImageFormatter()); } else { tim = new MapTileMaker(props); } Properties configurationProps = new Properties(); configurationProps = tim.getProperties(configurationProps); StringBuilder sb = new StringBuilder("#### MapTileMaker Properties ####\n"); if (!configurationProps.isEmpty()) { TreeMap orderedProperties = new TreeMap(configurationProps); for (Iterator keys = orderedProperties.keySet().iterator(); keys.hasNext();) { String key = (String) keys.next(); String value = configurationProps.getProperty(key); if (value != null) { sb.append(key).append("=").append(value).append("\n"); } } } try { FileOutputStream fos = new FileOutputStream(outputFile); PrintStream ps = new PrintStream(fos); ps.println(sb.toString()); ps.close(); } catch (IOException ioe) { logger.warning("caught IOException writing property file: " + ioe.getMessage()); } } System.exit(0); } }