Ticket #11216: jcs_cache_v02.patch

File jcs_cache_v02.patch, 56.1 KB (added by wiktorn, 9 years ago)

patch generated by svn diff

  • build.xml

     
    220220            destdir="build" target="1.7" source="1.7" debug="on" includeantruntime="false" createMissingPackageInfoClass="false" encoding="iso-8859-1">
    221221            <!-- get rid of "internal proprietary API" warning -->
    222222            <compilerarg value="-XDignore.symbol.file"/>
     223                <exclude name="org/apache/commons/jcs/admin/**"/>
     224                <exclude name="org/apache/commons/jcs/auxiliary/disk/jdbc/**"/>
     225                <exclude name="org/apache/commons/jcs/auxiliary/remote/**"/>
     226                <exclude name="org/apache/commons/jcs/utils/servlet/**"/>
     227                <exclude name="org/apache/commons/logging/impl/AvalonLogger.java"/>
     228                <exclude name="org/apache/commons/logging/impl/Log4JLogger.java"/>
     229                <exclude name="org/apache/commons/logging/impl/LogKitLogger.java"/>
     230                <exclude name="org/apache/commons/logging/impl/ServletContextCleaner.java"/>
    223231        </javac>
    224232        <!-- JMapViewer/JOSM -->
    225233        <javac srcdir="${src.dir}" excludes="com/**,oauth/**,org/apache/commons/**,org/glassfish/**,org/openstreetmap/gui/jmapviewer/Demo.java"
     
    581589        </java>
    582590    </target>
    583591</project>
     592
  • src/org/apache/commons

  • src/org/openstreetmap/josm/Main.java

    Property changes on: src/org/apache/commons
    ___________________________________________________________________
    Modified: svn:externals
    ## -1 +1,3 ##
    -codec http://svn.apache.org/repos/asf/commons/proper/codec/trunk/src/main/java/org/apache/commons/codec
    +http://svn.apache.org/repos/asf/commons/proper/codec/trunk/src/main/java/org/apache/commons/codec codec
    +http://svn.apache.org/repos/asf/commons/proper/jcs/trunk/commons-jcs-core/src/main/java/org/apache/commons/jcs jcs
    +http://svn.apache.org/repos/asf/commons/proper/logging/trunk/src/main/java/org/apache/commons/logging logging
     
    6767import org.openstreetmap.josm.data.ProjectionBounds;
    6868import org.openstreetmap.josm.data.UndoRedoHandler;
    6969import org.openstreetmap.josm.data.ViewportData;
     70import org.openstreetmap.josm.data.cache.JCSCacheManager;
    7071import org.openstreetmap.josm.data.coor.CoordinateFormat;
    7172import org.openstreetmap.josm.data.coor.LatLon;
    7273import org.openstreetmap.josm.data.osm.DataSet;
     
    10881089     * @since 3378
    10891090     */
    10901091    public static boolean exitJosm(boolean exit, int exitCode) {
     1092        JCSCacheManager.shutdown();
    10911093        if (Main.saveUnsavedModifications()) {
    10921094            geometry.remember("gui.geometry");
    10931095            if (map != null) {
  • src/org/openstreetmap/josm/data/cache/CacheEntry.java

     
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.data.cache;
     3
     4import java.io.Serializable;
     5
     6/**
     7 * @author Wiktor Niesiobędzki
     8 *
     9 * Class that will hold JCS cache entries
     10 *
     11 */
     12public class CacheEntry implements Serializable {
     13    private static final long serialVersionUID = 1L; //version
     14    private byte[] content;
     15
     16    /**
     17     * @param content of the cache entry
     18     */
     19    public CacheEntry(byte[] content) {
     20        this.content = content;
     21    }
     22
     23    /**
     24     * @return cache entry content
     25     */
     26    public byte[] getContent() {
     27        return content;
     28    }
     29}
  • src/org/openstreetmap/josm/data/cache/CacheEntryAttributes.java

     
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.data.cache;
     3
     4import java.util.HashMap;
     5import java.util.Map;
     6
     7import org.apache.commons.jcs.engine.ElementAttributes;
     8
     9public class CacheEntryAttributes extends ElementAttributes {
     10    private static final long serialVersionUID = 1L; //version
     11    private Map<String, String> attrs = new HashMap<String, String>();
     12    private final static String NO_TILE_AT_ZOOM = "noTileAtZoom";
     13    private final static String ETAG = "Etag";
     14    private final static String LAST_MODIFICATION = "lastModification";
     15    private final static String EXPIRATION_TIME = "expirationTime";
     16    //private boolean noTileAtZoom = false;
     17    private String Etag = null;
     18    private long lastModification = 0;
     19    private long expirationTime = 0;
     20
     21
     22    public CacheEntryAttributes() {
     23        super();
     24        attrs.put(NO_TILE_AT_ZOOM, "false");
     25        attrs.put(ETAG, null);
     26        attrs.put(LAST_MODIFICATION, "0");
     27        attrs.put(EXPIRATION_TIME, "0");
     28    }
     29    public boolean isNoTileAtZoom() {
     30        return Boolean.toString(true).equals(attrs.get(NO_TILE_AT_ZOOM));
     31    }
     32    public void setNoTileAtZoom(boolean noTileAtZoom) {
     33        attrs.put(NO_TILE_AT_ZOOM, Boolean.toString(noTileAtZoom));
     34    }
     35    public String getEtag() {
     36        return attrs.get(ETAG);
     37    }
     38    public void setEtag(String etag) {
     39        attrs.put(ETAG, etag);
     40    }
     41
     42    private long getLongAttr(String key) {
     43        try {
     44            return Long.parseLong(attrs.get(key));
     45        } catch (NumberFormatException e) {
     46            attrs.put(key, "0");
     47            return 0;
     48        }
     49    }
     50
     51    public long getLastModification() {
     52        return getLongAttr(LAST_MODIFICATION);
     53    }
     54    public void setLastModification(long lastModification) {
     55        attrs.put(LAST_MODIFICATION, Long.toString(lastModification));
     56    }
     57    public long getExpirationTime() {
     58        return getLongAttr(EXPIRATION_TIME);
     59    }
     60    public void setExpirationTime(long expirationTime) {
     61        attrs.put(EXPIRATION_TIME, Long.toString(expirationTime));
     62    }
     63
     64}
  • src/org/openstreetmap/josm/data/cache/ICachedLoaderJob.java

     
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.data.cache;
     3
     4import java.net.URL;
     5
     6
     7/**
     8 *
     9 * @author Wiktor Niesiobędzki
     10 *
     11 * @param <K> cache key type
     12 * @param <V> value that is returned from cache
     13 */
     14public interface ICachedLoaderJob<K> {
     15    /**
     16     * returns cache entry key
     17     *
     18     * @param tile
     19     * @return cache key for tile
     20     */
     21    public K getCacheKey();
     22
     23    /**
     24     * method to get download URL for Job
     25     * @return URL that should be fetched
     26     *
     27     */
     28    public URL getUrl();
     29    /**
     30     * implements the main algorithm for fetching
     31     */
     32    public void run();
     33
     34    /**
     35     * fetches object from cache, or returns null when object is not found
     36     *
     37     * @return filled tile with data or null when no cache entry found
     38     */
     39    public byte[] get();
     40
     41    /**
     42     * Submit job for background fetch, and listener will be
     43     * fed with value object
     44     *
     45     * @param listener
     46     */
     47    public void submit(ICachedLoaderListener listener);
     48}
  • src/org/openstreetmap/josm/data/cache/ICachedLoaderListener.java

     
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.data.cache;
     3
     4public interface ICachedLoaderListener {
     5    /**
     6     * Will be called when K object was successfully downloaded
     7     *
     8     * @param object
     9     * @param success
     10     */
     11    public void loadingFinished(byte[] object, boolean success);
     12
     13}
  • src/org/openstreetmap/josm/data/cache/JCSCacheManager.java

     
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.data.cache;
     3
     4import java.io.File;
     5import java.io.IOException;
     6import java.util.Properties;
     7import java.util.logging.Level;
     8import java.util.logging.Logger;
     9
     10import org.apache.commons.jcs.access.CacheAccess;
     11import org.apache.commons.jcs.auxiliary.AuxiliaryCache;
     12import org.apache.commons.jcs.auxiliary.disk.block.BlockDiskCacheAttributes;
     13import org.apache.commons.jcs.engine.control.CompositeCache;
     14import org.apache.commons.jcs.engine.control.CompositeCacheManager;
     15import org.openstreetmap.josm.Main;
     16
     17
     18/**
     19 * @author Wiktor Niesiobędzki
     20 *
     21 * Wrapper class for JCS Cache. Sets some sane environment and returns instances of cache objects.
     22 * Static configuration for now assumes some small LRU cache in memory and larger LRU cache on disk
     23 *
     24 */
     25public class JCSCacheManager {
     26
     27    private static volatile CompositeCacheManager cacheManager = null;
     28    private static long maxObjectTTL        = Long.MAX_VALUE;
     29
     30    private static int maxObjectsInMemory  = 1000;
     31    private static int maxObjectsOnDisk    = 25000;
     32
     33    private static void initialize() throws IOException {
     34        File cacheDir = new File(Main.pref.getCacheDirectory(), "jcs");
     35
     36        if ((!cacheDir.exists() && !cacheDir.mkdirs()))
     37            throw new IOException("Cannot access cache directory");
     38
     39        // raising logging level gives ~500x performance gain
     40        // http://westsworld.dk/blog/2008/01/jcs-and-performance/
     41        Logger.getLogger("org.apache.commons.jcs").setLevel(Level.INFO);
     42
     43        CompositeCacheManager cm  = CompositeCacheManager.getUnconfiguredInstance();
     44        // this could be moved to external file
     45        Properties props = new Properties();
     46        props.setProperty("jcs.default", "DC");
     47        props.setProperty("jcs.default.cacheattributes",                            org.apache.commons.jcs.engine.CompositeCacheAttributes.class.getCanonicalName());
     48        props.setProperty("jcs.default.cacheattributes.MaxObjects",                 Long.toString(maxObjectsInMemory));
     49        props.setProperty("jcs.default.cacheattributes.UseMemoryShrinker",          "true");
     50        props.setProperty("jcs.default.cacheattributes.DiskUsagePatternName",       "UPDATE"); // store elements on disk on put
     51        props.setProperty("jcs.default.elementattributes",                          CacheEntryAttributes.class.getCanonicalName());
     52        props.setProperty("jcs.default.elementattributes.IsEternal",                "false");
     53        props.setProperty("jcs.default.elementattributes.MaxLife",                  Long.toString(maxObjectTTL));
     54        props.setProperty("jcs.default.elementattributes.IdleTime",                 Long.toString(maxObjectTTL));
     55        props.setProperty("jcs.default.elementattributes.IsSpool",                  "true");
     56        props.setProperty("jcs.auxiliary.DC",                                       org.apache.commons.jcs.auxiliary.disk.block.BlockDiskCacheFactory.class.getCanonicalName());
     57        props.setProperty("jcs.auxiliary.DC.attributes",                            org.apache.commons.jcs.auxiliary.disk.block.BlockDiskCacheAttributes.class.getCanonicalName());
     58        props.setProperty("jcs.auxiliary.DC.attributes.DiskPath",                   cacheDir.getAbsolutePath());
     59        props.setProperty("jcs.auxiliary.DC.attributes.maxKeySize",                 Long.toString(maxObjectsOnDisk));
     60        props.setProperty("jcs.auxiliary.DC.attributes.blockSizeBytes",             "1024");
     61        cm.configure(props);
     62        cacheManager = cm;
     63
     64    }
     65
     66    /**
     67     * Returns configured cache object for named cache region
     68     * @param cacheName region name
     69     * @return cache access object
     70     * @throws IOException if directory is not found
     71     */
     72    public static <K,V> CacheAccess<K, V> getCache(String cacheName) throws IOException {
     73        return getCache(cacheName, maxObjectsInMemory, maxObjectsOnDisk);
     74    }
     75
     76    /**
     77     * Returns configured cache object with defined limits of memory cache and disk cache
     78     * @param cacheName region name
     79     * @param maxMemoryObjects number of objects to keep in memory
     80     * @param maxDiskObjects number of objects to keep on disk
     81     * @return cache access object
     82     * @throws IOException if directory is not found
     83     */
     84    public static <K,V> CacheAccess<K, V> getCache(String cacheName, int maxMemoryObjects, int maxDiskObjects) throws IOException {
     85        if (cacheManager != null)
     86            return getCacheInner(cacheName, maxMemoryObjects, maxDiskObjects);
     87
     88        synchronized (JCSCacheManager.class) {
     89            if (cacheManager == null)
     90                initialize();
     91            return getCacheInner(cacheName, maxMemoryObjects, maxDiskObjects);
     92        }
     93    }
     94
     95    private static <K,V> CacheAccess<K, V> getCacheInner(String cacheName, int maxMemoryObjects, int maxDiskObjects) {
     96        CompositeCache<K, V> cc = cacheManager.getCache(cacheName);
     97        cc.getCacheAttributes().setMaxObjects(maxMemoryObjects);
     98        AuxiliaryCache<K, V> ac[] = cc.getAuxCaches();
     99        if (ac!=null && ac.length > 0) {
     100            if (ac[0].getAuxiliaryCacheAttributes() instanceof BlockDiskCacheAttributes) {
     101                ((BlockDiskCacheAttributes) ac[0].getAuxiliaryCacheAttributes()).setMaxKeySize(maxDiskObjects);
     102            }
     103        }
     104        return new CacheAccess<K, V>(cc);
     105    }
     106
     107    public static void shutdown() {
     108        // use volatile semantics to get consistent object
     109        CompositeCacheManager localCacheManager = cacheManager;
     110        if (localCacheManager != null) {
     111            localCacheManager.shutDown();
     112        }
     113
     114    }
     115
     116}
  • src/org/openstreetmap/josm/data/cache/JCSCachedTileLoaderJob.java

     
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.data.cache;
     3
     4import java.io.ByteArrayOutputStream;
     5import java.io.IOException;
     6import java.io.InputStream;
     7import java.net.HttpURLConnection;
     8import java.net.MalformedURLException;
     9import java.net.URL;
     10import java.net.URLConnection;
     11import java.util.HashSet;
     12import java.util.Map;
     13import java.util.Random;
     14import java.util.Set;
     15import java.util.concurrent.ConcurrentHashMap;
     16import java.util.concurrent.ConcurrentMap;
     17import java.util.logging.Level;
     18import java.util.logging.Logger;
     19
     20import org.apache.commons.jcs.access.behavior.ICacheAccess;
     21import org.apache.commons.jcs.engine.behavior.ICacheElement;
     22import org.openstreetmap.gui.jmapviewer.FeatureAdapter;
     23
     24/**
     25 * @author Wiktor Niesiobędzki
     26 *
     27 * @param <K> cache entry key type
     28 *
     29 * Generic loader for HTTP based tiles. Uses custom attribute, to check, if entry has expired
     30 * according to HTTP headers sent with tile. If so, it tries to verify using Etags
     31 * or If-Modified-Since / Last-Modified.
     32 *
     33 * If the tile is not valid, it will try to download it from remote service and put it
     34 * to cache. If remote server will fail it will try to use stale entry.
     35 *
     36 * This class will keep only one Job running for specified tile. All others will just finish, but
     37 * listeners will be gathered and notified, once download job will be finished
     38 */
     39public abstract class JCSCachedTileLoaderJob<K> implements ICachedLoaderJob<K> {
     40    private static final Logger log = FeatureAdapter.getLogger(JCSCachedTileLoaderJob.class.getCanonicalName());
     41    protected static final long DEFAULT_EXPIRE_TIME = 1000L * 60 * 60 * 24 * 7; // 7 days
     42    // Limit for the max-age value send by the server.
     43    protected static final long EXPIRE_TIME_SERVER_LIMIT = 1000L * 60 * 60 * 24 * 28; // 4 weeks
     44    // Absolute expire time limit. Cached tiles that are older will not be used,
     45    // even if the refresh from the server fails.
     46    protected static final long ABSOLUTE_EXPIRE_TIME_LIMIT = Long.MAX_VALUE; // unlimited
     47
     48    private ICacheAccess<K, CacheEntry> cache;
     49    private long now;
     50    private ICacheElement<K,CacheEntry> cacheElement;
     51    private int connectTimeout;
     52    private int readTimeout;
     53    private Map<String, String> headers;
     54    private static ConcurrentMap<URL,Set<ICachedLoaderListener>> inProgress = new ConcurrentHashMap<>();
     55
     56    protected CacheEntryAttributes attributes = null;
     57    protected byte[] data = null;
     58    private static ConcurrentMap<String, Boolean> useHead = new ConcurrentHashMap<>();
     59
     60
     61
     62    /**
     63     * @param cache cache instance that we will work on
     64     * @param headers
     65     * @param readTimeout
     66     * @param connectTimeout
     67     */
     68    public JCSCachedTileLoaderJob(ICacheAccess<K,CacheEntry> cache,
     69            int connectTimeout, int readTimeout,
     70            Map<String, String> headers) {
     71
     72        this.cache = cache;
     73        this.now = System.currentTimeMillis();
     74        this.connectTimeout = connectTimeout;
     75        this.readTimeout = readTimeout;
     76        this.headers = headers;
     77    }
     78
     79    private void ensureCacheElement() {
     80        if (cacheElement == null && getCacheKey() != null) {
     81            cacheElement = cache.getCacheElement(getCacheKey());
     82            if (cacheElement != null) {
     83                attributes = (CacheEntryAttributes) cacheElement.getElementAttributes();
     84                data = cacheElement.getVal().getContent();
     85            }
     86        }
     87    }
     88
     89    public byte[] get() {
     90        ensureCacheElement();
     91        if (cacheElement != null) {
     92            return cacheElement.getVal().getContent();
     93        }
     94        return null;
     95    }
     96
     97    @Override
     98    public void submit(ICachedLoaderListener listener) {
     99        boolean first = false;
     100        URL url = getUrl();
     101        if (url == null) {
     102            log.log(Level.WARNING, "No url returned for: {0}, skipping", getCacheKey());
     103            return;
     104        }
     105        synchronized (inProgress) {
     106            Set<ICachedLoaderListener> newListeners = inProgress.get(url);
     107            if (newListeners == null) {
     108                newListeners = new HashSet<>();
     109                inProgress.put(url, newListeners);
     110                first = true;
     111            }
     112            newListeners.add(listener);
     113        }
     114
     115        if (first) {
     116            ensureCacheElement();
     117            if (cacheElement != null && isCacheElementValid() && (isObjectLoadable())) {
     118                // we got something in cache, verify it
     119                log.log(Level.FINE, "JCS - Returning object from cache: {0}", getCacheKey());
     120                finishLoading(true, true);
     121                return;
     122            }
     123            // object not in cache, so submit work to separate thread
     124            JCSJobDispatcher.getInstance().addJob(this);
     125        }
     126
     127    }
     128
     129    /**
     130     *
     131     * @return checks if object from cache has sufficient data to be returned
     132     */
     133    public boolean isObjectLoadable() {
     134        return data != null && data.length > 0;
     135    }
     136
     137    /**
     138     *
     139     * @return cache object as empty, regardless of what remote resource has returned (ex. based on headers)
     140     */
     141    protected boolean cacheAsEmpty() {
     142        return false;
     143    }
     144
     145    public void run() {
     146        // try to load object from remote resource
     147        if (loadObject()) {
     148            finishLoading(true, true);
     149        } else {
     150            // if loading failed - check if we can return stale entry
     151            if (isObjectLoadable()) {
     152                // try to get stale entry in cache
     153                finishLoading(true, true);
     154                log.log(Level.FINE, "JCS - found stale object in cache: {0}", getUrl());
     155            } else {
     156                // failed completely
     157                finishLoading(false, true);
     158            }
     159        }
     160    }
     161
     162
     163    private void finishLoading(boolean success, boolean loaded) {
     164        Set<ICachedLoaderListener> listeners = null;
     165        synchronized (inProgress) {
     166            listeners = inProgress.remove(getUrl());
     167        }
     168        if (listeners == null) {
     169            log.log(Level.WARNING, "Listener not found for URL: {0}. Listener not notified!", getUrl());
     170            return;
     171        }
     172        try {
     173            for (ICachedLoaderListener l: listeners) {
     174                l.loadingFinished(data, success);
     175            }
     176        } catch (Exception e) {
     177            log.log(Level.WARNING, "JCS TMS - Error while loading image from tile cache: {0}; {1}", new Object[]{e.getMessage(), getUrl()});
     178            log.log(Level.FINE, "Stacktrace", e);
     179            for (ICachedLoaderListener l: listeners) {
     180                l.loadingFinished(data, false);
     181            }
     182
     183        }
     184
     185    }
     186
     187    private boolean isCacheElementValid() {
     188        long expires = attributes.getExpirationTime();
     189
     190        // check by expire date set by server
     191        if (expires != 0L) {
     192            // put a limit to the expire time (some servers send a value
     193            // that is too large)
     194            expires = Math.min(expires, attributes.getCreateTime() + EXPIRE_TIME_SERVER_LIMIT);
     195            if (now > expires) {
     196                log.log(Level.FINE, "TMS - Tile has expired ({0})-> not valid {1}", new Object[]{Long.toString(expires), getUrl()});
     197                return false;
     198            }
     199        } else {
     200            // check by file modification date
     201            if (now - attributes.getLastModification() > DEFAULT_EXPIRE_TIME) {
     202                log.log(Level.FINE, "TMS - Tile has expired, maximum file age reached {0}", getUrl());
     203                return false;
     204            }
     205        }
     206        return true;
     207    }
     208
     209    private boolean loadObject() {
     210        try {
     211            // if we have object in cache, and host doesn't support If-Modified-Since nor If-None-Match
     212            // then just use HEAD request and check returned values
     213            if (isObjectLoadable() && useHead.get(getUrl().getHost()) && isCacheValidUsingHead()) {
     214                log.log(Level.FINE, "JCS - cache entry verified using HEAD request: {0}", getUrl());
     215                return true;
     216            }
     217            URLConnection urlConn = getURLConnection();
     218
     219            if (isObjectLoadable()  &&
     220                    (now - attributes.getLastModification()) <= ABSOLUTE_EXPIRE_TIME_LIMIT) {
     221                urlConn.setIfModifiedSince(attributes.getLastModification());
     222            }
     223            if (isObjectLoadable() && attributes.getEtag() != null) {
     224                urlConn.addRequestProperty("If-None-Match", attributes.getEtag());
     225            }
     226            if (urlConn instanceof HttpURLConnection && ((HttpURLConnection)urlConn).getResponseCode() == 304) {
     227                // If isModifiedSince or If-None-Match has been set
     228                // and the server answers with a HTTP 304 = "Not Modified"
     229                log.log(Level.FINE, "JCS - IfModifiedSince/Etag test: local version is up to date: {0}", getUrl());
     230                return true;
     231            } else if (isObjectLoadable()) {
     232                // we have an object in cache, but we haven't received 304 resposne code
     233                // check if we should use HEAD request to verify
     234                if((attributes.getEtag() != null && attributes.getEtag().equals(urlConn.getRequestProperty("ETag"))) ||
     235                        attributes.getLastModification() == urlConn.getLastModified()) {
     236                    // we sent ETag or If-Modified-Since, but didn't get 304 response code
     237                    // for further requests - use HEAD
     238                    useHead.put(getUrl().getHost(), Boolean.TRUE);
     239                }
     240            }
     241
     242            attributes = parseHeaders(urlConn);
     243
     244            for (int i = 0; i < 5; ++i) {
     245                if (urlConn instanceof HttpURLConnection && ((HttpURLConnection)urlConn).getResponseCode() == 503) {
     246                    Thread.sleep(5000+(new Random()).nextInt(5000));
     247                    continue;
     248                }
     249                data = read(urlConn);
     250                if (!cacheAsEmpty() && data != null && data.length > 0) {
     251                    cache.put(getCacheKey(), new CacheEntry(data), attributes);
     252                    log.log(Level.FINE, "JCS - downloaded key: {0}, length: {1}, url: {2}",
     253                            new Object[] {getCacheKey(), data.length, getUrl()});
     254                    return true;
     255                } else {
     256                    log.log(Level.FINE, "JCS - Caching empty object {0}", getUrl());
     257                    cache.put(getCacheKey(), new CacheEntry(new byte[]{}), attributes);
     258                    return true;
     259                }
     260            }
     261        } catch (Exception e) {
     262            log.log(Level.WARNING, "JCS - Exception during download {0}: {1}", new Object[]{getUrl(), e.getMessage() == null ? e.toString() : e.getMessage() });
     263        }
     264        log.log(Level.WARNING, "JCS - Silent failure during download: {0}", getUrl());
     265        return false;
     266
     267    }
     268
     269    private CacheEntryAttributes parseHeaders(URLConnection urlConn) {
     270        CacheEntryAttributes ret = new CacheEntryAttributes();
     271        ret.setNoTileAtZoom("no-tile".equals(urlConn.getHeaderField("X-VE-Tile-Info")));
     272
     273        Long lng = urlConn.getExpiration();
     274        if (lng.equals(0L)) {
     275            try {
     276                String str = urlConn.getHeaderField("Cache-Control");
     277                if (str != null) {
     278                    for (String token: str.split(",")) {
     279                        if (token.startsWith("max-age=")) {
     280                            lng = Long.parseLong(token.substring(8)) * 1000 +
     281                                    System.currentTimeMillis();
     282                        }
     283                    }
     284                }
     285            } catch (NumberFormatException e) {} //ignore malformed Cache-Control headers
     286        }
     287
     288        ret.setExpirationTime(lng);
     289        ret.setLastModification(now);
     290        ret.setEtag(urlConn.getHeaderField("ETag"));
     291        return ret;
     292    }
     293
     294    private HttpURLConnection getURLConnection() throws IOException, MalformedURLException {
     295        HttpURLConnection urlConn = (HttpURLConnection) getUrl().openConnection();
     296        urlConn.setRequestProperty("Accept", "text/html, image/png, image/jpeg, image/gif, */*");
     297        urlConn.setReadTimeout(readTimeout); // 30 seconds read timeout
     298        urlConn.setConnectTimeout(connectTimeout);
     299        for(Map.Entry<String, String> e: headers.entrySet()) {
     300            urlConn.setRequestProperty(e.getKey(), e.getValue());
     301        }
     302        return urlConn;
     303    }
     304
     305    private boolean isCacheValidUsingHead() throws IOException {
     306        HttpURLConnection urlConn = (HttpURLConnection) getUrl().openConnection();
     307        urlConn.setRequestMethod("HEAD");
     308        long lastModified = urlConn.getLastModified();
     309        return (
     310                (attributes.getEtag() != null && attributes.getEtag().equals(urlConn.getRequestProperty("ETag"))) ||
     311                (lastModified != 0 && lastModified <= attributes.getLastModification())
     312                );
     313    }
     314
     315    private static byte[] read(URLConnection urlConn) throws IOException {
     316        InputStream input = urlConn.getInputStream();
     317        try {
     318            ByteArrayOutputStream bout = new ByteArrayOutputStream(input.available());
     319            byte[] buffer = new byte[2048];
     320            boolean finished = false;
     321            do {
     322                int read = input.read(buffer);
     323                if (read >= 0) {
     324                    bout.write(buffer, 0, read);
     325                } else {
     326                    finished = true;
     327                }
     328            } while (!finished);
     329            if (bout.size() == 0)
     330                return null;
     331            return bout.toByteArray();
     332        } finally {
     333            input.close();
     334        }
     335    }
     336}
  • src/org/openstreetmap/josm/data/cache/JCSJobDispatcher.java

     
     1// License: GPL. For details, see Readme.txt file.
     2package org.openstreetmap.josm.data.cache;
     3
     4import java.util.concurrent.BlockingDeque;
     5import java.util.concurrent.LinkedBlockingDeque;
     6import java.util.concurrent.TimeUnit;
     7
     8import org.openstreetmap.gui.jmapviewer.interfaces.TileJob;
     9
     10/**
     11 * A generic class that processes a list of {@link Runnable} one-by-one using
     12 * one or more {@link Thread}-instances. The number of instances varies between
     13 * 1 and {@link #workerThreadMaxCount} (default: 8). If an instance is idle
     14 * more than {@link #workerThreadTimeout} seconds (default: 30), the instance
     15 * ends itself.
     16 *
     17 * @author Jan Peter Stotz
     18 */
     19public class JCSJobDispatcher {
     20
     21    private static final JCSJobDispatcher instance = new JCSJobDispatcher();
     22
     23    /**
     24     * @return the singelton instance of the {@link JCSJobDispatcher}
     25     */
     26    public static JCSJobDispatcher getInstance() {
     27        return instance;
     28    }
     29
     30    private JCSJobDispatcher() {
     31        addWorkerThread().firstThread = true;
     32    }
     33
     34    protected BlockingDeque<ICachedLoaderJob> jobQueue = new LinkedBlockingDeque<>();
     35
     36    protected static int workerThreadMaxCount = 8;
     37
     38    /**
     39     * Specifies the time span in seconds that a worker thread waits for new
     40     * jobs to perform. If the time span has elapsed the worker thread
     41     * terminates itself. Only the first worker thread works differently, it
     42     * ignores the timeout and will never terminate itself.
     43     */
     44    protected static int workerThreadTimeout = 30;
     45
     46    /**
     47     * Type of queue, FIFO if <code>false</code>, LIFO if <code>true</code>
     48     */
     49    protected boolean modeLIFO = false;
     50
     51    /**
     52     * Total number of worker threads currently idle or active
     53     */
     54    protected int workerThreadCount = 0;
     55
     56    /**
     57     * Number of worker threads currently idle
     58     */
     59    protected int workerThreadIdleCount = 0;
     60
     61    /**
     62     * Just an id for identifying an worker thread instance
     63     */
     64    protected int workerThreadId = 0;
     65
     66    /**
     67     * Removes all jobs from the queue that are currently not being processed.
     68     */
     69    public void cancelOutstandingJobs() {
     70        jobQueue.clear();
     71    }
     72
     73    /**
     74     * Function to set the maximum number of workers for tile loading.
     75     */
     76    static public void setMaxWorkers(int workers) {
     77        workerThreadMaxCount = workers;
     78    }
     79
     80    /**
     81     * Function to set the LIFO/FIFO mode for tile loading job.
     82     *
     83     * @param lifo <code>true</code> for LIFO mode, <code>false</code> for FIFO mode
     84     */
     85    public void setLIFO(boolean lifo) {
     86        modeLIFO = lifo;
     87    }
     88
     89    /**
     90     * Adds a job to the queue.
     91     * Jobs for tiles already contained in the are ignored (using a <code>null</code> tile
     92     * prevents skipping).
     93     *
     94     * @param job the the job to be added
     95     */
     96    public void addJob(ICachedLoaderJob job) {
     97        try {
     98            if(job.getCacheKey() != null) {
     99                for(ICachedLoaderJob oldJob : jobQueue) {
     100                    if(oldJob.getCacheKey() == job.getCacheKey()) {
     101                        return;
     102                    }
     103                }
     104            }
     105            jobQueue.put(job);
     106            if (workerThreadIdleCount == 0 && workerThreadCount < workerThreadMaxCount)
     107                addWorkerThread();
     108        } catch (InterruptedException e) {
     109        }
     110    }
     111
     112    protected JobThread addWorkerThread() {
     113        JobThread jobThread = new JobThread(++workerThreadId);
     114        synchronized (this) {
     115            workerThreadCount++;
     116        }
     117        jobThread.start();
     118        return jobThread;
     119    }
     120
     121    public class JobThread extends Thread {
     122
     123        ICachedLoaderJob job;
     124        boolean firstThread = false;
     125
     126        public JobThread(int threadId) {
     127            super("OSMJobThread " + threadId);
     128            setDaemon(true);
     129            job = null;
     130        }
     131
     132        @Override
     133        public void run() {
     134            executeJobs();
     135            synchronized (instance) {
     136                workerThreadCount--;
     137            }
     138        }
     139
     140        protected void executeJobs() {
     141            while (!isInterrupted()) {
     142                try {
     143                    synchronized (instance) {
     144                        workerThreadIdleCount++;
     145                    }
     146                    if(modeLIFO) {
     147                        if (firstThread)
     148                            job = jobQueue.takeLast();
     149                        else
     150                            job = jobQueue.pollLast(workerThreadTimeout, TimeUnit.SECONDS);
     151                    } else {
     152                        if (firstThread)
     153                            job = jobQueue.take();
     154                        else
     155                            job = jobQueue.poll(workerThreadTimeout, TimeUnit.SECONDS);
     156                    }
     157                } catch (InterruptedException e1) {
     158                    return;
     159                } finally {
     160                    synchronized (instance) {
     161                        workerThreadIdleCount--;
     162                    }
     163                }
     164                if (job == null)
     165                    return;
     166                try {
     167                    job.run();
     168                    job = null;
     169                } catch (Exception e) {
     170                    e.printStackTrace();
     171                }
     172            }
     173        }
     174    }
     175
     176}
  • src/org/openstreetmap/josm/data/imagery/TMSCachedTileJob.java

     
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.data.imagery;
     3
     4import java.io.ByteArrayInputStream;
     5import java.io.IOException;
     6import java.net.URL;
     7import java.util.Map;
     8import java.util.logging.Level;
     9import java.util.logging.Logger;
     10
     11import org.apache.commons.jcs.access.behavior.ICacheAccess;
     12import org.openstreetmap.gui.jmapviewer.FeatureAdapter;
     13import org.openstreetmap.gui.jmapviewer.Tile;
     14import org.openstreetmap.gui.jmapviewer.interfaces.TileJob;
     15import org.openstreetmap.gui.jmapviewer.interfaces.TileLoaderListener;
     16import org.openstreetmap.josm.data.cache.CacheEntry;
     17import org.openstreetmap.josm.data.cache.ICachedLoaderListener;
     18import org.openstreetmap.josm.data.cache.JCSCachedTileLoaderJob;
     19
     20/**
     21 * @author Wiktor Niesiobędzki
     22 *
     23 * Class bridging TMS requests to JCS cache requests
     24 *
     25 */
     26public class TMSCachedTileJob extends JCSCachedTileLoaderJob<String> implements TileJob, ICachedLoaderListener  {
     27    private static final Logger log = FeatureAdapter.getLogger(TMSCachedTileJob.class.getCanonicalName());
     28    private Tile tile;
     29    private TileLoaderListener listener;
     30    private URL url;
     31
     32    /**
     33     * Constructor for creating a job, to get a specific tile from cache
     34     * @param listener
     35     * @param tile to be fetched from cache
     36     * @param cache object
     37     * @param connectTimeout when connecting to remote resource
     38     * @param readTimeout when connecting to remote resource
     39     * @param headers to be sent together with request
     40     */
     41    public TMSCachedTileJob(TileLoaderListener listener, Tile tile, ICacheAccess<String, CacheEntry> cache, int connectTimeout, int readTimeout,
     42            Map<String, String> headers) {
     43        super(cache, connectTimeout, readTimeout, headers);
     44        this.tile = tile;
     45        this.listener = listener;
     46        // URLs tend to change for some tile providers. Make a static reference here, so the tile URL might be used as a key
     47        // for request deduplication
     48        try {
     49            this.url = new URL(tile.getUrl());
     50        } catch (IOException e) {
     51            log.log(Level.WARNING, "JCS TMS Cache - error creating URL for tile {0}: {1}", new Object[] {tile.getKey(), e.getMessage()});
     52        }
     53
     54    }
     55
     56    @Override
     57    public Tile getTile() {
     58        return tile;
     59    }
     60
     61    @Override
     62    public String getCacheKey() {
     63        if (tile != null)
     64            return tile.getKey();
     65        return null;
     66    }
     67
     68    @Override
     69    public URL getUrl() {
     70        return url;
     71    }
     72
     73    @Override
     74    public boolean isObjectLoadable() {
     75        return (data != null && data.length > 0) || cacheAsEmpty();
     76    }
     77
     78    @Override
     79    protected boolean cacheAsEmpty() {
     80        if (attributes != null && attributes.isNoTileAtZoom()) {
     81            // do not remove file - keep the information, that there is no tile, for further requests
     82            // the code above will check, if this information is still valid
     83            log.log(Level.FINE, "JCS TMS - Tile valid, but no file, as no tiles at this level {0}", tile);
     84            tile.setError("No tile at this zoom level");
     85            tile.putValue("tile-info", "no-tile");
     86            return true;
     87        }
     88        return false;
     89    }
     90
     91    public void submit() {
     92        super.submit(this);
     93    }
     94
     95    @Override
     96    public void loadingFinished(byte[] object, boolean success) {
     97        try {
     98            if (object != null && object.length > 0) {
     99                tile.loadImage(new ByteArrayInputStream(object));
     100            }
     101            tile.setLoaded(true);
     102            if (listener != null) {
     103                listener.tileLoadingFinished(tile, success);
     104            }
     105        } catch (IOException e) {
     106            log.log(Level.WARNING, "JCS TMS - error loading object for tile {0}: {1}", new Object[] {tile.getKey(), e.getMessage()});
     107            tile.setError(e.getMessage());
     108            tile.setLoaded(false);
     109            if (listener != null) {
     110                listener.tileLoadingFinished(tile, false);
     111            }
     112        }
     113    }
     114
     115    /**
     116     * Method for getting the tile from cache only, without trying to reach remote resource
     117     * @return tile or null, if nothing (useful) was found in cache
     118     */
     119    public Tile getCachedTile() {
     120        byte[] data = super.get();
     121        if (isObjectLoadable()) {
     122            loadingFinished(data, true);
     123            return tile;
     124        } else {
     125            return null;
     126        }
     127    }
     128}
     129 No newline at end of file
  • src/org/openstreetmap/josm/data/imagery/TMSCachedTileLoader.java

     
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.data.imagery;
     3
     4import java.io.IOException;
     5import java.util.Map;
     6
     7import org.apache.commons.jcs.access.behavior.ICacheAccess;
     8import org.openstreetmap.gui.jmapviewer.Tile;
     9import org.openstreetmap.gui.jmapviewer.interfaces.CachedTileLoader;
     10import org.openstreetmap.gui.jmapviewer.interfaces.TileJob;
     11import org.openstreetmap.gui.jmapviewer.interfaces.TileLoader;
     12import org.openstreetmap.gui.jmapviewer.interfaces.TileLoaderListener;
     13import org.openstreetmap.josm.data.cache.CacheEntry;
     14import org.openstreetmap.josm.data.cache.JCSCacheManager;
     15import org.openstreetmap.josm.data.preferences.IntegerProperty;
     16
     17/**
     18 * @author Wiktor Niesiobędzki
     19 *
     20 * Wrapper class that bridges between JCS cache and Tile Loaders
     21 *
     22 */
     23public class TMSCachedTileLoader implements TileLoader, CachedTileLoader {
     24
     25    private ICacheAccess<String, CacheEntry> cache;
     26    private int connectTimeout;
     27    private int readTimeout;
     28    private Map<String, String> headers;
     29    private TileLoaderListener listener;
     30    public static final String PREFERENCE_PREFIX   = "imagery.tms.cache.";
     31    // average tile size is about 20kb
     32    public static IntegerProperty MAX_OBJECTS_IN_MEMORY = new IntegerProperty(PREFERENCE_PREFIX + "max_objects_memory", 1000); // 1000 is around 20MB under this assumptions
     33    public static IntegerProperty MAX_OBJECTS_ON_DISK = new IntegerProperty(PREFERENCE_PREFIX + "max_objects_disk", 25000); // 25000 is around 500MB under this assumptions
     34
     35    /**
     36     * Constructor
     37     * @param name of the cache
     38     * @param connectTimeout to remote resource
     39     * @param readTimeout to remote resource
     40     * @param headers to be sent along with request
     41     * @throws IOException when cache initialization fails
     42     */
     43    public TMSCachedTileLoader(TileLoaderListener listener, String name, int connectTimeout, int readTimeout, Map<String, String> headers) throws IOException {
     44        this.cache = JCSCacheManager.getCache(name, MAX_OBJECTS_IN_MEMORY.get(), MAX_OBJECTS_ON_DISK.get());
     45        this.connectTimeout = connectTimeout;
     46        this.readTimeout = readTimeout;
     47        this.headers = headers;
     48        this.listener = listener;
     49    }
     50
     51    @Override
     52    public TileJob createTileLoaderJob(Tile tile) {
     53        return new TMSCachedTileJob(listener, tile, cache, connectTimeout, readTimeout, headers);
     54    }
     55
     56    @Override
     57    public void clearCache() {
     58        this.cache.clear();
     59    }
     60}
  • src/org/openstreetmap/josm/gui/bbox/SlippyMapBBoxChooser.java

     
    2525import org.openstreetmap.gui.jmapviewer.MemoryTileCache;
    2626import org.openstreetmap.gui.jmapviewer.OsmTileLoader;
    2727import org.openstreetmap.gui.jmapviewer.interfaces.MapMarker;
     28import org.openstreetmap.gui.jmapviewer.interfaces.TileLoader;
    2829import org.openstreetmap.gui.jmapviewer.interfaces.TileSource;
    2930import org.openstreetmap.gui.jmapviewer.tilesources.MapQuestOpenAerialTileSource;
    3031import org.openstreetmap.gui.jmapviewer.tilesources.MapQuestOsmTileSource;
     
    113114    private static final StringProperty PROP_MAPSTYLE = new StringProperty("slippy_map_chooser.mapstyle", "Mapnik");
    114115    public static final String RESIZE_PROP = SlippyMapBBoxChooser.class.getName() + ".resize";
    115116
    116     private OsmTileLoader cachedLoader;
     117    private TileLoader cachedLoader;
    117118    private OsmTileLoader uncachedLoader;
    118119
    119120    private final SizeButton iSizeButton;
  • src/org/openstreetmap/josm/gui/layer/TMSLayer.java

     
    2121import java.net.URL;
    2222import java.util.ArrayList;
    2323import java.util.Collections;
    24 import java.util.HashSet;
     24import java.util.HashMap;
    2525import java.util.LinkedList;
    2626import java.util.List;
    2727import java.util.Map;
    28 import java.util.Map.Entry;
    2928import java.util.Scanner;
    30 import java.util.Set;
    3129import java.util.concurrent.Callable;
    3230import java.util.regex.Matcher;
    3331import java.util.regex.Pattern;
     
    4341import org.openstreetmap.gui.jmapviewer.Coordinate;
    4442import org.openstreetmap.gui.jmapviewer.JobDispatcher;
    4543import org.openstreetmap.gui.jmapviewer.MemoryTileCache;
    46 import org.openstreetmap.gui.jmapviewer.OsmFileCacheTileLoader;
    4744import org.openstreetmap.gui.jmapviewer.OsmTileLoader;
    48 import org.openstreetmap.gui.jmapviewer.TMSFileCacheTileLoader;
    4945import org.openstreetmap.gui.jmapviewer.Tile;
    5046import org.openstreetmap.gui.jmapviewer.interfaces.CachedTileLoader;
    51 import org.openstreetmap.gui.jmapviewer.interfaces.TileClearController;
     47import org.openstreetmap.gui.jmapviewer.interfaces.TileLoader;
    5248import org.openstreetmap.gui.jmapviewer.interfaces.TileLoaderListener;
    5349import org.openstreetmap.gui.jmapviewer.interfaces.TileSource;
    5450import org.openstreetmap.gui.jmapviewer.tilesources.BingAerialTileSource;
     
    6359import org.openstreetmap.josm.data.coor.LatLon;
    6460import org.openstreetmap.josm.data.imagery.ImageryInfo;
    6561import org.openstreetmap.josm.data.imagery.ImageryInfo.ImageryType;
     62import org.openstreetmap.josm.data.imagery.TMSCachedTileLoader;
    6663import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor;
    6764import org.openstreetmap.josm.data.preferences.BooleanProperty;
    6865import org.openstreetmap.josm.data.preferences.IntegerProperty;
     
    7572import org.openstreetmap.josm.gui.dialogs.LayerListDialog;
    7673import org.openstreetmap.josm.gui.dialogs.LayerListPopup;
    7774import org.openstreetmap.josm.gui.progress.ProgressMonitor;
    78 import org.openstreetmap.josm.gui.progress.ProgressMonitor.CancelListener;
    7975import org.openstreetmap.josm.io.CacheCustomContent;
    8076import org.openstreetmap.josm.io.OsmTransferException;
    8177import org.openstreetmap.josm.io.UTFInputStreamReader;
     
    122118    }
    123119
    124120    public interface TileLoaderFactory {
    125         OsmTileLoader makeTileLoader(TileLoaderListener listener);
     121        TileLoader makeTileLoader(TileLoaderListener listener);
     122        TileLoader makeTileLoader(TileLoaderListener listener, Map<String, String> headers);
    126123    }
    127124
     125    // MemoryTileCache caches rendered tiles, to reduce latency during map panning
     126    // ImageIO.read() takes a lot of time, so we can't use JCS cache
    128127    protected MemoryTileCache tileCache;
    129128    protected TileSource tileSource;
    130     protected OsmTileLoader tileLoader;
     129    protected TileLoader tileLoader;
    131130
     131
    132132    public static TileLoaderFactory loaderFactory = new TileLoaderFactory() {
    133133        @Override
    134         public OsmTileLoader makeTileLoader(TileLoaderListener listener) {
    135             String cachePath = TMSLayer.PROP_TILECACHE_DIR.get();
    136             if (cachePath != null && !cachePath.isEmpty()) {
    137                 try {
    138                     OsmFileCacheTileLoader loader;
    139                     loader = new TMSFileCacheTileLoader(listener, new File(cachePath));
    140                     loader.headers.put("User-Agent", Version.getInstance().getFullAgentString());
    141                     return loader;
    142                 } catch (IOException e) {
    143                     Main.warn(e);
    144                 }
     134        public TileLoader makeTileLoader(TileLoaderListener listener, Map<String, String> inputHeaders) {
     135            Map<String, String> headers = new HashMap<>();
     136            headers.put("User-Agent", Version.getInstance().getFullAgentString());
     137            if (inputHeaders != null)
     138                headers.putAll(inputHeaders);
     139
     140            try {
     141                return new TMSCachedTileLoader(listener, "TMS",
     142                        Main.pref.getInteger("socket.timeout.connect",15) * 1000,
     143                        Main.pref.getInteger("socket.timeout.read", 30) * 1000,
     144                        headers);
     145            } catch (IOException e) {
     146                Main.warn(e);
    145147            }
    146148            return null;
    147149        }
     150
     151        @Override
     152        public TileLoader makeTileLoader(TileLoaderListener listener) {
     153            return makeTileLoader(listener, null);
     154        }
    148155    };
    149156
    150157    /**
    151158     * Plugins that wish to set custom tile loader should call this method
    152159     */
     160
    153161    public static void setCustomTileLoaderFactory(TileLoaderFactory loaderFactory) {
    154162        TMSLayer.loaderFactory = loaderFactory;
    155163    }
    156164
    157     private Set<Tile> tileRequestsOutstanding = new HashSet<>();
    158 
    159165    @Override
    160166    public synchronized void tileLoadingFinished(Tile tile, boolean success) {
    161167        if (tile.hasError()) {
     
    170176        if (Main.map != null) {
    171177            Main.map.repaint(100);
    172178        }
    173         tileRequestsOutstanding.remove(tile);
    174179        if (Main.isDebugEnabled()) {
    175180            Main.debug("tileLoadingFinished() tile: " + tile + " success: " + success);
    176181        }
    177182    }
    178183
    179     private static class TmsTileClearController implements TileClearController, CancelListener {
    180 
    181         private final ProgressMonitor monitor;
    182         private boolean cancel = false;
    183 
    184         public TmsTileClearController(ProgressMonitor monitor) {
    185             this.monitor = monitor;
    186             this.monitor.addCancelListener(this);
    187         }
    188 
    189         @Override
    190         public void initClearDir(File dir) {
    191         }
    192 
    193         @Override
    194         public void initClearFiles(File[] files) {
    195             monitor.setTicksCount(files.length);
    196             monitor.setTicks(0);
    197         }
    198 
    199         @Override
    200         public boolean cancel() {
    201             return cancel;
    202         }
    203 
    204         @Override
    205         public void fileDeleted(File file) {
    206             monitor.setTicks(monitor.getTicks()+1);
    207         }
    208 
    209         @Override
    210         public void clearFinished() {
    211             monitor.finishTask();
    212         }
    213 
    214         @Override
    215         public void operationCanceled() {
    216             cancel = true;
    217         }
    218     }
    219 
    220184    /**
    221185     * Clears the tile cache.
    222186     *
     
    231195    void clearTileCache(ProgressMonitor monitor) {
    232196        tileCache.clear();
    233197        if (tileLoader instanceof CachedTileLoader) {
    234             ((CachedTileLoader)tileLoader).clearCache(tileSource, new TmsTileClearController(monitor));
     198            ((CachedTileLoader)tileLoader).clearCache();
    235199        }
    236200    }
    237201
     
    412376    private void initTileSource(TileSource tileSource) {
    413377        this.tileSource = tileSource;
    414378        attribution.initialize(tileSource);
    415 
    416379        currentZoomLevel = getBestZoom();
    417380
     381        Map<String, String> headers = null;
     382        if (tileSource instanceof TemplatedTMSTileSource) {
     383            headers = (((TemplatedTMSTileSource)tileSource).getHeaders());
     384        }
     385
    418386        tileCache = new MemoryTileCache();
    419 
    420         tileLoader = loaderFactory.makeTileLoader(this);
    421         if (tileLoader == null) {
     387        tileLoader = loaderFactory.makeTileLoader(this, headers);
     388        if (tileLoader == null)
    422389            tileLoader = new OsmTileLoader(this);
    423         }
    424         tileLoader.timeoutConnect = Main.pref.getInteger("socket.timeout.connect",15) * 1000;
    425         tileLoader.timeoutRead = Main.pref.getInteger("socket.timeout.read", 30) * 1000;
    426         if (tileSource instanceof TemplatedTMSTileSource) {
    427             for(Entry<String, String> e : ((TemplatedTMSTileSource)tileSource).getHeaders().entrySet()) {
    428                 tileLoader.headers.put(e.getKey(), e.getValue());
    429             }
    430         }
    431         tileLoader.headers.put("User-Agent", Version.getInstance().getFullAgentString());
    432390    }
    433391
    434392    @Override
     
    487445        setMaxWorkers();
    488446        if(!isProjectionSupported(Main.getProjection())) {
    489447            JOptionPane.showMessageDialog(Main.parent,
    490                 tr("TMS layers do not support the projection {0}.\n{1}\n"
    491                 + "Change the projection or remove the layer.",
    492                 Main.getProjection().toCode(), nameSupportedProjections()),
    493                 tr("Warning"),
    494                 JOptionPane.WARNING_MESSAGE);
     448                    tr("TMS layers do not support the projection {0}.\n{1}\n"
     449                            + "Change the projection or remove the layer.",
     450                            Main.getProjection().toCode(), nameSupportedProjections()),
     451                            tr("Warning"),
     452                            JOptionPane.WARNING_MESSAGE);
    495453        }
    496454
    497455        setBackgroundLayer(true);
     
    629587                new PleaseWaitRunnable(tr("Flush Tile Cache")) {
    630588                    @Override
    631589                    protected void realRun() throws SAXException, IOException,
    632                             OsmTransferException {
     590                    OsmTransferException {
    633591                        clearTileCache(getProgressMonitor());
    634592                    }
    635593
     
    685643        }
    686644        needRedraw = true;
    687645        JobDispatcher.getInstance().cancelOutstandingJobs();
    688         tileRequestsOutstanding.clear();
    689646    }
    690647
    691648    int getMaxZoomLvl() {
     
    770727     * are temporary only and intentionally not inserted
    771728     * into the tileCache.
    772729     */
    773     synchronized Tile tempCornerTile(Tile t) {
     730    Tile tempCornerTile(Tile t) {
    774731        int x = t.getXtile() + 1;
    775732        int y = t.getYtile() + 1;
    776733        int zoom = t.getZoom();
     
    780737        return new Tile(tileSource, x, y, zoom);
    781738    }
    782739
    783     synchronized Tile getOrCreateTile(int x, int y, int zoom) {
     740    Tile getOrCreateTile(int x, int y, int zoom) {
    784741        Tile tile = getTile(x, y, zoom);
    785742        if (tile == null) {
    786743            tile = new Tile(tileSource, x, y, zoom);
     
    794751     * This can and will return null for tiles that are not
    795752     * already in the cache.
    796753     */
    797     synchronized Tile getTile(int x, int y, int zoom) {
     754    Tile getTile(int x, int y, int zoom) {
    798755        int max = (1 << zoom);
    799756        if (x < 0 || x >= max || y < 0 || y >= max)
    800757            return null;
     
    801758        return tileCache.getTile(tileSource, x, y, zoom);
    802759    }
    803760
    804     synchronized boolean loadTile(Tile tile, boolean force) {
     761    boolean loadTile(Tile tile, boolean force) {
    805762        if (tile == null)
    806763            return false;
    807         if (!force && (tile.hasError() || tile.isLoaded()))
     764        if (!force && (tile.isLoaded() || tile.hasError()))
    808765            return false;
    809766        if (tile.isLoading())
    810767            return false;
    811         if (tileRequestsOutstanding.contains(tile))
    812             return false;
    813         tileRequestsOutstanding.add(tile);
    814         JobDispatcher.getInstance().addJob(tileLoader.createTileLoaderJob(tile));
     768        tileLoader.createTileLoaderJob(tile).submit();
    815769        return true;
    816770    }
    817771
     
    12681222        public TileSet getTileSet(int zoom) {
    12691223            if (zoom < minZoom)
    12701224                return nullTileSet;
    1271             TileSet ts = tileSets[zoom-minZoom];
    1272             if (ts == null) {
    1273                 ts = new TileSet(topLeft, botRight, zoom);
    1274                 tileSets[zoom-minZoom] = ts;
     1225            synchronized (tileSets) {
     1226                TileSet ts = tileSets[zoom-minZoom];
     1227                if (ts == null) {
     1228                    ts = new TileSet(topLeft, botRight, zoom);
     1229                    tileSets[zoom-minZoom] = ts;
     1230                }
     1231                return ts;
    12751232            }
    1276             return ts;
    12771233        }
     1234
    12781235        public TileSetInfo getTileSetInfo(int zoom) {
    12791236            if (zoom < minZoom)
    12801237                return new TileSetInfo();
    1281             TileSetInfo tsi = tileSetInfos[zoom-minZoom];
    1282             if (tsi == null) {
    1283                 tsi = TMSLayer.getTileSetInfo(getTileSet(zoom));
    1284                 tileSetInfos[zoom-minZoom] = tsi;
     1238            synchronized (tileSetInfos) {
     1239                TileSetInfo tsi = tileSetInfos[zoom-minZoom];
     1240                if (tsi == null) {
     1241                    tsi = TMSLayer.getTileSetInfo(getTileSet(zoom));
     1242                    tileSetInfos[zoom-minZoom] = tsi;
     1243                }
     1244                return tsi;
    12851245            }
    1286             return tsi;
    12871246        }
    12881247    }
    12891248
    12901249    @Override
    12911250    public void paint(Graphics2D g, MapView mv, Bounds bounds) {
    1292         //long start = System.currentTimeMillis();
    12931251        EastNorth topLeft = mv.getEastNorth(0, 0);
    12941252        EastNorth botRight = mv.getEastNorth(mv.getWidth(), mv.getHeight());
    12951253