001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.data.imagery;
003
004import java.io.IOException;
005import java.util.Map;
006import java.util.concurrent.ThreadPoolExecutor;
007import java.util.concurrent.TimeUnit;
008
009import org.apache.commons.jcs.access.behavior.ICacheAccess;
010import org.openstreetmap.gui.jmapviewer.Tile;
011import org.openstreetmap.gui.jmapviewer.interfaces.CachedTileLoader;
012import org.openstreetmap.gui.jmapviewer.interfaces.TileJob;
013import org.openstreetmap.gui.jmapviewer.interfaces.TileLoader;
014import org.openstreetmap.gui.jmapviewer.interfaces.TileLoaderListener;
015import org.openstreetmap.gui.jmapviewer.interfaces.TileSource;
016import org.openstreetmap.josm.data.cache.BufferedImageCacheEntry;
017import org.openstreetmap.josm.data.cache.HostLimitQueue;
018import org.openstreetmap.josm.data.preferences.IntegerProperty;
019import org.openstreetmap.josm.tools.CheckParameterUtil;
020import org.openstreetmap.josm.tools.Utils;
021
022/**
023 * Wrapper class that bridges between JCS cache and Tile Loaders
024 *
025 * @author Wiktor Niesiobędzki
026 */
027public class TMSCachedTileLoader implements TileLoader, CachedTileLoader {
028
029    protected final ICacheAccess<String, BufferedImageCacheEntry> cache;
030    protected final int connectTimeout;
031    protected final int readTimeout;
032    protected final Map<String, String> headers;
033    protected final TileLoaderListener listener;
034
035    /**
036     * overrides the THREAD_LIMIT in superclass, as we want to have separate limit and pool for TMS
037     */
038
039    public static final IntegerProperty THREAD_LIMIT = new IntegerProperty("imagery.tms.tmsloader.maxjobs", 25);
040
041    /**
042     * Limit definition for per host concurrent connections
043     */
044    public static final IntegerProperty HOST_LIMIT = new IntegerProperty("imagery.tms.tmsloader.maxjobsperhost", 6);
045
046    /**
047     * separate from JCS thread pool for TMS loader, so we can have different thread pools for default JCS
048     * and for TMS imagery
049     */
050    private static ThreadPoolExecutor DEFAULT_DOWNLOAD_JOB_DISPATCHER = getNewThreadPoolExecutor("TMS-downloader-%d");
051
052
053    private ThreadPoolExecutor downloadExecutor = DEFAULT_DOWNLOAD_JOB_DISPATCHER;
054
055    /**
056     * Constructor
057     * @param listener          called when tile loading has finished
058     * @param cache              of the cache
059     * @param connectTimeout    to remote resource
060     * @param readTimeout       to remote resource
061     * @param headers           HTTP headers to be sent along with request
062     * @throws IOException      when cache initialization fails
063     */
064    public TMSCachedTileLoader(TileLoaderListener listener, ICacheAccess<String, BufferedImageCacheEntry> cache,
065            int connectTimeout, int readTimeout, Map<String, String> headers) throws IOException {
066        CheckParameterUtil.ensureParameterNotNull(cache, "cache");
067        this.cache = cache;
068        this.connectTimeout = connectTimeout;
069        this.readTimeout = readTimeout;
070        this.headers = headers;
071        this.listener = listener;
072    }
073
074    /**
075     * @param nameFormat see {@link Utils#newThreadFactory(String, int)}
076     * @param workers number of worker thread to keep
077     * @return new ThreadPoolExecutor that will use a @see HostLimitQueue based queue
078     */
079    public static ThreadPoolExecutor getNewThreadPoolExecutor(String nameFormat, int workers) {
080        return new ThreadPoolExecutor(
081                workers, // keep the thread number constant
082                workers, // do not this number of threads
083                30, // keepalive for thread
084                TimeUnit.SECONDS,
085                new HostLimitQueue(HOST_LIMIT.get().intValue()),
086                Utils.newThreadFactory(nameFormat, Thread.NORM_PRIORITY)
087                );
088    }
089
090    /**
091     * @param name name of threads
092     * @return new ThreadPoolExecutor that will use a @see HostLimitQueue based queue, with default number of threads
093     */
094    public static ThreadPoolExecutor getNewThreadPoolExecutor(String name) {
095        return getNewThreadPoolExecutor(name, THREAD_LIMIT.get().intValue());
096    }
097
098    @Override
099    public TileJob createTileLoaderJob(Tile tile) {
100        return new TMSCachedTileLoaderJob(listener, tile, cache,
101                connectTimeout, readTimeout, headers, getDownloadExecutor());
102    }
103
104    @Override
105    public void clearCache(TileSource source) {
106        this.cache.remove(source.getName() + ':');
107    }
108
109    /**
110     * @return cache statistics as string
111     */
112    public String getStats() {
113        return cache.getStats();
114    }
115
116    /**
117     * cancels all outstanding tasks in the queue. This rollbacks the state of the tiles in the queue
118     * to loading = false / loaded = false
119     */
120    @Override
121    public void cancelOutstandingTasks() {
122        for (Runnable r: downloadExecutor.getQueue()) {
123            if (downloadExecutor.remove(r) && r instanceof TMSCachedTileLoaderJob) {
124                ((TMSCachedTileLoaderJob) r).handleJobCancellation();
125            }
126        }
127    }
128
129    /**
130     * Sets the download executor that will be used to download tiles instead of default one.
131     * You can use {@link #getNewThreadPoolExecutor} to create a new download executor with separate
132     * queue from default.
133     *
134     * @param downloadExecutor download executor that will be used to download tiles
135     */
136    public void setDownloadExecutor(ThreadPoolExecutor downloadExecutor) {
137        this.downloadExecutor = downloadExecutor;
138    }
139
140    /**
141     * @return download executor that is used by this factory
142     */
143    public ThreadPoolExecutor getDownloadExecutor() {
144        return downloadExecutor;
145    }
146}