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

org.jxmapviewer.viewer.AbstractTileFactory Maven / Gradle / Ivy


package org.jxmapviewer.viewer;

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.jxmapviewer.cache.LocalCache;
import org.jxmapviewer.cache.NoOpLocalCache;
import org.jxmapviewer.util.ProjectProperties;
import org.jxmapviewer.viewer.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);

    /**
     * Note that the name and version are actually set by Gradle
     * so there is no need to bump a version manually when new release
     * is made.
     */
    private static final String DEFAULT_USER_AGENT = ProjectProperties.INSTANCE.getName() + "/"
            + ProjectProperties.INSTANCE.getVersion();

    private volatile int pendingTiles = 0;
    private int threadPoolSize = 4;
    private String userAgent = DEFAULT_USER_AGENT;
    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();

    /**
     * Creates a new instance of DefaultTileFactory using the spcified TileFactoryInfo
     * @param info a TileFactoryInfo to configure this TileFactory
     */
    public AbstractTileFactory(TileFactoryInfo info)
    {
        super(info);
    }

    /**
     * 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;
        // 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 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;

        }
    });

    private LocalCache localCache = new NoOpLocalCache();

    /**
     * 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;
    }

    /**
     * Set the User agent that will be used when making a tile request.
     *
     * Some tile server usage policies requires application to identify itself,
     * so please make sure that it is set properly.
     *
     * @param userAgent User agent to be used.
     */
    public void setUserAgent(String userAgent) {
        if (userAgent == null || userAgent.isEmpty()) {
            throw new IllegalArgumentException("User agent can't be null or empty.");
        }

        this.userAgent = userAgent;
    }


    @Override
    protected synchronized void startLoading(Tile tile)
    {
        if (tile.isLoading())
        {
            // System.out.println("already loading. bailing");
            return;
        }
        pendingTiles++;
        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();
            }
        }
    }

    @Override
    public void setLocalCache(LocalCache cache) {
        this.localCache = cache;
    }

    /**
     * @return the number of pending (loading or queues) tiles
     */
    public int getPendingTiles() {
        return pendingTiles;
    }

    /**
     * An inner class which actually loads the tiles. Used by the thread queue. Subclasses can override this
     * via {@link #createTileRunner(Tile)} 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());
        }

        @Override
        public void run()
        {
            /*
             * Attempt to load the tile from its URL. If loading fails, retry two more times.
             * If all attempts fail, nothing else is done. This way, if there is some kind of
             * URL-specific failure, the pooled thread can try to load other tiles.
             */
            final Tile tile = tileQueue.remove();

            int remainingAttempts = 3;
            while (!tile.isLoaded() && remainingAttempts > 0)
            {
                remainingAttempts--;
                try
                {
                    URI uri = getURI(tile);
                    BufferedImage 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);
                    }
                    else
                    {
                        final BufferedImage i = img;
                        SwingUtilities.invokeAndWait(new Runnable()
                        {
                            @Override
                            public void run()
                            {
                                tile.image = new SoftReference(i);
                                tile.setLoaded(true);
                                pendingTiles--;
                                fireTileLoadedEvent(tile);
                            }
                        });
                    }
                }
                catch (OutOfMemoryError memErr)
                {
                    cache.needMoreMemory();
                }
                catch (Throwable e)
                {
                    if (remainingAttempts == 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);
                    }
                }
            }
            tile.setLoading(false);
        }

        private byte[] cacheInputStream(URL url) throws IOException
        {
            InputStream ins = localCache.get(url);
            if (ins == null) {
                URLConnection connection = url.openConnection();
                connection.setRequestProperty("User-Agent", userAgent);
                ins = connection.getInputStream();
            }
            try {
                byte[] data = readAllBytes(ins);
                localCache.put(url, new ByteArrayInputStream(data));
                return data;
            }
            finally {
                ins.close();
            }
        }

        private byte[] readAllBytes(InputStream ins) throws IOException {
            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