org.jdesktop.swingx.mapviewer.AbstractTileFactory Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of jxmapviewer2 Show documentation
Show all versions of jxmapviewer2 Show documentation
This project is based on the JXMapViewer component of SwingX-WS.
The newest version!
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.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 org.jdesktop.swingx.mapviewer.util.GeoUtil;
/**
* The AbstractTileFactory
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 tileMap = new HashMap();
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 tileQueue = new PriorityBlockingQueue(5, new Comparator()
{
@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 = img;
SwingUtilities.invokeAndWait(new Runnable()
{
@Override
public void run()
{
tile.image = new SoftReference(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", "JxMapViewer/1.0");
InputStream ins = connection.getInputStream();
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();
}
}
}