//********************************************************************** // //<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.Image; import java.awt.Point; import java.awt.geom.Point2D; import java.awt.image.BufferedImage; import java.io.ByteArrayInputStream; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.net.MalformedURLException; import java.net.URL; import java.util.ArrayList; import java.util.List; import java.util.Properties; import java.util.Vector; import java.util.jar.JarEntry; import java.util.jar.JarInputStream; import java.util.logging.Level; import java.util.logging.Logger; import java.util.regex.Matcher; import java.util.regex.Pattern; import com.bbn.openmap.Environment; import com.bbn.openmap.I18n; import com.bbn.openmap.PropertyConsumer; import com.bbn.openmap.image.BufferedImageHelper; import com.bbn.openmap.omGraphics.OMGraphic; import com.bbn.openmap.omGraphics.OMGraphicList; import com.bbn.openmap.omGraphics.OMScalingRaster; import com.bbn.openmap.omGraphics.OMText; import com.bbn.openmap.omGraphics.OMTextLabeler; import com.bbn.openmap.omGraphics.OMWarpingImage; import com.bbn.openmap.proj.Mercator; import com.bbn.openmap.proj.Projection; import com.bbn.openmap.proj.coords.LatLonPoint; import com.bbn.openmap.util.ClasspathHacker; import com.bbn.openmap.util.ComponentFactory; import com.bbn.openmap.util.DataBounds; import com.bbn.openmap.util.PropUtils; import com.bbn.openmap.util.cacheHandler.CacheHandler; import com.bbn.openmap.util.cacheHandler.CacheObject; /** * The StandardImageTileFactory is a TileFactory implementation that retrieves * image tiles from local storage. These tiles are assumed to be stored in the * local file system, at some root directory, and then in some hierarchy like * zoom-level/x coord/y coord.file-extension. This class can be extended to * allow different tile naming/storing conventions to be used. * <p> * * This component can be configured using properties: * <p> * * <pre> * rootDir=the path to the parent directory of the tiles. The factory will construct specific file paths that are appended to this value. * fileExt=the file extension to append to the tile names * cacheSize=the number of mapTiles the factory should hold on to. The default is 100. * # default is OSMMapTileCoordinateTransform, but it depends on the source of tiles. GDAL is TSMMapTileCoordinateTransform * mapTileTransform=com.bbn.openmap.dataAccess.mapTile.OSMMapTileCoordinateTransform, or com.bbn.openmap.dataAccess.mapTile.TSMMapTileCoordinateTransform * # what to do about missing tiles? * emptyTileHandler=com.bbn.openmap.dataAccess.mapTile.SimpleEmptyTileHandler * # Set a tile image preparer, if you want to change how images are rendered (greyscale, for instance) * tileImagePreparer=com.bbn.openmap.dataAccess.mapTile.StandardImagePreparer * # or * tileImagePreparer=com.bbn.openmap.dataAccess.mapTile.GreyscaleImagePreparer * </pre> * * @author dietrick */ public class StandardMapTileFactory extends CacheHandler implements MapTileFactory, PropertyConsumer { protected String prefix = null; protected final static Logger logger = Logger.getLogger("com.bbn.openmap.dataAccess.mapTile.StandardMapTileFactory"); protected final static Logger mapTileLogger = Logger.getLogger("MAPTILE_DEBUGGING"); public final static String ROOT_DIR_PROPERTY = "rootDir"; public final static String FILE_EXT_PROPERTY = "fileExt"; public final static String CACHE_SIZE_PROPERTY = "cacheSize"; public final static String MTCTRANSFORM_PROPERTY = "mapTileTransform"; public final static String EMPTY_TILE_HANDLER_PROPERTY = "emptyTileHandler"; public final static String ZOOM_LEVEL_INFO_PROPERTY = "zoomLevelInfo"; public final static String ZOOM_LEVEL_TILE_SIZE_PROPERTY = "zoomLevelTileSize"; public final static String TILE_IMAGE_PREPARER_PROPERTY = "tileImagePreparer"; /** * Inserted into properties loaded via tiles.omp, so that the * EmptyTileHandler can know where the tile set is located, in case it needs * to know the absolute path. Will contain the root directory path specified * in the factory properties, as opposed to any rootDir property set in the * tiles.omp file that would specify a relative root directory path. */ public final static String ROOT_DIR_PATH_PROPERTY = "rootDirPath"; /** * The name of the properties file that the factory looks for in the root * directory of the data (tiles.omp). */ public final static String TILE_PROPERTIES = "tiles.omp"; protected ZoomLevelInfo zoomLevelInfo = new ZoomLevelInfo(); protected String rootDir; protected String fileExt = ".png"; protected String rootDirProperty; // For writing out later, if necessary protected EmptyTileHandler emptyTileHandler = null; protected boolean verbose = false; /** * The zoom level tile size is used by the factory to determine when it * needs to get tiles for a different zoom level. The default value is 350. * That is, when the factory is figuring out what zoom level to use, if the * pixel size of a tile is greater than or equal to 350 x 350, it decides to * check the next zoom level for retrieving tiles. This is used instead of * just comparing projection scales. */ protected int zoomLevelTileSize = 350; protected TileImagePreparer tileImagePreparer; /** * If set, the MapTileRequester will be notified when the list provided in * getTiles() has been updated, and asked if it should continue with the * getTiles() request at opportune times, when tile fetching is stable. */ protected MapTileRequester mapTileRequester; /** * Flag to tell the factory to create the extra tiles off-map. Tends to * cause the layer to do more work than necessary, so it's not used. */ private boolean doExtraTiles = false; /** * Coordinate transform for the uv coordinates of the tiles. Different * sources have different origins for tile coordinates. */ protected MapTileCoordinateTransform mtcTransform = new OSMMapTileCoordinateTransform(); public StandardMapTileFactory() { super(100); verbose = logger.isLoggable(Level.FINE); } public StandardMapTileFactory(MapTileRequester layer, String rootDir, String tileFileExt) { super(100); setRootDir(rootDir); setFileExt(tileFileExt); verbose = logger.isLoggable(Level.FINE); this.mapTileRequester = layer; } @Override public CacheObject load(Object key) { return null; } /** * Tell the factory to dump the cache. */ public void reset() { clear(); } /** * Called to load cache object from data source, when not found in cache. * * @param key cache key * @param x uv x coordinate * @param y uv y coordinate * @param zoomLevel zoom level for tile to load * @param proj passed solely to enable checking if the projection of the * tiles matches the rendered projection. * @return CacheObject returned from cache, null if not found */ public CacheObject load(Object key, int x, int y, int zoomLevel, Projection proj) { if (key instanceof String) { String imagePath = (String) key; if (verbose) { logger.fine("fetching file for cache: " + imagePath); } try { URL imageURL = PropUtils.getResourceOrFileOrURL(imagePath); if (imageURL != null) { BufferedImage bi = BufferedImageHelper.getBufferedImage(imageURL); OMGraphic raster = createOMGraphicFromBufferedImage(bi, x, y, zoomLevel, proj); if (raster != null) { return new CacheObject(imagePath, raster); } } else { logger.fine("Can't find resource located at " + imagePath); } } catch (MalformedURLException e) { logger.fine("Can't find resource located at " + imagePath); } catch (InterruptedException e) { logger.fine("Reading the image file was interrupted: " + imagePath); } catch (Exception fnfe) { logger.fine("file not found: " + imagePath); } } return null; } /** * Creates an OMRaster appropriate for projection and other parameters from * a buffered image. * * @param bi BufferedImage to use for tile. * @param x x uv coordinate for tile. * @param y y uv coordinate for tile. * @param zoomLevel zoom level for tile. * @param proj the current map projection * @return OMGraphic (OMScalingRaster or OMWarpingImage, most likely) * @throws InterruptedException */ protected OMGraphic createOMGraphicFromBufferedImage(BufferedImage bi, int x, int y, int zoomLevel, Projection proj) throws InterruptedException { OMGraphic raster = null; if (bi != null) { BufferedImage rasterImage = preprocessImage(bi, bi.getWidth(), bi.getHeight()); if (proj instanceof Mercator) { raster = getTileMatchingProjectionType(rasterImage, x, y, zoomLevel); } else { raster = getTileNotMatchingProjectionType(rasterImage, x, y, zoomLevel); } if (mapTileLogger.isLoggable(Level.FINE)) { raster.putAttribute(OMGraphic.LABEL, new OMTextLabeler("Tile: " + zoomLevel + "|" + x + "|" + y, OMText.JUSTIFY_CENTER)); raster.setSelected(true); } } return raster; } /** * Create an OMScalingRaster that matches the basic projection of the * current map. Only scales evenly for the opposite corner points. * * @param image BufferedImage created from tile file * @param x uv x coordinate * @param y uv y coordinate * @param zoomLevel zoom level for tile retrieval * @return OMGraphic, but really an OMScalingRaster. */ protected OMGraphic getTileMatchingProjectionType(BufferedImage image, int x, int y, int zoomLevel) { Point2D pnt = new Point2D.Double(); pnt.setLocation(x, y); Point2D tileUL = mtcTransform.tileUVToLatLon(pnt, zoomLevel); pnt.setLocation(x + 1, y + 1); Point2D tileLR = mtcTransform.tileUVToLatLon(pnt, zoomLevel); if (verbose) { logger.fine("tile coords: " + tileUL + ", " + tileLR); } double x1 = Math.min(tileUL.getX(), tileLR.getX()); double x2 = Math.max(tileUL.getX(), tileLR.getX()); double y1 = Math.min(tileUL.getY(), tileLR.getY()); double y2 = Math.max(tileUL.getY(), tileLR.getY()); return new OMScalingRaster(y2, x1, y1, x2, image); } /** * Create an OMWarpingImage that knows how to re-project itself for * different projections. The base projection is going to be defined for the * mtc transform set on the factory. Warping images are slower to generate * for a map projection than scaling rasters. * * @param image * @param x * @param y * @param zoomLevel * @return OMGraphic, but really an OMWarpingImage */ protected OMGraphic getTileNotMatchingProjectionType(BufferedImage image, int x, int y, int zoomLevel) { DataBounds dataBounds = new DataBounds(new Point(x, y), new Point(x + 1, y + 1)); dataBounds.setyDirUp(mtcTransform.isYDirectionUp()); return new OMWarpingImage(image, mtcTransform.getTransform(zoomLevel), dataBounds); } /** * Method that allows subclasses to modify the image as necessary before it * is passed into an OMGraphic. * * @param origImage Any java Image * @param imageWidth pixel width * @param imageHeight pixel height * @return BufferedImage with any changes necessary. * @throws InterruptedException */ protected BufferedImage preprocessImage(Image origImage, int imageWidth, int imageHeight) throws InterruptedException { return getTileImagePreparer().preprocessImage(origImage, imageWidth, imageHeight); } /** * The main call to retrieve something from the cache, modified to allow * load method to do some projection calculations to initialize tile * parameters. If the object is not found in the cache, then load is called * to get it from the data source. * * @param key cache key, usually string of location of a tile * @param x uv x location of tile * @param y uv y location of tile * @param zoomLevel zoom level of tile * @param proj passed solely to enable checking if the projection of the * tiles matches the rendered projection. * @return object from cache. */ public Object get(Object key, int x, int y, int zoomLevel, Projection proj) { CacheObject ret = searchCache(key); if (ret != null) { if (logger.isLoggable(Level.FINE)) { logger.fine("found tile (" + x + ", " + y + ") in cache"); } return ret.obj; } ret = load(key, x, y, zoomLevel, proj); if (ret == null) { return null; } replaceLeastUsed(ret); return ret.obj; } /** * An auxiliary call to retrieve something from the cache, modified to allow * load method to do some projection calculations to initialize tile * parameters. If the object is not found in the cache, null is returned. * * @param key cache key, usually string of location of a tile * @param x uv x location of tile * @param y uv y location of tile * @param zoomLevel zoom level of tile * @return cache object if found, null if not. */ public Object getFromCache(Object key, int x, int y, int zoomLevel) { CacheObject ret = searchCache(key); if (ret != null) { if (logger.isLoggable(Level.FINE)) { logger.fine("found tile (" + x + ", " + y + ") in cache"); } return ret.obj; } return null; } /** * Call to make when you want the tile factory to create some empty tile * representation for the given location. You can return any type of * OMGraphic embedded in a CacheObject. * * @param key the cache key for this object * @param x the uv x coordinate of the tile * @param y the uv y coordinate of the tile * @param zoomLevel the zoom level for the tile * @param proj the projection being used for the map. * @return CacheObject, or null if the empty tile should be blank. */ public CacheObject getEmptyTile(Object key, int x, int y, int zoomLevel, Projection proj) { getTileImagePreparer().prepareForEmptyTile(this); EmptyTileHandler empTileHandler = getEmptyTileHandler(); if (empTileHandler != null) { BufferedImage bi = empTileHandler.getImageForEmptyTile((String) key, x, y, zoomLevel, mtcTransform, proj); OMGraphic raster; try { raster = createOMGraphicFromBufferedImage(bi, x, y, zoomLevel, proj); if (raster != null) { if (mapTileLogger.isLoggable(Level.FINE)) { Object labelObj = raster.getAttribute(OMGraphic.LABEL); if (labelObj instanceof OMTextLabeler) { OMTextLabeler label = (OMTextLabeler) labelObj; label.setData("EMPTY " + label.getData()); } } return new CacheObject(key, raster); } } catch (InterruptedException e) { if (logger.isLoggable(Level.FINE)) { e.printStackTrace(); } } } return null; } /** * Returns projected tiles for the given projection. * * @param proj the projection to fetch tiles for. * @return OMGraphicList containing projected OMGraphics. */ public OMGraphicList getTiles(Projection proj) { return getTiles(proj, -1, new OMGraphicList()); } /** * Returns projected tiles for given projection at specified zoom level. * * @param proj projection for query * @param zoomLevel zoom level 1-20 for tiles to be returned, -1 for code to * figure out appropriate zoom level. * @return OMGraphicList with tiles. */ public OMGraphicList getTiles(Projection proj, int zoomLevel) { return getTiles(proj, zoomLevel, new OMGraphicList()); } protected Projection lastProj; /** * Returns projected tiles for given projection at specified zoom level. Use * this call if you are providing a repaint callback component to the * factory, so you will have a handle on the OMGraphicList to render to. * * @param proj projection for query * @param zoomLevel zoom level 1-20 for tiles to be returned, -1 for code to * figure out appropriate zoom level. * @param list OMGraphicList that is returned, that will also have tiles * added to it. * @return OMGraphicList with tiles. */ public OMGraphicList getTiles(Projection proj, int zoomLevel, OMGraphicList list) { String fExt = getFileExt(); if (fExt == null || rootDir == null) { logger.warning("No path to tile files provided (" + rootDir + "), or file extension (" + fExt + ") not specified"); return list; } if (lastProj == null || !proj.getClass().isAssignableFrom(lastProj.getClass())) { logger.fine("Clearing out cache for new projection type"); clear(); // empty the cache to rebuild OMGraphics for different type // projection. } lastProj = proj; /** * Given a projection, a couple of things have to happen. * * - First, we need to figure out what zoom level fits us best if it is * not specified. * * - Second, we need to figure out the uv bounds that fit the * projection. * * - Third, we need to grab the images for uv grid, by cycling through * the limits in both directions. * * The TileMaker static methods let us convert uv to lat/lon and back, a * ZoomLevelInfo object can be used to figure out what the file path * looks like. */ if (zoomLevel < 0) { zoomLevel = mtcTransform.getZoomLevelForProj(proj, zoomLevelTileSize); if (verbose) { logger.fine("Best zoom level calculated at: " + zoomLevel); } } if (zoomLevel >= 0) { if (zoomLevel == 0) { zoomLevel++; } zoomLevelInfo.setZoomLevel(zoomLevel); Point2D upperLeft = proj.getUpperLeft(); Point2D lowerRight = proj.getLowerRight(); int[] uvBounds = mtcTransform.getTileBoundsForProjection(upperLeft, lowerRight, zoomLevel); int uvup = uvBounds[0]; int uvleft = uvBounds[1]; int uvbottom = uvBounds[2]; int uvright = uvBounds[3]; if (verbose) { logger.fine("for " + proj + ", fetching tiles between x(" + uvleft + ", " + uvright + ") y(" + uvup + ", " + uvbottom + ")"); } // dateline test Point2D datelinePnt = proj.forward(new LatLonPoint.Double(upperLeft.getY(), 180d)); double dlx = datelinePnt.getX(); boolean dateline = dlx > 0 & dlx < proj.getWidth(); logger.fine("Long(180) located at " + dlx); if (!dateline) { getTiles(uvleft, uvright, uvup, uvbottom, zoomLevelInfo, proj, list); } else { logger.fine("handling DATELINE"); getTiles(uvleft, (int) Math.pow(2, zoomLevel), uvup, uvbottom, zoomLevelInfo, proj, list); getTiles(0, uvright, uvup, uvbottom, zoomLevelInfo, proj, list); } } return list; } /** * A temporary object used to store information about map tiles that are not * found in the cache. The caching mechanism has been modified to search for * cached tiles first, and using this object to hold information about map * tiles that need to be loaded. The cached tiles will be immediately * displayed, and then these tiles will be displayed after that as they are * loaded. * * @author dietrick */ class LoadObj { String imagePath; int x; int y; int zoomLevel; LoadObj(String p, int x, int y, int z) { this.imagePath = p; this.x = x; this.y = y; this.zoomLevel = z; } } protected void getTiles(int uvleft, int uvright, int uvup, int uvbottom, ZoomLevelInfo zoomLevelInfo, Projection proj, OMGraphicList list) { if (verbose) { logger.fine("for zoom level: " + zoomLevelInfo.getZoomLevel() + ", screen covers uv coords [t:" + uvup + ", l:" + uvleft + ", b:" + uvbottom + ", r:" + uvright + "]"); } if (zoomLevelInfo.getZoomLevel() == 0) { logger.fine("got one tile, OM can't draw a single tile covering the earth. Sorry."); } List<LoadObj> reloads = new ArrayList<LoadObj>(); int zoomLevel = zoomLevelInfo.getZoomLevel(); int uvleftM = (int) Math.min(uvleft, uvright); int uvrightM = (int) Math.max(uvleft, uvright); int uvupM = (int) Math.min(uvbottom, uvup); int uvbottomM = (int) Math.max(uvbottom, uvup); for (int x = uvleftM; x < uvrightM; x++) { for (int y = uvupM; y < uvbottomM; y++) { if (mapTileRequester != null && !mapTileRequester.shouldContinue()) { return; } String imagePath = buildCacheKey(x, y, zoomLevel, getFileExt()); /** * Need to modify the action of the cache a little to make the * map appear more responsive. So, we cycle through the desired * tiles, gathering all of the tiles that are immediately * available. Generate them, add them to list, and call repaint * when they are set. * * Keep track of the ones that are not there, and load those * one-by-one after, calling repaint as they are added to the * list. */ OMGraphic tileGraphic = (OMGraphic) getFromCache(imagePath, x, y, zoomLevel); if (tileGraphic != null) { if (mapTileLogger.isLoggable(Level.FINE)) { tileGraphic.putAttribute(OMGraphic.LABEL, new OMTextLabeler("Tile: " + zoomLevel + "|" + x + "|" + y, OMText.JUSTIFY_CENTER)); tileGraphic.setSelected(true); } tileGraphic.generate(proj); list.add(tileGraphic); } else { reloads.add(new LoadObj(imagePath, x, y, zoomLevel)); } } } if (verbose) { logger.fine("found " + list.size() + " frames in cache, loading " + reloads.size() + " others now..."); } if (mapTileRequester != null) { mapTileRequester.listUpdated(); } /* * Load the tiles that are not already in the cache, that need to be * fetched from the source. */ for (LoadObj reload : reloads) { // Check and see of we should bother fetching the new tile. if (mapTileRequester != null && !mapTileRequester.shouldContinue()) { return; } loadTile(reload.imagePath, reload.x, reload.y, reload.zoomLevel, proj, list); // OK, got it, notify requester the list has been updated. if (mapTileRequester != null) { mapTileRequester.listUpdated(); } } if (verbose) { logger.fine("finished loading " + reloads.size() + " frames from source for screen" + (doExtraTiles ? ", moving to off-screen frames..." : "")); } if (!doExtraTiles) { return; } // Just for giggles, lets go ahead and walk around the edge of the area // and prefetch tiles to load them into memory... // int uvleft, int uvright, int uvup, int uvbottom int x1 = uvleft; int y1 = uvup; int x2 = uvright; int y2 = uvbottom; boolean top = false; boolean left = false; boolean right = false; boolean bottom = false; if (x1 > 0) { x1--; left = true; } if (y1 > 0) { y1--; top = true; } int edgeTileCount = zoomLevelInfo.getEdgeTileCount(); if (x2 < edgeTileCount - 1) { x2++; right = true; } if (y2 < edgeTileCount - 1) { y2++; bottom = true; } // Get the corners if (top && left) { loadTile(x1, y1, zoomLevel, proj, list); } if (bottom && left) { loadTile(x1, y2, zoomLevel, proj, list); } if (bottom && right) { loadTile(x2, y2, zoomLevel, proj, list); } if (top && right) { loadTile(x2, y1, zoomLevel, proj, list); } // Now go along the sides if (top) { for (int x = uvleft; x < uvright; x++) { loadTile(x, y1, zoomLevel, proj, list); } } if (bottom) { for (int x = uvleft; x < uvright; x++) { loadTile(x, y2, zoomLevel, proj, list); } } if (right) { for (int y = uvup; y < uvbottom; y++) { loadTile(x2, y, zoomLevel, proj, list); } } if (left) { for (int y = uvup; y < uvbottom; y++) { loadTile(x1, y, zoomLevel, proj, list); } } if (verbose) { logger.fine("finished loading all tiles (" + list.size() + ")"); } } /** * Handles going to the cache, getting the cache to load the tile, and then * manage the resulting OMRaster tile. Adds the tile to the list after * generating it with the projection, and calls the repaintCallback if there * is one. * * @param imagePath the image path for the tile * @param x the x uv coordinate of the tile * @param y the y uv coordinate of the tile * @param zoomLevel the zoomLevel of the tile * @param proj the current projection. * @param list the OMGraphicList to add the tile to. * @throws InterruptedException */ private void loadTile(String imagePath, int x, int y, int zoomLevel, Projection proj, OMGraphicList list) { CacheObject ret = load(imagePath, x, y, zoomLevel, proj); if (ret == null) { // Check if the factory wants to do anything for empty tiles. ret = getEmptyTile(imagePath, x, y, zoomLevel, proj); } if (ret != null) { replaceLeastUsed(ret); OMGraphic raster = (OMGraphic) ret.obj; if (raster != null) { raster.generate(proj); list.add(raster); if (logger.isLoggable(Level.FINE)) { raster.putAttribute(OMGraphic.TOOLTIP, imagePath); } } } } /** * Handles going to the cache, getting the cache to load the tile, and then * manage the resulting OMRaster tile. Adds the tile to the list after * generating it with the projection, and calls the repaintCallback if there * is one. Handles creating the image file path given the other info. * * @param x the x uv coordinate of the tile * @param y the y uv coordinate of the tile * @param zoomLevel the zoomLevel of the tile * @param proj the current projection. * @param list the OMGraphicList to add the tile to. * @throws InterruptedException */ private void loadTile(int x, int y, int zoomLevel, Projection proj, OMGraphicList list) { // String imagePath = zoomLevelInfo.formatImageFilePath(rootDir, x, y) + // fileExt; String imagePath = buildFilePath(x, y, zoomLevel, getFileExt()); loadTile(imagePath, x, y, zoomLevel, proj, list); } /** * Build an image path to load, based on specified tile coordinates, zoom * level and file extension settings. * * Look at the root directory definition and determine if the x,y,z values * of the path are specified as the holder values {z}, {x} and {y}. * * If they aren't specified, the provided values will be appended to the * rootDir as z/x/y.fileExt. * * If {z}, {x} or {y} are found in the root dir path, then it's assumed that * the rootDir contains all the information needed to specify the path and * regular expressions will be used to replace those value holders with the * values specified. The file extension in this case will not be appended to * the rootDir. * * @param x the x tile coordinate * @param y the y tile coordinate * @param z the zoom level * @param fileExt the file extension to use for the path. */ public String buildFilePath(int x, int y, int z, String fileExt) { TilePathBuilder pathBuilder = getTilePathBuilder(); if (pathBuilder == null) { pathBuilder = new TilePathBuilder(rootDir); setTilePathBuilder(pathBuilder); } return pathBuilder.buildTilePath(x, y, z, fileExt); } private TilePathBuilder tilePathBuilder = null; protected void setTilePathBuilder(TilePathBuilder tpb) { tilePathBuilder = tpb; } protected TilePathBuilder getTilePathBuilder() { return tilePathBuilder; } /** * Creates a unique cache key for this tile based on zoom, x, y. This method * was created so the ServerMapTileFactory could override it and use local * cache names for keys if a local cache was being used. * * @param x tile coord. * @param y tile coord. * @param z zoomLevel. * @param fileExt file extension. * @return String used in cache. */ protected String buildCacheKey(int x, int y, int z, String fileExt) { return buildFilePath(x, y, z, fileExt); } public static class TilePathBuilder { String rez = "(\\{z\\})"; // Curly Braces 1 String rex = "(\\{x\\})"; // Curly Braces 2 String rey = "(\\{y\\})"; // Curly Braces 3 Pattern pz = Pattern.compile(rez, Pattern.CASE_INSENSITIVE); Pattern px = Pattern.compile(rex, Pattern.CASE_INSENSITIVE); Pattern py = Pattern.compile(rey, Pattern.CASE_INSENSITIVE); String startingPath; boolean patternsUsed = false; boolean patternUseChecked = false; public TilePathBuilder(String rootDir) { startingPath = rootDir; } public boolean isPatternsUsed() { return patternsUsed; } public String buildTilePath(int x, int y, int z, String fileExt) { String ret = startingPath; if (((!patternUseChecked) || (patternUseChecked && patternsUsed)) && startingPath != null && startingPath.length() != 0) { ret = updatePath(ret, pz, Integer.toString(z)); ret = updatePath(ret, px, Integer.toString(x)); ret = updatePath(ret, py, Integer.toString(y)); patternUseChecked = true; } if (!patternsUsed) { // No pattern matching, need to build from scratch, with fileExt return buildDefaultTilePath(x, y, z, fileExt); } return ret; } private String buildDefaultTilePath(int x, int y, int z, String fileExt) { return startingPath + "/" + z + "/" + x + "/" + y + fileExt; } private String updatePath(String currentPath, Pattern p, String replaceWith) { Matcher m = p.matcher(currentPath); if (m.find()) { patternsUsed = true; return m.replaceAll(replaceWith); } return currentPath; } } public MapTileRequester getMapTileRequester() { return mapTileRequester; } public void setMapTileRequester(MapTileRequester mtRequestor) { this.mapTileRequester = mtRequestor; } public Properties getProperties(Properties getList) { String prefix = PropUtils.getScopedPropertyPrefix(this); getList.put(prefix + ROOT_DIR_PROPERTY, PropUtils.unnull(rootDirProperty)); getList.put(prefix + FILE_EXT_PROPERTY, PropUtils.unnull(getFileExt())); getList.put(prefix + CACHE_SIZE_PROPERTY, Integer.toString(getCacheSize())); getList.put(prefix + MTCTRANSFORM_PROPERTY, mtcTransform.getClass().toString()); if (emptyTileHandler != null) { getList.put(prefix + EMPTY_TILE_HANDLER_PROPERTY, emptyTileHandler.getClass().toString()); if (emptyTileHandler instanceof PropertyConsumer) { ((PropertyConsumer) emptyTileHandler).getProperties(getList); } } // Only save the zoomLevelInfo property if it's not the default. if (zoomLevelInfo != null && !zoomLevelInfo.getClass().equals(ZoomLevelInfo.class)) { getList.put(prefix + ZOOM_LEVEL_INFO_PROPERTY, zoomLevelInfo.getClass().getName()); } getList.put(prefix + ZOOM_LEVEL_TILE_SIZE_PROPERTY, Integer.toString(zoomLevelTileSize)); TileImagePreparer tip = getTileImagePreparer(); if (!(tip instanceof StandardImagePreparer)) { getList.put(prefix + TILE_IMAGE_PREPARER_PROPERTY, tip.getClass().getName()); if (tip instanceof PropertyConsumer) { ((PropertyConsumer) tip).getProperties(getList); } } return getList; } public Properties getPropertyInfo(Properties list) { I18n i18n = Environment.getI18n(); PropUtils.setI18NPropertyInfo(i18n, list, com.bbn.openmap.dataAccess.mapTile.StandardMapTileFactory.class, ROOT_DIR_PROPERTY, "Tile URL or Path", "Root directory containing image tiles, or URL (http://tileserver/{z}/{x}/{y}.png)", null); PropUtils.setI18NPropertyInfo(i18n, list, com.bbn.openmap.dataAccess.mapTile.StandardMapTileFactory.class, FILE_EXT_PROPERTY, "Image File Extension", "Extension of image files (.jpg, .png, etc)", null); PropUtils.setI18NPropertyInfo(i18n, list, com.bbn.openmap.dataAccess.mapTile.StandardMapTileFactory.class, CACHE_SIZE_PROPERTY, "Cache Size", "Number of tile images held in memory", null); PropUtils.setI18NPropertyInfo(i18n, list, com.bbn.openmap.dataAccess.mapTile.StandardMapTileFactory.class, ZOOM_LEVEL_TILE_SIZE_PROPERTY, "Zoom Level Tile Size", "The maximum pixel size of a tile before switching to a higher zoom level (350 is default)", null); return list; } public String getInitPropertiesOrder() { return ROOT_DIR_PROPERTY + " " + FILE_EXT_PROPERTY; } public String getPropertyPrefix() { return prefix; } public void setProperties(Properties setList) { setProperties(null, setList); } public void setProperties(String prefix, Properties setList) { setPropertyPrefix(prefix); prefix = PropUtils.getScopedPropertyPrefix(prefix); String rootDirectory = setList.getProperty(prefix + ROOT_DIR_PROPERTY); if (rootDirectory != null) { setRootDir(rootDirectory); } String tmpFileExt = setList.getProperty(prefix + FILE_EXT_PROPERTY, getFileExt()); // Add a period if it doesn't exist. if (tmpFileExt != null) { setFileExt(tmpFileExt); } String mapTileCoordinateTransform = setList.getProperty(prefix + MTCTRANSFORM_PROPERTY); if (mapTileCoordinateTransform != null) { Object obj = ComponentFactory.create(mapTileCoordinateTransform); if (obj instanceof MapTileCoordinateTransform) { setMtcTransform((MapTileCoordinateTransform) obj); } } String emptyTileHandlerString = setList.getProperty(prefix + EMPTY_TILE_HANDLER_PROPERTY); if (emptyTileHandlerString != null) { Object obj = ComponentFactory.create(emptyTileHandlerString, prefix, setList); if (obj instanceof EmptyTileHandler) { setEmptyTileHandler((EmptyTileHandler) obj); } } String zoomLevelInfoString = setList.getProperty(prefix + ZOOM_LEVEL_INFO_PROPERTY); if (zoomLevelInfoString != null) { Object obj = ComponentFactory.create(zoomLevelInfoString, prefix, setList); if (obj instanceof ZoomLevelInfo) { setZoomLevelInfo((ZoomLevelInfo) obj); } } String tileImagePreparerString = setList.getProperty(prefix + TILE_IMAGE_PREPARER_PROPERTY); if (tileImagePreparerString != null) { Object obj = ComponentFactory.create(tileImagePreparerString, prefix, setList); if (obj instanceof TileImagePreparer) { setTileImagePreparer((TileImagePreparer) obj); } } super.resetCache(PropUtils.intFromProperties(setList, prefix + CACHE_SIZE_PROPERTY, getCacheSize())); zoomLevelTileSize = PropUtils.intFromProperties(setList, prefix + ZOOM_LEVEL_TILE_SIZE_PROPERTY, zoomLevelTileSize); } public void setPropertyPrefix(String prefix) { this.prefix = prefix; } public String getRootDir() { return rootDir; } public void setRootDir(String rootDirectory) { if (rootDirectory != null) { if (rootDirectory.endsWith("jar")) { rootDirProperty = rootDirectory; String jarFileNames = rootDirectory; // Only use the tiles.omp file in the first file found. boolean tilesFileFound = false; Vector<String> jarNames = PropUtils.parseMarkers(jarFileNames, ";"); for (String jarName : jarNames) { boolean jarFileFound = false; try { if (!tilesFileFound) { URL jarURL = PropUtils.getResourceOrFileOrURL(jarName); if (jarURL != null) { jarFileFound = true; JarInputStream jarStream = new JarInputStream(jarURL.openStream()); JarEntry jarEntry = null; while ((jarEntry = jarStream.getNextJarEntry()) != null) { String entryName = jarEntry.getName(); if (entryName.equals(TILE_PROPERTIES)) { byte[] readBytes = new byte[100]; byte[] contentBytes = new byte[0]; int numRead = 0; while ((numRead = jarStream.read(readBytes, 0, readBytes.length)) > 0) { byte[] tmpBytes = new byte[numRead + contentBytes.length]; System.arraycopy(contentBytes, 0, tmpBytes, 0, contentBytes.length); System.arraycopy(readBytes, 0, tmpBytes, contentBytes.length, numRead); contentBytes = tmpBytes; } ByteArrayInputStream bais = new ByteArrayInputStream(contentBytes); configureFromProperties(bais, rootDirectory); bais.close(); jarStream.closeEntry(); tilesFileFound = true; break; } } jarStream.close(); } } if (jarFileFound) { logger.fine("adding " + jarName + " to classpath"); ClasspathHacker.addFile(jarName); } else { logger.fine("can't find " + jarName + ", not adding to classpath"); } // JarFile jarFile = new JarFile(jarName); // JarEntry jarPropertyFile = (JarEntry) // jarFile.getEntry(TILE_PROPERTIES); // // if (jarPropertyFile != null) { // InputStream is = // jarFile.getInputStream(jarPropertyFile); // configureFromProperties(is, rootDirectory); // } } catch (IOException ioe) { logger.warning("couldn't add map data jar file: " + jarName); } } // You might notice that we didn't set the rootDir here if a jar // file is being used. That's because we just want to use // whatever // the tile file says, and this method will be called again if // needed when the properties get written. } else { // check for tile.omp file that may describe how to read tiles. File tileProps = new File(rootDirectory, TILE_PROPERTIES); // Keep track of what the root directory was before we read // tiles.omp String currentRootDirectory = this.rootDir; String currentRootDirProperty = rootDirProperty; if (tileProps.exists()) { try { // Do this in case other properties are set for the tile // set, file ext, transform. configureFromProperties(tileProps.toURI().toURL().openStream(), rootDirectory); } catch (MalformedURLException murle) { logger.warning("tile file for " + rootDirectory + " couldn't be read: " + tileProps.getAbsolutePath()); } catch (IOException ioe) { logger.warning("tile file for " + rootDirectory + " couldn't be read"); } } /* * OK, things look a little crazy here, because there is a bit * of recursion going on. The configure call above may cause a * setRootDir call with a relative path stored in a jar file. * These rules below make sure the relative root dir from the * inner loop sets the rootDir, while preserving the * rootDirProperty from the outer loop. */ if (currentRootDirectory == null && this.rootDir == null) { // the tiles.omp file didn't change the root directory, so // lets go with the directory given. this.rootDir = rootDirectory; } if (currentRootDirProperty == null) { // Assuming a file path being set, not as a result of a jar // file rootDirProperty = rootDirectory; } } } else { // nulled out this.rootDir = rootDirectory; rootDirProperty = rootDirectory; } // Reset for new path tilePathBuilder = null; } public TileImagePreparer getTileImagePreparer() { if (tileImagePreparer == null) { tileImagePreparer = new StandardImagePreparer(); } return tileImagePreparer; } public void setTileImagePreparer(TileImagePreparer tileImagePreparer) { this.tileImagePreparer = tileImagePreparer; } /** * Called with an input stream for a properties file, used for reading * tiles.omp files. * * @param is input stream for tiles.omp file. * @param rootDirectory original path to what was specified as root * directory * @throws IOException */ protected void configureFromProperties(InputStream is, String rootDirectory) throws IOException { Properties props = new Properties(); props.load(is); props.put(ROOT_DIR_PATH_PROPERTY, rootDirectory); String oldPrefix = getPropertyPrefix(); setProperties(null, props); setPropertyPrefix(oldPrefix); } public String getFileExt() { return fileExt; } public void setFileExt(String fileExt) { this.fileExt = (fileExt != null && fileExt.startsWith(".")) ? fileExt : "." + fileExt; } /** * Get the ZoomLevelInfo set on the factory. The ZoomLevelInfo has basic * layout information about tiles for a particular zoom level, including how * tiles are named and how the factory should go about loading them. The * default ZoomLevelInfo is based on the OpenStreetMap tile layout, zoom * levels 0-20 (where level 0 is all the way zoomed out), and the tiles are * stored zoomLevel/x/y.(fileExt). * * @return the zoomLevelInfo */ public ZoomLevelInfo getZoomLevelInfo() { return zoomLevelInfo; } /** * Get the ZoomLevelInfo set on the factory. The ZoomLevelInfo has basic * layout information about tiles for a particular zoom level, including how * tiles are named and how the factory should go about loading them. The * default ZoomLevelInfo is based on the OpenStreetMap tile layout, zoom * levels 0-20 (where level 0 is all the way zoomed out), and the tiles are * stored zoomLevel/x/y.(fileExt). You can set a different zoom level info * if you want to work with a tile set that is stored/defined differently * than OSM. * <p> * Won't allow itself to be set to null. * * @param zoomLevelInfo the zoomLevelInfo to set */ public void setZoomLevelInfo(ZoomLevelInfo zoomLevelInfo) { if (zoomLevelInfo != null) { this.zoomLevelInfo = zoomLevelInfo; } } public MapTileCoordinateTransform getMtcTransform() { return mtcTransform; } /** * Set the map tile coordinate transformed used to figure out lat/lon to * tile coordinates. Can't be null, if you set it to null an * OSMMapTileCoordTransform will be created instead. * * @param mtcTransform */ public void setMtcTransform(MapTileCoordinateTransform mtcTransform) { if (mtcTransform == null) { mtcTransform = new OSMMapTileCoordinateTransform(); } this.mtcTransform = mtcTransform; } /** * @return the emptyTileHandler */ public EmptyTileHandler getEmptyTileHandler() { return emptyTileHandler; } /** * @param emptyTileHandler the emptyTileHandler to set */ public void setEmptyTileHandler(EmptyTileHandler emptyTileHandler) { this.emptyTileHandler = emptyTileHandler; } }