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