/******************************************************************************* * SDR Trunk * Copyright (C) 2014 Dennis Sheirer * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/> ******************************************************************************/ package org.jdesktop.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.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.jdesktop.swingx.mapviewer.util.GeoUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * The <code>AbstractTileFactory</code> provides * a basic implementation for the TileFactory. */ public abstract class AbstractTileFactory extends TileFactory { private final static Logger mLog = LoggerFactory.getLogger( 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, "map tile fetcher-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); trys--; } else { final BufferedImage i = 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) { mLog.error("Failed to load a tile at url: " + tile.getURL() + ", stopping", e ); } else { mLog.error("Failed to load a tile at url: " + tile.getURL() + ", retrying", e ); trys--; } } } tile.setLoading(false); } private byte[] cacheInputStream(URL url) throws IOException { 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(); } } }