package org.jdesktop.swingx.mapviewer; import java.awt.image.BufferedImage; import java.lang.ref.SoftReference; import java.net.URI; import java.util.Comparator; import java.util.HashMap; import java.util.Map; import java.util.concurrent.BlockingQueue; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.PriorityBlockingQueue; import java.util.concurrent.ThreadFactory; import javax.swing.SwingUtilities; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; /** * The <code>DefaultTileFactory</code> (was AbstractTileFactory) provides a * basic implementation for the TileFactory. * * @author unknown * @author Simon Templer * * @version $Id$ */ public class DefaultTileFactory implements TileFactory { private static final Log log = LogFactory.getLog(DefaultTileFactory.class); private int threadPoolSize = 4; private ExecutorService service; private final TileProvider provider; // TODO the tile map should be static ALWAYS, regardless of the number // of GoogleTileFactories because each tile is, really, a singleton. private final Map<String, Tile> tileMap = new HashMap<String, Tile>(); private TileCache cache; /** * Create a {@link TileFactory} using a {@link TileProvider} * * @param provider the {@link TileProvider} * @param cache the tile cache to use */ public DefaultTileFactory(TileProvider provider, TileCache cache) { this.provider = provider; this.cache = cache; } /** * Returns the tile that is located at the given tile point for this zoom. * * @param x the tile's x coordinate * @param y the tile's y coordinate * @param zoom the zoom level * * @return the tile for the given coordinates and zoom level */ @Override public Tile getTile(int x, int y, int zoom) { return getTile(x, y, zoom, true); } private Tile getTile(int tpx, int tpy, int zoom, boolean eagerLoad) { // wrap the tiles horizontally --> mod the X with the max width and use // that int tileX = tpx; int numTilesWide = provider.getMapWidthInTiles(zoom); if (tileX < 0) { tileX = numTilesWide - (Math.abs(tileX) % numTilesWide); } tileX = tileX % numTilesWide; int tileY = tpy; URI[] uris; if (TileProviderUtils.isValidTile(provider, tileX, tileY, zoom)) { try { uris = provider.getTileUris(tileX, tileY, zoom); } catch (Exception e) { uris = null; log.warn("Error getting tile urls", e); } } else { uris = null; log.warn( "Invalid tile requested: x = " + tileX + ", y = " + tileY + ", zoom = " + zoom); } Tile.Priority pri = Tile.Priority.High; if (!eagerLoad) { pri = Tile.Priority.Low; } Tile tile = null; if (uris == null || uris.length == 0) { // invalid/empty tile tile = new Tile(tileX, tileY, zoom); } else if (!tileMap.containsKey(uris[0].toString())) { // tile not found -> create new tile tile = new Tile(tileX, tileY, zoom, pri, this, uris); startLoading(tile); tileMap.put(uris[0].toString(), tile); } else { // retrieve tile from map tile = tileMap.get(uris[0].toString()); // if its in the map but is low and isn't loaded yet // but we are in high mode if (tile.getPriority() == Tile.Priority.Low && eagerLoad && !tile.isLoaded()) { promote(tile); } } /* * if (eagerLoad && doEagerLoading) { for (int i = 0; i<1; i++) { for * (int j = 0; j<1; j++) { // preload the 4 tiles under the current one * if(zoom > 0) { eagerlyLoad(tilePoint.getX()*2, tilePoint.getY()*2, * zoom-1); eagerlyLoad(tilePoint.getX()*2+1, tilePoint.getY()*2, * zoom-1); eagerlyLoad(tilePoint.getX()*2, tilePoint.getY()*2+1, * zoom-1); eagerlyLoad(tilePoint.getX()*2+1, tilePoint.getY()*2+1, * zoom-1); } } } } */ return tile; } /** * Get the tile cache * * @return the tile cache */ public TileCache getTileCache() { return cache; } /** * Set the tile cache * * @param cache the tile cache */ public void setTileCache(TileCache cache) { this.cache = cache; } /** ==== threaded tile loading stuff === */ /** * Thread pool for loading the tiles */ private final BlockingQueue<Tile> tileQueue = new PriorityBlockingQueue<Tile>(5, new Comparator<Tile>() { @Override public int compare(Tile o1, Tile o2) { if (o1.getPriority() == Tile.Priority.Low && o2.getPriority() == Tile.Priority.High) { return 1; } if (o1.getPriority() == Tile.Priority.High && o2.getPriority() == Tile.Priority.Low) { return -1; } return 0; } @Override public int hashCode() { return super.hashCode(); } @Override public boolean equals(Object obj) { return obj == this; } }); /** * Subclasses may override this method to provide their own executor * services. This method will be called each time a tile needs to be loaded. * Implementations should cache the ExecutorService when possible. * * @return ExecutorService to load tiles with */ protected synchronized ExecutorService getService() { if (service == null) { // System.out.println("creating an executor service with a // threadpool of size " + threadPoolSize); service = Executors.newFixedThreadPool(threadPoolSize, new ThreadFactory() { private int count = 0; @Override public Thread newThread(Runnable r) { Thread t = new Thread(r, "tile-pool-" + count++); t.setPriority(Thread.MIN_PRIORITY); t.setDaemon(true); return t; } }); } return service; } /** * Set the number of threads to use for loading the tiles. This controls the * number of threads used by the ExecutorService returned from getService(). * Note, this method should be called before loading the first tile. Calls * after the first tile are loaded will have no effect by default. * * @param size the thread pool size */ public synchronized void setThreadPoolSize(int size) { if (size <= 0) { throw new IllegalArgumentException("size invalid: " + size + ". The size of the threadpool must be greater than 0."); } threadPoolSize = size; } /** * Start loading a tile * * @param tile the tile to start loading */ @Override public synchronized void startLoading(Tile tile) { if (tile.isLoading()) { System.out.println("already loading. bailing"); return; } tile.setLoading(true); try { tileQueue.put(tile); if (!getService().isShutdown()) { getService().submit(createTileRunner(tile)); } } catch (Exception ex) { ex.printStackTrace(); } } /** * Subclasses can override this if they need custom TileRunners for some * reason * * @param tile the tile to be loaded * * @return a tile runner */ protected Runnable createTileRunner(TileInfo tile) { return new TileRunner(); } /** * Increase the priority of this tile so it will be loaded sooner. * * @param tile the tile for which to increase the priority */ public synchronized void promote(Tile tile) { if (tileQueue.contains(tile)) { try { tileQueue.remove(tile); tile.setPriority(Tile.Priority.High); tileQueue.put(tile); } catch (Exception ex) { ex.printStackTrace(); } } } /** * An inner class which actually loads the tiles. Used by the thread queue. * Subclasses can override this if necessary. */ public class TileRunner implements Runnable { /** * @return the maximum number of tries to fetch a tile */ protected int maxTries() { return 3; } /** * Called when loading failed and there are tries left * * @param tile the tile to load * @param triesLeft number of tries left * @param error the error that occurred (if available) */ protected void onRetry(TileInfo tile, int triesLeft, Throwable error) { log.debug("Failed to load tile: " + tile.getURI() + " - Retrying", error); } /** * Called when loading of a tile failed in all tries * * @param tile the tile to load * @param error the error that occurred last (if available) */ protected void onFailed(TileInfo tile, Throwable error) { log.warn("Failed to load tile: " + tile.getURI(), error); } /** * Called when a tile was successfully loaded * * @param tile the tile */ protected void onLoaded(TileInfo tile) { // do nothing } /** * Called loading of a tile failed because there was not enough memory * * @param tile the tile to load */ protected void onOutOfMem(TileInfo tile) { log.error("Failed to load tile: " + tile.getURI() + " - Not enough memory"); // cache.clear(); } /** * implementation of the Runnable interface. */ @Override public void run() { // get a tile out of the queue final Tile tile = tileQueue.remove(); // XXX tries handled by HttpClient? int trys = maxTries(); try { while (!tile.isLoaded() && trys > 0) { try { BufferedImage img = null; img = cache.get(tile); if (img == null) { trys--; if (trys == 0) // tile loading failed onFailed(tile, null); else { // give it another try onRetry(tile, trys, null); tile.notifyBeforeRetry(); } } else { final BufferedImage i = img; SwingUtilities.invokeAndWait(new Runnable() { @Override public void run() { try { tile.image = new SoftReference<BufferedImage>(i); tile.setLoaded(true); // tile was succesfully loaded onLoaded(tile); } catch (Exception e) { log.error(e.getMessage(), e); } } }); } } catch (OutOfMemoryError memErr) { onOutOfMem(tile); trys = 0; } catch (Throwable e) { Object oldError = tile.getError(); tile.setError(e); tile.firePropertyChangeOnEDT("loadingError", oldError, e); if (trys == 0) { tile.firePropertyChangeOnEDT("unrecoverableError", null, e); } else { trys--; } if (trys == 0) onFailed(tile, e); else onRetry(tile, trys, e); } } } finally { tile.setLoading(false); } } } /** * @see TileFactory#getTileProvider() */ @Override public TileProvider getTileProvider() { return provider; } /** * @see TileFactory#cleanup() */ @Override public void cleanup() { getService().shutdownNow(); clearCache(); } /** * @see TileFactory#clearCache() */ @Override public void clearCache() { getTileCache().clear(); // setTileCache(new DefaultTileCache()); } }