All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.jdesktop.swingx.mapviewer.AbstractTileFactory Maven / Gradle / Ivy

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();
		}
	}
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy