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