package com.opendoorlogistics.codefromweb.jxmapviewer2.fork.swingx.mapviewer; import java.awt.image.BufferedImage; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.lang.ref.SoftReference; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; import java.net.URLConnection; 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.imageio.ImageIO; import javax.swing.SwingUtilities; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import com.opendoorlogistics.codefromweb.jxmapviewer2.fork.swingx.mapviewer.util.GeoUtil; /** * The <code>AbstractTileFactory</code> provides a basic implementation for the TileFactory. */ public abstract class AbstractTileFactory extends TileFactory { private static final Log log = LogFactory.getLog(AbstractTileFactory.class); /** * Creates a new instance of DefaultTileFactory using the spcified TileFactoryInfo * * @param info * a TileFactoryInfo to configure this TileFactory */ public AbstractTileFactory(TileFactoryInfo info) { super(info); } // private static final boolean doEagerLoading = true; private int threadPoolSize = 4; private ExecutorService service; // TODO the tile map should be static ALWAYS, regardless of the number // of GoogleTileFactories because each tile is, really, a singleton. private Map<String, Tile> tileMap = new HashMap<String, Tile>(); private TileCache cache = new TileCache(); /** * Returns the tile that is located at the given tilePoint for this zoom. For example, if getMapSize() returns 10x20 for this zoom, and the tilePoint is * (3,5), then the appropriate tile will be located and returned. */ @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;// tilePoint.getX(); int numTilesWide = (int) getMapSize(zoom).getWidth(); if (tileX < 0) { tileX = numTilesWide - (Math.abs(tileX) % numTilesWide); } tileX = tileX % numTilesWide; int tileY = tpy; // TilePoint tilePoint = new TilePoint(tileX, tpy); String url = getInfo().getTileUrl(tileX, tileY, zoom);// tilePoint); // System.out.println("loading: " + url); Tile.Priority pri = Tile.Priority.High; if (!eagerLoad) { pri = Tile.Priority.Low; } Tile tile = null; // System.out.println("testing for validity: " + tilePoint + " zoom = " + zoom); if (!tileMap.containsKey(url)) { if (!GeoUtil.isValidTile(tileX, tileY, zoom, getInfo())) { tile = new Tile(tileX, tileY, zoom); } else { tile = new Tile(tileX, tileY, zoom, url, pri, this); startLoading(tile); } tileMap.put(url, tile); } else { tile = tileMap.get(url); // 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()) { // System.out.println("in high mode and want a low"); // tile.promote(); 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; } /* * private void eagerlyLoad(int x, int y, int zoom) { TilePoint t1 = new TilePoint(x,y); if(!isLoaded(t1,zoom)) { getTile(t1,zoom,false); } } */ // private boolean isLoaded(int x, int y, int zoom) { // String url = getInfo().getTileUrl(zoom,x,y); // return tileMap.containsKey(url); // } /** * @return the tile cache */ public TileCache getTileCache() { return 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 static 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; } }); /** * 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; } @Override public void dispose() { if (service != null) { service.shutdown(); service = null; } } /** * 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 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; } @Override protected synchronized void startLoading(Tile tile) { if (tile.isLoading()) { System.out.println("already loading. bailing"); return; } tile.setLoading(true); try { tileQueue.put(tile); 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 (unused!) * @return the tile runner */ protected Runnable createTileRunner(Tile tile) { return new TileRunner(); } /** * Increase the priority of this tile so it will be loaded sooner. * * @param tile * the tile */ 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. */ private class TileRunner implements Runnable { /** * Gets the full URI of a tile. * * @param tile * the tile * @throws URISyntaxException * if the URI is invalid * @return a URI for the tile */ protected URI getURI(Tile tile) throws URISyntaxException { if (tile.getURL() == null) { return null; } return new URI(tile.getURL()); } /** * implementation of the Runnable interface. */ @Override public void run() { /* * 3 strikes and you're out. Attempt to load the url. If it fails, decrement the number of tries left and try again. Log failures. If I run out of * try s just get out. This way, if there is some kind of serious failure, I can get out and let other tiles try to load. */ final Tile tile = tileQueue.remove(); int trys = 3; while (!tile.isLoaded() && trys > 0) { try { BufferedImage img = null; URI uri = getURI(tile); img = cache.get(uri); if (img == null) { byte[] bimg = cacheInputStream(uri.toURL()); // img = PaintUtils.loadCompatibleImage(new ByteArrayInputStream(bimg)); img = ImageIO.read(new ByteArrayInputStream(bimg)); cache.put(uri, bimg, img); img = cache.get(uri); } if (img == null) { System.out.println("error loading: " + uri); log.info("Failed to load: " + uri); trys--; } else { final BufferedImage i = processLoadedImage(img); SwingUtilities.invokeAndWait(new Runnable() { @Override public void run() { tile.image = new SoftReference<BufferedImage>(i); tile.setLoaded(true); fireTileLoadedEvent(tile); } }); } } catch (OutOfMemoryError memErr) { cache.needMoreMemory(); } catch (Throwable e) { if (trys == 0) { log.error("Failed to load a tile at url: " + tile.getURL() + ", stopping", e); } else { log.warn("Failed to load a tile at url: " + tile.getURL() + ", retrying", e); trys--; } } } tile.setLoading(false); } private byte[] cacheInputStream(URL url) throws IOException { URLConnection connection = url.openConnection(); connection.setRequestProperty("User-Agent", "MyApp/1.0"); InputStream ins = connection.getInputStream(); // InputStream ins = url.openStream(); ByteArrayOutputStream bout = new ByteArrayOutputStream(); byte[] buf = new byte[256]; while (true) { int n = ins.read(buf); if (n == -1) break; bout.write(buf, 0, n); } return bout.toByteArray(); } } protected BufferedImage processLoadedImage(BufferedImage img){ return img; } }